]> git.basschouten.com Git - openhab-addons.git/blob
dffde476fa0d2de0a6e33211199f098f616be4f3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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 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);
191             }
192         }
193     }
194
195     protected void sendCommandToSameDevicesInPlace(String cmd) {
196         sendCommandToSameDevicesInPlace(cmd, "[]");
197     }
198
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");
203         }
204     }
205
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);
211         }
212     }
213
214     protected void executeActionGroup() {
215         SomfyTahomaBridgeHandler handler = getBridgeHandler();
216         if (handler != null) {
217             handler.executeActionGroup(url);
218         }
219     }
220
221     protected @Nullable String getCurrentExecutions() {
222         SomfyTahomaBridgeHandler handler = getBridgeHandler();
223         if (handler != null) {
224             return handler.getCurrentExecutions(url);
225         }
226         return null;
227     }
228
229     protected void cancelExecution(String executionId) {
230         SomfyTahomaBridgeHandler handler = getBridgeHandler();
231         if (handler != null) {
232             handler.cancelExecution(executionId);
233         }
234     }
235
236     protected SomfyTahomaStatus getTahomaStatus(String id) {
237         SomfyTahomaBridgeHandler handler = getBridgeHandler();
238         if (handler != null) {
239             return handler.getTahomaStatus(id);
240         }
241         return new SomfyTahomaStatus();
242     }
243
244     private void cacheStateType(SomfyTahomaState state) {
245         if (state.getType() > 0 && !typeTable.containsKey(state.getName())) {
246             typeTable.put(state.getName(), state.getType());
247         }
248     }
249
250     protected void cacheStateType(String stateName, int type) {
251         if (type > 0 && !typeTable.containsKey(stateName)) {
252             typeTable.put(stateName, type);
253         }
254     }
255
256     protected Unit<?> getTemperatureUnit() {
257         return Objects.requireNonNull(units.get("Number:Temperature"));
258     }
259
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);
267                         break;
268                     case "core:TemperatureInKelvin":
269                         units.put("Number:Temperature", Units.KELVIN);
270                         break;
271                     case "core:TemperatureInFahrenheit":
272                         units.put("Number:Temperature", ImperialUnits.FAHRENHEIT);
273                         break;
274                     case "core:RelativeValueInPercentage":
275                         units.put("Number:Dimensionless", Units.PERCENT);
276                         break;
277                     case "core:LuminanceInLux":
278                         units.put("Number:Illuminance", Units.LUX);
279                         break;
280                     case "core:ElectricalEnergyInWh":
281                         units.put("Number:Energy", Units.WATT_HOUR);
282                         break;
283                     case "core:ElectricalEnergyInKWh":
284                         units.put("Number:Energy", Units.KILOWATT_HOUR);
285                         break;
286                     case "core:ElectricalEnergyInMWh":
287                         units.put("Number:Energy", Units.MEGAWATT_HOUR);
288                         break;
289                     default:
290                         logger.warn("Unhandled value \"{}\" for attribute \"core:MeasuredValueType\"", attr.getValue());
291                         break;
292                 }
293                 break;
294             }
295         }
296     }
297
298     protected @Nullable State parseTahomaState(@Nullable SomfyTahomaState state) {
299         return parseTahomaState(null, state);
300     }
301
302     protected @Nullable State parseTahomaState(@Nullable String acceptedItemType, @Nullable SomfyTahomaState state) {
303         if (state == null) {
304             return UnDefType.NULL;
305         }
306
307         int type = state.getType();
308
309         try {
310             if (typeTable.containsKey(state.getName())) {
311                 type = typeTable.get(state.getName());
312             } else {
313                 cacheStateType(state);
314             }
315
316             if (type == 0) {
317                 logger.debug("{} Cannot recognize the state type for: {}!", url, state.getValue());
318                 return null;
319             }
320
321             logger.trace("Value to parse: {}, type: {}", state.getValue(), type);
322             switch (type) {
323                 case TYPE_PERCENT:
324                     Double valPct = Double.parseDouble(state.getValue().toString());
325                     if (acceptedItemType != null && acceptedItemType.startsWith(CoreItemFactory.NUMBER + ":")) {
326                         Unit<?> unit = units.get(acceptedItemType);
327                         if (unit != null) {
328                             return new QuantityType<>(normalizePercent(valPct), unit);
329                         } else {
330                             logger.warn("Do not return a quantity for {} because the unit is unknown",
331                                     acceptedItemType);
332                         }
333                     }
334                     return new PercentType(normalizePercent(valPct));
335                 case TYPE_DECIMAL:
336                     Double valDec = Double.parseDouble(state.getValue().toString());
337                     if (acceptedItemType != null && acceptedItemType.startsWith(CoreItemFactory.NUMBER + ":")) {
338                         Unit<?> unit = units.get(acceptedItemType);
339                         if (unit != null) {
340                             return new QuantityType<>(valDec, unit);
341                         } else {
342                             logger.warn("Do not return a quantity for {} because the unit is unknown",
343                                     acceptedItemType);
344                         }
345                     }
346                     return new DecimalType(valDec);
347                 case TYPE_STRING:
348                 case TYPE_BOOLEAN:
349                     String value = state.getValue().toString();
350                     if ("String".equals(acceptedItemType)) {
351                         return new StringType(value);
352                     } else {
353                         return parseStringState(value);
354                     }
355                 default:
356                     return null;
357             }
358         } catch (IllegalArgumentException ex) {
359             logger.debug("{} Error while parsing Tahoma state! Value: {} type: {}", url, state.getValue(), type, ex);
360         }
361         return null;
362     }
363
364     private int normalizePercent(Double valPct) {
365         int value = valPct.intValue();
366         if (value < 0) {
367             value = 0;
368         } else if (value > 100) {
369             value = 100;
370         }
371         return value;
372     }
373
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);
381         }
382         switch (value.toLowerCase()) {
383             case "on":
384             case "true":
385             case "active":
386                 return OnOffType.ON;
387             case "off":
388             case "false":
389             case "inactive":
390                 return OnOffType.OFF;
391             case "notdetected":
392             case "nopersoninside":
393             case "closed":
394             case "locked":
395                 return OpenClosedType.CLOSED;
396             case "detected":
397             case "personinside":
398             case "open":
399             case "opened":
400             case "unlocked":
401                 return OpenClosedType.OPEN;
402             case "unknown":
403                 return UnDefType.UNDEF;
404             default:
405                 logger.debug("{} Unknown thing state returned: {}", url, value);
406                 return UnDefType.UNDEF;
407         }
408     }
409
410     public void updateThingStatus(List<SomfyTahomaState> states) {
411         SomfyTahomaState state = getStatusState(states);
412         updateThingStatus(state);
413     }
414
415     private @Nullable SomfyTahomaState getStatusState(List<SomfyTahomaState> states) {
416         return getState(states, STATUS_STATE, TYPE_STRING);
417     }
418
419     private void updateThingStatus(@Nullable SomfyTahomaState state) {
420         if (state == null) {
421             // Most probably we are dealing with RTS device which does not return states
422             // so we have to setup ONLINE status manually
423             setAvailable();
424             return;
425         }
426         if (STATUS_STATE.equals(state.getName()) && state.getType() == TYPE_STRING) {
427             if (UNAVAILABLE.equals(state.getValue())) {
428                 setUnavailable();
429             } else {
430                 setAvailable();
431             }
432         }
433     }
434
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);
443             } else {
444                 updateThingChannels(state);
445             }
446         }
447         updateProperties(properties);
448     }
449
450     private void updateRSSIChannel(SomfyTahomaState state) {
451         createRSSIChannel();
452         Channel ch = thing.getChannel(RSSI);
453         if (ch != null) {
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);
458             }
459         }
460     }
461
462     public void updateThingChannels(SomfyTahomaState state) {
463         stateNames.forEach((k, v) -> {
464             if (v.equals(state.getName())) {
465                 Channel ch = thing.getChannel(k);
466                 if (ch != null) {
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);
471                     }
472                 }
473             }
474         });
475     }
476
477     public int toInteger(Command command) {
478         return (command instanceof DecimalType) ? ((DecimalType) command).intValue() : 0;
479     }
480
481     public @Nullable BigDecimal toTemperature(Command command) {
482         BigDecimal temperature = null;
483         if (command instanceof QuantityType<?>) {
484             QuantityType<?> quantity = (QuantityType<?>) command;
485             QuantityType<?> convertedQuantity = quantity.toUnit(getTemperatureUnit());
486             if (convertedQuantity != null) {
487                 quantity = convertedQuantity;
488             }
489             temperature = quantity.toBigDecimal();
490         } else if (command instanceof DecimalType) {
491             temperature = ((DecimalType) command).toBigDecimal();
492         }
493         return temperature;
494     }
495
496     public static @Nullable SomfyTahomaState getState(List<SomfyTahomaState> states, String stateName,
497             @Nullable Integer stateType) {
498         for (SomfyTahomaState state : states) {
499             if (stateName.equals(state.getName()) && (stateType == null || stateType == state.getType())) {
500                 return state;
501             }
502         }
503         return null;
504     }
505 }