]> git.basschouten.com Git - openhab-addons.git/blob
674e721fa18d6a268da52ae5350b2afee18ce4fe
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.ZoneId;
19 import java.time.ZonedDateTime;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.Map;
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.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;
56
57 /**
58  * The {@link AutomowerHandler} is responsible for handling commands, which are
59  * sent to one of the channels.
60  *
61  * @author Markus Pfleger - Initial contribution
62  */
63 @NonNullByDefault
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);
69
70     private final Logger logger = LoggerFactory.getLogger(AutomowerHandler.class);
71     private AtomicReference<String> automowerId = new AtomicReference<String>(NO_ID);
72     private long lastQueryTimeMs = 0L;
73
74     private @Nullable ScheduledFuture<?> automowerPollingJob;
75     private long maxQueryFrequencyNanos = TimeUnit.MINUTES.toNanos(1);
76
77     private Runnable automowerPollingRunnable = () -> {
78         Bridge bridge = getBridge();
79         if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
80             updateAutomowerState();
81         } else {
82             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
83         }
84     };
85
86     public AutomowerHandler(Thing thing) {
87         super(thing);
88     }
89
90     @Override
91     public void handleCommand(ChannelUID channelUID, Command command) {
92         if (command instanceof RefreshType) {
93             refreshChannels(channelUID);
94         }
95     }
96
97     private void refreshChannels(ChannelUID channelUID) {
98         updateAutomowerState();
99     }
100
101     @Override
102     public Collection<Class<? extends ThingHandlerService>> getServices() {
103         return Collections.singleton(AutomowerActions.class);
104     }
105
106     @Override
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();
113
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");
120             } else {
121                 automowerId.set(configMowerId);
122                 startAutomowerPolling(pollingIntervalS);
123             }
124
125         } else {
126             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
127         }
128     }
129
130     @Nullable
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();
138             }
139         }
140         return null;
141     }
142
143     @Override
144     public void dispose() {
145         if (!automowerId.get().equals(NO_ID)) {
146             stopAutomowerPolling();
147             automowerId.set(NO_ID);
148         }
149     }
150
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,
155                     TimeUnit.SECONDS);
156         }
157     }
158
159     private void stopAutomowerPolling() {
160         if (automowerPollingJob != null) {
161             automowerPollingJob.cancel(true);
162             automowerPollingJob = null;
163         }
164     }
165
166     private boolean isValidResult(Mower mower) {
167         return mower.getAttributes() != null && mower.getAttributes().getMetadata() != null
168                 && mower.getAttributes().getBattery() != null && mower.getAttributes().getSystem() != null;
169     }
170
171     private boolean isConnected(Mower mower) {
172         return mower.getAttributes() != null && mower.getAttributes().getMetadata() != null
173                 && mower.getAttributes().getMetadata().isConnected();
174     }
175
176     private synchronized void updateAutomowerState() {
177         if (System.nanoTime() - lastQueryTimeMs > maxQueryFrequencyNanos) {
178             lastQueryTimeMs = System.nanoTime();
179             String id = automowerId.get();
180             try {
181                 AutomowerBridge automowerBridge = getAutomowerBridge();
182                 if (automowerBridge != null) {
183                     Mower mower = automowerBridge.getAutomowerStatus(id);
184
185                     if (isValidResult(mower)) {
186                         initializeProperties(mower);
187
188                         updateChannelState(mower);
189
190                         if (isConnected(mower)) {
191                             updateStatus(ThingStatus.ONLINE);
192                         } else {
193                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
194                                     "@text/comm-error-mower-not-connected-to-cloud");
195                         }
196                     } else {
197                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
198                                 "@text/comm-error-query-mower-failed");
199                     }
200                 } else {
201                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
202                             "@text/conf-error-no-bridge");
203                 }
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());
208             }
209         }
210     }
211
212     /**
213      * Sends a command to the automower with the default duration of 60min
214      *
215      * @param command The command that should be sent. Valid values are: "Start", "ResumeSchedule", "Pause", "Park",
216      *            "ParkUntilNextSchedule", "ParkUntilFurtherNotice"
217      */
218     public void sendAutomowerCommand(AutomowerCommand command) {
219         sendAutomowerCommand(command, DEFAULT_COMMAND_DURATION_MIN);
220     }
221
222     /**
223      * Sends a command to the automower with the given duration
224      *
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
228      *            "Park" commands
229      */
230     public void sendAutomowerCommand(AutomowerCommand command, long commandDurationMinutes) {
231         String id = automowerId.get();
232         try {
233             AutomowerBridge automowerBridge = getAutomowerBridge();
234             if (automowerBridge != null) {
235                 automowerBridge.sendAutomowerCommand(id, command, commandDurationMinutes);
236             } else {
237                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/conf-error-no-bridge");
238             }
239         } catch (AutomowerCommunicationException e) {
240             logger.warn("Unable to send command to automower: {}, Error: {}", id, e.getMessage());
241         }
242
243         updateAutomowerState();
244     }
245
246     private void updateChannelState(Mower mower) {
247         if (isValidResult(mower)) {
248             updateState(CHANNEL_MOWER_NAME, new StringType(mower.getAttributes().getSystem().getName()));
249
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()));
253
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));
259
260             updateState(CHANNEL_STATUS_ERROR_CODE, new DecimalType(mower.getAttributes().getMower().getErrorCode()));
261
262             Instant errorCodeTimestamp = Instant.ofEpochMilli(mower.getAttributes().getMower().getErrorCodeTimestamp());
263             updateState(CHANNEL_STATUS_ERROR_TIMESTAMP,
264                     new DateTimeType(ZonedDateTime.ofInstant(errorCodeTimestamp, ZoneId.systemDefault())));
265
266         }
267     }
268
269     private void initializeProperties(Mower mower) {
270         Map<String, String> properties = editProperties();
271
272         properties.put(AutomowerBindingConstants.AUTOMOWER_ID, mower.getId());
273
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());
279         }
280
281         updateProperties(properties);
282     }
283 }