[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 | | 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 | | cycle | String | Current mission: none, clean, spot | Y |
| phase | String | Current phase of the mission; see below. | Y | | phase | String | Current phase of the mission; see below. | Y |
| battery | Number | Battery charge in percents | 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 | | always_finish | Switch | Whether to keep cleaning if the bin becomes full | N |
| power_boost | String | Power boost mode: "auto", "performance", "eco" | N | | power_boost | String | Power boost mode: "auto", "performance", "eco" | N |
| clean_passes | String | Number of cleaning passes: "auto", "1", "2" | 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: 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 | | 75 | Navigation problem |
| 76 | Hardware problem detected | | 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 ## Known Problems / Caveats
1. Sending "pause" command during missions other than "clean" is equivalent to sending "stop" 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_ALWAYS_FINISH = "always_finish";
public static final String CHANNEL_POWER_BOOST = "power_boost"; public static final String CHANNEL_POWER_BOOST = "power_boost";
public static final String CHANNEL_CLEAN_PASSES = "clean_passes"; 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 = "clean";
public static final String CMD_CLEAN_REGIONS = "cleanRegions";
public static final String CMD_SPOT = "spot"; public static final String CMD_SPOT = "spot";
public static final String CMD_DOCK = "dock"; public static final String CMD_DOCK = "dock";
public static final String CMD_PAUSE = "pause"; public static final String CMD_PAUSE = "pause";

View File

@ -12,10 +12,17 @@
*/ */
package org.openhab.binding.irobot.internal.dto; 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 * iRobot MQTT protocol messages
* *
* @author Pavel Fedin - Initial contribution * @author Pavel Fedin - Initial contribution
* @author Florian Binder - Added CleanRoomsRequest
* *
*/ */
public class MQTTProtocol { public class MQTTProtocol {
@ -23,6 +30,29 @@ public class MQTTProtocol {
public String getTopic(); 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 static class CommandRequest implements Request {
public String command; public String command;
public long time; public long time;
@ -31,7 +61,7 @@ public class MQTTProtocol {
public CommandRequest(String cmd) { public CommandRequest(String cmd) {
command = cmd; command = cmd;
time = System.currentTimeMillis() / 1000; time = System.currentTimeMillis() / 1000;
initiator = "localApp"; initiator = "openhab";
} }
@Override @Override
@ -56,6 +86,7 @@ public class MQTTProtocol {
public static class CleanMissionStatus { public static class CleanMissionStatus {
public String cycle; public String cycle;
public String phase; public String phase;
public String initiator;
public int error; public int error;
} }
@ -183,6 +214,9 @@ public class MQTTProtocol {
public String bootloaderVer; public String bootloaderVer;
// "umiVer":"6", // "umiVer":"6",
public String umiVer; 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>}} // 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.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -65,6 +66,7 @@ import com.google.gson.stream.JsonReader;
* *
* @author hkuhn42 - Initial contribution * @author hkuhn42 - Initial contribution
* @author Pavel Fedin - Rewrite for 900 series * @author Pavel Fedin - Rewrite for 900 series
* @author Florian Binder - added cleanRegions command and lastCommand channel
*/ */
@NonNullByDefault @NonNullByDefault
public class RoombaHandler extends BaseThingHandler implements MqttConnectionObserver, MqttMessageSubscriber { public class RoombaHandler extends BaseThingHandler implements MqttConnectionObserver, MqttMessageSubscriber {
@ -128,7 +130,24 @@ public class RoombaHandler extends BaseThingHandler implements MqttConnectionObs
cmd = isPaused ? "resume" : "start"; 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)) { } else if (ch.startsWith(CHANNEL_SCHED_SWITCH_PREFIX)) {
MQTTProtocol.Schedule schedule = lastSchedule; 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(Thing.PROPERTY_FIRMWARE_VERSION, reported.softwareVer);
reportProperty("navSwVer", reported.navSwVer); reportProperty("navSwVer", reported.navSwVer);
reportProperty("wifiSwVer", reported.wifiSwVer); reportProperty("wifiSwVer", reported.wifiSwVer);

View File

@ -10,6 +10,7 @@
<channels> <channels>
<channel id="command" typeId="command"/> <channel id="command" typeId="command"/>
<channel id="last_command" typeId="lastCommand"/>
<channel id="cycle" typeId="cycle"/> <channel id="cycle" typeId="cycle"/>
<channel id="phase" typeId="phase"/> <channel id="phase" typeId="phase"/>
<channel id="battery" typeId="battery"/> <channel id="battery" typeId="battery"/>
@ -255,4 +256,12 @@
</state> </state>
</channel-type> </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> </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();
}
}