diff --git a/homeassistant/components/roborock/const.py b/homeassistant/components/roborock/const.py index 61a9a70dd20..5fb0f888ead 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] +PLATFORMS = [Platform.VACUUM, Platform.SELECT, Platform.SENSOR] diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py new file mode 100644 index 00000000000..5841641d6cb --- /dev/null +++ b/homeassistant/components/roborock/sensor.py @@ -0,0 +1,122 @@ +"""Support for Roborock sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from roborock.typing import DeviceProp + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory, UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.util import slugify + +from .const import DOMAIN +from .coordinator import RoborockDataUpdateCoordinator +from .device import RoborockCoordinatedEntity +from .models import RoborockHassDeviceInfo + + +@dataclass +class RoborockSensorDescriptionMixin: + """A class that describes sensor entities.""" + + value_fn: Callable[[DeviceProp], int] + + +@dataclass +class RoborockSensorDescription( + SensorEntityDescription, RoborockSensorDescriptionMixin +): + """A class that describes Roborock sensors.""" + + +CONSUMABLE_SENSORS = [ + RoborockSensorDescription( + native_unit_of_measurement=UnitOfTime.SECONDS, + key="main_brush_time_left", + icon="mdi:brush", + device_class=SensorDeviceClass.DURATION, + translation_key="main_brush_time_left", + value_fn=lambda data: data.consumable.main_brush_time_left, + entity_category=EntityCategory.DIAGNOSTIC, + ), + RoborockSensorDescription( + native_unit_of_measurement=UnitOfTime.SECONDS, + key="side_brush_time_left", + icon="mdi:brush", + device_class=SensorDeviceClass.DURATION, + translation_key="side_brush_time_left", + value_fn=lambda data: data.consumable.side_brush_time_left, + entity_category=EntityCategory.DIAGNOSTIC, + ), + RoborockSensorDescription( + native_unit_of_measurement=UnitOfTime.SECONDS, + key="filter_time_left", + icon="mdi:air-filter", + device_class=SensorDeviceClass.DURATION, + translation_key="filter_time_left", + value_fn=lambda data: data.consumable.filter_time_left, + entity_category=EntityCategory.DIAGNOSTIC, + ), + RoborockSensorDescription( + native_unit_of_measurement=UnitOfTime.SECONDS, + key="sensor_time_left", + icon="mdi:eye-outline", + device_class=SensorDeviceClass.DURATION, + translation_key="sensor_time_left", + value_fn=lambda data: data.consumable.sensor_time_left, + entity_category=EntityCategory.DIAGNOSTIC, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Roborock vacuum sensors.""" + coordinator: RoborockDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + async_add_entities( + RoborockSensorEntity( + f"{description.key}_{slugify(device_id)}", + device_info, + coordinator, + description, + ) + for device_id, device_info in coordinator.devices_info.items() + for description in CONSUMABLE_SENSORS + ) + + +class RoborockSensorEntity(RoborockCoordinatedEntity, SensorEntity): + """Representation of a Roborock sensor.""" + + entity_description: RoborockSensorDescription + + def __init__( + self, + unique_id: str, + device_info: RoborockHassDeviceInfo, + coordinator: RoborockDataUpdateCoordinator, + description: RoborockSensorDescription, + ) -> None: + """Initialize the entity.""" + super().__init__(unique_id, device_info, coordinator) + self.entity_description = description + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + return self.entity_description.value_fn(self.coordinator.data[self._device_id]) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 64c9d268e56..7e8e2ba37a0 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -27,6 +27,20 @@ } }, "entity": { + "sensor": { + "main_brush_time_left": { + "name": "Main brush time left" + }, + "side_brush_time_left": { + "name": "Side brush time left" + }, + "filter_time_left": { + "name": "Filter time left" + }, + "sensor_time_left": { + "name": "Sensor time left" + } + }, "select": { "mop_mode": { "name": "Mop mode", diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index d767505feeb..f95436474e8 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -53,7 +53,12 @@ async def setup_entry( with patch( "homeassistant.components.roborock.RoborockApiClient.get_home_data", return_value=HOME_DATA, - ), patch("homeassistant.components.roborock.RoborockMqttClient.get_networking"): + ), patch( + "homeassistant.components.roborock.RoborockMqttClient.get_networking" + ), patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + return_value=PROP, + ): assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() return mock_roborock_entry diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py new file mode 100644 index 00000000000..84443506208 --- /dev/null +++ b/tests/components/roborock/test_sensor.py @@ -0,0 +1,29 @@ +"""Test Roborock Sensors.""" + +from roborock.const import ( + FILTER_REPLACE_TIME, + MAIN_BRUSH_REPLACE_TIME, + SENSOR_DIRTY_REPLACE_TIME, + SIDE_BRUSH_REPLACE_TIME, +) + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> None: + """Test sensors and check test values are correctly set.""" + assert len(hass.states.async_all("sensor")) == 4 + assert hass.states.get("sensor.roborock_s7_maxv_main_brush_time_left").state == str( + MAIN_BRUSH_REPLACE_TIME - 74382 + ) + assert hass.states.get("sensor.roborock_s7_maxv_side_brush_time_left").state == str( + SIDE_BRUSH_REPLACE_TIME - 74382 + ) + assert hass.states.get("sensor.roborock_s7_maxv_filter_time_left").state == str( + FILTER_REPLACE_TIME - 74382 + ) + assert hass.states.get("sensor.roborock_s7_maxv_sensor_time_left").state == str( + SENSOR_DIRTY_REPLACE_TIME - 74382 + )