Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions roborock/data/dyad/dyad_code_mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,57 @@ class DyadCleanser(RoborockEnum):
max = 3


class DyadCleanserAmount(RoborockEnum):
"""Detergent dosing level (DP 238)."""

unknown = -1
off = 0
low = 1
standard = 2
high = 3


class DyadCleanAssistPower(RoborockEnum):
"""Wheel assist power (DP 241)."""

unknown = -1
gentle = 1
moderate = 2
strong = 3


class DyadSelfCleanWaterTemp(RoborockEnum):
"""Water temperature used for self-cleaning (DP 242, reported back on DP 271)."""

unknown = -1
high = 3
steam = 4


class DyadSelfCleanIntensity(RoborockEnum):
"""Self-cleaning intensity (DP 270).

Smart mode is enabled through DP 239; standard and deep follow DP 203.
"""

unknown = -1
smart = 0
standard = 1
deep = 2


class DyadPowerwheelDirection(RoborockEnum):
"""Drive direction while the vacuum is remote controlled from the app (DP 249)."""

unknown = -1
stop = 0
forward = 1
backward = 2
left = 5
right = 6
exit = 9 # sent when remote control ends


class DyadError(RoborockEnum):
none = 0
dirty_tank_full = 20000 # Dirty tank full. Empty it
Expand Down
23 changes: 23 additions & 0 deletions roborock/devices/traits/a01/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@
from roborock.data import DyadProductInfo, DyadSndState, HomeDataProduct, RoborockCategory
from roborock.data.dyad.dyad_code_mappings import (
DyadBrushSpeed,
DyadCleanAssistPower,
DyadCleanMode,
DyadCleanserAmount,
DyadError,
DyadPowerwheelDirection,
DyadSelfCleanIntensity,
DyadSelfCleanLevel,
DyadSelfCleanMode,
DyadSelfCleanWaterTemp,
DyadSuction,
DyadWarmLevel,
DyadWaterLevel,
Expand Down Expand Up @@ -93,12 +98,14 @@
RoborockDyadDataProtocol.WATER_LEVEL: lambda val: DyadWaterLevel(val).name,
RoborockDyadDataProtocol.BRUSH_SPEED: lambda val: DyadBrushSpeed(val).name,
RoborockDyadDataProtocol.POWER: lambda val: int(val),
RoborockDyadDataProtocol.AUTO_SELF_CLEAN_SET: lambda val: bool(val), # self-clean after docking
RoborockDyadDataProtocol.AUTO_DRY: lambda val: bool(val),
RoborockDyadDataProtocol.MESH_LEFT: lambda val: int(360000 - val * 60),
RoborockDyadDataProtocol.BRUSH_LEFT: lambda val: int(360000 - val * 60),
RoborockDyadDataProtocol.ERROR: lambda val: DyadError(val).name,
RoborockDyadDataProtocol.VOLUME_SET: lambda val: int(val),
RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: lambda val: bool(val),
RoborockDyadDataProtocol.AUTO_SELF_CLEAN_SET_MODE: lambda val: DyadSelfCleanLevel(val).name,
RoborockDyadDataProtocol.AUTO_DRY_MODE: lambda val: bool(val),
RoborockDyadDataProtocol.SILENT_DRY_DURATION: lambda val: int(val), # in minutes
RoborockDyadDataProtocol.SILENT_MODE: lambda val: bool(val),
Expand All @@ -112,6 +119,22 @@
int(v) for v in val.split(",")
], # minutes of cleaning in past few days.
RoborockDyadDataProtocol.TOTAL_RUN_TIME: lambda val: int(val),
RoborockDyadDataProtocol.SET_CLEANSER_AMOUNT: lambda val: DyadCleanserAmount(val).name,
RoborockDyadDataProtocol.AUTO_SMART_SELF_CLEAN_SET: lambda val: bool(val),
RoborockDyadDataProtocol.LIGHT_SET: lambda val: bool(val),
RoborockDyadDataProtocol.CLEAN_ASSIST_POWER_SET: lambda val: DyadCleanAssistPower(val).name,
RoborockDyadDataProtocol.SELF_CLEAN_WATER_TEMP_LEVEL_SET: lambda val: DyadSelfCleanWaterTemp(val).name,
RoborockDyadDataProtocol.DOCK_TYPE: lambda val: int(val),
RoborockDyadDataProtocol.WATER_MODE_SUCTION: lambda val: DyadSuction(val).name,
RoborockDyadDataProtocol.WATER_MODE_BRUSH_SPEED: lambda val: DyadBrushSpeed(val).name,
RoborockDyadDataProtocol.CHARGE_ERROR_STATUS: lambda val: bool(val),
RoborockDyadDataProtocol.DRYING_RATE: lambda val: int(val), # percent
RoborockDyadDataProtocol.FLAT_STATUS: lambda val: bool(val),
RoborockDyadDataProtocol.POWERWHEEL_DIRECTION: lambda val: DyadPowerwheelDirection(val).name,
RoborockDyadDataProtocol.POWERWHEEL_SPEED: lambda val: int(val),
RoborockDyadDataProtocol.SELF_CLEAN_CUR_MODE: lambda val: DyadSelfCleanIntensity(val).name,
RoborockDyadDataProtocol.SELF_CLEAN_CUR_TEMP: lambda val: DyadSelfCleanWaterTemp(val).name,
RoborockDyadDataProtocol.DRY_CUR_MODE: lambda val: int(val), # in minutes
RoborockDyadDataProtocol.SND_STATE: lambda val: DyadSndState.from_dict(val),
RoborockDyadDataProtocol.PRODUCT_INFO: lambda val: DyadProductInfo.from_dict(val),
}
Expand Down
16 changes: 16 additions & 0 deletions roborock/roborock_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,22 @@ class RoborockDyadDataProtocol(RoborockEnum):
FEATURE_INFO = 235
RECOVER_SETTINGS = 236
DRY_COUNTDOWN = 237
SET_CLEANSER_AMOUNT = 238 # detergent dosing level
AUTO_SMART_SELF_CLEAN_SET = 239 # smart self-clean intensity enabled
LIGHT_SET = 240 # brush head light
CLEAN_ASSIST_POWER_SET = 241 # wheel assist power
SELF_CLEAN_WATER_TEMP_LEVEL_SET = 242 # self-clean water temperature setting
DOCK_TYPE = 243
WATER_MODE_SUCTION = 244 # suction in sponge (water pickup) mode
WATER_MODE_BRUSH_SPEED = 245 # brush speed in sponge (water pickup) mode
CHARGE_ERROR_STATUS = 246 # F25 Ultra: 1 while not charging (off the dock, self-clean, drying)
DRYING_RATE = 247 # percent
FLAT_STATUS = 248 # 1 while the vacuum is lying flat
POWERWHEEL_DIRECTION = 249 # remote control drive direction
POWERWHEEL_SPEED = 250 # remote control drive speed
SELF_CLEAN_CUR_MODE = 270 # intensity of the current or last self-clean run
SELF_CLEAN_CUR_TEMP = 271 # water temperature of the current or last self-clean run
DRY_CUR_MODE = 272 # drying time in minutes of the current or next run, follows SILENT_DRY_DURATION
ID_QUERY = 10000
F_C = 10001
SCHEDULE_TASK = 10002
Expand Down
76 changes: 76 additions & 0 deletions tests/devices/traits/a01/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,82 @@ async def test_dyad_values_track_pushes(dyad_api: DyadApi, fake_channel: FakeCha
}


