mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 03:03:17 +01:00
Use component role in Shelly sensor platform (#152710)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -29,7 +29,6 @@ from aioshelly.const import (
|
||||
)
|
||||
|
||||
from homeassistant.components.number import NumberMode
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import UnitOfVolumeFlowRate
|
||||
|
||||
DOMAIN: Final = "shelly"
|
||||
@@ -298,14 +297,6 @@ API_WS_URL = "/api/shelly/ws"
|
||||
|
||||
COMPONENT_ID_PATTERN = re.compile(r"[a-z\d]+:\d+")
|
||||
|
||||
ROLE_TO_DEVICE_CLASS_MAP = {
|
||||
"current_humidity": SensorDeviceClass.HUMIDITY,
|
||||
"current_temperature": SensorDeviceClass.TEMPERATURE,
|
||||
"flow_rate": SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
"water_pressure": SensorDeviceClass.PRESSURE,
|
||||
"water_temperature": SensorDeviceClass.TEMPERATURE,
|
||||
}
|
||||
|
||||
# Mapping for units that require conversion to a Home Assistant recognized unit
|
||||
# e.g. "m3/min" to "m³/min"
|
||||
DEVICE_UNIT_MAP = {
|
||||
|
||||
@@ -188,7 +188,7 @@ def async_setup_rpc_attribute_entities(
|
||||
# Filter non-existing sensors
|
||||
if description.role and description.role != coordinator.device.config[
|
||||
key
|
||||
].get("role"):
|
||||
].get("role", "generic"):
|
||||
continue
|
||||
|
||||
if description.sub_key not in coordinator.device.status[
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Final, cast
|
||||
from functools import partial
|
||||
from typing import Any, Final, cast
|
||||
|
||||
from aioshelly.block_device import Block
|
||||
from aioshelly.const import RPC_GENERATIONS
|
||||
@@ -31,15 +31,18 @@ from homeassistant.const import (
|
||||
UnitOfEnergy,
|
||||
UnitOfFrequency,
|
||||
UnitOfPower,
|
||||
UnitOfPressure,
|
||||
UnitOfTemperature,
|
||||
UnitOfVolume,
|
||||
UnitOfVolumeFlowRate,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import CONF_SLEEP_PERIOD, ROLE_TO_DEVICE_CLASS_MAP
|
||||
from .const import CONF_SLEEP_PERIOD, LOGGER
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import (
|
||||
BlockEntityDescription,
|
||||
@@ -78,7 +81,6 @@ class BlockSensorDescription(BlockEntityDescription, SensorEntityDescription):
|
||||
class RpcSensorDescription(RpcEntityDescription, SensorEntityDescription):
|
||||
"""Class to describe a RPC sensor."""
|
||||
|
||||
device_class_fn: Callable[[dict], SensorDeviceClass | None] | None = None
|
||||
emeter_phase: str | None = None
|
||||
|
||||
|
||||
@@ -105,12 +107,6 @@ class RpcSensor(ShellyRpcAttributeEntity, SensorEntity):
|
||||
if self.option_map:
|
||||
self._attr_options = list(self.option_map.values())
|
||||
|
||||
if description.device_class_fn is not None:
|
||||
if device_class := description.device_class_fn(
|
||||
coordinator.device.config[key]
|
||||
):
|
||||
self._attr_device_class = device_class
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return value of sensor."""
|
||||
@@ -1383,25 +1379,24 @@ RPC_SENSORS: Final = {
|
||||
),
|
||||
unit=lambda config: config["xfreq"]["unit"] or None,
|
||||
),
|
||||
"text": RpcSensorDescription(
|
||||
"text_generic": RpcSensorDescription(
|
||||
key="text",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, SENSOR_PLATFORM
|
||||
),
|
||||
role="generic",
|
||||
),
|
||||
"number": RpcSensorDescription(
|
||||
"number_generic": RpcSensorDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, SENSOR_PLATFORM
|
||||
),
|
||||
unit=get_virtual_component_unit,
|
||||
device_class_fn=lambda config: ROLE_TO_DEVICE_CLASS_MAP.get(config["role"])
|
||||
if "role" in config
|
||||
else None,
|
||||
role="generic",
|
||||
),
|
||||
"enum": RpcSensorDescription(
|
||||
"enum_generic": RpcSensorDescription(
|
||||
key="enum",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
@@ -1409,6 +1404,7 @@ RPC_SENSORS: Final = {
|
||||
),
|
||||
options_fn=lambda config: config["options"],
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
role="generic",
|
||||
),
|
||||
"valve_position": RpcSensorDescription(
|
||||
key="blutrv",
|
||||
@@ -1450,6 +1446,49 @@ RPC_SENSORS: Final = {
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["dark", "twilight", "bright"],
|
||||
),
|
||||
"number_current_humidity": RpcSensorDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
suggested_display_precision=1,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
role="current_humidity",
|
||||
),
|
||||
"number_current_temperature": RpcSensorDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=1,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
role="current_temperature",
|
||||
),
|
||||
"number_flow_rate": RpcSensorDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_MINUTE,
|
||||
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
role="flow_rate",
|
||||
),
|
||||
"number_water_pressure": RpcSensorDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
native_unit_of_measurement=UnitOfPressure.KPA,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
role="water_pressure",
|
||||
),
|
||||
"number_water_temperature": RpcSensorDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=1,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
role="water_temperature",
|
||||
),
|
||||
"presence_num_objects": RpcSensorDescription(
|
||||
key="presence",
|
||||
sub_key="num_objects",
|
||||
@@ -1547,6 +1586,39 @@ RPC_SENSORS: Final = {
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def async_migrate_unique_ids(
|
||||
coordinator: ShellyRpcCoordinator,
|
||||
entity_entry: er.RegistryEntry,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Migrate sensor unique IDs to include role."""
|
||||
if not entity_entry.entity_id.startswith("sensor."):
|
||||
return None
|
||||
|
||||
for sensor_id in ("text", "number", "enum"):
|
||||
old_unique_id = entity_entry.unique_id
|
||||
if old_unique_id.endswith(f"-{sensor_id}"):
|
||||
if entity_entry.original_device_class == SensorDeviceClass.HUMIDITY:
|
||||
new_unique_id = f"{old_unique_id}_current_humidity"
|
||||
elif entity_entry.original_device_class == SensorDeviceClass.TEMPERATURE:
|
||||
new_unique_id = f"{old_unique_id}_current_temperature"
|
||||
else:
|
||||
new_unique_id = f"{old_unique_id}_generic"
|
||||
LOGGER.debug(
|
||||
"Migrating unique_id for %s entity from [%s] to [%s]",
|
||||
entity_entry.entity_id,
|
||||
old_unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
return {
|
||||
"new_unique_id": entity_entry.unique_id.replace(
|
||||
old_unique_id, new_unique_id
|
||||
)
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ShellyConfigEntry,
|
||||
@@ -1566,6 +1638,12 @@ async def async_setup_entry(
|
||||
coordinator = config_entry.runtime_data.rpc
|
||||
assert coordinator
|
||||
|
||||
await er.async_migrate_entries(
|
||||
hass,
|
||||
config_entry.entry_id,
|
||||
partial(async_migrate_unique_ids, coordinator),
|
||||
)
|
||||
|
||||
async_setup_entry_rpc(
|
||||
hass, config_entry, async_add_entities, RPC_SENSORS, RpcSensor
|
||||
)
|
||||
|
||||
@@ -1070,7 +1070,7 @@ async def test_rpc_device_virtual_text_sensor(
|
||||
assert state.state == "lorem ipsum"
|
||||
|
||||
assert (entry := entity_registry.async_get(entity_id))
|
||||
assert entry.unique_id == "123456789ABC-text:203-text"
|
||||
assert entry.unique_id == "123456789ABC-text:203-text_generic"
|
||||
|
||||
monkeypatch.setitem(mock_rpc_device.status["text:203"], "value", "dolor sit amet")
|
||||
mock_rpc_device.mock_update()
|
||||
@@ -1078,6 +1078,52 @@ async def test_rpc_device_virtual_text_sensor(
|
||||
assert state.state == "dolor sit amet"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("old_id", "new_id", "device_class"),
|
||||
[
|
||||
("enum", "enum_generic", SensorDeviceClass.ENUM),
|
||||
("number", "number_generic", None),
|
||||
("number", "number_current_humidity", SensorDeviceClass.HUMIDITY),
|
||||
("number", "number_current_temperature", SensorDeviceClass.TEMPERATURE),
|
||||
("text", "text_generic", None),
|
||||
],
|
||||
)
|
||||
async def test_migrate_unique_id_virtual_components_roles(
|
||||
hass: HomeAssistant,
|
||||
mock_rpc_device: Mock,
|
||||
entity_registry: EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
old_id: str,
|
||||
new_id: str,
|
||||
device_class: SensorDeviceClass | None,
|
||||
) -> None:
|
||||
"""Test migration of unique_id for virtual components to include role."""
|
||||
entry = await init_integration(hass, 3, skip_setup=True)
|
||||
unique_base = f"{MOCK_MAC}-{old_id}:200"
|
||||
old_unique_id = f"{unique_base}-{old_id}"
|
||||
new_unique_id = f"{unique_base}-{new_id}"
|
||||
|
||||
entity = entity_registry.async_get_or_create(
|
||||
suggested_object_id="test_name_test_sensor",
|
||||
disabled_by=None,
|
||||
domain=SENSOR_DOMAIN,
|
||||
platform=DOMAIN,
|
||||
unique_id=old_unique_id,
|
||||
config_entry=entry,
|
||||
original_device_class=device_class,
|
||||
)
|
||||
assert entity.unique_id == old_unique_id
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_entry = entity_registry.async_get("sensor.test_name_test_sensor")
|
||||
assert entity_entry
|
||||
assert entity_entry.unique_id == new_unique_id
|
||||
|
||||
assert "Migrating unique_id for sensor.test_name_test_sensor" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("disable_async_remove_shelly_rpc_entities")
|
||||
async def test_rpc_remove_text_virtual_sensor_when_mode_field(
|
||||
hass: HomeAssistant,
|
||||
@@ -1101,7 +1147,7 @@ async def test_rpc_remove_text_virtual_sensor_when_mode_field(
|
||||
hass,
|
||||
SENSOR_DOMAIN,
|
||||
"test_name_text_200",
|
||||
"text:200-text",
|
||||
"text:200-text_generic",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
@@ -1125,7 +1171,7 @@ async def test_rpc_remove_text_virtual_sensor_when_orphaned(
|
||||
hass,
|
||||
SENSOR_DOMAIN,
|
||||
"test_name_text_200",
|
||||
"text:200-text",
|
||||
"text:200-text_generic",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
@@ -1175,7 +1221,7 @@ async def test_rpc_device_virtual_number_sensor(
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == expected_unit
|
||||
|
||||
assert (entry := entity_registry.async_get(entity_id))
|
||||
assert entry.unique_id == "123456789ABC-number:203-number"
|
||||
assert entry.unique_id == "123456789ABC-number:203-number_generic"
|
||||
|
||||
monkeypatch.setitem(mock_rpc_device.status["number:203"], "value", 56.7)
|
||||
mock_rpc_device.mock_update()
|
||||
@@ -1211,7 +1257,7 @@ async def test_rpc_remove_number_virtual_sensor_when_mode_field(
|
||||
hass,
|
||||
SENSOR_DOMAIN,
|
||||
"test_name_number_200",
|
||||
"number:200-number",
|
||||
"number:200-number_generic",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
@@ -1235,7 +1281,7 @@ async def test_rpc_remove_number_virtual_sensor_when_orphaned(
|
||||
hass,
|
||||
SENSOR_DOMAIN,
|
||||
"test_name_number_200",
|
||||
"number:200-number",
|
||||
"number:200-number_generic",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
@@ -1289,7 +1335,7 @@ async def test_rpc_device_virtual_enum_sensor(
|
||||
assert state.attributes.get(ATTR_OPTIONS) == ["Title 1", "two", "three"]
|
||||
|
||||
assert (entry := entity_registry.async_get(entity_id))
|
||||
assert entry.unique_id == "123456789ABC-enum:203-enum"
|
||||
assert entry.unique_id == "123456789ABC-enum:203-enum_generic"
|
||||
|
||||
monkeypatch.setitem(mock_rpc_device.status["enum:203"], "value", "two")
|
||||
mock_rpc_device.mock_update()
|
||||
@@ -1329,7 +1375,7 @@ async def test_rpc_remove_enum_virtual_sensor_when_mode_dropdown(
|
||||
hass,
|
||||
SENSOR_DOMAIN,
|
||||
"test_name_enum_200",
|
||||
"enum:200-enum",
|
||||
"enum:200-enum_generic",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
@@ -1353,7 +1399,7 @@ async def test_rpc_remove_enum_virtual_sensor_when_orphaned(
|
||||
hass,
|
||||
SENSOR_DOMAIN,
|
||||
"test_name_enum_200",
|
||||
"enum:200-enum",
|
||||
"enum:200-enum_generic",
|
||||
config_entry,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
@@ -1516,8 +1562,10 @@ async def test_rpc_device_virtual_number_sensor_with_device_class(
|
||||
hass: HomeAssistant,
|
||||
mock_rpc_device: Mock,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
entity_registry: EntityRegistry,
|
||||
) -> None:
|
||||
"""Test a virtual number sensor with device class for RPC device."""
|
||||
entity_id = f"{SENSOR_DOMAIN}.test_name_current_humidity"
|
||||
config = deepcopy(mock_rpc_device.config)
|
||||
config["number:203"] = {
|
||||
"name": "Current humidity",
|
||||
@@ -1534,7 +1582,10 @@ async def test_rpc_device_virtual_number_sensor_with_device_class(
|
||||
|
||||
await init_integration(hass, 3)
|
||||
|
||||
assert (state := hass.states.get("sensor.test_name_current_humidity"))
|
||||
assert (entry := entity_registry.async_get(entity_id))
|
||||
assert entry.unique_id == "123456789ABC-number:203-number_current_humidity"
|
||||
|
||||
assert (state := hass.states.get(entity_id))
|
||||
assert state.state == "34"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY
|
||||
|
||||
Reference in New Issue
Block a user