]> git.basschouten.com Git - openhab-addons.git/blob
7e2e546f42521646b528e03329a7990962b2d37a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.somfytahoma.internal.handler;
14
15 import static org.openhab.binding.somfytahoma.internal.SomfyTahomaBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Objects;
22
23 import javax.measure.Unit;
24
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;
57
58 /**
59  * The {@link SomfyTahomaBaseThingHandler} is base thing handler for all things.
60  *
61  * @author Ondrej Pecta - Initial contribution
62  * @author Laurent Garnier - Setting of channels at init + UoM for channels
63  */
64 @NonNullByDefault
65 public abstract class SomfyTahomaBaseThingHandler extends BaseThingHandler {
66
67     private final Logger logger = LoggerFactory.getLogger(getClass());
68     private HashMap<String, Integer> typeTable = new HashMap<>();
69     protected HashMap<String, String> stateNames = new HashMap<>();
70
71     protected String url = "";
72
73     private Map<String, Unit<?>> units = new HashMap<>();
74
75     public SomfyTahomaBaseThingHandler(Thing thing) {
76         super(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);
82     }
83
84     public HashMap<String, String> getStateNames() {
85         return stateNames;
86     }
87
88     @Override
89     public void initialize() {
90         Bridge bridge = getBridge();
91         initializeThing(bridge != null ? bridge.getStatus() : null);
92     }
93
94     @Override
95     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
96         initializeThing(bridgeStatusInfo.getStatus());
97     }
98
99     public void initializeThing(@Nullable ThingStatus bridgeStatus) {
100         SomfyTahomaBridgeHandler bridgeHandler = getBridgeHandler();
101         if (bridgeHandler != null && bridgeStatus != null) {
102             url = getURL();
103             if (getThing().getProperties().containsKey(RSSI_LEVEL_STATE)) {
104                 createRSSIChannel();
105             }
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);
113                 } else {
114                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, UNAVAILABLE);
115                 }
116             } else {
117                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
118             }
119         } else {
120             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
121         }
122     }
123
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);
129         }
130     }
131
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());
138     }
139
140     @Override
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());
145         }
146     }
147
148     public Logger getLogger() {
149         return logger;
150     }
151
152     protected @Nullable SomfyTahomaBridgeHandler getBridgeHandler() {
153         Bridge localBridge = this.getBridge();
154         return localBridge != null ? (SomfyTahomaBridgeHandler) localBridge.getHandler() : null;
155     }
156
157     protected String getURL() {
158         return getThing().getConfiguration().get("url") != null ? getThing().getConfiguration().get("url").toString()
159                 : "";
160     }
161
162     private void setAvailable() {
163         if (ThingStatus.ONLINE != thing.getStatus()) {
164             updateStatus(ThingStatus.ONLINE);
165         }
166     }
167
168     private void setUnavailable() {
169         if (ThingStatus.OFFLINE != thing.getStatus()) {
170             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, UNAVAILABLE);
171         }
172     }
173
174     protected void sendCommand(String cmd) {
175         sendCommand(cmd, "[]");
176     }
177
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");
182         }
183     }
184
185     protected void sendCommandToSameDevicesInPlace(String cmd) {
186         sendCommandToSameDevicesInPlace(cmd, "[]");
187     }
188
189     protected void sendCommandToSameDevicesInPlace(String cmd, String param) {
190         SomfyTahomaBridgeHandler handler = getBridgeHandler();
191         if (handler != null) {
192             handler.sendCommandToSameDevicesInPlace(url, cmd, param, EXEC_URL + "apply");
193         }
194     }
195
196     protected void refresh(String channel) {
197         SomfyTahomaBridgeHandler handler = getBridgeHandler();
198         String stateName = stateNames.get(channel);
199         if (handler != null && stateName != null) {
200             handler.refresh(url, stateName);
201         }
202     }
203
204     protected void executeActionGroup() {
205         SomfyTahomaBridgeHandler handler = getBridgeHandler();
206         if (handler != null) {
207             handler.executeActionGroup(url);
208         }
209     }
210
211     protected @Nullable String getCurrentExecutions() {
212         SomfyTahomaBridgeHandler handler = getBridgeHandler();
213         if (handler != null) {
214             return handler.getCurrentExecutions(url);
215         }
216         return null;
217     }
218
219     protected void cancelExecution(String executionId) {
220         SomfyTahomaBridgeHandler handler = getBridgeHandler();
221         if (handler != null) {
222             handler.cancelExecution(executionId);
223         }
224     }
225
226     protected SomfyTahomaStatus getTahomaStatus(String id) {
227         SomfyTahomaBridgeHandler handler = getBridgeHandler();
228         if (handler != null) {
229             return handler.getTahomaStatus(id);
230         }
231         return new SomfyTahomaStatus();
232     }
233
234     private void cacheStateType(SomfyTahomaState state) {
235         if (state.getType() > 0 && !typeTable.containsKey(state.getName())) {
236             typeTable.put(state.getName(), state.getType());
237         }
238     }
239
240     protected void cacheStateType(String stateName, int type) {
241         if (type > 0 && !typeTable.containsKey(stateName)) {
242             typeTable.put(stateName, type);
243         }
244     }
245
246     protected Unit<?> getTemperatureUnit() {
247         return Objects.requireNonNull(units.get("Number:Temperature"));
248     }
249
250     private void updateUnits(List<SomfyTahomaState> attributes) {
251         for (SomfyTahomaState attr : attributes) {
252             if ("core:MeasuredValueType".equals(attr.getName()) && attr.getType() == TYPE_STRING) {
253                 switch ((String) attr.getValue()) {
254                     case "core:TemperatureInCelcius":
255                     case "core:TemperatureInCelsius":
256                         units.put("Number:Temperature", SIUnits.CELSIUS);
257                         break;
258                     case "core:TemperatureInKelvin":
259                         units.put("Number:Temperature", Units.KELVIN);
260                         break;
261                     case "core:TemperatureInFahrenheit":
262                         units.put("Number:Temperature", ImperialUnits.FAHRENHEIT);
263                         break;
264                     case "core:RelativeValueInPercentage":
265                         units.put("Number:Dimensionless", Units.PERCENT);
266                         break;
267                     case "core:LuminanceInLux":
268                         units.put("Number:Illuminance", Units.LUX);
269                         break;
270                     case "core:ElectricalEnergyInWh":
271                         units.put("Number:Energy", Units.WATT_HOUR);
272                         break;
273                     case "core:ElectricalEnergyInKWh":
274                         units.put("Number:Energy", Units.KILOWATT_HOUR);
275                         break;
276                     case "core:ElectricalEnergyInMWh":
277                         units.put("Number:Energy", Units.MEGAWATT_HOUR);
278                         break;
279                     default:
280                         logger.warn("Unhandled value \"{}\" for attribute \"core:MeasuredValueType\"", attr.getValue());
281                         break;
282                 }
283                 break;
284             }
285         }
286     }
287
288     protected @Nullable State parseTahomaState(@Nullable SomfyTahomaState state) {
289         return parseTahomaState(null, state);
290     }
291
292     protected @Nullable State parseTahomaState(@Nullable String acceptedItemType, @Nullable SomfyTahomaState state) {
293         if (state == null) {
294             return UnDefType.NULL;
295         }
296
297         int type = state.getType();
298
299         try {
300             if (typeTable.containsKey(state.getName())) {
301                 type = typeTable.get(state.getName());
302             } else {
303                 cacheStateType(state);
304             }
305
306             if (type == 0) {
307                 logger.debug("{} Cannot recognize the state type for: {}!", url, state.getValue());
308                 return null;
309             }
310
311             logger.trace("Value to parse: {}, type: {}", state.getValue(), type);
312             switch (type) {
313                 case TYPE_PERCENT:
314                     Double valPct = Double.parseDouble(state.getValue().toString());
315                     if (acceptedItemType != null && acceptedItemType.startsWith(CoreItemFactory.NUMBER + ":")) {
316                         Unit<?> unit = units.get(acceptedItemType);
317                         if (unit != null) {
318                             return new QuantityType<>(normalizePercent(valPct), unit);
319                         } else {
320                             logger.warn("Do not return a quantity for {} because the unit is unknown",
321                                     acceptedItemType);
322                         }
323                     }
324                     return new PercentType(normalizePercent(valPct));
325                 case TYPE_DECIMAL:
326                     Double valDec = Double.parseDouble(state.getValue().toString());
327                     if (acceptedItemType != null && acceptedItemType.startsWith(CoreItemFactory.NUMBER + ":")) {
328                         Unit<?> unit = units.get(acceptedItemType);
329                         if (unit != null) {
330                             return new QuantityType<>(valDec, unit);
331                         } else {
332                             logger.warn("Do not return a quantity for {} because the unit is unknown",
333                                     acceptedItemType);
334                         }
335                     }
336                     return new DecimalType(valDec);
337                 case TYPE_STRING:
338                 case TYPE_BOOLEAN:
339                     String value = state.getValue().toString();
340                     if ("String".equals(acceptedItemType)) {
341                         return new StringType(value);
342                     } else {
343                         return parseStringState(value);
344                     }
345                 default:
346                     return null;
347             }
348         } catch (IllegalArgumentException ex) {
349             logger.debug("{} Error while parsing Tahoma state! Value: {} type: {}", url, state.getValue(), type, ex);
350         }
351         return null;
352     }
353
354     private int normalizePercent(Double valPct) {
355         int value = valPct.intValue();
356         if (value < 0) {
357             value = 0;
358         } else if (value > 100) {
359             value = 100;
360         }
361         return value;
362     }
363
364     private State parseStringState(String value) {
365         if (value.endsWith("%")) {
366             // convert "100%" to 100 decimal
367             String val = value.replace("%", "");
368             logger.trace("converting: {} to value: {}", value, val);
369             Double valDec = Double.parseDouble(val);
370             return new DecimalType(valDec);
371         }
372         switch (value.toLowerCase()) {
373             case "on":
374             case "true":
375             case "active":
376                 return OnOffType.ON;
377             case "off":
378             case "false":
379             case "inactive":
380                 return OnOffType.OFF;
381             case "notdetected":
382             case "nopersoninside":
383             case "closed":
384             case "locked":
385                 return OpenClosedType.CLOSED;
386             case "detected":
387             case "personinside":
388             case "open":
389             case "opened":
390             case "unlocked":
391                 return OpenClosedType.OPEN;
392             case "unknown":
393                 return UnDefType.UNDEF;
394             default:
395                 logger.debug("{} Unknown thing state returned: {}", url, value);
396                 return UnDefType.UNDEF;
397         }
398     }
399
400     public void updateThingStatus(List<SomfyTahomaState> states) {
401         SomfyTahomaState state = getStatusState(states);
402         updateThingStatus(state);
403     }
404
405     private @Nullable SomfyTahomaState getStatusState(List<SomfyTahomaState> states) {
406         return getState(states, STATUS_STATE, TYPE_STRING);
407     }
408
409     private void updateThingStatus(@Nullable SomfyTahomaState state) {
410         if (state == null) {
411             // Most probably we are dealing with RTS device which does not return states
412             // so we have to setup ONLINE status manually
413             setAvailable();
414             return;
415         }
416         if (STATUS_STATE.equals(state.getName()) && state.getType() == TYPE_STRING) {
417             if (UNAVAILABLE.equals(state.getValue())) {
418                 setUnavailable();
419             } else {
420                 setAvailable();
421             }
422         }
423     }
424
425     public void updateThingChannels(List<SomfyTahomaState> states) {
426         Map<String, String> properties = new HashMap<>();
427         for (SomfyTahomaState state : states) {
428             logger.trace("{} processing state: {} with value: {}", url, state.getName(), state.getValue());
429             properties.put(state.getName(), TYPE_NONE != state.getType() ? state.getValue().toString() : "");
430             if (RSSI_LEVEL_STATE.equals(state.getName())) {
431                 // RSSI channel is a dynamic one
432                 updateRSSIChannel(state);
433             } else {
434                 updateThingChannels(state);
435             }
436         }
437         updateProperties(properties);
438     }
439
440     private void updateRSSIChannel(SomfyTahomaState state) {
441         createRSSIChannel();
442         Channel ch = thing.getChannel(RSSI);
443         if (ch != null) {
444             logger.debug("{} updating RSSI channel with value: {}", url, state.getValue());
445             State newState = parseTahomaState(ch.getAcceptedItemType(), state);
446             if (newState != null) {
447                 updateState(ch.getUID(), newState);
448             }
449         }
450     }
451
452     public void updateThingChannels(SomfyTahomaState state) {
453         stateNames.forEach((k, v) -> {
454             if (v.equals(state.getName())) {
455                 Channel ch = thing.getChannel(k);
456                 if (ch != null) {
457                     logger.debug("{} updating channel: {} with value: {}", url, k, state.getValue());
458                     State newState = parseTahomaState(ch.getAcceptedItemType(), state);
459                     if (newState != null) {
460                         updateState(ch.getUID(), newState);
461                     }
462                 }
463             }
464         });
465     }
466
467     public int toInteger(Command command) {
468         return (command instanceof DecimalType) ? ((DecimalType) command).intValue() : 0;
469     }
470
471     public @Nullable BigDecimal toTemperature(Command command) {
472         BigDecimal temperature = null;
473         if (command instanceof QuantityType<?>) {
474             QuantityType<?> quantity = (QuantityType<?>) command;
475             QuantityType<?> convertedQuantity = quantity.toUnit(getTemperatureUnit());
476             if (convertedQuantity != null) {
477                 quantity = convertedQuantity;
478             }
479             temperature = quantity.toBigDecimal();
480         } else if (command instanceof DecimalType) {
481             temperature = ((DecimalType) command).toBigDecimal();
482         }
483         return temperature;
484     }
485
486     public static @Nullable SomfyTahomaState getState(List<SomfyTahomaState> states, String stateName,
487             @Nullable Integer stateType) {
488         for (SomfyTahomaState state : states) {
489             if (stateName.equals(state.getName()) && (stateType == null || stateType == state.getType())) {
490                 return state;
491             }
492         }
493         return null;
494     }
495 }