async def test_dyad_f25_ultra_status_snapshot(dyad_api: DyadApi, fake_channel: FakeChannel):
"""Data points pushed by an F25 Ultra (roborock.wdv.a194) are decoded."""
await dyad_api.start()

fake_channel.notify_subscribers(
build_a01_message({238: 0, 239: 1, 240: 1, 241: 2, 242: 3, 243: 2, 244: 3, 245: 2, 246: 0, 247: 100, 248: 0})
)
fake_channel.notify_subscribers(build_a01_message({270: 0, 271: 3, 272: 5}))

assert dyad_api.values == {
RoborockDyadDataProtocol.SET_CLEANSER_AMOUNT: "off",
RoborockDyadDataProtocol.AUTO_SMART_SELF_CLEAN_SET: True,
RoborockDyadDataProtocol.LIGHT_SET: True,
RoborockDyadDataProtocol.CLEAN_ASSIST_POWER_SET: "moderate",
RoborockDyadDataProtocol.SELF_CLEAN_WATER_TEMP_LEVEL_SET: "high",
RoborockDyadDataProtocol.DOCK_TYPE: 2,
RoborockDyadDataProtocol.WATER_MODE_SUCTION: "l3",
RoborockDyadDataProtocol.WATER_MODE_BRUSH_SPEED: "l2",
RoborockDyadDataProtocol.CHARGE_ERROR_STATUS: False,
RoborockDyadDataProtocol.DRYING_RATE: 100,
RoborockDyadDataProtocol.FLAT_STATUS: False,
RoborockDyadDataProtocol.SELF_CLEAN_CUR_MODE: "smart",
RoborockDyadDataProtocol.SELF_CLEAN_CUR_TEMP: "high",
RoborockDyadDataProtocol.DRY_CUR_MODE: 5,
}


