]> git.basschouten.com Git - openhab-addons.git/blob
f30f26ac9c07accc477452839c6e19c1bdcfc550
[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.Units;
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, new QuantityType<>(info.getStatus().getDuration(), Units.SECOND));
239             updateState(CHANNEL_STATUS_HOURS, new QuantityType<>(info.getStatus().getHours(), Units.HOUR));
240             updateState(CHANNEL_STATUS_MODE, new StringType(info.getStatus().getMode().name()));
241             updateState(CHANNEL_MOWER_START, info.getStatus().isStopped() ? OnOffType.OFF : OnOffType.ON);
242             if (info.getHealth() != null) {
243                 updateState(CHANNEL_HEALTH_TEMP,
244                         new QuantityType<>(info.getHealth().getTemperature(), SIUnits.CELSIUS));
245                 updateState(CHANNEL_HEALTH_HUM, new QuantityType(info.getHealth().getHumidity(), Units.PERCENT));
246             }
247             if (info.getTimer() != null) {
248                 if (info.getTimer().getNext() != null) {
249                     updateNextTimer(info);
250                 }
251                 updateState(CHANNEL_TIMER_STATUS, new StringType(info.getTimer().getStatus().name()));
252             }
253             updateState(CHANNEL_WLAN_SIGNAL, new DecimalType(info.getWlan().getSignal()));
254             updateState(CHANNEL_JOB, robonectClient.isJobRunning() ? OnOffType.ON : OnOffType.OFF);
255         } else {
256             logger.error("Could not retrieve mower info. Robonect error response message: {}", info.getErrorMessage());
257         }
258     }
259
260     private void clearErrorInfo() {
261         updateState(CHANNEL_ERROR_DATE, UnDefType.UNDEF);
262         updateState(CHANNEL_ERROR_CODE, UnDefType.UNDEF);
263         updateState(CHANNEL_ERROR_MESSAGE, UnDefType.UNDEF);
264     }
265
266     private void updateErrorInfo(ErrorEntry error) {
267         if (error.getErrorMessage() != null) {
268             updateState(CHANNEL_ERROR_MESSAGE, new StringType(error.getErrorMessage()));
269         }
270         if (error.getErrorCode() != null) {
271             updateState(CHANNEL_ERROR_CODE, new DecimalType(error.getErrorCode().intValue()));
272         }
273         if (error.getDate() != null) {
274             State dateTime = convertUnixToDateTimeType(error.getUnix());
275             updateState(CHANNEL_ERROR_DATE, dateTime);
276         }
277     }
278
279     private void updateNextTimer(MowerInfo info) {
280         State dateTime = convertUnixToDateTimeType(info.getTimer().getNext().getUnix());
281         updateState(CHANNEL_TIMER_NEXT_TIMER, dateTime);
282     }
283
284     private State convertUnixToDateTimeType(String unixTimeSec) {
285         // the value in unixTimeSec represents the time on the robot in its configured timezone. However, it does not
286         // provide which zone this is. Thus we have to add the zone information from the Thing configuration in order to
287         // provide correct results.
288         Instant rawInstant = Instant.ofEpochMilli(Long.valueOf(unixTimeSec) * 1000);
289
290         ZoneId timeZoneOfThing = timeZone;
291         if (timeZoneOfThing == null) {
292             timeZoneOfThing = timeZoneProvider.getTimeZone();
293         }
294         ZoneOffset offsetToConfiguredZone = timeZoneOfThing.getRules().getOffset(rawInstant);
295         long adjustedTime = rawInstant.getEpochSecond() - offsetToConfiguredZone.getTotalSeconds();
296         Instant adjustedInstant = Instant.ofEpochMilli(adjustedTime * 1000);
297
298         // we provide the time in the format as configured in the openHAB settings
299         ZonedDateTime zdt = adjustedInstant.atZone(timeZoneProvider.getTimeZone());
300         return new DateTimeType(zdt);
301     }
302
303     private void refreshVersionInfo() {
304         VersionInfo info = robonectClient.getVersionInfo();
305         if (info.isSuccessful()) {
306             Map<String, String> properties = editProperties();
307             properties.put(Thing.PROPERTY_SERIAL_NUMBER, info.getRobonect().getSerial());
308             properties.put(Thing.PROPERTY_FIRMWARE_VERSION, info.getRobonect().getVersion());
309             properties.put(PROPERTY_COMPILED, info.getRobonect().getCompiled());
310             properties.put(PROPERTY_COMMENT, info.getRobonect().getComment());
311             updateProperties(properties);
312         } else {
313             logger.debug("Could not retrieve mower version info. Robonect error response message: {}",
314                     info.getErrorMessage());
315         }
316     }
317
318     private void refreshLastErrorInfo() {
319         ErrorList errorList = robonectClient.errorList();
320         if (errorList.isSuccessful()) {
321             List<ErrorEntry> errors = errorList.getErrors();
322             if (errors != null && !errors.isEmpty()) {
323                 ErrorEntry lastErrorEntry = errors.get(0);
324                 updateLastErrorChannels(lastErrorEntry);
325             }
326         } else {
327             logger.debug("Could not retrieve mower error list. Robonect error response message: {}",
328                     errorList.getErrorMessage());
329         }
330     }
331
332     private void updateLastErrorChannels(ErrorEntry error) {
333         if (error.getErrorMessage() != null) {
334             updateState(CHANNEL_LAST_ERROR_MESSAGE, new StringType(error.getErrorMessage()));
335         }
336         if (error.getErrorCode() != null) {
337             updateState(CHANNEL_LAST_ERROR_CODE, new DecimalType(error.getErrorCode().intValue()));
338         }
339         if (error.getDate() != null) {
340             State dateTime = convertUnixToDateTimeType(error.getUnix());
341             updateState(CHANNEL_LAST_ERROR_DATE, dateTime);
342         }
343     }
344
345     @Override
346     public void initialize() {
347         RobonectConfig robonectConfig = getConfigAs(RobonectConfig.class);
348         RobonectEndpoint endpoint = new RobonectEndpoint(robonectConfig.getHost(), robonectConfig.getUser(),
349                 robonectConfig.getPassword());
350
351         String timeZoneString = robonectConfig.getTimezone();
352         try {
353             if (timeZoneString != null) {
354                 timeZone = ZoneId.of(timeZoneString);
355             } else {
356                 logger.warn("No timezone provided, falling back to the default timezone configured in openHAB: '{}'",
357                         timeZoneProvider.getTimeZone());
358             }
359         } catch (DateTimeException e) {
360             logger.warn("Error setting timezone '{}', falling back to the default timezone configured in openHAB: '{}'",
361                     timeZoneString, timeZoneProvider.getTimeZone(), e);
362         }
363
364         try {
365             httpClient.start();
366             robonectClient = new RobonectClient(httpClient, endpoint);
367         } catch (Exception e) {
368             logger.error("Exception while trying to start http client", e);
369             throw new RuntimeException("Exception while trying to start http client", e);
370         }
371         Runnable runnable = new MowerChannelPoller(TimeUnit.SECONDS.toMillis(robonectConfig.getOfflineTimeout()));
372         int pollInterval = robonectConfig.getPollInterval();
373         pollingJob = scheduler.scheduleWithFixedDelay(runnable, 0, pollInterval, TimeUnit.SECONDS);
374     }
375
376     @Override
377     public void dispose() {
378         if (pollingJob != null) {
379             pollingJob.cancel(true);
380             pollingJob = null;
381         }
382
383         httpClient = null;
384     }
385
386     /**
387      * method to inject the robonect client to be used in test cases to allow mocking.
388      *
389      * @param robonectClient
390      */
391     protected void setRobonectClient(RobonectClient robonectClient) {
392         this.robonectClient = robonectClient;
393     }
394
395     private class MowerChannelPoller implements Runnable {
396
397         private long offlineSince;
398         private long offlineTriggerDelay;
399         private boolean offlineTimeoutTriggered;
400         private boolean loadVersionInfo = true;
401
402         public MowerChannelPoller(long offlineTriggerDelay) {
403             offlineSince = -1;
404             this.offlineTriggerDelay = offlineTriggerDelay;
405             offlineTimeoutTriggered = false;
406         }
407
408         @Override
409         public void run() {
410             try {
411                 if (loadVersionInfo) {
412                     refreshVersionInfo();
413                     loadVersionInfo = false;
414                 }
415                 refreshMowerInfo();
416                 updateStatus(ThingStatus.ONLINE);
417                 offlineSince = -1;
418                 offlineTimeoutTriggered = false;
419             } catch (RobonectCommunicationException rce) {
420                 if (offlineSince < 0) {
421                     offlineSince = System.currentTimeMillis();
422                 }
423                 if (!offlineTimeoutTriggered && System.currentTimeMillis() - offlineSince > offlineTriggerDelay) {
424                     // trigger offline
425                     updateState(CHANNEL_MOWER_STATUS_OFFLINE_TRIGGER, new StringType("OFFLINE_TIMEOUT"));
426                     offlineTimeoutTriggered = true;
427                 }
428                 logger.debug("Failed to communicate with the mower. Taking it offline.", rce);
429                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, rce.getMessage());
430                 loadVersionInfo = true;
431             } catch (JsonSyntaxException jse) {
432                 // the module sporadically sends invalid json responses. As this is usually recovered with the
433                 // next poll interval, we just log it to debug here.
434                 logger.debug("Failed to parse response.", jse);
435             }
436         }
437     }
438 }