client.py icon indicating copy to clipboard operation
client.py copied to clipboard

Support for DEEBOT X8 PRO OMNI

Open ytorres opened this issue 10 months ago • 25 comments

Checks

  • [x] I have searched the existing issues and no issue is describing my issue
  • [x] I have checked the FAQ
  • [x] I have checked the documentation
  • [x] I have installed the latest version

The problem

Device "DEEBOT X8 PRO OMNI" not supported. More information at https://github.com/DeebotUniverse/client.py/issues/612: {'did': 'f56f062e-8100-44b8-a027-1166cfa6482f', 'name': 'E0C235557F1FPNV20270', 'class': 'n0vyif', 'resource': '4oLeIYS8', 'company': 'eco-ng', 'bindTs': 1739616918621, 'service': {'jmq': 'jmq-ngiot-eu.dc.ww.ecouser.net', 'mqs': 'api-ngiot.dc-eu.ww.ecouser.net'}, 'deviceName': 'DEEBOT X8 PRO OMNI', 'icon': 'https://portal-ww.ecouser.net/api/pim/file/get/66e3ac63a2928902a25d83a0', 'ota': True, 'UILogicId': 'keplerh_ww_h_keplerh5', 'materialNo': '110-2417-0402', 'pid': '66daaa789dd37cf146cb1d2e', 'product_category': 'DEEBOT', 'model': 'KEPLER_BLACK_AI_INT', 'updateInfo': {'needUpdate': False, 'changeLog': ''}, 'nick': 'Mon pote', 'homeId': '631a26505466082f01388b04', 'homeSort': 1, 'status': 1, 'btName': 'ECOVACS-n0vyif-0270', 'btMac': '28:F5:2B:6C:56:05', 'otaUpgrade': {}}

On which deebot device (vacuum) you have the issue?

DEEBOT X8 PRO OMNI

Which version of the deebot-client are you using?

HA 2025.2.5

Country

Fr

Continent

eu

Anything in the logs that might be useful for us?


Additional information

No response

ytorres avatar Feb 23 '25 18:02 ytorres

Any testing we can do or more logs we can provide to get this vacuum supported?

LeoDevop avatar Mar 28 '25 13:03 LeoDevop

Hello, I have the same vacuum robot and I am experiencing the same issue in Italy (not supported). Let me know if I can help in any way.

MattiaCarli avatar Mar 30 '25 10:03 MattiaCarli

Same issue for me with the Ecovacs Deebot X8 Pro in Germany. Would be great if the flagship model would be supported. If I can help, feel free to contact me.

galmi77 avatar Apr 02 '25 06:04 galmi77

As I don't have the model, I cannot test and ensure it works correctly. Sometimes, Ecovacs changes the API from one model to the other one, and therefore, we cannot make assumptions.

You can search for a similar model in the code base and then try to determine whether that one works, but you may need to modify it slightly. Please be aware that Ecovacs it not sharing anything with this project and so it takes some time until new models are supported. Any contribution is welcome

edenhaus avatar Apr 03 '25 07:04 edenhaus

My model is DEEBOT X8 PRO PLUS AI(In China). Is it similar to X5 Pro Omini (lr4qcs. py). If they are similar, may I ask how to create a symbolic link between the model and similar models, and how to operate it specifically? Please guide me, thank you!

STRHOME avatar Apr 09 '25 01:04 STRHOME

@STRHOME i managed to link X5, X2 and many others to see in my dev box. While a lot of the sensors and functions worked the "Start Cleaning" button and start always threw up this error in the logs.

`Logger: deebot_client.commands.json.common Source: components/ecovacs/vacuum.py:341 First occurred: April 4, 2025 at 9:04:19 PM (2 occurrences) Last logged: April 4, 2025 at 9:04:21 PM

Command "clean" was not successfully. body={'code': 20003, 'msg': 'rcp not support'}`

LeoDevop avatar Apr 09 '25 02:04 LeoDevop

@LeoDevop May I ask how you linked and do you have detailed steps? thank you!

STRHOME avatar Apr 09 '25 03:04 STRHOME

Writing a python script using CleanV2 instructs the robovac to start cleaning but first copy p1jij8.py and rename as n0vyif.py within deebot_client/hardware/deebot/:

