From 0d795aad16fa095f93f305e428ec26d7dadf93e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Sun, 6 Oct 2024 12:40:13 +0200 Subject: [PATCH] Use BSH keys as unique ID's suffix at Home Connect (#126143) * Use BSH keys as as unique id suffix instead of the simple description * Update tests/components/home_connect/test_init.py --------- Co-authored-by: Joost Lekkerkerker --- .../components/home_connect/__init__.py | 33 +++++++- homeassistant/components/home_connect/api.py | 57 +++++++++++-- .../components/home_connect/binary_sensor.py | 25 +++--- .../components/home_connect/config_flow.py | 2 + .../components/home_connect/const.py | 32 +++++++ .../components/home_connect/entity.py | 5 +- .../components/home_connect/light.py | 39 ++++----- .../components/home_connect/sensor.py | 53 ++++++------ .../components/home_connect/switch.py | 31 ++++--- tests/components/home_connect/conftest.py | 14 ++++ tests/components/home_connect/test_init.py | 84 ++++++++++++++++++- 11 files changed, 283 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 5f07b8075ce..87f4bfa7799 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -4,18 +4,20 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from requests import HTTPError import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DEVICE_ID, CONF_DEVICE, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, device_registry as dr, ) +from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle @@ -28,6 +30,7 @@ from .const import ( BSH_PAUSE, BSH_RESUME, DOMAIN, + OLD_NEW_UNIQUE_ID_SUFFIX_MAP, SERVICE_OPTION_ACTIVE, SERVICE_OPTION_SELECTED, SERVICE_PAUSE_PROGRAM, @@ -268,3 +271,31 @@ async def update_all_devices(hass: HomeAssistant, entry: ConfigEntry) -> None: await hass.async_add_executor_job(device.initialize) except HTTPError as err: _LOGGER.warning("Cannot update devices: %s", err.response.status_code) + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + + if config_entry.version == 1 and config_entry.minor_version == 1: + + @callback + def update_unique_id( + entity_entry: RegistryEntry, + ) -> dict[str, Any] | None: + """Update unique ID of entity entry.""" + for old_id_suffix, new_id_suffix in OLD_NEW_UNIQUE_ID_SUFFIX_MAP.items(): + if entity_entry.unique_id.endswith(f"-{old_id_suffix}"): + return { + "new_unique_id": entity_entry.unique_id.replace( + old_id_suffix, new_id_suffix + ) + } + return None + + await async_migrate_entries(hass, config_entry.entry_id, update_unique_id) + + hass.config_entries.async_update_entry(config_entry, minor_version=2) + + _LOGGER.debug("Migration to version %s successful", config_entry.version) + return True diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index f03093b46b9..4324edc8c1e 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -24,6 +24,7 @@ from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( ATTR_AMBIENT, + ATTR_BSH_KEY, ATTR_DESC, ATTR_DEVICE, ATTR_KEY, @@ -32,9 +33,16 @@ from .const import ( ATTR_UNIT, ATTR_VALUE, BSH_ACTIVE_PROGRAM, + BSH_AMBIENT_LIGHT_ENABLED, + BSH_COMMON_OPTION_DURATION, + BSH_COMMON_OPTION_PROGRAM_PROGRESS, BSH_OPERATION_STATE, BSH_POWER_OFF, BSH_POWER_STANDBY, + BSH_REMAINING_PROGRAM_TIME, + BSH_REMOTE_CONTROL_ACTIVATION_STATE, + BSH_REMOTE_START_ALLOWANCE_STATE, + COOKING_LIGHTING, SIGNAL_UPDATE_ENTITIES, ) @@ -181,21 +189,39 @@ class DeviceWithPrograms(HomeConnectDevice): device. """ sensors = { - "Remaining Program Time": (None, None, SensorDeviceClass.TIMESTAMP, 1), - "Duration": (UnitOfTime.SECONDS, "mdi:update", None, 1), - "Program Progress": (PERCENTAGE, "mdi:progress-clock", None, 1), + BSH_REMAINING_PROGRAM_TIME: ( + "Remaining Program Time", + None, + None, + SensorDeviceClass.TIMESTAMP, + 1, + ), + BSH_COMMON_OPTION_DURATION: ( + "Duration", + UnitOfTime.SECONDS, + "mdi:update", + None, + 1, + ), + BSH_COMMON_OPTION_PROGRAM_PROGRESS: ( + "Program Progress", + PERCENTAGE, + "mdi:progress-clock", + None, + 1, + ), } return [ { ATTR_DEVICE: self, - ATTR_DESC: k, + ATTR_BSH_KEY: k, + ATTR_DESC: desc, ATTR_UNIT: unit, - ATTR_KEY: f"BSH.Common.Option.{k.replace(' ', '')}", ATTR_ICON: icon, ATTR_DEVICE_CLASS: device_class, ATTR_SIGN: sign, } - for k, (unit, icon, device_class, sign) in sensors.items() + for k, (desc, unit, icon, device_class, sign) in sensors.items() ] @@ -208,9 +234,9 @@ class DeviceWithOpState(HomeConnectDevice): return [ { ATTR_DEVICE: self, + ATTR_BSH_KEY: BSH_OPERATION_STATE, ATTR_DESC: "Operation State", ATTR_UNIT: None, - ATTR_KEY: BSH_OPERATION_STATE, ATTR_ICON: "mdi:state-machine", ATTR_DEVICE_CLASS: None, ATTR_SIGN: 1, @@ -225,6 +251,7 @@ class DeviceWithDoor(HomeConnectDevice): """Get a dictionary with info about the door binary sensor.""" return { ATTR_DEVICE: self, + ATTR_BSH_KEY: "Door", ATTR_DESC: "Door", ATTR_SENSOR_TYPE: "door", ATTR_DEVICE_CLASS: "door", @@ -236,7 +263,12 @@ class DeviceWithLight(HomeConnectDevice): def get_light_entity(self) -> dict[str, Any]: """Get a dictionary with info about the lighting.""" - return {ATTR_DEVICE: self, ATTR_DESC: "Light", ATTR_AMBIENT: None} + return { + ATTR_DEVICE: self, + ATTR_BSH_KEY: COOKING_LIGHTING, + ATTR_DESC: "Light", + ATTR_AMBIENT: None, + } class DeviceWithAmbientLight(HomeConnectDevice): @@ -244,7 +276,12 @@ class DeviceWithAmbientLight(HomeConnectDevice): def get_ambientlight_entity(self) -> dict[str, Any]: """Get a dictionary with info about the ambient lighting.""" - return {ATTR_DEVICE: self, ATTR_DESC: "AmbientLight", ATTR_AMBIENT: True} + return { + ATTR_DEVICE: self, + ATTR_BSH_KEY: BSH_AMBIENT_LIGHT_ENABLED, + ATTR_DESC: "AmbientLight", + ATTR_AMBIENT: True, + } class DeviceWithRemoteControl(HomeConnectDevice): @@ -254,6 +291,7 @@ class DeviceWithRemoteControl(HomeConnectDevice): """Get a dictionary with info about the remote control sensor.""" return { ATTR_DEVICE: self, + ATTR_BSH_KEY: BSH_REMOTE_CONTROL_ACTIVATION_STATE, ATTR_DESC: "Remote Control", ATTR_SENSOR_TYPE: "remote_control", } @@ -266,6 +304,7 @@ class DeviceWithRemoteStart(HomeConnectDevice): """Get a dictionary with info about the remote start sensor.""" return { ATTR_DEVICE: self, + ATTR_BSH_KEY: BSH_REMOTE_START_ALLOWANCE_STATE, ATTR_DESC: "Remote Start", ATTR_SENSOR_TYPE: "remote_start", } diff --git a/homeassistant/components/home_connect/binary_sensor.py b/homeassistant/components/home_connect/binary_sensor.py index c6c43a3119c..7c99ee5421f 100644 --- a/homeassistant/components/home_connect/binary_sensor.py +++ b/homeassistant/components/home_connect/binary_sensor.py @@ -39,7 +39,7 @@ _LOGGER = logging.getLogger(__name__) class HomeConnectBinarySensorEntityDescription(BinarySensorEntityDescription): """Entity Description class for binary sensors.""" - state_key: str | None + desc: str device_class: BinarySensorDeviceClass | None = BinarySensorDeviceClass.DOOR boolean_map: dict[str, bool] = field( default_factory=lambda: { @@ -51,16 +51,16 @@ class HomeConnectBinarySensorEntityDescription(BinarySensorEntityDescription): BINARY_SENSORS: tuple[HomeConnectBinarySensorEntityDescription, ...] = ( HomeConnectBinarySensorEntityDescription( - key="Chiller Door", - state_key=REFRIGERATION_STATUS_DOOR_CHILLER, + key=REFRIGERATION_STATUS_DOOR_CHILLER, + desc="Chiller Door", ), HomeConnectBinarySensorEntityDescription( - key="Freezer Door", - state_key=REFRIGERATION_STATUS_DOOR_FREEZER, + key=REFRIGERATION_STATUS_DOOR_FREEZER, + desc="Freezer Door", ), HomeConnectBinarySensorEntityDescription( - key="Refrigerator Door", - state_key=REFRIGERATION_STATUS_DOOR_REFRIGERATOR, + key=REFRIGERATION_STATUS_DOOR_REFRIGERATOR, + desc="Refrigerator Door", ), ) @@ -85,7 +85,7 @@ async def async_setup_entry( device=device, entity_description=description ) for description in BINARY_SENSORS - if description.state_key in device.appliance.status + if description.key in device.appliance.status ) return entities @@ -98,12 +98,13 @@ class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity): def __init__( self, device: HomeConnectDevice, + bsh_key: str, desc: str, sensor_type: str, device_class: BinarySensorDeviceClass | None = None, ) -> None: """Initialize the entity.""" - super().__init__(device, desc) + super().__init__(device, bsh_key, desc) self._attr_device_class = device_class self._type = sensor_type self._false_value_list = None @@ -162,7 +163,7 @@ class HomeConnectFridgeDoorBinarySensor(HomeConnectEntity, BinarySensorEntity): ) -> None: """Initialize the entity.""" self.entity_description = entity_description - super().__init__(device, entity_description.key) + super().__init__(device, entity_description.key, entity_description.desc) async def async_update(self) -> None: """Update the binary sensor's status.""" @@ -172,9 +173,7 @@ class HomeConnectFridgeDoorBinarySensor(HomeConnectEntity, BinarySensorEntity): self.state, ) self._attr_is_on = self.entity_description.boolean_map.get( - self.device.appliance.status.get(self.entity_description.state_key, {}).get( - ATTR_VALUE - ) + self.device.appliance.status.get(self.bsh_key, {}).get(ATTR_VALUE) ) self._attr_available = self._attr_is_on is not None _LOGGER.debug( diff --git a/homeassistant/components/home_connect/config_flow.py b/homeassistant/components/home_connect/config_flow.py index f6616bf98ca..444ea24cb6b 100644 --- a/homeassistant/components/home_connect/config_flow.py +++ b/homeassistant/components/home_connect/config_flow.py @@ -14,6 +14,8 @@ class OAuth2FlowHandler( DOMAIN = DOMAIN + MINOR_VERSION = 2 + @property def logger(self) -> logging.Logger: """Return logger.""" diff --git a/homeassistant/components/home_connect/const.py b/homeassistant/components/home_connect/const.py index f86b43511ec..1da9e517ad5 100644 --- a/homeassistant/components/home_connect/const.py +++ b/homeassistant/components/home_connect/const.py @@ -14,6 +14,10 @@ BSH_REMOTE_CONTROL_ACTIVATION_STATE = "BSH.Common.Status.RemoteControlActive" BSH_REMOTE_START_ALLOWANCE_STATE = "BSH.Common.Status.RemoteControlStartAllowed" BSH_CHILD_LOCK_STATE = "BSH.Common.Setting.ChildLock" +BSH_REMAINING_PROGRAM_TIME = "BSH.Common.Option.RemainingProgramTime" +BSH_COMMON_OPTION_DURATION = "BSH.Common.Option.Duration" +BSH_COMMON_OPTION_PROGRAM_PROGRESS = "BSH.Common.Option.ProgramProgress" + BSH_EVENT_PRESENT_STATE_PRESENT = "BSH.Common.EnumType.EventPresentState.Present" BSH_EVENT_PRESENT_STATE_CONFIRMED = "BSH.Common.EnumType.EventPresentState.Confirmed" BSH_EVENT_PRESENT_STATE_OFF = "BSH.Common.EnumType.EventPresentState.Off" @@ -92,6 +96,7 @@ SERVICE_SETTING = "change_setting" SERVICE_START_PROGRAM = "start_program" ATTR_AMBIENT = "ambient" +ATTR_BSH_KEY = "bsh_key" ATTR_DESC = "desc" ATTR_DEVICE = "device" ATTR_KEY = "key" @@ -100,3 +105,30 @@ ATTR_SENSOR_TYPE = "sensor_type" ATTR_SIGN = "sign" ATTR_UNIT = "unit" ATTR_VALUE = "value" + +OLD_NEW_UNIQUE_ID_SUFFIX_MAP = { + "ChildLock": BSH_CHILD_LOCK_STATE, + "Operation State": BSH_OPERATION_STATE, + "Light": COOKING_LIGHTING, + "AmbientLight": BSH_AMBIENT_LIGHT_ENABLED, + "Power": BSH_POWER_STATE, + "Remaining Program Time": BSH_REMAINING_PROGRAM_TIME, + "Duration": BSH_COMMON_OPTION_DURATION, + "Program Progress": BSH_COMMON_OPTION_PROGRAM_PROGRESS, + "Remote Control": BSH_REMOTE_CONTROL_ACTIVATION_STATE, + "Remote Start": BSH_REMOTE_START_ALLOWANCE_STATE, + "Supermode Freezer": REFRIGERATION_SUPERMODEFREEZER, + "Supermode Refrigerator": REFRIGERATION_SUPERMODEREFRIGERATOR, + "Dispenser Enabled": REFRIGERATION_DISPENSER, + "Internal Light": REFRIGERATION_INTERNAL_LIGHT_POWER, + "External Light": REFRIGERATION_EXTERNAL_LIGHT_POWER, + "Chiller Door": REFRIGERATION_STATUS_DOOR_CHILLER, + "Freezer Door": REFRIGERATION_STATUS_DOOR_FREEZER, + "Refrigerator Door": REFRIGERATION_STATUS_DOOR_REFRIGERATOR, + "Door Alarm Freezer": REFRIGERATION_EVENT_DOOR_ALARM_FREEZER, + "Door Alarm Refrigerator": REFRIGERATION_EVENT_DOOR_ALARM_REFRIGERATOR, + "Temperature Alarm Freezer": REFRIGERATION_EVENT_TEMP_ALARM_FREEZER, + "Bean Container Empty": COFFEE_EVENT_BEAN_CONTAINER_EMPTY, + "Water Tank Empty": COFFEE_EVENT_WATER_TANK_EMPTY, + "Drip Tray Full": COFFEE_EVENT_DRIP_TRAY_FULL, +} diff --git a/homeassistant/components/home_connect/entity.py b/homeassistant/components/home_connect/entity.py index 4ed14cd99af..6cad310f76a 100644 --- a/homeassistant/components/home_connect/entity.py +++ b/homeassistant/components/home_connect/entity.py @@ -18,11 +18,12 @@ class HomeConnectEntity(Entity): _attr_should_poll = False - def __init__(self, device: HomeConnectDevice, desc: str) -> None: + def __init__(self, device: HomeConnectDevice, bsh_key: str, desc: str) -> None: """Initialize the entity.""" self.device = device + self.bsh_key = bsh_key self._attr_name = f"{device.appliance.name} {desc}" - self._attr_unique_id = f"{device.appliance.haId}-{desc}" + self._attr_unique_id = f"{device.appliance.haId}-{bsh_key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device.appliance.haId)}, manufacturer=device.appliance.brand, diff --git a/homeassistant/components/home_connect/light.py b/homeassistant/components/home_connect/light.py index b7696493baa..7f6ea1bb4be 100644 --- a/homeassistant/components/home_connect/light.py +++ b/homeassistant/components/home_connect/light.py @@ -27,8 +27,6 @@ from .const import ( BSH_AMBIENT_LIGHT_COLOR, BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR, BSH_AMBIENT_LIGHT_CUSTOM_COLOR, - BSH_AMBIENT_LIGHT_ENABLED, - COOKING_LIGHTING, COOKING_LIGHTING_BRIGHTNESS, DOMAIN, REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS, @@ -45,19 +43,19 @@ _LOGGER = logging.getLogger(__name__) class HomeConnectLightEntityDescription(LightEntityDescription): """Light entity description.""" - on_key: str + desc: str brightness_key: str | None LIGHTS: tuple[HomeConnectLightEntityDescription, ...] = ( HomeConnectLightEntityDescription( - key="Internal Light", - on_key=REFRIGERATION_INTERNAL_LIGHT_POWER, + key=REFRIGERATION_INTERNAL_LIGHT_POWER, + desc="Internal Light", brightness_key=REFRIGERATION_INTERNAL_LIGHT_BRIGHTNESS, ), HomeConnectLightEntityDescription( - key="External Light", - on_key=REFRIGERATION_EXTERNAL_LIGHT_POWER, + key=REFRIGERATION_EXTERNAL_LIGHT_POWER, + desc="External Light", brightness_key=REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS, ), ) @@ -86,7 +84,7 @@ async def async_setup_entry( entity_description=description, ) for description in LIGHTS - if description.on_key in device.appliance.status + if description.key in device.appliance.status ) entities.extend(entity_list) return entities @@ -97,9 +95,11 @@ async def async_setup_entry( class HomeConnectLight(HomeConnectEntity, LightEntity): """Light for Home Connect.""" - def __init__(self, device, desc, ambient) -> None: + def __init__( + self, device: HomeConnectDevice, bsh_key: str, desc: str, ambient: bool + ) -> None: """Initialize the entity.""" - super().__init__(device, desc) + super().__init__(device, bsh_key, desc) self._ambient = ambient self._percentage_scale = (10, 100) self._brightness_key: str | None @@ -107,14 +107,12 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): self._color_key: str | None if ambient: self._brightness_key = BSH_AMBIENT_LIGHT_BRIGHTNESS - self._key = BSH_AMBIENT_LIGHT_ENABLED self._custom_color_key = BSH_AMBIENT_LIGHT_CUSTOM_COLOR self._color_key = BSH_AMBIENT_LIGHT_COLOR self._attr_color_mode = ColorMode.HS self._attr_supported_color_modes = {ColorMode.HS} else: self._brightness_key = COOKING_LIGHTING_BRIGHTNESS - self._key = COOKING_LIGHTING self._custom_color_key = None self._color_key = None self._attr_color_mode = ColorMode.BRIGHTNESS @@ -126,7 +124,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): _LOGGER.debug("Switching ambient light on for: %s", self.name) try: await self.hass.async_add_executor_job( - self.device.appliance.set_setting, self._key, True + self.device.appliance.set_setting, self.bsh_key, True ) except HomeConnectError as err: _LOGGER.error("Error while trying to turn on ambient light: %s", err) @@ -189,7 +187,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): _LOGGER.debug("Switching light on for: %s", self.name) try: await self.hass.async_add_executor_job( - self.device.appliance.set_setting, self._key, True + self.device.appliance.set_setting, self.bsh_key, True ) except HomeConnectError as err: _LOGGER.error("Error while trying to turn on light: %s", err) @@ -201,7 +199,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): _LOGGER.debug("Switching light off for: %s", self.name) try: await self.hass.async_add_executor_job( - self.device.appliance.set_setting, self._key, False + self.device.appliance.set_setting, self.bsh_key, False ) except HomeConnectError as err: _LOGGER.error("Error while trying to turn off light: %s", err) @@ -209,9 +207,11 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): async def async_update(self) -> None: """Update the light's status.""" - if self.device.appliance.status.get(self._key, {}).get(ATTR_VALUE) is True: + if self.device.appliance.status.get(self.bsh_key, {}).get(ATTR_VALUE) is True: self._attr_is_on = True - elif self.device.appliance.status.get(self._key, {}).get(ATTR_VALUE) is False: + elif ( + self.device.appliance.status.get(self.bsh_key, {}).get(ATTR_VALUE) is False + ): self._attr_is_on = False else: self._attr_is_on = None @@ -255,8 +255,9 @@ class HomeConnectCoolingLight(HomeConnectLight): entity_description: HomeConnectLightEntityDescription, ) -> None: """Initialize Cooling Light Entity.""" - super().__init__(device, entity_description.key, ambient) + super().__init__( + device, entity_description.key, entity_description.desc, ambient + ) self.entity_description = entity_description - self._key = entity_description.on_key self._brightness_key = entity_description.brightness_key self._percentage_scale = (1, 100) diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index d1635a6bdfa..599156a6b3a 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -46,45 +46,39 @@ class HomeConnectSensorEntityDescription(SensorEntityDescription): options: list[str] | None = field( default_factory=lambda: ["confirmed", "off", "present"] ) - state_key: str + desc: str appliance_types: tuple[str, ...] SENSORS: tuple[HomeConnectSensorEntityDescription, ...] = ( HomeConnectSensorEntityDescription( - key="Door Alarm Freezer", - translation_key="alarm_sensor_freezer", - state_key=REFRIGERATION_EVENT_DOOR_ALARM_FREEZER, + key=REFRIGERATION_EVENT_DOOR_ALARM_FREEZER, + desc="Door Alarm Freezer", appliance_types=("FridgeFreezer", "Freezer"), ), HomeConnectSensorEntityDescription( - key="Door Alarm Refrigerator", - translation_key="alarm_sensor_fridge", - state_key=REFRIGERATION_EVENT_DOOR_ALARM_REFRIGERATOR, + key=REFRIGERATION_EVENT_DOOR_ALARM_REFRIGERATOR, + desc="Door Alarm Refrigerator", appliance_types=("FridgeFreezer", "Refrigerator"), ), HomeConnectSensorEntityDescription( - key="Temperature Alarm Freezer", - translation_key="alarm_sensor_temp", - state_key=REFRIGERATION_EVENT_TEMP_ALARM_FREEZER, + key=REFRIGERATION_EVENT_TEMP_ALARM_FREEZER, + desc="Temperature Alarm Freezer", appliance_types=("FridgeFreezer", "Freezer"), ), HomeConnectSensorEntityDescription( - key="Bean Container Empty", - translation_key="alarm_sensor_coffee_bean_container", - state_key=COFFEE_EVENT_BEAN_CONTAINER_EMPTY, + key=COFFEE_EVENT_BEAN_CONTAINER_EMPTY, + desc="Bean Container Empty", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( - key="Water Tank Empty", - translation_key="alarm_sensor_coffee_water_tank", - state_key=COFFEE_EVENT_WATER_TANK_EMPTY, + key=COFFEE_EVENT_WATER_TANK_EMPTY, + desc="Water Tank Empty", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( - key="Drip Tray Full", - translation_key="alarm_sensor_coffee_drip_tray", - state_key=COFFEE_EVENT_DRIP_TRAY_FULL, + key=COFFEE_EVENT_DRIP_TRAY_FULL, + desc="Drip Tray Full", appliance_types=("CoffeeMaker",), ), ) @@ -128,16 +122,15 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity): def __init__( self, device: HomeConnectDevice, + bsh_key: str, desc: str, - key: str, unit: str, icon: str, device_class: SensorDeviceClass, sign: int = 1, ) -> None: """Initialize the entity.""" - super().__init__(device, desc) - self._key = key + super().__init__(device, bsh_key, desc) self._sign = sign self._attr_native_unit_of_measurement = unit self._attr_icon = icon @@ -151,10 +144,10 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity): async def async_update(self) -> None: """Update the sensor's status.""" status = self.device.appliance.status - if self._key not in status: + if self.bsh_key not in status: self._attr_native_value = None elif self.device_class == SensorDeviceClass.TIMESTAMP: - if ATTR_VALUE not in status[self._key]: + if ATTR_VALUE not in status[self.bsh_key]: self._attr_native_value = None elif ( self._attr_native_value is not None @@ -175,13 +168,13 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity): BSH_OPERATION_STATE_FINISHED, ] ): - seconds = self._sign * float(status[self._key][ATTR_VALUE]) + seconds = self._sign * float(status[self.bsh_key][ATTR_VALUE]) self._attr_native_value = dt_util.utcnow() + timedelta(seconds=seconds) else: self._attr_native_value = None else: - self._attr_native_value = status[self._key].get(ATTR_VALUE) - if self._key == BSH_OPERATION_STATE: + self._attr_native_value = status[self.bsh_key].get(ATTR_VALUE) + if self.bsh_key == BSH_OPERATION_STATE: # Value comes back as an enum, we only really care about the # last part, so split it off # https://developer.home-connect.com/docs/status/operation_state @@ -203,7 +196,9 @@ class HomeConnectAlarmSensor(HomeConnectEntity, SensorEntity): ) -> None: """Initialize the entity.""" self.entity_description = entity_description - super().__init__(device, self.entity_description.key) + super().__init__( + device, self.entity_description.key, self.entity_description.desc + ) @property def available(self) -> bool: @@ -213,7 +208,7 @@ class HomeConnectAlarmSensor(HomeConnectEntity, SensorEntity): async def async_update(self) -> None: """Update the sensor's status.""" self._attr_native_value = ( - self.device.appliance.status.get(self.entity_description.state_key, {}) + self.device.appliance.status.get(self.bsh_key, {}) .get(ATTR_VALUE, BSH_EVENT_PRESENT_STATE_OFF) .rsplit(".", maxsplit=1)[-1] .lower() diff --git a/homeassistant/components/home_connect/switch.py b/homeassistant/components/home_connect/switch.py index 63eabc2e31e..6e96b371b82 100644 --- a/homeassistant/components/home_connect/switch.py +++ b/homeassistant/components/home_connect/switch.py @@ -34,22 +34,21 @@ _LOGGER = logging.getLogger(__name__) class HomeConnectSwitchEntityDescription(SwitchEntityDescription): """Switch entity description.""" - on_key: str + desc: str SWITCHES: tuple[HomeConnectSwitchEntityDescription, ...] = ( HomeConnectSwitchEntityDescription( - key="Supermode Freezer", - on_key=REFRIGERATION_SUPERMODEFREEZER, + key=REFRIGERATION_SUPERMODEFREEZER, + desc="Supermode Freezer", ), HomeConnectSwitchEntityDescription( - key="Supermode Refrigerator", - on_key=REFRIGERATION_SUPERMODEREFRIGERATOR, + key=REFRIGERATION_SUPERMODEREFRIGERATOR, + desc="Supermode Refrigerator", ), HomeConnectSwitchEntityDescription( - key="Dispenser Enabled", - on_key=REFRIGERATION_DISPENSER, - translation_key="refrigeration_dispenser", + key=REFRIGERATION_DISPENSER, + desc="Dispenser Enabled", ), ) @@ -75,7 +74,7 @@ async def async_setup_entry( entities.extend( HomeConnectSwitch(device=hc_device, entity_description=description) for description in SWITCHES - if description.on_key in hc_device.appliance.status + if description.key in hc_device.appliance.status ) return entities @@ -96,7 +95,7 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity): """Initialize the entity.""" self.entity_description = entity_description self._attr_available = False - super().__init__(device=device, desc=entity_description.key) + super().__init__(device, entity_description.key, entity_description.desc) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on setting.""" @@ -104,7 +103,7 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity): _LOGGER.debug("Turning on %s", self.entity_description.key) try: await self.hass.async_add_executor_job( - self.device.appliance.set_setting, self.entity_description.on_key, True + self.device.appliance.set_setting, self.entity_description.key, True ) except HomeConnectError as err: _LOGGER.error("Error while trying to turn on: %s", err) @@ -120,7 +119,7 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity): _LOGGER.debug("Turning off %s", self.entity_description.key) try: await self.hass.async_add_executor_job( - self.device.appliance.set_setting, self.entity_description.on_key, False + self.device.appliance.set_setting, self.entity_description.key, False ) except HomeConnectError as err: _LOGGER.error("Error while trying to turn off: %s", err) @@ -134,7 +133,7 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity): """Update the switch's status.""" self._attr_is_on = self.device.appliance.status.get( - self.entity_description.on_key, {} + self.entity_description.key, {} ).get(ATTR_VALUE) self._attr_available = True _LOGGER.debug( @@ -154,7 +153,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity): desc = " ".join( ["Program", program_name.split(".")[-3], program_name.split(".")[-1]] ) - super().__init__(device, desc) + super().__init__(device, desc, desc) self.program_name = program_name async def async_turn_on(self, **kwargs: Any) -> None: @@ -192,7 +191,7 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): def __init__(self, device: HomeConnectDevice) -> None: """Initialize the entity.""" - super().__init__(device, "Power") + super().__init__(device, BSH_POWER_STATE, "Power") async def async_turn_on(self, **kwargs: Any) -> None: """Switch the device on.""" @@ -259,7 +258,7 @@ class HomeConnectChildLockSwitch(HomeConnectEntity, SwitchEntity): def __init__(self, device: HomeConnectDevice) -> None: """Initialize the entity.""" - super().__init__(device, "ChildLock") + super().__init__(device, BSH_CHILD_LOCK_STATE, "ChildLock") async def async_turn_on(self, **kwargs: Any) -> None: """Switch child lock on.""" diff --git a/tests/components/home_connect/conftest.py b/tests/components/home_connect/conftest.py index c8137a044a1..2c5231d2e7d 100644 --- a/tests/components/home_connect/conftest.py +++ b/tests/components/home_connect/conftest.py @@ -67,6 +67,20 @@ def mock_config_entry(token_entry: dict[str, Any]) -> MockConfigEntry: "auth_implementation": FAKE_AUTH_IMPL, "token": token_entry, }, + minor_version=2, + ) + + +@pytest.fixture(name="config_entry_v1_1") +def mock_config_entry_v1_1(token_entry: dict[str, Any]) -> MockConfigEntry: + """Fixture for a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + "auth_implementation": FAKE_AUTH_IMPL, + "token": token_entry, + }, + minor_version=1, ) diff --git a/tests/components/home_connect/test_init.py b/tests/components/home_connect/test_init.py index adfb4ff7a1d..52550d705a9 100644 --- a/tests/components/home_connect/test_init.py +++ b/tests/components/home_connect/test_init.py @@ -2,18 +2,31 @@ from collections.abc import Awaitable, Callable from typing import Any -from unittest.mock import MagicMock, Mock +from unittest.mock import MagicMock, Mock, patch from freezegun.api import FrozenDateTimeFactory import pytest from requests import HTTPError import requests_mock +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.home_connect import SCAN_INTERVAL -from homeassistant.components.home_connect.const import DOMAIN, OAUTH2_TOKEN +from homeassistant.components.home_connect.const import ( + BSH_CHILD_LOCK_STATE, + BSH_OPERATION_STATE, + BSH_POWER_STATE, + BSH_REMOTE_START_ALLOWANCE_STATE, + COOKING_LIGHTING, + DOMAIN, + OAUTH2_TOKEN, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from .conftest import ( CLIENT_ID, @@ -294,3 +307,68 @@ async def test_services_exception( with pytest.raises(ValueError): await hass.services.async_call(**service_call) + + +async def test_entity_migration( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + config_entry_v1_1: MockConfigEntry, + appliance: Mock, + platforms: list[Platform], +) -> None: + """Test entity migration.""" + + config_entry_v1_1.add_to_hass(hass) + + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry_v1_1.entry_id, + identifiers={(DOMAIN, appliance.haId)}, + ) + + test_entities = [ + ( + SENSOR_DOMAIN, + "Operation State", + BSH_OPERATION_STATE, + ), + ( + SWITCH_DOMAIN, + "ChildLock", + BSH_CHILD_LOCK_STATE, + ), + ( + SWITCH_DOMAIN, + "Power", + BSH_POWER_STATE, + ), + ( + BINARY_SENSOR_DOMAIN, + "Remote Start", + BSH_REMOTE_START_ALLOWANCE_STATE, + ), + ( + LIGHT_DOMAIN, + "Light", + COOKING_LIGHTING, + ), + ] + + for domain, old_unique_id_suffix, _ in test_entities: + entity_registry.async_get_or_create( + domain, + DOMAIN, + f"{appliance.haId}-{old_unique_id_suffix}", + device_id=device_entry.id, + config_entry=config_entry_v1_1, + ) + + with patch("homeassistant.components.home_connect.PLATFORMS", platforms): + await hass.config_entries.async_setup(config_entry_v1_1.entry_id) + await hass.async_block_till_done() + + for domain, _, expected_unique_id_suffix in test_entities: + assert entity_registry.async_get_entity_id( + domain, DOMAIN, f"{appliance.haId}-{expected_unique_id_suffix}" + ) + assert config_entry_v1_1.minor_version == 2