Skip to content

Add Full support for roborock Zeo washing/drying machines#159575

Merged
allenporter merged 41 commits intohome-assistant:devfrom
yangqian:roborock-cloud-push-for-A01-devices
Feb 25, 2026
Merged

Add Full support for roborock Zeo washing/drying machines#159575
allenporter merged 41 commits intohome-assistant:devfrom
yangqian:roborock-cloud-push-for-A01-devices

Conversation

@yangqian
Copy link
Copy Markdown
Contributor

@yangqian yangqian commented Dec 22, 2025

Breaking change

Proposed change

The original roborock washing machine only exposes four sesnsors.
Here, this patch adds a bunch more. Not just sensors, but also buttons, selects, and switches.

Configuration Controls & Diagnostic
Configuration Controls

Diagnostic

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 link to the changelog, or at minimum a diff between library versions 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) to the pull request.
  • @home-assistant remove-label needs-more-information Remove a label (needs-more-information, problem in dependency, problem in custom component) on the pull request.

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

This PR adds comprehensive support for Roborock washing machines (A01 devices) by introducing new sensors, buttons, select entities, and switches to control various washing machine functions like programs, temperature settings, spin levels, and more.

Key changes:

  • Adds 6 new select entities for controlling washing machine settings (program, mode, temperature, drying mode, spin level, rinse times)
  • Adds 3 new buttons for controlling the washing machine (start, pause, shutdown)
  • Adds 1 new switch for sound settings
  • Adds 5 new sensors for monitoring detergent/softener status and type
  • Updates coordinator to fetch additional protocol data needed by the new entities
  • Adds comprehensive translations for all new entities

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
button.py Adds A01 button entity class and 3 button descriptions (start, pause, shutdown)
select.py Adds A01 select entity class and 6 select descriptions for washing machine settings
switch.py Adds A01 switch entity class and sound setting switch
sensor.py Adds 5 new sensor descriptions and removes entity_category from some existing sensors
coordinator.py Adds additional protocol fields to request list for washing machine data
strings.json Adds translations for all new entities with proper state mappings
Comments suppressed due to low confidence (1)

homeassistant/components/roborock/button.py:238

  • This PR adds new A01 button entities but doesn't include corresponding test coverage. According to the guidelines, test coverage above 95% is required for all modules. Please add tests for the new RoborockButtonEntityA01 class, covering both successful button presses and error scenarios.
    """A class to define Roborock A01 button entities."""

    entity_description: RoborockButtonDescriptionA01

    def __init__(
        self,
        coordinator: RoborockDataUpdateCoordinatorA01,
        entity_description: RoborockButtonDescriptionA01,
    ) -> None:
        """Create an A01 button entity."""
        self.entity_description = entity_description
        super().__init__(f"{entity_description.key}_{coordinator.duid_slug}", coordinator)

    async def async_press(self) -> None:
        """Press the button."""
        try:
            if self.entity_description.param is not None:
                await self.coordinator.api.set_value(
                    self.entity_description.data_protocol,
                    self.entity_description.param,
                )
            else:
                await self.coordinator.api.set_value(
                    self.entity_description.data_protocol,
                    1,  # Default value for button press
                )
            await self.coordinator.async_request_refresh()
        except Exception as err:
            raise HomeAssistantError(
                translation_domain=DOMAIN,
                translation_key="button_press_failed",
            ) from err

Comment on lines +335 to +336
translation_domain=DOMAIN,
translation_key="select_option_failed",
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The translation key "select_option_failed" is used here but is not defined in strings.json. This will result in an untranslated error message being shown to users. Please add the translation key to the "exceptions" section of strings.json.

Suggested change
translation_domain=DOMAIN,
translation_key="select_option_failed",
"Failed to set selected option; try again."

Copilot uses AI. Check for mistakes.
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 address

self.entity_description.option_values,
)
# Find the option name that matches the current value
return current_value
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The current_option property returns the raw current_value instead of mapping it back to the option name. This will cause the select entity to display the numeric protocol value (e.g., "1", "2") instead of the user-friendly option name (e.g., "wash", "dry"). You need to reverse-lookup the option name from the value, similar to how it's done in the async_select_option method. Consider adding logic like: return next((name for name, val in self.entity_description.option_values.items() if val == current_value), None)