cp p1jij8.py n0vyif.py
import aiohttp
import asyncio
import logging
import time

from deebot_client.api_client import ApiClient
from deebot_client.authentication import Authenticator, create_rest_config
from deebot_client.commands.json.clean import CleanAction, CleanV2
from deebot_client.events import BatteryEvent
from deebot_client.mqtt_client import MqttClient, create_mqtt_config
from deebot_client.util import md5
from deebot_client.device import Device

device_id = md5(str(time.time()))
account_id = "your email or phonenumber (cn)"
password_hash = md5("yourPassword")
country = "DE"


async def main():
  async with aiohttp.ClientSession() as session:
    logging.basicConfig(level=logging.DEBUG)
    rest_config = create_rest_config(session, device_id=device_id, alpha_2_country=country)

    authenticator = Authenticator(rest_config, account_id, password_hash)
    api_client = ApiClient(authenticator)

    devices_ = await api_client.get_devices()

    bot = Device(devices_.mqtt[0], authenticator)

    mqtt_config = create_mqtt_config(device_id=device_id, country=country)
    mqtt = MqttClient(mqtt_config, authenticator)
    await bot.initialize(mqtt)

    # Execute commands
    await bot.execute_command(CleanV2(CleanAction.START))

if __name__ == '__main__':
  loop = asyncio.get_event_loop()
  loop.create_task(main())
  loop.run_forever()

So far, I've been able to find that the following commands work: Cleaning Mode:

await bot.execute_command(SetWorkMode(WorkMode.MOP_AFTER_VACUUM))

[<WorkMode.VACUUM_AND_MOP: 0>, <WorkMode.VACUUM: 1>, <WorkMode.MOP: 2>, <WorkMode.MOP_AFTER_VACUUM: 3>]

Suction Power:

await bot.execute_command(SetFanSpeed(FanSpeedLevel.MAX_PLUS))

[<FanSpeedLevel.QUIET: 1000>, <FanSpeedLevel.NORMAL: 0>, <FanSpeedLevel.MAX: 1>, <FanSpeedLevel.MAX_PLUS: 2>]

Cleaning Passes:

await bot.execute_command(SetCleanCount(1))

[<1>, <2>]

Start Cleaning:

await bot.execute_command(CleanV2(CleanAction.START))

Maybe someone with a big more Python knowledge can implement these: Water Flow Rate: customAmount appears to be the set value

Topic: iot/atr/onWaterInfo/<did>/n0vyif/N77sTJy1/j
Payload:
{"body":{"data":{"customAmount":10,"enable":1,"mopCount":2,"sideMop":0,"sweepType":1,"type":1}},"header":{"fwVer":"1.101.0","hwVer":"0.1.1","pri":1,"ts":"1744734574192","tzm":480,"ver":"0.0.1","wkVer":"0.1.54"}}

Cleaning Speed:

Topic: iot/atr/onCustomAreaMode/<did>/n0vyif/N77sTJy1/j
Payload:
{"body":{"data":{"sweepMode":1}},"header":{"fwVer":"1.101.0","hwVer":"0.1.1","pri":1,"ts":"1744734272721","tzm":480,"ver":"0.0.1","wkVer":"0.1.54"}}

I've been able to implement room clean and scenario clean by adding the following to models.py under CleanMode (L83):

    FREE_CLEAN = "freeClean"
    SCENARIO_CLEAN = "qcClean"
    await bot.execute_command(CleanAreaV2(CleanMode.FREE_CLEAN, "1,0"))
    await bot.execute_command(CleanAreaV2(CleanMode.SCENARIO_CLEAN, "5638"))

If you add the below to mqtt_client.py under line 248, then when you make a request from your app. you can see the value for each room or scenario:

        _LOGGER.info("FULL MQTT MESSAGE:\nTopic: %s\nPayload:\n%s", message.topic, message.payload.decode("utf-8", errors="replace"))
        self._last_message_received_at = datetime.now()

For example, freeClean, 1.3 is to clean my Living Room:

{"body":{"data":{"act":"start","content":{"type":"freeClean","value":"1,3"}}},"header":{"channel":"Android","m":"request","pri":2,"reqid":"FhY7rH","ts":"1744735616838","tzc":"Europe/London","tzm":60,"ver":"0.0.22"}}

