2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.gardena.internal.handler;
15 import static org.openhab.binding.gardena.internal.GardenaBindingConstants.*;
17 import java.time.ZonedDateTime;
18 import java.util.Date;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
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;
66 * The {@link GardenaThingHandler} is responsible for handling commands, which are sent to one of the channels.
68 * @author Gerhard Riegler - Initial contribution
71 public class GardenaThingHandler extends BaseThingHandler {
72 private final Logger logger = LoggerFactory.getLogger(GardenaThingHandler.class);
73 private TimeZoneProvider timeZoneProvider;
74 private @Nullable ScheduledFuture<?> commandResetFuture;
76 public GardenaThingHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
78 this.timeZoneProvider = timeZoneProvider;
82 public void initialize() {
84 Device device = getDevice();
85 updateProperties(device);
87 } catch (GardenaException ex) {
88 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getMessage());
89 } catch (AccountHandlerNotAvailableException ex) {
95 public void dispose() {
96 final ScheduledFuture<?> commandResetFuture = this.commandResetFuture;
97 if (commandResetFuture != null) {
98 commandResetFuture.cancel(true);
104 * Updates the thing properties from the Gardena device.
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);
112 String modelType = PropertyUtils.getPropertyValue(device, "common.attributes.modelType.value", String.class);
113 if (modelType != null) {
114 properties.put(PROPERTY_MODELTYPE, modelType);
116 updateProperties(properties);
120 public void channelLinked(ChannelUID channelUID) {
122 updateChannel(channelUID);
123 } catch (GardenaDeviceNotFoundException | AccountHandlerNotAvailableException ex) {
124 logger.debug("{}", ex.getMessage(), ex);
125 } catch (GardenaException ex) {
126 logger.error("{}", ex.getMessage(), ex);
131 * Updates the channel from the Gardena device.
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);
141 updateState(channelUID, state);
148 * Converts a Gardena property value to an openHAB state.
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));
156 String propertyPath = channelUID.getGroupId() + ".attributes.";
157 String propertyName = channelUID.getIdWithoutGroup();
159 if (propertyName.endsWith("_timestamp")) {
160 propertyPath += propertyName.replace("_", ".");
162 propertyPath += propertyName + ".value";
165 String acceptedItemType = null;
167 Channel channel = getThing().getChannel(channelUID.getId());
168 if (channel != null) {
169 acceptedItemType = StringUtils.substringBefore(channel.getAcceptedItemType(), ":");
171 if (acceptedItemType != null) {
172 boolean isNullPropertyValue = PropertyUtils.isNull(device, propertyPath);
173 boolean isDurationProperty = "duration".equals(propertyName);
175 if (isNullPropertyValue && !isDurationProperty) {
176 return UnDefType.NULL;
178 switch (acceptedItemType) {
180 return new StringType(PropertyUtils.getPropertyValue(device, propertyPath, String.class));
182 if (isNullPropertyValue) {
183 return new DecimalType(0);
185 Number value = PropertyUtils.getPropertyValue(device, propertyPath, Number.class);
186 // convert duration from seconds to minutes
188 if (isDurationProperty) {
189 value = Math.round(value.longValue() / 60.0);
191 return new DecimalType(value.longValue());
193 return UnDefType.NULL;
196 Date date = PropertyUtils.getPropertyValue(device, propertyPath, Date.class);
198 ZonedDateTime zdt = ZonedDateTime.ofInstant(date.toInstant(),
199 timeZoneProvider.getTimeZone());
200 return new DateTimeType(zdt);
202 return UnDefType.NULL;
206 } catch (GardenaException e) {
207 logger.warn("Channel '{}' cannot be updated as device does not contain propertyPath '{}'", channelUID,
209 } catch (ClassCastException ex) {
210 logger.warn("Value of propertyPath '{}' can not be casted to {}: {}", propertyPath, acceptedItemType,
217 public void handleCommand(ChannelUID channelUID, Command command) {
218 logger.debug("Command received: {}", command);
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);
233 DataItem<?> dataItem = PropertyUtils.getPropertyValue(getDevice(), dataItemProperty, DataItem.class);
234 if (dataItem == null) {
235 logger.warn("DataItem {} is empty, ignoring command.", dataItemProperty);
237 getGardenaSmart().sendCommand(dataItem, gardenaCommand);
239 commandResetFuture = scheduler.schedule(() -> {
240 updateState(channelUID, OnOffType.OFF);
241 }, 3, TimeUnit.SECONDS);
244 } catch (AccountHandlerNotAvailableException | GardenaDeviceNotFoundException ex) {
246 } catch (Exception ex) {
247 logger.warn("{}", ex.getMessage());
249 final ThingHandler handler;
250 if ((bridge = getBridge()) != null && (handler = bridge.getHandler()) != null) {
251 ((GardenaSmartEventListener) handler).onError();
257 * Returns the Gardena command from the channel.
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);
277 throw new GardenaException("Command " + channelUID.getId() + " not found or groupId null");
281 * Updates the thing status based on the Gardena device status.
283 protected void updateStatus(Device device) {
284 ThingStatus oldStatus = thing.getStatus();
285 ThingStatus newStatus = ThingStatus.ONLINE;
286 ThingStatusDetail newDetail = ThingStatusDetail.NONE;
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;
295 if (oldStatus != newStatus || thing.getStatusInfo().getStatusDetail() != newDetail) {
296 updateStatus(newStatus, newDetail);
301 * Returns the device property for the dataItem from the channel.
303 private String getDeviceDataItemProperty(ChannelUID channelUID) throws GardenaException {
304 String dataItemProperty = StringUtils.substringBeforeLast(channelUID.getGroupId(), "_");
305 if (dataItemProperty != null) {
306 return dataItemProperty;
308 throw new GardenaException("Can't extract dataItemProperty from channel group " + channelUID.getGroupId());
312 * Returns true, if the channel is the duration command.
314 private boolean isLocalDurationCommand(ChannelUID channelUID) {
315 return "commandDuration".equals(channelUID.getIdWithoutGroup());
319 * Returns the Gardena device for this ThingHandler.
321 private Device getDevice() throws GardenaException, AccountHandlerNotAvailableException {
322 return getGardenaSmart().getDevice(UidUtils.getGardenaDeviceId(getThing()));
326 * Returns the Gardena smart system implementation if the bridge is available.
328 private GardenaSmart getGardenaSmart() throws AccountHandlerNotAvailableException {
330 final ThingHandler handler;
331 if ((bridge = getBridge()) != null && (handler = bridge.getHandler()) != null) {
332 final GardenaSmart gardenaSmart = ((GardenaAccountHandler) handler).getGardenaSmart();
333 if (gardenaSmart != null) {
337 if (thing.getStatus() != ThingStatus.INITIALIZING) {
338 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR);
340 throw new AccountHandlerNotAvailableException("Gardena AccountHandler not yet available!");