mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[ecovacs] Add support for Deebot X8 (#19750)
* [ecovacs] Handle error JSON during map image download Signed-off-by: Danny Baumann <dannybaumann@web.de>
This commit is contained in:
@@ -67,6 +67,7 @@ In case a particular channel is not supported by a given device (see remarks), i
|
||||
| Channel | Type | Description | Read Only | Updated By | Remarks |
|
||||
|-----------------------------------------|----------------------|-----------------------------------------------------------|-----------|------------|----------|
|
||||
| actions#command | String | Command to execute | No | Event | [1] |
|
||||
| status#battery | Number | Current battery level | Yes | Event | |
|
||||
| status#state | String | Current operational state | Yes | Event | [2] |
|
||||
| status#current-cleaning-mode | String | Mode used in current cleaning run | Yes | Event | [3], [4] |
|
||||
| status#current-cleaning-time | Number:Time | Time spent in current cleaning run | Yes | Event | [4] |
|
||||
@@ -94,6 +95,7 @@ In case a particular channel is not supported by a given device (see remarks), i
|
||||
| settings#true-detect-3d | Switch | Whether True Detect 3D is enabled | No | Polling | [13] |
|
||||
| settings#voice-volume | Dimmer | The voice volume level in percent | No | Polling | [14] |
|
||||
| settings#water-amount | String | The amount of water to be used when mopping | No | Polling | [15] |
|
||||
| settings#water-amount-percent | Dimmer | The amount of water to be used when mopping (in percent) | No | Polling | [16] |
|
||||
|
||||
Remarks:
|
||||
|
||||
@@ -112,6 +114,7 @@ Remarks:
|
||||
- [13] Only present if device supports True Detect 3D
|
||||
- [14] Only present if device has voice reporting
|
||||
- [15] Only present if device has a mopping system. Possible values include `low`, `medium`, `high` and `veryhigh`
|
||||
- [16] Only present on Deebot X8 or newer.
|
||||
|
||||
## Command Channel Actions
|
||||
|
||||
|
||||
+1
@@ -74,6 +74,7 @@ public class EcovacsBindingConstants {
|
||||
public static final String CHANNEL_ID_VOICE_VOLUME = "settings#voice-volume";
|
||||
public static final String CHANNEL_ID_WATER_PLATE_PRESENT = "status#water-system-present";
|
||||
public static final String CHANNEL_ID_WATER_AMOUNT = "settings#water-amount";
|
||||
public static final String CHANNEL_ID_WATER_AMOUNT_PERCENT = "settings#water-amount-percent";
|
||||
public static final String CHANNEL_ID_WIFI_RSSI = "status#wifi-rssi";
|
||||
|
||||
public static final String CHANNEL_TYPE_ID_CLEAN_MODE = "current-cleaning-mode";
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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;
|
||||
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
|
||||
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.WaterInfoReport;
|
||||
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.AbstractPortalIotCommandResponse;
|
||||
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalIotCommandJsonResponse;
|
||||
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* @author Danny Baumann - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GetCustomMoppingWaterAmountCommand extends IotDeviceCommand<Integer> {
|
||||
@Override
|
||||
public String getName(ProtocolVersion version) {
|
||||
if (version != ProtocolVersion.JSON_V2) {
|
||||
throw new IllegalStateException("Get custom water amount is only supported for JSON V2");
|
||||
}
|
||||
return "getWaterInfo";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer convertResponse(AbstractPortalIotCommandResponse response, ProtocolVersion version, Gson gson)
|
||||
throws DataParsingException {
|
||||
PortalIotCommandJsonResponse jsonResponse = (PortalIotCommandJsonResponse) response;
|
||||
WaterInfoReport resp = jsonResponse.getResponsePayloadAs(gson, WaterInfoReport.class);
|
||||
return resp.customWaterAmount;
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ecovacs.internal.api.impl.ProtocolVersion;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* @author Danny Baumann - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SetCustomMoppingWaterAmountCommand extends AbstractNoResponseCommand {
|
||||
private final int amount;
|
||||
|
||||
public SetCustomMoppingWaterAmountCommand(int amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(ProtocolVersion version) {
|
||||
if (version != ProtocolVersion.JSON_V2) {
|
||||
throw new IllegalStateException("Set custom water amount is only supported for JSON V2");
|
||||
}
|
||||
return "setWaterInfo";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable JsonElement getJsonPayloadArgs(ProtocolVersion version) {
|
||||
JsonObject args = new JsonObject();
|
||||
args.addProperty("customAmount", amount);
|
||||
return args;
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -346,10 +346,16 @@ public final class EcovacsApiImpl implements EcovacsApi {
|
||||
return responseObj.records;
|
||||
}
|
||||
|
||||
public byte[] downloadCleanMapImage(String url, boolean useSigning)
|
||||
public Optional<byte[]> downloadCleanMapImage(Device device, String url, boolean useSigning)
|
||||
throws EcovacsApiException, InterruptedException {
|
||||
Request request = useSigning ? createSignedAppRequest(url) : httpClient.newRequest(url).method(HttpMethod.GET);
|
||||
return executeRequest(request).getContent();
|
||||
ContentResponse response = executeRequest(request);
|
||||
if ("application/json".equals(response.getMediaType())) {
|
||||
logger.warn("{}: Could not download map image {}: {}", device.getName(), url,
|
||||
response.getContentAsString());
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(response.getContent());
|
||||
}
|
||||
|
||||
private PortalAuthRequestParameter createAuthData() {
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ public class EcovacsIotMqDevice implements EcovacsDevice {
|
||||
return Optional.empty();
|
||||
}
|
||||
boolean needsSigning = hasCapability(DeviceCapability.USES_CLEAN_RESULTS_LOG_API);
|
||||
return Optional.of(api.downloadCleanMapImage(record.mapImageUrl.get(), needsSigning));
|
||||
return api.downloadCleanMapImage(device, record.mapImageUrl.get(), needsSigning);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+10
@@ -25,6 +25,7 @@ import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json
|
||||
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.ErrorReport;
|
||||
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.StatsReport;
|
||||
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.WaterInfoReport;
|
||||
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.deviceapi.json.WorkStateReport;
|
||||
import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalIotCommandJsonResponse.JsonResponsePayloadWrapper;
|
||||
import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
|
||||
import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
|
||||
@@ -128,6 +129,15 @@ class JsonReportParser implements ReportParser {
|
||||
listener.onWaterSystemPresentUpdated(device, report.waterPlatePresent != 0);
|
||||
break;
|
||||
}
|
||||
case "workstate": {
|
||||
WorkStateReport report = payloadAs(response, WorkStateReport.class);
|
||||
CleanMode mode = report.determineCleanMode(gson);
|
||||
if (mode == null) {
|
||||
throw new DataParsingException("Could not get clean mode from response " + payload);
|
||||
}
|
||||
handleCleanModeChange(mode, null);
|
||||
break;
|
||||
}
|
||||
// more possible events (unused for now):
|
||||
// - "evt" -> EventReport
|
||||
// - "lifespan" -> ComponentLifeSpanReport
|
||||
|
||||
+3
-1
@@ -21,5 +21,7 @@ public class WaterInfoReport {
|
||||
@SerializedName("enable")
|
||||
public int waterPlatePresent;
|
||||
@SerializedName("amount")
|
||||
public int waterAmount;
|
||||
public Integer waterAmount; // enum (1-4)
|
||||
@SerializedName("customAmount")
|
||||
public Integer customWaterAmount; // numeric value (0-100)
|
||||
}
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.impl.dto.response.deviceapi.json;
|
||||
|
||||
import org.openhab.binding.ecovacs.internal.api.model.CleanMode;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* @author Danny Baumann - Initial contribution
|
||||
*/
|
||||
public class WorkStateReport {
|
||||
@SerializedName("paused")
|
||||
public int paused;
|
||||
@SerializedName("robotState")
|
||||
public RobotState robotState;
|
||||
@SerializedName("stationState")
|
||||
public DeviceState stationState;
|
||||
|
||||
public static class DeviceState {
|
||||
@SerializedName("trigger")
|
||||
public String trigger; // app, workComplete, voice, ...?
|
||||
@SerializedName("state")
|
||||
public String state;
|
||||
}
|
||||
|
||||
public static class RobotState extends DeviceState {
|
||||
@SerializedName("cleanState")
|
||||
public CleanStateReport cleanState;
|
||||
}
|
||||
|
||||
public static class CleanStateReport {
|
||||
@SerializedName("cid")
|
||||
public String id;
|
||||
@SerializedName("type")
|
||||
public String type;
|
||||
}
|
||||
|
||||
public CleanMode determineCleanMode(Gson gson) {
|
||||
if (paused != 0) {
|
||||
return CleanMode.PAUSE;
|
||||
}
|
||||
final String modeValue;
|
||||
if ("cleaning".equals(robotState.state) && robotState.cleanState != null) {
|
||||
modeValue = robotState.cleanState.type;
|
||||
} else if ("idle".equals(robotState.state)) {
|
||||
modeValue = stationState.state;
|
||||
} else {
|
||||
modeValue = robotState.state;
|
||||
}
|
||||
return gson.fromJson(modeValue, CleanMode.class);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -33,7 +33,7 @@ public enum CleanMode {
|
||||
CUSTOM_AREA,
|
||||
@SerializedName("singleRoom")
|
||||
SINGLE_ROOM,
|
||||
@SerializedName("sceneClean")
|
||||
@SerializedName(value = "sceneClean", alternate = { "qcClean" })
|
||||
SCENE_CLEAN,
|
||||
@SerializedName("pause")
|
||||
PAUSE,
|
||||
@@ -41,7 +41,7 @@ public enum CleanMode {
|
||||
STOP,
|
||||
@SerializedName(value = "going", alternate = { "goCharging" })
|
||||
RETURNING,
|
||||
@SerializedName("autoEmpty")
|
||||
@SerializedName(value = "autoEmpty", alternate = { "emptying" })
|
||||
EMPTYING,
|
||||
@SerializedName("washing")
|
||||
WASHING,
|
||||
|
||||
+2
@@ -55,6 +55,8 @@ public enum DeviceCapability {
|
||||
ROUND_MOP_LIFESPAN,
|
||||
@SerializedName("air_drying")
|
||||
AIR_DRYING,
|
||||
@SerializedName("custom_water_amount")
|
||||
CUSTOM_WATER_AMOUNT,
|
||||
// implicit capabilities added in code
|
||||
EDGE_CLEANING,
|
||||
SPOT_CLEANING,
|
||||
|
||||
+18
-2
@@ -40,6 +40,7 @@ import org.openhab.binding.ecovacs.internal.api.commands.GetChargeStateCommand;
|
||||
import org.openhab.binding.ecovacs.internal.api.commands.GetCleanStateCommand;
|
||||
import org.openhab.binding.ecovacs.internal.api.commands.GetComponentLifeSpanCommand;
|
||||
import org.openhab.binding.ecovacs.internal.api.commands.GetContinuousCleaningCommand;
|
||||
import org.openhab.binding.ecovacs.internal.api.commands.GetCustomMoppingWaterAmountCommand;
|
||||
import org.openhab.binding.ecovacs.internal.api.commands.GetDefaultCleanPassesCommand;
|
||||
import org.openhab.binding.ecovacs.internal.api.commands.GetDustbinAutoEmptyCommand;
|
||||
import org.openhab.binding.ecovacs.internal.api.commands.GetErrorCommand;
|
||||
@@ -57,6 +58,7 @@ 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.SetCustomMoppingWaterAmountCommand;
|
||||
import org.openhab.binding.ecovacs.internal.api.commands.SetDefaultCleanPassesCommand;
|
||||
import org.openhab.binding.ecovacs.internal.api.commands.SetDustbinAutoEmptyCommand;
|
||||
import org.openhab.binding.ecovacs.internal.api.commands.SetMoppingWaterAmountCommand;
|
||||
@@ -187,6 +189,9 @@ public class EcovacsVacuumHandler extends BaseThingHandler implements EcovacsDev
|
||||
device.sendCommand(new SetMoppingWaterAmountCommand(amount.get()));
|
||||
return;
|
||||
}
|
||||
} else if (channel.equals(CHANNEL_ID_WATER_AMOUNT_PERCENT) && command instanceof PercentType percent) {
|
||||
device.sendCommand(new SetCustomMoppingWaterAmountCommand(percent.intValue()));
|
||||
return;
|
||||
} else if (channel.equals(CHANNEL_ID_AUTO_EMPTY)) {
|
||||
if (command instanceof OnOffType) {
|
||||
device.sendCommand(new SetDustbinAutoEmptyCommand(command == OnOffType.ON));
|
||||
@@ -411,7 +416,12 @@ public class EcovacsVacuumHandler extends BaseThingHandler implements EcovacsDev
|
||||
|
||||
if (!device.hasCapability(DeviceCapability.MOPPING_SYSTEM)) {
|
||||
hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_WATER_AMOUNT);
|
||||
hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_WATER_AMOUNT_PERCENT);
|
||||
hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_WATER_PLATE_PRESENT);
|
||||
} else if (device.hasCapability(DeviceCapability.CUSTOM_WATER_AMOUNT)) {
|
||||
hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_WATER_AMOUNT);
|
||||
} else {
|
||||
hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_WATER_AMOUNT_PERCENT);
|
||||
}
|
||||
if (!device.hasCapability(DeviceCapability.CLEAN_SPEED_CONTROL)) {
|
||||
hasChanges |= removeUnsupportedChannel(builder, CHANNEL_ID_SUCTION_POWER);
|
||||
@@ -615,8 +625,14 @@ public class EcovacsVacuumHandler extends BaseThingHandler implements EcovacsDev
|
||||
}
|
||||
|
||||
if (device.hasCapability(DeviceCapability.MOPPING_SYSTEM)) {
|
||||
MoppingWaterAmount waterAmount = device.sendCommand(new GetMoppingWaterAmountCommand());
|
||||
updateState(CHANNEL_ID_WATER_AMOUNT, new StringType(WATER_AMOUNT_MAPPING.getMappedValue(waterAmount)));
|
||||
if (device.hasCapability(DeviceCapability.CUSTOM_WATER_AMOUNT)) {
|
||||
Integer waterAmount = device.sendCommand(new GetCustomMoppingWaterAmountCommand());
|
||||
updateState(CHANNEL_ID_WATER_AMOUNT_PERCENT, new PercentType(waterAmount));
|
||||
} else {
|
||||
MoppingWaterAmount waterAmount = device.sendCommand(new GetMoppingWaterAmountCommand());
|
||||
updateState(CHANNEL_ID_WATER_AMOUNT,
|
||||
new StringType(WATER_AMOUNT_MAPPING.getMappedValue(waterAmount)));
|
||||
}
|
||||
}
|
||||
|
||||
if (device.hasCapability(DeviceCapability.READ_NETWORK_INFO)) {
|
||||
|
||||
@@ -108,6 +108,7 @@ channel-type.ecovacs.true-detect-3d.label = True Detect 3D
|
||||
channel-type.ecovacs.true-detect-3d.description = Enable the True Detect 3D object recognition technology
|
||||
channel-type.ecovacs.voice-volume.label = Voice Volume
|
||||
channel-type.ecovacs.voice-volume.description = Volume level of voice reports
|
||||
channel-type.ecovacs.water-amount-percent.label = Mopping Water Amount
|
||||
channel-type.ecovacs.water-amount.label = Mopping Water Amount
|
||||
channel-type.ecovacs.water-amount.state.option.low = Low
|
||||
channel-type.ecovacs.water-amount.state.option.medium = Medium
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
<label>Settings</label>
|
||||
<channels>
|
||||
<channel id="water-amount" typeId="water-amount"/>
|
||||
<channel id="water-amount-percent" typeId="water-amount-percent"/>
|
||||
<channel id="suction-power" typeId="suction-power"/>
|
||||
<channel id="voice-volume" typeId="voice-volume"/>
|
||||
<channel id="auto-empty" typeId="auto-empty"/>
|
||||
@@ -346,6 +347,12 @@
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="water-amount-percent" advanced="true">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Mopping Water Amount</label>
|
||||
<state pattern="%d%%" min="1" max="100"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="wifi-rssi" advanced="true">
|
||||
<item-type unitHint="dBm">Number:Power</item-type>
|
||||
<label>Wi-Fi Signal Strength</label>
|
||||
|
||||
+31
@@ -949,5 +949,36 @@
|
||||
"modelName": "DEEBOT N30 PRO OMNI",
|
||||
"deviceClass": "87swps",
|
||||
"deviceClassLink": "z4lvk7"
|
||||
},
|
||||
|
||||
{
|
||||
"modelName": "DEEBOT X8 PRO OMNI",
|
||||
"deviceClass": "n0vyif",
|
||||
"protoVersion": "json_v2",
|
||||
"usesMqtt": true,
|
||||
"capabilities": [
|
||||
"mopping_system",
|
||||
"main_brush",
|
||||
"spot_area_cleaning",
|
||||
"free_clean_command",
|
||||
"custom_area_cleaning",
|
||||
"scenario_cleaning",
|
||||
"clean_speed_control",
|
||||
"voice_reporting",
|
||||
"read_network_info",
|
||||
"unit_care_lifespan",
|
||||
"round_mop_lifespan",
|
||||
"true_detect_3d",
|
||||
"mapping",
|
||||
"auto_empty_station",
|
||||
"air_drying",
|
||||
"uses_clean_results_log_api",
|
||||
"custom_water_amount"
|
||||
]
|
||||
},
|
||||
{
|
||||
"modelName": "DEEBOT X8 OMNI",
|
||||
"deviceClass": "4bx3w9",
|
||||
"deviceClassLink": "n0vyif"
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user