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