Skip to content

feat: add MQTT push subscription and real-time state tracking for Zeo devices#895

Open
NOisi-x wants to merge 5 commits into
Python-roborock:mainfrom
NOisi-x:pr/zeo-push
Open

feat: add MQTT push subscription and real-time state tracking for Zeo devices#895
NOisi-x wants to merge 5 commits into
Python-roborock:mainfrom
NOisi-x:pr/zeo-push

Conversation

@NOisi-x

@NOisi-x NOisi-x commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Builds on #892. Add MQTT push subscription and real-time state tracking for Zeo devices.

What's new in this PR

1. MQTT Push Subscription

ZeoApi.start() subscribes to the device's DPS MQTT topic after connection. All device state changes — both physical (button presses, dial rotations) and remote (App commands) — are received as real-time push messages.

device.connect() → zeo.start() → MQTT subscribe
                                 → _discover_features() pull full state

2. DPS Cache with Incremental Updates

Incoming push messages are decoded from RPC protocol 102, parsed as {dps: {dp_id: value, ...}}, and merged into _dps_cache. Only changed DPs are pushed — the device sends incremental updates, not full state each time.

3. Feature Discovery (_discover_features)

Mirrors V1's discover_features() pattern: after subscribing, queries FEATURE_BITS (DP 237) to wake the device and cache supported capabilities. Exposes supports(ZeoFeatureBits) for runtime feature checks.

