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