Add Roborock entity with the name of the current room (#140895)

* Add current room entity

* Update homeassistant/components/roborock/models.py

Co-authored-by: Allen Porter <allen.porter@gmail.com>

* Update homeassistant/components/roborock/models.py

Co-authored-by: Allen Porter <allen.porter@gmail.com>

* use current_room property

* remove select changes

---------

Co-authored-by: Allen Porter <allen.porter@gmail.com>
This commit is contained in:
Luke Lashley 2025-03-18 21:48:34 -04:00 committed by GitHub
parent c41d5f2577
commit 254622878a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 77 additions and 13 deletions

View File

@ -29,6 +29,7 @@ from roborock.web_api import RoborockApiClient
from vacuum_map_parser_base.config.color import ColorsPalette
from vacuum_map_parser_base.config.image_config import ImageConfig
from vacuum_map_parser_base.config.size import Sizes
from vacuum_map_parser_base.map_data import MapData
from vacuum_map_parser_roborock.map_data_parser import RoborockMapDataParser
from homeassistant.config_entries import ConfigEntry
@ -168,18 +169,20 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
sw_version=self.roborock_device_info.device.fv,
)
def parse_image(self, map_bytes: bytes) -> bytes | None:
"""Parse map_bytes and store it as image bytes."""
def parse_map_data_v1(
self, map_bytes: bytes
) -> tuple[bytes | None, MapData | None]:
"""Parse map_bytes and return MapData and the image."""
try:
parsed_map = self.map_parser.parse(map_bytes)
except (IndexError, ValueError) as err:
_LOGGER.debug("Exception when parsing map contents: %s", err)
return None
return None, None
if parsed_map.image is None:
return None
return None, None
img_byte_arr = io.BytesIO()
parsed_map.image.data.save(img_byte_arr, format=MAP_FILE_FORMAT)
return img_byte_arr.getvalue()
return img_byte_arr.getvalue(), parsed_map
async def _async_setup(self) -> None:
"""Set up the coordinator."""
@ -206,6 +209,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
rooms={},
image=image,
last_updated=dt_util.utcnow() - IMAGE_CACHE_INTERVAL,
map_data=None,
)
for image, roborock_map in zip(stored_images, roborock_maps, strict=False)
}
@ -230,20 +234,21 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
translation_domain=DOMAIN,
translation_key="map_failure",
)
parsed_image = self.parse_image(response)
if parsed_image is None:
parsed_image, parsed_map = self.parse_map_data_v1(response)
if parsed_image is None or parsed_map is None:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="map_failure",
)
current_roborock_map_info = self.maps[self.current_map]
if parsed_image != self.maps[self.current_map].image:
await self.map_storage.async_save_map(
self.current_map,
parsed_image,
)
current_roborock_map_info = self.maps[self.current_map]
current_roborock_map_info.image = parsed_image
current_roborock_map_info.last_updated = dt_util.utcnow()
current_roborock_map_info.map_data = parsed_map
async def _verify_api(self) -> None:
"""Verify that the api is reachable. If it is not, switch clients."""

View File

@ -6,6 +6,7 @@ from typing import Any
from roborock.containers import HomeDataDevice, HomeDataProduct, NetworkInfo
from roborock.roborock_typing import DeviceProp
from vacuum_map_parser_base.map_data import MapData
@dataclass
@ -51,3 +52,11 @@ class RoborockMapInfo:
rooms: dict[int, str]
image: bytes | None
last_updated: datetime
map_data: MapData | None
@property
def current_room(self) -> str | None:
"""Get the currently active room for this map if any."""
if self.map_data is None or self.map_data.vacuum_room is None:
return None
return self.rooms.get(self.map_data.vacuum_room)

View File

@ -36,7 +36,11 @@ from .coordinator import (
RoborockDataUpdateCoordinator,
RoborockDataUpdateCoordinatorA01,
)
from .entity import RoborockCoordinatedEntityA01, RoborockCoordinatedEntityV1
from .entity import (
RoborockCoordinatedEntityA01,
RoborockCoordinatedEntityV1,
RoborockEntity,
)
PARALLEL_UPDATES = 0
@ -306,7 +310,7 @@ async def async_setup_entry(
) -> None:
"""Set up the Roborock vacuum sensors."""
coordinators = config_entry.runtime_data
async_add_entities(
entities: list[RoborockEntity] = [
RoborockSensorEntity(
coordinator,
description,
@ -314,8 +318,9 @@ async def async_setup_entry(
for coordinator in coordinators.v1
for description in SENSOR_DESCRIPTIONS
if description.value_fn(coordinator.roborock_device_info.props) is not None
)
async_add_entities(
]
entities.extend(RoborockCurrentRoom(coordinator) for coordinator in coordinators.v1)
entities.extend(
RoborockSensorEntityA01(
coordinator,
description,
@ -324,6 +329,7 @@ async def async_setup_entry(
for description in A01_SENSOR_DESCRIPTIONS
if description.data_protocol in coordinator.data
)
async_add_entities(entities)
class RoborockSensorEntity(RoborockCoordinatedEntityV1, SensorEntity):
@ -353,6 +359,42 @@ class RoborockSensorEntity(RoborockCoordinatedEntityV1, SensorEntity):
)
class RoborockCurrentRoom(RoborockCoordinatedEntityV1, SensorEntity):
"""Representation of a Current Room Sensor."""
_attr_device_class = SensorDeviceClass.ENUM
_attr_translation_key = "current_room"
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(
self,
coordinator: RoborockDataUpdateCoordinator,
) -> None:
"""Initialize the entity."""
super().__init__(
f"current_room_{coordinator.duid_slug}",
coordinator,
None,
is_dock_entity=False,
)
@property
def options(self) -> list[str]:
"""Return the currently valid rooms."""
if self.coordinator.current_map is not None:
return list(
self.coordinator.maps[self.coordinator.current_map].rooms.values()
)
return []
@property
def native_value(self) -> str | None:
"""Return the value reported by the sensor."""
if self.coordinator.current_map is not None:
return self.coordinator.maps[self.coordinator.current_map].current_room
return None
class RoborockSensorEntityA01(RoborockCoordinatedEntityA01, SensorEntity):
"""Representation of a A01 Roborock sensor."""

View File

@ -181,6 +181,9 @@
"countdown": {
"name": "Countdown"
},
"current_room": {
"name": "Current room"
},
"dock_error": {
"name": "Dock error",
"state": {

View File

@ -1151,6 +1151,7 @@ MAP_DATA = MapData(0, 0)
MAP_DATA.image = ImageData(
100, 10, 10, 10, 10, ImageConfig(), Image.new("RGB", (1, 1)), lambda p: p
)
MAP_DATA.vacuum_room = 17
SCENES = [

View File

@ -29,7 +29,7 @@ def platforms() -> list[Platform]:
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")) == 40
assert len(hass.states.async_all("sensor")) == 42
assert hass.states.get("sensor.roborock_s7_maxv_main_brush_time_left").state == str(
MAIN_BRUSH_REPLACE_TIME - 74382
)
@ -63,6 +63,10 @@ async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> Non
hass.states.get("sensor.roborock_s7_maxv_last_clean_end").state
== "2023-01-01T03:43:58+00:00"
)
assert (
hass.states.get("sensor.roborock_s7_maxv_current_room").state
== "Example room 2"
)
assert hass.states.get("sensor.dyad_pro_status").state == "drying"
assert hass.states.get("sensor.dyad_pro_battery").state == "100"
assert hass.states.get("sensor.dyad_pro_filter_time_left").state == "111"