]> git.basschouten.com Git - openhab-addons.git/blob
51970b933b840c20d07d91fdf1ff9c33ca76368d
[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.robonect.internal.handler;
14
15 import static org.openhab.binding.robonect.internal.RobonectBindingConstants.*;
16
17 import java.time.DateTimeException;
18 import java.time.Instant;
19 import java.time.ZoneId;
20 import java.time.ZoneOffset;
21 import java.time.ZonedDateTime;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26
27 import org.eclipse.jetty.client.HttpClient;
28 import org.openhab.binding.robonect.internal.RobonectClient;
29 import org.openhab.binding.robonect.internal.RobonectCommunicationException;
30 import org.openhab.binding.robonect.internal.RobonectEndpoint;
31 import org.openhab.binding.robonect.internal.config.JobChannelConfig;
32 import org.openhab.binding.robonect.internal.config.RobonectConfig;
33 import org.openhab.binding.robonect.internal.model.ErrorEntry;
34 import org.openhab.binding.robonect.internal.model.ErrorList;
35 import org.openhab.binding.robonect.internal.model.MowerInfo;
36 import org.openhab.binding.robonect.internal.model.Name;
37 import org.openhab.binding.robonect.internal.model.RobonectAnswer;
38 import org.openhab.binding.robonect.internal.model.VersionInfo;
39 import org.openhab.binding.robonect.internal.model.cmd.ModeCommand;
40 import org.openhab.core.i18n.TimeZoneProvider;
41 import org.openhab.core.library.types.DateTimeType;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.OnOffType;
44 import org.openhab.core.library.types.QuantityType;
45 import org.openhab.core.library.types.StringType;
46 import org.openhab.core.library.unit.SIUnits;
47 import org.openhab.core.library.unit.SmartHomeUnits;
48 import org.openhab.core.thing.ChannelUID;
49 import org.openhab.core.thing.Thing;
50 import org.openhab.core.thing.ThingStatus;
51 import org.openhab.core.thing.ThingStatusDetail;
52 import org.openhab.core.thing.binding.BaseThingHandler;
53 import org.openhab.core.types.Command;
54 import org.openhab.core.types.RefreshType;
55 import org.openhab.core.types.State;
56 import org.openhab.core.types.UnDefType;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
59
60 import com.google.gson.JsonSyntaxException;
61
62 /**
63  * The {@link RobonectHandler} is responsible for handling commands, which are
64  * sent to one of the channels.
65  *
66  * The channels are periodically updated by polling the mower via HTTP in a separate thread.
67  *
68  * @author Marco Meyer - Initial contribution
69  */
70 public class RobonectHandler extends BaseThingHandler {
71
72     private final Logger logger = LoggerFactory.getLogger(RobonectHandler.class);
73
74     private ScheduledFuture<?> pollingJob;
75
76     private HttpClient httpClient;
77     private TimeZoneProvider timeZoneProvider;
78
79     private ZoneId timeZone;
80
81     private RobonectClient robonectClient;
82
83     public RobonectHandler(Thing thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
84         super(thing);
85         this.httpClient = httpClient;
86         this.timeZoneProvider = timeZoneProvider;
87     }
88
89     @Override
90     public void handleCommand(ChannelUID channelUID, Command command) {
91         try {
92             if (command instanceof RefreshType) {
93                 refreshChannels(channelUID);
94             } else {
95                 sendCommand(channelUID, command);
96             }
97             updateStatus(ThingStatus.ONLINE);
98         } catch (RobonectCommunicationException rce) {
99             logger.debug("Failed to communicate with the mower. Taking it offline.", rce);
100             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, rce.getMessage());
101         }
102     }
103
104     private void sendCommand(ChannelUID channelUID, Command command) {
105         switch (channelUID.getId()) {
106             case CHANNEL_MOWER_NAME:
107                 if (command instanceof StringType) {
108                     updateName((StringType) command);
109                 } else {
110                     logger.debug("Got name update of type {} but StringType is expected.",
111                             command.getClass().getName());
112                 }
113                 break;
114
115             case CHANNEL_STATUS_MODE:
116                 if (command instanceof StringType) {
117                     setMowerMode(command);
118                 } else {
119                     logger.debug("Got job remote start update of type {} but StringType is expected.",
120                             command.getClass().getName());
121                 }
122                 break;
123
124             case CHANNEL_MOWER_START:
125                 if (command instanceof OnOffType) {
126                     handleStartStop((OnOffType) command);
127                 } else {
128                     logger.debug("Got stopped update of type {} but OnOffType is expected.",
129                             command.getClass().getName());
130                 }
131                 break;
132
133             case CHANNEL_JOB:
134                 if (command instanceof OnOffType) {
135                     handleJobCommand(channelUID, command);
136                 } else {
137                     logger.debug("Got job update of type {} but OnOffType is expected.", command.getClass().getName());
138                 }
139                 break;
140         }
141     }
142
143     private void handleJobCommand(ChannelUID channelUID, Command command) {
144         JobChannelConfig jobConfig = getThing().getChannel(channelUID.getId()).getConfiguration()
145                 .as(JobChannelConfig.class);
146         if (command == OnOffType.ON) {
147             robonectClient.startJob(
148                     new RobonectClient.JobSettings().withAfterMode(ModeCommand.Mode.valueOf(jobConfig.getAfterMode()))
149                             .withRemoteStart(ModeCommand.RemoteStart.valueOf(jobConfig.getRemoteStart()))
150                             .withDuration(jobConfig.getDuration()));
151         } else if (command == OnOffType.OFF) {
152             robonectClient.stopJob(
153                     new RobonectClient.JobSettings().withAfterMode(ModeCommand.Mode.valueOf(jobConfig.getAfterMode())));
154         }
155     }
156
157     private void refreshChannels(ChannelUID channelUID) {
158         switch (channelUID.getId()) {
159             case CHANNEL_MOWER_NAME:
160             case CHANNEL_STATUS_BATTERY:
161             case CHANNEL_STATUS:
162             case CHANNEL_STATUS_DURATION:
163             case CHANNEL_STATUS_HOURS:
164             case CHANNEL_STATUS_MODE:
165             case CHANNEL_MOWER_START:
166             case CHANNEL_TIMER_NEXT_TIMER:
167             case CHANNEL_TIMER_STATUS:
168             case CHANNEL_WLAN_SIGNAL:
169             case CHANNEL_JOB:
170                 refreshMowerInfo();
171                 break;
172             default:
173             case CHANNEL_LAST_ERROR_CODE:
174             case CHANNEL_LAST_ERROR_DATE:
175             case CHANNEL_LAST_ERROR_MESSAGE:
176                 refreshLastErrorInfo();
177                 break;
178         }
179     }
180
181     private void setMowerMode(Command command) {
182         String modeStr = command.toFullString();
183         ModeCommand.Mode newMode = ModeCommand.Mode.valueOf(modeStr.toUpperCase());
184         if (robonectClient.setMode(newMode).isSuccessful()) {
185             updateState(CHANNEL_STATUS_MODE, new StringType(newMode.name()));
186         } else {
187             refreshMowerInfo();
188         }
189     }
190
191     private void logErrorFromResponse(RobonectAnswer result) {
192         if (!result.isSuccessful()) {
193             logger.debug("Could not send EOD Trigger. Robonect error message: {}", result.getErrorMessage());
194         }
195     }
196
197     private void handleStartStop(final OnOffType command) {
198         RobonectAnswer answer = null;
199         boolean currentlyStopped = robonectClient.getMowerInfo().getStatus().isStopped();
200         if (command == OnOffType.ON && currentlyStopped) {
201             answer = robonectClient.start();
202         } else if (command == OnOffType.OFF && !currentlyStopped) {
203             answer = robonectClient.stop();
204         }
205         if (answer != null) {
206             if (answer.isSuccessful()) {
207                 updateState(CHANNEL_MOWER_START, command);
208             } else {
209                 logErrorFromResponse(answer);
210                 refreshMowerInfo();
211             }
212         }
213     }
214
215     private void updateName(StringType command) {
216         String newName = command.toFullString();
217         Name name = robonectClient.setName(newName);
218         if (name.isSuccessful()) {
219             updateState(CHANNEL_MOWER_NAME, new StringType(name.getName()));
220         } else {
221             logErrorFromResponse(name);
222             refreshMowerInfo();
223         }
224     }
225
226     private void refreshMowerInfo() {
227         MowerInfo info = robonectClient.getMowerInfo();
228         if (info.isSuccessful()) {
229             if (info.getError() != null) {
230                 updateErrorInfo(info.getError());
231                 refreshLastErrorInfo();
232             } else {
233                 clearErrorInfo();
234             }
235             updateState(CHANNEL_MOWER_NAME, new StringType(info.getName()));
236             updateState(CHANNEL_STATUS_BATTERY, new DecimalType(info.getStatus().getBattery()));
237             updateState(CHANNEL_STATUS, new DecimalType(info.getStatus().getStatus().getStatusCode()));
238             updateState(CHANNEL_STATUS_DURATION,
239                     new QuantityType<>(info.getStatus().getDuration(), SmartHomeUnits.SECOND));
240             updateState(CHANNEL_STATUS_HOURS, new QuantityType<>(info.getStatus().getHours(), SmartHomeUnits.HOUR));
241             updateState(CHANNEL_STATUS_MODE, new StringType(info.getStatus().getMode().name()));
242             updateState(CHANNEL_MOWER_START, info.getStatus().isStopped() ? OnOffType.OFF : OnOffType.ON);
243             if (info.getHealth() != null) {
244                 updateState(CHANNEL_HEALTH_TEMP,
245                         new QuantityType<>(info.getHealth().getTemperature(), SIUnits.CELSIUS));
246                 updateState(CHANNEL_HEALTH_HUM,
247                         new QuantityType(info.getHealth().getHumidity(), SmartHomeUnits.PERCENT));
248             }
249             if (info.getTimer() != null) {
250                 if (info.getTimer().getNext() != null) {
251                     updateNextTimer(info);
252                 }
253                 updateState(CHANNEL_TIMER_STATUS, new StringType(info.getTimer().getStatus().name()));
254             }
255             updateState(CHANNEL_WLAN_SIGNAL, new DecimalType(info.getWlan().getSignal()));
256             updateState(CHANNEL_JOB, robonectClient.isJobRunning() ? OnOffType.ON : OnOffType.OFF);
257         } else {
258             logger.error("Could not retrieve mower info. Robonect error response message: {}", info.getErrorMessage());
259         }
260     }
261
262     private void clearErrorInfo() {
263         updateState(CHANNEL_ERROR_DATE, UnDefType.UNDEF);
264         updateState(CHANNEL_ERROR_CODE, UnDefType.UNDEF);
265         updateState(CHANNEL_ERROR_MESSAGE, UnDefType.UNDEF);
266     }
267
268     private void updateErrorInfo(ErrorEntry error) {
269         if (error.getErrorMessage() != null) {
270             updateState(CHANNEL_ERROR_MESSAGE, new StringType(error.getErrorMessage()));
271         }
272         if (error.getErrorCode() != null) {
273             updateState(CHANNEL_ERROR_CODE, new DecimalType(error.getErrorCode().intValue()));
274         }
275         if (error.getDate() != null) {
276             State dateTime = convertUnixToDateTimeType(error.getUnix());
277             updateState(CHANNEL_ERROR_DATE, dateTime);
278         }
279     }
280
281     private void updateNextTimer(MowerInfo info) {
282         State dateTime = convertUnixToDateTimeType(info.getTimer().getNext().getUnix());
283         updateState(CHANNEL_TIMER_NEXT_TIMER, dateTime);
284     }
285
286     private State convertUnixToDateTimeType(String unixTimeSec) {
287         // the value in unixTimeSec represents the time on the robot in its configured timezone. However, it does not
288         // provide which zone this is. Thus we have to add the zone information from the Thing configuration in order to
289         // provide correct results.
290         Instant rawInstant = Instant.ofEpochMilli(Long.valueOf(unixTimeSec) * 1000);
291
292         ZoneId timeZoneOfThing = timeZone;
293         if (timeZoneOfThing == null) {
294             timeZoneOfThing = timeZoneProvider.getTimeZone();
295         }
296         ZoneOffset offsetToConfiguredZone = timeZoneOfThing.getRules().getOffset(rawInstant);
297         long adjustedTime = rawInstant.getEpochSecond() - offsetToConfiguredZone.getTotalSeconds();
298         Instant adjustedInstant = Instant.ofEpochMilli(adjustedTime * 1000);
299
300         // we provide the time in the format as configured in the openHAB settings
301         ZonedDateTime zdt = adjustedInstant.atZone(timeZoneProvider.getTimeZone());
302         return new DateTimeType(zdt);
303     }
304
305     private void refreshVersionInfo() {
306         VersionInfo info = robonectClient.getVersionInfo();
307         if (info.isSuccessful()) {
308             Map<String, String> properties = editProperties();
309             properties.put(Thing.PROPERTY_SERIAL_NUMBER, info.getRobonect().getSerial());
310             properties.put(Thing.PROPERTY_FIRMWARE_VERSION, info.getRobonect().getVersion());
311             properties.put(PROPERTY_COMPILED, info.getRobonect().getCompiled());
312             properties.put(PROPERTY_COMMENT, info.getRobonect().getComment());
313             updateProperties(properties);
314         } else {
315             logger.debug("Could not retrieve mower version info. Robonect error response message: {}",
316                     info.getErrorMessage());
317         }
318     }
319
320     private void refreshLastErrorInfo() {
321         ErrorList errorList = robonectClient.errorList();
322         if (errorList.isSuccessful()) {
323             List<ErrorEntry> errors = errorList.getErrors();
324             if (errors != null && !errors.isEmpty()) {
325                 ErrorEntry lastErrorEntry = errors.get(0);
326                 updateLastErrorChannels(lastErrorEntry);
327             }
328         } else {
329             logger.debug("Could not retrieve mower error list. Robonect error response message: {}",
330                     errorList.getErrorMessage());
331         }
332     }
333
334     private void updateLastErrorChannels(ErrorEntry error) {
335         if (error.getErrorMessage() != null) {
336             updateState(CHANNEL_LAST_ERROR_MESSAGE, new StringType(error.getErrorMessage()));
337         }
338         if (error.getErrorCode() != null) {
339             updateState(CHANNEL_LAST_ERROR_CODE, new DecimalType(error.getErrorCode().intValue()));
340         }
341         if (error.getDate() != null) {
342             State dateTime = convertUnixToDateTimeType(error.getUnix());
343             updateState(CHANNEL_LAST_ERROR_DATE, dateTime);
344         }
345     }
346
347     @Override
348     public void initialize() {
349         RobonectConfig robonectConfig = getConfigAs(RobonectConfig.class);
350         RobonectEndpoint endpoint = new RobonectEndpoint(robonectConfig.getHost(), robonectConfig.getUser(),
351                 robonectConfig.getPassword());
352
353         String timeZoneString = robonectConfig.getTimezone();
354         try {
355             if (timeZoneString != null) {
356                 timeZone = ZoneId.of(timeZoneString);
357             } else {
358                 logger.warn("No timezone provided, falling back to the default timezone configured in openHAB: '{}'",
359                         timeZoneProvider.getTimeZone());
360             }
361         } catch (DateTimeException e) {
362             logger.warn("Error setting timezone '{}', falling back to the default timezone configured in openHAB: '{}'",
363                     timeZoneString, timeZoneProvider.getTimeZone(), e);
364         }
365
366         try {
367             httpClient.start();
368             robonectClient = new RobonectClient(httpClient, endpoint);
369         } catch (Exception e) {
370             logger.error("Exception while trying to start http client", e);
371             throw new RuntimeException("Exception while trying to start http client", e);
372         }
373         Runnable runnable = new MowerChannelPoller(TimeUnit.SECONDS.toMillis(robonectConfig.getOfflineTimeout()));
374         int pollInterval = robonectConfig.getPollInterval();
375         pollingJob = scheduler.scheduleWithFixedDelay(runnable, 0, pollInterval, TimeUnit.SECONDS);
376     }
377
378     @Override
379     public void dispose() {
380         if (pollingJob != null) {
381             pollingJob.cancel(true);
382             pollingJob = null;
383         }
384         try {
385             if (httpClient != null) {
386                 httpClient.stop();
387                 httpClient = null;
388             }
389         } catch (Exception e) {
390             logger.debug("Could not stop http client", e);
391         }
392     }
393
394     /**
395      * method to inject the robonect client to be used in test cases to allow mocking.
396      *
397      * @param robonectClient
398      */
399     protected void setRobonectClient(RobonectClient robonectClient) {
400         this.robonectClient = robonectClient;
401     }
402
403     private class MowerChannelPoller implements Runnable {
404
405         private long offlineSince;
406         private long offlineTriggerDelay;
407         private boolean offlineTimeoutTriggered;
408         private boolean loadVersionInfo = true;
409
410         public MowerChannelPoller(long offlineTriggerDelay) {
411             offlineSince = -1;
412             this.offlineTriggerDelay = offlineTriggerDelay;
413             offlineTimeoutTriggered = false;
414         }
415
416         @Override
417         public void run() {
418             try {
419                 if (loadVersionInfo) {
420                     refreshVersionInfo();
421                     loadVersionInfo = false;
422                 }
423                 refreshMowerInfo();
424                 updateStatus(ThingStatus.ONLINE);
425                 offlineSince = -1;
426                 offlineTimeoutTriggered = false;
427             } catch (RobonectCommunicationException rce) {
428                 if (offlineSince < 0) {
429                     offlineSince = System.currentTimeMillis();
430                 }
431                 if (!offlineTimeoutTriggered && System.currentTimeMillis() - offlineSince > offlineTriggerDelay) {
432                     // trigger offline
433                     updateState(CHANNEL_MOWER_STATUS_OFFLINE_TRIGGER, new StringType("OFFLINE_TIMEOUT"));
434                     offlineTimeoutTriggered = true;
435                 }
436                 logger.debug("Failed to communicate with the mower. Taking it offline.", rce);
437                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, rce.getMessage());
438                 loadVersionInfo = true;
439             } catch (JsonSyntaxException jse) {
440                 // the module sporadically sends invalid json responses. As this is usually recovered with the
441                 // next poll interval, we just log it to debug here.
442                 logger.debug("Failed to parse response.", jse);
443             }
444         }
445     }
446 }