qcClean, 5395 is to start a scenario:

{"body":{"data":{"content":{"type":"qcClean","value":"5395"},"act":"start"}},"header":{"channel":"Android","m":"request","pri":2,"reqid":"8XEXbC","ts":"1744822549783","tzc":"Europe/London","tzm":60,"ver":"0.0.22"}}

Also added the below to models.py below line 73 to stop and return the robovac to the station:

    STOP_AND_RETURN = "stop_and_return"

Will continue to update this as I discover more.

Note: I'm no expert in this - just crashing my way through!

slflowfoon avatar Apr 16 '25 18:04 slflowfoon

Are there any news on this? I also cant wait for this model to be supported 👼

brstgt avatar May 20 '25 15:05 brstgt

Hi, I have the Deebot X80 Omni (without PRO) I presume the integration might be quite similar to the pro model, so I don't think a separate ticket needed for this. I am looking really forward for this integration.

socke-ber avatar Jun 04 '25 09:06 socke-ber

Has anyone seen this MCP protocol that uses an API? https://github.com/ecovacs-ai/ecovacs-mcp/blob/main/ecovacs_mcp/robot_mcp_stdio.py I got Claude to access info about my Omni X8 Pro from here and offer to control it. Can we leverage this? More info - https://open.ecovacs.com/#/serviceOverview I got an API key by logging into my ecovacs account

wroughtsteel avatar Jun 17 '25 04:06 wroughtsteel

Hello, I have the same vacuum robot and I am experiencing the same issue in Italy (not supported).

GuidoTraversari avatar Jul 21 '25 17:07 GuidoTraversari

I have Deebot X8 Omni PRO. I patched select.py and water-info.py to allow to change water level from 1 to 50.

# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->import re
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->_LOGGER = logging.getLogger(__name__)
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->###################            
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->        # event_bus.notify(WaterAmountEvent(WaterAmount(int(data["amount"]))))
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->        amount = data.get("amount")
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->        custom_amount = data.get("customAmount")
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->        if amount is not None:
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->            try:
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->                event_bus.notify(WaterAmountEvent(WaterAmount(int(amount))))
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->            except ValueError:
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->                _LOGGER.warning("Unknown water amount value: %s", amount)
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->        elif custom_amount is not None:
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->            _LOGGER.info("Custom water amount received: %s", custom_amount)
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->            event_bus.notify(WaterAmountEvent(int(custom_amount)))  # You may define a separate event if needed
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->        else:
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->            _LOGGER.warning("No amount or customAmount in getWaterInfo payload: %s", data)
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->###################            
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->        # if isinstance(amount, str):
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->            # amount = get_enum(WaterAmount, amount)
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->        # params["amount"] = amount.value
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->        if isinstance(amount, str):
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->            match = re.match(r"level[_\s]*(\d+)", amount, re.IGNORECASE)
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->            if match:
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->                params["customAmount"] = int(match.group(1))
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->            else:
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->                # Fallback to enum name (e.g., "HIGH")
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->                amount = get_enum(WaterAmount, amount)
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->                params["amount"] = amount.value
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->        elif isinstance(amount, int):
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->            params["customAmount"] = amount
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->        elif isinstance(amount, WaterAmount):
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->            params["amount"] = amount.value
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->        else:
# \usr\local\lib\python3.13\site-packages\deebot_client\commands\json\water_info.py -->            raise ValueError(f"Unsupported water amount: {amount}")            
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py --># from deebot_client.events.water_info import WaterAmountEvent
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->from deebot_client.events.water_info import WaterAmount,WaterAmountEvent
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->################### 
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->import logging
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->_LOGGER = logging.getLogger(__name__)
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->def current_water_option(e):    
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->    if isinstance(e.value, WaterAmount):
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->        return get_name_key(e.value)
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->    try:
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->        return f"Level {int(e.value)}"
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->    except Exception as ex:
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->        _LOGGER.warning("Failed to interpret custom water amount: %s", ex)
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->        return None
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->def get_water_options(water):
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->    if getattr(water, "types", None):
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->        opts = [get_name_key(amount) for amount in water.types]
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->    else:
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->        opts = [f"Level {i}" for i in range(1, 51)]
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->    return opts
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->################### 
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->        # current_option_fn=lambda e: get_name_key(e.value),
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->        # options_fn=lambda water: [get_name_key(amount) for amount in water.types],
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->        current_option_fn=current_water_option,
# \usr\src\homeassistant\homeassistant\components\ecovacs\select.py -->        options_fn=get_water_options,

