Repository navigation
feat: implement Q10VacuumSimulator for B01 integration testing - #2
Closed
allenporter wants to merge 21 commits into
Closed
allenporter wants to merge 21 commits into
allenporter wants to merge 21 commits into
Conversation
…oborock#851) * feat: expand Q10 (B01/ss07) status support and add device info Verified against two physical Roborock Q10 (roborock.vacuum.ss07) devices: - Add the roborock.vacuum.ss07 schema to device_info.yaml (captured via `roborock get-device-info`), per CONTRIBUTING. - Expand Q10Status to capture the ~25 additional fields the device reports in its full status dump (volume, child_lock, DND, net_info, time_zone, etc.). - Fix dpNetInfo/dpTimeZone container field names so the decamelized device keys actually parse (they previously failed to construct). - Add a CLI `status` path for B01 Q10 devices, which refreshes and prints the decoded status (previously `status` only worked for V1 devices). - Add a second real (scrubbed) full-status protocol fixture and extend the status trait tests to cover the new fields. * feat: add Q10 (B01/ss07) settings writers Adds a SettingsTrait so Q10 settings the status trait already reads can also be written: volume, child lock, Do Not Disturb, indicator LED, and dust collection. Q10 setting writes were reverse-engineered + validated against a live ss07 robot: the bare data point write ({"dps": {"26": v}}) is silently ignored; the value must be wrapped in the dpCommon (101) data point ({"dps": {"101": {"26": v}}}). Verified live by setting volume and child lock and reading the new value back from the status trait, then restoring. - traits/b01/q10/settings.py: SettingsTrait wired into Q10PropertiesApi. - cli: q10-set-volume / -child-lock / -dnd / -led / -dust-collection. - Unit tests assert the exact dpCommon-wrapped payload for each setter. * feat: add Q10 dust-collection frequency writer Adds SettingsTrait.set_dust_collection_frequency (dpDustSetting 50 via the dpCommon wrapper) accepting YXDeviceDustCollectionFrequency or its int code (0=daily, 15/30/45/60 cleans), plus a q10-set-dust-frequency CLI command. Validated live on an R1: dust_setting 0 -> 15 -> restored to 0. * refactor: split Q10 status/settings into per-concern traits Address PR Python-roborock#846 review feedback: - Split the monolithic Q10Status read-model + catch-all SettingsTrait into per-concern read/write traits mirroring the v1 layout: SoundVolumeTrait, ChildLockTrait, DoNotDisturbTrait, DustCollectionTrait, ButtonLightTrait, NetworkInfoTrait, ConsumableTrait. Each owns its read-model and setters. - Add an UpdatableTrait base (q10/common.py) that wires a RoborockBase read-model to the DPS push stream; Q10PropertiesApi fans each decoded message out to every read-model trait. - Convert clear on/off flags to bool (child_lock, not_disturb, dust_switch, auto_boost, multi_map_switch, line_laser_obstacle_avoidance) and map dust_setting to YXDeviceDustCollectionFrequency. - set_frequency now takes YXDeviceDustCollectionFrequency only (no int union). - Drop redundant decamelize comments on dpNetInfo/dpTimeZone; keep the note documenting the device's "ipAdress" typo. - Update CLI Q10 setters and tests for the new trait layout. * feat: decode Q10 carpet/area/mop/floor-direction status into enums+bools Reverse-engineered against live Q10 hardware (toggling each setting in the app and diffing the status dump): - area_unit -> YXAreaUnit (square_meter=0, square_feet=1) - carpet_clean_type -> YXCarpetCleanType (rise=0, avoid=1, ignore=2, cross=3) - mop_state -> bool (mop module attached) - ground_clean -> bool ("clean along floor direction") - Rename YXDeviceDustCollectionFrequency.DAILY -> REGULAR to match the app ("regular" vs "frequent" 15/30/45/60); codes unchanged. breakpoint_clean / add_clean_state / timer_type / user_plan / robot_type remain int (no app control found, runtime-only, or constant). * feat: decode Q10 add_clean_state as a bool Observed on live hardware: dpAddCleanState (96) pulses 0->1 while the app's "re cleaning" (draw-a-rectangle / add-area) request is in progress, then back to 0 once the robot has the area. Model it as a bool. * fix: stop unmapped Q10 data points from logging "not a valid code" warnings ss07 hardware pushes data points this library does not model (confirmed live: DPs 112 and 113, which stay 0 across docked/charging, segment cleaning, lifted-off-ground fault, returning to dock and dustbin-removed states, and are absent from the official app's vacuum plugin). Decoding every status push ran them through `from_code_optional`, which routed through `from_code` and logged a WARNING before returning None -- so each unknown DP spammed "<code> is not a valid code for B01_Q10_DP". - Make `from_code_optional` (and the int path of `from_any_optional`) do their own silent lookup instead of going through `from_code`. `from_code` stays strict (warns + raises) for callers that require a known code. - Remove the duplicate-coded legacy entries `JUMP_SCAN` (101) and `CLIFF_RESTRICTED_AREA` (102): they collided with the confirmed `COMMON` (101) / `REQUEST_DPS` (102) codes, shadowing them in `from_code`, and are unused with unverifiable codes. Also drop the orphaned `CLIFF_RESTRICTED_AREA_UP` (103). - Document DPs 112/113 as observed-but-unidentified. - Tests: assert `from_code_optional` is silent, `from_code` still warns, and the B01 decode drops 112/113 without logging. * fix: avoid Q10 Consumable/NetworkInfo shadowing v1 in roborock.data The new per-concern Q10 read-models `Consumable` and `NetworkInfo` were star-exported into the `roborock.data` namespace where v1 also exports those names, so `from roborock.data import Consumable` silently resolved to the v1 class (and mypy flagged the incompatible re-import). Rename the Q10 read-models to `Q10Consumable` / `Q10NetworkInfo`; the traits already import them by fully qualified path. * fix: keep Q10 CLIFF_RESTRICTED_AREA_UP (103); ss07 pushes it While removing the duplicate-coded legacy entries I also dropped DP 103, but live capture shows ss07 hardware does push data point 103 (an empty list when no cliff-restricted areas are configured). Restore CLIFF_RESTRICTED_AREA_UP at 103; only the genuinely-colliding dpJumpScan (101) and dpCliffRestrictedArea (102) stay removed. * fix: correct Q10 vacuum command payloads, verified against ss07 hardware The VacuumTrait movement commands were unverified guesses (with TODO notes) and sent payload shapes the device ignores. Reverse-engineered the correct format from the official app's vacuum plugin and confirmed each one live against an ss07 robot (watching the DP stream react): - start_clean: {"201": 1} (was {"201": {"cmd": 1}}) -> clean_task_type 1 - pause_clean: {"204": 0} (was {"204": {}}) - resume_clean: {"205": 0} (was {"205": {}}) - stop_clean: {"206": 0} (was {"206": {}}) - return_to_dock: {"202": 5} (was {"203": {}}; dpStartBack=charge, and DP 203 is not the dock-return command) - empty_dustbin: {"203": 2} (already correct; confirmed -> status emptying) Also add spot_clean ({"201": 5} -> clean_task_type 5), confirmed live, plus a `q10-vacuum-spot` CLI command. set_fan_level / set_clean_mode were confirmed to work as bare DP writes (no dpCommon wrapper needed, unlike the settings). Segment ({"201": 2}), zone ({"201": 3}) and build-map ({"201": 4}) clean are documented but not exposed: they need a room/zone selection payload whose shape could not be verified (the broker rejects subscribing to the app's command topic, so the app's exact request can't be captured). * fix: show all Q10 read-model traits in status, wait for fresh push The Q10 status command only printed properties.status, omitting the other read-model traits that refresh() populates (volume, child lock, do-not-disturb, dust collection, network info, consumables). It also gated on properties.status.status is not None, which the persistent subscribe loop can satisfy with stale data pushed before the command ran. Register update listeners on all refreshed traits, fire refresh(), and wait for a fresh update (with a short settle window for multi-packet pushes) before dumping every trait. Verified live against ss07 (R1). * feat: Q10 (B01/ss07) room/segment cleaning Add VacuumTrait.clean_segments(segment_ids) for the Q10. dpStartClean (201) carries the room selection as an object: {"cmd": 2, "clean_paramters": [ids]} (cmd 2 = segment clean; "clean_paramters" mirrors the firmware's misspelling). Segment ids are the same room ids reported by the map (map.rooms). Captured from the official app and verified live against ss07 hardware (clean_task_type -> 2 / electoral). Adds a q10-clean-segments CLI command and trait payload tests. * refactor: use YXDeviceCleanTask.ELECTORAL and inline misspelling note Address review feedback on clean_segments: replace the literal 2 with YXDeviceCleanTask.ELECTORAL.code, move the 'clean_paramters' misspelling note inline next to the params, and drop the official-app comment.
* feat: add Q10 (B01/ss07) clean-record history trait
Q10 clean history is push-driven over dpCleanRecord (DP 52), unlike the Q7's
synchronous get_record_list RPC. Adds:
- Q10CleanRecord: parses the 12-field op:list underscore string (raw retained),
with crash-safe enum accessors .scope/.work/.result/.started_by (IntEnums that
return None for an unmapped code, never raising like the YX from_code path)
- CleanHistoryTrait: refresh() sends {op:list}; update_from_dps decodes both the
full list and the single op:notify clean-finished push (upsert by record_id,
newest-first); registered as a read-model trait in the dispatch loop
Field layout is the device app's own (setHoldData), cross-confirmed by ioBroker's
Q10CleanRecordService. Tests cover decode, enum labels + unmapped-safety, and the
list/notify paths.
* refactor: widen parse_record's parameter type to Any | None
Co-authored-by: Allen Porter <allen.porter@gmail.com>
* refactor: call the static parse_record via the class, not self
parse_record is a @staticmethod, so call it as CleanRecordConverter.parse_record(...)
rather than self.parse_record(...).
---------
Co-authored-by: Andrew Yeats <ayeats@users-MacBook-Air.local>
Co-authored-by: Allen Porter <allen.porter@gmail.com>
…roborock#858) * fix: verify MQTT connection requires successful subscription * docs: clarify is_mqtt_connected docstring with motivation * refactor: remove unused fixtures in tests
…it (Python-roborock#861) This change enables Home Assistant to dynamically add and remove maps/camera entities without restarting. Resolves HA issue: home-assistant/core#175400
* feat: add better cleaning mode support * chore: address comments from copilot * chore: address comments from copilot * chore: address comments from copilot * chore: address comments * chore: drop int typing and improve comment * chore: add some comments that better explain 'get_mop_only_vacuum_mode'
* chore: add dataclasses for Roborock mower * chore: apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…-roborock#863) * feat: decode Q10 (B01/ss07) dpFault codes into a YXFault map Add a Q10-specific YXFault enum (seeded from the ss07 app fault i18n) and an additive Q10Status.fault_name property. The raw fault int is untouched, so unknown/new codes are never lost. Addresses Python-roborock#855. * refactor: type Q10Status.fault as YXFault, mirroring the Q7 fault field Retype the fault field from a raw int to YXFault | None and make fault_name a str accessor, matching B01Props.fault / fault_name on the Q7. Update the enum provenance notes from live hardware confirmation (10 codes) and drop the unconfirmed 569 label. * docs: note fault 501 fires per completed task * docs: mark fault 5 hw-confirmed (physically ground-truthed brush jams) * docs: mark fault 12 hw-confirmed (fired at 14% battery, corpus recount) * docs: mark fault 407 hw-confirmed (deterministically reproduced: due schedule ignored mid-clean) * docs: mark fault 46 hw-confirmed (docked with dust bag removed) * feat: re-add fault 569 as too_frequent_emptying, hw-confirmed (dock refuses a too-soon empty) * docs: add 569 back to the docstring's Q7-divergence list * docs: mark fault 2 hw-confirmed (bumper held during commanded motion) * docs: note the error state can carry no fault code (observed live) * docs: refer to the ss07 fault spec, not the app (review feedback) --------- Co-authored-by: Andrew Yeats <ayeats@users-MacBook-Air.local>
…ting (Python-roborock#860) * feat: add roborock.testing module for stateful integration testing Introduces a new roborock.testing package that provides stateful firmware simulators, fake transport channels, and cloud environment fakes. This allows downstream consumers (like the Home Assistant integration) to write high-fidelity integration tests using the real client library classes instead of fragile top-level mocks. New modules: - channel.py: FakeChannel in-memory transport implementing Channel protocol - simulator.py: RoborockDeviceSimulator base class - v1_simulator.py: V1VacuumSimulator with stateful command handlers - cloud.py: FakeRoborockCloud with HTTP endpoint mocking Tests split by module: - test_channel.py: FakeChannel subscribe/publish/notify - test_cloud.py: Discovery, login errors, dynamic device addition - test_v1_simulator.py: Trait refresh/reset, state transitions, push updates * docs: add comment explaining dss field in v1_simulator * feat: expose in_cleaning, in_returning, charge_status, and dss on V1VacuumSimulator * feat: expose dock_type configuration in V1VacuumSimulator * feat: honor publish_side_effect in simulator and raise NotImplementedError in base _handle_publish * refactor: inherit FakeChannel from Channel Protocol * refactor: type simulator consumables state using Consumable dataclass * refactor: type simulator dnd, clean summary and record using real dataclasses * refactor: type simulator status, network info and init status using real dataclasses * refactor: expose raw status and app_init dataclasses on simulator for direct test manipulation * refactor: export and define all simulator state default constants at package level * refactor: update _handle_app_get_init_status to serialize self.app_init and dynamically resolve status_info fields from self.status * test: add E2E test case for multiple simulators with independent network info overrides * refactor: replace FakeUserState with library UserData dataclass on FakeRoborockCloud * refactor: decouple default product id from device duid and share DEFAULT_PRODUCT_ID constant * refactor: rename self.servers to self.simulated_devices and restore hardcoded qiCNieZa in mock_data * refactor: delete redundant properties from V1VacuumSimulator and update trigger_push_update
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds stateful device simulation support for B01 protocol devices (specifically Roborock Q10 / ss07 model) in
roborock.testing.Changes
Q10VacuumSimulatorinroborock/testing/b01_q10_simulator.pywith stateful DP (Data Point) tracking.dpCommon) sub-property mapping (sound volume, DND, auto-dust collection, child lock).FakeChannel.tests/testing/test_q10_simulator.py.Verification
Ran test suite (
uv run python -m pytest), all 748 tests passed.