From e4b959382fe7d783ebc7757c26407d685c86d6fb Mon Sep 17 00:00:00 2001 From: rimago Date: Tue, 26 Jan 2021 05:19:49 +0100 Subject: [PATCH] [irobot] Add command "cleanRegions" to clean specific regions only (#9775) Signed-off-by: Florian Binder --- bundles/org.openhab.binding.irobot/README.md | 11 +- .../internal/IRobotBindingConstants.java | 2 + .../irobot/internal/dto/MQTTProtocol.java | 36 +++++- .../internal/handler/RoombaHandler.java | 25 +++- .../resources/OH-INF/thing/thing-types.xml | 9 ++ .../internal/handler/RoombaHandlerTest.java | 120 ++++++++++++++++++ 6 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 bundles/org.openhab.binding.irobot/src/test/java/org/openhab/binding/irobot/internal/handler/RoombaHandlerTest.java diff --git a/bundles/org.openhab.binding.irobot/README.md b/bundles/org.openhab.binding.irobot/README.md index 568a0ab98..327ea3815 100644 --- a/bundles/org.openhab.binding.irobot/README.md +++ b/bundles/org.openhab.binding.irobot/README.md @@ -32,7 +32,7 @@ known, however, whether the password is eternal or can change during factory res | channel | type | description | Read-only | |---------------|--------|----------------------------------------------------|-----------| -| command | String | Command to execute: clean, spot, dock, pause, stop | N | +| command | String | Command to execute: clean, cleanRegions, spot, dock, pause, stop | N | | cycle | String | Current mission: none, clean, spot | Y | | phase | String | Current phase of the mission; see below. | Y | | battery | Number | Battery charge in percents | Y | @@ -52,6 +52,7 @@ known, however, whether the password is eternal or can change during factory res | always_finish | Switch | Whether to keep cleaning if the bin becomes full | N | | power_boost | String | Power boost mode: "auto", "performance", "eco" | N | | clean_passes | String | Number of cleaning passes: "auto", "1", "2" | N | +| last_command | String | Json string containing the parameters of the last executed command | N | Known phase strings and their meanings: @@ -137,6 +138,14 @@ Error codes. Data type is string in order to be able to utilize mapping to human | 75 | Navigation problem | | 76 | Hardware problem detected | +## Cleaning specific regions +You can clean one or many specific regions of a given map by sending the following String to the command channel: + +``` + cleanRegions:;,,.. +``` +The easiest way to determine the pmapId and region_ids is to monitor the last_command channel while starting a new mission for the specific region with the iRobot-App. + ## Known Problems / Caveats 1. Sending "pause" command during missions other than "clean" is equivalent to sending "stop" diff --git a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/IRobotBindingConstants.java b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/IRobotBindingConstants.java index 8e50cf56a..6f96e48e3 100644 --- a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/IRobotBindingConstants.java +++ b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/IRobotBindingConstants.java @@ -48,8 +48,10 @@ public class IRobotBindingConstants { public static final String CHANNEL_ALWAYS_FINISH = "always_finish"; public static final String CHANNEL_POWER_BOOST = "power_boost"; public static final String CHANNEL_CLEAN_PASSES = "clean_passes"; + public static final String CHANNEL_LAST_COMMAND = "last_command"; public static final String CMD_CLEAN = "clean"; + public static final String CMD_CLEAN_REGIONS = "cleanRegions"; public static final String CMD_SPOT = "spot"; public static final String CMD_DOCK = "dock"; public static final String CMD_PAUSE = "pause"; diff --git a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/dto/MQTTProtocol.java b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/dto/MQTTProtocol.java index 584e7748a..30ab3722e 100644 --- a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/dto/MQTTProtocol.java +++ b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/dto/MQTTProtocol.java @@ -12,10 +12,17 @@ */ package org.openhab.binding.irobot.internal.dto; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import com.google.gson.JsonElement; + /** * iRobot MQTT protocol messages * * @author Pavel Fedin - Initial contribution + * @author Florian Binder - Added CleanRoomsRequest * */ public class MQTTProtocol { @@ -23,6 +30,29 @@ public class MQTTProtocol { public String getTopic(); } + public static class CleanRoomsRequest extends CommandRequest { + public int ordered; + public String pmap_id; + public List regions; + + public CleanRoomsRequest(String cmd, String mapId, String[] regions) { + super(cmd); + ordered = 1; + pmap_id = mapId; + this.regions = Arrays.stream(regions).map(i -> new Region(i)).collect(Collectors.toList()); + } + + public static class Region { + public String region_id; + public String type; + + public Region(String id) { + this.region_id = id; + this.type = "rid"; + } + } + } + public static class CommandRequest implements Request { public String command; public long time; @@ -31,7 +61,7 @@ public class MQTTProtocol { public CommandRequest(String cmd) { command = cmd; time = System.currentTimeMillis() / 1000; - initiator = "localApp"; + initiator = "openhab"; } @Override @@ -56,6 +86,7 @@ public class MQTTProtocol { public static class CleanMissionStatus { public String cycle; public String phase; + public String initiator; public int error; } @@ -183,6 +214,9 @@ public class MQTTProtocol { public String bootloaderVer; // "umiVer":"6", public String umiVer; + // "lastCommand": + // {"command":"start","initiator":"localApp","time":1610283995,"ordered":1,"pmap_id":"AAABBBCCCSDDDEEEFFF","regions":[{"region_id":"6","type":"rid"}]} + public JsonElement lastCommand; } // Data comes as JSON string: {"state":{"reported":}} diff --git a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/handler/RoombaHandler.java b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/handler/RoombaHandler.java index 943f4de95..39a51da8b 100644 --- a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/handler/RoombaHandler.java +++ b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/handler/RoombaHandler.java @@ -25,6 +25,7 @@ import java.util.Hashtable; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.regex.Pattern; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -65,6 +66,7 @@ import com.google.gson.stream.JsonReader; * * @author hkuhn42 - Initial contribution * @author Pavel Fedin - Rewrite for 900 series + * @author Florian Binder - added cleanRegions command and lastCommand channel */ @NonNullByDefault public class RoombaHandler extends BaseThingHandler implements MqttConnectionObserver, MqttMessageSubscriber { @@ -128,7 +130,24 @@ public class RoombaHandler extends BaseThingHandler implements MqttConnectionObs cmd = isPaused ? "resume" : "start"; } - sendRequest(new MQTTProtocol.CommandRequest(cmd)); + if (cmd.startsWith(CMD_CLEAN_REGIONS)) { + // format: cleanRegions:;,,... + if (Pattern.matches("cleanRegions:[^:;,]+;.+(,[^:;,]+)*", cmd)) { + String[] cmds = cmd.split(":"); + String[] params = cmds[1].split(";"); + + String mapId = params[0]; + String[] regionIds = params[1].split(","); + + sendRequest(new MQTTProtocol.CleanRoomsRequest("start", mapId, regionIds)); + } else { + logger.warn("Invalid request: {}", cmd); + logger.warn("Correct format: cleanRegions:;,,...>"); + } + } else { + sendRequest(new MQTTProtocol.CommandRequest(cmd)); + } + } } else if (ch.startsWith(CHANNEL_SCHED_SWITCH_PREFIX)) { MQTTProtocol.Schedule schedule = lastSchedule; @@ -489,6 +508,10 @@ public class RoombaHandler extends BaseThingHandler implements MqttConnectionObs } } + if (reported.lastCommand != null) { + reportString(CHANNEL_LAST_COMMAND, reported.lastCommand.toString()); + } + reportProperty(Thing.PROPERTY_FIRMWARE_VERSION, reported.softwareVer); reportProperty("navSwVer", reported.navSwVer); reportProperty("wifiSwVer", reported.wifiSwVer); diff --git a/bundles/org.openhab.binding.irobot/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.irobot/src/main/resources/OH-INF/thing/thing-types.xml index b13b25c7a..88101263c 100644 --- a/bundles/org.openhab.binding.irobot/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.irobot/src/main/resources/OH-INF/thing/thing-types.xml @@ -10,6 +10,7 @@ + @@ -255,4 +256,12 @@ + + String + + The last command which has been received by the iRobot + + + + diff --git a/bundles/org.openhab.binding.irobot/src/test/java/org/openhab/binding/irobot/internal/handler/RoombaHandlerTest.java b/bundles/org.openhab.binding.irobot/src/test/java/org/openhab/binding/irobot/internal/handler/RoombaHandlerTest.java new file mode 100644 index 000000000..afb9b8250 --- /dev/null +++ b/bundles/org.openhab.binding.irobot/src/test/java/org/openhab/binding/irobot/internal/handler/RoombaHandlerTest.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.irobot.internal.handler; + +import java.io.IOException; +import java.lang.reflect.Field; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.spi.LocationAwareLogger; + +/** + * Test the MQTT protocol with local iRobot (without openhab running). + * This class is used to test the binding against a local iRobot instance. + * + * @author Florian Binder - Initial contribution + */ + +@ExtendWith(MockitoExtension.class) +@TestInstance(Lifecycle.PER_CLASS) +class RoombaHandlerTest { + + private static final String IP_ADDRESS = ""; + private static final String PASSWORD = ""; + + private RoombaHandler handler; + private @Mock Thing myThing; + private ThingHandlerCallback callback; + + @BeforeEach + void setUp() throws Exception { + Logger l = LoggerFactory.getLogger(RoombaHandler.class); + Field f = l.getClass().getDeclaredField("currentLogLevel"); + f.setAccessible(true); + f.set(l, LocationAwareLogger.TRACE_INT); + + Configuration config = new Configuration(); + config.put("ipaddress", RoombaHandlerTest.IP_ADDRESS); + config.put("password", RoombaHandlerTest.PASSWORD); + + Mockito.when(myThing.getConfiguration()).thenReturn(config); + Mockito.when(myThing.getUID()).thenReturn(new ThingUID("mocked", "irobot", "uid")); + + callback = Mockito.mock(ThingHandlerCallback.class); + + handler = new RoombaHandler(myThing); + handler.setCallback(callback); + } + + // @Test + void testInit() throws InterruptedException, IOException { + handler.initialize(); + Mockito.verify(myThing, Mockito.times(1)).getConfiguration(); + + System.in.read(); + handler.dispose(); + } + + // @Test + void testCleanRegion() throws IOException, InterruptedException { + handler.initialize(); + + System.in.read(); + + ChannelUID cmd = new ChannelUID("my:thi:blabla:command"); + handler.handleCommand(cmd, new StringType("cleanRegions:AABBCCDDEEFFGGHH;2,3")); + + System.in.read(); + handler.dispose(); + } + + // @Test + void testDock() throws IOException, InterruptedException { + handler.initialize(); + + System.in.read(); + + ChannelUID cmd = new ChannelUID("my:thi:blabla:command"); + handler.handleCommand(cmd, new StringType("dock")); + + System.in.read(); + handler.dispose(); + } + + // @Test + void testStop() throws IOException, InterruptedException { + handler.initialize(); + + System.in.read(); + + ChannelUID cmd = new ChannelUID("my:thi:blabla:command"); + handler.handleCommand(cmd, new StringType("stop")); + + System.in.read(); + handler.dispose(); + } +}