feat: complete full Zeo device support#871
Conversation
|
Hey @NOisi-x something about your changes seem to have broken tests. Tests are running indefinitely, can you take a look? |
This reverts commit 613db33.
…erministic results
Hi, @Lash-L ,I've updated the test files and all tests are now passing. Thanks! CI Failure Analysis and Fix SummaryApologies for the broken CI — I'm a hobbyist with fairly limited programming experience and I overlooked the fact that the source changes would affect the existing test suite. Lesson learned for next time. Below is an analysis of what went wrong and what was changed to fix it. Root CausesThe CI test suite failed for three reasons:
Changes Made
|
allenporter
left a comment
There was a problem hiding this comment.
Fantastic contribution, thank you so much for the improvements here!
| unsub() | ||
|
|
||
| async def publish(self, message: RoborockMessage) -> None: | ||
| async def publish(self, message: RoborockMessage, qos: int = 0) -> None: |
There was a problem hiding this comment.
Let's define an enum for qos level. Just having numbers 0, 1, 2 doesn't really say what these are, or why they exist. Why would someone set a lower or higher level, etc should be explained in the code and doing it from the enum could be a good way.
| done = 10 | ||
| aftercare = 12 | ||
| waiting_for_aftercare = 13 | ||
| updating = 11 |
There was a problem hiding this comment.
let's tag as a breaking change. i think it's that commits have feat!: ?
| params = {protocol: value} | ||
| return await send_decoded_command(self._channel, params, value_encoder=lambda x: x) | ||
| params: dict[RoborockZeoProtocol, Any] = {protocol: value} | ||
| if protocol == RoborockZeoProtocol.START and value == 1: |
There was a problem hiding this comment.
This makes me think it may be time to define actual API methods for these specialized commands rather than a single generic query_values/set_values (e.g. a "start" and "stop" methods etc for the washing machine.
Do you expect there to be more specialized commands like this or is this the only one?
At a minimum: Split out the function that queries the current values to a separate method
| params[dp] = fallback | ||
| except RoborockException: | ||
| _LOGGER.warning( | ||
| "Failed to query device state before start, using defaults", |
There was a problem hiding this comment.
i'm not sure this fallback is a great idea e.g. if the user is expecting some values to be preserved then silently not preserving them worries me e.g. as a user I would be concerned if my washing machine changed my water temp due to flaky wifi and i might prefer it just fail rather than shrinking cotton clothes or whatever.
Can we either remove or provide some rationale here why this is expected or ok?
| ) -> None: | ||
| """Test the device manager end to end flow with an A01 device.""" | ||
| # Use a fixed timestamp so the MQTT payload snapshot is deterministic. | ||
| monkeypatch.setattr("roborock.protocols.a01_protocol.time.time", lambda: 1755750947.0) |
There was a problem hiding this comment.
We have deterministic_message_fixtures
Summary
This PR completes Zeo (washing machine) device support in
python-roborock.The library already had foundational A01 protocol support for Zeo devices, but several critical gaps prevented practical use: the start command didn't work, roughly half of the DP IDs were undefined, several device states and enum classes were missing, and most data points had no value converters. This PR closes those gaps, bringing the DP and enum definitions in line with the Roborock Washer app protocol.
With these changes, Zeo devices support the full range of operations defined by the protocol: start/stop/pause/shutdown, mode and program selection, temperature, rinse, spin, drying control, child lock, auto detergent/softener, UV sterilization, soak, silent mode, dry care, steam, ion deodorization, wash-dry linkage, and more.
Motivation
The library's existing Zeo
set_value(START, 1)did not actually start the device. Through protocol analysis and comparison with the Roborock Washer HomeAssistant integration, three root causes were identified:"t"(Unix timestamp) field.Beyond the start command,
RoborockZeoProtocolcovered only 31 of 67 known DP IDs,zeo_code_mappings.pywas missing 4 enum classes and several enum values, andZEO_PROTOCOL_ENTRIESprovided converters for only 16 of the 52 user-facing DPs — meaningquery_values()silently returnedNonefor most data points.Changes
1. MQTT QoS —
roborock/mqtt/roborock_session.pyChanged
client.publish()to useqos=1(at-least-once delivery).2. A01 payload timestamp —
roborock/protocols/a01_protocol.pyAdded
"t": int(time.time())to the JSON payload structure inencode_mqtt_payload().3. Combined start command —
roborock/devices/traits/a01/__init__.pyZeoApi.set_value(START, 1)now:{START: 1}into a single MQTT messageMODE=1, PROGRAM=1if the query fails4. Extended DP ID enum —
roborock/roborock_message.pyRoborockZeoProtocolexpanded from 31 to 67 entries, now covering all 52 user-facing DPs (200-266) plus meta DPs. Newly added: dirt detection (215-216), UV light (228), soak (233), total time (234), smart hosting (235-238), silent mode (240-242), dry care mode (244), wash-dry linkage (255), drying method (256), steam volume (257), ion deodorization (258), dryer start error (265), and more.5. Enum updates —
roborock/data/zeo/zeo_code_mappings.pyZeoMode: addedtreatment = 4ZeoState: expanded from 13 to 20 statesZeoSoak: addedvery_max = 5ZeoDryingMethod,ZeoSteamVolume,ZeoDryAndCare,ZeoDryerStartError6. Value converters —
roborock/devices/traits/a01/__init__.pyZEO_PROTOCOL_ENTRIESexpanded from 16 to 57 entries, providing typed converters for all user-facing DPs (200-266).Backward Compatibility
All changes are additive or transparent:
ZeoApi.query_values()andZeoApi.set_value()signatures unchangedset_valuecalls unaffectedKnown Limitation
sent_decoded_commanduses a strict "all requested DPs must respond" completion condition. Since A01 devices do not expose a capability probing mechanism, there is currently no clean way to determine which optional DPs (e.g., SOAK, STEAM_VOLUME) a given device supports before querying them.As a temporary workaround, the start command queries and bundles only the following 6 core DPs, which has been verified to successfully start devices on physical Zeo hardware:
These are bundled with
{START: 1}and sent as a single MQTT message. The device applies its internally-stored values for any parameters not explicitly included.A better long-term solution for dynamically detecting device capabilities is welcome — contributions or ideas in this area are appreciated.
Testing
set_value(START, 1)verified working on physical Zeo One and Zeo M1S devicesset_valuefor non-START DPs (mode, program, temperature, etc.) continues to work as beforeAcknowledgments
This PR draws heavily on the excellent reverse-engineering work done in ndwzy/roborock_washer_ha. The A01 protocol structure, DP definitions, enum mappings, and the combined start command behavior were all cross-referenced against that project's implementation. The DP ID list (200-266), state enum values, and device error codes were verified against the Roborock app bundle extracted and analyzed by that project.
Closes #833