]> git.basschouten.com Git - openhab-addons.git/blob
a8b6634adc0ed5734af6ede176e4962c5a2452c2
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.boschindego.internal.handler;
14
15 import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*;
16
17 import java.time.Instant;
18 import java.time.ZonedDateTime;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.eclipse.jetty.client.HttpClient;
25 import org.openhab.binding.boschindego.internal.BoschIndegoTranslationProvider;
26 import org.openhab.binding.boschindego.internal.DeviceStatus;
27 import org.openhab.binding.boschindego.internal.IndegoController;
28 import org.openhab.binding.boschindego.internal.config.BoschIndegoConfiguration;
29 import org.openhab.binding.boschindego.internal.dto.DeviceCommand;
30 import org.openhab.binding.boschindego.internal.dto.response.DeviceStateResponse;
31 import org.openhab.binding.boschindego.internal.dto.response.OperatingDataResponse;
32 import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
33 import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
34 import org.openhab.binding.boschindego.internal.exceptions.IndegoUnreachableException;
35 import org.openhab.core.i18n.TimeZoneProvider;
36 import org.openhab.core.library.types.DateTimeType;
37 import org.openhab.core.library.types.DecimalType;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.library.types.PercentType;
40 import org.openhab.core.library.types.QuantityType;
41 import org.openhab.core.library.types.StringType;
42 import org.openhab.core.library.unit.SIUnits;
43 import org.openhab.core.library.unit.Units;
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.binding.BaseThingHandler;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.RefreshType;
51 import org.openhab.core.types.UnDefType;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 /**
56  * The {@link BoschIndegoHandler} is responsible for handling commands, which are
57  * sent to one of the channels.
58  *
59  * @author Jonas Fleck - Initial contribution
60  * @author Jacob Laursen - Refactoring, bugfixing and removal of dependency towards abandoned library
61  */
62 @NonNullByDefault
63 public class BoschIndegoHandler extends BaseThingHandler {
64
65     private final Logger logger = LoggerFactory.getLogger(BoschIndegoHandler.class);
66     private final HttpClient httpClient;
67     private final BoschIndegoTranslationProvider translationProvider;
68     private final TimeZoneProvider timeZoneProvider;
69
70     private @NonNullByDefault({}) IndegoController controller;
71     private @Nullable ScheduledFuture<?> statePollFuture;
72     private @Nullable ScheduledFuture<?> cuttingTimeMapPollFuture;
73     private boolean propertiesInitialized;
74     private int previousStateCode;
75
76     public BoschIndegoHandler(Thing thing, HttpClient httpClient, BoschIndegoTranslationProvider translationProvider,
77             TimeZoneProvider timeZoneProvider) {
78         super(thing);
79         this.httpClient = httpClient;
80         this.translationProvider = translationProvider;
81         this.timeZoneProvider = timeZoneProvider;
82     }
83
84     @Override
85     public void initialize() {
86         logger.debug("Initializing Indego handler");
87         BoschIndegoConfiguration config = getConfigAs(BoschIndegoConfiguration.class);
88         String username = config.username;
89         String password = config.password;
90
91         if (username == null || username.isBlank()) {
92             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
93                     "@text/offline.conf-error.missing-username");
94             return;
95         }
96         if (password == null || password.isBlank()) {
97             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
98                     "@text/offline.conf-error.missing-password");
99             return;
100         }
101
102         controller = new IndegoController(httpClient, username, password);
103
104         updateStatus(ThingStatus.UNKNOWN);
105         this.statePollFuture = scheduler.scheduleWithFixedDelay(this::refreshStateAndOperatingDataWithExceptionHandling,
106                 0, config.refresh, TimeUnit.SECONDS);
107         this.cuttingTimeMapPollFuture = scheduler.scheduleWithFixedDelay(
108                 this::refreshCuttingTimesAndMapWithExceptionHandling, 0, config.cuttingTimeMapRefresh,
109                 TimeUnit.MINUTES);
110     }
111
112     @Override
113     public void dispose() {
114         logger.debug("Disposing Indego handler");
115         ScheduledFuture<?> pollFuture = this.statePollFuture;
116         if (pollFuture != null) {
117             pollFuture.cancel(true);
118         }
119         this.statePollFuture = null;
120         pollFuture = this.cuttingTimeMapPollFuture;
121         if (pollFuture != null) {
122             pollFuture.cancel(true);
123         }
124         this.cuttingTimeMapPollFuture = null;
125
126         scheduler.execute(() -> {
127             try {
128                 controller.deauthenticate();
129             } catch (IndegoException e) {
130                 logger.debug("Deauthentication failed", e);
131             }
132         });
133     }
134
135     @Override
136     public void handleCommand(ChannelUID channelUID, Command command) {
137         logger.debug("handleCommand {} for channel {}", command, channelUID);
138         try {
139             if (command == RefreshType.REFRESH) {
140                 handleRefreshCommand(channelUID.getId());
141                 return;
142             }
143             if (command instanceof DecimalType && channelUID.getId().equals(STATE)) {
144                 sendCommand(((DecimalType) command).intValue());
145             }
146         } catch (IndegoAuthenticationException e) {
147             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
148                     "@text/offline.comm-error.authentication-failure");
149         } catch (IndegoUnreachableException e) {
150             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
151                     "@text/offline.comm-error.unreachable");
152         } catch (IndegoException e) {
153             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
154         }
155     }
156
157     private void handleRefreshCommand(String channelId)
158             throws IndegoAuthenticationException, IndegoUnreachableException, IndegoException {
159         switch (channelId) {
160             case STATE:
161             case TEXTUAL_STATE:
162             case MOWED:
163             case ERRORCODE:
164             case STATECODE:
165             case READY:
166                 refreshState();
167                 break;
168             case LAST_CUTTING:
169             case NEXT_CUTTING:
170                 refreshCuttingTimes();
171                 break;
172             case BATTERY_LEVEL:
173             case LOW_BATTERY:
174             case BATTERY_VOLTAGE:
175             case BATTERY_TEMPERATURE:
176             case GARDEN_SIZE:
177                 refreshOperatingData();
178                 break;
179             case GARDEN_MAP:
180                 refreshMap();
181                 break;
182         }
183     }
184
185     private void sendCommand(int commandInt) throws IndegoException {
186         DeviceCommand command;
187         switch (commandInt) {
188             case 1:
189                 command = DeviceCommand.MOW;
190                 break;
191             case 2:
192                 command = DeviceCommand.RETURN;
193                 break;
194             case 3:
195                 command = DeviceCommand.PAUSE;
196                 break;
197             default:
198                 logger.warn("Invalid command {}", commandInt);
199                 return;
200         }
201
202         DeviceStateResponse state = controller.getState();
203         DeviceStatus deviceStatus = DeviceStatus.fromCode(state.state);
204         if (!verifyCommand(command, deviceStatus, state.error)) {
205             return;
206         }
207         logger.debug("Sending command {}", command);
208         updateState(TEXTUAL_STATE, UnDefType.UNDEF);
209         controller.sendCommand(command);
210         refreshState();
211     }
212
213     private void refreshStateAndOperatingDataWithExceptionHandling() {
214         try {
215             refreshState();
216             refreshOperatingData();
217         } catch (IndegoAuthenticationException e) {
218             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
219                     "@text/offline.comm-error.authentication-failure");
220         } catch (IndegoUnreachableException e) {
221             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
222                     "@text/offline.comm-error.unreachable");
223         } catch (IndegoException e) {
224             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
225         }
226     }
227
228     private void refreshState() throws IndegoAuthenticationException, IndegoException {
229         if (!propertiesInitialized) {
230             getThing().setProperty(Thing.PROPERTY_SERIAL_NUMBER, controller.getSerialNumber());
231             propertiesInitialized = true;
232         }
233
234         DeviceStateResponse state = controller.getState();
235         updateState(state);
236
237         // When state code changed, refresh cutting times immediately.
238         if (state.state != previousStateCode) {
239             refreshCuttingTimes();
240             previousStateCode = state.state;
241         }
242     }
243
244     private void refreshOperatingData()
245             throws IndegoAuthenticationException, IndegoUnreachableException, IndegoException {
246         updateOperatingData(controller.getOperatingData());
247         updateStatus(ThingStatus.ONLINE);
248     }
249
250     private void refreshCuttingTimes() throws IndegoAuthenticationException, IndegoException {
251         if (isLinked(LAST_CUTTING)) {
252             Instant lastCutting = controller.getPredictiveLastCutting();
253             if (lastCutting != null) {
254                 updateState(LAST_CUTTING,
255                         new DateTimeType(ZonedDateTime.ofInstant(lastCutting, timeZoneProvider.getTimeZone())));
256             } else {
257                 updateState(LAST_CUTTING, UnDefType.UNDEF);
258             }
259         }
260
261         if (isLinked(NEXT_CUTTING)) {
262             Instant nextCutting = controller.getPredictiveNextCutting();
263             if (nextCutting != null) {
264                 updateState(NEXT_CUTTING,
265                         new DateTimeType(ZonedDateTime.ofInstant(nextCutting, timeZoneProvider.getTimeZone())));
266             } else {
267                 updateState(NEXT_CUTTING, UnDefType.UNDEF);
268             }
269         }
270     }
271
272     private void refreshCuttingTimesAndMapWithExceptionHandling() {
273         try {
274             refreshCuttingTimes();
275             refreshMap();
276         } catch (IndegoAuthenticationException e) {
277             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
278                     "@text/offline.comm-error.authentication-failure");
279         } catch (IndegoException e) {
280             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
281         }
282     }
283
284     private void refreshMap() throws IndegoAuthenticationException, IndegoException {
285         if (isLinked(GARDEN_MAP)) {
286             updateState(GARDEN_MAP, controller.getMap());
287         }
288     }
289
290     private void updateState(DeviceStateResponse state) {
291         DeviceStatus deviceStatus = DeviceStatus.fromCode(state.state);
292         int status = getStatusFromCommand(deviceStatus.getAssociatedCommand());
293         int mowed = state.mowed;
294         int error = state.error;
295         int statecode = state.state;
296         boolean ready = isReadyToMow(deviceStatus, state.error);
297
298         updateState(STATECODE, new DecimalType(statecode));
299         updateState(READY, new DecimalType(ready ? 1 : 0));
300         updateState(ERRORCODE, new DecimalType(error));
301         updateState(MOWED, new PercentType(mowed));
302         updateState(STATE, new DecimalType(status));
303         updateState(TEXTUAL_STATE, new StringType(deviceStatus.getMessage(translationProvider)));
304     }
305
306     private void updateOperatingData(OperatingDataResponse operatingData) {
307         updateState(BATTERY_VOLTAGE, new QuantityType<>(operatingData.battery.voltage, Units.VOLT));
308         updateState(BATTERY_LEVEL, new DecimalType(operatingData.battery.percent));
309         updateState(LOW_BATTERY, OnOffType.from(operatingData.battery.percent < 20));
310         updateState(BATTERY_TEMPERATURE, new QuantityType<>(operatingData.battery.batteryTemperature, SIUnits.CELSIUS));
311         updateState(GARDEN_SIZE, new QuantityType<>(operatingData.garden.size, SIUnits.SQUARE_METRE));
312     }
313
314     private boolean isReadyToMow(DeviceStatus deviceStatus, int error) {
315         return deviceStatus.isReadyToMow() && error == 0;
316     }
317
318     private boolean verifyCommand(DeviceCommand command, DeviceStatus deviceStatus, int errorCode) {
319         // Mower reported an error
320         if (errorCode != 0) {
321             logger.error("The mower reported an error.");
322             return false;
323         }
324
325         // Command is equal to current state
326         if (command == deviceStatus.getAssociatedCommand()) {
327             logger.debug("Command is equal to state");
328             return false;
329         }
330         // Can't pause while the mower is docked
331         if (command == DeviceCommand.PAUSE && deviceStatus.getAssociatedCommand() == DeviceCommand.RETURN) {
332             logger.debug("Can't pause the mower while it's docked or docking");
333             return false;
334         }
335         // Command means "MOW" but mower is not ready
336         if (command == DeviceCommand.MOW && !isReadyToMow(deviceStatus, errorCode)) {
337             logger.debug("The mower is not ready to mow at the moment");
338             return false;
339         }
340         return true;
341     }
342
343     private int getStatusFromCommand(@Nullable DeviceCommand command) {
344         if (command == null) {
345             return 0;
346         }
347         int status;
348         switch (command) {
349             case MOW:
350                 status = 1;
351                 break;
352             case RETURN:
353                 status = 2;
354                 break;
355             case PAUSE:
356                 status = 3;
357                 break;
358             default:
359                 status = 0;
360         }
361         return status;
362     }
363 }