Sanji78 avatar Aug 01 '25 22:08 Sanji78

This is instead my working n0vyif.py hardware files


from __future__ import annotations

from deebot_client.capabilities import (
    Capabilities,
    CapabilityClean,
    CapabilityCleanAction,
    CapabilityCustomCommand,
    CapabilityEvent,
    CapabilityExecute,
    CapabilityExecuteTypes,
    CapabilityLifeSpan,
    CapabilityMap,
    CapabilitySet,
    CapabilitySetEnable,
    CapabilitySettings,
    CapabilitySetTypes,
    CapabilityStation,
    CapabilityStats,
    CapabilityWater,
    DeviceType,
)
from deebot_client.commands import StationAction
from deebot_client.commands.json import station_action
from deebot_client.commands.json.advanced_mode import GetAdvancedMode, SetAdvancedMode
from deebot_client.commands.json.auto_empty import GetAutoEmpty, SetAutoEmpty
from deebot_client.commands.json.battery import GetBattery
from deebot_client.commands.json.carpet import (
    GetCarpetAutoFanBoost,
    SetCarpetAutoFanBoost,
)
from deebot_client.commands.json.charge import Charge
from deebot_client.commands.json.charge_state import GetChargeState
from deebot_client.commands.json.child_lock import GetChildLock, SetChildLock
from deebot_client.commands.json.clean import (
    CleanArea,
    CleanV2,
    GetCleanInfoV2,
)
from deebot_client.commands.json.clean_count import GetCleanCount, SetCleanCount
from deebot_client.commands.json.clean_logs import GetCleanLogs
from deebot_client.commands.json.clean_preference import (
    GetCleanPreference,
    SetCleanPreference,
)
from deebot_client.commands.json.continuous_cleaning import (
    GetContinuousCleaning,
    SetContinuousCleaning,
)
from deebot_client.commands.json.custom import CustomCommand
from deebot_client.commands.json.efficiency import GetEfficiencyMode, SetEfficiencyMode
from deebot_client.commands.json.error import GetError
from deebot_client.commands.json.fan_speed import GetFanSpeed, SetFanSpeed
from deebot_client.commands.json.life_span import GetLifeSpan, ResetLifeSpan
from deebot_client.commands.json.map import (
    GetCachedMapInfo,
    GetMajorMap,
    GetMapTrace,
    GetMinorMap,
)
from deebot_client.commands.json.multimap_state import (
    GetMultimapState,
    SetMultimapState,
)
from deebot_client.commands.json.network import GetNetInfo
from deebot_client.commands.json.ota import GetOta, SetOta
from deebot_client.commands.json.play_sound import PlaySound
from deebot_client.commands.json.pos import GetPos
from deebot_client.commands.json.relocation import SetRelocationState
from deebot_client.commands.json.station_state import GetStationState
from deebot_client.commands.json.stats import GetStats, GetTotalStats
from deebot_client.commands.json.sweep_mode import GetSweepMode, SetSweepMode
from deebot_client.commands.json.true_detect import GetTrueDetect, SetTrueDetect
from deebot_client.commands.json.voice_assistant_state import (
    GetVoiceAssistantState,
    SetVoiceAssistantState,
)
from deebot_client.commands.json.volume import GetVolume, SetVolume
from deebot_client.commands.json.water_info import GetWaterInfo, SetWaterInfo, WaterAmountEvent
from deebot_client.commands.json.work_mode import GetWorkMode, SetWorkMode
from deebot_client.const import DataType
from deebot_client.events import (
    AdvancedModeEvent,
    AvailabilityEvent,
    BatteryEvent,
    CachedMapInfoEvent,
    CarpetAutoFanBoostEvent,
    ChildLockEvent,
    CleanCountEvent,
    CleanLogEvent,
    CleanPreferenceEvent,
    ContinuousCleaningEvent,
    CustomCommandEvent,
    EfficiencyModeEvent,
    ErrorEvent,
    FanSpeedEvent,
    FanSpeedLevel,
    LifeSpan,
    LifeSpanEvent,
    MajorMapEvent,
    MapChangedEvent,
    MapTraceEvent,
    MultimapStateEvent,
    NetworkInfoEvent,
    OtaEvent,
    PositionsEvent,
    ReportStatsEvent,
    RoomsEvent,
    StateEvent,
    StationEvent,
    StatsEvent,
    SweepModeEvent,
    TotalStatsEvent,
    TrueDetectEvent,
    VoiceAssistantStateEvent,
    VolumeEvent,
    water_info,
    WorkMode,
    WorkModeEvent,
    auto_empty,
)
from deebot_client.events.auto_empty import AutoEmptyEvent
from deebot_client.events.efficiency_mode import EfficiencyMode
from deebot_client.models import StaticDeviceInfo
from deebot_client.util import short_name

