Normalize deCONZ sensor unique IDs (#76357)
* Normalize deCONZ sensor unique IDs * Handle battery sensors properly * Fix daylight sensor unique IDpull/77586/head
parent
3df2ec1ed6
commit
61ff52c93a
|
@ -9,7 +9,7 @@ from pydeconz.interfaces.sensors import SensorResources
|
|||
from pydeconz.models.event import EventType
|
||||
from pydeconz.models.sensor.air_quality import AirQuality
|
||||
from pydeconz.models.sensor.consumption import Consumption
|
||||
from pydeconz.models.sensor.daylight import Daylight
|
||||
from pydeconz.models.sensor.daylight import DAYLIGHT_STATUS, Daylight
|
||||
from pydeconz.models.sensor.generic_status import GenericStatus
|
||||
from pydeconz.models.sensor.humidity import Humidity
|
||||
from pydeconz.models.sensor.light_level import LightLevel
|
||||
|
@ -41,21 +41,23 @@ from homeassistant.const import (
|
|||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
from homeassistant.helpers.typing import StateType
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import ATTR_DARK, ATTR_ON
|
||||
from .const import ATTR_DARK, ATTR_ON, DOMAIN as DECONZ_DOMAIN
|
||||
from .deconz_device import DeconzDevice
|
||||
from .gateway import DeconzGateway, get_gateway_from_config_entry
|
||||
|
||||
PROVIDES_EXTRA_ATTRIBUTES = (
|
||||
"battery",
|
||||
"consumption",
|
||||
"status",
|
||||
"daylight_status",
|
||||
"humidity",
|
||||
"light_level",
|
||||
"power",
|
||||
"pressure",
|
||||
"status",
|
||||
"temperature",
|
||||
)
|
||||
|
||||
|
@ -119,8 +121,8 @@ ENTITY_DESCRIPTIONS = {
|
|||
],
|
||||
Daylight: [
|
||||
DeconzSensorDescription(
|
||||
key="status",
|
||||
value_fn=lambda device: device.status
|
||||
key="daylight_status",
|
||||
value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status]
|
||||
if isinstance(device, Daylight)
|
||||
else None,
|
||||
update_key="status",
|
||||
|
@ -232,6 +234,27 @@ COMMON_SENSOR_DESCRIPTIONS = [
|
|||
]
|
||||
|
||||
|
||||
@callback
|
||||
def async_update_unique_id(
|
||||
hass: HomeAssistant, unique_id: str, description: DeconzSensorDescription
|
||||
) -> None:
|
||||
"""Update unique ID to always have a suffix.
|
||||
|
||||
Introduced with release 2022.9.
|
||||
"""
|
||||
ent_reg = er.async_get(hass)
|
||||
|
||||
new_unique_id = f"{unique_id}-{description.key}"
|
||||
if ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, new_unique_id):
|
||||
return
|
||||
|
||||
if description.suffix:
|
||||
unique_id = f'{unique_id.split("-", 1)[0]}-{description.suffix.lower()}'
|
||||
|
||||
if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id):
|
||||
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
|
@ -241,29 +264,46 @@ async def async_setup_entry(
|
|||
gateway = get_gateway_from_config_entry(hass, config_entry)
|
||||
gateway.entities[DOMAIN] = set()
|
||||
|
||||
known_device_entities: dict[str, set[str]] = {
|
||||
description.key: set() for description in COMMON_SENSOR_DESCRIPTIONS
|
||||
}
|
||||
|
||||
@callback
|
||||
def async_add_sensor(_: EventType, sensor_id: str) -> None:
|
||||
"""Add sensor from deCONZ."""
|
||||
sensor = gateway.api.sensors[sensor_id]
|
||||
entities: list[DeconzSensor] = []
|
||||
|
||||
if sensor.battery is None and not sensor.type.startswith("CLIP"):
|
||||
DeconzBatteryTracker(sensor_id, gateway, async_add_entities)
|
||||
|
||||
known_entities = set(gateway.entities[DOMAIN])
|
||||
|
||||
for description in (
|
||||
ENTITY_DESCRIPTIONS.get(type(sensor), []) + COMMON_SENSOR_DESCRIPTIONS
|
||||
):
|
||||
no_sensor_data = False
|
||||
if (
|
||||
not hasattr(sensor, description.key)
|
||||
or description.value_fn(sensor) is None
|
||||
):
|
||||
no_sensor_data = True
|
||||
|
||||
if description in COMMON_SENSOR_DESCRIPTIONS:
|
||||
if (
|
||||
sensor.type.startswith("CLIP")
|
||||
or (no_sensor_data and description.key != "battery")
|
||||
or (
|
||||
(unique_id := sensor.unique_id.rsplit("-", 1)[0])
|
||||
in known_device_entities[description.key]
|
||||
)
|
||||
):
|
||||
continue
|
||||
known_device_entities[description.key].add(unique_id)
|
||||
if no_sensor_data and description.key == "battery":
|
||||
DeconzBatteryTracker(sensor_id, gateway, async_add_entities)
|
||||
continue
|
||||
|
||||
if no_sensor_data:
|
||||
continue
|
||||
|
||||
entity = DeconzSensor(sensor, gateway, description)
|
||||
if entity.unique_id not in known_entities:
|
||||
entities.append(entity)
|
||||
async_update_unique_id(hass, sensor.unique_id, description)
|
||||
entities.append(DeconzSensor(sensor, gateway, description))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
@ -301,21 +341,7 @@ class DeconzSensor(DeconzDevice[SensorResources], SensorEntity):
|
|||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique identifier for this device."""
|
||||
if (
|
||||
self.entity_description.key == "battery"
|
||||
and self._device.manufacturer == "Danfoss"
|
||||
and self._device.model_id
|
||||
in [
|
||||
"0x8030",
|
||||
"0x8031",
|
||||
"0x8034",
|
||||
"0x8035",
|
||||
]
|
||||
):
|
||||
return f"{super().unique_id}-battery"
|
||||
if self.entity_description.suffix:
|
||||
return f"{self.serial}-{self.entity_description.suffix.lower()}"
|
||||
return super().unique_id
|
||||
return f"{self._device.unique_id}-{self.entity_description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType | datetime:
|
||||
|
@ -386,9 +412,6 @@ class DeconzBatteryTracker:
|
|||
"""Update the device's state."""
|
||||
if "battery" in self.sensor.changed_keys:
|
||||
self.unsubscribe()
|
||||
known_entities = set(self.gateway.entities[DOMAIN])
|
||||
entity = DeconzSensor(
|
||||
self.sensor, self.gateway, COMMON_SENSOR_DESCRIPTIONS[0]
|
||||
)
|
||||
if entity.unique_id not in known_entities:
|
||||
self.async_add_entities([entity])
|
||||
desc = COMMON_SENSOR_DESCRIPTIONS[0]
|
||||
async_update_unique_id(self.gateway.hass, self.sensor.unique_id, desc)
|
||||
self.async_add_entities([DeconzSensor(self.sensor, self.gateway, desc)])
|
||||
|
|
|
@ -5,7 +5,10 @@ from unittest.mock import patch
|
|||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR
|
||||
from homeassistant.components.deconz.const import (
|
||||
CONF_ALLOW_CLIP_SENSOR,
|
||||
DOMAIN as DECONZ_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
SensorDeviceClass,
|
||||
|
@ -54,7 +57,8 @@ TEST_DATA = [
|
|||
"entity_count": 2,
|
||||
"device_count": 3,
|
||||
"entity_id": "sensor.bosch_air_quality_sensor",
|
||||
"unique_id": "00:12:4b:00:14:4d:00:07-02-fdef",
|
||||
"unique_id": "00:12:4b:00:14:4d:00:07-02-fdef-air_quality",
|
||||
"old_unique_id": "00:12:4b:00:14:4d:00:07-02-fdef",
|
||||
"state": "poor",
|
||||
"entity_category": None,
|
||||
"device_class": None,
|
||||
|
@ -92,7 +96,8 @@ TEST_DATA = [
|
|||
"entity_count": 2,
|
||||
"device_count": 3,
|
||||
"entity_id": "sensor.bosch_air_quality_sensor_ppb",
|
||||
"unique_id": "00:12:4b:00:14:4d:00:07-ppb",
|
||||
"unique_id": "00:12:4b:00:14:4d:00:07-02-fdef-air_quality_ppb",
|
||||
"old_unique_id": "00:12:4b:00:14:4d:00:07-ppb",
|
||||
"state": "809",
|
||||
"entity_category": None,
|
||||
"device_class": SensorDeviceClass.AQI,
|
||||
|
@ -131,7 +136,8 @@ TEST_DATA = [
|
|||
"entity_count": 1,
|
||||
"device_count": 3,
|
||||
"entity_id": "sensor.fyrtur_block_out_roller_blind_battery",
|
||||
"unique_id": "00:0d:6f:ff:fe:01:23:45-battery",
|
||||
"unique_id": "00:0d:6f:ff:fe:01:23:45-01-0001-battery",
|
||||
"old_unique_id": "00:0d:6f:ff:fe:01:23:45-battery",
|
||||
"state": "100",
|
||||
"entity_category": EntityCategory.DIAGNOSTIC,
|
||||
"device_class": SensorDeviceClass.BATTERY,
|
||||
|
@ -167,7 +173,8 @@ TEST_DATA = [
|
|||
"entity_count": 1,
|
||||
"device_count": 3,
|
||||
"entity_id": "sensor.consumption_15",
|
||||
"unique_id": "00:0d:6f:00:0b:7a:64:29-01-0702",
|
||||
"unique_id": "00:0d:6f:00:0b:7a:64:29-01-0702-consumption",
|
||||
"old_unique_id": "00:0d:6f:00:0b:7a:64:29-01-0702",
|
||||
"state": "11.342",
|
||||
"entity_category": None,
|
||||
"device_class": SensorDeviceClass.ENERGY,
|
||||
|
@ -203,13 +210,15 @@ TEST_DATA = [
|
|||
},
|
||||
"swversion": "1.0",
|
||||
"type": "Daylight",
|
||||
"uniqueid": "01:23:4E:FF:FF:56:78:9A-01",
|
||||
},
|
||||
{
|
||||
"enable_entity": True,
|
||||
"entity_count": 1,
|
||||
"device_count": 2,
|
||||
"device_count": 3,
|
||||
"entity_id": "sensor.daylight",
|
||||
"unique_id": "",
|
||||
"unique_id": "01:23:4E:FF:FF:56:78:9A-01-daylight_status",
|
||||
"old-unique_id": "01:23:4E:FF:FF:56:78:9A-01",
|
||||
"state": "solar_noon",
|
||||
"entity_category": None,
|
||||
"device_class": None,
|
||||
|
@ -246,7 +255,8 @@ TEST_DATA = [
|
|||
"entity_count": 1,
|
||||
"device_count": 2,
|
||||
"entity_id": "sensor.fsm_state_motion_stair",
|
||||
"unique_id": "fsm-state-1520195376277",
|
||||
"unique_id": "fsm-state-1520195376277-status",
|
||||
"old_unique_id": "fsm-state-1520195376277",
|
||||
"state": "0",
|
||||
"entity_category": None,
|
||||
"device_class": None,
|
||||
|
@ -284,7 +294,8 @@ TEST_DATA = [
|
|||
"entity_count": 2,
|
||||
"device_count": 3,
|
||||
"entity_id": "sensor.mi_temperature_1",
|
||||
"unique_id": "00:15:8d:00:02:45:dc:53-01-0405",
|
||||
"unique_id": "00:15:8d:00:02:45:dc:53-01-0405-humidity",
|
||||
"old_unique_id": "00:15:8d:00:02:45:dc:53-01-0405",
|
||||
"state": "35.5",
|
||||
"entity_category": None,
|
||||
"device_class": SensorDeviceClass.HUMIDITY,
|
||||
|
@ -333,7 +344,8 @@ TEST_DATA = [
|
|||
"entity_count": 2,
|
||||
"device_count": 3,
|
||||
"entity_id": "sensor.motion_sensor_4",
|
||||
"unique_id": "00:17:88:01:03:28:8c:9b-02-0400",
|
||||
"unique_id": "00:17:88:01:03:28:8c:9b-02-0400-light_level",
|
||||
"old_unique_id": "00:17:88:01:03:28:8c:9b-02-0400",
|
||||
"state": "5.0",
|
||||
"entity_category": None,
|
||||
"device_class": SensorDeviceClass.ILLUMINANCE,
|
||||
|
@ -375,7 +387,8 @@ TEST_DATA = [
|
|||
"entity_count": 1,
|
||||
"device_count": 3,
|
||||
"entity_id": "sensor.power_16",
|
||||
"unique_id": "00:0d:6f:00:0b:7a:64:29-01-0b04",
|
||||
"unique_id": "00:0d:6f:00:0b:7a:64:29-01-0b04-power",
|
||||
"old_unique_id": "00:0d:6f:00:0b:7a:64:29-01-0b04",
|
||||
"state": "64",
|
||||
"entity_category": None,
|
||||
"device_class": SensorDeviceClass.POWER,
|
||||
|
@ -417,7 +430,8 @@ TEST_DATA = [
|
|||
"entity_count": 2,
|
||||
"device_count": 3,
|
||||
"entity_id": "sensor.mi_temperature_1",
|
||||
"unique_id": "00:15:8d:00:02:45:dc:53-01-0403",
|
||||
"unique_id": "00:15:8d:00:02:45:dc:53-01-0403-pressure",
|
||||
"old_unique_id": "00:15:8d:00:02:45:dc:53-01-0403",
|
||||
"state": "1010",
|
||||
"entity_category": None,
|
||||
"device_class": SensorDeviceClass.PRESSURE,
|
||||
|
@ -458,7 +472,8 @@ TEST_DATA = [
|
|||
"entity_count": 2,
|
||||
"device_count": 3,
|
||||
"entity_id": "sensor.mi_temperature_1",
|
||||
"unique_id": "00:15:8d:00:02:45:dc:53-01-0402",
|
||||
"unique_id": "00:15:8d:00:02:45:dc:53-01-0402-temperature",
|
||||
"old_unique_id": "00:15:8d:00:02:45:dc:53-01-0402",
|
||||
"state": "21.8",
|
||||
"entity_category": None,
|
||||
"device_class": SensorDeviceClass.TEMPERATURE,
|
||||
|
@ -501,7 +516,8 @@ TEST_DATA = [
|
|||
"entity_count": 2,
|
||||
"device_count": 3,
|
||||
"entity_id": "sensor.etrv_sejour",
|
||||
"unique_id": "cc:cc:cc:ff:fe:38:4d:b3-01-000a",
|
||||
"unique_id": "cc:cc:cc:ff:fe:38:4d:b3-01-000a-last_set",
|
||||
"old_unique_id": "cc:cc:cc:ff:fe:38:4d:b3-01-000a",
|
||||
"state": "2020-11-19T08:07:08+00:00",
|
||||
"entity_category": None,
|
||||
"device_class": SensorDeviceClass.TIMESTAMP,
|
||||
|
@ -515,7 +531,7 @@ TEST_DATA = [
|
|||
"next_state": "2020-12-14T10:12:14+00:00",
|
||||
},
|
||||
),
|
||||
( # Secondary temperature sensor
|
||||
( # Internal temperature sensor
|
||||
{
|
||||
"config": {
|
||||
"battery": 100,
|
||||
|
@ -542,7 +558,8 @@ TEST_DATA = [
|
|||
"entity_count": 3,
|
||||
"device_count": 3,
|
||||
"entity_id": "sensor.alarm_10_temperature",
|
||||
"unique_id": "00:15:8d:00:02:b5:d1:80-temperature",
|
||||
"unique_id": "00:15:8d:00:02:b5:d1:80-01-0500-internal_temperature",
|
||||
"old_unique_id": "00:15:8d:00:02:b5:d1:80-temperature",
|
||||
"state": "26.0",
|
||||
"entity_category": None,
|
||||
"device_class": SensorDeviceClass.TEMPERATURE,
|
||||
|
@ -583,7 +600,8 @@ TEST_DATA = [
|
|||
"entity_count": 1,
|
||||
"device_count": 3,
|
||||
"entity_id": "sensor.dimmer_switch_3_battery",
|
||||
"unique_id": "00:17:88:01:02:0e:32:a3-battery",
|
||||
"unique_id": "00:17:88:01:02:0e:32:a3-02-fc00-battery",
|
||||
"old_unique_id": "00:17:88:01:02:0e:32:a3-battery",
|
||||
"state": "90",
|
||||
"entity_category": EntityCategory.DIAGNOSTIC,
|
||||
"device_class": SensorDeviceClass.BATTERY,
|
||||
|
@ -611,6 +629,15 @@ async def test_sensors(
|
|||
ent_reg = er.async_get(hass)
|
||||
dev_reg = dr.async_get(hass)
|
||||
|
||||
# Create entity entry to migrate to new unique ID
|
||||
if "old_unique_id" in expected:
|
||||
ent_reg.async_get_or_create(
|
||||
SENSOR_DOMAIN,
|
||||
DECONZ_DOMAIN,
|
||||
expected["old_unique_id"],
|
||||
suggested_object_id=expected["entity_id"].replace("sensor.", ""),
|
||||
)
|
||||
|
||||
with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"1": sensor_data}}):
|
||||
config_entry = await setup_deconz_integration(
|
||||
hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True}
|
||||
|
@ -848,7 +875,11 @@ async def test_air_quality_sensor_without_ppb(hass, aioclient_mock):
|
|||
|
||||
|
||||
async def test_add_battery_later(hass, aioclient_mock, mock_deconz_websocket):
|
||||
"""Test that a sensor without an initial battery state creates a battery sensor once state exist."""
|
||||
"""Test that a battery sensor can be created later on.
|
||||
|
||||
Without an initial battery state a battery sensor
|
||||
can be created once a value is reported.
|
||||
"""
|
||||
data = {
|
||||
"sensors": {
|
||||
"1": {
|
||||
|
@ -856,15 +887,33 @@ async def test_add_battery_later(hass, aioclient_mock, mock_deconz_websocket):
|
|||
"type": "ZHASwitch",
|
||||
"state": {"buttonevent": 1000},
|
||||
"config": {},
|
||||
"uniqueid": "00:00:00:00:00:00:00:00-00",
|
||||
}
|
||||
"uniqueid": "00:00:00:00:00:00:00:00-00-0000",
|
||||
},
|
||||
"2": {
|
||||
"name": "Switch 2",
|
||||
"type": "ZHASwitch",
|
||||
"state": {"buttonevent": 1000},
|
||||
"config": {},
|
||||
"uniqueid": "00:00:00:00:00:00:00:00-00-0001",
|
||||
},
|
||||
}
|
||||
}
|
||||
with patch.dict(DECONZ_WEB_REQUEST, data):
|
||||
await setup_deconz_integration(hass, aioclient_mock)
|
||||
|
||||
assert len(hass.states.async_all()) == 0
|
||||
assert not hass.states.get("sensor.switch_1_battery")
|
||||
|
||||
event_changed_sensor = {
|
||||
"t": "event",
|
||||
"e": "changed",
|
||||
"r": "sensors",
|
||||
"id": "2",
|
||||
"config": {"battery": 50},
|
||||
}
|
||||
await mock_deconz_websocket(data=event_changed_sensor)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 0
|
||||
|
||||
event_changed_sensor = {
|
||||
"t": "event",
|
||||
|
|
Loading…
Reference in New Issue