Skip to content

Add support for Yeedi Floor 3 Station (kd0una)#1472

Merged
edenhaus merged 2 commits into
DeebotUniverse:devfrom
Qmo37:feat/add-kd0una-yeedi-floor3-station
Mar 25, 2026
Merged

Add support for Yeedi Floor 3 Station (kd0una)#1472
edenhaus merged 2 commits into
DeebotUniverse:devfrom
Qmo37:feat/add-kd0una-yeedi-floor3-station

Conversation

@Qmo37

@Qmo37 Qmo37 commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Pull Request: Add Yeedi Floor 3 Station (kd0una) Hardware Support

Target repo: DeebotUniverse/client.py
Status: DRAFT — core functionality confirmed working, some features pending verification


Summary

Adds hardware capability definition for the Yeedi Floor 3 Station (model ID kd0una, product K960_ACS_INT). This device uses the eco-ng protocol (JSON over MQTT) and connects to a self-hosted Bumper server (with companion Bumper PR for port 443 MQTT support).

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Bug fix
  • Breaking change
  • Documentation update

Changes

New file: deebot_client/hardware/kd0una.py

Complete capability definition based on:

  • MQTT traffic capture from a real device connecting to Ecovacs cloud (Taiwan region)
  • Cross-referenced against confirmed working commands via Bumper self-hosted server
  • Modelled after zwkcqc.py (Deebot T20 Omni) as the closest known similar device

Capabilities included:

Capability Details
availability GetBattery availability check
battery GetBattery
charge Charge
clean.action Clean, CleanArea
clean.continuous GetContinuousCleaning / SetContinuousCleaning (= getBreakPoint)
clean.count GetCleanCount / SetCleanCount
clean.preference GetCleanPreference / SetCleanPreference
error GetError
fan_speed QUIET, NORMAL, MAX, MAX_PLUS
life_span BRUSH, FILTER, SIDE_BRUSH
map cached info, major/minor map, multimap, position, rooms, set, trace, relocation
network GetNetInfo
play_sound PlaySound
settings.carpet_auto_fan_boost GetCarpetAutoFanBoost / SetCarpetAutoFanBoost
settings.child_lock GetChildLock / SetChildLock
settings.mop_auto_wash_frequency GetMopAutoWashFrequency / SetMopAutoWashFrequency (0–60 min)
settings.true_detect GetTrueDetect / SetTrueDetect
settings.volume GetVolume / SetVolume
state GetChargeState, GetCleanInfo
stats GetStats, GetTotalStats
station.action EMPTY_DUSTBIN, DRY_MOP, CLEAN_BASE, WASH_MOP
station.auto_empty AUTO, SMART, MIN_10, MIN_15, MIN_25
station.state GetStationState
water.amount LOW, MEDIUM, HIGH, ULTRAHIGH
water.mop_attached GetWaterInfo
custom CustomCommand pass-through
"""Yeedi Floor 3 Station Capabilities."""

from __future__ import annotations

from deebot_client import commands
from deebot_client.capabilities import (
    Capabilities,
    CapabilityClean,
    CapabilityCleanAction,
    CapabilityCustomCommand,
    CapabilityEvent,
    CapabilityExecute,
    CapabilityExecuteTypes,
    CapabilityLifeSpan,
    CapabilityMap,
    CapabilityNumber,
    CapabilitySet,
    CapabilitySetEnable,
    CapabilitySettings,
    CapabilitySetTypes,
    CapabilityStation,
    CapabilityStats,
    CapabilityWater,
    DeviceType,
)
from deebot_client.commands.json import station_action
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 Clean, CleanArea, GetCleanInfo
from deebot_client.commands.json.clean_count import GetCleanCount, SetCleanCount
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.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,
    GetMapSet,
    GetMapTrace,
    GetMinorMap,
    SetMajorMap,
)
from deebot_client.commands.json.mop_auto_wash_frequency import (
    GetMopAutoWashFrequency,
    SetMopAutoWashFrequency,
)
from deebot_client.commands.json.multimap_state import (
    GetMultimapState,
    SetMultimapState,
)
from deebot_client.commands.json.network import GetNetInfo
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.stats import GetStats, GetTotalStats
from deebot_client.commands.json.station_state import GetStationState
from deebot_client.commands.json.true_detect import GetTrueDetect, SetTrueDetect
from deebot_client.commands.json.volume import GetVolume, SetVolume
from deebot_client.commands.json.water_info import GetWaterInfo, SetWaterInfo
from deebot_client.const import DataType
from deebot_client.events import (
    AutoEmptyEvent,
    AvailabilityEvent,
    BatteryEvent,
    CachedMapInfoEvent,
    CarpetAutoFanBoostEvent,
    ChildLockEvent,
    CleanCountEvent,
    CleanPreferenceEvent,
    ContinuousCleaningEvent,
    CustomCommandEvent,
    ErrorEvent,
    FanSpeedEvent,
    FanSpeedLevel,
    LifeSpan,
    LifeSpanEvent,
    MajorMapEvent,
    MapChangedEvent,
    MapTraceEvent,
    MultimapStateEvent,
    NetworkInfoEvent,
    PositionsEvent,
    ReportStatsEvent,
    RoomsEvent,
    StateEvent,
    StatsEvent,
    StationEvent,
    TotalStatsEvent,
    TrueDetectEvent,
    VolumeEvent,
    auto_empty,
    water_info,
)
from deebot_client.events.mop_auto_wash_frequency import MopAutoWashFrequencyEvent
from deebot_client.models import StaticDeviceInfo


