From 049582ec50b731e7f3d7049c325ef040a052a5c9 Mon Sep 17 00:00:00 2001 From: Luke Date: Tue, 30 May 2023 21:10:28 -0400 Subject: [PATCH] Add Switch platform to Roborock (#93833) * init * add switch platform * remove stale comments * remove stale list * set entity category to config --- homeassistant/components/roborock/const.py | 2 +- .../components/roborock/strings.json | 5 + homeassistant/components/roborock/switch.py | 110 ++++++++++++++++++ tests/components/roborock/conftest.py | 2 + tests/components/roborock/test_switch.py | 67 +++++++++++ 5 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/roborock/switch.py create mode 100644 tests/components/roborock/test_switch.py diff --git a/homeassistant/components/roborock/const.py b/homeassistant/components/roborock/const.py index 5fb0f888ead..287229c9fd1 100644 --- a/homeassistant/components/roborock/const.py +++ b/homeassistant/components/roborock/const.py @@ -6,4 +6,4 @@ CONF_ENTRY_CODE = "code" CONF_BASE_URL = "base_url" CONF_USER_DATA = "user_data" -PLATFORMS = [Platform.VACUUM, Platform.SELECT, Platform.SENSOR] +PLATFORMS = [Platform.VACUUM, Platform.SELECT, Platform.SENSOR, Platform.SWITCH] diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index f8b8dae38d2..17b9b12cab7 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -97,6 +97,11 @@ } } }, + "switch": { + "child_lock": { + "name": "Child lock" + } + }, "vacuum": { "roborock": { "state_attributes": { diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py new file mode 100644 index 00000000000..7b675c3be38 --- /dev/null +++ b/homeassistant/components/roborock/switch.py @@ -0,0 +1,110 @@ +"""Support for Roborock switch.""" +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +import logging +from typing import Any + +from roborock.roborock_typing import RoborockCommand + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify + +from .const import DOMAIN +from .coordinator import RoborockDataUpdateCoordinator +from .device import RoborockCoordinatedEntity + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class RoborockSwitchDescriptionMixin: + """Define an entity description mixin for switch entities.""" + + # Gets the status of the switch + get_value: Callable[[RoborockCoordinatedEntity], Coroutine[Any, Any, dict]] + # Evaluate the result of get_value to determine a bool + evaluate_value: Callable[[dict], bool] + # Sets the status of the switch + set_command: Callable[[RoborockCoordinatedEntity, bool], Coroutine[Any, Any, dict]] + + +@dataclass +class RoborockSwitchDescription( + SwitchEntityDescription, RoborockSwitchDescriptionMixin +): + """Class to describe an Roborock switch entity.""" + + +SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [ + RoborockSwitchDescription( + set_command=lambda entity, value: entity.send( + RoborockCommand.SET_CHILD_LOCK_STATUS, {"lock_status": 1 if value else 0} + ), + get_value=lambda data: data.send(RoborockCommand.GET_CHILD_LOCK_STATUS), + evaluate_value=lambda data: data["lock_status"] == 1, + key="child_lock", + translation_key="child_lock", + icon="mdi:account-lock", + entity_category=EntityCategory.CONFIG, + ) +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Roborock switch platform.""" + + coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ + config_entry.entry_id + ] + async_add_entities( + ( + RoborockSwitchEntity( + f"{description.key}_{slugify(device_id)}", + coordinator, + description, + ) + for device_id, coordinator in coordinators.items() + for description in SWITCH_DESCRIPTIONS + ), + True, + ) + + +class RoborockSwitchEntity(RoborockCoordinatedEntity, SwitchEntity): + """A class to let you turn functionality on Roborock devices on and off.""" + + entity_description: RoborockSwitchDescription + + def __init__( + self, + unique_id: str, + coordinator: RoborockDataUpdateCoordinator, + entity_description: RoborockSwitchDescription, + ) -> None: + """Create a switch entity.""" + self.entity_description = entity_description + super().__init__(unique_id, coordinator) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the switch.""" + await self.entity_description.set_command(self, False) + return self.async_schedule_update_ha_state(True) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the switch.""" + await self.entity_description.set_command(self, True) + return self.async_schedule_update_ha_state(True) + + async def async_update(self) -> None: + """Update switch.""" + self._attr_is_on = self.entity_description.evaluate_value( + await self.entity_description.get_value(self) + ) diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index f95436474e8..b311f84f94a 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -58,6 +58,8 @@ async def setup_entry( ), patch( "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", return_value=PROP, + ), patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" ): assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/roborock/test_switch.py b/tests/components/roborock/test_switch.py new file mode 100644 index 00000000000..153ddbd8293 --- /dev/null +++ b/tests/components/roborock/test_switch.py @@ -0,0 +1,67 @@ +"""Test Roborock Switch platform.""" +from unittest.mock import patch + +import pytest +from roborock.exceptions import RoborockException + +from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("entity_id"), + [ + ("switch.roborock_s7_maxv_child_lock"), + ], +) +async def test_update_success( + hass: HomeAssistant, + bypass_api_fixture, + setup_entry: MockConfigEntry, + entity_id: str, +) -> None: + """Test turning switch entities on and off.""" + with patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + ) as mock_send_message: + await hass.services.async_call( + "switch", + SERVICE_TURN_ON, + service_data=None, + blocking=True, + target={"entity_id": entity_id}, + ) + assert mock_send_message.assert_called_once + with patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + ) as mock_send_message: + await hass.services.async_call( + "switch", + SERVICE_TURN_OFF, + service_data=None, + blocking=True, + target={"entity_id": entity_id}, + ) + assert mock_send_message.assert_called_once + + +async def test_update_failure( + hass: HomeAssistant, + bypass_api_fixture, + setup_entry: MockConfigEntry, +) -> None: + """Test that changing a value will raise a homeassistanterror when it fails.""" + with patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message", + side_effect=RoborockException(), + ), pytest.raises(HomeAssistantError): + await hass.services.async_call( + "switch", + SERVICE_TURN_ON, + service_data=None, + blocking=True, + target={"entity_id": "switch.roborock_s7_maxv_child_lock"}, + )