4. Bug Fixes Found During Testing

  • ZeoApi.__init__: missing TraitUpdateListener.__init__() call caused _update_callbacks AttributeError
  • device.py connect(): self.a01_properties (doesn't exist) → self.zeo

5. DP 243 Placeholder

Observed on a114, increments with each push. Not found in the plugin bundle. Added as UNKNOWN_243 with a comment.

Testing

Tested on two Zeo devices (a234, a114) using a multi-device MQTT monitor:

Scenario Result
Physical operations (power on, dial rotate, wash start/pause/cancel) Real-time push < 1s
Door lock/unlock events Correctly tracked
Incremental DP updates Only changed DPs pushed
Two devices concurrently Independent streams, no crosstalk
_discover_features() wake Device pushes full state

Add MqttQos enum (AT_MOST_ONCE=0, AT_LEAST_ONCE=1, EXACTLY_ONCE=2) and thread a qos parameter through the publish chain (MqttSession -> MqttChannel -> send_decoded_command). All existing callers keep default AT_MOST_ONCE (backward compatible). Also add a unix timestamp field to A01 encode_mqtt_payload, required by Zeo/Dyad devices for command acceptance.
@NOisi-x
NOisi-x force-pushed the pr/zeo-push branch 7 times, most recently from 698b11b to 44465af Compare July 22, 2026 04:26
NOisi-X added 2 commits July 22, 2026 18:45
… all 56 devices covered

Expand RoborockZeoProtocol from 31 to 67 DP entries, ordered by numeric ID. Add all missing enum classes (ZeoFeatureBits, ZeoDryingMethod, ZeoSteamVolume, ZeoDryAndCare, ZeoDryerStartError) and extend existing enums to cover every state/value found in the official app plugin bundle. Add ZeoStartParams, ZeoCustomMode, and ZeoDryerCustomMode data containers inheriting from RoborockBase, placed in zeo_containers.py per reviewer guidance.
Update ZeoStartParams, ZeoCustomMode, and ZeoDryerCustomMode to use typed enum fields (ZeoMode, ZeoProgram, ZeoTemperature, etc.) instead of raw int, aligning with the V1 container pattern in v1_containers.py. Rename shorthand fields (rinse_times→rinse, spin_level→spin) for consistency across all three classes. Unify drying-mode field naming.
@NOisi-x
NOisi-x force-pushed the pr/zeo-push branch 4 times, most recently from 8d33ed9 to 2764f2e Compare July 22, 2026 13:28
…overy

Subscribes to the device DPS MQTT topic after connection. Incoming RPC_RESPONSE messages are decoded and merged into _dps_cache with incremental updates. _discover_features() queries FEATURE_BITS (DP 237) to wake the device and cache capabilities — equivalent to V1's discover_features(). Also fixes TraitUpdateListener init in ZeoApi and a01_properties routing in connect().
NOisi-x pushed a commit to NOisi-x/python-roborock that referenced this pull request Jul 22, 2026
ZeoCommandTrait (new: command.py, 165 lines)
- start_program: bundles start params via FIELD_TO_DP, QoS 1
- pause / resume / shutdown: single-DP commands
- Start param sets split by device type (washer 10 DPs, dryer 7 DPs)
- Feature-gated DPs conditionally included from cache
- Dependencies injected: channel, dps_cache, feature_trait, proto_entries

ZeoFeatureTrait (new: device_features.py, 185 lines)
- ZeoFeatures dataclass (24 bool flags): from_feature_bits() via name reflection
- Product type detection: static model ID whitelists (dryer/Hyperion/M1)
- is_dryer / is_hyperion_halia_hera / is_m1_muse_metis — no device query
- refresh(): query DP 237 once, cache in memory

ZeoStartParams (zeo_containers.py): expanded to 13 fields covering washer + dryer
ZEO_PROTOCOL_ENTRIES (__init__.py): expanded to ~69 DP-type mappings
RoborockZeoProtocol: add UNKNOWN_246, UNKNOWN_259 placeholders

Builds on Python-roborock#895 (MQTT push subscription).
NOisi-x pushed a commit to NOisi-x/python-roborock that referenced this pull request Jul 22, 2026
ZeoCommandTrait (new: command.py, 165 lines)
- start_program: bundles start params via FIELD_TO_DP, QoS 1
- pause / resume / shutdown: single-DP commands
- Start param sets split by device type (washer 10 DPs, dryer 7 DPs)
- Feature-gated DPs conditionally included from cache
- Dependencies injected: channel, dps_cache, feature_trait, proto_entries

ZeoFeatureTrait (new: device_features.py, 185 lines)
- ZeoFeatures dataclass (24 bool flags): from_feature_bits() via name reflection
- Product type detection: static model ID whitelists (dryer/Hyperion/M1)
- is_dryer / is_hyperion_halia_hera / is_m1_muse_metis — no device query
- refresh(): query DP 237 once, cache in memory

ZeoStartParams (zeo_containers.py): expanded to 13 fields covering washer + dryer
ZEO_PROTOCOL_ENTRIES (__init__.py): expanded to ~69 DP-type mappings
RoborockZeoProtocol: add UNKNOWN_246, UNKNOWN_259 placeholders

Builds on Python-roborock#895 (MQTT push subscription).
NOisi-x pushed a commit to NOisi-x/python-roborock that referenced this pull request Jul 22, 2026
ZeoCommandTrait (new: command.py, 165 lines)
- start_program: bundles start params via FIELD_TO_DP, QoS 1
- pause / resume / shutdown: single-DP commands
- Start param sets split by device type (washer 10 DPs, dryer 7 DPs)
- Feature-gated DPs conditionally included from cache
- Dependencies injected: channel, dps_cache, feature_trait, proto_entries

ZeoFeatureTrait (new: device_features.py, 185 lines)
- ZeoFeatures dataclass (24 bool flags): from_feature_bits() via name reflection
- Product type detection: static model ID whitelists (dryer/Hyperion/M1)
- is_dryer / is_hyperion_halia_hera / is_m1_muse_metis — no device query
- refresh(): query DP 237 once, cache in memory

ZeoStartParams (zeo_containers.py): expanded to 13 fields covering washer + dryer
ZEO_PROTOCOL_ENTRIES (__init__.py): expanded to ~69 DP-type mappings
RoborockZeoProtocol: add UNKNOWN_246, UNKNOWN_259 placeholders

Builds on Python-roborock#895 (MQTT push subscription).
NOisi-x pushed a commit to NOisi-x/python-roborock that referenced this pull request Jul 22, 2026
ZeoCommandTrait (new: command.py, 165 lines)
- start_program: bundles start params via FIELD_TO_DP, QoS 1
- pause / resume / shutdown: single-DP commands
- Start param sets split by device type (washer 10 DPs, dryer 7 DPs)
- Feature-gated DPs conditionally included from cache
- Dependencies injected: channel, dps_cache, feature_trait, proto_entries

ZeoFeatureTrait (new: device_features.py, 185 lines)
- ZeoFeatures dataclass (24 bool flags): from_feature_bits() via name reflection
- Product type detection: static model ID whitelists (dryer/Hyperion/M1)
- is_dryer / is_hyperion_halia_hera / is_m1_muse_metis — no device query
- refresh(): query DP 237 once, cache in memory

ZeoStartParams (zeo_containers.py): expanded to 13 fields covering washer + dryer
ZEO_PROTOCOL_ENTRIES (__init__.py): expanded to ~69 DP-type mappings
RoborockZeoProtocol: add UNKNOWN_246, UNKNOWN_259 placeholders

Builds on Python-roborock#895 (MQTT push subscription).
@@ -1,142 +1,320 @@
"""Zeo (washing machine) device enums.

Member-level comments show the manufacturer's official identifiers

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This line isn't needed. A lot of what I said on the other or stands here imo

class ZeoFeatureBits(RoborockEnum):
"""Bit positions in DP 237 (FEATURE_BITS).

Extracted from the official Roborock Washer app plugin bundle

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same here

@Lash-L

Lash-L commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Will review more when we merge the other pr as this seems to have some of the same work in it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants