Skip to content

fix: request Q10 maps through dpMultiMap - #901

Open
tubededentifrice wants to merge 4 commits into
Python-roborock:mainfrom
tubededentifrice:vc/q10-map-refresh
Open

fix: request Q10 maps through dpMultiMap#901
tubededentifrice wants to merge 4 commits into
Python-roborock:mainfrom
tubededentifrice:vc/q10-map-refresh

Conversation

@tubededentifrice

@tubededentifrice tubededentifrice commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR separates the Q10 saved-map list from the Q10 map content.

MapsTrait requests and stores the saved-map list. MapContentTrait requests content for a stored map ID. Q10PropertiesApi routes both map-list data and other DPS data through its normal update list.

Why

The map list and map content can change at different times. A general status refresh does not reliably publish the map. The Q10 map request has this flow:

  1. Send dpMultiMap with op: list.
  2. Receive the saved-map list later in the DPS stream.
  3. Send dpMultiMap with op: get and a string map ID.
  4. Receive the content later in a protocol-301 MAP_RESPONSE.

The map list is an initial dependency only when no map ID is available. After that, a caller can refresh map content without a new list request.

Changes

  • Add MapsTrait for the map list, its typed converter, and the list request.
  • Keep content requests, map packets, trace packets, and rendered content in MapContentTrait.
  • Require CommandTrait in both traits that send commands.
  • Add MapsTrait to _updatable_traits.
  • Ignore op: get acknowledgements in MapsTrait so they cannot replace a valid list.
  • Keep general status, map-list, and map-content refresh schedules separate.
  • Make the CLI request the list only when it has no stored map ID.
  • Wait for a real map or trace packet, not an overlay-only render.
  • Use the standard DPS converter pattern for the typed string map ID.

Validation

  • pytest -q: 895 tests passed and 86 snapshots passed.
  • All pre-commit checks passed, including Ruff and mypy.
  • The branch includes the latest main commit.

Relationship to #900

This PR is independent and targets main. The rendering work is split into #902, #903, and #904. PR #900 remains the full real-device and Home Assistant reference.

Comment thread roborock/devices/traits/b01/q10/__init__.py Outdated
Comment thread roborock/devices/traits/b01/q10/__init__.py Outdated
Comment thread roborock/devices/traits/b01/q10/__init__.py Outdated
Comment thread roborock/devices/traits/b01/q10/__init__.py Outdated

@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.

Please see requested changes to where the code lives

# only updates the fields that it is responsible for.
for trait in self._updatable_traits:
trait.update_from_dps(message.dps)
await self.map.update_from_dps(message.dps)

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.

does it make sense for map to be at the end of the list of _updatable_traits?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Its DPS update is synchronous. Q10PropertiesApi can then request the map content after the list update.

Comment thread roborock/devices/traits/b01/q10/map.py Outdated

async def refresh(self) -> None:
"""Request the current saved map independently of general status."""
if self._command is 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.

This seems like a case that would never happen give it is always passed into the constructor. Update the typing to be CommandTrait?

Comment thread roborock/devices/traits/b01/q10/map.py Outdated
multi_map: dpMultiMap | None = field(default=None, metadata={"dps": B01_Q10_DP.MULTI_MAP})


class MapContentTrait(MapListDps, TraitUpdateListener):

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.

My previous comment to move things here may have been based on a misunderstanding of what is happening. Perhaps you can explain the overview of whats happening here. I see multiple requests happening... One is to refresh the list of maps and one is to get the map content?

In v1 we use separate traits for these separate concepts:

  • maps: gets the list of maps
  • map_content: gets the image byte content of for the current map
  • home: combines all the data from rooms/maps/etc to hold state e.g.in case different map flags are loaded etc

It seems like we're moving to do something more like that here were we need muliple things, but i'm not sure so I don't want to give you bad advice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The current code combines two separate operations:
dpMultiMap with op: list requests the map list. The list arrives later in the DPS stream.
The code reads the first string map ID and sends dpMultiMap with op: get.
The get command does not contain the map data. The map data arrives later as a protocol-301 MAP_RESPONSE.
I combined these operations in MapContentTrait because the first response is asynchronous. However, this mixes the map-list state with the map-content state.
I propose this split:
MapsTrait owns the map list, the typed converter, and the list request.
MapContentTrait owns the get request, map packets, and rendered content.
Q10PropertiesApi connects the asynchronous map-list response to the map-content request. This has the same role as the V1 home-level coordination.
Does this match the design that you expect?

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.

Having split responsibilities like that seems good to me.

However on implementation details: I don't think we necessarily need to coordinate a map list with async updating map content. My take is that map lists may or may not change over time independent of map content. There may be a dependency, but only initially? I do understand that loading a new map can invalidate the map image which can be handled when accessing the map content.

Thank you for the thoughtful consideration on approach.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. The map list is only a dependency when no map ID is available or when the selected map changes. Map content can otherwise refresh independently with the stored map ID.

So let's split the responsibilities:

  • MapsTrait owns the map list
  • MapContentTrait gets content for the stored map ID
  • Q10PropertiesApi does not request content after each list update
  • I will also require CommandTrait and add MapsTrait to _updatable_traits

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