@pytest.mark.parametrize(
("protocol", "raw", "expected"),
[
(RoborockDyadDataProtocol.SET_CLEANSER_AMOUNT, 0, "off"),
(RoborockDyadDataProtocol.SET_CLEANSER_AMOUNT, 1, "low"),
(RoborockDyadDataProtocol.SET_CLEANSER_AMOUNT, 2, "standard"),
(RoborockDyadDataProtocol.SET_CLEANSER_AMOUNT, 3, "high"),
(RoborockDyadDataProtocol.SET_CLEANSER_AMOUNT, 42, "unknown"),
(RoborockDyadDataProtocol.CLEAN_ASSIST_POWER_SET, 1, "gentle"),
(RoborockDyadDataProtocol.CLEAN_ASSIST_POWER_SET, 2, "moderate"),
(RoborockDyadDataProtocol.CLEAN_ASSIST_POWER_SET, 3, "strong"),
(RoborockDyadDataProtocol.SELF_CLEAN_WATER_TEMP_LEVEL_SET, 3, "high"),
(RoborockDyadDataProtocol.SELF_CLEAN_WATER_TEMP_LEVEL_SET, 4, "steam"),
(RoborockDyadDataProtocol.SELF_CLEAN_WATER_TEMP_LEVEL_SET, 1, "unknown"),
(RoborockDyadDataProtocol.SELF_CLEAN_CUR_TEMP, 4, "steam"),
(RoborockDyadDataProtocol.SELF_CLEAN_CUR_MODE, 0, "smart"),
(RoborockDyadDataProtocol.SELF_CLEAN_CUR_MODE, 1, "standard"),
(RoborockDyadDataProtocol.SELF_CLEAN_CUR_MODE, 2, "deep"),
(RoborockDyadDataProtocol.WATER_MODE_SUCTION, 4, "l4"),
(RoborockDyadDataProtocol.LIGHT_SET, 0, False),
(RoborockDyadDataProtocol.AUTO_SMART_SELF_CLEAN_SET, 0, False),
(RoborockDyadDataProtocol.CHARGE_ERROR_STATUS, 1, True),
(RoborockDyadDataProtocol.AUTO_SELF_CLEAN_SET, 1, True),
(RoborockDyadDataProtocol.AUTO_SELF_CLEAN_SET, 0, False),
(RoborockDyadDataProtocol.AUTO_SELF_CLEAN_SET_MODE, 2, "deep"),
(RoborockDyadDataProtocol.POWERWHEEL_DIRECTION, 0, "stop"),
(RoborockDyadDataProtocol.POWERWHEEL_DIRECTION, 1, "forward"),
(RoborockDyadDataProtocol.POWERWHEEL_DIRECTION, 2, "backward"),
(RoborockDyadDataProtocol.POWERWHEEL_DIRECTION, 5, "left"),
(RoborockDyadDataProtocol.POWERWHEEL_DIRECTION, 6, "right"),
(RoborockDyadDataProtocol.POWERWHEEL_DIRECTION, 9, "exit"),
(RoborockDyadDataProtocol.POWERWHEEL_DIRECTION, 3, "unknown"),
(RoborockDyadDataProtocol.POWERWHEEL_SPEED, 15, 15),
(RoborockDyadDataProtocol.DRY_CUR_MODE, 22, 22),
],
)
async def test_dyad_f25_ultra_value_conversion(
protocol: RoborockDyadDataProtocol,
raw: int,
expected: Any,
dyad_api: DyadApi,
fake_channel: FakeChannel,
):
"""Values observed on an F25 Ultra while changing settings in the app."""
await dyad_api.start()
fake_channel.notify_subscribers(build_a01_message({int(protocol): raw}))
assert dyad_api.values == {protocol: expected}


async def test_dyad_update_listener(dyad_api: DyadApi, fake_channel: FakeChannel):
"""Update listeners are notified when a value changes, not on identical pushes."""
updates: list[dict[RoborockDyadDataProtocol, Any]] = []
Expand Down
Loading