from . import DEVICES

DEVICES[short_name(__name__)] = StaticDeviceInfo(
    DataType.JSON,
    Capabilities(
        device_type=DeviceType.VACUUM,
        availability=CapabilityEvent(
            AvailabilityEvent, [GetBattery(is_available_check=True)]
        ),
        battery=CapabilityEvent(BatteryEvent, [GetBattery()]),
        charge=CapabilityExecute(Charge),
        clean=CapabilityClean(
            action=CapabilityCleanAction(command=CleanV2, area=CleanArea),
            continuous=CapabilitySetEnable(
                ContinuousCleaningEvent,
                [GetContinuousCleaning()],
                SetContinuousCleaning,
            ),
            count=CapabilitySet(CleanCountEvent, [GetCleanCount()], SetCleanCount),
            log=CapabilityEvent(CleanLogEvent, [GetCleanLogs()]),
            preference=CapabilitySetEnable(
                CleanPreferenceEvent, [GetCleanPreference()], SetCleanPreference
            ),
            work_mode=CapabilitySetTypes(
                event=WorkModeEvent,
                get=[GetWorkMode()],
                set=SetWorkMode,
                types=(
                    WorkMode.MOP,
                    WorkMode.MOP_AFTER_VACUUM,
                    WorkMode.VACUUM,
                    WorkMode.VACUUM_AND_MOP,
                ),
            ),
        ),
        custom=CapabilityCustomCommand(
            event=CustomCommandEvent, get=[], set=CustomCommand
        ),
        error=CapabilityEvent(ErrorEvent, [GetError()]),
        fan_speed=CapabilitySetTypes(
            event=FanSpeedEvent,
            get=[GetFanSpeed()],
            set=SetFanSpeed,
            types=(
                FanSpeedLevel.QUIET,
                FanSpeedLevel.NORMAL,
                FanSpeedLevel.MAX,
                FanSpeedLevel.MAX_PLUS,
            ),
        ),
        life_span=CapabilityLifeSpan(
            types=(
                LifeSpan.BRUSH,
                LifeSpan.FILTER,
                LifeSpan.HAND_FILTER,
                LifeSpan.SIDE_BRUSH,
                LifeSpan.UNIT_CARE,
            ),
            event=LifeSpanEvent,
            get=[
                GetLifeSpan(
                    [
                        LifeSpan.BRUSH,
                        LifeSpan.FILTER,
                        LifeSpan.HAND_FILTER,
                        LifeSpan.SIDE_BRUSH,
                        LifeSpan.UNIT_CARE,
                    ]
                )
            ],
            reset=ResetLifeSpan,
        ),
        map=CapabilityMap(
            cached_info=CapabilityEvent(
                CachedMapInfoEvent, [GetCachedMapInfo(version=2)]
            ),
            changed=CapabilityEvent(MapChangedEvent, []),
            major=CapabilityEvent(MajorMapEvent, [GetMajorMap()]),
            minor=CapabilityExecute(GetMinorMap),
            multi_state=CapabilitySetEnable(
                MultimapStateEvent, [GetMultimapState()], SetMultimapState
            ),
            position=CapabilityEvent(PositionsEvent, [GetPos()]),
            relocation=CapabilityExecute(SetRelocationState),
            rooms=CapabilityEvent(RoomsEvent, [GetCachedMapInfo(version=2)]),
            trace=CapabilityEvent(MapTraceEvent, [GetMapTrace()]),
        ),
        network=CapabilityEvent(NetworkInfoEvent, [GetNetInfo()]),
        play_sound=CapabilityExecute(PlaySound),
        settings=CapabilitySettings(
            advanced_mode=CapabilitySetEnable(
                AdvancedModeEvent, [GetAdvancedMode()], SetAdvancedMode
            ),
            carpet_auto_fan_boost=CapabilitySetEnable(
                CarpetAutoFanBoostEvent,
                [GetCarpetAutoFanBoost()],
                SetCarpetAutoFanBoost,
            ),
            child_lock=CapabilitySetEnable(
                ChildLockEvent, [GetChildLock()], SetChildLock
            ),
            efficiency_mode=CapabilitySetTypes(
                event=EfficiencyModeEvent,
                get=[GetEfficiencyMode()],
                set=SetEfficiencyMode,
                types=(
                    EfficiencyMode.ENERGY_EFFICIENT_MODE,
                    EfficiencyMode.STANDARD_MODE,
                ),
            ),
            ota=CapabilitySetEnable(OtaEvent, [GetOta()], SetOta),
            sweep_mode=CapabilitySetEnable(
                SweepModeEvent, [GetSweepMode()], SetSweepMode
            ),
            true_detect=CapabilitySetEnable(
                TrueDetectEvent, [GetTrueDetect()], SetTrueDetect
            ),
            voice_assistant=CapabilitySetEnable(
                VoiceAssistantStateEvent,
                [GetVoiceAssistantState()],
                SetVoiceAssistantState,
            ),
            volume=CapabilitySet(VolumeEvent, [GetVolume()], SetVolume),
        ),
        state=CapabilityEvent(StateEvent, [GetChargeState(), GetCleanInfoV2()]),
        station=CapabilityStation(
            action=CapabilityExecuteTypes(
                station_action.StationAction, types=(StationAction.EMPTY_DUSTBIN,)
            ),
            auto_empty=CapabilitySetTypes(
                event=AutoEmptyEvent,
                get=[GetAutoEmpty()],
                set=SetAutoEmpty,
                types=(
                    auto_empty.Frequency.AUTO,
                    auto_empty.Frequency.SMART,
                ),
            ),
            state=CapabilityEvent(StationEvent, [GetStationState()]),
        ),
        stats=CapabilityStats(
            clean=CapabilityEvent(StatsEvent, [GetStats()]),
            report=CapabilityEvent(ReportStatsEvent, []),
            total=CapabilityEvent(TotalStatsEvent, [GetTotalStats()]),
        ),
        water=CapabilityWater(
            amount=CapabilitySet(
                event=water_info.WaterAmountEvent,
                get=[GetWaterInfo()],
                set=SetWaterInfo,
            ),
            mop_attached=CapabilityEvent(water_info.MopAttachedEvent, [GetWaterInfo()]),
        ),        
    ),
)```

Sanji78 avatar Aug 01 '25 22:08 Sanji78

I see three problems so far:

  1. robot state (docked, cleaning) doesn't seem to update...
  2. the room id/labels are not part of the entity attributes anymore (while they were with my X1 OMNI)
  3. I don't see the map

Sanji78 avatar Aug 01 '25 22:08 Sanji78

I have mitmproxy configured to read Ecovacs API calls, if needed.

Sanji78 avatar Aug 02 '25 05:08 Sanji78

@slflowfoon @ytorres

Sanji78 avatar Aug 06 '25 10:08 Sanji78

Unfortunately my X8 broke and got an X9 as a replacement so I can no longer do any further testing

slflowfoon avatar Sep 09 '25 16:09 slflowfoon

I think X9 is very similar to X8... they have same features/API calls @slflowfoon . Can we work together to make X8 and X9 fully supported on HA? So far also the X9 support has a lot of limitations (e.g. map, water level..)

Sanji78 avatar Sep 09 '25 18:09 Sanji78

PR: 1167 will fix this https://github.com/DeebotUniverse/client.py/pull/1167

Sanji78 avatar Sep 17 '25 13:09 Sanji78

I use HA 2025.10.3 with Ecovacs-Integration. I can use Vacuum-Card well with this version. Scripts for cleaning single rooms don't work.

action: vacuum.send_command metadata: {} data: command: spot_area params: rooms: 4 target: entity_id: vacuum.deebot_x8_pro_onmi

This leads to no action on my robot.

michael15831 avatar Oct 19 '25 10:10 michael15831

You will need to use:

- service: vacuum.send_command
    target:
      entity_id: vacuum.XXX
    data_template:
       command: clean_V2
       params:
            act: "start"
            content: 
{"type":"freeClean","value":"1,1;1,3;1,5"} 

if you want to clean room 1, 3 and 5...

Sanji78 avatar Oct 20 '25 17:10 Sanji78

You will need to use:

- service: vacuum.send_command
    target:
      entity_id: vacuum.XXX
    data_template:
       command: clean_V2
       params:
            act: "start"
            content: 
{"type":"freeClean","value":"1,1;1,3;1,5"} 

if you want to clean room 1, 3 and 5...

It worked. Thank you very much

michael15831 avatar Oct 20 '25 18:10 michael15831

@edenhaus since many of these new vaccums utalize clean_V2, a departure from the other commands documented in the deebotuniverse for room or custom area cleans. Would it be possible to map clean_V2 so that other hacs intigrations like "Xiaomi Lovelace Vacuum Map card" could send the legacy commands and these new vaccums preform the action?

LeoDevop avatar Nov 06 '25 01:11 LeoDevop

I used this script to start room cleaning for one or more rooms for my Deebot X9 Pro Omni:

alias: Start Room Cleaning
sequence:
  - data:
      option: >-
        {{ iif(is_state('input_select.deebot_x9_pro_omni_work_mode', 'Vacuum &
        Mop'), 'vacuum_mop', work_mode | lower) }}
    target:
      entity_id: select.deebot_x9_pro_omni_work_mode
    action: select.select_option
  - data:
      command: clean_V2
      params:
        act: start
        content:
          type: freeClean
          value: >
            {% set room_map = states.vacuum.deebot_x9_pro_omni.attributes.rooms
            %} {# Get user selected rooms (variable 'area' from fields) #} {%
            set selected_rooms = area %} {# Initialize a namespace to allow
            appending to the list inside the loop #} {% set ns =
            namespace(commands=[]) %}

            {% if room_map is defined and selected_rooms is defined %}
              
              {% for room_name in selected_rooms %}
                {# Convert user-friendly name (e.g., "Guest Bathroom") to map key (e.g., "guest_bathroom") #}
                {% set room_key = room_name | lower | replace(' ', '_') %}
                
                {% if room_key in room_map %}
                  {# Construct the full command part (e.g., "1,2") and append it to the list #}
                  {% set room_id = room_map[room_key] | string %}
                  {% set ns.commands = ns.commands + ["1," ~ room_id] %}
                {% else %}
                  {{ log("Room '" ~ room_name ~ "' not found in vacuum map.", level='warning') }}
                {% endif %}
                
              {% endfor %}
              
            {% endif %}

            {# FINAL OUTPUT: Join the list of full command strings using a
            SEMICOLON (;) #} {{ ns.commands | join(';') }}
    target:
      entity_id: vacuum.deebot_x9_pro_omni
    action: vacuum.send_command
mode: single
fields:
  work_mode:
    name: Work Mode
    required: true
    selector:
      select:
        options:
          - Vacuum
          - Vacuum & Mop
    default: Vacuum
    description: Select the work mode
  area:
    selector:
      select:
        options:
          - Guest Bathroom
          - Corridor
          - Bathroom
          - Bedroom
          - Kids Room
          - Dining Room
          - Kitchen
          - Living Room
        multiple: true
    name: Area
    description: Select areas to clean
    required: true
description: Start room cleaning for one or more rooms
icon: mdi:robot-vacuum

This looks like this in the UI:

Image

tzwickl avatar Nov 11 '25 07:11 tzwickl