def get_device_info() -> StaticDeviceInfo:
    """Get device info for Yeedi Floor 3 Station (kd0una)."""
    return 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=Clean, area=CleanArea),
                continuous=CapabilitySetEnable(
                    ContinuousCleaningEvent,
                    [GetContinuousCleaning()],
                    SetContinuousCleaning,
                ),
                count=CapabilitySet(CleanCountEvent, [GetCleanCount()], SetCleanCount),
                preference=CapabilitySetEnable(
                    CleanPreferenceEvent, [GetCleanPreference()], SetCleanPreference
                ),
            ),
            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.SIDE_BRUSH),
                event=LifeSpanEvent,
                get=[
                    GetLifeSpan([LifeSpan.BRUSH, LifeSpan.FILTER, LifeSpan.SIDE_BRUSH])
                ],
                reset=ResetLifeSpan,
            ),
            map=CapabilityMap(
                cached_info=CapabilityEvent(CachedMapInfoEvent, [GetCachedMapInfo()]),
                changed=CapabilityEvent(MapChangedEvent, []),
                major=CapabilitySet(MajorMapEvent, [GetMajorMap()], SetMajorMap),
                minor=CapabilityExecute(GetMinorMap),
                multi_state=CapabilitySetEnable(
                    MultimapStateEvent, [GetMultimapState()], SetMultimapState
                ),
                position=CapabilityEvent(PositionsEvent, [GetPos()]),
                relocation=CapabilityExecute(SetRelocationState),
                rooms=CapabilityEvent(RoomsEvent, [GetCachedMapInfo()]),
                set=CapabilityExecute(GetMapSet),
                trace=CapabilityEvent(MapTraceEvent, [GetMapTrace()]),
            ),
            network=CapabilityEvent(NetworkInfoEvent, [GetNetInfo()]),
            play_sound=CapabilityExecute(PlaySound),
            settings=CapabilitySettings(
                carpet_auto_fan_boost=CapabilitySetEnable(
                    CarpetAutoFanBoostEvent,
                    [GetCarpetAutoFanBoost()],
                    SetCarpetAutoFanBoost,
                ),
                child_lock=CapabilitySetEnable(
                    ChildLockEvent, [GetChildLock()], SetChildLock
                ),
                mop_auto_wash_frequency=CapabilityNumber(
                    event=MopAutoWashFrequencyEvent,
                    get=[GetMopAutoWashFrequency()],
                    set=SetMopAutoWashFrequency,
                    min=0,
                    max=60,
                ),
                true_detect=CapabilitySetEnable(
                    TrueDetectEvent, [GetTrueDetect()], SetTrueDetect
                ),
                volume=CapabilitySet(VolumeEvent, [GetVolume()], SetVolume),
            ),
            state=CapabilityEvent(StateEvent, [GetChargeState(), GetCleanInfo()]),
            station=CapabilityStation(
                action=CapabilityExecuteTypes(
                    station_action.StationAction,
                    types=(
                        commands.StationAction.EMPTY_DUSTBIN,
                        commands.StationAction.DRY_MOP,
                        commands.StationAction.CLEAN_BASE,
                        commands.StationAction.WASH_MOP,
                    ),
                ),
                auto_empty=CapabilitySetTypes(
                    event=AutoEmptyEvent,
                    get=[GetAutoEmpty()],
                    set=SetAutoEmpty,
                    types=(
                        auto_empty.Frequency.AUTO,
                        auto_empty.Frequency.SMART,
                        auto_empty.Frequency.MIN_10,
                        auto_empty.Frequency.MIN_15,
                        auto_empty.Frequency.MIN_25,
                    ),
                ),
                state=CapabilityEvent(StationEvent, [GetStationState()]),
            ),
            stats=CapabilityStats(
                clean=CapabilityEvent(StatsEvent, [GetStats()]),
                report=CapabilityEvent(ReportStatsEvent, []),
                total=CapabilityEvent(TotalStatsEvent, [GetTotalStats()]),
            ),
            water=CapabilityWater(
                amount=CapabilitySetTypes(
                    event=water_info.WaterAmountEvent,
                    get=[GetWaterInfo()],
                    set=SetWaterInfo,
                    types=(
                        water_info.WaterAmount.LOW,
                        water_info.WaterAmount.MEDIUM,
                        water_info.WaterAmount.HIGH,
                        water_info.WaterAmount.ULTRAHIGH,
                    ),
                ),
                mop_attached=CapabilityEvent(
                    water_info.MopAttachedEvent, [GetWaterInfo()]
                ),
            ),
        ),
    )

