2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.robonect.internal.handler;
15 import static org.openhab.binding.robonect.internal.RobonectBindingConstants.*;
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;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
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;
60 import com.google.gson.JsonSyntaxException;
63 * The {@link RobonectHandler} is responsible for handling commands, which are
64 * sent to one of the channels.
66 * The channels are periodically updated by polling the mower via HTTP in a separate thread.
68 * @author Marco Meyer - Initial contribution
70 public class RobonectHandler extends BaseThingHandler {
72 private final Logger logger = LoggerFactory.getLogger(RobonectHandler.class);
74 private ScheduledFuture<?> pollingJob;
76 private HttpClient httpClient;
77 private TimeZoneProvider timeZoneProvider;
79 private ZoneId timeZone;
81 private RobonectClient robonectClient;
83 public RobonectHandler(Thing thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
85 this.httpClient = httpClient;
86 this.timeZoneProvider = timeZoneProvider;
90 public void handleCommand(ChannelUID channelUID, Command command) {
92 if (command instanceof RefreshType) {
93 refreshChannels(channelUID);
95 sendCommand(channelUID, command);
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());
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);
110 logger.debug("Got name update of type {} but StringType is expected.",
111 command.getClass().getName());
115 case CHANNEL_STATUS_MODE:
116 if (command instanceof StringType) {
117 setMowerMode(command);
119 logger.debug("Got job remote start update of type {} but StringType is expected.",
120 command.getClass().getName());
124 case CHANNEL_MOWER_START:
125 if (command instanceof OnOffType) {
126 handleStartStop((OnOffType) command);
128 logger.debug("Got stopped update of type {} but OnOffType is expected.",
129 command.getClass().getName());
134 if (command instanceof OnOffType) {
135 handleJobCommand(channelUID, command);
137 logger.debug("Got job update of type {} but OnOffType is expected.", command.getClass().getName());
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())));
157 private void refreshChannels(ChannelUID channelUID) {
158 switch (channelUID.getId()) {
159 case CHANNEL_MOWER_NAME:
160 case CHANNEL_STATUS_BATTERY:
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:
173 case CHANNEL_LAST_ERROR_CODE:
174 case CHANNEL_LAST_ERROR_DATE:
175 case CHANNEL_LAST_ERROR_MESSAGE:
176 refreshLastErrorInfo();
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()));
191 private void logErrorFromResponse(RobonectAnswer result) {
192 if (!result.isSuccessful()) {
193 logger.debug("Could not send EOD Trigger. Robonect error message: {}", result.getErrorMessage());
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();
205 if (answer != null) {
206 if (answer.isSuccessful()) {
207 updateState(CHANNEL_MOWER_START, command);
209 logErrorFromResponse(answer);
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()));
221 logErrorFromResponse(name);
226 private void refreshMowerInfo() {
227 MowerInfo info = robonectClient.getMowerInfo();
228 if (info.isSuccessful()) {
229 if (info.getError() != null) {
230 updateErrorInfo(info.getError());
231 refreshLastErrorInfo();
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));
247 if (info.getTimer() != null) {
248 if (info.getTimer().getNext() != null) {
249 updateNextTimer(info);
251 updateState(CHANNEL_TIMER_STATUS, new StringType(info.getTimer().getStatus().name()));
253 updateState(CHANNEL_WLAN_SIGNAL, new DecimalType(info.getWlan().getSignal()));
254 updateState(CHANNEL_JOB, robonectClient.isJobRunning() ? OnOffType.ON : OnOffType.OFF);
256 logger.error("Could not retrieve mower info. Robonect error response message: {}", info.getErrorMessage());
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);
266 private void updateErrorInfo(ErrorEntry error) {
267 if (error.getErrorMessage() != null) {
268 updateState(CHANNEL_ERROR_MESSAGE, new StringType(error.getErrorMessage()));
270 if (error.getErrorCode() != null) {
271 updateState(CHANNEL_ERROR_CODE, new DecimalType(error.getErrorCode().intValue()));
273 if (error.getDate() != null) {
274 State dateTime = convertUnixToDateTimeType(error.getUnix());
275 updateState(CHANNEL_ERROR_DATE, dateTime);
279 private void updateNextTimer(MowerInfo info) {
280 State dateTime = convertUnixToDateTimeType(info.getTimer().getNext().getUnix());
281 updateState(CHANNEL_TIMER_NEXT_TIMER, dateTime);
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);
290 ZoneId timeZoneOfThing = timeZone;
291 if (timeZoneOfThing == null) {
292 timeZoneOfThing = timeZoneProvider.getTimeZone();
294 ZoneOffset offsetToConfiguredZone = timeZoneOfThing.getRules().getOffset(rawInstant);
295 long adjustedTime = rawInstant.getEpochSecond() - offsetToConfiguredZone.getTotalSeconds();
296 Instant adjustedInstant = Instant.ofEpochMilli(adjustedTime * 1000);
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);
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);
313 logger.debug("Could not retrieve mower version info. Robonect error response message: {}",
314 info.getErrorMessage());
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);
327 logger.debug("Could not retrieve mower error list. Robonect error response message: {}",
328 errorList.getErrorMessage());
332 private void updateLastErrorChannels(ErrorEntry error) {
333 if (error.getErrorMessage() != null) {
334 updateState(CHANNEL_LAST_ERROR_MESSAGE, new StringType(error.getErrorMessage()));
336 if (error.getErrorCode() != null) {
337 updateState(CHANNEL_LAST_ERROR_CODE, new DecimalType(error.getErrorCode().intValue()));
339 if (error.getDate() != null) {
340 State dateTime = convertUnixToDateTimeType(error.getUnix());
341 updateState(CHANNEL_LAST_ERROR_DATE, dateTime);
346 public void initialize() {
347 RobonectConfig robonectConfig = getConfigAs(RobonectConfig.class);
348 RobonectEndpoint endpoint = new RobonectEndpoint(robonectConfig.getHost(), robonectConfig.getUser(),
349 robonectConfig.getPassword());
351 String timeZoneString = robonectConfig.getTimezone();
353 if (timeZoneString != null) {
354 timeZone = ZoneId.of(timeZoneString);
356 logger.warn("No timezone provided, falling back to the default timezone configured in openHAB: '{}'",
357 timeZoneProvider.getTimeZone());
359 } catch (DateTimeException e) {
360 logger.warn("Error setting timezone '{}', falling back to the default timezone configured in openHAB: '{}'",
361 timeZoneString, timeZoneProvider.getTimeZone(), e);
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);
371 Runnable runnable = new MowerChannelPoller(TimeUnit.SECONDS.toMillis(robonectConfig.getOfflineTimeout()));
372 int pollInterval = robonectConfig.getPollInterval();
373 pollingJob = scheduler.scheduleWithFixedDelay(runnable, 0, pollInterval, TimeUnit.SECONDS);
377 public void dispose() {
378 if (pollingJob != null) {
379 pollingJob.cancel(true);
383 if (httpClient != null) {
387 } catch (Exception e) {
388 logger.debug("Could not stop http client", e);
393 * method to inject the robonect client to be used in test cases to allow mocking.
395 * @param robonectClient
397 protected void setRobonectClient(RobonectClient robonectClient) {
398 this.robonectClient = robonectClient;
401 private class MowerChannelPoller implements Runnable {
403 private long offlineSince;
404 private long offlineTriggerDelay;
405 private boolean offlineTimeoutTriggered;
406 private boolean loadVersionInfo = true;
408 public MowerChannelPoller(long offlineTriggerDelay) {
410 this.offlineTriggerDelay = offlineTriggerDelay;
411 offlineTimeoutTriggered = false;
417 if (loadVersionInfo) {
418 refreshVersionInfo();
419 loadVersionInfo = false;
422 updateStatus(ThingStatus.ONLINE);
424 offlineTimeoutTriggered = false;
425 } catch (RobonectCommunicationException rce) {
426 if (offlineSince < 0) {
427 offlineSince = System.currentTimeMillis();
429 if (!offlineTimeoutTriggered && System.currentTimeMillis() - offlineSince > offlineTriggerDelay) {
431 updateState(CHANNEL_MOWER_STATUS_OFFLINE_TRIGGER, new StringType("OFFLINE_TIMEOUT"));
432 offlineTimeoutTriggered = true;
434 logger.debug("Failed to communicate with the mower. Taking it offline.", rce);
435 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, rce.getMessage());
436 loadVersionInfo = true;
437 } catch (JsonSyntaxException jse) {
438 // the module sporadically sends invalid json responses. As this is usually recovered with the
439 // next poll interval, we just log it to debug here.
440 logger.debug("Failed to parse response.", jse);