]> git.basschouten.com Git - openhab-addons.git/blob
0dec921f0763db08e516ee6b88a56ceb6e018f8c
[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
126     @Override
127     public void handleCommand(ChannelUID channelUID, Command command) {
128         logger.debug("handleCommand {} for channel {}", command, channelUID);
129         try {
130             if (command == RefreshType.REFRESH) {
131                 handleRefreshCommand(channelUID.getId());
132                 return;
133             }
134             if (command instanceof DecimalType && channelUID.getId().equals(STATE)) {
135                 sendCommand(((DecimalType) command).intValue());
136             }
137         } catch (IndegoAuthenticationException e) {
138             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
139                     "@text/offline.comm-error.authentication-failure");
140         } catch (IndegoException e) {
141             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
142         }
143     }
144
145     private void handleRefreshCommand(String channelId) throws IndegoAuthenticationException, IndegoException {
146         switch (channelId) {
147             case STATE:
148             case TEXTUAL_STATE:
149             case MOWED:
150             case ERRORCODE:
151             case STATECODE:
152             case READY:
153                 refreshState();
154                 break;
155             case LAST_CUTTING:
156             case NEXT_CUTTING:
157                 refreshCuttingTimes();
158                 break;
159             case BATTERY_LEVEL:
160             case LOW_BATTERY:
161             case BATTERY_VOLTAGE:
162             case BATTERY_TEMPERATURE:
163             case GARDEN_SIZE:
164                 refreshOperatingData();
165                 break;
166             case GARDEN_MAP:
167                 refreshMap();
168                 break;
169         }
170     }
171
172     private void sendCommand(int commandInt) throws IndegoException {
173         DeviceCommand command;
174         switch (commandInt) {
175             case 1:
176                 command = DeviceCommand.MOW;
177                 break;
178             case 2:
179                 command = DeviceCommand.RETURN;
180                 break;
181             case 3:
182                 command = DeviceCommand.PAUSE;
183                 break;
184             default:
185                 logger.warn("Invalid command {}", commandInt);
186                 return;
187         }
188
189         DeviceStateResponse state = controller.getState();
190         DeviceStatus deviceStatus = DeviceStatus.fromCode(state.state);
191         if (!verifyCommand(command, deviceStatus, state.error)) {
192             return;
193         }
194         logger.debug("Sending command {}", command);
195         updateState(TEXTUAL_STATE, UnDefType.UNDEF);
196         controller.sendCommand(command);
197         refreshState();
198     }
199
200     private void refreshStateAndOperatingDataWithExceptionHandling() {
201         try {
202             refreshState();
203             refreshOperatingData();
204         } catch (IndegoAuthenticationException e) {
205             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
206                     "@text/offline.comm-error.authentication-failure");
207         } catch (IndegoException e) {
208             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
209         }
210     }
211
212     private void refreshState() throws IndegoAuthenticationException, IndegoException {
213         if (!propertiesInitialized) {
214             getThing().setProperty(Thing.PROPERTY_SERIAL_NUMBER, controller.getSerialNumber());
215             propertiesInitialized = true;
216         }
217
218         DeviceStateResponse state = controller.getState();
219         updateState(state);
220
221         // When state code changed, refresh cutting times immediately.
222         if (state.state != previousStateCode) {
223             refreshCuttingTimes();
224             previousStateCode = state.state;
225         }
226     }
227
228     private void refreshOperatingData() throws IndegoAuthenticationException, IndegoException {
229         updateOperatingData(controller.getOperatingData());
230         updateStatus(ThingStatus.ONLINE);
231     }
232
233     private void refreshCuttingTimes() throws IndegoAuthenticationException, IndegoException {
234         if (isLinked(LAST_CUTTING)) {
235             Instant lastCutting = controller.getPredictiveLastCutting();
236             if (lastCutting != null) {
237                 updateState(LAST_CUTTING,
238                         new DateTimeType(ZonedDateTime.ofInstant(lastCutting, timeZoneProvider.getTimeZone())));
239             } else {
240                 updateState(LAST_CUTTING, UnDefType.UNDEF);
241             }
242         }
243
244         if (isLinked(NEXT_CUTTING)) {
245             Instant nextCutting = controller.getPredictiveNextCutting();
246             if (nextCutting != null) {
247                 updateState(NEXT_CUTTING,
248                         new DateTimeType(ZonedDateTime.ofInstant(nextCutting, timeZoneProvider.getTimeZone())));
249             } else {
250                 updateState(NEXT_CUTTING, UnDefType.UNDEF);
251             }
252         }
253     }
254
255     private void refreshCuttingTimesAndMapWithExceptionHandling() {
256         try {
257             refreshCuttingTimes();
258             refreshMap();
259         } catch (IndegoAuthenticationException e) {
260             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
261                     "@text/offline.comm-error.authentication-failure");
262         } catch (IndegoException e) {
263             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
264         }
265     }
266
267     private void refreshMap() throws IndegoAuthenticationException, IndegoException {
268         if (isLinked(GARDEN_MAP)) {
269             updateState(GARDEN_MAP, controller.getMap());
270         }
271     }
272
273     private void updateState(DeviceStateResponse state) {
274         DeviceStatus deviceStatus = DeviceStatus.fromCode(state.state);
275         int status = getStatusFromCommand(deviceStatus.getAssociatedCommand());
276         int mowed = state.mowed;
277         int error = state.error;
278         int statecode = state.state;
279         boolean ready = isReadyToMow(deviceStatus, state.error);
280
281         updateState(STATECODE, new DecimalType(statecode));
282         updateState(READY, new DecimalType(ready ? 1 : 0));
283         updateState(ERRORCODE, new DecimalType(error));
284         updateState(MOWED, new PercentType(mowed));
285         updateState(STATE, new DecimalType(status));
286         updateState(TEXTUAL_STATE, new StringType(deviceStatus.getMessage(translationProvider)));
287     }
288
289     private void updateOperatingData(OperatingDataResponse operatingData) {
290         updateState(BATTERY_VOLTAGE, new QuantityType<>(operatingData.battery.voltage, Units.VOLT));
291         updateState(BATTERY_LEVEL, new DecimalType(operatingData.battery.percent));
292         updateState(LOW_BATTERY, OnOffType.from(operatingData.battery.percent < 20));
293         updateState(BATTERY_TEMPERATURE, new QuantityType<>(operatingData.battery.batteryTemperature, SIUnits.CELSIUS));
294         updateState(GARDEN_SIZE, new QuantityType<>(operatingData.garden.size, SIUnits.SQUARE_METRE));
295     }
296
297     private boolean isReadyToMow(DeviceStatus deviceStatus, int error) {
298         return deviceStatus.isReadyToMow() && error == 0;
299     }
300
301     private boolean verifyCommand(DeviceCommand command, DeviceStatus deviceStatus, int errorCode) {
302         // Mower reported an error
303         if (errorCode != 0) {
304             logger.error("The mower reported an error.");
305             return false;
306         }
307
308         // Command is equal to current state
309         if (command == deviceStatus.getAssociatedCommand()) {
310             logger.debug("Command is equal to state");
311             return false;
312         }
313         // Can't pause while the mower is docked
314         if (command == DeviceCommand.PAUSE && deviceStatus.getAssociatedCommand() == DeviceCommand.RETURN) {
315             logger.debug("Can't pause the mower while it's docked or docking");
316             return false;
317         }
318         // Command means "MOW" but mower is not ready
319         if (command == DeviceCommand.MOW && !isReadyToMow(deviceStatus, errorCode)) {
320             logger.debug("The mower is not ready to mow at the moment");
321             return false;
322         }
323         return true;
324     }
325
326     private int getStatusFromCommand(@Nullable DeviceCommand command) {
327         if (command == null) {
328             return 0;
329         }
330         int status;
331         switch (command) {
332             case MOW:
333                 status = 1;
334                 break;
335             case RETURN:
336                 status = 2;
337                 break;
338             case PAUSE:
339                 status = 3;
340                 break;
341             default:
342                 status = 0;
343         }
344         return status;
345     }
346 }