diff --git a/roborock/data/dyad/dyad_code_mappings.py b/roborock/data/dyad/dyad_code_mappings.py index ec12fdb2a..0b049047e 100644 --- a/roborock/data/dyad/dyad_code_mappings.py +++ b/roborock/data/dyad/dyad_code_mappings.py @@ -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 diff --git a/roborock/devices/traits/a01/__init__.py b/roborock/devices/traits/a01/__init__.py index 8161c3a50..0df06f743 100644 --- a/roborock/devices/traits/a01/__init__.py +++ b/roborock/devices/traits/a01/__init__.py @@ -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, @@ -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), @@ -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), } diff --git a/roborock/roborock_message.py b/roborock/roborock_message.py index b044b8ac7..50bd3c7c3 100644 --- a/roborock/roborock_message.py +++ b/roborock/roborock_message.py @@ -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 diff --git a/tests/devices/traits/a01/test_init.py b/tests/devices/traits/a01/test_init.py index 4da98f5b3..5fcc3ee32 100644 --- a/tests/devices/traits/a01/test_init.py +++ b/tests/devices/traits/a01/test_init.py @@ -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]] = []