Add ability to select current map for Roborock (#120882)
Co-authored-by: J. Nick Koston <nick@koston.org>pull/121313/head
parent
45ab9cae1a
commit
ffc39585ed
|
@ -15,7 +15,6 @@ from homeassistant.components.binary_sensor import (
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import slugify
|
|
||||||
|
|
||||||
from . import RoborockConfigEntry
|
from . import RoborockConfigEntry
|
||||||
from .coordinator import RoborockDataUpdateCoordinator
|
from .coordinator import RoborockDataUpdateCoordinator
|
||||||
|
@ -97,7 +96,7 @@ class RoborockBinarySensorEntity(RoborockCoordinatedEntityV1, BinarySensorEntity
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
f"{description.key}_{slugify(coordinator.duid)}",
|
f"{description.key}_{coordinator.duid_slug}",
|
||||||
coordinator,
|
coordinator,
|
||||||
)
|
)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
|
|
@ -10,7 +10,6 @@ from homeassistant.components.button import ButtonEntity, ButtonEntityDescriptio
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import slugify
|
|
||||||
|
|
||||||
from . import RoborockConfigEntry
|
from . import RoborockConfigEntry
|
||||||
from .coordinator import RoborockDataUpdateCoordinator
|
from .coordinator import RoborockDataUpdateCoordinator
|
||||||
|
@ -90,7 +89,7 @@ class RoborockButtonEntity(RoborockEntityV1, ButtonEntity):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create a button entity."""
|
"""Create a button entity."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
f"{entity_description.key}_{slugify(coordinator.duid)}",
|
f"{entity_description.key}_{coordinator.duid_slug}",
|
||||||
coordinator.device_info,
|
coordinator.device_info,
|
||||||
coordinator.api,
|
coordinator.api,
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from functools import cached_property
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from roborock import HomeDataRoom
|
from roborock import HomeDataRoom
|
||||||
|
@ -21,6 +22,7 @@ from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .models import RoborockA01HassDeviceInfo, RoborockHassDeviceInfo, RoborockMapInfo
|
from .models import RoborockA01HassDeviceInfo, RoborockHassDeviceInfo, RoborockMapInfo
|
||||||
|
@ -142,11 +144,16 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||||
self._home_data_rooms.get(room.iot_id, "Unknown")
|
self._home_data_rooms.get(room.iot_id, "Unknown")
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def duid(self) -> str:
|
def duid(self) -> str:
|
||||||
"""Get the unique id of the device as specified by Roborock."""
|
"""Get the unique id of the device as specified by Roborock."""
|
||||||
return self.roborock_device_info.device.duid
|
return self.roborock_device_info.device.duid
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def duid_slug(self) -> str:
|
||||||
|
"""Get the slug of the duid."""
|
||||||
|
return slugify(self.duid)
|
||||||
|
|
||||||
|
|
||||||
class RoborockDataUpdateCoordinatorA01(
|
class RoborockDataUpdateCoordinatorA01(
|
||||||
DataUpdateCoordinator[
|
DataUpdateCoordinator[
|
||||||
|
@ -191,7 +198,12 @@ class RoborockDataUpdateCoordinatorA01(
|
||||||
"""Disconnect from API."""
|
"""Disconnect from API."""
|
||||||
await self.api.async_release()
|
await self.api.async_release()
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def duid(self) -> str:
|
def duid(self) -> str:
|
||||||
"""Get the unique id of the device as specified by Roborock."""
|
"""Get the unique id of the device as specified by Roborock."""
|
||||||
return self.roborock_device_info.device.duid
|
return self.roborock_device_info.device.duid
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def duid_slug(self) -> str:
|
||||||
|
"""Get the slug of the duid."""
|
||||||
|
return slugify(self.duid)
|
||||||
|
|
|
@ -14,7 +14,6 @@ from homeassistant.components.number import NumberEntity, NumberEntityDescriptio
|
||||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import slugify
|
|
||||||
|
|
||||||
from . import RoborockConfigEntry
|
from . import RoborockConfigEntry
|
||||||
from .coordinator import RoborockDataUpdateCoordinator
|
from .coordinator import RoborockDataUpdateCoordinator
|
||||||
|
@ -77,7 +76,7 @@ async def async_setup_entry(
|
||||||
else:
|
else:
|
||||||
valid_entities.append(
|
valid_entities.append(
|
||||||
RoborockNumberEntity(
|
RoborockNumberEntity(
|
||||||
f"{description.key}_{slugify(coordinator.duid)}",
|
f"{description.key}_{coordinator.duid_slug}",
|
||||||
coordinator,
|
coordinator,
|
||||||
description,
|
description,
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,7 +11,6 @@ from homeassistant.components.select import SelectEntity, SelectEntityDescriptio
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import slugify
|
|
||||||
|
|
||||||
from . import RoborockConfigEntry
|
from . import RoborockConfigEntry
|
||||||
from .coordinator import RoborockDataUpdateCoordinator
|
from .coordinator import RoborockDataUpdateCoordinator
|
||||||
|
@ -79,6 +78,12 @@ async def async_setup_entry(
|
||||||
)
|
)
|
||||||
is not None
|
is not None
|
||||||
)
|
)
|
||||||
|
async_add_entities(
|
||||||
|
RoborockCurrentMapSelectEntity(
|
||||||
|
f"selected_map_{coordinator.duid_slug}", coordinator
|
||||||
|
)
|
||||||
|
for coordinator in config_entry.runtime_data.v1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RoborockSelectEntity(RoborockCoordinatedEntityV1, SelectEntity):
|
class RoborockSelectEntity(RoborockCoordinatedEntityV1, SelectEntity):
|
||||||
|
@ -95,7 +100,7 @@ class RoborockSelectEntity(RoborockCoordinatedEntityV1, SelectEntity):
|
||||||
"""Create a select entity."""
|
"""Create a select entity."""
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
super().__init__(
|
super().__init__(
|
||||||
f"{entity_description.key}_{slugify(coordinator.duid)}",
|
f"{entity_description.key}_{coordinator.duid_slug}",
|
||||||
coordinator,
|
coordinator,
|
||||||
entity_description.protocol_listener,
|
entity_description.protocol_listener,
|
||||||
)
|
)
|
||||||
|
@ -112,3 +117,32 @@ class RoborockSelectEntity(RoborockCoordinatedEntityV1, SelectEntity):
|
||||||
def current_option(self) -> str | None:
|
def current_option(self) -> str | None:
|
||||||
"""Get the current status of the select entity from device_status."""
|
"""Get the current status of the select entity from device_status."""
|
||||||
return self.entity_description.value_fn(self._device_status)
|
return self.entity_description.value_fn(self._device_status)
|
||||||
|
|
||||||
|
|
||||||
|
class RoborockCurrentMapSelectEntity(RoborockCoordinatedEntityV1, SelectEntity):
|
||||||
|
"""A class to let you set the selected map on Roborock vacuum."""
|
||||||
|
|
||||||
|
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
|
_attr_translation_key = "selected_map"
|
||||||
|
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
"""Set the option."""
|
||||||
|
for map_id, map_ in self.coordinator.maps.items():
|
||||||
|
if map_.name == option:
|
||||||
|
await self.send(
|
||||||
|
RoborockCommand.LOAD_MULTI_MAP,
|
||||||
|
[map_id],
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self) -> list[str]:
|
||||||
|
"""Gets all of the names of rooms that we are currently aware of."""
|
||||||
|
return [roborock_map.name for roborock_map in self.coordinator.maps.values()]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_option(self) -> str | None:
|
||||||
|
"""Get the current status of the select entity from device_status."""
|
||||||
|
if current_map := self.coordinator.current_map:
|
||||||
|
return self.coordinator.maps[current_map].name
|
||||||
|
return None
|
||||||
|
|
|
@ -30,7 +30,6 @@ from homeassistant.const import (
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
from homeassistant.util import slugify
|
|
||||||
|
|
||||||
from . import RoborockConfigEntry
|
from . import RoborockConfigEntry
|
||||||
from .coordinator import RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01
|
from .coordinator import RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01
|
||||||
|
@ -291,7 +290,7 @@ class RoborockSensorEntity(RoborockCoordinatedEntityV1, SensorEntity):
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
super().__init__(
|
super().__init__(
|
||||||
f"{description.key}_{slugify(coordinator.duid)}",
|
f"{description.key}_{coordinator.duid_slug}",
|
||||||
coordinator,
|
coordinator,
|
||||||
description.protocol_listener,
|
description.protocol_listener,
|
||||||
)
|
)
|
||||||
|
@ -316,7 +315,7 @@ class RoborockSensorEntityA01(RoborockCoordinatedEntityA01, SensorEntity):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
super().__init__(f"{description.key}_{slugify(coordinator.duid)}", coordinator)
|
super().__init__(f"{description.key}_{coordinator.duid_slug}", coordinator)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
|
|
|
@ -298,6 +298,9 @@
|
||||||
"custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]",
|
"custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]",
|
||||||
"custom_water_flow": "Custom water flow"
|
"custom_water_flow": "Custom water flow"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"selected_map": {
|
||||||
|
"name": "Selected map"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
|
|
|
@ -15,7 +15,6 @@ from homeassistant.components.switch import SwitchEntity, SwitchEntityDescriptio
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import slugify
|
|
||||||
|
|
||||||
from . import RoborockConfigEntry
|
from . import RoborockConfigEntry
|
||||||
from .coordinator import RoborockDataUpdateCoordinator
|
from .coordinator import RoborockDataUpdateCoordinator
|
||||||
|
@ -125,7 +124,7 @@ async def async_setup_entry(
|
||||||
else:
|
else:
|
||||||
valid_entities.append(
|
valid_entities.append(
|
||||||
RoborockSwitch(
|
RoborockSwitch(
|
||||||
f"{description.key}_{slugify(coordinator.duid)}",
|
f"{description.key}_{coordinator.duid_slug}",
|
||||||
coordinator,
|
coordinator,
|
||||||
description,
|
description,
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,7 +16,6 @@ from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import slugify
|
|
||||||
|
|
||||||
from . import RoborockConfigEntry
|
from . import RoborockConfigEntry
|
||||||
from .coordinator import RoborockDataUpdateCoordinator
|
from .coordinator import RoborockDataUpdateCoordinator
|
||||||
|
@ -141,7 +140,7 @@ async def async_setup_entry(
|
||||||
else:
|
else:
|
||||||
valid_entities.append(
|
valid_entities.append(
|
||||||
RoborockTimeEntity(
|
RoborockTimeEntity(
|
||||||
f"{description.key}_{slugify(coordinator.duid)}",
|
f"{description.key}_{coordinator.duid_slug}",
|
||||||
coordinator,
|
coordinator,
|
||||||
description,
|
description,
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,7 +20,6 @@ from homeassistant.components.vacuum import (
|
||||||
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
|
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
|
||||||
from homeassistant.helpers import entity_platform
|
from homeassistant.helpers import entity_platform
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import slugify
|
|
||||||
|
|
||||||
from . import RoborockConfigEntry
|
from . import RoborockConfigEntry
|
||||||
from .const import DOMAIN, GET_MAPS_SERVICE_NAME
|
from .const import DOMAIN, GET_MAPS_SERVICE_NAME
|
||||||
|
@ -103,7 +102,7 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
|
||||||
StateVacuumEntity.__init__(self)
|
StateVacuumEntity.__init__(self)
|
||||||
RoborockCoordinatedEntityV1.__init__(
|
RoborockCoordinatedEntityV1.__init__(
|
||||||
self,
|
self,
|
||||||
slugify(coordinator.duid),
|
coordinator.duid_slug,
|
||||||
coordinator,
|
coordinator,
|
||||||
listener_request=[
|
listener_request=[
|
||||||
RoborockDataProtocol.FAN_POWER,
|
RoborockDataProtocol.FAN_POWER,
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
"""Test Roborock Select platform."""
|
"""Test Roborock Select platform."""
|
||||||
|
|
||||||
|
import copy
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from roborock.exceptions import RoborockException
|
from roborock.exceptions import RoborockException
|
||||||
|
|
||||||
from homeassistant.const import SERVICE_SELECT_OPTION
|
from homeassistant.components.roborock import DOMAIN
|
||||||
|
from homeassistant.const import SERVICE_SELECT_OPTION, STATE_UNKNOWN
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from .mock_data import PROP
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
@ -17,6 +22,7 @@ from tests.common import MockConfigEntry
|
||||||
[
|
[
|
||||||
("select.roborock_s7_maxv_mop_mode", "deep"),
|
("select.roborock_s7_maxv_mop_mode", "deep"),
|
||||||
("select.roborock_s7_maxv_mop_intensity", "mild"),
|
("select.roborock_s7_maxv_mop_intensity", "mild"),
|
||||||
|
("select.roborock_s7_maxv_selected_map", "Downstairs"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_update_success(
|
async def test_update_success(
|
||||||
|
@ -62,3 +68,21 @@ async def test_update_failure(
|
||||||
blocking=True,
|
blocking=True,
|
||||||
target={"entity_id": "select.roborock_s7_maxv_mop_mode"},
|
target={"entity_id": "select.roborock_s7_maxv_mop_mode"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_none_map_select(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
bypass_api_fixture,
|
||||||
|
mock_roborock_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test that the select entity correctly handles not having a current map."""
|
||||||
|
prop = copy.deepcopy(PROP)
|
||||||
|
# Set map status to None so that current map is never set
|
||||||
|
prop.status.map_status = None
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop",
|
||||||
|
return_value=prop,
|
||||||
|
):
|
||||||
|
await async_setup_component(hass, DOMAIN, {})
|
||||||
|
select_entity = hass.states.get("select.roborock_s7_maxv_selected_map")
|
||||||
|
assert select_entity.state == STATE_UNKNOWN
|
||||||
|
|
Loading…
Reference in New Issue