Suggested change
return current_value
return next(
(
name
for name, val in self.entity_description.option_values.items()
if val == current_value
),
None,
)

Copilot uses AI. Check for mistakes.
Comment on lines +304 to +355
class RoborockSelectEntityA01(RoborockCoordinatedEntityA01, SelectEntity):
"""A class to let you set options on a Roborock A01 device."""

entity_description: RoborockSelectDescriptionA01

def __init__(
self,
coordinator: RoborockDataUpdateCoordinatorA01,
entity_description: RoborockSelectDescriptionA01,
) -> None:
"""Create an A01 select entity."""
self.entity_description = entity_description
super().__init__(
f"{entity_description.key}_{coordinator.duid_slug}",
coordinator,
)
self._attr_options = entity_description.options

async def async_select_option(self, option: str) -> None:
"""Set the option."""
try:
# Get the protocol value for the selected option
value = self.entity_description.option_values.get(option)
if value is not None:
await self.coordinator.api.set_value(
self.entity_description.data_protocol,
value,
)
await self.coordinator.async_request_refresh()
except Exception as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="select_option_failed",
) from err

@property
def current_option(self) -> str | None:
"""Get the current status of the select entity from coordinator data."""
if self.entity_description.data_protocol not in self.coordinator.data:
return None

current_value = self.coordinator.data[self.entity_description.data_protocol]
if current_value is None:
return None
_LOGGER.debug(
"current_value: %s for %s with values %s",
current_value,
self.entity_description.key,
self.entity_description.option_values,
)
# Find the option name that matches the current value
return current_value
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

This PR adds new A01 select entities but doesn't include corresponding test coverage. According to the guidelines, test coverage above 95% is required for all modules. Please add tests for the new RoborockSelectEntityA01 class, covering option selection, current_option retrieval, and error scenarios.

Copilot uses AI. Check for mistakes.
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 address this.

except Exception as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="button_press_failed",
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The translation key "button_press_failed" is used here but is not defined in strings.json. This will result in an untranslated error message being shown to users. Please add the translation key to the "exceptions" section of strings.json.

Suggested change
translation_key="button_press_failed",
translation_key="command_failed",
translation_placeholders={
"command": "BUTTON_PRESS",
},

Copilot uses AI. Check for mistakes.
value,
)
await self.coordinator.async_request_refresh()
except Exception as err:
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

This code uses a bare Exception catch, which is too broad. According to the coding guidelines, bare exceptions should only be used in config flows or background tasks. In regular entity code like this, you should catch the specific RoborockException instead. This ensures that unexpected errors are properly surfaced rather than being silently converted to generic HomeAssistantErrors.

Copilot generated this review using guidance from repository custom instructions.
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 address

Comment on lines +176 to +219
class RoborockSwitchA01(RoborockCoordinatedEntityA01, SwitchEntity):
"""A class to let you turn functionality on Roborock A01 devices on and off."""

entity_description: RoborockSwitchDescriptionA01

def __init__(
self,
coordinator: RoborockDataUpdateCoordinatorA01,
description: RoborockSwitchDescriptionA01,
) -> None:
"""Initialize the entity."""
self.entity_description = description
super().__init__(f"{description.key}_{coordinator.duid_slug}", coordinator)

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the switch."""
try:
await self.coordinator.api.set_value(self.entity_description.data_protocol, 0)
await self.coordinator.async_request_refresh()
except RoborockException as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="update_options_failed",
) from err

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the switch."""
try:
await self.coordinator.api.set_value(self.entity_description.data_protocol, 1)
await self.coordinator.async_request_refresh()
except RoborockException as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="update_options_failed",
) from err

@property
def is_on(self) -> bool | None:
"""Return True if entity is on."""
status = self.coordinator.data.get(self.entity_description.data_protocol)
if status is None:
return None
return bool(status)

Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

This PR adds new A01 switch entities but doesn't include corresponding test coverage. According to the guidelines, test coverage above 95% is required for all modules. Please add tests for the new RoborockSwitchA01 class, covering turn on, turn off, and error scenarios.

