2 * Copyright (c) 2010-2024 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;
19 import java.util.HashMap;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import javax.measure.Unit;
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;
70 * The {@link GardenaThingHandler} is responsible for handling commands, which are sent to one of the channels.
72 * @author Gerhard Riegler - Initial contribution
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<>();
81 public GardenaThingHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
83 this.timeZoneProvider = timeZoneProvider;
87 public void initialize() {
89 Device device = getDevice();
90 updateProperties(device);
92 } catch (GardenaException ex) {
93 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getMessage());
94 } catch (AccountHandlerNotAvailableException ex) {
100 public void dispose() {
101 final ScheduledFuture<?> commandResetFuture = this.commandResetFuture;
102 if (commandResetFuture != null) {
103 commandResetFuture.cancel(true);
109 * Updates the thing properties from the Gardena device.
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);
117 String modelType = PropertyUtils.getPropertyValue(device, "common.attributes.modelType.value", String.class);
118 if (modelType != null) {
119 properties.put(PROPERTY_MODELTYPE, modelType);
121 updateProperties(properties);
125 public void channelLinked(ChannelUID channelUID) {
127 updateChannel(channelUID);
128 } catch (GardenaDeviceNotFoundException | AccountHandlerNotAvailableException ex) {
129 logger.debug("{}", ex.getMessage(), ex);
130 } catch (GardenaException ex) {
131 logger.error("{}", ex.getMessage(), ex);
136 * Updates the channel from the Gardena device.
138 protected void updateChannel(ChannelUID channelUID) throws GardenaException, AccountHandlerNotAvailableException {
139 String groupId = channelUID.getGroupId();
140 if (groupId == null) {
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);
150 updateState(channelUID, state);
156 * Converts a Gardena property value to an openHAB state.
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;
163 if (propertyName.endsWith("_timestamp")) {
164 propertyPath += propertyName.replace("_", ".");
166 propertyPath += propertyName + ".value";
167 unitPropertyPath += propertyName + "Unit";
170 Channel channel = getThing().getChannel(channelUID.getId());
171 String acceptedItemType = channel != null ? channel.getAcceptedItemType() : null;
172 String baseItemType = StringUtils.substringBefore(acceptedItemType, ":");
174 boolean isNullPropertyValue = PropertyUtils.isNull(device, propertyPath);
176 if (isNullPropertyValue) {
177 return UnDefType.NULL;
179 if (baseItemType == null || acceptedItemType == null) {
184 switch (baseItemType) {
186 return new StringType(PropertyUtils.getPropertyValue(device, propertyPath, String.class));
188 if (isNullPropertyValue) {
189 return new DecimalType(0);
191 Number value = PropertyUtils.getPropertyValue(device, propertyPath, Number.class);
192 Unit<?> unit = PropertyUtils.getPropertyValue(device, unitPropertyPath, Unit.class);
194 return UnDefType.NULL;
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;
203 if (acceptedItemType.equals(baseItemType) || unit == null) {
204 // No UoM or no unit found
205 return new DecimalType(value);
207 return new QuantityType<>(value, unit);
212 Date date = PropertyUtils.getPropertyValue(device, propertyPath, Date.class);
214 return UnDefType.NULL;
216 ZonedDateTime zdt = ZonedDateTime.ofInstant(date.toInstant(), timeZoneProvider.getTimeZone());
217 return new DateTimeType(zdt);
220 } catch (GardenaException e) {
221 logger.warn("Channel '{}' cannot be updated as device does not contain propertyPath '{}'", channelUID,
223 } catch (ClassCastException ex) {
224 logger.warn("Value of propertyPath '{}' can not be casted to {}: {}", propertyPath, acceptedItemType,
231 public void handleCommand(ChannelUID channelUID, Command command) {
232 logger.debug("Command received: {}", command);
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);
244 if (commandInSeconds != null) {
245 commandDurations.put(dataItemProperty, commandInSeconds.intValue());
247 logger.info("Invalid command '{}' for command duration channel, ignoring.", command);
249 } else if (isOnCommand) {
250 GardenaCommand gardenaCommand = getGardenaCommand(dataItemProperty, channelUID);
251 logger.debug("Received Gardena command: {}, {}", gardenaCommand.getClass().getSimpleName(),
252 gardenaCommand.attributes.command);
254 DataItem<?> dataItem = PropertyUtils.getPropertyValue(getDevice(), dataItemProperty, DataItem.class);
255 if (dataItem == null) {
256 logger.warn("DataItem {} is empty, ignoring command.", dataItemProperty);
258 getGardenaSmart().sendCommand(dataItem, gardenaCommand);
260 commandResetFuture = scheduler.schedule(() -> {
261 updateState(channelUID, OnOffType.OFF);
262 }, 3, TimeUnit.SECONDS);
265 } catch (AccountHandlerNotAvailableException | GardenaDeviceNotFoundException ex) {
267 } catch (Exception ex) {
268 logger.warn("{}", ex.getMessage());
270 final ThingHandler handler;
271 if ((bridge = getBridge()) != null && (handler = bridge.getHandler()) != null) {
272 ((GardenaSmartEventListener) handler).onError();
278 * Returns the Gardena command from the channel.
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);
296 throw new GardenaException("Command " + channelUID.getId() + " not found or groupId null");
300 * Updates the thing status based on the Gardena device status.
302 protected void updateStatus(Device device) {
303 ThingStatus oldStatus = thing.getStatus();
304 ThingStatus newStatus = ThingStatus.ONLINE;
305 ThingStatusDetail newDetail = ThingStatusDetail.NONE;
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;
314 if (oldStatus != newStatus || thing.getStatusInfo().getStatusDetail() != newDetail) {
315 updateStatus(newStatus, newDetail);
320 * Returns the device property for the dataItem from the channel.
322 private String getDeviceDataItemProperty(ChannelUID channelUID) throws GardenaException {
323 String dataItemProperty = StringUtils.substringBeforeLast(channelUID.getGroupId(), "_");
324 if (dataItemProperty != null) {
325 return dataItemProperty;
327 throw new GardenaException("Can't extract dataItemProperty from channel group " + channelUID.getGroupId());
330 private int getCommandDurationSeconds(String dataItemProperty) {
331 Integer duration = commandDurations.get(dataItemProperty);
332 return duration != null ? duration : 3600;
336 * Returns true, if the channel is the duration command.
338 private boolean isLocalDurationCommand(ChannelUID channelUID) {
339 return "commandDuration".equals(channelUID.getIdWithoutGroup());
343 * Returns the Gardena device for this ThingHandler.
345 private Device getDevice() throws GardenaException, AccountHandlerNotAvailableException {
346 return getGardenaSmart().getDevice(UidUtils.getGardenaDeviceId(getThing()));
350 * Returns the Gardena smart system implementation if the bridge is available.
352 private GardenaSmart getGardenaSmart() throws AccountHandlerNotAvailableException {
354 final ThingHandler handler;
355 if ((bridge = getBridge()) != null && (handler = bridge.getHandler()) != null) {
356 final GardenaSmart gardenaSmart = ((GardenaAccountHandler) handler).getGardenaSmart();
357 if (gardenaSmart != null) {
361 if (thing.getStatus() != ThingStatus.INITIALIZING) {
362 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR);
364 throw new AccountHandlerNotAvailableException("Gardena AccountHandler not yet available!");