2 * Copyright (c) 2010-2020 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.automower.internal.things;
15 import static org.openhab.binding.automower.internal.AutomowerBindingConstants.*;
17 import java.time.Instant;
18 import java.time.ZoneId;
19 import java.time.ZonedDateTime;
20 import java.util.Collection;
21 import java.util.Collections;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.atomic.AtomicReference;
28 import javax.measure.quantity.Dimensionless;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.automower.internal.AutomowerBindingConstants;
33 import org.openhab.binding.automower.internal.actions.AutomowerActions;
34 import org.openhab.binding.automower.internal.bridge.AutomowerBridge;
35 import org.openhab.binding.automower.internal.bridge.AutomowerBridgeHandler;
36 import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.Mower;
37 import org.openhab.binding.automower.internal.rest.exceptions.AutomowerCommunicationException;
38 import org.openhab.core.library.types.DateTimeType;
39 import org.openhab.core.library.types.DecimalType;
40 import org.openhab.core.library.types.QuantityType;
41 import org.openhab.core.library.types.StringType;
42 import org.openhab.core.library.unit.SmartHomeUnits;
43 import org.openhab.core.thing.Bridge;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.thing.ThingTypeUID;
49 import org.openhab.core.thing.binding.BaseThingHandler;
50 import org.openhab.core.thing.binding.ThingHandler;
51 import org.openhab.core.thing.binding.ThingHandlerService;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.RefreshType;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
58 * The {@link AutomowerHandler} is responsible for handling commands, which are
59 * sent to one of the channels.
61 * @author Markus Pfleger - Initial contribution
64 public class AutomowerHandler extends BaseThingHandler {
65 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_AUTOMOWER);
66 private static final String NO_ID = "NO_ID";
67 private static final long DEFAULT_COMMAND_DURATION_MIN = 60;
68 private static final long DEFAULT_POLLING_INTERVAL_S = TimeUnit.MINUTES.toSeconds(10);
70 private final Logger logger = LoggerFactory.getLogger(AutomowerHandler.class);
71 private AtomicReference<String> automowerId = new AtomicReference<String>(NO_ID);
72 private long lastQueryTimeMs = 0L;
74 private @Nullable ScheduledFuture<?> automowerPollingJob;
75 private long maxQueryFrequencyNanos = TimeUnit.MINUTES.toNanos(1);
77 private Runnable automowerPollingRunnable = () -> {
78 Bridge bridge = getBridge();
79 if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
80 updateAutomowerState();
82 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
86 public AutomowerHandler(Thing thing) {
91 public void handleCommand(ChannelUID channelUID, Command command) {
92 if (command instanceof RefreshType) {
93 refreshChannels(channelUID);
97 private void refreshChannels(ChannelUID channelUID) {
98 updateAutomowerState();
102 public Collection<Class<? extends ThingHandlerService>> getServices() {
103 return Collections.singleton(AutomowerActions.class);
107 public void initialize() {
108 Bridge bridge = getBridge();
109 if (bridge != null) {
110 AutomowerConfiguration currentConfig = getConfigAs(AutomowerConfiguration.class);
111 final String configMowerId = currentConfig.getMowerId();
112 final Integer pollingIntervalS = currentConfig.getPollingInterval();
114 if (configMowerId == null) {
115 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
116 "@text/conf-error-no-mower-id");
117 } else if (pollingIntervalS != null && pollingIntervalS < 1) {
118 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
119 "@text/conf-error-invalid-polling-interval");
121 automowerId.set(configMowerId);
122 startAutomowerPolling(pollingIntervalS);
126 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
131 private AutomowerBridge getAutomowerBridge() {
132 Bridge bridge = getBridge();
133 if (bridge != null) {
134 ThingHandler handler = bridge.getHandler();
135 if (handler instanceof AutomowerBridgeHandler) {
136 AutomowerBridgeHandler bridgeHandler = (AutomowerBridgeHandler) handler;
137 return bridgeHandler.getAutomowerBridge();
144 public void dispose() {
145 if (!automowerId.get().equals(NO_ID)) {
146 stopAutomowerPolling();
147 automowerId.set(NO_ID);
151 private void startAutomowerPolling(@Nullable Integer pollingIntervalS) {
152 if (automowerPollingJob == null) {
153 final long pollingIntervalToUse = pollingIntervalS == null ? DEFAULT_POLLING_INTERVAL_S : pollingIntervalS;
154 automowerPollingJob = scheduler.scheduleWithFixedDelay(automowerPollingRunnable, 1, pollingIntervalToUse,
159 private void stopAutomowerPolling() {
160 if (automowerPollingJob != null) {
161 automowerPollingJob.cancel(true);
162 automowerPollingJob = null;
166 private boolean isValidResult(Mower mower) {
167 return mower.getAttributes() != null && mower.getAttributes().getMetadata() != null
168 && mower.getAttributes().getBattery() != null && mower.getAttributes().getSystem() != null;
171 private boolean isConnected(Mower mower) {
172 return mower.getAttributes() != null && mower.getAttributes().getMetadata() != null
173 && mower.getAttributes().getMetadata().isConnected();
176 private synchronized void updateAutomowerState() {
177 if (System.nanoTime() - lastQueryTimeMs > maxQueryFrequencyNanos) {
178 lastQueryTimeMs = System.nanoTime();
179 String id = automowerId.get();
181 AutomowerBridge automowerBridge = getAutomowerBridge();
182 if (automowerBridge != null) {
183 Mower mower = automowerBridge.getAutomowerStatus(id);
185 if (isValidResult(mower)) {
186 initializeProperties(mower);
188 updateChannelState(mower);
190 if (isConnected(mower)) {
191 updateStatus(ThingStatus.ONLINE);
193 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
194 "@text/comm-error-mower-not-connected-to-cloud");
197 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
198 "@text/comm-error-query-mower-failed");
201 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
202 "@text/conf-error-no-bridge");
204 } catch (AutomowerCommunicationException e) {
205 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
206 "@text/comm-error-query-mower-failed");
207 logger.warn("Unable to query automower status for: {}. Error: {}", id, e.getMessage());
213 * Sends a command to the automower with the default duration of 60min
215 * @param command The command that should be sent. Valid values are: "Start", "ResumeSchedule", "Pause", "Park",
216 * "ParkUntilNextSchedule", "ParkUntilFurtherNotice"
218 public void sendAutomowerCommand(AutomowerCommand command) {
219 sendAutomowerCommand(command, DEFAULT_COMMAND_DURATION_MIN);
223 * Sends a command to the automower with the given duration
225 * @param command The command that should be sent. Valid values are: "Start", "ResumeSchedule", "Pause", "Park",
226 * "ParkUntilNextSchedule", "ParkUntilFurtherNotice"
227 * @param commandDurationMinutes The duration of the command in minutes. This is only evaluated for "Start" and
230 public void sendAutomowerCommand(AutomowerCommand command, long commandDurationMinutes) {
231 String id = automowerId.get();
233 AutomowerBridge automowerBridge = getAutomowerBridge();
234 if (automowerBridge != null) {
235 automowerBridge.sendAutomowerCommand(id, command, commandDurationMinutes);
237 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/conf-error-no-bridge");
239 } catch (AutomowerCommunicationException e) {
240 logger.warn("Unable to send command to automower: {}, Error: {}", id, e.getMessage());
243 updateAutomowerState();
246 private void updateChannelState(Mower mower) {
247 if (isValidResult(mower)) {
248 updateState(CHANNEL_MOWER_NAME, new StringType(mower.getAttributes().getSystem().getName()));
250 updateState(CHANNEL_STATUS_MODE, new StringType(mower.getAttributes().getMower().getMode().name()));
251 updateState(CHANNEL_STATUS_ACTIVITY, new StringType(mower.getAttributes().getMower().getActivity().name()));
252 updateState(CHANNEL_STATUS_STATE, new StringType(mower.getAttributes().getMower().getState().name()));
254 Instant statusTimestamp = Instant.ofEpochMilli(mower.getAttributes().getMetadata().getStatusTimestamp());
255 updateState(CHANNEL_STATUS_LAST_UPDATE,
256 new DateTimeType(ZonedDateTime.ofInstant(statusTimestamp, ZoneId.systemDefault())));
257 updateState(CHANNEL_STATUS_BATTERY, new QuantityType<Dimensionless>(
258 mower.getAttributes().getBattery().getBatteryPercent(), SmartHomeUnits.PERCENT));
260 updateState(CHANNEL_STATUS_ERROR_CODE, new DecimalType(mower.getAttributes().getMower().getErrorCode()));
262 Instant errorCodeTimestamp = Instant.ofEpochMilli(mower.getAttributes().getMower().getErrorCodeTimestamp());
263 updateState(CHANNEL_STATUS_ERROR_TIMESTAMP,
264 new DateTimeType(ZonedDateTime.ofInstant(errorCodeTimestamp, ZoneId.systemDefault())));
269 private void initializeProperties(Mower mower) {
270 Map<String, String> properties = editProperties();
272 properties.put(AutomowerBindingConstants.AUTOMOWER_ID, mower.getId());
274 if (mower.getAttributes() != null && mower.getAttributes().getSystem() != null) {
275 properties.put(AutomowerBindingConstants.AUTOMOWER_SERIAL_NUMBER,
276 mower.getAttributes().getSystem().getSerialNumber());
277 properties.put(AutomowerBindingConstants.AUTOMOWER_MODEL, mower.getAttributes().getSystem().getModel());
278 properties.put(AutomowerBindingConstants.AUTOMOWER_NAME, mower.getAttributes().getSystem().getName());
281 updateProperties(properties);