Copilot uses AI. Check for mistakes.
key="sound_setting",
data_protocol=RoborockZeoProtocol.SOUND_SET,
translation_key="zeo_sound_setting",
entity_category=EntityCategory.DIAGNOSTIC,
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The sound_setting switch is assigned EntityCategory.DIAGNOSTIC, but sound settings are typically user-configurable preferences and should use EntityCategory.CONFIG instead. Looking at similar switches in this file (like child_lock, status_indicator, dnd_switch), they all use EntityCategory.CONFIG for user-configurable settings. DIAGNOSTIC is typically reserved for technical information about the device, not user preferences.

Suggested change
entity_category=EntityCategory.DIAGNOSTIC,
entity_category=EntityCategory.CONFIG,

Copilot uses AI. Check for mistakes.
Comment thread homeassistant/components/roborock/button.py
Copy link
Copy Markdown
Contributor

@allenporter allenporter left a comment

Choose a reason for hiding this comment

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

Thank you very much for this contribution.

Comment thread homeassistant/components/roborock/button.py
"""Describes a Roborock A01 button entity."""

data_protocol: RoborockZeoProtocol
param: Any = 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.

What sets param? How is this ever set to non-None? I think this can be deleted here and below.

# The protocol that the select entity will send to the api.
data_protocol: RoborockZeoProtocol
# Available options for the select entity
options: list[str]
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.

Can you replace options and option_values with a single RoborockEnum field then let the entity call keys or as_dict().items() so every entity description doesn't need to?

Comment on lines +304 to +355
class RoborockSelectEntityA01(RoborockCoordinatedEntityA01, SelectEntity):
"""A class to let you set options on a Roborock A01 device."""

entity_description: RoborockSelectDescriptionA01

def __init__(
self,
coordinator: RoborockDataUpdateCoordinatorA01,
entity_description: RoborockSelectDescriptionA01,
) -> None:
"""Create an A01 select entity."""
self.entity_description = entity_description
super().__init__(
f"{entity_description.key}_{coordinator.duid_slug}",
coordinator,
)
self._attr_options = entity_description.options

async def async_select_option(self, option: str) -> None:
"""Set the option."""
try:
# Get the protocol value for the selected option
value = self.entity_description.option_values.get(option)
if value is not None:
await self.coordinator.api.set_value(
self.entity_description.data_protocol,
value,
)
await self.coordinator.async_request_refresh()
except Exception as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="select_option_failed",
) from err

@property
def current_option(self) -> str | None:
"""Get the current status of the select entity from coordinator data."""
if self.entity_description.data_protocol not in self.coordinator.data:
return None

current_value = self.coordinator.data[self.entity_description.data_protocol]
if current_value is None:
return None
_LOGGER.debug(
"current_value: %s for %s with values %s",
current_value,
self.entity_description.key,
self.entity_description.option_values,
)
# Find the option name that matches the current value
return current_value
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 address this.

Comment on lines +335 to +336
translation_domain=DOMAIN,
translation_key="select_option_failed",
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 address

value,
)
await self.coordinator.async_request_refresh()
except Exception as err:
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 address

