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