Skip to content

Roborock Q10 (B01) part 1 vacuum entity#163112

Closed
lboue wants to merge 52 commits intohome-assistant:devfrom
lboue:feat/roborock-q10-s5-plus-vacuum
Closed

Roborock Q10 (B01) part 1 vacuum entity#163112
lboue wants to merge 52 commits intohome-assistant:devfrom
lboue:feat/roborock-q10-s5-plus-vacuum

Conversation

@lboue
Copy link
Copy Markdown
Contributor

@lboue lboue commented Feb 15, 2026

Proposed change

Add Roborock Q10 (B01) vacuum support, including Q10-specific coordinator handling, entity features (vacuum, sensors, selects), and command mapping.
Update error handling for unsupported map services, add translations, and expand test coverage for Q10 behavior and helper mappings.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

  • This PR fixes or closes issue: fixes #
  • This PR is related to issue:
  • Link to documentation pull request:
  • Link to developer documentation pull request:
  • Link to frontend pull request:

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

@home-assistant
Copy link
Copy Markdown

Hey there @Lash-L, @allenporter, mind taking a look at this pull request as it has been labeled with an integration (roborock) you are listed as a code owner for? Thanks!

Code owner commands

Code owners of roborock can trigger bot actions by commenting:

  • @home-assistant close Closes the pull request.
  • @home-assistant rename Awesome new title Renames the pull request.
  • @home-assistant reopen Reopen the pull request.
  • @home-assistant unassign roborock Removes the current integration label and assignees on the pull request, add the integration domain after the command.
  • @home-assistant add-label needs-more-information Add a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) to the pull request.
  • @home-assistant remove-label needs-more-information Remove a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) on the pull request.

@lboue lboue changed the title Feat/roborock q10 s5 plus vacuum Roborock Q10 (B01) part 1 vacuum entity Feb 15, 2026
base_url=entry.data[CONF_BASE_URL],
)
cache = CacheStore(hass, entry.entry_id)
try:
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.

Changes to init should probably be in a separate PR. Also the approach for error message handling by string is not following the existing design patterns, and having some separate exception types if needed would be more appropriate. (e.g. raising a specific exception when rate limited)

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.

Even adding RoborockB01Q10UpdateCoordinator?

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, adding the coordinator is required, and is OK. I just meant everything else: Changing prefer_cache or changing the exception handling behavior are independent of adding a new device I believe.

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.

OK

Comment thread homeassistant/components/roborock/__init__.py Outdated
Comment thread homeassistant/components/roborock/coordinator.py Outdated
Comment thread homeassistant/components/roborock/coordinator.py Outdated
Comment thread homeassistant/components/roborock/select.py Outdated
Comment thread homeassistant/components/roborock/vacuum.py Outdated
Comment thread homeassistant/components/roborock/vacuum.py Outdated
@lboue lboue mentioned this pull request Feb 22, 2026
21 tasks
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

homeassistant/components/roborock/vacuum.py:497

  • Missing await self.coordinator.async_refresh() call after async_return_to_base command. All other state-changing commands in this class (async_start, async_pause, async_stop, async_locate, async_set_fan_speed) call async_refresh() after executing their commands to update the coordinator's state. This method should follow the same pattern for consistency.
    async def async_return_to_base(self, **kwargs: Any) -> None:
        """Send vacuum back to base."""
        try:
            await self.coordinator.api.return_to_dock()
        except RoborockException as err:
            raise HomeAssistantError(
                translation_domain=DOMAIN,
                translation_key="command_failed",
                translation_placeholders={
                    "command": "return_to_dock",
                },
            ) from err

@lboue lboue marked this pull request as ready for review February 23, 2026 08:10
Copilot AI review requested due to automatic review settings February 23, 2026 08:10
@home-assistant home-assistant bot requested a review from allenporter February 23, 2026 08:10
@lboue
Copy link
Copy Markdown
Contributor Author

lboue commented Feb 23, 2026

I cleaned up and moved the code concerning Q7 to another PR.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comment thread tests/components/roborock/conftest.py
Copilot AI review requested due to automatic review settings February 23, 2026 14:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comment on lines +569 to +625
class RoborockB01Q10UpdateCoordinator(DataUpdateCoordinator[dict[B01_Q10_DP, Any]]):
"""Coordinator for B01 Q10 devices."""

config_entry: RoborockConfigEntry

def __init__(
self,
hass: HomeAssistant,
config_entry: RoborockConfigEntry,
device: RoborockDevice,
api: Q7PropertiesApi,
api: Q10PropertiesApi,
) -> None:
"""Initialize."""
super().__init__(hass, config_entry, device)
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_interval=A01_UPDATE_INTERVAL,
)
self._device = device
self.device_info = DeviceInfo(
name=device.name,
identifiers={(DOMAIN, device.duid)},
manufacturer="Roborock",
model=device.product.model,
sw_version=device.device_info.fv,
)
self.api = api
self.request_protocols: list[RoborockB01Props] = [
RoborockB01Props.STATUS,
RoborockB01Props.MAIN_BRUSH,
RoborockB01Props.SIDE_BRUSH,
RoborockB01Props.DUST_BAG_USED,
RoborockB01Props.MOP_LIFE,
RoborockB01Props.MAIN_SENSOR,
RoborockB01Props.CLEANING_TIME,
RoborockB01Props.REAL_CLEAN_TIME,
RoborockB01Props.HYPA,
RoborockB01Props.WIND,
RoborockB01Props.WATER,
RoborockB01Props.MODE,
]