translation_key="times_after_clean",
entity_category=EntityCategory.DIAGNOSTIC,
),
RoborockSensorDescriptionA01(
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 looks like a binary_sensor here and below where there are two values

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This one is still relevant and has not been answered to

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.

No, it is a sensor recording how many times the washing machine started after the self-clean operation

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 guess you meant the one below. OK.

"False": "Available"
}
},
"zeo_detergent_type": {
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 think it would make sense to drop zeo_ prefix from the translation keys

@home-assistant home-assistant bot marked this pull request as draft December 22, 2025 16:15
@home-assistant
Copy link
Copy Markdown

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

NoRi2909

This comment was marked as outdated.

@yangqian yangqian marked this pull request as ready for review December 30, 2025 00:27
@home-assistant home-assistant bot requested a review from allenporter December 30, 2025 00:27
@allenporter
Copy link
Copy Markdown
Contributor

This PR makes also a bunch of other small changes to the existing code. Can you send them in logical separate PRs with description that give context/motivation? They should be fast to review. But generally I'm left wondering what is intentional versus accidental AI changes.

device_class=SensorDeviceClass.ENUM,
options=RoborockDyadStateCode.keys(),
),
RoborockSensorDescriptionA01(
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.

why is this removed?

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.

battery is not used in Zeo, but I guess it may show up in other devices, will revert.

),
RoborockSensorDescriptionA01(
key="error",
key="dyad_error",
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 believe that changing this key will change unique ids which will break existing use cases. can this be reverted?

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.

Will revert.

"water_level_error": "Water level error"
}
},
"zeo_state": {
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 realize i told you to rename to not have "zeo" prefix but the existing key is using it. can you revert this so we don't lose translations?

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 think my comment may have been a bad idea for some of the fields that have unique values to these specific devices, like the mode etc

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.

Will revert.

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.

OK though I also think how you had it originally before may be needed in the case where there are modes specific to the zeo device (e.g. if we add another device type it may also have a drying mode with different options). For cases where there are device specific things i think it makes sense to keep. Sorry i'm slow to catch this.

)
for coordinator in config_entry.runtime_data.a01
for description in A01_SWITCH_DESCRIPTIONS
if description.data_protocol in coordinator.data
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 want to avoid adding new dependencies on coordinator.data since we want to make it so it may not be fetched by the time we get here.

If this is not supported on all devices, perhaps we can treat it disabled by default?

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 tested my Zeo device, the switch is added correctly.

Sorry, I did not fully understand your comment. Do you mean to delete this async_add_entities for A01 switches and add "entity_registry_enabled_default=False" to A01_SWITCH_DESCRIPTIONS for "sound_setting"?
Could you make or suggest your change?

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.

Sorry for not fully explaining, let me elaborate.

The problem with depending on coordinator.data here is that it assumes the coordinator has always been initialized in async_setup_entry here. I just made a short writeup on some of the problems with doing this.

Thinking through solutions our options are either:
(1) Understand what features are supported based on the device type / metadata available from the API
(2) Do additional queries and hide them behind the device API
(3) Make the entities optional and let the user figure it out. Yes, this would be entity_registry_enabled_default=False for new items that we don't think are supported.

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 only have one Zeo(A01) device, so I am not sure if sound_setting is universal or not. I'm now just commenting out the if statement.

"door_lock_error": "Door lock error",
"drain_error": "Drain error",
"drying_error": "Drying error",
"drying_error_e_12": "Drying error E12",
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 think these are actually valuable to show to the user? It seems like we should just map all these to drying_error

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 encouter a dain error ealier, the debug info could be helpful.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does E1* mean the same for every device? If so, can we just call it how they call it?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do you still have an answer for this one?

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.

They are the same for all zeo devices. I removed the duplicated ones. One possibility is to add the error message:
│ drying_error │ Drying error E11: air inlet temperature sensor error │
│ drying_error_e_12 │ Drying error E12: air outlet temperature sensor error │
│ drying_error_e_14 │ Drying error E14: inlet condenser temperature sensor error │
│ drying_error_e_15 │ Drying error E15: heating element or turntable error │
│ drying_error_e_16 │ Drying error E16: drying fan error │

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we add the error then? Maybe remove the E part and make sure the state also looks nice

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

@home-assistant home-assistant bot marked this pull request as draft December 30, 2025 05:09
@yangqian yangqian marked this pull request as ready for review December 30, 2025 23:02
@home-assistant home-assistant bot requested a review from allenporter December 30, 2025 23:02
description,
)
for coordinator in config_entry.runtime_data.a01
for description in A01_BUTTON_DESCRIPTIONS
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 think i realize what needs to be checked for eligibility: it's just is the device type zeo or dyad. Check out how other platforms do this, i think they check the coordinator instance type.

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 see. Could you check the new commit?

@joostlek joostlek marked this pull request as draft January 2, 2026 15:08
@yangqian yangqian marked this pull request as ready for review January 10, 2026 15:09
@home-assistant home-assistant bot requested a review from allenporter January 10, 2026 15:09
Copy link
Copy Markdown
Member

@joostlek joostlek left a comment

