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