]> git.basschouten.com Git - openhab-addons.git/blob
3d4bec32c1b9901d1c9da69dfcdb74d2f50d48f3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.gardena.internal.handler;
14
15 import static org.openhab.binding.gardena.internal.GardenaBindingConstants.*;
16
17 import java.time.ZonedDateTime;
18 import java.util.Date;
19 import java.util.HashMap;
20 import java.util.Map;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23
24 import javax.measure.Unit;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.gardena.internal.GardenaSmart;
29 import org.openhab.binding.gardena.internal.GardenaSmartEventListener;
30 import org.openhab.binding.gardena.internal.exception.GardenaDeviceNotFoundException;
31 import org.openhab.binding.gardena.internal.exception.GardenaException;
32 import org.openhab.binding.gardena.internal.model.dto.Device;
33 import org.openhab.binding.gardena.internal.model.dto.api.CommonService;
34 import org.openhab.binding.gardena.internal.model.dto.api.DataItem;
35 import org.openhab.binding.gardena.internal.model.dto.command.GardenaCommand;
36 import org.openhab.binding.gardena.internal.model.dto.command.MowerCommand;
37 import org.openhab.binding.gardena.internal.model.dto.command.MowerCommand.MowerControl;
38 import org.openhab.binding.gardena.internal.model.dto.command.PowerSocketCommand;
39 import org.openhab.binding.gardena.internal.model.dto.command.PowerSocketCommand.PowerSocketControl;
40 import org.openhab.binding.gardena.internal.model.dto.command.ValveCommand;
41 import org.openhab.binding.gardena.internal.model.dto.command.ValveCommand.ValveControl;
42 import org.openhab.binding.gardena.internal.model.dto.command.ValveSetCommand;
43 import org.openhab.binding.gardena.internal.model.dto.command.ValveSetCommand.ValveSetControl;
44 import org.openhab.binding.gardena.internal.util.PropertyUtils;
45 import org.openhab.binding.gardena.internal.util.StringUtils;
46 import org.openhab.binding.gardena.internal.util.UidUtils;
47 import org.openhab.core.i18n.TimeZoneProvider;
48 import org.openhab.core.library.types.DateTimeType;
49 import org.openhab.core.library.types.DecimalType;
50 import org.openhab.core.library.types.OnOffType;
51 import org.openhab.core.library.types.QuantityType;
52 import org.openhab.core.library.types.StringType;
53 import org.openhab.core.library.unit.Units;
54 import org.openhab.core.thing.Bridge;
55 import org.openhab.core.thing.Channel;
56 import org.openhab.core.thing.ChannelUID;
57 import org.openhab.core.thing.Thing;
58 import org.openhab.core.thing.ThingStatus;
59 import org.openhab.core.thing.ThingStatusDetail;
60 import org.openhab.core.thing.binding.BaseThingHandler;
61 import org.openhab.core.thing.binding.ThingHandler;
62 import org.openhab.core.types.Command;
63 import org.openhab.core.types.RefreshType;
64 import org.openhab.core.types.State;
65 import org.openhab.core.types.UnDefType;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
68
69 /**
70  * The {@link GardenaThingHandler} is responsible for handling commands, which are sent to one of the channels.
71  *
72  * @author Gerhard Riegler - Initial contribution
73  */
74 @NonNullByDefault
75 public class GardenaThingHandler extends BaseThingHandler {
76     private final Logger logger = LoggerFactory.getLogger(GardenaThingHandler.class);
77     private TimeZoneProvider timeZoneProvider;
78     private @Nullable ScheduledFuture<?> commandResetFuture;
79     private Map<String, Integer> commandDurations = new HashMap<>();
80
81     public GardenaThingHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
82         super(thing);
83         this.timeZoneProvider = timeZoneProvider;
84     }
85
86     @Override
87     public void initialize() {
88         try {
89             Device device = getDevice();
90             updateProperties(device);
91             updateStatus(device);
92         } catch (GardenaException ex) {
93             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getMessage());
94         } catch (AccountHandlerNotAvailableException ex) {
95             // ignore
96         }
97     }
98
99     @Override
100     public void dispose() {
101         final ScheduledFuture<?> commandResetFuture = this.commandResetFuture;
102         if (commandResetFuture != null) {
103             commandResetFuture.cancel(true);
104         }
105         super.dispose();
106     }
107
108     /**
109      * Updates the thing properties from the Gardena device.
110      */
111     protected void updateProperties(Device device) throws GardenaException {
112         Map<String, String> properties = editProperties();
113         String serial = PropertyUtils.getPropertyValue(device, "common.attributes.serial.value", String.class);
114         if (serial != null) {
115             properties.put(PROPERTY_SERIALNUMBER, serial);
116         }
117         String modelType = PropertyUtils.getPropertyValue(device, "common.attributes.modelType.value", String.class);
118         if (modelType != null) {
119             properties.put(PROPERTY_MODELTYPE, modelType);
120         }
121         updateProperties(properties);
122     }
123
124     @Override
125     public void channelLinked(ChannelUID channelUID) {
126         try {
127             updateChannel(channelUID);
128         } catch (GardenaDeviceNotFoundException | AccountHandlerNotAvailableException ex) {
129             logger.debug("{}", ex.getMessage(), ex);
130         } catch (GardenaException ex) {
131             logger.error("{}", ex.getMessage(), ex);
132         }
133     }
134
135     /**
136      * Updates the channel from the Gardena device.
137      */
138     protected void updateChannel(ChannelUID channelUID) throws GardenaException, AccountHandlerNotAvailableException {
139         String groupId = channelUID.getGroupId();
140         if (groupId == null) {
141             return;
142         }
143         if (isLocalDurationCommand(channelUID)) {
144             int commandDuration = getCommandDurationSeconds(getDeviceDataItemProperty(channelUID));
145             updateState(channelUID, new QuantityType<>(commandDuration, Units.SECOND));
146         } else if (!groupId.endsWith("_commands")) {
147             Device device = getDevice();
148             State state = convertToState(device, channelUID);
149             if (state != null) {
150                 updateState(channelUID, state);
151             }
152         }
153     }
154
155     /**
156      * Converts a Gardena property value to an openHAB state.
157      */
158     private @Nullable State convertToState(Device device, ChannelUID channelUID) throws GardenaException {
159         String propertyPath = channelUID.getGroupId() + ".attributes.";
160         String propertyName = channelUID.getIdWithoutGroup();
161         String unitPropertyPath = propertyPath;
162
163         if (propertyName.endsWith("_timestamp")) {
164             propertyPath += propertyName.replace("_", ".");
165         } else {
166             propertyPath += propertyName + ".value";
167             unitPropertyPath += propertyName + "Unit";
168         }
169
170         Channel channel = getThing().getChannel(channelUID.getId());
171         String acceptedItemType = channel != null ? channel.getAcceptedItemType() : null;
172         String baseItemType = StringUtils.substringBefore(acceptedItemType, ":");
173
174         boolean isNullPropertyValue = PropertyUtils.isNull(device, propertyPath);
175
176         if (isNullPropertyValue) {
177             return UnDefType.NULL;
178         }
179         if (baseItemType == null || acceptedItemType == null) {
180             return null;
181         }
182
183         try {
184             switch (baseItemType) {
185                 case "String":
186                     return new StringType(PropertyUtils.getPropertyValue(device, propertyPath, String.class));
187                 case "Number":
188                     if (isNullPropertyValue) {
189                         return new DecimalType(0);
190                     } else {
191                         Number value = PropertyUtils.getPropertyValue(device, propertyPath, Number.class);
192                         Unit<?> unit = PropertyUtils.getPropertyValue(device, unitPropertyPath, Unit.class);
193                         if (value == null) {
194                             return UnDefType.NULL;
195                         } else {
196                             if ("rfLinkLevel".equals(propertyName)) {
197                                 // Gardena gives us link level as 0..100%, while the system.signal-strength
198                                 // channel type wants a 0..4 enum
199                                 int percent = value.intValue();
200                                 value = percent == 100 ? 4 : percent / 20;
201                                 unit = null;
202                             }
203                             if (acceptedItemType.equals(baseItemType) || unit == null) {
204                                 // No UoM or no unit found
205                                 return new DecimalType(value);
206                             } else {
207                                 return new QuantityType<>(value, unit);
208                             }
209                         }
210                     }
211                 case "DateTime":
212                     Date date = PropertyUtils.getPropertyValue(device, propertyPath, Date.class);
213                     if (date == null) {
214                         return UnDefType.NULL;
215                     } else {
216                         ZonedDateTime zdt = ZonedDateTime.ofInstant(date.toInstant(), timeZoneProvider.getTimeZone());
217                         return new DateTimeType(zdt);
218                     }
219             }
220         } catch (GardenaException e) {
221             logger.warn("Channel '{}' cannot be updated as device does not contain propertyPath '{}'", channelUID,
222                     propertyPath);
223         } catch (ClassCastException ex) {
224             logger.warn("Value of propertyPath '{}' can not be casted to {}: {}", propertyPath, acceptedItemType,
225                     ex.getMessage());
226         }
227         return null;
228     }
229
230     @Override
231     public void handleCommand(ChannelUID channelUID, Command command) {
232         logger.debug("Command received: {}", command);
233         try {
234             boolean isOnCommand = command instanceof OnOffType onOffCommand && onOffCommand == OnOffType.ON;
235             String dataItemProperty = getDeviceDataItemProperty(channelUID);
236             if (RefreshType.REFRESH == command) {
237                 logger.debug("Refreshing Gardena connection");
238                 getGardenaSmart().restartWebsockets();
239             } else if (isLocalDurationCommand(channelUID)) {
240                 QuantityType<?> commandInSeconds = null;
241                 if (command instanceof QuantityType<?> timeCommand) {
242                     commandInSeconds = timeCommand.toUnit(Units.SECOND);
243                 }
244                 if (commandInSeconds != null) {
245                     commandDurations.put(dataItemProperty, commandInSeconds.intValue());
246                 } else {
247                     logger.info("Invalid command '{}' for command duration channel, ignoring.", command);
248                 }
249             } else if (isOnCommand) {
250                 GardenaCommand gardenaCommand = getGardenaCommand(dataItemProperty, channelUID);
251                 logger.debug("Received Gardena command: {}, {}", gardenaCommand.getClass().getSimpleName(),
252                         gardenaCommand.attributes.command);
253
254                 DataItem<?> dataItem = PropertyUtils.getPropertyValue(getDevice(), dataItemProperty, DataItem.class);
255                 if (dataItem == null) {
256                     logger.warn("DataItem {} is empty, ignoring command.", dataItemProperty);
257                 } else {
258                     getGardenaSmart().sendCommand(dataItem, gardenaCommand);
259
260                     commandResetFuture = scheduler.schedule(() -> {
261                         updateState(channelUID, OnOffType.OFF);
262                     }, 3, TimeUnit.SECONDS);
263                 }
264             }
265         } catch (AccountHandlerNotAvailableException | GardenaDeviceNotFoundException ex) {
266             // ignore
267         } catch (Exception ex) {
268             logger.warn("{}", ex.getMessage());
269             final Bridge bridge;
270             final ThingHandler handler;
271             if ((bridge = getBridge()) != null && (handler = bridge.getHandler()) != null) {
272                 ((GardenaSmartEventListener) handler).onError();
273             }
274         }
275     }
276
277     /**
278      * Returns the Gardena command from the channel.
279      */
280     private GardenaCommand getGardenaCommand(String dataItemProperty, ChannelUID channelUID)
281             throws GardenaException, AccountHandlerNotAvailableException {
282         String commandName = channelUID.getIdWithoutGroup().toUpperCase();
283         String groupId = channelUID.getGroupId();
284         if (groupId != null) {
285             int commandDuration = getCommandDurationSeconds(dataItemProperty);
286             if ("valveSet_commands".equals(groupId)) {
287                 return new ValveSetCommand(ValveSetControl.valueOf(commandName));
288             } else if (groupId.startsWith("valve") && groupId.endsWith("_commands")) {
289                 return new ValveCommand(ValveControl.valueOf(commandName), commandDuration);
290             } else if ("mower_commands".equals(groupId)) {
291                 return new MowerCommand(MowerControl.valueOf(commandName), commandDuration);
292             } else if ("powerSocket_commands".equals(groupId)) {
293                 return new PowerSocketCommand(PowerSocketControl.valueOf(commandName), commandDuration);
294             }
295         }
296         throw new GardenaException("Command " + channelUID.getId() + " not found or groupId null");
297     }
298
299     /**
300      * Updates the thing status based on the Gardena device status.
301      */
302     protected void updateStatus(Device device) {
303         ThingStatus oldStatus = thing.getStatus();
304         ThingStatus newStatus = ThingStatus.ONLINE;
305         ThingStatusDetail newDetail = ThingStatusDetail.NONE;
306
307         CommonService commonServiceAttributes = device.common.attributes;
308         if (commonServiceAttributes == null
309                 || !CONNECTION_STATUS_ONLINE.equals(commonServiceAttributes.rfLinkState.value)) {
310             newStatus = ThingStatus.OFFLINE;
311             newDetail = ThingStatusDetail.COMMUNICATION_ERROR;
312         }
313
314         if (oldStatus != newStatus || thing.getStatusInfo().getStatusDetail() != newDetail) {
315             updateStatus(newStatus, newDetail);
316         }
317     }
318
319     /**
320      * Returns the device property for the dataItem from the channel.
321      */
322     private String getDeviceDataItemProperty(ChannelUID channelUID) throws GardenaException {
323         String dataItemProperty = StringUtils.substringBeforeLast(channelUID.getGroupId(), "_");
324         if (dataItemProperty != null) {
325             return dataItemProperty;
326         }
327         throw new GardenaException("Can't extract dataItemProperty from channel group " + channelUID.getGroupId());
328     }
329
330     private int getCommandDurationSeconds(String dataItemProperty) {
331         Integer duration = commandDurations.get(dataItemProperty);
332         return duration != null ? duration : 3600;
333     }
334
335     /**
336      * Returns true, if the channel is the duration command.
337      */
338     private boolean isLocalDurationCommand(ChannelUID channelUID) {
339         return "commandDuration".equals(channelUID.getIdWithoutGroup());
340     }
341
342     /**
343      * Returns the Gardena device for this ThingHandler.
344      */
345     private Device getDevice() throws GardenaException, AccountHandlerNotAvailableException {
346         return getGardenaSmart().getDevice(UidUtils.getGardenaDeviceId(getThing()));
347     }
348
349     /**
350      * Returns the Gardena smart system implementation if the bridge is available.
351      */
352     private GardenaSmart getGardenaSmart() throws AccountHandlerNotAvailableException {
353         final Bridge bridge;
354         final ThingHandler handler;
355         if ((bridge = getBridge()) != null && (handler = bridge.getHandler()) != null) {
356             final GardenaSmart gardenaSmart = ((GardenaAccountHandler) handler).getGardenaSmart();
357             if (gardenaSmart != null) {
358                 return gardenaSmart;
359             }
360         }
361         if (thing.getStatus() != ThingStatus.INITIALIZING) {
362             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR);
363         }
364         throw new AccountHandlerNotAvailableException("Gardena AccountHandler not yet available!");
365     }
366 }