[ecovacs] Add Deebot T30 (PRO) OMNI and support Scenario Cleaning (#17162)

* [ecovacs] Add Deebot T30 (PRO) OMNI

Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
This commit is contained in:
jimtng
2025-08-03 14:04:28 +02:00
committed by GitHub
parent a358bf83dd
commit b57b1caf60
10 changed files with 124 additions and 39 deletions
+17 -15
View File
@@ -16,6 +16,7 @@ At this point, the following devices are fully supported and verified to be work
- Deebot OZMO 950
- Deebot OZMO Slim 10/11
- Deebot N8 series
- Deebot T30 OMNI
The following devices will likely work because they are using similar protocols as the above ones:
@@ -92,33 +93,34 @@ In case a particular channel is not supported by a given device (see remarks), i
Remarks:
- [1] See [section below](#command-channel-actions)
- [2] Possible states: 'cleaning', 'pause', 'stop', 'drying', 'washing', 'returning' and 'charging' (where 'drying' and 'washing' are only available on newer models with auto empty station)
- [3] Possible states: 'auto', 'edge', 'spot', 'spotArea', 'customArea', 'singleRoom' (some of which depend on device capabilities)
- [2] Possible states: `cleaning`, `pause`, `stop`, `autoEmpty`, `drying`, `washing`, `returning` and `charging` (where `autoEmpty`, `drying` and `washing` are only available on newer models with auto empty station)
- [3] Possible states: `auto`, `edge`, `spot`, `spotArea`, `customArea`, `singleRoom`, and `sceneClean` (some of which depend on device capabilities)
- [4] Current cleaning status is only valid if the device is currently cleaning
- [5] Only valid for 'spot', 'spotArea' and 'customArea' cleaning modes; value can be used for 'spotArea' and 'customArea' commands (see below)
- [5] Only valid for `spot`, `spotArea`, `customArea`, and `sceneClean` cleaning modes; value can be used for `spotArea`, `customArea`, or `sceneClean` commands (see below)
- [6] Only present if device has a mopping system
- [7] Only present on newer generation devices (Deebot OZMO 950 and newer)
- [8] Only present if device has a main brush
- [9] Only present on newer generation devices (Deebot N8/T8 or newer)
- [10] Only present if device has a dustbin auto empty station; supports both on/off command (to turn on/off the setting) and the string 'trigger' (to trigger immediate auto empty)
- [11] Only present if device can control power level. Possible values vary by device: 'normal' and 'high' are always supported, 'silent' and 'higher' are supported for some models
- [10] Only present if device has a dustbin auto empty station; supports both on/off command (to turn on/off the setting) and the string `trigger` (to trigger immediate auto empty)
- [11] Only present if device can control power level. Possible values vary by device: `normal` and `high` are always supported, `silent` and `higher` are supported for some models
- [12] Only present if device supports True Detect 3D
- [13] Only present if device has voice reporting
- [14] Only present if device has a mopping system. Possible values include 'low', 'medium', 'high' and 'veryhigh'
- [14] Only present if device has a mopping system. Possible values include `low`, `medium`, `high` and `veryhigh`
## Command Channel Actions
The following actions are supported by the `command` channel:
| Name | Action | Remarks |
|--------------|-------------------------------------------|------------------------------------------------------|
| `clean` | Start cleaning in automatic mode. | |
| `spotArea` | Start cleaning specific rooms. | <ul><li>Only if supported by device, which can be recognized by `spotArea` being present in the list of possible states of the `current-cleaning-mode` channel.</li><li>Format: `spotArea:<room IDs>`, where `room IDs` is a semicolon separated list of room letters as shown in Ecovacs' app, so a valid command could e.g. be `spotArea:A;D;E`.</li><li>If you want to run 2 clean passes, amend `:x2` to the command, e.g. `spotArea:A;C;B:x2`.</li></ul> |
| `customArea` | Start cleaning specific areas. | <ul><li>Only if supported by device, which can be recognized by `customArea` being present in the list of possible states of the `current-cleaning-mode` channel.</li><li>Format: `customArea:<x1>;<y1>;<x2>;<y2>`, where the parameters are coordinates (in mm) relative to the map.</li><li>The coordinates can be obtained from the `current-cleaning-spot-definition` channel when starting a custom area run from the app.</li><li>If you want to run 2 clean passes, amend `:x2` to the command, e.g. `customArea:100;100;1000;1000:x2`.</li></ul> |
| `pause` | Pause cleaning if it's currently active. | If the device is idle, the command is ignored. |
| `resume` | Resume cleaning if it's currently paused. | If the device is not paused, the command is ignored. |
| `stop` | Stop cleaning immediately. | |
| `charge` | Send device to charging station. | |
| Name | Action | Remarks |
|--------------|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `clean` | Start cleaning in automatic mode. | |
| `spotArea` | Start cleaning specific rooms. | <ul><li>Only if supported by device, which can be recognized by `spotArea` being present in the list of possible states of the `current-cleaning-mode` channel.</li><li>Format: `spotArea:<room IDs>`, where `room IDs` is a semicolon separated list of room letters as shown in Ecovacs' app, so a valid command could e.g. be `spotArea:A;D;E`.</li><li>If you want to run 2 clean passes, append `:x2` to the command, e.g. `spotArea:A;C;B:x2`.</li></ul> |
| `customArea` | Start cleaning specific areas. | <ul><li>Only if supported by device, which can be recognized by `customArea` being present in the list of possible states of the `current-cleaning-mode` channel.</li><li>Format: `customArea:<x1>;<y1>;<x2>;<y2>`, where the parameters are coordinates (in mm) relative to the map.</li><li>The coordinates can be obtained from the `current-cleaning-spot-definition` channel when starting a custom area run from the app.</li><li>If you want to run 2 clean passes, append `:x2` to the command, e.g. `customArea:100;100;1000;1000:x2`.</li></ul> |
| `sceneClean` | Start cleaning using a predefined scenario. | <ul><li>Only if supported by device, which can be recognized by `sceneClean` being present in the list of possible states of the `current-cleaning-mode` channel.</li><li>Format: `sceneClean:<scenarioId>`. The `scenarioID` can be obtained from the `current-cleaning-spot-definition` channel when starting a scenario run from the app. Example: `sceneClean:5318`.</li></ul> |
| `pause` | Pause cleaning if it's currently active. | If the device is idle, the command is ignored. |
| `resume` | Resume cleaning if it's currently paused. | If the device is not paused, the command is ignored. |
| `stop` | Stop cleaning immediately. | |
| `charge` | Send device to charging station. | |
## Rule actions
@@ -85,6 +85,7 @@ public class EcovacsBindingConstants {
public static final String CMD_STOP = "stop";
public static final String CMD_SPOT_AREA = "spotArea";
public static final String CMD_CUSTOM_AREA = "customArea";
public static final String CMD_SCENE_CLEAN = "sceneClean";
public static final StateOptionMapping<CleanMode> CLEAN_MODE_MAPPING = StateOptionMapping.of(
new StateOptionEntry<>(CleanMode.AUTO, "auto"),
@@ -93,8 +94,12 @@ public class EcovacsBindingConstants {
new StateOptionEntry<>(CleanMode.SPOT_AREA, "spotArea", DeviceCapability.SPOT_AREA_CLEANING),
new StateOptionEntry<>(CleanMode.CUSTOM_AREA, "customArea", DeviceCapability.CUSTOM_AREA_CLEANING),
new StateOptionEntry<>(CleanMode.SINGLE_ROOM, "singleRoom", DeviceCapability.SINGLE_ROOM_CLEANING),
new StateOptionEntry<>(CleanMode.PAUSE, "pause"), new StateOptionEntry<>(CleanMode.STOP, "stop"),
new StateOptionEntry<>(CleanMode.WASHING, "washing"), new StateOptionEntry<>(CleanMode.DRYING, "drying"),
new StateOptionEntry<>(CleanMode.SCENE_CLEAN, "sceneClean", DeviceCapability.SCENARIO_CLEANING),
new StateOptionEntry<>(CleanMode.PAUSE, "pause"), //
new StateOptionEntry<>(CleanMode.STOP, "stop"), //
new StateOptionEntry<>(CleanMode.AUTO_EMPTY, "autoEmpty"),
new StateOptionEntry<>(CleanMode.WASHING, "washing"), //
new StateOptionEntry<>(CleanMode.DRYING, "drying"),
new StateOptionEntry<>(CleanMode.RETURNING, "returning"));
public static final StateOptionMapping<MoppingWaterAmount> WATER_AMOUNT_MAPPING = StateOptionMapping.of(
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2010-2025 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.ecovacs.internal.api.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Jimmy Tanagra - Initial contribution
*/
@NonNullByDefault
public class SceneCleaningCommand extends AbstractAreaCleaningCommand {
public SceneCleaningCommand(String sceneDefinition, int cleanPasses) {
super("sceneClean", sceneDefinition, cleanPasses);
}
}
@@ -59,7 +59,8 @@ class JsonReportParser implements ReportParser {
} catch (JsonSyntaxException e) {
// The onFwBuryPoint-bd_sysinfo sends a JSON array instead of the expected JsonResponsePayloadBody object.
// Since we don't do anything with it anyway, just ignore it
logger.debug("{}: Got invalid JSON message payload, ignoring: {}", device.getSerialNumber(), payload, e);
logger.debug("{}: Got invalid JSON data ({}), ignoring payload: {}", device.getSerialNumber(),
e.getMessage(), payload);
response = null;
}
if (response == null) {
@@ -33,25 +33,29 @@ public enum CleanMode {
CUSTOM_AREA,
@SerializedName("singleRoom")
SINGLE_ROOM,
@SerializedName("sceneClean")
SCENE_CLEAN,
@SerializedName("pause")
PAUSE,
@SerializedName("stop")
STOP,
@SerializedName(value = "going", alternate = { "goCharging" })
RETURNING,
@SerializedName("autoEmpty")
AUTO_EMPTY,
@SerializedName("washing")
WASHING,
@SerializedName("drying")
@SerializedName(value = "drying", alternate = { "spin-dry" })
DRYING,
@SerializedName("idle")
IDLE;
public boolean isActive() {
return this == AUTO || this == EDGE || this == SPOT || this == SPOT_AREA || this == CUSTOM_AREA
|| this == SINGLE_ROOM;
|| this == SINGLE_ROOM || this == SCENE_CLEAN;
}
public boolean isIdle() {
return this == IDLE || this == DRYING || this == WASHING;
return this == IDLE || this == DRYING || this == WASHING || this == AUTO_EMPTY;
}
}
@@ -35,6 +35,8 @@ public enum DeviceCapability {
CUSTOM_AREA_CLEANING,
@SerializedName("single_room_cleaning")
SINGLE_ROOM_CLEANING,
@SerializedName("scenario_cleaning")
SCENARIO_CLEANING,
@SerializedName("clean_speed_control")
CLEAN_SPEED_CONTROL,
@SerializedName("mapping")
@@ -55,6 +55,7 @@ import org.openhab.binding.ecovacs.internal.api.commands.GoChargingCommand;
import org.openhab.binding.ecovacs.internal.api.commands.PauseCleaningCommand;
import org.openhab.binding.ecovacs.internal.api.commands.PlaySoundCommand;
import org.openhab.binding.ecovacs.internal.api.commands.ResumeCleaningCommand;
import org.openhab.binding.ecovacs.internal.api.commands.SceneCleaningCommand;
import org.openhab.binding.ecovacs.internal.api.commands.SetContinuousCleaningCommand;
import org.openhab.binding.ecovacs.internal.api.commands.SetDefaultCleanPassesCommand;
import org.openhab.binding.ecovacs.internal.api.commands.SetDustbinAutoEmptyCommand;
@@ -530,6 +531,7 @@ public class EcovacsVacuumHandler extends BaseThingHandler implements EcovacsDev
} catch (ConfigurationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getRawMessage());
} catch (EcovacsApiException e) {
logger.debug("API Exception: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
@@ -673,7 +675,8 @@ public class EcovacsVacuumHandler extends BaseThingHandler implements EcovacsDev
// Some devices already report charging state while returning to charging station, make sure to not report
// charging in that case. The same applies for models with pad washing/drying station, as those states imply
// the device being charging.
if (cleanMode != CleanMode.RETURNING && cleanMode != CleanMode.WASHING && cleanMode != CleanMode.DRYING) {
if (cleanMode != CleanMode.RETURNING && cleanMode != CleanMode.WASHING && cleanMode != CleanMode.DRYING
&& cleanMode != CleanMode.AUTO_EMPTY) {
return "charging";
}
}
@@ -693,21 +696,15 @@ public class EcovacsVacuumHandler extends BaseThingHandler implements EcovacsDev
if (charging) {
return CMD_CHARGE;
}
switch (cleanMode) {
case AUTO:
return CMD_AUTO_CLEAN;
case SPOT_AREA:
return CMD_SPOT_AREA;
case PAUSE:
return CMD_PAUSE;
case STOP:
return CMD_STOP;
case RETURNING:
return CMD_CHARGE;
default:
break;
}
return null;
return switch (cleanMode) {
case AUTO -> CMD_AUTO_CLEAN;
case SPOT_AREA -> CMD_SPOT_AREA;
case SCENE_CLEAN -> CMD_SCENE_CLEAN;
case PAUSE -> CMD_PAUSE;
case STOP -> CMD_STOP;
case RETURNING -> CMD_CHARGE;
default -> null;
};
}
private State stringToState(@Nullable String value) {
@@ -755,7 +752,7 @@ public class EcovacsVacuumHandler extends BaseThingHandler implements EcovacsDev
device.hasCapability(DeviceCapability.FREE_CLEAN_FOR_SPOT_AREA));
}
} else {
logger.info("{}: spotArea command needs to have the form spotArea:<room1>[;<room2>][;<...roomX>][:x2]",
logger.warn("{}: spotArea command needs to have the form spotArea:<room1>[;<room2>][;<...roomX>][:x2]",
serialNumber);
}
}
@@ -769,9 +766,19 @@ public class EcovacsVacuumHandler extends BaseThingHandler implements EcovacsDev
return new CustomAreaCleaningCommand(String.join(",", splittedAreaDef), passes);
}
}
logger.info("{}: customArea command needs to have the form customArea:<x1>;<y1>;<x2>;<y2>[:x2]",
logger.warn("{}: customArea command needs to have the form customArea:<x1>;<y1>;<x2>;<y2>[:x2]",
serialNumber);
}
if (command.startsWith(CMD_SCENE_CLEAN) && device.hasCapability(DeviceCapability.SCENARIO_CLEANING)) {
String[] splitted = command.split(":");
if (splitted.length == 2) {
String scenarioId = splitted[1];
// Setting passes > 1 does not seem to have any effect (tested on T30).
return new SceneCleaningCommand(scenarioId, 1);
}
logger.warn("{}: {} command needs to have the form {}:<scenarioId>", serialNumber, CMD_SCENE_CLEAN,
CMD_SCENE_CLEAN);
}
return null;
}
@@ -124,6 +124,7 @@ ecovacs.cleaning-mode.spot = Spot cleaning
ecovacs.cleaning-mode.spotArea = Spot area cleaning
ecovacs.cleaning-mode.customArea = Custom area cleaning
ecovacs.cleaning-mode.singleRoom = Single room cleaning
ecovacs.cleaning-mode.sceneClean = Scenario cleaning
# error codes
@@ -151,6 +151,7 @@
<option value="spotArea">@text/ecovacs.cleaning-mode.spotArea</option>
<option value="customArea">@text/ecovacs.cleaning-mode.customArea</option>
<option value="singleRoom">@text/ecovacs.cleaning-mode.singleRoom</option>
<option value="sceneClean">@text/ecovacs.cleaning-mode.sceneClean</option>
</options>
</state>
</channel-type>
@@ -615,5 +615,42 @@
"modelName": "DEEBOT X2 PRO OMNI",
"deviceClass": "ip3mmy",
"deviceClassLink": "e6ofmn"
},
{
"modelName": "DEEBOT T30 OMNI",
"deviceClass": "z4lvk7",
"protoVersion": "json_v2",
"usesMqtt": true,
"capabilities": [
"mopping_system",
"main_brush",
"spot_area_cleaning",
"custom_area_cleaning",
"scenario_cleaning",
"clean_speed_control",
"voice_reporting",
"read_network_info",
"unit_care_lifespan",
"true_detect_3d",
"mapping",
"auto_empty_station",
"uses_clean_results_log_api"
]
},
{
"modelName": "DEEBOT T30 PRO OMNI",
"deviceClass": "4vhygi",
"deviceClassLink": "z4lvk7"
},
{
"modelName": "DEEBOT T30 PRO OMNI",
"deviceClass": "3w7j5e",
"deviceClassLink": "z4lvk7"
},
{
"modelName": "DEEBOT T30 PRO OMNI",
"deviceClass": "tlthqk",
"deviceClassLink": "z4lvk7"
}
]