Skip to content

feat: expose Q10 current cleaning room - #939

Open
hCoureau wants to merge 33 commits into
Python-roborock:mainfrom
hCoureau:feat/q10-current-room
Open

hCoureau wants to merge 33 commits into
Python-roborock:mainfrom
hCoureau:feat/q10-current-room

Conversation

@hCoureau

@hCoureau hCoureau commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Q10 firmware does not provide a validated active-room field. This exposes map.current_room by conservatively matching the latest live robot position to the segmented map grid.

Behavior

  • Return the existing Q10Room model or None, with a defensive copy of a resolved room.
  • Use the exact segmented cell when available. A bounded nearby-cell search handles unsegmented positions; ambiguous results remain None.
  • Prefer usable header calibration, with path-fit calibration as a fallback for older or incomplete packets.
  • Require an active or paused cleaning state and a fresh live trace. Inactive/docked updates clear freshness so a prior session is not reused.
  • Preserve custom room labels and redact raw room names in diagnostics.

Session validity stays in the trait. Spatial resolution is a pure function of typed map/trace models.

Dependencies

Depends on #938 and #937. #936, #908, and #965 are merged. Upstream main through b3a98b9 (7.12.0) is integrated.

Allen’s follow-up feedback on merged #936 is addressed by separate clean-record and saved-map decoder tests, with no conditional assertions.

Validation and limits

Validation of 33b5af5:

  • uv run pytest -q on Python 3.13: 1,497 passed, 53 xfailed, 92 snapshots passed.
  • All pre-commit hooks passed, including Ruff, mypy, codespell, and structured-file checks.
  • uv build passed for both wheel and sdist.
  • The affected cumulative protocol suite passed on Python 3.11 and 3.14: 96 passed, 10 snapshots passed on each version. The full cumulative suite passed on both versions before this test-only follow-up and merge.

No physical-device run was performed for this revision.

Existing tests cover room resolution, ambiguity, calibration, session freshness, custom labels, and diagnostic redaction. Earlier physical Q10 S5/S5+ testing observed a custom room label during a single-room clean and clearing during return/docking. A room-to-room transition was not covered by that reported run.

This is inferred room occupancy during cleaning, not an authoritative firmware report of the selected cleaning segment. Missing or unusable map/trace data returns None. Archive requests inherit the bounded lifecycle from #937, including its clean-history ambiguity barrier and same-map freshness limitation.

No private map captures, room labels, or account data are included. Related: #767.

@hCoureau hCoureau mentioned this pull request Aug 31, 2026
8 of 23 tasks
@lboue