@cached_property
def duid(self) -> str:
"""Get the unique id of the device as specified by Roborock."""
return self._device.duid

@cached_property
def duid_slug(self) -> str:
"""Get the slug of the duid."""
return slugify(self.duid)

@property
def device(self) -> RoborockDevice:
"""Get the RoborockDevice."""
return self._device

async def _async_update_data(
self,
) -> B01Props:
) -> Any:
try:
data = await self.api.query_values(self.request_protocols)
await self.api.refresh()
except RoborockException as ex:
_LOGGER.debug("Failed to update Q7 data: %s", ex)
_LOGGER.debug("Failed to update Q10 data: %s", ex)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_data_fail",
) from ex
if data is None:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_data_fail",
)
return data
return self.api.status
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The coordinator's generic type parameter DataUpdateCoordinator[dict[B01_Q10_DP, Any]] doesn't match what _async_update_data actually returns. The method returns self.api.status (line 625), which based on the test code (lines 1328-1335 in test_vacuum.py) is a status trait object with a .data attribute, not a raw dictionary. This mismatch could cause type checking issues. Consider either:

  1. Using a more specific type like DataUpdateCoordinator[Q10Status] if that's the actual type, or
  2. Returning self.api.status.data instead of self.api.status to match the declared dict type.

Note that Q10 entities access data via self.coordinator.api.status rather than self.coordinator.data, which is unconventional but intentional for the Q10's push-based MQTT updates.

Copilot uses AI. Check for mistakes.

class RoborockB01Q7UpdateCoordinator(RoborockDataUpdateCoordinatorB01):
"""Coordinator for B01 Q7 devices."""
class RoborockB01Q10UpdateCoordinator(DataUpdateCoordinator[dict[B01_Q10_DP, Any]]):
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 no longer works with B01_Q10_DP

translation_key="update_data_fail",
)
return data
return self.api.status
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.

So big picture I think what will happen is:

  • coordinator sends polls to the device "hey, please refresh"
  • we won't know when they arrive, so here at this point we may have stale data. that is, the coordinator likely won't be able to hold any state data so its data type should probably just be None
  • the trait itself will notify listeners when data arrives as you have done with the vacuum entity. it will update its own state when that happens
  • the coordinator + entity already has a method for handling state updates on the coordinated entity. that probably needs to be overridden to be a no-op since the coordinator wont know when new data is available

So this needs a couple more changes

@property
def activity(self) -> VacuumActivity | None:
"""Return the status of the vacuum cleaner."""
if self.coordinator.data.status is not 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.

seems like this doesn't need to change in this PR

},
) from err

async def get_maps(self) -> ServiceResponse:
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.

i'm still seeing this

)
_attr_translation_key = DOMAIN
_attr_name = None
_attr_fan_speed_list = [
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.

Do you know what "CLOSE" is or if its ever returned?

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.

I don't understand the connection between the selected lines and the message about CLOSE.

# Simulate MQTT data update
status_trait.update_from_dps({B01_Q10_DP.STATUS: q10_trait._current_status})

q10_trait.start_clean = AsyncMock(side_effect=start_clean_side_effect)
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.

Q10PropertiesApi does not have any of these methods

initial_data[B01_Q10_DP.STATUS] = q10_trait._current_status
status_trait.update_from_dps(initial_data)

q10_trait.start = AsyncMock(side_effect=start_side_effect)
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.

in the device: "start" will subscribe, but it won't request anything explicitly. Is an update automatically sent or do we need to query first?

Comment thread tests/components/roborock/conftest.py Outdated
device.device_id = "q10_s5_plus_duid"
device.name = "Roborock Q10 S5+"
device.model = "roborock.vacuum.q10"
device.product_id = "q10_product_id"
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.

Many of these fields do not actually exist on RoborockDevice. Please use FakeDevice like the other tests, populated from home data (device data and product data).

Comment thread tests/components/roborock/conftest.py Outdated

# Add B01 trait for Q10
device.b01_properties = create_b01_q10_trait()
device.status_trait = create_b01_q10_trait()
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.

status_trat is not a thing on device

Comment thread tests/components/roborock/conftest.py Outdated
device.b01_properties = create_b01_q10_trait()
device.status_trait = create_b01_q10_trait()

# Mock async methods
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.

I don't think these two things exist either

@home-assistant home-assistant bot marked this pull request as draft February 23, 2026 15:36
@lboue
Copy link
Copy Markdown
Contributor Author

lboue commented Mar 19, 2026

Closing this in favor of #165624

@lboue lboue closed this Mar 19, 2026
@github-actions github-actions bot locked and limited conversation to collaborators Mar 20, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants