| 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 |
| 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:
| 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:<pmapId>;<region_id1>,<region_id2>,..
+```
+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"
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";
*/
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 {
public String getTopic();
}
+ public static class CleanRoomsRequest extends CommandRequest {
+ public int ordered;
+ public String pmap_id;
+ public List<Region> 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;
public CommandRequest(String cmd) {
command = cmd;
time = System.currentTimeMillis() / 1000;
- initiator = "localApp";
+ initiator = "openhab";
}
@Override
public static class CleanMissionStatus {
public String cycle;
public String phase;
+ public String initiator;
public int error;
}
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":<Actual content here>}}
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;
*
* @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 {
cmd = isPaused ? "resume" : "start";
}
- sendRequest(new MQTTProtocol.CommandRequest(cmd));
+ if (cmd.startsWith(CMD_CLEAN_REGIONS)) {
+ // format: cleanRegions:<pmid>;<region_id1>,<region_id2>,...
+ 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:<pmid>;<region_id1>,<region_id2>,...>");
+ }
+ } else {
+ sendRequest(new MQTTProtocol.CommandRequest(cmd));
+ }
+
}
} else if (ch.startsWith(CHANNEL_SCHED_SWITCH_PREFIX)) {
MQTTProtocol.Schedule schedule = lastSchedule;
}
}
+ 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);
<channels>
<channel id="command" typeId="command"/>
+ <channel id="last_command" typeId="lastCommand"/>
<channel id="cycle" typeId="cycle"/>
<channel id="phase" typeId="phase"/>
<channel id="battery" typeId="battery"/>
</state>
</channel-type>
+ <channel-type id="lastCommand" advanced="true">
+ <item-type>String</item-type>
+ <label>Last Command</label>
+ <description>The last command which has been received by the iRobot</description>
+ <state readOnly="true">
+ </state>
+ </channel-type>
+
</thing:thing-descriptions>
--- /dev/null
+/**
+ * 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 = "<iRobotIP>";
+ private static final String PASSWORD = "<PasswordForIRobot>";
+
+ 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();
+ }
+}