mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 09:23:17 -04:00
homematicip_cloud: migrate entity unique IDs to stable format (#166580)
Co-authored-by: Christian Lackas <9592452+lackas@users.noreply.github.com>
This commit is contained in:
co-authored by
Christian Lackas
parent
759ac2eacd
commit
f225d8162b
@@ -1,5 +1,9 @@
|
||||
"""Support for HomematicIP Cloud devices."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
@@ -21,8 +25,11 @@ from .const import (
|
||||
HMIPC_NAME,
|
||||
)
|
||||
from .hap import HomematicIPConfigEntry, HomematicipHAP
|
||||
from .migration import _migrate_unique_id
|
||||
from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(DOMAIN, default=[]): vol.All(
|
||||
@@ -85,8 +92,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry)
|
||||
if not await hap.async_setup():
|
||||
return False
|
||||
|
||||
_async_remove_obsolete_entities(hass, entry, hap)
|
||||
|
||||
# Register on HA stop event to gracefully shutdown HomematicIP Cloud connection
|
||||
hap.reset_connection_listener = hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, hap.shutdown
|
||||
@@ -119,22 +124,61 @@ async def async_unload_entry(
|
||||
return await hap.async_reset()
|
||||
|
||||
|
||||
@callback
|
||||
def _async_remove_obsolete_entities(
|
||||
hass: HomeAssistant, entry: HomematicIPConfigEntry, hap: HomematicipHAP
|
||||
):
|
||||
"""Remove obsolete entities from entity registry."""
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: config_entries.ConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate the config entry from version 1 to version 2."""
|
||||
if config_entry.version > 2:
|
||||
return False
|
||||
|
||||
if hap.home.currentAPVersion < "2.2.12":
|
||||
return
|
||||
if config_entry.version == 1:
|
||||
_LOGGER.debug("Migrating HomematicIP Cloud config entry to version 2")
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
er_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id)
|
||||
for er_entry in er_entries:
|
||||
if er_entry.unique_id.startswith("HomematicipAccesspointStatus"):
|
||||
entity_registry.async_remove(er_entry.entity_id)
|
||||
continue
|
||||
# Remove obsolete entities before the bulk unique_id rewrite.
|
||||
# After rewrite, old-format patterns would no longer be matchable.
|
||||
# HomematicipAccesspointStatus* entities are always obsolete (removed
|
||||
# in firmware 2.2.12+). HomematicipBatterySensor_{hapid} entities for
|
||||
# access points are also obsolete. Those legacy access point battery
|
||||
# entities do not belong to a device registry device, unlike real
|
||||
# device battery sensors, so we can safely remove them before rewrite.
|
||||
entity_registry = er.async_get(hass)
|
||||
entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry.entry_id
|
||||
)
|
||||
for entry in entries:
|
||||
if entry.unique_id.startswith("HomematicipAccesspointStatus") or (
|
||||
entry.unique_id.startswith("HomematicipBatterySensor_")
|
||||
and entry.device_id is None
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Removing obsolete entity: %s (%s)",
|
||||
entry.entity_id,
|
||||
entry.unique_id,
|
||||
)
|
||||
entity_registry.async_remove(entry.entity_id)
|
||||
|
||||
for hapid in hap.home.accessPointUpdateStates:
|
||||
if er_entry.unique_id == f"HomematicipBatterySensor_{hapid}":
|
||||
entity_registry.async_remove(er_entry.entity_id)
|
||||
@callback
|
||||
def _update_unique_id(
|
||||
entity_entry: er.RegistryEntry,
|
||||
) -> dict[str, str] | None:
|
||||
new_unique_id = _migrate_unique_id(entity_entry.unique_id)
|
||||
if new_unique_id is None:
|
||||
_LOGGER.debug(
|
||||
"Skipping unique_id %s (already stable format)",
|
||||
entity_entry.unique_id,
|
||||
)
|
||||
return None
|
||||
_LOGGER.debug(
|
||||
"Migrating %s: %s -> %s",
|
||||
entity_entry.entity_id,
|
||||
entity_entry.unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
return {"new_unique_id": new_unique_id}
|
||||
|
||||
await er.async_migrate_entries(hass, config_entry.entry_id, _update_unique_id)
|
||||
|
||||
hass.config_entries.async_update_entry(config_entry, version=2)
|
||||
_LOGGER.info("Migration to version 2 successful")
|
||||
|
||||
return True
|
||||
|
||||
@@ -42,6 +42,7 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity):
|
||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
)
|
||||
_attr_code_arm_required = False
|
||||
_feature_id = "alarm"
|
||||
|
||||
def __init__(self, hap: HomematicipHAP) -> None:
|
||||
"""Initialize the alarm control panel."""
|
||||
@@ -127,4 +128,4 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity):
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
return f"{self.__class__.__name__}_{self._home.id}"
|
||||
return f"{self._home.id}_{self._feature_id}"
|
||||
|
||||
@@ -179,7 +179,7 @@ class HomematicipCloudConnectionSensor(HomematicipGenericEntity, BinarySensorEnt
|
||||
|
||||
def __init__(self, hap: HomematicipHAP) -> None:
|
||||
"""Initialize the cloud connection sensor."""
|
||||
super().__init__(hap, hap.home)
|
||||
super().__init__(hap, hap.home, feature_id="cloud_connection")
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -245,10 +245,18 @@ class HomematicipBaseActionSensor(HomematicipGenericEntity, BinarySensorEntity):
|
||||
class HomematicipAccelerationSensor(HomematicipBaseActionSensor):
|
||||
"""Representation of the HomematicIP acceleration sensor."""
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the acceleration sensor."""
|
||||
super().__init__(hap, device, feature_id="acceleration")
|
||||
|
||||
|
||||
class HomematicipTiltVibrationSensor(HomematicipBaseActionSensor):
|
||||
"""Representation of the HomematicIP tilt vibration sensor."""
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the tilt vibration sensor."""
|
||||
super().__init__(hap, device, feature_id="tilt_vibration")
|
||||
|
||||
|
||||
class HomematicipMultiContactInterface(HomematicipGenericEntity, BinarySensorEntity):
|
||||
"""Representation of the HomematicIP multi room/area contact interface."""
|
||||
@@ -262,6 +270,7 @@ class HomematicipMultiContactInterface(HomematicipGenericEntity, BinarySensorEnt
|
||||
channel=1,
|
||||
is_multi_channel=True,
|
||||
channel_real_index=None,
|
||||
feature_id: str = "contact",
|
||||
) -> None:
|
||||
"""Initialize the multi contact entity."""
|
||||
super().__init__(
|
||||
@@ -270,6 +279,7 @@ class HomematicipMultiContactInterface(HomematicipGenericEntity, BinarySensorEnt
|
||||
channel=channel,
|
||||
is_multi_channel=is_multi_channel,
|
||||
channel_real_index=channel_real_index,
|
||||
feature_id=feature_id,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -286,7 +296,7 @@ class HomematicipContactInterface(HomematicipMultiContactInterface, BinarySensor
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the multi contact entity."""
|
||||
super().__init__(hap, device, is_multi_channel=False)
|
||||
super().__init__(hap, device, is_multi_channel=False, feature_id="contact")
|
||||
|
||||
|
||||
class HomematicipShutterContact(HomematicipMultiContactInterface, BinarySensorEntity):
|
||||
@@ -298,7 +308,9 @@ class HomematicipShutterContact(HomematicipMultiContactInterface, BinarySensorEn
|
||||
self, hap: HomematicipHAP, device, has_additional_state: bool = False
|
||||
) -> None:
|
||||
"""Initialize the shutter contact."""
|
||||
super().__init__(hap, device, is_multi_channel=False)
|
||||
super().__init__(
|
||||
hap, device, is_multi_channel=False, feature_id="shutter_contact"
|
||||
)
|
||||
self.has_additional_state = has_additional_state
|
||||
|
||||
@property
|
||||
@@ -319,6 +331,10 @@ class HomematicipMotionDetector(HomematicipGenericEntity, BinarySensorEntity):
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.MOTION
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the motion detector."""
|
||||
super().__init__(hap, device, feature_id="motion")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if motion is detected."""
|
||||
@@ -334,7 +350,7 @@ class HomematicipFullFlushLockControllerLocked(
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the full flush lock controller lock sensor."""
|
||||
super().__init__(hap, device, post="Locked")
|
||||
super().__init__(hap, device, post="Locked", feature_id="lock_locked")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
@@ -359,7 +375,7 @@ class HomematicipFullFlushLockControllerGlassBreak(
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the full flush lock controller glass break sensor."""
|
||||
super().__init__(hap, device, post="Glass break")
|
||||
super().__init__(hap, device, post="Glass break", feature_id="glass_break")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
@@ -379,6 +395,10 @@ class HomematicipPresenceDetector(HomematicipGenericEntity, BinarySensorEntity):
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.PRESENCE
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the presence detector."""
|
||||
super().__init__(hap, device, feature_id="presence")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if presence is detected."""
|
||||
@@ -390,6 +410,10 @@ class HomematicipSmokeDetector(HomematicipGenericEntity, BinarySensorEntity):
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.SMOKE
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the smoke detector."""
|
||||
super().__init__(hap, device, feature_id="smoke")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if smoke is detected."""
|
||||
@@ -410,7 +434,9 @@ class HomematicipSmokeDetectorChamberDegraded(
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize smoke detector chamber health sensor."""
|
||||
super().__init__(hap, device, post="Chamber Degraded")
|
||||
super().__init__(
|
||||
hap, device, post="Chamber Degraded", feature_id="chamber_degraded"
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
@@ -423,6 +449,10 @@ class HomematicipWaterDetector(HomematicipGenericEntity, BinarySensorEntity):
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.MOISTURE
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the water detector."""
|
||||
super().__init__(hap, device, feature_id="water")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true, if moisture or waterlevel is detected."""
|
||||
@@ -434,7 +464,7 @@ class HomematicipStormSensor(HomematicipGenericEntity, BinarySensorEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize storm sensor."""
|
||||
super().__init__(hap, device, "Storm")
|
||||
super().__init__(hap, device, "Storm", feature_id="storm")
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
@@ -454,7 +484,7 @@ class HomematicipRainSensor(HomematicipGenericEntity, BinarySensorEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize rain sensor."""
|
||||
super().__init__(hap, device, "Raining")
|
||||
super().__init__(hap, device, "Raining", feature_id="rain")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
@@ -469,7 +499,7 @@ class HomematicipSunshineSensor(HomematicipGenericEntity, BinarySensorEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize sunshine sensor."""
|
||||
super().__init__(hap, device, post="Sunshine")
|
||||
super().__init__(hap, device, post="Sunshine", feature_id="sunshine")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
@@ -495,7 +525,7 @@ class HomematicipBatterySensor(HomematicipGenericEntity, BinarySensorEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize battery sensor."""
|
||||
super().__init__(hap, device, post="Battery")
|
||||
super().__init__(hap, device, post="Battery", channel=0, feature_id="battery")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
@@ -512,7 +542,7 @@ class HomematicipPluggableMainsFailureSurveillanceSensor(
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize pluggable mains failure surveillance sensor."""
|
||||
super().__init__(hap, device)
|
||||
super().__init__(hap, device, feature_id="mains_failure")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
@@ -525,10 +555,16 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericEntity, BinarySensorE
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.SAFETY
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device, post: str = "SecurityZone") -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hap: HomematicipHAP,
|
||||
device,
|
||||
post: str = "SecurityZone",
|
||||
feature_id: str = "security_zone",
|
||||
) -> None:
|
||||
"""Initialize security zone group."""
|
||||
device.modelType = f"HmIP-{post}"
|
||||
super().__init__(hap, device, post=post)
|
||||
super().__init__(hap, device, post=post, feature_id=feature_id)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
@@ -578,7 +614,7 @@ class HomematicipSecuritySensorGroup(
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize security group."""
|
||||
super().__init__(hap, device, post="Sensors")
|
||||
super().__init__(hap, device, post="Sensors", feature_id="security")
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
|
||||
@@ -45,7 +45,7 @@ class HomematicipGarageDoorControllerButton(HomematicipGenericEntity, ButtonEnti
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize a wall mounted garage door controller."""
|
||||
super().__init__(hap, device)
|
||||
super().__init__(hap, device, feature_id="garage_button")
|
||||
self._attr_icon = "mdi:arrow-up-down"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
@@ -58,7 +58,9 @@ class HomematicipFullFlushLockControllerButton(HomematicipGenericEntity, ButtonE
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the full flush lock controller opener button."""
|
||||
super().__init__(hap, device, post="Door opener")
|
||||
super().__init__(
|
||||
hap, device, post="Door opener", feature_id="lock_opener_button"
|
||||
)
|
||||
self._attr_icon = "mdi:door-open"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
|
||||
@@ -83,7 +83,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity):
|
||||
def __init__(self, hap: HomematicipHAP, device: HeatingGroup) -> None:
|
||||
"""Initialize heating group."""
|
||||
device.modelType = "HmIP-Heating-Group"
|
||||
super().__init__(hap, device)
|
||||
super().__init__(hap, device, feature_id="climate")
|
||||
self._simple_heating = None
|
||||
if device.actualTemperature is None:
|
||||
self._simple_heating = self._first_radiator_thermostat
|
||||
|
||||
@@ -16,7 +16,7 @@ from .hap import HomematicipAuth
|
||||
class HomematicipCloudFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for the HomematicIP Cloud component."""
|
||||
|
||||
VERSION = 1
|
||||
VERSION = 2
|
||||
|
||||
auth: HomematicipAuth
|
||||
|
||||
|
||||
@@ -69,6 +69,10 @@ class HomematicipBlindModule(HomematicipGenericEntity, CoverEntity):
|
||||
|
||||
_attr_device_class = CoverDeviceClass.BLIND
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the blind module entity."""
|
||||
super().__init__(hap, device, feature_id="blind")
|
||||
|
||||
@property
|
||||
def current_cover_position(self) -> int | None:
|
||||
"""Return current position of cover."""
|
||||
@@ -153,10 +157,15 @@ class HomematicipMultiCoverShutter(HomematicipGenericEntity, CoverEntity):
|
||||
device,
|
||||
channel=1,
|
||||
is_multi_channel=True,
|
||||
feature_id="shutter",
|
||||
) -> None:
|
||||
"""Initialize the multi cover entity."""
|
||||
super().__init__(
|
||||
hap, device, channel=channel, is_multi_channel=is_multi_channel
|
||||
hap,
|
||||
device,
|
||||
channel=channel,
|
||||
is_multi_channel=is_multi_channel,
|
||||
feature_id=feature_id,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -218,7 +227,11 @@ class HomematicipMultiCoverSlats(HomematicipMultiCoverShutter, CoverEntity):
|
||||
) -> None:
|
||||
"""Initialize the multi slats entity."""
|
||||
super().__init__(
|
||||
hap, device, channel=channel, is_multi_channel=is_multi_channel
|
||||
hap,
|
||||
device,
|
||||
channel=channel,
|
||||
is_multi_channel=is_multi_channel,
|
||||
feature_id="slats",
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -269,6 +282,10 @@ class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity):
|
||||
|
||||
_attr_device_class = CoverDeviceClass.GARAGE
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the garage door module entity."""
|
||||
super().__init__(hap, device, feature_id="garage_door")
|
||||
|
||||
@property
|
||||
def current_cover_position(self) -> int | None:
|
||||
"""Return current position of cover."""
|
||||
@@ -310,7 +327,9 @@ class HomematicipCoverShutterGroup(HomematicipGenericEntity, CoverEntity):
|
||||
def __init__(self, hap: HomematicipHAP, device, post: str = "ShutterGroup") -> None:
|
||||
"""Initialize switching group."""
|
||||
device.modelType = f"HmIP-{post}"
|
||||
super().__init__(hap, device, post, is_multi_channel=False)
|
||||
super().__init__(
|
||||
hap, device, post, is_multi_channel=False, feature_id="shutter"
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
||||
@@ -86,6 +86,8 @@ class HomematicipGenericEntity(Entity):
|
||||
channel: int | None = None,
|
||||
is_multi_channel: bool | None = False,
|
||||
channel_real_index: int | None = None,
|
||||
*,
|
||||
feature_id: str,
|
||||
) -> None:
|
||||
"""Initialize the generic entity."""
|
||||
self._hap = hap
|
||||
@@ -101,6 +103,7 @@ class HomematicipGenericEntity(Entity):
|
||||
# Using channel_real_index ensures you reference the correct channel.
|
||||
self._channel_real_index: int | None = channel_real_index
|
||||
|
||||
self._feature_id = feature_id
|
||||
self._is_multi_channel = is_multi_channel
|
||||
self.functional_channel = None
|
||||
with contextlib.suppress(ValueError):
|
||||
@@ -237,11 +240,10 @@ class HomematicipGenericEntity(Entity):
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
unique_id = f"{self.__class__.__name__}_{self._device.id}"
|
||||
if self._is_multi_channel:
|
||||
unique_id = f"{self.__class__.__name__}_Channel{self.get_channel_index()}_{self._device.id}"
|
||||
|
||||
return unique_id
|
||||
if not isinstance(self._device, Device):
|
||||
return f"{self._device.id}_{self._feature_id}"
|
||||
channel_index = self.get_channel_index()
|
||||
return f"{self._device.id}_{channel_index}_{self._feature_id}"
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
|
||||
@@ -85,6 +85,7 @@ class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity):
|
||||
post=description.key,
|
||||
channel=channel,
|
||||
is_multi_channel=False,
|
||||
feature_id="doorbell",
|
||||
)
|
||||
|
||||
self.entity_description = description
|
||||
|
||||
@@ -126,7 +126,7 @@ class HomematicipLight(HomematicipGenericEntity, LightEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the light entity."""
|
||||
super().__init__(hap, device)
|
||||
super().__init__(hap, device, feature_id="light")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
@@ -147,7 +147,13 @@ class HomematicipColorLight(HomematicipGenericEntity, LightEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device: Device, channel_index: int) -> None:
|
||||
"""Initialize the light entity."""
|
||||
super().__init__(hap, device, channel=channel_index, is_multi_channel=True)
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
channel=channel_index,
|
||||
is_multi_channel=True,
|
||||
feature_id="color_light",
|
||||
)
|
||||
|
||||
def _supports_color(self) -> bool:
|
||||
"""Return true if device supports hue/saturation color control."""
|
||||
@@ -243,7 +249,11 @@ class HomematicipMultiDimmer(HomematicipGenericEntity, LightEntity):
|
||||
) -> None:
|
||||
"""Initialize the dimmer light entity."""
|
||||
super().__init__(
|
||||
hap, device, channel=channel, is_multi_channel=is_multi_channel
|
||||
hap,
|
||||
device,
|
||||
channel=channel,
|
||||
is_multi_channel=is_multi_channel,
|
||||
feature_id="dimmer",
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -290,7 +300,14 @@ class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device, channel: int, post: str) -> None:
|
||||
"""Initialize the notification light entity."""
|
||||
super().__init__(hap, device, post=post, channel=channel, is_multi_channel=True)
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
post=post,
|
||||
channel=channel,
|
||||
is_multi_channel=True,
|
||||
feature_id="notification_light",
|
||||
)
|
||||
|
||||
self._color_switcher: dict[str, tuple[float, float]] = {
|
||||
RGBColorState.WHITE: (0.0, 0.0),
|
||||
@@ -335,11 +352,6 @@ class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity):
|
||||
|
||||
return state_attr
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
return f"{self.__class__.__name__}_{self._post}_{self._device.id}"
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on."""
|
||||
# Use hs_color from kwargs,
|
||||
@@ -513,6 +525,7 @@ class HomematicipOpticalSignalLight(HomematicipGenericEntity, LightEntity):
|
||||
channel=channel_index,
|
||||
is_multi_channel=True,
|
||||
channel_real_index=channel_index,
|
||||
feature_id="optical_signal_light",
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -614,7 +627,13 @@ class HomematicipCombinationSignallingLight(HomematicipGenericEntity, LightEntit
|
||||
self, hap: HomematicipHAP, device: CombinationSignallingDevice
|
||||
) -> None:
|
||||
"""Initialize the combination signalling light entity."""
|
||||
super().__init__(hap, device, channel=1, is_multi_channel=False)
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
channel=1,
|
||||
is_multi_channel=False,
|
||||
feature_id="combination_signalling_light",
|
||||
)
|
||||
|
||||
@property
|
||||
def _func_channel(self) -> NotificationMp3SoundChannel:
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .entity import HomematicipGenericEntity
|
||||
from .hap import HomematicIPConfigEntry
|
||||
from .hap import HomematicIPConfigEntry, HomematicipHAP
|
||||
from .helpers import handle_errors
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -53,6 +53,10 @@ class HomematicipDoorLockDrive(HomematicipGenericEntity, LockEntity):
|
||||
|
||||
_attr_supported_features = LockEntityFeature.OPEN
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device: DoorLockDrive) -> None:
|
||||
"""Initialize the door lock drive."""
|
||||
super().__init__(hap, device, feature_id="lock")
|
||||
|
||||
@property
|
||||
def is_locked(self) -> bool | None:
|
||||
"""Return true if device is locked."""
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
"""Unique ID migration for HomematicIP Cloud entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
import re
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _MigrationConfig:
|
||||
"""Configuration for migrating a single entity class to the new unique_id format."""
|
||||
|
||||
feature_id: str
|
||||
channel: int | None = None
|
||||
is_group: bool = False
|
||||
|
||||
|
||||
UNIQUE_ID_MIGRATION_MAP: dict[str, _MigrationConfig] = {
|
||||
# binary_sensor
|
||||
"HomematicipCloudConnectionSensor": _MigrationConfig(
|
||||
"cloud_connection", is_group=True
|
||||
),
|
||||
"HomematicipAccelerationSensor": _MigrationConfig("acceleration", channel=1),
|
||||
"HomematicipTiltVibrationSensor": _MigrationConfig("tilt_vibration", channel=1),
|
||||
"HomematicipMultiContactInterface": _MigrationConfig("contact"),
|
||||
"HomematicipContactInterface": _MigrationConfig("contact", channel=1),
|
||||
"HomematicipShutterContact": _MigrationConfig("shutter_contact", channel=1),
|
||||
"HomematicipMotionDetector": _MigrationConfig("motion", channel=1),
|
||||
"HomematicipPresenceDetector": _MigrationConfig("presence", channel=1),
|
||||
"HomematicipSmokeDetector": _MigrationConfig("smoke", channel=1),
|
||||
"HomematicipWaterDetector": _MigrationConfig("water", channel=1),
|
||||
"HomematicipStormSensor": _MigrationConfig("storm", channel=1),
|
||||
"HomematicipRainSensor": _MigrationConfig("rain", channel=1),
|
||||
"HomematicipSunshineSensor": _MigrationConfig("sunshine", channel=1),
|
||||
"HomematicipBatterySensor": _MigrationConfig("battery", channel=0),
|
||||
"HomematicipPluggableMainsFailureSurveillanceSensor": _MigrationConfig(
|
||||
"mains_failure", channel=1
|
||||
),
|
||||
"HomematicipSecurityZoneSensorGroup": _MigrationConfig(
|
||||
"security_zone", is_group=True
|
||||
),
|
||||
"HomematicipSecuritySensorGroup": _MigrationConfig("security", is_group=True),
|
||||
"HomematicipFullFlushLockControllerLocked": _MigrationConfig(
|
||||
"lock_locked", channel=1
|
||||
),
|
||||
"HomematicipFullFlushLockControllerGlassBreak": _MigrationConfig(
|
||||
"glass_break", channel=1
|
||||
),
|
||||
"HomematicipSmokeDetectorChamberDegraded": _MigrationConfig(
|
||||
"chamber_degraded", channel=1
|
||||
),
|
||||
# sensor
|
||||
"HomematicipAccesspointDutyCycle": _MigrationConfig("duty_cycle", channel=0),
|
||||
"HomematicipHeatingThermostat": _MigrationConfig("valve_position", channel=1),
|
||||
"HomematicipHumiditySensor": _MigrationConfig("humidity", channel=1),
|
||||
"HomematicipTemperatureSensor": _MigrationConfig("temperature", channel=1),
|
||||
"HomematicipAbsoluteHumiditySensor": _MigrationConfig(
|
||||
"absolute_humidity", channel=1
|
||||
),
|
||||
"HomematicipIlluminanceSensor": _MigrationConfig("illuminance", channel=1),
|
||||
"HomematicipPowerSensor": _MigrationConfig("power", channel=1),
|
||||
"HomematicipEnergySensor": _MigrationConfig("energy", channel=1),
|
||||
"HomematicipWindspeedSensor": _MigrationConfig("wind_speed", channel=1),
|
||||
"HomematicipTodayRainSensor": _MigrationConfig("today_rain", channel=1),
|
||||
"HomematicipPassageDetectorDeltaCounter": _MigrationConfig(
|
||||
"passage_counter", channel=1
|
||||
),
|
||||
"HomematicipWaterFlowSensor": _MigrationConfig("water_flow"),
|
||||
"HomematicipWaterVolumeSensor": _MigrationConfig("water_volume"),
|
||||
"HomematicipWaterVolumeSinceOpenSensor": _MigrationConfig(
|
||||
"water_volume_since_open"
|
||||
),
|
||||
"HomematicipTiltAngleSensor": _MigrationConfig("tilt_angle", channel=1),
|
||||
"HomematicipTiltStateSensor": _MigrationConfig("tilt_state", channel=1),
|
||||
"HomematicipFloorTerminalBlockMechanicChannelValve": _MigrationConfig(
|
||||
"ftb_valve_position"
|
||||
),
|
||||
"HomematicpTemperatureExternalSensorCh1": _MigrationConfig(
|
||||
"temperature_external_ch1", channel=1
|
||||
),
|
||||
"HomematicpTemperatureExternalSensorCh2": _MigrationConfig(
|
||||
"temperature_external_ch2", channel=1
|
||||
),
|
||||
"HomematicpTemperatureExternalSensorDelta": _MigrationConfig(
|
||||
"temperature_external_delta", channel=1
|
||||
),
|
||||
"HmipEsiIecPowerConsumption": _MigrationConfig("esi_iec_power", channel=1),
|
||||
"HmipEsiIecEnergyCounterHighTariff": _MigrationConfig(
|
||||
"esi_iec_energy_high", channel=1
|
||||
),
|
||||
"HmipEsiIecEnergyCounterLowTariff": _MigrationConfig(
|
||||
"esi_iec_energy_low", channel=1
|
||||
),
|
||||
"HmipEsiIecEnergyCounterInputSingleTariff": _MigrationConfig(
|
||||
"esi_iec_energy_input", channel=1
|
||||
),
|
||||
"HmipEsiGasCurrentGasFlow": _MigrationConfig("esi_gas_flow", channel=1),
|
||||
"HmipEsiGasGasVolume": _MigrationConfig("esi_gas_volume", channel=1),
|
||||
"HmipEsiLedCurrentPowerConsumption": _MigrationConfig("esi_led_power", channel=1),
|
||||
"HmipEsiLedEnergyCounterHighTariff": _MigrationConfig(
|
||||
"esi_led_energy_high", channel=1
|
||||
),
|
||||
"HomematicipSoilMoistureSensor": _MigrationConfig("soil_moisture", channel=1),
|
||||
"HomematicipSoilTemperatureSensor": _MigrationConfig("soil_temperature", channel=1),
|
||||
# light
|
||||
"HomematicipLight": _MigrationConfig("light", channel=1),
|
||||
"HomematicipLightHS": _MigrationConfig("light"),
|
||||
"HomematicipLightMeasuring": _MigrationConfig("light", channel=1),
|
||||
"HomematicipMultiDimmer": _MigrationConfig("dimmer"),
|
||||
"HomematicipDimmer": _MigrationConfig("dimmer", channel=1),
|
||||
"HomematicipNotificationLight": _MigrationConfig("notification_light"),
|
||||
"HomematicipNotificationLightV2": _MigrationConfig("notification_light"),
|
||||
"HomematicipColorLight": _MigrationConfig("color_light", channel=1),
|
||||
"HomematicipOpticalSignalLight": _MigrationConfig(
|
||||
"optical_signal_light", channel=1
|
||||
),
|
||||
"HomematicipCombinationSignallingLight": _MigrationConfig(
|
||||
"combination_signalling_light", channel=1
|
||||
),
|
||||
# switch
|
||||
"HomematicipMultiSwitch": _MigrationConfig("switch"),
|
||||
"HomematicipSwitch": _MigrationConfig("switch", channel=1),
|
||||
"HomematicipGroupSwitch": _MigrationConfig("switch", is_group=True),
|
||||
"HomematicipSwitchMeasuring": _MigrationConfig("switch", channel=1),
|
||||
# cover
|
||||
"HomematicipBlindModule": _MigrationConfig("blind", channel=1),
|
||||
"HomematicipMultiCoverShutter": _MigrationConfig("shutter"),
|
||||
"HomematicipCoverShutter": _MigrationConfig("shutter", channel=1),
|
||||
"HomematicipMultiCoverSlats": _MigrationConfig("slats"),
|
||||
"HomematicipCoverSlats": _MigrationConfig("slats", channel=1),
|
||||
"HomematicipGarageDoorModule": _MigrationConfig("garage_door", channel=1),
|
||||
"HomematicipCoverShutterGroup": _MigrationConfig("shutter", is_group=True),
|
||||
# climate
|
||||
"HomematicipHeatingGroup": _MigrationConfig("climate", is_group=True),
|
||||
# weather
|
||||
"HomematicipWeatherSensor": _MigrationConfig("weather", channel=1),
|
||||
"HomematicipWeatherSensorPro": _MigrationConfig("weather", channel=1),
|
||||
"HomematicipHomeWeather": _MigrationConfig("home_weather", is_group=True),
|
||||
# valve
|
||||
"HomematicipWateringValve": _MigrationConfig("watering"),
|
||||
# lock
|
||||
"HomematicipDoorLockDrive": _MigrationConfig("lock", channel=1),
|
||||
# button
|
||||
"HomematicipGarageDoorControllerButton": _MigrationConfig(
|
||||
"garage_button", channel=1
|
||||
),
|
||||
"HomematicipFullFlushLockControllerButton": _MigrationConfig(
|
||||
"lock_opener_button", channel=1
|
||||
),
|
||||
# event
|
||||
"HomematicipDoorBellEvent": _MigrationConfig("doorbell", channel=1),
|
||||
# alarm_control_panel
|
||||
"HomematicipAlarmControlPanelEntity": _MigrationConfig("alarm", is_group=True),
|
||||
# siren
|
||||
"HomematicipMP3Siren": _MigrationConfig("siren", channel=1),
|
||||
}
|
||||
|
||||
# Sorted by length descending so longer class names match before shorter ones
|
||||
# (e.g., "HomematicipSwitchMeasuring" before "HomematicipSwitch")
|
||||
_SORTED_CLASS_NAMES = sorted(UNIQUE_ID_MIGRATION_MAP, key=len, reverse=True)
|
||||
|
||||
_CHANNEL_RE = re.compile(r"^Channel(\d+)_(.+)$")
|
||||
_NOTIFICATION_LIGHT_RE = re.compile(r"^(Top|Bottom)_(.+)$")
|
||||
|
||||
_NOTIFICATION_LIGHT_CHANNEL_MAP = {"Top": 2, "Bottom": 3}
|
||||
|
||||
|
||||
def _migrate_unique_id(old_unique_id: str) -> str | None:
|
||||
"""Convert an old-format unique_id to the new format.
|
||||
|
||||
Old formats:
|
||||
{ClassName}_{device_id}
|
||||
{ClassName}_Channel{N}_{device_id}
|
||||
{ClassName}_{Top|Bottom}_{device_id} (NotificationLight only)
|
||||
|
||||
New format:
|
||||
{device_id}_{channel}_{feature_id} (device entities)
|
||||
{device_id}_{feature_id} (group/home entities)
|
||||
"""
|
||||
# Find the matching class name (longest first)
|
||||
matched_class: str | None = None
|
||||
for class_name in _SORTED_CLASS_NAMES:
|
||||
prefix = class_name + "_"
|
||||
if old_unique_id.startswith(prefix):
|
||||
matched_class = class_name
|
||||
break
|
||||
|
||||
if matched_class is None:
|
||||
return None
|
||||
|
||||
config = UNIQUE_ID_MIGRATION_MAP[matched_class]
|
||||
remainder = old_unique_id[len(matched_class) + 1 :]
|
||||
|
||||
# Parse remainder to extract channel and device_id
|
||||
channel: int | None = None
|
||||
device_id: str
|
||||
|
||||
# Check for Channel{N}_{rest} pattern
|
||||
channel_match = _CHANNEL_RE.match(remainder)
|
||||
if channel_match:
|
||||
channel = int(channel_match.group(1))
|
||||
device_id = channel_match.group(2)
|
||||
elif matched_class in (
|
||||
"HomematicipNotificationLight",
|
||||
"HomematicipNotificationLightV2",
|
||||
):
|
||||
# Check for Top/Bottom pattern
|
||||
notif_match = _NOTIFICATION_LIGHT_RE.match(remainder)
|
||||
if notif_match:
|
||||
channel = _NOTIFICATION_LIGHT_CHANNEL_MAP[notif_match.group(1)]
|
||||
device_id = notif_match.group(2)
|
||||
else:
|
||||
device_id = remainder
|
||||
channel = config.channel
|
||||
else:
|
||||
device_id = remainder
|
||||
channel = config.channel
|
||||
|
||||
# Build new unique_id
|
||||
if config.is_group:
|
||||
return f"{device_id}_{config.feature_id}"
|
||||
|
||||
if channel is not None:
|
||||
return f"{device_id}_{channel}_{config.feature_id}"
|
||||
|
||||
_LOGGER.warning(
|
||||
"Cannot determine channel for unique_id: %s",
|
||||
old_unique_id,
|
||||
)
|
||||
return None
|
||||
@@ -383,7 +383,14 @@ class HomematicipWaterFlowSensor(HomematicipGenericEntity, SensorEntity):
|
||||
self, hap: HomematicipHAP, device: Device, channel: int, post: str
|
||||
) -> None:
|
||||
"""Initialize the watering flow sensor device."""
|
||||
super().__init__(hap, device, post=post, channel=channel, is_multi_channel=True)
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
post=post,
|
||||
channel=channel,
|
||||
is_multi_channel=True,
|
||||
feature_id="water_flow",
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
@@ -405,9 +412,17 @@ class HomematicipWaterVolumeSensor(HomematicipGenericEntity, SensorEntity):
|
||||
channel: int,
|
||||
post: str,
|
||||
attribute: str,
|
||||
feature_id: str = "water_volume",
|
||||
) -> None:
|
||||
"""Initialize the watering volume sensor device."""
|
||||
super().__init__(hap, device, post=post, channel=channel, is_multi_channel=True)
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
post=post,
|
||||
channel=channel,
|
||||
is_multi_channel=True,
|
||||
feature_id=feature_id,
|
||||
)
|
||||
self._attribute_name = attribute
|
||||
|
||||
@property
|
||||
@@ -430,6 +445,7 @@ class HomematicipWaterVolumeSinceOpenSensor(HomematicipWaterVolumeSensor):
|
||||
channel=channel,
|
||||
post="waterVolumeSinceOpen",
|
||||
attribute="waterVolumeSinceOpen",
|
||||
feature_id="water_volume_since_open",
|
||||
)
|
||||
|
||||
|
||||
@@ -441,7 +457,7 @@ class HomematicipTiltAngleSensor(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the tilt angle sensor device."""
|
||||
super().__init__(hap, device, post="Tilt Angle")
|
||||
super().__init__(hap, device, post="Tilt Angle", feature_id="tilt_angle")
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
@@ -458,7 +474,7 @@ class HomematicipTiltStateSensor(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the tilt sensor device."""
|
||||
super().__init__(hap, device, post="Tilt State")
|
||||
super().__init__(hap, device, post="Tilt State", feature_id="tilt_state")
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
@@ -502,6 +518,7 @@ class HomematicipFloorTerminalBlockMechanicChannelValve(
|
||||
channel=channel,
|
||||
is_multi_channel=is_multi_channel,
|
||||
post="Valve Position",
|
||||
feature_id="ftb_valve_position",
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -540,7 +557,9 @@ class HomematicipAccesspointDutyCycle(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize access point status entity."""
|
||||
super().__init__(hap, device, post="Duty Cycle")
|
||||
super().__init__(
|
||||
hap, device, post="Duty Cycle", channel=0, feature_id="duty_cycle"
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
@@ -555,7 +574,7 @@ class HomematicipHeatingThermostat(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize heating thermostat device."""
|
||||
super().__init__(hap, device, post="Heating")
|
||||
super().__init__(hap, device, post="Heating", feature_id="valve_position")
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
@@ -583,7 +602,7 @@ class HomematicipHumiditySensor(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the thermometer device."""
|
||||
super().__init__(hap, device, post="Humidity")
|
||||
super().__init__(hap, device, post="Humidity", feature_id="humidity")
|
||||
|
||||
@property
|
||||
def native_value(self) -> int:
|
||||
@@ -600,7 +619,7 @@ class HomematicipTemperatureSensor(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the thermometer device."""
|
||||
super().__init__(hap, device, post="Temperature")
|
||||
super().__init__(hap, device, post="Temperature", feature_id="temperature")
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
@@ -633,7 +652,9 @@ class HomematicipAbsoluteHumiditySensor(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the thermometer device."""
|
||||
super().__init__(hap, device, post="Absolute Humidity")
|
||||
super().__init__(
|
||||
hap, device, post="Absolute Humidity", feature_id="absolute_humidity"
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
@@ -654,7 +675,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(hap, device, post="Illuminance")
|
||||
super().__init__(hap, device, post="Illuminance", feature_id="illuminance")
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
@@ -685,7 +706,7 @@ class HomematicipPowerSensor(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(hap, device, post="Power")
|
||||
super().__init__(hap, device, post="Power", feature_id="power")
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
@@ -702,7 +723,7 @@ class HomematicipEnergySensor(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(hap, device, post="Energy")
|
||||
super().__init__(hap, device, post="Energy", feature_id="energy")
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
@@ -719,7 +740,7 @@ class HomematicipWindspeedSensor(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the windspeed sensor."""
|
||||
super().__init__(hap, device, post="Windspeed")
|
||||
super().__init__(hap, device, post="Windspeed", feature_id="wind_speed")
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
@@ -751,7 +772,7 @@ class HomematicipTodayRainSensor(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(hap, device, post="Today Rain")
|
||||
super().__init__(hap, device, post="Today Rain", feature_id="today_rain")
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
@@ -768,7 +789,12 @@ class HomematicpTemperatureExternalSensorCh1(HomematicipGenericEntity, SensorEnt
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(hap, device, post="Channel 1 Temperature")
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
post="Channel 1 Temperature",
|
||||
feature_id="temperature_external_ch1",
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
@@ -785,7 +811,12 @@ class HomematicpTemperatureExternalSensorCh2(HomematicipGenericEntity, SensorEnt
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(hap, device, post="Channel 2 Temperature")
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
post="Channel 2 Temperature",
|
||||
feature_id="temperature_external_ch2",
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
@@ -802,7 +833,12 @@ class HomematicpTemperatureExternalSensorDelta(HomematicipGenericEntity, SensorE
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(hap, device, post="Delta Temperature")
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
post="Delta Temperature",
|
||||
feature_id="temperature_external_delta",
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
@@ -820,6 +856,7 @@ class HmipEsiSensorEntity(HomematicipGenericEntity, SensorEntity):
|
||||
key: str,
|
||||
value_fn: Callable[[FunctionalChannel], StateType],
|
||||
type_fn: Callable[[FunctionalChannel], str],
|
||||
feature_id: str,
|
||||
) -> None:
|
||||
"""Initialize Sensor Entity."""
|
||||
super().__init__(
|
||||
@@ -828,6 +865,7 @@ class HmipEsiSensorEntity(HomematicipGenericEntity, SensorEntity):
|
||||
channel=1,
|
||||
post=key,
|
||||
is_multi_channel=False,
|
||||
feature_id=feature_id,
|
||||
)
|
||||
|
||||
self._value_fn = value_fn
|
||||
@@ -862,6 +900,7 @@ class HmipEsiIecPowerConsumption(HmipEsiSensorEntity):
|
||||
key="CurrentPowerConsumption",
|
||||
value_fn=lambda channel: channel.currentPowerConsumption,
|
||||
type_fn=lambda channel: "CurrentPowerConsumption",
|
||||
feature_id="esi_iec_power",
|
||||
)
|
||||
|
||||
|
||||
@@ -880,6 +919,7 @@ class HmipEsiIecEnergyCounterHighTariff(HmipEsiSensorEntity):
|
||||
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
|
||||
value_fn=lambda channel: channel.energyCounterOne,
|
||||
type_fn=lambda channel: channel.energyCounterOneType,
|
||||
feature_id="esi_iec_energy_high",
|
||||
)
|
||||
|
||||
|
||||
@@ -898,6 +938,7 @@ class HmipEsiIecEnergyCounterLowTariff(HmipEsiSensorEntity):
|
||||
key=ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF,
|
||||
value_fn=lambda channel: channel.energyCounterTwo,
|
||||
type_fn=lambda channel: channel.energyCounterTwoType,
|
||||
feature_id="esi_iec_energy_low",
|
||||
)
|
||||
|
||||
|
||||
@@ -916,6 +957,7 @@ class HmipEsiIecEnergyCounterInputSingleTariff(HmipEsiSensorEntity):
|
||||
key=ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF,
|
||||
value_fn=lambda channel: channel.energyCounterThree,
|
||||
type_fn=lambda channel: channel.energyCounterThreeType,
|
||||
feature_id="esi_iec_energy_input",
|
||||
)
|
||||
|
||||
|
||||
@@ -934,6 +976,7 @@ class HmipEsiGasCurrentGasFlow(HmipEsiSensorEntity):
|
||||
key="CurrentGasFlow",
|
||||
value_fn=lambda channel: channel.currentGasFlow,
|
||||
type_fn=lambda channel: "CurrentGasFlow",
|
||||
feature_id="esi_gas_flow",
|
||||
)
|
||||
|
||||
|
||||
@@ -952,6 +995,7 @@ class HmipEsiGasGasVolume(HmipEsiSensorEntity):
|
||||
key="GasVolume",
|
||||
value_fn=lambda channel: channel.gasVolume,
|
||||
type_fn=lambda channel: "GasVolume",
|
||||
feature_id="esi_gas_volume",
|
||||
)
|
||||
|
||||
|
||||
@@ -970,6 +1014,7 @@ class HmipEsiLedCurrentPowerConsumption(HmipEsiSensorEntity):
|
||||
key="CurrentPowerConsumption",
|
||||
value_fn=lambda channel: channel.currentPowerConsumption,
|
||||
type_fn=lambda channel: "CurrentPowerConsumption",
|
||||
feature_id="esi_led_power",
|
||||
)
|
||||
|
||||
|
||||
@@ -988,12 +1033,17 @@ class HmipEsiLedEnergyCounterHighTariff(HmipEsiSensorEntity):
|
||||
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
|
||||
value_fn=lambda channel: channel.energyCounterOne,
|
||||
type_fn=lambda channel: ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
|
||||
feature_id="esi_led_energy_high",
|
||||
)
|
||||
|
||||
|
||||
class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEntity):
|
||||
"""Representation of the HomematicIP passage detector delta counter."""
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the passage detector delta counter."""
|
||||
super().__init__(hap, device, feature_id="passage_counter")
|
||||
|
||||
@property
|
||||
def native_value(self) -> int:
|
||||
"""Return the passage detector delta counter value."""
|
||||
@@ -1022,7 +1072,9 @@ class HmipSmokeDetectorSensor(HomematicipGenericEntity, SensorEntity):
|
||||
description: HmipSmokeDetectorSensorDescription,
|
||||
) -> None:
|
||||
"""Initialize the smoke detector sensor."""
|
||||
super().__init__(hap, device, post=description.key)
|
||||
super().__init__(
|
||||
hap, device, post=description.key, feature_id="smoke_detector_sensor"
|
||||
)
|
||||
self.entity_description = description
|
||||
self._sensor_unique_id = f"{device.id}_{description.key}"
|
||||
|
||||
@@ -1047,7 +1099,12 @@ class HomematicipSoilMoistureSensor(HomematicipGenericEntity, SensorEntity):
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the soil moisture sensor device."""
|
||||
super().__init__(
|
||||
hap, device, post="Soil Moisture", channel=1, is_multi_channel=True
|
||||
hap,
|
||||
device,
|
||||
post="Soil Moisture",
|
||||
channel=1,
|
||||
is_multi_channel=True,
|
||||
feature_id="soil_moisture",
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -1068,7 +1125,12 @@ class HomematicipSoilTemperatureSensor(HomematicipGenericEntity, SensorEntity):
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the soil temperature sensor device."""
|
||||
super().__init__(
|
||||
hap, device, post="Soil Temperature", channel=1, is_multi_channel=True
|
||||
hap,
|
||||
device,
|
||||
post="Soil Temperature",
|
||||
channel=1,
|
||||
is_multi_channel=True,
|
||||
feature_id="soil_temperature",
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -60,7 +60,14 @@ class HomematicipMP3Siren(HomematicipGenericEntity, SirenEntity):
|
||||
self, hap: HomematicipHAP, device: CombinationSignallingDevice
|
||||
) -> None:
|
||||
"""Initialize the siren entity."""
|
||||
super().__init__(hap, device, post="Siren", channel=1, is_multi_channel=False)
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
post="Siren",
|
||||
channel=1,
|
||||
is_multi_channel=False,
|
||||
feature_id="siren",
|
||||
)
|
||||
|
||||
@property
|
||||
def _func_channel(self) -> NotificationMp3SoundChannel:
|
||||
|
||||
@@ -109,7 +109,11 @@ class HomematicipMultiSwitch(HomematicipGenericEntity, SwitchEntity):
|
||||
) -> None:
|
||||
"""Initialize the multi switch device."""
|
||||
super().__init__(
|
||||
hap, device, channel=channel, is_multi_channel=is_multi_channel
|
||||
hap,
|
||||
device,
|
||||
channel=channel,
|
||||
is_multi_channel=is_multi_channel,
|
||||
feature_id="switch",
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -143,7 +147,7 @@ class HomematicipGroupSwitch(HomematicipGenericEntity, SwitchEntity):
|
||||
def __init__(self, hap: HomematicipHAP, device, post: str = "Group") -> None:
|
||||
"""Initialize switching group."""
|
||||
device.modelType = f"HmIP-{post}"
|
||||
super().__init__(hap, device, post)
|
||||
super().__init__(hap, device, post, feature_id="switch")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
|
||||
@@ -42,7 +42,12 @@ class HomematicipWateringValve(HomematicipGenericEntity, ValveEntity):
|
||||
def __init__(self, hap: HomematicipHAP, device: Device, channel: int) -> None:
|
||||
"""Initialize the valve."""
|
||||
super().__init__(
|
||||
hap, device=device, channel=channel, post="watering", is_multi_channel=True
|
||||
hap,
|
||||
device=device,
|
||||
channel=channel,
|
||||
post="watering",
|
||||
is_multi_channel=True,
|
||||
feature_id="watering",
|
||||
)
|
||||
|
||||
async def async_open_valve(self) -> None:
|
||||
|
||||
@@ -72,7 +72,7 @@ class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity):
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the weather sensor."""
|
||||
super().__init__(hap, device)
|
||||
super().__init__(hap, device, feature_id="weather")
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -125,7 +125,7 @@ class HomematicipHomeWeather(HomematicipGenericEntity, WeatherEntity):
|
||||
def __init__(self, hap: HomematicipHAP) -> None:
|
||||
"""Initialize the home weather."""
|
||||
hap.home.modelType = "HmIP-Home-Weather"
|
||||
super().__init__(hap, hap.home)
|
||||
super().__init__(hap, hap.home, feature_id="home_weather")
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from homematicip.exceptions.connection_exceptions import HmipConnectionError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homematicip_cloud.const import (
|
||||
CONF_ACCESSPOINT,
|
||||
@@ -16,6 +17,7 @@ from homeassistant.components.homematicip_cloud.hap import HomematicipHAP
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -202,3 +204,161 @@ async def test_setup_services(hass: HomeAssistant) -> None:
|
||||
assert len(config_entries) == 1
|
||||
|
||||
await hass.config_entries.async_unload(config_entries[0].entry_id)
|
||||
|
||||
|
||||
# --- Unique ID migration tests ---
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry_v1(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Create a v1 config entry for migration testing."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "token", HMIPC_NAME: ""},
|
||||
version=1,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
return entry
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("platform", "old_unique_id", "new_unique_id"),
|
||||
[
|
||||
(
|
||||
"binary_sensor",
|
||||
"HomematicipMotionDetector_3014F711ABCD",
|
||||
"3014F711ABCD_1_motion",
|
||||
),
|
||||
(
|
||||
"switch",
|
||||
"HomematicipMultiSwitch_Channel3_3014F711ABCD",
|
||||
"3014F711ABCD_3_switch",
|
||||
),
|
||||
(
|
||||
"light",
|
||||
"HomematicipNotificationLight_Top_3014F711ABCD",
|
||||
"3014F711ABCD_2_notification_light",
|
||||
),
|
||||
("climate", "HomematicipHeatingGroup_UUID-GROUP-123", "UUID-GROUP-123_climate"),
|
||||
],
|
||||
ids=["single_channel", "multi_channel", "notification_light", "group"],
|
||||
)
|
||||
async def test_migrate_unique_id(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry_v1: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
platform: str,
|
||||
old_unique_id: str,
|
||||
new_unique_id: str,
|
||||
) -> None:
|
||||
"""Test unique_id migration for different entity types."""
|
||||
entity_registry.async_get_or_create(
|
||||
platform,
|
||||
DOMAIN,
|
||||
old_unique_id,
|
||||
config_entry=mock_config_entry_v1,
|
||||
)
|
||||
|
||||
with patch("homeassistant.components.homematicip_cloud.HomematicipHAP") as mock_hap:
|
||||
instance = mock_hap.return_value
|
||||
instance.async_setup = AsyncMock(return_value=True)
|
||||
instance.home.id = "1"
|
||||
instance.home.modelType = "mock-type"
|
||||
instance.home.name = "mock-name"
|
||||
instance.home.label = "mock-label"
|
||||
instance.home.currentAPVersion = "mock-ap-version"
|
||||
instance.async_reset = AsyncMock(return_value=True)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry_v1.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry_v1.version == 2
|
||||
assert entity_registry.async_get_entity_id(platform, DOMAIN, new_unique_id)
|
||||
|
||||
|
||||
async def test_migrate_stable_unique_id_skipped(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry_v1: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that a non-class-name unique_id is silently skipped and preserved."""
|
||||
entity_registry.async_get_or_create(
|
||||
"sensor",
|
||||
DOMAIN,
|
||||
"HomematicipFutureEntity_3014F711ABCD",
|
||||
config_entry=mock_config_entry_v1,
|
||||
)
|
||||
|
||||
with patch("homeassistant.components.homematicip_cloud.HomematicipHAP") as mock_hap:
|
||||
instance = mock_hap.return_value
|
||||
instance.async_setup = AsyncMock(return_value=True)
|
||||
instance.home.id = "1"
|
||||
instance.home.modelType = "mock-type"
|
||||
instance.home.name = "mock-name"
|
||||
instance.home.label = "mock-label"
|
||||
instance.home.currentAPVersion = "mock-ap-version"
|
||||
instance.async_reset = AsyncMock(return_value=True)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry_v1.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry_v1.version == 2
|
||||
# Unknown prefix is not a known class name, so it's treated as already
|
||||
# stable and skipped silently (no warning, just debug).
|
||||
assert "already stable format" in caplog.text
|
||||
# Old unique_id is preserved (not migrated)
|
||||
assert entity_registry.async_get_entity_id(
|
||||
"sensor", DOMAIN, "HomematicipFutureEntity_3014F711ABCD"
|
||||
)
|
||||
|
||||
|
||||
async def test_migrate_battery_and_obsolete_access_point(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry_v1: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test battery migration and obsolete access point entity removal."""
|
||||
|
||||
# Obsolete access point battery entity: legacy unique_id, no linked device.
|
||||
obsolete_entity_id = entity_registry.async_get_or_create(
|
||||
"binary_sensor",
|
||||
DOMAIN,
|
||||
"HomematicipBatterySensor_ABC123",
|
||||
config_entry=mock_config_entry_v1,
|
||||
).entity_id
|
||||
|
||||
# Real device battery entity: same legacy class prefix, but attached to a device.
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=mock_config_entry_v1.entry_id,
|
||||
identifiers={(DOMAIN, "3014F711ABCD")},
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"binary_sensor",
|
||||
DOMAIN,
|
||||
"HomematicipBatterySensor_3014F711ABCD",
|
||||
config_entry=mock_config_entry_v1,
|
||||
device_id=device_entry.id,
|
||||
)
|
||||
|
||||
with patch("homeassistant.components.homematicip_cloud.HomematicipHAP") as mock_hap:
|
||||
instance = mock_hap.return_value
|
||||
instance.async_setup = AsyncMock(return_value=True)
|
||||
instance.home.id = "1"
|
||||
instance.home.modelType = "mock-type"
|
||||
instance.home.name = "mock-name"
|
||||
instance.home.label = "mock-label"
|
||||
instance.home.currentAPVersion = "mock-ap-version"
|
||||
instance.async_reset = AsyncMock(return_value=True)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry_v1.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry_v1.version == 2
|
||||
# Obsolete access point battery entity removed
|
||||
assert entity_registry.async_get(obsolete_entity_id) is None
|
||||
# Real device battery entity migrated
|
||||
assert entity_registry.async_get_entity_id(
|
||||
"binary_sensor", DOMAIN, "3014F711ABCD_0_battery"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user