Device Details

Device:       Yeedi Floor 3 Station
Model ID:     kd0una
Product:      K960_ACS_INT
Protocol:     eco-ng (JSON over MQTT v3.1.1)
MQTT Port:    443 (TLS) — requires companion Bumper PR
Firmware:     1.10.0
Station:      ACSH (auto-empty, mop wash, mop dry)
Region:       TW (Taiwan)

DNS domains (TW region):

  • jmq-ngiot-tw.area.ww.ecouser.net (MQTT broker)
  • iotin-ww.ecouser.net (IoT initialization)
  • portal-ww.ecouser.net (REST portal)

Testing

Confirmed Working (via Bumper self-hosted server)

  • Device recognized and entities created in HA Ecovacs integration
  • Battery level reporting
  • Charge state reporting
  • Fan speed reporting and control
  • Water amount reporting
  • Volume reporting
  • True detect (3D obstacle avoidance) reporting
  • Consumable sensors (main brush, side brush, HEPA filter)
  • Vacuum start / stop / dock controls
  • Map rendering in HA (after first cleaning run)
  • Robot position tracking
  • Map trace overlay
  • Child lock switch
  • Carpet auto fan boost switch
  • Continuous cleaning (resume after recharge) switch
  • Mop auto wash frequency number entity
  • Station state sensor
  • Station action buttons (empty dustbin, wash mop, dry mop, clean base)
  • Auto empty frequency select

Not Yet Verified

  • Area / room cleaning (CleanArea)
  • Fan speed changes via HA UI
  • Water amount changes via HA UI
  • Multi-map switching
  • Auto empty frequency — captured value was "30" which is outside the Frequency enum (10/15/25/AUTO/SMART); may need adjustment
  • Long-term stability (>24h)

Known Limitations

auto_empty frequency mismatch: The MQTT capture showed "frequency":"30" in the getAutoEmpty response. The auto_empty.Frequency enum defines 10, 15, 25, AUTO, SMART. The robot may return a value not in the enum, causing the entity to show an unknown state. This may need a follow-up fix once the exact supported values are confirmed.

getDryingDuration / setDryingDuration: Confirmed in MQTT capture ({"duration":120}), but no corresponding command class exists in deebot_client yet. Not included in this PR.

getBlock / setBlock (DND schedule): Confirmed in capture, but no GetDnd/SetDnd command class found in the current codebase. Not included.


Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have added tests that prove my fix is effective or that my feature works
  • My changes generate no new warnings
  • New and existing unit tests pass locally with my changes

Related Work

  • Bumper PR: Companion PR to DeebotUniverse/Bumper adds port 443 MQTT listener, kd0una device registration, and post-connect handshake — required for local operation without cloud
  • Follow-up: getDryingDuration and getBlock support would require new command classes in this repo

Add hardware capability definition for the Yeedi Floor 3 Station
(model ID kd0una, product K960_ACS_INT). Device uses eco-ng protocol
(JSON over MQTT) and connects via MQTT over TLS on port 443.

Capabilities include: battery, charge, clean (with area), fan speed
(quiet/normal/max/max_plus), life span (brush/filter/side brush), map
(full map support with trace and position), network, play sound, stats,
water (4 levels + mop attached), station (auto empty, actions, state),
settings (true detect, volume, carpet boost, child lock, mop wash freq),
continuous cleaning, clean count, clean preference, error, custom command.

Verified working via Bumper self-hosted server with a real device.
Companion Bumper PR: DeebotUniverse/bumper#3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@edenhaus

Copy link
Copy Markdown
Member

