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