2 * Copyright (c) 2010-2023 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.lghombot.internal;
15 import static org.openhab.binding.lghombot.internal.LGHomBotBindingConstants.*;
17 import java.awt.image.BufferedImage;
18 import java.io.ByteArrayOutputStream;
19 import java.io.IOException;
20 import java.time.DateTimeException;
21 import java.time.LocalDateTime;
22 import java.time.ZoneId;
23 import java.time.ZonedDateTime;
24 import java.time.format.DateTimeFormatter;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
28 import javax.imageio.ImageIO;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.util.UrlEncoded;
33 import org.openhab.core.io.net.http.HttpUtil;
34 import org.openhab.core.library.types.DateTimeType;
35 import org.openhab.core.library.types.DecimalType;
36 import org.openhab.core.library.types.OnOffType;
37 import org.openhab.core.library.types.RawType;
38 import org.openhab.core.library.types.StringType;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.binding.BaseThingHandler;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.RefreshType;
46 import org.openhab.core.types.State;
47 import org.openhab.core.types.UnDefType;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * The {@link LGHomBotHandler} is responsible for handling commands, which are
53 * sent to one of the channels.
55 * @author Fredrik Ahlström - Initial contribution
58 public class LGHomBotHandler extends BaseThingHandler {
60 private final Logger logger = LoggerFactory.getLogger(LGHomBotHandler.class);
62 // This is setup in initialize().
63 private LGHomBotConfiguration config = new LGHomBotConfiguration();
65 private @Nullable ScheduledFuture<?> refreshTimer;
68 private String currentState = "";
69 private String currentMode = "";
70 private String currentNickname = "";
71 private String currentSrvMem = "";
72 private DecimalType currentBattery = DecimalType.ZERO;
73 private DecimalType currentCPULoad = DecimalType.ZERO;
74 private OnOffType currentCleanState = OnOffType.OFF;
75 private OnOffType currentStartState = OnOffType.OFF;
76 private OnOffType currentHomeState = OnOffType.OFF;
77 private OnOffType currentTurbo = OnOffType.OFF;
78 private OnOffType currentRepeat = OnOffType.OFF;
79 private State currentImage = UnDefType.UNDEF;
80 private State currentMap = UnDefType.UNDEF;
81 private DateTimeType currentLastClean = new DateTimeType();
82 private String currentMonday = "";
83 private String currentTuesday = "";
84 private String currentWednesday = "";
85 private String currentThursday = "";
86 private String currentFriday = "";
87 private String currentSaturday = "";
88 private String currentSunday = "";
90 private final DateTimeFormatter formatterLG = DateTimeFormatter.ofPattern("yyyy/MM/dd/HH/mm/ss");
91 private boolean disposed = false;
92 private boolean refreshSchedule = false;
94 public LGHomBotHandler(Thing thing) {
99 public void dispose() {
105 public void handleCommand(ChannelUID channelUID, Command command) {
106 if (command.equals(RefreshType.REFRESH)) {
107 refreshFromState(channelUID);
109 switch (channelUID.getId()) {
111 if (command == OnOffType.ON) {
112 if (currentState.equals(HBSTATE_HOMING)) {
113 sendHomBotCommand("PAUSE");
115 sendHomBotCommand("CLEAN_START");
116 } else if (command == OnOffType.OFF) {
117 sendHomBotCommand("HOMING");
121 if (command == OnOffType.ON) {
122 sendHomBotCommand("CLEAN_START");
126 if (command == OnOffType.ON) {
127 sendHomBotCommand("HOMING");
131 if (command instanceof OnOffType) {
132 sendHomBotCommand("PAUSE");
136 if (command == OnOffType.ON) {
137 sendHomBotCommand("TURBO", "true");
138 } else if (command == OnOffType.OFF) {
139 sendHomBotCommand("TURBO", "false");
143 if (command == OnOffType.ON) {
144 sendHomBotCommand("REPEAT", "true");
145 } else if (command == OnOffType.OFF) {
146 sendHomBotCommand("REPEAT", "false");
150 if (command instanceof StringType) {
151 switch (command.toString()) {
153 sendHomBotCommand("CLEAN_MODE", "CLEAN_SB");
156 sendHomBotCommand("CLEAN_MODE", "CLEAN_ZZ");
159 sendHomBotCommand("CLEAN_MODE", "CLEAN_SPOT");
162 sendHomBotCommand("CLEAN_MODE", "CLEAN_MACRO_SECTOR");
170 if (command instanceof StringType) {
171 String commandString = command.toString();
172 switch (commandString) {
175 case "FORWARD_RIGHT":
179 case "BACKWARD_LEFT":
180 case "BACKWARD_RIGHT":
182 sendHomBotJoystick(commandString);
190 logger.debug("Command received for unknown channel {}: {}", channelUID.getId(), command);
197 public void initialize() {
199 logger.debug("Initializing handler for LG HomBot");
200 config = getConfigAs(LGHomBotConfiguration.class);
202 setupRefreshTimer(0);
206 public void handleRemoval() {
207 ScheduledFuture<?> localTimer = refreshTimer;
208 if (localTimer != null) {
209 localTimer.cancel(false);
212 updateStatus(ThingStatus.REMOVED);
216 * Sets up a refresh timer (using the scheduler) with the given interval.
218 * @param initialWaitTime The delay before the first refresh. Maybe 0 to immediately
219 * initiate a refresh.
221 private void setupRefreshTimer(int initialWaitTime) {
222 ScheduledFuture<?> localTimer = refreshTimer;
223 if (localTimer != null) {
224 localTimer.cancel(false);
226 refreshTimer = scheduler.scheduleWithFixedDelay(this::updateAllChannels, initialWaitTime, config.pollingPeriod,
230 private String buildHttpAddress(String path) {
231 return "http://" + config.ipAddress + ":" + config.port + path;
234 private void sendHomBotCommand(String command) {
235 String fullCmd = "/json.cgi?" + UrlEncoded.encodeString("{\"COMMAND\":\"" + command + "\"}");
236 sendCommand(fullCmd);
239 private void sendHomBotCommand(String command, String argument) {
240 String fullCmd = "/json.cgi?"
241 + UrlEncoded.encodeString("{\"COMMAND\":{\"" + command + "\":\"" + argument + "\"}}");
242 sendCommand(fullCmd);
245 private void sendHomBotJoystick(String command) {
246 String fullCmd = "/json.cgi?" + UrlEncoded.encodeString("{\"JOY\":\"" + command + "\"}");
247 sendCommand(fullCmd);
250 private @Nullable String sendCommand(String path) {
251 String url = buildHttpAddress(path);
252 logger.trace("Executing: {}", url);
253 String status = null;
255 status = HttpUtil.executeUrl("GET", url, 1000);
256 if (getThing().getStatus() != ThingStatus.ONLINE) {
257 updateStatus(ThingStatus.ONLINE);
259 } catch (IOException e) {
260 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
262 logger.trace("Status received: {}", status);
266 private void refreshFromState(ChannelUID channelUID) {
267 switch (channelUID.getId()) {
269 updateState(channelUID, StringType.valueOf(currentState));
272 updateState(channelUID, currentCleanState);
275 updateState(channelUID, currentStartState);
278 updateState(channelUID, currentHomeState);
280 case CHANNEL_BATTERY:
281 updateState(channelUID, currentBattery);
283 case CHANNEL_CPU_LOAD:
284 updateState(channelUID, currentCPULoad);
286 case CHANNEL_SRV_MEM:
287 updateState(channelUID, StringType.valueOf(currentSrvMem));
290 updateState(channelUID, currentTurbo);
293 updateState(channelUID, currentRepeat);
296 updateState(channelUID, StringType.valueOf(currentMode));
298 case CHANNEL_NICKNAME:
299 updateState(channelUID, StringType.valueOf(currentNickname));
303 updateState(channelUID, currentImage);
305 case CHANNEL_LAST_CLEAN:
306 updateState(channelUID, currentLastClean);
310 updateState(channelUID, currentMap);
313 updateState(channelUID, StringType.valueOf(currentMonday));
314 refreshSchedule = true;
316 case CHANNEL_TUESDAY:
317 updateState(channelUID, StringType.valueOf(currentTuesday));
318 refreshSchedule = true;
320 case CHANNEL_WEDNESDAY:
321 updateState(channelUID, StringType.valueOf(currentWednesday));
322 refreshSchedule = true;
324 case CHANNEL_THURSDAY:
325 updateState(channelUID, StringType.valueOf(currentThursday));
326 refreshSchedule = true;
329 updateState(channelUID, StringType.valueOf(currentFriday));
330 refreshSchedule = true;
332 case CHANNEL_SATURDAY:
333 updateState(channelUID, StringType.valueOf(currentSaturday));
334 refreshSchedule = true;
337 updateState(channelUID, StringType.valueOf(currentSunday));
338 refreshSchedule = true;
341 logger.warn("Channel refresh for {} not implemented!", channelUID.getId());
345 private void updateAllChannels() {
349 if (refreshSchedule) {
350 refreshSchedule = false;
355 String status = sendCommand("/status.txt");
356 if (status != null && !status.isEmpty()) {
357 boolean parsingOk = true;
358 String[] rows = status.split("\\r?\\n");
359 for (String row : rows) {
360 int idx = row.indexOf('=');
364 final String key = row.substring(0, idx);
365 String value = row.substring(idx + 1).replace("\"", "");
367 case "JSON_ROBOT_STATE":
368 if (value.isEmpty()) {
369 value = HBSTATE_UNKNOWN;
371 if (!value.equals(currentState)) {
372 currentState = value;
373 updateState(CHANNEL_STATE, StringType.valueOf(value));
376 case HBSTATE_WORKING:
377 case HBSTATE_BACKMOVING:
378 case HBSTATE_BACKMOVING_INIT:
379 currentCleanState = OnOffType.ON;
380 currentStartState = OnOffType.ON;
381 currentHomeState = OnOffType.OFF;
384 case HBSTATE_DOCKING:
385 currentCleanState = OnOffType.OFF;
386 currentStartState = OnOffType.OFF;
387 currentHomeState = OnOffType.ON;
390 currentCleanState = OnOffType.OFF;
391 currentStartState = OnOffType.OFF;
392 currentHomeState = OnOffType.OFF;
395 updateState(CHANNEL_CLEAN, currentCleanState);
396 updateState(CHANNEL_START, currentStartState);
397 updateState(CHANNEL_HOME, currentHomeState);
400 case "JSON_BATTPERC":
402 DecimalType battery = DecimalType.valueOf(value);
403 if (!battery.equals(currentBattery)) {
404 currentBattery = battery;
405 updateState(CHANNEL_BATTERY, battery);
407 } catch (NumberFormatException e) {
408 logger.debug("Couldn't parse Battery Percent.");
413 if (isLinked(CHANNEL_CPU_LOAD)) {
415 DecimalType cpuLoad = new DecimalType(100 - Double.valueOf(value).longValue());
416 if (!cpuLoad.equals(currentCPULoad)) {
417 currentCPULoad = cpuLoad;
418 updateState(CHANNEL_CPU_LOAD, cpuLoad);
420 } catch (NumberFormatException e) {
421 logger.debug("Couldn't parse CPU Idle.");
426 case "LGSRV_MEMUSAGE":
427 if (!value.equals(currentSrvMem)) {
428 currentSrvMem = value;
429 updateState(CHANNEL_SRV_MEM, StringType.valueOf(value));
433 OnOffType turbo = OnOffType.from("true".equalsIgnoreCase(value));
434 if (!turbo.equals(currentTurbo)) {
435 currentTurbo = turbo;
436 updateState(CHANNEL_TURBO, turbo);
440 OnOffType repeat = OnOffType.from("true".equalsIgnoreCase(value));
441 if (!repeat.equals(currentRepeat)) {
442 currentRepeat = repeat;
443 updateState(CHANNEL_REPEAT, repeat);
447 if (!value.equals(currentMode)) {
449 updateState(CHANNEL_MODE, StringType.valueOf(value));
452 case "JSON_NICKNAME":
453 if (!value.equals(currentNickname)) {
454 currentNickname = value;
455 updateState(CHANNEL_NICKNAME, StringType.valueOf(value));
458 case "CLREC_LAST_CLEAN":
459 if (value.length() < 19) {
460 logger.debug("Couldn't parse Last Clean from: String length: {}", value.length());
464 final String stringDate = value.substring(0, 19);
466 LocalDateTime localDateTime = LocalDateTime.parse(stringDate, formatterLG);
467 ZonedDateTime date = ZonedDateTime.of(localDateTime, ZoneId.systemDefault());
468 DateTimeType lastClean = new DateTimeType(date);
469 if (!lastClean.equals(currentLastClean)) {
470 currentLastClean = lastClean;
471 updateState(CHANNEL_LAST_CLEAN, lastClean);
473 } catch (DateTimeException e) {
474 logger.debug("Couldn't parse Last Clean from: {}", stringDate);
483 logger.debug("Couldn't parse status response;\n {}", status);
488 private void fetchSchedule() {
489 String status = sendCommand("/.../usr/data/htdocs/timer.txt");
491 if (status != null && !status.isEmpty()) {
494 String wednesday = "";
495 String thursday = "";
497 String saturday = "";
499 String[] rows = status.split("\\r?\\n");
500 for (String row : rows) {
501 int idx = row.indexOf('=');
502 String name = row.substring(0, idx);
503 String state = row.substring(idx + 1);
531 if (!currentMonday.equals(monday)) {
532 currentMonday = monday;
533 updateState(CHANNEL_MONDAY, StringType.valueOf(monday));
535 if (!currentTuesday.equals(tuesday)) {
536 currentTuesday = tuesday;
537 updateState(CHANNEL_TUESDAY, StringType.valueOf(tuesday));
539 if (!currentWednesday.equals(wednesday)) {
540 currentWednesday = wednesday;
541 updateState(CHANNEL_WEDNESDAY, StringType.valueOf(wednesday));
543 if (!currentThursday.equals(thursday)) {
544 currentThursday = thursday;
545 updateState(CHANNEL_THURSDAY, StringType.valueOf(thursday));
547 if (!currentFriday.equals(friday)) {
548 currentFriday = friday;
549 updateState(CHANNEL_FRIDAY, StringType.valueOf(friday));
551 if (!currentSaturday.equals(saturday)) {
552 currentSaturday = saturday;
553 updateState(CHANNEL_SATURDAY, StringType.valueOf(saturday));
555 if (!currentSunday.equals(sunday)) {
556 currentSunday = sunday;
557 updateState(CHANNEL_SUNDAY, StringType.valueOf(sunday));
562 private void parseImage() {
563 if (!isLinked(CHANNEL_CAMERA)) {
566 final int width = 320;
567 final int height = 240;
568 final int size = width * height;
569 String url = buildHttpAddress("/images/snapshot.yuv");
570 RawType rawData = HttpUtil.downloadData(url, null, false, size * 2);
571 if (rawData != null) {
572 byte[] yuvData = rawData.getBytes();
573 currentImage = CameraUtil.parseImageFromBytes(yuvData, width, height);
575 logger.info("No camera image returned from HomBot.");
579 /** Parse the maps.html file to find the black-box filename. */
580 private String findBlackBoxFile() {
581 String url = buildHttpAddress("/sites/maps.html");
583 String htmlString = HttpUtil.executeUrl("GET", url, 1000);
584 int idx = htmlString.indexOf("blkfiles");
585 return "/.../usr/data/blackbox/" + htmlString.substring(idx + 13, idx + 50);
586 } catch (IOException e1) {
587 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e1.getMessage());
592 private void parseMap() {
593 if (!isLinked(CHANNEL_MAP)) {
596 final int tileSize = 10;
597 final int tileArea = tileSize * tileSize;
598 final int rowLength = 100;
601 String blackBox = findBlackBoxFile();
602 String url = buildHttpAddress(blackBox);
603 RawType dlData = HttpUtil.downloadData(url, null, false, -1);
604 if (dlData == null) {
607 byte[] mapData = dlData.getBytes();
609 final int tileCount = mapData[32];
616 for (int i = 0; i < tileCount; i++) {
617 pixPos = (mapData[52 + i * 16] & 0xFF) + (mapData[52 + 1 + i * 16] << 8);
618 int xPos = (pixPos % rowLength) * tileSize;
619 int yPos = (pixPos / rowLength) * tileSize;
634 final int width = (tileSize + maxX - minX) * scale;
635 final int height = (tileSize + maxY - minY) * scale;
637 BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
638 for (int i = 0; i < height; i++) {
639 for (int j = 0; j < width; j++) {
640 image.setRGB(j, i, 0xFFFFFF);
643 for (int i = 0; i < tileCount; i++) {
644 pixPos = (mapData[52 + i * 16] & 0xFF) + (mapData[52 + 1 + i * 16] << 8);
645 int xPos = ((pixPos % rowLength) * tileSize - minX) * scale;
646 int yPos = (maxY - (pixPos / rowLength) * tileSize) * scale;
647 int indexTab = 16044 + i * tileArea;
648 for (int j = 0; j < tileSize; j++) {
649 for (int k = 0; k < tileSize; k++) {
651 if ((mapData[indexTab] & 0xF0) != 0) {
653 } else if (mapData[indexTab] != 0) {
656 image.setRGB(xPos + k * scale, yPos + (9 - j) * scale, p);
662 ByteArrayOutputStream baos = new ByteArrayOutputStream();
664 if (!ImageIO.write(image, "png", baos)) {
665 logger.debug("Couldn't find PNG writer.");
667 } catch (IOException e) {
668 logger.info("IOException creating PNG image.", e);
670 byte[] byteArray = baos.toByteArray();
671 if (byteArray != null && byteArray.length > 0) {
672 currentMap = new RawType(byteArray, "image/png");
674 currentMap = UnDefType.UNDEF;