[irobot] Add command "cleanRegions" to clean specific regions only (#9775)

Signed-off-by: Florian Binder <fb@java4.info>
This commit is contained in:
rimago 2021-01-26 05:19:49 +01:00 committed by GitHub
parent c9dbc46fd1
commit e4b959382f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 200 additions and 3 deletions

View File

@ -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:<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"

View File

@ -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";

View File

@ -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<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;
@ -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":<Actual content here>}}

View File

@ -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,8 +130,25 @@ public class RoombaHandler extends BaseThingHandler implements MqttConnectionObs
cmd = isPaused ? "resume" : "start";
}
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;
@ -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);

View File

@ -10,6 +10,7 @@
<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"/>
@ -255,4 +256,12 @@
</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>

View File

@ -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 = "<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();
}
}