2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.somfytahoma.internal.handler;
15 import static org.openhab.binding.somfytahoma.internal.SomfyTahomaBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.util.HashMap;
19 import java.util.List;
21 import java.util.Objects;
23 import javax.measure.Unit;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaDevice;
28 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaState;
29 import org.openhab.binding.somfytahoma.internal.model.SomfyTahomaStatus;
30 import org.openhab.core.library.CoreItemFactory;
31 import org.openhab.core.library.types.DecimalType;
32 import org.openhab.core.library.types.OnOffType;
33 import org.openhab.core.library.types.OpenClosedType;
34 import org.openhab.core.library.types.PercentType;
35 import org.openhab.core.library.types.QuantityType;
36 import org.openhab.core.library.types.StringType;
37 import org.openhab.core.library.unit.ImperialUnits;
38 import org.openhab.core.library.unit.SIUnits;
39 import org.openhab.core.library.unit.Units;
40 import org.openhab.core.thing.Bridge;
41 import org.openhab.core.thing.Channel;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.ThingStatusInfo;
47 import org.openhab.core.thing.binding.BaseThingHandler;
48 import org.openhab.core.thing.binding.builder.ChannelBuilder;
49 import org.openhab.core.thing.binding.builder.ThingBuilder;
50 import org.openhab.core.thing.type.ChannelTypeUID;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.RefreshType;
53 import org.openhab.core.types.State;
54 import org.openhab.core.types.UnDefType;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
59 * The {@link SomfyTahomaBaseThingHandler} is base thing handler for all things.
61 * @author Ondrej Pecta - Initial contribution
62 * @author Laurent Garnier - Setting of channels at init + UoM for channels
65 public abstract class SomfyTahomaBaseThingHandler extends BaseThingHandler {
67 private final Logger logger = LoggerFactory.getLogger(getClass());
68 private HashMap<String, Integer> typeTable = new HashMap<>();
69 protected HashMap<String, String> stateNames = new HashMap<>();
71 protected String url = "";
73 private Map<String, Unit<?>> units = new HashMap<>();
75 public SomfyTahomaBaseThingHandler(Thing thing) {
77 // Define default units
78 units.put("Number:Temperature", SIUnits.CELSIUS);
79 units.put("Number:Energy", Units.WATT_HOUR);
80 units.put("Number:Illuminance", Units.LUX);
81 units.put("Number:Dimensionless", Units.PERCENT);
84 public HashMap<String, String> getStateNames() {
89 public void initialize() {
90 Bridge bridge = getBridge();
91 initializeThing(bridge != null ? bridge.getStatus() : null);
95 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
96 initializeThing(bridgeStatusInfo.getStatus());
99 public void initializeThing(@Nullable ThingStatus bridgeStatus) {
100 SomfyTahomaBridgeHandler bridgeHandler = getBridgeHandler();
101 if (bridgeHandler != null && bridgeStatus != null) {
103 if (getThing().getProperties().containsKey(RSSI_LEVEL_STATE)) {
106 if (bridgeStatus == ThingStatus.ONLINE) {
107 SomfyTahomaDevice device = bridgeHandler.getCachedDevice(url);
108 if (device != null) {
109 updateUnits(device.getAttributes());
110 List<SomfyTahomaState> states = device.getStates();
111 updateThingStatus(states);
112 updateThingChannels(states);
114 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, UNAVAILABLE);
117 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
120 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
124 private void createRSSIChannel() {
125 if (thing.getChannel(RSSI) == null) {
126 logger.debug("{} Creating a rssi channel", url);
127 ChannelTypeUID rssi = new ChannelTypeUID(BINDING_ID, "rssi");
128 createChannel(RSSI, "Number", "RSSI Level", rssi);
132 private void createChannel(String name, String type, String label, ChannelTypeUID channelType) {
133 ThingBuilder thingBuilder = editThing();
134 Channel channel = ChannelBuilder.create(new ChannelUID(thing.getUID(), name), type).withLabel(label)
135 .withType(channelType).build();
136 thingBuilder.withChannel(channel);
137 updateThing(thingBuilder.build());
141 public void handleCommand(ChannelUID channelUID, Command command) {
142 logger.debug("{} Received command {} for channel {}", url, command, channelUID);
143 if (command instanceof RefreshType) {
144 refresh(channelUID.getId());
148 public Logger getLogger() {
152 protected @Nullable SomfyTahomaBridgeHandler getBridgeHandler() {
153 Bridge localBridge = this.getBridge();
154 return localBridge != null ? (SomfyTahomaBridgeHandler) localBridge.getHandler() : null;
157 protected String getURL() {
158 return getThing().getConfiguration().get("url") != null ? getThing().getConfiguration().get("url").toString()
162 private void setAvailable() {
163 if (ThingStatus.ONLINE != thing.getStatus()) {
164 updateStatus(ThingStatus.ONLINE);
168 private void setUnavailable() {
169 if (ThingStatus.OFFLINE != thing.getStatus()) {
170 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, UNAVAILABLE);
174 protected void sendCommand(String cmd) {
175 sendCommand(cmd, "[]");
178 protected void sendCommand(String cmd, String param) {
179 SomfyTahomaBridgeHandler handler = getBridgeHandler();
180 if (handler != null) {
181 handler.sendCommand(url, cmd, param, EXEC_URL + "apply");
185 protected void sendTempCommand(String cmd, Command command) {
186 if (command instanceof DecimalType || command instanceof QuantityType) {
187 BigDecimal temperature = toTemperature(command);
188 if (temperature != null) {
189 String param = "[" + temperature.toPlainString() + "]";
190 sendCommand(cmd, param);
195 protected void sendCommandToSameDevicesInPlace(String cmd) {
196 sendCommandToSameDevicesInPlace(cmd, "[]");
199 protected void sendCommandToSameDevicesInPlace(String cmd, String param) {
200 SomfyTahomaBridgeHandler handler = getBridgeHandler();
201 if (handler != null) {
202 handler.sendCommandToSameDevicesInPlace(url, cmd, param, EXEC_URL + "apply");
206 protected void refresh(String channel) {
207 SomfyTahomaBridgeHandler handler = getBridgeHandler();
208 String stateName = stateNames.get(channel);
209 if (handler != null && stateName != null) {
210 handler.refresh(url, stateName);
214 protected void executeActionGroup() {
215 SomfyTahomaBridgeHandler handler = getBridgeHandler();
216 if (handler != null) {
217 handler.executeActionGroup(url);
221 protected @Nullable String getCurrentExecutions() {
222 SomfyTahomaBridgeHandler handler = getBridgeHandler();
223 if (handler != null) {
224 return handler.getCurrentExecutions(url);
229 protected void cancelExecution(String executionId) {
230 SomfyTahomaBridgeHandler handler = getBridgeHandler();
231 if (handler != null) {
232 handler.cancelExecution(executionId);
236 protected SomfyTahomaStatus getTahomaStatus(String id) {
237 SomfyTahomaBridgeHandler handler = getBridgeHandler();
238 if (handler != null) {
239 return handler.getTahomaStatus(id);
241 return new SomfyTahomaStatus();
244 private void cacheStateType(SomfyTahomaState state) {
245 if (state.getType() > 0 && !typeTable.containsKey(state.getName())) {
246 typeTable.put(state.getName(), state.getType());
250 protected void cacheStateType(String stateName, int type) {
251 if (type > 0 && !typeTable.containsKey(stateName)) {
252 typeTable.put(stateName, type);
256 protected Unit<?> getTemperatureUnit() {
257 return Objects.requireNonNull(units.get("Number:Temperature"));
260 private void updateUnits(List<SomfyTahomaState> attributes) {
261 for (SomfyTahomaState attr : attributes) {
262 if ("core:MeasuredValueType".equals(attr.getName()) && attr.getType() == TYPE_STRING) {
263 switch ((String) attr.getValue()) {
264 case "core:TemperatureInCelcius":
265 case "core:TemperatureInCelsius":
266 units.put("Number:Temperature", SIUnits.CELSIUS);
268 case "core:TemperatureInKelvin":
269 units.put("Number:Temperature", Units.KELVIN);
271 case "core:TemperatureInFahrenheit":
272 units.put("Number:Temperature", ImperialUnits.FAHRENHEIT);
274 case "core:RelativeValueInPercentage":
275 units.put("Number:Dimensionless", Units.PERCENT);
277 case "core:LuminanceInLux":
278 units.put("Number:Illuminance", Units.LUX);
280 case "core:ElectricalEnergyInWh":
281 units.put("Number:Energy", Units.WATT_HOUR);
283 case "core:ElectricalEnergyInKWh":
284 units.put("Number:Energy", Units.KILOWATT_HOUR);
286 case "core:ElectricalEnergyInMWh":
287 units.put("Number:Energy", Units.MEGAWATT_HOUR);
290 logger.warn("Unhandled value \"{}\" for attribute \"core:MeasuredValueType\"", attr.getValue());
298 protected @Nullable State parseTahomaState(@Nullable SomfyTahomaState state) {
299 return parseTahomaState(null, state);
302 protected @Nullable State parseTahomaState(@Nullable String acceptedItemType, @Nullable SomfyTahomaState state) {
304 return UnDefType.NULL;
307 int type = state.getType();
310 if (typeTable.containsKey(state.getName())) {
311 type = typeTable.get(state.getName());
313 cacheStateType(state);
317 logger.debug("{} Cannot recognize the state type for: {}!", url, state.getValue());
321 logger.trace("Value to parse: {}, type: {}", state.getValue(), type);
324 Double valPct = Double.parseDouble(state.getValue().toString());
325 if (acceptedItemType != null && acceptedItemType.startsWith(CoreItemFactory.NUMBER + ":")) {
326 Unit<?> unit = units.get(acceptedItemType);
328 return new QuantityType<>(normalizePercent(valPct), unit);
330 logger.warn("Do not return a quantity for {} because the unit is unknown",
334 return new PercentType(normalizePercent(valPct));
336 Double valDec = Double.parseDouble(state.getValue().toString());
337 if (acceptedItemType != null && acceptedItemType.startsWith(CoreItemFactory.NUMBER + ":")) {
338 Unit<?> unit = units.get(acceptedItemType);
340 return new QuantityType<>(valDec, unit);
342 logger.warn("Do not return a quantity for {} because the unit is unknown",
346 return new DecimalType(valDec);
349 String value = state.getValue().toString();
350 if ("String".equals(acceptedItemType)) {
351 return new StringType(value);
353 return parseStringState(value);
358 } catch (IllegalArgumentException ex) {
359 logger.debug("{} Error while parsing Tahoma state! Value: {} type: {}", url, state.getValue(), type, ex);
364 private int normalizePercent(Double valPct) {
365 int value = valPct.intValue();
368 } else if (value > 100) {
374 private State parseStringState(String value) {
375 if (value.endsWith("%")) {
376 // convert "100%" to 100 decimal
377 String val = value.replace("%", "");
378 logger.trace("converting: {} to value: {}", value, val);
379 Double valDec = Double.parseDouble(val);
380 return new DecimalType(valDec);
382 switch (value.toLowerCase()) {
390 return OnOffType.OFF;
392 case "nopersoninside":
395 return OpenClosedType.CLOSED;
401 return OpenClosedType.OPEN;
403 return UnDefType.UNDEF;
405 logger.debug("{} Unknown thing state returned: {}", url, value);
406 return UnDefType.UNDEF;
410 public void updateThingStatus(List<SomfyTahomaState> states) {
411 SomfyTahomaState state = getStatusState(states);
412 updateThingStatus(state);
415 private @Nullable SomfyTahomaState getStatusState(List<SomfyTahomaState> states) {
416 return getState(states, STATUS_STATE, TYPE_STRING);
419 private void updateThingStatus(@Nullable SomfyTahomaState state) {
421 // Most probably we are dealing with RTS device which does not return states
422 // so we have to setup ONLINE status manually
426 if (STATUS_STATE.equals(state.getName()) && state.getType() == TYPE_STRING) {
427 if (UNAVAILABLE.equals(state.getValue())) {
435 public void updateThingChannels(List<SomfyTahomaState> states) {
436 Map<String, String> properties = new HashMap<>();
437 for (SomfyTahomaState state : states) {
438 logger.trace("{} processing state: {} with value: {}", url, state.getName(), state.getValue());
439 properties.put(state.getName(), TYPE_NONE != state.getType() ? state.getValue().toString() : "");
440 if (RSSI_LEVEL_STATE.equals(state.getName())) {
441 // RSSI channel is a dynamic one
442 updateRSSIChannel(state);
444 updateThingChannels(state);
447 updateProperties(properties);
450 private void updateRSSIChannel(SomfyTahomaState state) {
452 Channel ch = thing.getChannel(RSSI);
454 logger.debug("{} updating RSSI channel with value: {}", url, state.getValue());
455 State newState = parseTahomaState(ch.getAcceptedItemType(), state);
456 if (newState != null) {
457 updateState(ch.getUID(), newState);
462 public void updateThingChannels(SomfyTahomaState state) {
463 stateNames.forEach((k, v) -> {
464 if (v.equals(state.getName())) {
465 Channel ch = thing.getChannel(k);
467 logger.debug("{} updating channel: {} with value: {}", url, k, state.getValue());
468 State newState = parseTahomaState(ch.getAcceptedItemType(), state);
469 if (newState != null) {
470 updateState(ch.getUID(), newState);
477 public int toInteger(Command command) {
478 return (command instanceof DecimalType dateTimeCommand) ? dateTimeCommand.intValue() : 0;
481 public @Nullable BigDecimal toTemperature(Command command) {
482 BigDecimal temperature = null;
483 if (command instanceof QuantityType<?> quantityCommand) {
484 QuantityType<?> convertedQuantity = quantityCommand.toUnit(getTemperatureUnit());
485 if (convertedQuantity != null) {
486 quantityCommand = convertedQuantity;
488 temperature = quantityCommand.toBigDecimal();
489 } else if (command instanceof DecimalType decimalCommand) {
490 temperature = decimalCommand.toBigDecimal();
495 public static @Nullable SomfyTahomaState getState(List<SomfyTahomaState> states, String stateName,
496 @Nullable Integer stateType) {
497 for (SomfyTahomaState state : states) {
498 if (stateName.equals(state.getName()) && (stateType == null || stateType == state.getType())) {