Choose a reason for hiding this comment

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

There's a merge conflict, can you take a look?

@home-assistant home-assistant bot marked this pull request as draft February 12, 2026 21:30
Copilot AI review requested due to automatic review settings February 24, 2026 05:39
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 15 out of 15 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

homeassistant/components/roborock/strings.json:445

  • The strings.json file adds duplicate sensor translations "error" (lines 368-390) and "state" (lines 431-445) that are not used by any sensors in the code. The code only uses "zeo_error" and "zeo_state" translation keys for the Zeo washing machine sensors. These unused translations should be removed to avoid confusion and maintain consistency.
      "error": {
        "name": "Error",
        "state": {
          "communication_error": "Communication error",
          "door_lock_error": "Door lock error",
          "drain_error": "Drain error",
          "drying_error": "Drying error",
          "drying_error_e_12": "Drying error E12",
          "drying_error_e_13": "Drying error E13",
          "drying_error_e_14": "Drying error E14",
          "drying_error_e_15": "Drying error E15",
          "drying_error_e_16": "Drying error E16",
          "drying_error_restart": "Restart the washer",
          "drying_error_water_flow": "Check water flow",
          "heating_error": "Heating error",
          "inverter_error": "Inverter error",
          "none": "[%key:component::roborock::entity::sensor::vacuum_error::state::none%]",
          "refill_error": "Refill error",
          "spin_error": "Re-arrange clothes",
          "temperature_error": "Temperature error",
          "water_level_error": "Water level error"
        }
      },
      "filter_time_left": {
        "name": "Filter time left"
      },
      "last_clean_end": {
        "name": "Last clean end"
      },
      "last_clean_start": {
        "name": "Last clean begin"
      },
      "main_brush_time_left": {
        "name": "Main brush time left"
      },
      "mop_drying_remaining_time": {
        "name": "Mop drying remaining time"
      },
      "mop_life_time_left": {
        "name": "Mop life time left"
      },
      "q7_status": {
        "name": "Status",
        "state": {
          "charging": "[%key:common::state::charging%]",
          "docking": "[%key:component::roborock::entity::sensor::status::state::docking%]",
          "mop_airdrying": "Mop air drying",
          "mop_cleaning": "Mop cleaning",
          "moping": "Mopping",
          "paused": "[%key:common::state::paused%]",
          "sleeping": "Sleeping",
          "sweep_moping": "Sweep mopping",
          "sweep_moping_2": "Sweep mopping",
          "updating": "[%key:component::roborock::entity::sensor::status::state::updating%]",
          "waiting_for_orders": "Waiting for orders"
        }
      },
      "sensor_time_left": {
        "name": "Sensor time left"
      },
      "side_brush_time_left": {
        "name": "Side brush time left"
      },
      "state": {
        "name": "State",
        "state": {
          "cooling": "Cooling",
          "done": "Done",
          "drying": "Drying",
          "rinsing": "Rinsing",
          "soaking": "Soaking",
          "spinning": "Spinning",
          "standby": "[%key:common::state::standby%]",
          "under_delay_start": "Delayed start",
          "washing": "Washing",
          "weighing": "Weighing"
        }
      },

homeassistant/components/roborock/button.py:235

  • The finally block causes the coordinator to refresh even when the button press fails and raises an exception. This is inconsistent with the pattern used in switch and select entities (see switch.py lines 196, 209) where refresh only happens on successful operations. Move the refresh call inside the try block after the set_value call to ensure it only executes on success.
        finally:
            await self.coordinator.async_request_refresh()

