mirror of
https://github.com/home-assistant/core.git
synced 2026-08-16 10:13:55 +01:00
220 lines
8.0 KiB
Python
220 lines
8.0 KiB
Python
"""Base classes for KNX entities."""
|
|
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING, Any, override
|
|
|
|
from xknx.devices import Device as XknxDevice
|
|
from xknx.telegram.address import DeviceGroupAddress, GroupAddress
|
|
|
|
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, EntityCategory
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.helpers import entity_registry as er
|
|
from homeassistant.helpers.device_registry import DeviceInfo
|
|
from homeassistant.helpers.entity import Entity
|
|
from homeassistant.helpers.entity_platform import (
|
|
EntityPlatform,
|
|
async_get_current_platform,
|
|
)
|
|
from homeassistant.helpers.entity_registry import RegistryEntry
|
|
|
|
from .const import CONF_DEFAULT_ENTITY_ID, DOMAIN
|
|
from .storage.config_store import PlatformControllerBase
|
|
from .storage.const import CONF_DEVICE_INFO
|
|
|
|
if TYPE_CHECKING:
|
|
from .knx_module import KNXModule
|
|
|
|
|
|
def _stable_group_address_repr(part: DeviceGroupAddress | int | str | None) -> str:
|
|
"""Render a unique_id part independent of `GroupAddress.address_format`."""
|
|
if isinstance(part, GroupAddress):
|
|
# Always LONG (main/middle/sub) derived from raw, so the representation
|
|
# does not change when the global address format changes (e.g. on ETS
|
|
# project import). This is bijective with raw, keeping ids unique.
|
|
return (
|
|
f"{(part.raw >> 11) & 0b11111}/{(part.raw >> 8) & 0b111}/{part.raw & 0xFF}"
|
|
)
|
|
# InternalGroupAddress is already format-independent; None renders as "None"
|
|
return str(part)
|
|
|
|
|
|
def build_yaml_unique_id(
|
|
*parts: DeviceGroupAddress | int | str | None,
|
|
) -> tuple[str, str]:
|
|
"""Return `(new_stable_id, legacy_id)` for a YAML entity.
|
|
|
|
`new_stable_id` is independent of the global group address format. `legacy_id`
|
|
matches the id produced before this fix (using the current global format) and is
|
|
used to migrate registry entries of installations not using the 3-level style.
|
|
Pass the result as `unique_id` to `KnxYamlEntity`, which runs the migration.
|
|
"""
|
|
new_id = "_".join(_stable_group_address_repr(part) for part in parts)
|
|
legacy_id = "_".join(str(part) for part in parts)
|
|
return new_id, legacy_id
|
|
|
|
|
|
@callback
|
|
def async_migrate_yaml_unique_id(
|
|
hass: HomeAssistant, platform: str, legacy_id: str, new_id: str
|
|
) -> None:
|
|
"""Migrate a YAML entity unique_id from the legacy format to the stable one."""
|
|
# migration from unstable group address string parts added in 2026.8
|
|
if legacy_id == new_id:
|
|
return
|
|
ent_reg = er.async_get(hass)
|
|
if (entity_id := ent_reg.async_get_entity_id(platform, DOMAIN, legacy_id)) is None:
|
|
return
|
|
try:
|
|
ent_reg.async_update_entity(entity_id, new_unique_id=new_id)
|
|
except ValueError:
|
|
# A stable-id entity already exists next to the legacy one - e.g. the
|
|
# original entity was orphaned under the stable id when the pre-fix bug
|
|
# registered the legacy entry. Keep the stable entry, drop the legacy one.
|
|
ent_reg.async_remove(entity_id)
|
|
|
|
|
|
@dataclass(slots=True, frozen=True)
|
|
class KnxEntityIdentifier:
|
|
"""Class to identify KNX entities in KNX frontend."""
|
|
|
|
platform: str
|
|
unique_id: str
|
|
ui: bool # ui or yaml entity
|
|
|
|
|
|
class KnxUiEntityPlatformController(PlatformControllerBase):
|
|
"""Class to manage dynamic adding and reloading of UI entities."""
|
|
|
|
def __init__(
|
|
self,
|
|
knx_module: KNXModule,
|
|
entity_platform: EntityPlatform,
|
|
entity_class: type[KnxUiEntity],
|
|
) -> None:
|
|
"""Initialize the UI platform."""
|
|
self._knx_module = knx_module
|
|
self._entity_platform = entity_platform
|
|
self._entity_class = entity_class
|
|
|
|
@override
|
|
async def create_entity(self, unique_id: str, config: dict[str, Any]) -> None:
|
|
"""Add a new UI entity."""
|
|
await self._entity_platform.async_add_entities(
|
|
[self._entity_class(self._knx_module, unique_id, config)]
|
|
)
|
|
|
|
@override
|
|
async def update_entity(
|
|
self, entity_entry: RegistryEntry, config: dict[str, Any]
|
|
) -> None:
|
|
"""Update an existing UI entities configuration."""
|
|
await self._entity_platform.async_remove_entity(entity_entry.entity_id)
|
|
await self.create_entity(unique_id=entity_entry.unique_id, config=config)
|
|
|
|
|
|
class _KnxEntityBase(Entity):
|
|
"""Representation of a KNX entity."""
|
|
|
|
_attr_should_poll = False
|
|
|
|
_attr_unique_id: str
|
|
_knx_module: KNXModule
|
|
_device: XknxDevice
|
|
|
|
_knx_entity_identifier: KnxEntityIdentifier | None = None
|
|
|
|
@property
|
|
@override
|
|
def available(self) -> bool:
|
|
"""Return True if entity is available."""
|
|
return self._knx_module.connected
|
|
|
|
async def async_update(self) -> None:
|
|
"""Request a state update from KNX bus."""
|
|
await self._device.sync()
|
|
|
|
def after_update_callback(self, device: XknxDevice) -> None:
|
|
"""Call after device was updated."""
|
|
self.async_write_ha_state()
|
|
|
|
@override
|
|
async def async_added_to_hass(self) -> None:
|
|
"""Store register state change callback and start device object."""
|
|
self._device.register_device_updated_cb(self.after_update_callback)
|
|
self._device.xknx.devices.async_add(self._device)
|
|
if uid := self.unique_id:
|
|
self._knx_entity_identifier = KnxEntityIdentifier(
|
|
platform=self.platform_data.domain,
|
|
unique_id=uid,
|
|
ui=isinstance(self, KnxUiEntity),
|
|
)
|
|
self._knx_module.add_to_group_address_entities(
|
|
group_addresses=self._device.group_addresses(),
|
|
identifier=self._knx_entity_identifier,
|
|
)
|
|
|
|
# super call needed to have methods of multi-inherited classes called
|
|
# eg. for restoring state (like _KNXSwitch)
|
|
await super().async_added_to_hass()
|
|
|
|
@override
|
|
async def async_will_remove_from_hass(self) -> None:
|
|
"""Disconnect device object when removed."""
|
|
self._device.unregister_device_updated_cb(self.after_update_callback)
|
|
self._device.xknx.devices.async_remove(self._device)
|
|
if self._knx_entity_identifier:
|
|
self._knx_module.remove_from_group_address_entities(
|
|
group_addresses=self._device.group_addresses(),
|
|
identifier=self._knx_entity_identifier,
|
|
)
|
|
|
|
|
|
class KnxYamlEntity(_KnxEntityBase):
|
|
"""Representation of a KNX entity configured from YAML."""
|
|
|
|
def __init__(
|
|
self,
|
|
knx_module: KNXModule,
|
|
unique_id: tuple[str, str], # new_stable_id, legacy_id for migration
|
|
entity_config: dict[str, Any],
|
|
) -> None:
|
|
"""Initialize the YAML entity.
|
|
|
|
`unique_id` is the `(new_stable_id, legacy_id)` tuple from
|
|
`build_yaml_unique_id`; the legacy id is migrated to the stable one.
|
|
"""
|
|
new_unique_id, legacy_unique_id = unique_id
|
|
async_migrate_yaml_unique_id(
|
|
knx_module.hass,
|
|
async_get_current_platform().domain,
|
|
legacy_unique_id,
|
|
new_unique_id,
|
|
)
|
|
self._knx_module = knx_module
|
|
self._attr_name = entity_config[CONF_NAME] or None
|
|
self._attr_unique_id = new_unique_id
|
|
self._attr_entity_category = entity_config.get(CONF_ENTITY_CATEGORY)
|
|
|
|
default_entity_id: str | None
|
|
if (default_entity_id := entity_config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
|
|
self.entity_id = default_entity_id
|
|
|
|
|
|
class KnxUiEntity(_KnxEntityBase):
|
|
"""Representation of a KNX UI entity."""
|
|
|
|
_attr_has_entity_name = True
|
|
|
|
def __init__(
|
|
self, knx_module: KNXModule, unique_id: str, entity_config: dict[str, Any]
|
|
) -> None:
|
|
"""Initialize the UI entity."""
|
|
self._knx_module = knx_module
|
|
|
|
self._attr_name = entity_config[CONF_NAME]
|
|
self._attr_unique_id = unique_id
|
|
if entity_category := entity_config.get(CONF_ENTITY_CATEGORY):
|
|
self._attr_entity_category = EntityCategory(entity_category)
|
|
if device_info := entity_config.get(CONF_DEVICE_INFO):
|
|
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)})
|