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