diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 709a82b31fd..fd46cad8489 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -302,7 +302,7 @@ class KNXModule: self.entry = entry self.project = KNXProject(hass=hass, entry=entry) - self.config_store = KNXConfigStore(hass=hass, entry=entry) + self.config_store = KNXConfigStore(hass=hass, config_entry=entry) self.xknx = XKNX( connection_config=self.connection_config(), diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 0423c1d7b32..ff15f725fae 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Any -from xknx import XKNX from xknx.devices import BinarySensor as XknxBinarySensor from homeassistant import config_entries @@ -23,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ATTR_COUNTER, ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxEntity from .schema import BinarySensorSchema @@ -34,11 +34,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the KNX binary sensor platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: ConfigType = hass.data[DATA_KNX_CONFIG] async_add_entities( - KNXBinarySensor(xknx, entity_config) + KNXBinarySensor(knx_module, entity_config) for entity_config in config[Platform.BINARY_SENSOR] ) @@ -48,11 +48,12 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity): _device: XknxBinarySensor - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of KNX binary sensor.""" super().__init__( + knx_module=knx_module, device=XknxBinarySensor( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address_state=config[BinarySensorSchema.CONF_STATE_ADDRESS], invert=config[BinarySensorSchema.CONF_INVERT], @@ -62,7 +63,7 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity): ], context_timeout=config.get(BinarySensorSchema.CONF_CONTEXT_TIMEOUT), reset_after=config.get(BinarySensorSchema.CONF_RESET_AFTER), - ) + ), ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_device_class = config.get(CONF_DEVICE_CLASS) diff --git a/homeassistant/components/knx/button.py b/homeassistant/components/knx/button.py index a38d8ad1b6c..2eb68eebe43 100644 --- a/homeassistant/components/knx/button.py +++ b/homeassistant/components/knx/button.py @@ -2,7 +2,6 @@ from __future__ import annotations -from xknx import XKNX from xknx.devices import RawValue as XknxRawValue from homeassistant import config_entries @@ -12,6 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import CONF_PAYLOAD_LENGTH, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity @@ -22,11 +22,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the KNX binary sensor platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: ConfigType = hass.data[DATA_KNX_CONFIG] async_add_entities( - KNXButton(xknx, entity_config) for entity_config in config[Platform.BUTTON] + KNXButton(knx_module, entity_config) + for entity_config in config[Platform.BUTTON] ) @@ -35,15 +36,16 @@ class KNXButton(KnxEntity, ButtonEntity): _device: XknxRawValue - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX button.""" super().__init__( + knx_module=knx_module, device=XknxRawValue( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], payload_length=config[CONF_PAYLOAD_LENGTH], group_address=config[KNX_ADDRESS], - ) + ), ) self._payload = config[CONF_PAYLOAD] self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 26be6a03a79..7470d60ef4b 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -27,6 +27,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONTROLLER_MODES, CURRENT_HVAC_ACTIONS, @@ -48,10 +49,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up climate(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.CLIMATE] - async_add_entities(KNXClimate(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXClimate(knx_module, entity_config) for entity_config in config + ) def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate: @@ -137,9 +140,12 @@ class KNXClimate(KnxEntity, ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _enable_turn_on_off_backwards_compatibility = False - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of a KNX climate device.""" - super().__init__(_create_climate(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_climate(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE if self._device.supports_on_off: diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 9d86d6ac272..1962db0ad3f 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import Callable from typing import Any -from xknx import XKNX from xknx.devices import Cover as XknxCover from homeassistant import config_entries @@ -26,6 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxEntity from .schema import CoverSchema @@ -37,10 +37,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up cover(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.COVER] - async_add_entities(KNXCover(xknx, entity_config) for entity_config in config) + async_add_entities(KNXCover(knx_module, entity_config) for entity_config in config) class KNXCover(KnxEntity, CoverEntity): @@ -48,11 +48,12 @@ class KNXCover(KnxEntity, CoverEntity): _device: XknxCover - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize the cover.""" super().__init__( + knx_module=knx_module, device=XknxCover( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address_long=config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS), group_address_short=config.get(CoverSchema.CONF_MOVE_SHORT_ADDRESS), @@ -70,7 +71,7 @@ class KNXCover(KnxEntity, CoverEntity): invert_updown=config[CoverSchema.CONF_INVERT_UPDOWN], invert_position=config[CoverSchema.CONF_INVERT_POSITION], invert_angle=config[CoverSchema.CONF_INVERT_ANGLE], - ) + ), ) self._unsubscribe_auto_updater: Callable[[], None] | None = None diff --git a/homeassistant/components/knx/date.py b/homeassistant/components/knx/date.py index 98cd22e0751..80fea63d0a6 100644 --- a/homeassistant/components/knx/date.py +++ b/homeassistant/components/knx/date.py @@ -22,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -39,10 +40,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.DATE] - async_add_entities(KNXDateEntity(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXDateEntity(knx_module, entity_config) for entity_config in config + ) def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateDevice: @@ -63,9 +66,12 @@ class KNXDateEntity(KnxEntity, DateEntity, RestoreEntity): _device: XknxDateDevice - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX time.""" - super().__init__(_create_xknx_device(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_xknx_device(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) diff --git a/homeassistant/components/knx/datetime.py b/homeassistant/components/knx/datetime.py index d4a25b522eb..16ccb7474a7 100644 --- a/homeassistant/components/knx/datetime.py +++ b/homeassistant/components/knx/datetime.py @@ -23,6 +23,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -40,11 +41,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.DATETIME] async_add_entities( - KNXDateTimeEntity(xknx, entity_config) for entity_config in config + KNXDateTimeEntity(knx_module, entity_config) for entity_config in config ) @@ -66,9 +67,12 @@ class KNXDateTimeEntity(KnxEntity, DateTimeEntity, RestoreEntity): _device: XknxDateTimeDevice - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX time.""" - super().__init__(_create_xknx_device(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_xknx_device(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 426a750f766..940e241ccda 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -5,7 +5,6 @@ from __future__ import annotations import math from typing import Any, Final -from xknx import XKNX from xknx.devices import Fan as XknxFan from homeassistant import config_entries @@ -20,6 +19,7 @@ from homeassistant.util.percentage import ( ) from homeassistant.util.scaling import int_states_in_range +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import FanSchema @@ -33,10 +33,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up fan(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.FAN] - async_add_entities(KNXFan(xknx, entity_config) for entity_config in config) + async_add_entities(KNXFan(knx_module, entity_config) for entity_config in config) class KNXFan(KnxEntity, FanEntity): @@ -45,12 +45,13 @@ class KNXFan(KnxEntity, FanEntity): _device: XknxFan _enable_turn_on_off_backwards_compatibility = False - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of KNX fan.""" max_step = config.get(FanSchema.CONF_MAX_STEP) super().__init__( + knx_module=knx_module, device=XknxFan( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address_speed=config.get(KNX_ADDRESS), group_address_speed_state=config.get(FanSchema.CONF_STATE_ADDRESS), @@ -61,7 +62,7 @@ class KNXFan(KnxEntity, FanEntity): FanSchema.CONF_OSCILLATION_STATE_ADDRESS ), max_step=max_step, - ) + ), ) # FanSpeedMode.STEP if max_step is set self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index eebddbb0623..2b8d2e71186 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -2,23 +2,29 @@ from __future__ import annotations -from typing import cast +from typing import TYPE_CHECKING from xknx.devices import Device as XknxDevice +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import KNXModule from .const import DOMAIN +if TYPE_CHECKING: + from . import KNXModule + +SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal.{{}}" + class KnxEntity(Entity): """Representation of a KNX entity.""" _attr_should_poll = False - def __init__(self, device: XknxDevice) -> None: + def __init__(self, knx_module: KNXModule, device: XknxDevice) -> None: """Set up device.""" + self._knx_module = knx_module self._device = device @property @@ -29,8 +35,7 @@ class KnxEntity(Entity): @property def available(self) -> bool: """Return True if entity is available.""" - knx_module = cast(KNXModule, self.hass.data[DOMAIN]) - return knx_module.connected + return self._knx_module.connected async def async_update(self) -> None: """Request a state update from KNX bus.""" @@ -44,8 +49,29 @@ class KnxEntity(Entity): """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) + # super call needed to have methods of mulit-inherited classes called + # eg. for restoring state (like _KNXSwitch) + await super().async_added_to_hass() 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) + + +class KnxUIEntity(KnxEntity): + """Representation of a KNX UI entity.""" + + _attr_unique_id: str + + async def async_added_to_hass(self) -> None: + """Register callbacks when entity added to hass.""" + await super().async_added_to_hass() + self._knx_module.config_store.entities.add(self._attr_unique_id) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_ENTITY_REMOVE.format(self._attr_unique_id), + self.async_remove, + ) + ) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 8ec42f3ee56..1197f09354b 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -27,7 +27,7 @@ import homeassistant.util.color as color_util from . import KNXModule from .const import CONF_SYNC_STATE, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, ColorTempModes -from .knx_entity import KnxEntity +from .knx_entity import KnxEntity, KnxUIEntity from .schema import LightSchema from .storage.const import ( CONF_COLOR_TEMP_MAX, @@ -65,10 +65,10 @@ async def async_setup_entry( knx_module: KNXModule = hass.data[DOMAIN] entities: list[KnxEntity] = [] - if yaml_config := hass.data[DATA_KNX_CONFIG].get(Platform.LIGHT): + if yaml_platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.LIGHT): entities.extend( - KnxYamlLight(knx_module.xknx, entity_config) - for entity_config in yaml_config + KnxYamlLight(knx_module, entity_config) + for entity_config in yaml_platform_config ) if ui_config := knx_module.config_store.data["entities"].get(Platform.LIGHT): entities.extend( @@ -294,7 +294,7 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight ) -class _KnxLight(KnxEntity, LightEntity): +class _KnxLight(LightEntity): """Representation of a KNX light.""" _attr_max_color_temp_kelvin: int @@ -519,14 +519,17 @@ class _KnxLight(KnxEntity, LightEntity): await self._device.set_off() -class KnxYamlLight(_KnxLight): +class KnxYamlLight(_KnxLight, KnxEntity): """Representation of a KNX light.""" _device: XknxLight - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of KNX light.""" - super().__init__(_create_yaml_light(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_yaml_light(knx_module.xknx, config), + ) self._attr_max_color_temp_kelvin: int = config[LightSchema.CONF_MAX_KELVIN] self._attr_min_color_temp_kelvin: int = config[LightSchema.CONF_MIN_KELVIN] self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) @@ -543,20 +546,21 @@ class KnxYamlLight(_KnxLight): ) -class KnxUiLight(_KnxLight): +class KnxUiLight(_KnxLight, KnxUIEntity): """Representation of a KNX light.""" - _device: XknxLight _attr_has_entity_name = True + _device: XknxLight def __init__( self, knx_module: KNXModule, unique_id: str, config: ConfigType ) -> None: """Initialize of KNX light.""" super().__init__( - _create_ui_light( + knx_module=knx_module, + device=_create_ui_light( knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME] - ) + ), ) self._attr_max_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MAX] self._attr_min_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MIN] @@ -565,5 +569,3 @@ class KnxUiLight(_KnxLight): self._attr_unique_id = unique_id if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO): self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)}) - - knx_module.config_store.entities[unique_id] = self diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 997bdb81057..b349681990c 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity @@ -44,7 +45,7 @@ async def async_get_service( class KNXNotificationService(BaseNotificationService): - """Implement demo notification service.""" + """Implement notification service.""" def __init__(self, devices: list[XknxNotification]) -> None: """Initialize the service.""" @@ -86,10 +87,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up notify(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.NOTIFY] - async_add_entities(KNXNotify(xknx, entity_config) for entity_config in config) + async_add_entities(KNXNotify(knx_module, entity_config) for entity_config in config) def _create_notification_instance(xknx: XKNX, config: ConfigType) -> XknxNotification: @@ -107,9 +108,12 @@ class KNXNotify(KnxEntity, NotifyEntity): _device: XknxNotification - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX notification.""" - super().__init__(_create_notification_instance(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_notification_instance(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index 8a9f1dea87c..3d4af503dff 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -22,6 +22,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -39,10 +40,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up number(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.NUMBER] - async_add_entities(KNXNumber(xknx, entity_config) for entity_config in config) + async_add_entities(KNXNumber(knx_module, entity_config) for entity_config in config) def _create_numeric_value(xknx: XKNX, config: ConfigType) -> NumericValue: @@ -62,9 +63,12 @@ class KNXNumber(KnxEntity, RestoreNumber): _device: NumericValue - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX number.""" - super().__init__(_create_numeric_value(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_numeric_value(knx_module.xknx, config), + ) self._attr_native_max_value = config.get( NumberSchema.CONF_MAX, self._device.sensor_value.dpt_class.value_max, diff --git a/homeassistant/components/knx/project.py b/homeassistant/components/knx/project.py index 3b3309dfc7d..b5bafe00724 100644 --- a/homeassistant/components/knx/project.py +++ b/homeassistant/components/knx/project.py @@ -8,9 +8,11 @@ from typing import Final from xknx import XKNX from xknx.dpt import DPTBase +from xknx.telegram.address import DeviceAddressableType from xknxproject import XKNXProj from xknxproject.models import ( Device, + DPTType, GroupAddress as GroupAddressModel, KNXProject as KNXProjectModel, ProjectInfo, @@ -89,7 +91,7 @@ class KNXProject: self.devices = project["devices"] self.info = project["info"] xknx.group_address_dpt.clear() - xknx_ga_dict = {} + xknx_ga_dict: dict[DeviceAddressableType, DPTType] = {} for ga_model in project["group_addresses"].values(): ga_info = _create_group_address_info(ga_model) @@ -97,7 +99,7 @@ class KNXProject: if (dpt_model := ga_model.get("dpt")) is not None: xknx_ga_dict[ga_model["address"]] = dpt_model - xknx.group_address_dpt.set(xknx_ga_dict) # type: ignore[arg-type] + xknx.group_address_dpt.set(xknx_ga_dict) _LOGGER.debug( "Loaded KNX project data with %s group addresses from storage", diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index 342d0f9eb83..fc37f36dd01 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Any -from xknx import XKNX from xknx.devices import Scene as XknxScene from homeassistant import config_entries @@ -14,6 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import SceneSchema @@ -25,10 +25,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up scene(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.SCENE] - async_add_entities(KNXScene(xknx, entity_config) for entity_config in config) + async_add_entities(KNXScene(knx_module, entity_config) for entity_config in config) class KNXScene(KnxEntity, Scene): @@ -36,15 +36,16 @@ class KNXScene(KnxEntity, Scene): _device: XknxScene - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Init KNX scene.""" super().__init__( + knx_module=knx_module, device=XknxScene( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address=config[KNX_ADDRESS], scene_number=config[SceneSchema.CONF_SCENE_NUMBER], - ) + ), ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = ( diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py index f338bf9feaf..1b862010c2a 100644 --- a/homeassistant/components/knx/select.py +++ b/homeassistant/components/knx/select.py @@ -20,6 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_PAYLOAD_LENGTH, CONF_RESPOND_TO_READ, @@ -39,10 +40,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up select(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.SELECT] - async_add_entities(KNXSelect(xknx, entity_config) for entity_config in config) + async_add_entities(KNXSelect(knx_module, entity_config) for entity_config in config) def _create_raw_value(xknx: XKNX, config: ConfigType) -> RawValue: @@ -63,9 +64,12 @@ class KNXSelect(KnxEntity, SelectEntity, RestoreEntity): _device: RawValue - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX select.""" - super().__init__(_create_raw_value(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_raw_value(knx_module.xknx, config), + ) self._option_payloads: dict[str, int] = { option[SelectSchema.CONF_OPTION]: option[CONF_PAYLOAD] for option in config[SelectSchema.CONF_OPTIONS] diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 5a09a921901..ab363e2a35f 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -116,17 +116,17 @@ async def async_setup_entry( ) -> None: """Set up sensor(s) for KNX platform.""" knx_module: KNXModule = hass.data[DOMAIN] - - async_add_entities( + entities: list[SensorEntity] = [] + entities.extend( KNXSystemSensor(knx_module, description) for description in SYSTEM_ENTITY_DESCRIPTIONS ) - config: list[ConfigType] = hass.data[DATA_KNX_CONFIG].get(Platform.SENSOR) if config: - async_add_entities( - KNXSensor(knx_module.xknx, entity_config) for entity_config in config + entities.extend( + KNXSensor(knx_module, entity_config) for entity_config in config ) + async_add_entities(entities) def _create_sensor(xknx: XKNX, config: ConfigType) -> XknxSensor: @@ -146,9 +146,12 @@ class KNXSensor(KnxEntity, SensorEntity): _device: XknxSensor - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of a KNX sensor.""" - super().__init__(_create_sensor(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_sensor(knx_module.xknx, config), + ) if device_class := config.get(CONF_DEVICE_CLASS): self._attr_device_class = device_class else: diff --git a/homeassistant/components/knx/storage/config_store.py b/homeassistant/components/knx/storage/config_store.py index 7ea61e1dd3e..876fe19a4b9 100644 --- a/homeassistant/components/knx/storage/config_store.py +++ b/homeassistant/components/knx/storage/config_store.py @@ -2,21 +2,20 @@ from collections.abc import Callable import logging -from typing import TYPE_CHECKING, Any, Final, TypedDict +from typing import Any, Final, TypedDict from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PLATFORM, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store from homeassistant.util.ulid import ulid_now from ..const import DOMAIN +from ..knx_entity import SIGNAL_ENTITY_REMOVE from .const import CONF_DATA -if TYPE_CHECKING: - from ..knx_entity import KnxEntity - _LOGGER = logging.getLogger(__name__) STORAGE_VERSION: Final = 1 @@ -40,15 +39,16 @@ class KNXConfigStore: def __init__( self, hass: HomeAssistant, - entry: ConfigEntry, + config_entry: ConfigEntry, ) -> None: """Initialize config store.""" self.hass = hass + self.config_entry = config_entry self._store = Store[KNXConfigStoreModel](hass, STORAGE_VERSION, STORAGE_KEY) self.data = KNXConfigStoreModel(entities={}) - # entities and async_add_entity are filled by platform setups - self.entities: dict[str, KnxEntity] = {} # unique_id as key + # entities and async_add_entity are filled by platform / entity setups + self.entities: set[str] = set() # unique_id as values self.async_add_entity: dict[ Platform, Callable[[str, dict[str, Any]], None] ] = {} @@ -108,7 +108,7 @@ class KNXConfigStore: raise ConfigStoreException( f"Entity not found in storage: {entity_id} - {unique_id}" ) - await self.entities.pop(unique_id).async_remove() + async_dispatcher_send(self.hass, SIGNAL_ENTITY_REMOVE.format(unique_id)) self.async_add_entity[platform](unique_id, data) # store data after entity is added to make sure config doesn't raise exceptions self.data["entities"][platform][unique_id] = data @@ -126,7 +126,7 @@ class KNXConfigStore: f"Entity not found in {entry.domain}: {entry.unique_id}" ) from err try: - del self.entities[entry.unique_id] + self.entities.remove(entry.unique_id) except KeyError: _LOGGER.warning("Entity not initialized when deleted: %s", entity_id) entity_registry.async_remove(entity_id) @@ -134,10 +134,14 @@ class KNXConfigStore: def get_entity_entries(self) -> list[er.RegistryEntry]: """Get entity_ids of all configured entities by platform.""" + entity_registry = er.async_get(self.hass) + return [ - entity.registry_entry - for entity in self.entities.values() - if entity.registry_entry is not None + registry_entry + for registry_entry in er.async_entries_for_config_entry( + entity_registry, self.config_entry.entry_id + ) + if registry_entry.unique_id in self.entities ] diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 0a8a1dff964..a5f430e6157 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Any -from xknx import XKNX from xknx.devices import Switch as XknxSwitch from homeassistant import config_entries @@ -33,7 +32,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxEntity, KnxUIEntity from .schema import SwitchSchema from .storage.const import ( CONF_DEVICE_INFO, @@ -54,10 +53,10 @@ async def async_setup_entry( knx_module: KNXModule = hass.data[DOMAIN] entities: list[KnxEntity] = [] - if yaml_config := hass.data[DATA_KNX_CONFIG].get(Platform.SWITCH): + if yaml_platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.SWITCH): entities.extend( - KnxYamlSwitch(knx_module.xknx, entity_config) - for entity_config in yaml_config + KnxYamlSwitch(knx_module, entity_config) + for entity_config in yaml_platform_config ) if ui_config := knx_module.config_store.data["entities"].get(Platform.SWITCH): entities.extend( @@ -75,7 +74,7 @@ async def async_setup_entry( knx_module.config_store.async_add_entity[Platform.SWITCH] = add_new_ui_switch -class _KnxSwitch(KnxEntity, SwitchEntity, RestoreEntity): +class _KnxSwitch(SwitchEntity, RestoreEntity): """Base class for a KNX switch.""" _device: XknxSwitch @@ -103,36 +102,41 @@ class _KnxSwitch(KnxEntity, SwitchEntity, RestoreEntity): await self._device.set_off() -class KnxYamlSwitch(_KnxSwitch): +class KnxYamlSwitch(_KnxSwitch, KnxEntity): """Representation of a KNX switch configured from YAML.""" - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + _device: XknxSwitch + + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of KNX switch.""" super().__init__( + knx_module=knx_module, device=XknxSwitch( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address=config[KNX_ADDRESS], group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS), respond_to_read=config[CONF_RESPOND_TO_READ], invert=config[SwitchSchema.CONF_INVERT], - ) + ), ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_device_class = config.get(CONF_DEVICE_CLASS) self._attr_unique_id = str(self._device.switch.group_address) -class KnxUiSwitch(_KnxSwitch): +class KnxUiSwitch(_KnxSwitch, KnxUIEntity): """Representation of a KNX switch configured from UI.""" _attr_has_entity_name = True + _device: XknxSwitch def __init__( self, knx_module: KNXModule, unique_id: str, config: dict[str, Any] ) -> None: """Initialize of KNX switch.""" super().__init__( + knx_module=knx_module, device=XknxSwitch( knx_module.xknx, name=config[CONF_ENTITY][CONF_NAME], @@ -144,11 +148,9 @@ class KnxUiSwitch(_KnxSwitch): respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ], sync_state=config[DOMAIN][CONF_SYNC_STATE], invert=config[DOMAIN][CONF_INVERT], - ) + ), ) self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY] self._attr_unique_id = unique_id if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO): self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)}) - - knx_module.config_store.entities[unique_id] = self diff --git a/homeassistant/components/knx/text.py b/homeassistant/components/knx/text.py index 22d008cd5ce..9bca37434ac 100644 --- a/homeassistant/components/knx/text.py +++ b/homeassistant/components/knx/text.py @@ -22,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -38,10 +39,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensor(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.TEXT] - async_add_entities(KNXText(xknx, entity_config) for entity_config in config) + async_add_entities(KNXText(knx_module, entity_config) for entity_config in config) def _create_notification(xknx: XKNX, config: ConfigType) -> XknxNotification: @@ -62,9 +63,12 @@ class KNXText(KnxEntity, TextEntity, RestoreEntity): _device: XknxNotification _attr_native_max = 14 - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX text.""" - super().__init__(_create_notification(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_notification(knx_module.xknx, config), + ) self._attr_mode = config[CONF_MODE] self._attr_pattern = ( r"[\u0000-\u00ff]*" # Latin-1 diff --git a/homeassistant/components/knx/time.py b/homeassistant/components/knx/time.py index 28e1419233c..5d9225a1e41 100644 --- a/homeassistant/components/knx/time.py +++ b/homeassistant/components/knx/time.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import time as dt_time -from typing import Final from xknx import XKNX from xknx.devices import TimeDevice as XknxTimeDevice @@ -23,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -33,8 +33,6 @@ from .const import ( ) from .knx_entity import KnxEntity -_TIME_TRANSLATION_FORMAT: Final = "%H:%M:%S" - async def async_setup_entry( hass: HomeAssistant, @@ -42,10 +40,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.TIME] - async_add_entities(KNXTimeEntity(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXTimeEntity(knx_module, entity_config) for entity_config in config + ) def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxTimeDevice: @@ -66,9 +66,12 @@ class KNXTimeEntity(KnxEntity, TimeEntity, RestoreEntity): _device: XknxTimeDevice - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX time.""" - super().__init__(_create_xknx_device(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_xknx_device(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 584c9fd3323..11dae452e2f 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -19,6 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxEntity from .schema import WeatherSchema @@ -30,10 +31,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up switch(es) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.WEATHER] - async_add_entities(KNXWeather(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXWeather(knx_module, entity_config) for entity_config in config + ) def _create_weather(xknx: XKNX, config: ConfigType) -> XknxWeather: @@ -80,9 +83,12 @@ class KNXWeather(KnxEntity, WeatherEntity): _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of a KNX sensor.""" - super().__init__(_create_weather(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_weather(knx_module.xknx, config), + ) self._attr_unique_id = str(self._device._temperature.group_address_state) # noqa: SLF001 self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)