"zeo_state": {
"name": "State",
"state": {
"aftercare": "Aftercare",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are we sure removing states here is correct?

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.

added back.

@home-assistant home-assistant bot marked this pull request as draft February 24, 2026 15:03
@yangqian yangqian marked this pull request as ready for review February 24, 2026 16:02
Copilot AI review requested due to automatic review settings February 24, 2026 16:02
@home-assistant home-assistant bot requested a review from joostlek February 24, 2026 16:02
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 15 out of 15 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (6)

homeassistant/components/roborock/sensor.py:541

  • Direct dictionary access could raise a KeyError if the protocol is not present in coordinator.data at runtime. Although entities are only created when the protocol is in request_protocols, there's no guarantee the key will always be in the data dictionary during updates. Use coordinator.data.get() with a fallback to None to safely handle missing keys, consistent with Home Assistant best practices for entity state handling.
        value = self.coordinator.data[self.entity_description.data_protocol]
        if self.entity_description.value_fn is not None:
            return self.entity_description.value_fn(value)
        return value

homeassistant/components/roborock/binary_sensor.py:219

  • Direct dictionary access could raise a KeyError if the protocol is not present in coordinator.data at runtime. Although entities are only created when the protocol is in request_protocols, there's no guarantee the key will always be in the data dictionary during updates. Use coordinator.data.get() with a fallback to return False or None to safely handle missing keys, consistent with Home Assistant best practices for entity state handling.
        value = self.coordinator.data[self.entity_description.data_protocol]
        return self.entity_description.value_fn(value)

tests/components/roborock/test_select.py:297

  • The select platform is missing comprehensive snapshot testing like the other platforms (button, switch, sensor, binary_sensor all have snapshot tests). For consistency with the testing patterns in this integration, add a test_selects function using snapshot_platform similar to the pattern in test_button.py lines 33-41, test_switch.py lines 28-36, etc. This ensures all entity states and registry entries for the new A01 select entities are properly validated.
@pytest.fixture
def zeo_device(fake_devices: list[FakeDevice]) -> FakeDevice:
    """Get the fake Zeo washing machine device."""
    return next(device for device in fake_devices if getattr(device, "zeo", None))

homeassistant/components/roborock/button.py:235

  • Using finally block here causes async_request_refresh to execute even when the button press fails. This could be problematic because if the set_value operation fails, the coordinator data hasn't changed, but we're still refreshing it. This is inconsistent with the switch and select implementations which only refresh after successful operations. Move async_request_refresh outside the try-finally block, after the exception handling, to only refresh on success.
        finally:
            await self.coordinator.async_request_refresh()

homeassistant/components/roborock/switch.py:201

  • The async_request_refresh call is inside the try block, but according to Home Assistant coding guidelines, the try block should be minimal and only wrap operations that can throw exceptions. If set_value succeeds but async_request_refresh fails, this would incorrectly report that the switch operation failed, even though the device state was successfully changed. Move async_request_refresh outside the try-except block to ensure it always executes after a successful set_value call.
        try:
            await self.coordinator.api.set_value(  # type: ignore[attr-defined]
                self.entity_description.data_protocol, 0
            )
            await self.coordinator.async_request_refresh()
        except RoborockException as err:
            raise HomeAssistantError(
                translation_domain=DOMAIN,
                translation_key="update_options_failed",
            ) from err

homeassistant/components/roborock/switch.py:214

  • The async_request_refresh call is inside the try block, but according to Home Assistant coding guidelines, the try block should be minimal and only wrap operations that can throw exceptions. If set_value succeeds but async_request_refresh fails, this would incorrectly report that the switch operation failed, even though the device state was successfully changed. Move async_request_refresh outside the try-except block to ensure it always executes after a successful set_value call.
        try:
            await self.coordinator.api.set_value(  # type: ignore[attr-defined]
                self.entity_description.data_protocol, 1
            )
            await self.coordinator.async_request_refresh()
        except RoborockException as err:
            raise HomeAssistantError(
                translation_domain=DOMAIN,
                translation_key="update_options_failed",
            ) from err

Copilot AI review requested due to automatic review settings February 25, 2026 00:49
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 15 out of 15 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

homeassistant/components/roborock/sensor.py:538

  • The sensor entity directly accesses coordinator.data dictionary without checking if the key exists first. This could cause a KeyError if the protocol is not present in the data. The switch entity (line 219 in switch.py) uses .get() to safely access data, which returns None if the key doesn't exist. You should follow the same pattern here for consistency and to prevent potential errors.

Consider changing:

value = self.coordinator.data[self.entity_description.data_protocol]

To:

value = self.coordinator.data.get(self.entity_description.data_protocol)

And then handle the None case appropriately.

        value = self.coordinator.data[self.entity_description.data_protocol]

homeassistant/components/roborock/binary_sensor.py:218

  • The binary sensor entity directly accesses coordinator.data dictionary without checking if the key exists first. This could cause a KeyError if the protocol is not present in the data. The switch entity (line 219 in switch.py) uses .get() to safely access data. You should follow the same pattern here for consistency and to prevent potential errors.

Consider changing:

value = self.coordinator.data[self.entity_description.data_protocol]

To:

value = self.coordinator.data.get(self.entity_description.data_protocol)

And then handle the None case appropriately (e.g., return False or set the entity as unavailable).

        value = self.coordinator.data[self.entity_description.data_protocol]

homeassistant/components/roborock/button.py:235

  • The coordinator refresh is called in a finally block, which means it will execute even if the set_value operation fails. This is inconsistent with the switch entity pattern (lines 190-214 in switch.py) where the refresh is only called on success. If the set_value fails, there's no state change to refresh, so calling async_request_refresh would be unnecessary and potentially misleading.

Consider moving the refresh call outside the try/except block (like in select.py line 452) so it only executes on success:

try:
    await self.coordinator.api.set_value(
        self.entity_description.data_protocol,
        1,
    )
except RoborockException as err:
    raise HomeAssistantError(
        translation_domain=DOMAIN,
        translation_key="button_press_failed",
    ) from err

await self.coordinator.async_request_refresh()
        finally:
            await self.coordinator.async_request_refresh()

Copilot AI review requested due to automatic review settings February 25, 2026 01:36
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 15 out of 15 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (4)

homeassistant/components/roborock/button.py:235

  • The async_request_refresh() call in the finally block will execute even when an exception is raised, which may not be the intended behavior. According to Home Assistant guidelines, the try block should only wrap code that can throw exceptions, and data processing (like refresh) should happen after successful execution.

Consider moving the refresh call outside the try-except block and only calling it after successful execution, or ensuring that refresh is only called when the operation succeeds.

        finally:
            await self.coordinator.async_request_refresh()

homeassistant/components/roborock/switch.py:196

  • According to Home Assistant coding guidelines, the try block should only wrap code that can throw exceptions, and data processing should happen outside the try block. The async_request_refresh() call should be moved outside the try-except block to follow the recommended pattern.

The guideline states: "Only wrap code that can throw exceptions" and "Keep try blocks minimal - process data after the try/catch".

        try:
            await self.coordinator.api.set_value(  # type: ignore[attr-defined]
                self.entity_description.data_protocol, 0
            )
            await self.coordinator.async_request_refresh()

homeassistant/components/roborock/switch.py:209

  • According to Home Assistant coding guidelines, the try block should only wrap code that can throw exceptions, and data processing should happen outside the try block. The async_request_refresh() call should be moved outside the try-except block to follow the recommended pattern.

The guideline states: "Only wrap code that can throw exceptions" and "Keep try blocks minimal - process data after the try/catch".

        try:
            await self.coordinator.api.set_value(  # type: ignore[attr-defined]
                self.entity_description.data_protocol, 1
            )
            await self.coordinator.async_request_refresh()

homeassistant/components/roborock/binary_sensor.py:219

  • The is_on property directly accesses self.coordinator.data[self.entity_description.data_protocol] without checking if the key exists. This could raise a KeyError if the protocol is not present in the data dictionary. Consider using .get() with a default value or adding a None check, similar to how the switch's is_on property handles this case.
    @property
    def is_on(self) -> bool:
        """Return the value reported by the sensor."""
        value = self.coordinator.data[self.entity_description.data_protocol]
        return self.entity_description.value_fn(value)

@allenporter allenporter merged commit 2f80720 into home-assistant:dev Feb 25, 2026
32 of 33 checks passed
@joostlek joostlek added the noteworthy Marks a PR as noteworthy and should be in the release notes (in case it normally would not appear) label Feb 25, 2026
@github-actions github-actions bot locked and limited conversation to collaborators Feb 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

cla-signed integration: roborock new-feature noteworthy Marks a PR as noteworthy and should be in the release notes (in case it normally would not appear) Quality Scale: silver

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants