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