lboue commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Tested against a real device: Roborock Q10 S5+ (ss07 firmware family), on commit cd2621d (this PR's isolated commit, rebased onto its stacked dependencies).

Watched device.b01_q10_properties.map.current_room live via add_update_listener while running a targeted clean:

{"time": "13:16:10", "state": null,            "room_id": null, "room_name": null,        "position": null}
{"time": "13:16:11", "state": "SWEEP_AND_MOP",  "room_id": null, "room_name": null,        "position": {"x": -976, "y": -888}}
{"time": "13:16:11", "state": "SWEEP_AND_MOP",  "room_id": 3,    "room_name": "Salle d’eau","position": {"x": -976, "y": -888}}
...
{"time": "13:18:26", "state": "RETURNING_HOME", "room_id": null, "room_name": null,        "position": {"x": -412, "y": -508}}
{"time": "13:20:03", "state": "EMPTYING_THE_BIN","room_id": null,"room_name": null,        "position": null}

Observations:

  • current_room correctly reflects a custom room label (Salle d’eau, French for "bathroom") rather than a raw firmware rr_... default - matches the "preserve custom room labels" goal.
  • It's briefly None right after the position/state first arrive (before the first segmented-grid match resolves), then locks onto the right room.
  • It correctly clears back to None as soon as the state moves to RETURNING_HOME / EMPTYING_THE_BIN, confirming stale session state doesn't leak into docking/post-clean states.

This was a single-room spot clean, so I didn't get to observe a room-to-room transition in this pass.

@allenporter allenporter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note: Conducting this code review following our /review skill using Antigravity (AGY) and Gemini 4 Argon.

Preserving custom room labels verbatim in Q10Room.name, redacting rawName in diagnostics, and calibrating trace coordinates from the header metadata look good.

We can simplify the current_room trait state and tests considerably:

  1. Remove _CURRENT_ROOM_STATES and self._current_room_trace_fresh state tracking in MapContentTrait:

    • Right now MapContentTrait gates current_room on _CURRENT_ROOM_STATES and tracks self._current_room_trace_fresh across status and trace updates.
    • This makes current_room inconsistent with robot_position: when the robot is IDLE or RETURNING_HOME (or when map+trace packets arrive before the first status DPS), trait.robot_position returns the live position from self._trace_packet while trait.current_room is forced to None.
    • In V1 (RoborockMapDataParser._get_current_vacuum_room), the current room (map_data.vacuum_room) is simply the room at the robot's position whenever the map and robot position are present—without gating on cleaning status.
    • Since MapContentTrait already clears self._trace_packet = None when self._map_dps.robot_at_dock is true, current_room can be a stateless 3-line property derived directly from (self._map_packet, self._trace_packet). That also removes the need for the 7 mock-heavy status/arrival-order permutation tests in tests/devices/traits/b01/q10/test_map.py.
  2. Remove defensive math.isfinite checks on int coordinates and replace(room) cloning:

    • See inline comments in roborock/map/b01_q10_render.py.
  3. Test through public entrypoints rather than private helpers or patched functions:

    • See inline comments in tests/devices/traits/b01/q10/test_map.py and tests/map/test_b01_q10_render.py.

outside cleaning states or whenever the position cannot be mapped
unambiguously.
"""
if (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's remove _CURRENT_ROOM_STATES and self._current_room_trace_fresh so current_room stays consistent with robot_position (and V1's map_data.vacuum_room):

        if self._map_packet is None or self._trace_packet is None:
            return None
        return resolve_q10_current_room(self._map_packet, self._trace_packet)

(self._trace_packet is already cleared when self._map_dps.robot_at_dock is true.)

room.
"""
pixel_x, pixel_y = calibration.world_to_pixel(position.x, position.y)
if not math.isfinite(pixel_x) or not math.isfinite(pixel_y):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Q10Point.x and Q10Point.y are int values unpacked from int16 wire bytes, and calibration.resolution > 0 is already validated, so pixel_x and pixel_y are always finite. Let's remove the math.isfinite checks here and in _trace_projects_onto_floor (as well as test_room_at_position_rejects_nonfinite_coordinates which uses cast(int, float("nan"))).

return None
return solve_calibration_with_origin(packet.layers, points, origin, resolutions=_Q10_RESOLUTIONS)
room = _room_at_position(packet, position, calibration)
return replace(room) if room is not None else None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No need to clone room via replace(room) on every property access (consistent with trait.rooms and trait.path). We can return _room_at_position(packet, position, calibration) directly and remove the mutation assertions in the tests.

trait.update_from_map_packet(packet)
trait.update_from_trace_packet(trace)

with patch(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Once _CURRENT_ROOM_STATES and self._current_room_trace_fresh are removed, we can replace these 7 status/arrival-order tests that patch resolve_q10_current_room with a direct test using a map packet and a trace packet whose point resolves to one of the fixture rooms.

q10_api._handle_message(parse_map_packet(b"\x04\x01" + payload[2:]))
assert q10_api.map.current_room == current.rooms[0]

assert not hasattr(q10_api.clean_history, "current_room")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We don't need to assert not hasattr(..., "current_room") on unrelated traits.

packet = _room_packet([12, 12, 12, 12, 8, 12, 12, 12, 12], width=3)
calibration = GridCalibration(resolution=10, origin_x=0, origin_y=0, y_sign=-1)

assert _room_at_position(packet, Q10Point(10, 10), calibration) == packet.rooms[0]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please test room resolution through the public resolve_q10_current_room(packet, trace) function rather than importing and calling the private _room_at_position helper directly.

This branch has not been deployed

No deployments
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.

3 participants