Can you also control your Yeedi using the official servers?

@edenhaus edenhaus changed the title feat: add Yeedi Floor 3 Station (kd0una) hardware capabilities Add support Yeedi Floor 3 Station (kd0una) Mar 23, 2026
@Qmo37

Qmo37 commented Mar 23, 2026

Copy link
Copy Markdown
Contributor Author

Can you also control your Yeedi using the official servers?

Yes, and I used those MQTT packets from official Yeedi server to modify the code.

@edenhaus

Copy link
Copy Markdown
Member

So you can control your vacuum also without using bumper?

@Qmo37

Qmo37 commented Mar 25, 2026

Copy link
Copy Markdown
Contributor Author

So you can control your vacuum also without using bumper?

Yes.

@edenhaus edenhaus added the pr: new-feature PR, which adds a new feature label Mar 25, 2026

@edenhaus edenhaus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Qmo37 👍

@edenhaus edenhaus marked this pull request as ready for review March 25, 2026 12:30
Copilot AI review requested due to automatic review settings March 25, 2026 12:30

@edenhaus edenhaus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix the CI issues

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new hardware capability module for the Yeedi Floor 3 Station (kd0una) so the client can construct the correct StaticDeviceInfo/Capabilities set for this eco-ng (JSON/MQTT) device.

Changes:

  • Introduces deebot_client/hardware/kd0una.py defining supported capabilities (cleaning, map, station actions, settings, stats, water, etc.).
  • Wires up station-related features (actions, auto-empty config, station state) using existing JSON command/event types.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

auto_empty.Frequency.SMART,
auto_empty.Frequency.MIN_10,
auto_empty.Frequency.MIN_15,
auto_empty.Frequency.MIN_25,

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabling GetAutoEmpty() for this model can crash message handling if the device reports an unknown frequency (e.g. the captured "30"). OnAutoEmpty currently does Frequency(frequency_str) without guarding, which raises ValueError for values not in the enum. Either extend auto_empty.Frequency to include the device’s value(s) (e.g. MIN_30 = "30") and include it in types, or update the auto-empty message parsing to tolerate unknown values (map to None) before shipping this capability for kd0una.

Suggested change
auto_empty.Frequency.MIN_25,
auto_empty.Frequency.MIN_25,
auto_empty.Frequency.MIN_30,

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +6
from deebot_client import commands
from deebot_client.capabilities import (

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import style is inconsistent with the other hardware capability modules: they typically import StationAction directly (from deebot_client.commands import StationAction) instead of importing the commands package and using commands.StationAction.*. Aligning with the existing pattern improves readability and keeps imports uniform across hardware definitions.

Copilot uses AI. Check for mistakes.
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

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import line exceeds the formatting used in similar hardware modules (most split these into a parenthesized multi-line import). Please reformat to match the project’s style (and avoid line-length/formatter churn).

Suggested change
from deebot_client.commands.json.charge import Charge
from deebot_client.commands.json.charge import (
Charge,
)

Copilot uses AI. Check for mistakes.
- Add `Capabilities` to correct alphabetical position in import block
- Remove duplicate `Capabilities` import
- Split `GetCarpetAutoFanBoost, SetCarpetAutoFanBoost` into multi-line
- Alphabetize `station_state`/`stats` and `StationEvent`/`StatsEvent`

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov

codecov Bot commented Mar 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.82%. Comparing base (34a6f4e) to head (f4f534e).
⚠️ Report is 25 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #1472      +/-   ##
==========================================
+ Coverage   94.79%   94.82%   +0.03%     
==========================================
  Files         152      153       +1     
  Lines        5974     6010      +36     
  Branches      350      350              
==========================================
+ Hits         5663     5699      +36     
  Misses        249      249              
  Partials       62       62              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq

codspeed-hq Bot commented Mar 25, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 113 untouched benchmarks


Comparing Qmo37:feat/add-kd0una-yeedi-floor3-station (f4f534e) with dev (5a9653a)

Open in CodSpeed

@edenhaus edenhaus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Qmo37 👍

@edenhaus edenhaus changed the title Add support Yeedi Floor 3 Station (kd0una) Add support for Yeedi Floor 3 Station (kd0una) Mar 25, 2026
@Qmo37

Qmo37 commented Mar 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @Qmo37 👍

Thank you so much for accepting my idea, it means a lot to me since this is my first PR.
I'm really grateful that there is project existed, and I'm happy to be a part of it.

@edenhaus edenhaus merged commit f6fbd9d into DeebotUniverse:dev Mar 25, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr: new-feature PR, which adds a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants