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