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