]> git.basschouten.com Git - openhab-addons.git/blob
de705216ade68f234c65db299afb7ede80635d63
[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.automower.internal.things;
14
15 import static org.openhab.binding.automower.internal.AutomowerBindingConstants.*;
16
17 import java.time.*;
18 import java.util.Collection;
19 import java.util.Collections;
20 import java.util.Map;
21 import java.util.Optional;
22 import java.util.Set;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.atomic.AtomicReference;
26
27 import javax.measure.quantity.Dimensionless;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.automower.internal.AutomowerBindingConstants;
32 import org.openhab.binding.automower.internal.actions.AutomowerActions;
33 import org.openhab.binding.automower.internal.bridge.AutomowerBridge;
34 import org.openhab.binding.automower.internal.bridge.AutomowerBridgeHandler;
35 import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.Mower;
36 import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.RestrictedReason;
37 import org.openhab.binding.automower.internal.rest.api.automowerconnect.dto.State;
38 import org.openhab.binding.automower.internal.rest.exceptions.AutomowerCommunicationException;
39 import org.openhab.core.i18n.TimeZoneProvider;
40 import org.openhab.core.library.types.DateTimeType;
41 import org.openhab.core.library.types.DecimalType;
42 import org.openhab.core.library.types.QuantityType;
43 import org.openhab.core.library.types.StringType;
44 import org.openhab.core.library.unit.Units;
45 import org.openhab.core.thing.Bridge;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.ThingTypeUID;
51 import org.openhab.core.thing.binding.BaseThingHandler;
52 import org.openhab.core.thing.binding.ThingHandler;
53 import org.openhab.core.thing.binding.ThingHandlerService;
54 import org.openhab.core.types.Command;
55 import org.openhab.core.types.RefreshType;
56 import org.openhab.core.types.Type;
57 import org.openhab.core.types.UnDefType;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
60
61 import com.google.gson.Gson;
62
63 /**
64  * The {@link AutomowerHandler} is responsible for handling commands, which are
65  * sent to one of the channels.
66  *
67  * @author Markus Pfleger - Initial contribution
68  * @author Marcin Czeczko - Added support for planner & calendar data
69  */
70 @NonNullByDefault
71 public class AutomowerHandler extends BaseThingHandler {
72     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_AUTOMOWER);
73     private static final String NO_ID = "NO_ID";
74     private static final long DEFAULT_COMMAND_DURATION_MIN = 60;
75     private static final long DEFAULT_POLLING_INTERVAL_S = TimeUnit.MINUTES.toSeconds(10);
76
77     private final Logger logger = LoggerFactory.getLogger(AutomowerHandler.class);
78     private final TimeZoneProvider timeZoneProvider;
79
80     private AtomicReference<String> automowerId = new AtomicReference<String>(NO_ID);
81     private long lastQueryTimeMs = 0L;
82
83     private @Nullable ScheduledFuture<?> automowerPollingJob;
84     private long maxQueryFrequencyNanos = TimeUnit.MINUTES.toNanos(1);
85
86     private @Nullable Mower mowerState;
87
88     private Gson gson = new Gson();
89
90     private Runnable automowerPollingRunnable = () -> {
91         Bridge bridge = getBridge();
92         if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
93             updateAutomowerState();
94         } else {
95             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
96         }
97     };
98
99     public AutomowerHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
100         super(thing);
101         this.timeZoneProvider = timeZoneProvider;
102     }
103
104     @Override
105     public void handleCommand(ChannelUID channelUID, Command command) {
106         if (RefreshType.REFRESH == command) {
107             logger.debug("Refreshing channel '{}'", channelUID);
108             refreshChannels(channelUID);
109         } else {
110             AutomowerCommand.fromChannelUID(channelUID).ifPresent(commandName -> {
111                 logger.debug("Sending command '{}'", commandName);
112                 getCommandValue(command).ifPresentOrElse(duration -> sendAutomowerCommand(commandName, duration),
113                         () -> sendAutomowerCommand(commandName));
114             });
115         }
116     }
117
118     private Optional<Integer> getCommandValue(Type type) {
119         if (type instanceof DecimalType) {
120             return Optional.of(((DecimalType) type).intValue());
121         }
122         return Optional.empty();
123     }
124
125     private void refreshChannels(ChannelUID channelUID) {
126         updateAutomowerState();
127     }
128
129     @Override
130     public Collection<Class<? extends ThingHandlerService>> getServices() {
131         return Collections.singleton(AutomowerActions.class);
132     }
133
134     @Override
135     public void initialize() {
136         Bridge bridge = getBridge();
137         if (bridge != null) {
138             AutomowerConfiguration currentConfig = getConfigAs(AutomowerConfiguration.class);
139             final String configMowerId = currentConfig.getMowerId();
140             final Integer pollingIntervalS = currentConfig.getPollingInterval();
141
142             if (configMowerId == null) {
143                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
144                         "@text/conf-error-no-mower-id");
145             } else if (pollingIntervalS != null && pollingIntervalS < 1) {
146                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
147                         "@text/conf-error-invalid-polling-interval");
148             } else {
149                 automowerId.set(configMowerId);
150                 startAutomowerPolling(pollingIntervalS);
151             }
152         } else {
153             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
154         }
155     }
156
157     @Nullable
158     private AutomowerBridge getAutomowerBridge() {
159         Bridge bridge = getBridge();
160         if (bridge != null) {
161             ThingHandler handler = bridge.getHandler();
162             if (handler instanceof AutomowerBridgeHandler) {
163                 AutomowerBridgeHandler bridgeHandler = (AutomowerBridgeHandler) handler;
164                 return bridgeHandler.getAutomowerBridge();
165             }
166         }
167         return null;
168     }
169
170     @Override
171     public void dispose() {
172         if (!automowerId.get().equals(NO_ID)) {
173             stopAutomowerPolling();
174             automowerId.set(NO_ID);
175         }
176     }
177
178     private void startAutomowerPolling(@Nullable Integer pollingIntervalS) {
179         if (automowerPollingJob == null) {
180             final long pollingIntervalToUse = pollingIntervalS == null ? DEFAULT_POLLING_INTERVAL_S : pollingIntervalS;
181             automowerPollingJob = scheduler.scheduleWithFixedDelay(automowerPollingRunnable, 1, pollingIntervalToUse,
182                     TimeUnit.SECONDS);
183         }
184     }
185
186     private void stopAutomowerPolling() {
187         if (automowerPollingJob != null) {
188             automowerPollingJob.cancel(true);
189             automowerPollingJob = null;
190         }
191     }
192
193     private boolean isValidResult(@Nullable Mower mower) {
194         return mower != null && mower.getAttributes() != null && mower.getAttributes().getMetadata() != null
195                 && mower.getAttributes().getBattery() != null && mower.getAttributes().getSystem() != null;
196     }
197
198     private boolean isConnected(@Nullable Mower mower) {
199         return mower != null && mower.getAttributes() != null && mower.getAttributes().getMetadata() != null
200                 && mower.getAttributes().getMetadata().isConnected();
201     }
202
203     private synchronized void updateAutomowerState() {
204         String id = automowerId.get();
205         try {
206             AutomowerBridge automowerBridge = getAutomowerBridge();
207             if (automowerBridge != null) {
208                 if (mowerState == null || (System.nanoTime() - lastQueryTimeMs > maxQueryFrequencyNanos)) {
209                     lastQueryTimeMs = System.nanoTime();
210                     mowerState = automowerBridge.getAutomowerStatus(id);
211                 }
212                 if (isValidResult(mowerState)) {
213                     initializeProperties(mowerState);
214
215                     updateChannelState(mowerState);
216
217                     if (isConnected(mowerState)) {
218                         updateStatus(ThingStatus.ONLINE);
219                     } else {
220                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
221                                 "@text/comm-error-mower-not-connected-to-cloud");
222                     }
223                 } else {
224                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
225                             "@text/comm-error-query-mower-failed");
226                 }
227             } else {
228                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/conf-error-no-bridge");
229             }
230         } catch (AutomowerCommunicationException e) {
231             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
232                     "@text/comm-error-query-mower-failed");
233             logger.warn("Unable to query automower status for:  {}. Error: {}", id, e.getMessage());
234         }
235     }
236
237     /**
238      * Sends a command to the automower with the default duration of 60min
239      *
240      * @param command The command that should be sent. Valid values are: "Start", "ResumeSchedule", "Pause", "Park",
241      *            "ParkUntilNextSchedule", "ParkUntilFurtherNotice"
242      */
243     public void sendAutomowerCommand(AutomowerCommand command) {
244         sendAutomowerCommand(command, DEFAULT_COMMAND_DURATION_MIN);
245     }
246
247     /**
248      * Sends a command to the automower with the given duration
249      *
250      * @param command The command that should be sent. Valid values are: "Start", "ResumeSchedule", "Pause", "Park",
251      *            "ParkUntilNextSchedule", "ParkUntilFurtherNotice"
252      * @param commandDurationMinutes The duration of the command in minutes. This is only evaluated for "Start" and
253      *            "Park" commands
254      */
255     public void sendAutomowerCommand(AutomowerCommand command, long commandDurationMinutes) {
256         logger.debug("Sending command '{} {}'", command.getCommand(), commandDurationMinutes);
257         String id = automowerId.get();
258         try {
259             AutomowerBridge automowerBridge = getAutomowerBridge();
260             if (automowerBridge != null) {
261                 automowerBridge.sendAutomowerCommand(id, command, commandDurationMinutes);
262             } else {
263                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/conf-error-no-bridge");
264             }
265         } catch (AutomowerCommunicationException e) {
266             logger.warn("Unable to send command to automower: {}, Error: {}", id, e.getMessage());
267         }
268
269         updateAutomowerState();
270     }
271
272     private String restrictedState(RestrictedReason reason) {
273         return "RESTRICTED_" + reason.name();
274     }
275
276     private void updateChannelState(@Nullable Mower mower) {
277         if (isValidResult(mower)) {
278             updateState(CHANNEL_STATUS_NAME, new StringType(mower.getAttributes().getSystem().getName()));
279             updateState(CHANNEL_STATUS_MODE, new StringType(mower.getAttributes().getMower().getMode().name()));
280             updateState(CHANNEL_STATUS_ACTIVITY, new StringType(mower.getAttributes().getMower().getActivity().name()));
281
282             if (mower.getAttributes().getMower().getState() != State.RESTRICTED) {
283                 updateState(CHANNEL_STATUS_STATE, new StringType(mower.getAttributes().getMower().getState().name()));
284             } else {
285                 updateState(CHANNEL_STATUS_STATE,
286                         new StringType(restrictedState(mower.getAttributes().getPlanner().getRestrictedReason())));
287             }
288
289             updateState(CHANNEL_STATUS_LAST_UPDATE,
290                     new DateTimeType(toZonedDateTime(mower.getAttributes().getMetadata().getStatusTimestamp())));
291             updateState(CHANNEL_STATUS_BATTERY, new QuantityType<Dimensionless>(
292                     mower.getAttributes().getBattery().getBatteryPercent(), Units.PERCENT));
293
294             updateState(CHANNEL_STATUS_ERROR_CODE, new DecimalType(mower.getAttributes().getMower().getErrorCode()));
295
296             long errorCodeTimestamp = mower.getAttributes().getMower().getErrorCodeTimestamp();
297             if (errorCodeTimestamp == 0L) {
298                 updateState(CHANNEL_STATUS_ERROR_TIMESTAMP, UnDefType.NULL);
299             } else {
300                 updateState(CHANNEL_STATUS_ERROR_TIMESTAMP, new DateTimeType(toZonedDateTime(errorCodeTimestamp)));
301             }
302
303             long nextStartTimestamp = mower.getAttributes().getPlanner().getNextStartTimestamp();
304             // If next start timestamp is 0 it means the mower should start now, so using current timestamp
305             if (nextStartTimestamp == 0L) {
306                 updateState(CHANNEL_PLANNER_NEXT_START, UnDefType.NULL);
307             } else {
308                 updateState(CHANNEL_PLANNER_NEXT_START, new DateTimeType(toZonedDateTime(nextStartTimestamp)));
309             }
310             updateState(CHANNEL_PLANNER_OVERRIDE_ACTION,
311                     new StringType(mower.getAttributes().getPlanner().getOverride().getAction()));
312
313             updateState(CHANNEL_CALENDAR_TASKS,
314                     new StringType(gson.toJson(mower.getAttributes().getCalendar().getTasks())));
315         }
316     }
317
318     private void initializeProperties(@Nullable Mower mower) {
319         Map<String, String> properties = editProperties();
320
321         properties.put(AutomowerBindingConstants.AUTOMOWER_ID, mower.getId());
322
323         if (mower.getAttributes() != null && mower.getAttributes().getSystem() != null) {
324             properties.put(AutomowerBindingConstants.AUTOMOWER_SERIAL_NUMBER,
325                     mower.getAttributes().getSystem().getSerialNumber());
326             properties.put(AutomowerBindingConstants.AUTOMOWER_MODEL, mower.getAttributes().getSystem().getModel());
327             properties.put(AutomowerBindingConstants.AUTOMOWER_NAME, mower.getAttributes().getSystem().getName());
328         }
329
330         updateProperties(properties);
331     }
332
333     /**
334      * Converts timestamp returned by the Automower API into local time-zone.
335      * Timestamp returned by the API doesn't have offset and it always in the current time zone - it can be treated as
336      * UTC.
337      * Method builds a ZonedDateTime with same hour value but in the current system timezone.
338      * 
339      * @param timestamp - Automower API timestamp
340      * @return ZonedDateTime in system timezone
341      */
342     private ZonedDateTime toZonedDateTime(long timestamp) {
343         Instant timestampInstant = Instant.ofEpochMilli(timestamp);
344         return ZonedDateTime.ofInstant(timestampInstant, timeZoneProvider.getTimeZone());
345     }
346 }