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 <joostlek@outlook.com>
This commit is contained in:
J. Diego Rodríguez Royo 2024-10-06 12:40:13 +02:00 committed by GitHub
parent 3e8bc98f23
commit 0d795aad16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 283 additions and 92 deletions

View File

@ -4,18 +4,20 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any
from requests import HTTPError from requests import HTTPError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_DEVICE_ID, CONF_DEVICE, Platform 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 ( from homeassistant.helpers import (
config_entry_oauth2_flow, config_entry_oauth2_flow,
config_validation as cv, config_validation as cv,
device_registry as dr, device_registry as dr,
) )
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -28,6 +30,7 @@ from .const import (
BSH_PAUSE, BSH_PAUSE,
BSH_RESUME, BSH_RESUME,
DOMAIN, DOMAIN,
OLD_NEW_UNIQUE_ID_SUFFIX_MAP,
SERVICE_OPTION_ACTIVE, SERVICE_OPTION_ACTIVE,
SERVICE_OPTION_SELECTED, SERVICE_OPTION_SELECTED,
SERVICE_PAUSE_PROGRAM, 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) await hass.async_add_executor_job(device.initialize)
except HTTPError as err: except HTTPError as err:
_LOGGER.warning("Cannot update devices: %s", err.response.status_code) _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

View File

@ -24,6 +24,7 @@ from homeassistant.helpers.dispatcher import dispatcher_send
from .const import ( from .const import (
ATTR_AMBIENT, ATTR_AMBIENT,
ATTR_BSH_KEY,
ATTR_DESC, ATTR_DESC,
ATTR_DEVICE, ATTR_DEVICE,
ATTR_KEY, ATTR_KEY,
@ -32,9 +33,16 @@ from .const import (
ATTR_UNIT, ATTR_UNIT,
ATTR_VALUE, ATTR_VALUE,
BSH_ACTIVE_PROGRAM, BSH_ACTIVE_PROGRAM,
BSH_AMBIENT_LIGHT_ENABLED,
BSH_COMMON_OPTION_DURATION,
BSH_COMMON_OPTION_PROGRAM_PROGRESS,
BSH_OPERATION_STATE, BSH_OPERATION_STATE,
BSH_POWER_OFF, BSH_POWER_OFF,
BSH_POWER_STANDBY, BSH_POWER_STANDBY,
BSH_REMAINING_PROGRAM_TIME,
BSH_REMOTE_CONTROL_ACTIVATION_STATE,
BSH_REMOTE_START_ALLOWANCE_STATE,
COOKING_LIGHTING,
SIGNAL_UPDATE_ENTITIES, SIGNAL_UPDATE_ENTITIES,
) )
@ -181,21 +189,39 @@ class DeviceWithPrograms(HomeConnectDevice):
device. device.
""" """
sensors = { sensors = {
"Remaining Program Time": (None, None, SensorDeviceClass.TIMESTAMP, 1), BSH_REMAINING_PROGRAM_TIME: (
"Duration": (UnitOfTime.SECONDS, "mdi:update", None, 1), "Remaining Program Time",
"Program Progress": (PERCENTAGE, "mdi:progress-clock", None, 1), 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 [ return [
{ {
ATTR_DEVICE: self, ATTR_DEVICE: self,
ATTR_DESC: k, ATTR_BSH_KEY: k,
ATTR_DESC: desc,
ATTR_UNIT: unit, ATTR_UNIT: unit,
ATTR_KEY: f"BSH.Common.Option.{k.replace(' ', '')}",
ATTR_ICON: icon, ATTR_ICON: icon,
ATTR_DEVICE_CLASS: device_class, ATTR_DEVICE_CLASS: device_class,
ATTR_SIGN: sign, 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 [ return [
{ {
ATTR_DEVICE: self, ATTR_DEVICE: self,
ATTR_BSH_KEY: BSH_OPERATION_STATE,
ATTR_DESC: "Operation State", ATTR_DESC: "Operation State",
ATTR_UNIT: None, ATTR_UNIT: None,
ATTR_KEY: BSH_OPERATION_STATE,
ATTR_ICON: "mdi:state-machine", ATTR_ICON: "mdi:state-machine",
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
ATTR_SIGN: 1, ATTR_SIGN: 1,
@ -225,6 +251,7 @@ class DeviceWithDoor(HomeConnectDevice):
"""Get a dictionary with info about the door binary sensor.""" """Get a dictionary with info about the door binary sensor."""
return { return {
ATTR_DEVICE: self, ATTR_DEVICE: self,
ATTR_BSH_KEY: "Door",
ATTR_DESC: "Door", ATTR_DESC: "Door",
ATTR_SENSOR_TYPE: "door", ATTR_SENSOR_TYPE: "door",
ATTR_DEVICE_CLASS: "door", ATTR_DEVICE_CLASS: "door",
@ -236,7 +263,12 @@ class DeviceWithLight(HomeConnectDevice):
def get_light_entity(self) -> dict[str, Any]: def get_light_entity(self) -> dict[str, Any]:
"""Get a dictionary with info about the lighting.""" """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): class DeviceWithAmbientLight(HomeConnectDevice):
@ -244,7 +276,12 @@ class DeviceWithAmbientLight(HomeConnectDevice):
def get_ambientlight_entity(self) -> dict[str, Any]: def get_ambientlight_entity(self) -> dict[str, Any]:
"""Get a dictionary with info about the ambient lighting.""" """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): class DeviceWithRemoteControl(HomeConnectDevice):
@ -254,6 +291,7 @@ class DeviceWithRemoteControl(HomeConnectDevice):
"""Get a dictionary with info about the remote control sensor.""" """Get a dictionary with info about the remote control sensor."""
return { return {
ATTR_DEVICE: self, ATTR_DEVICE: self,
ATTR_BSH_KEY: BSH_REMOTE_CONTROL_ACTIVATION_STATE,
ATTR_DESC: "Remote Control", ATTR_DESC: "Remote Control",
ATTR_SENSOR_TYPE: "remote_control", ATTR_SENSOR_TYPE: "remote_control",
} }
@ -266,6 +304,7 @@ class DeviceWithRemoteStart(HomeConnectDevice):
"""Get a dictionary with info about the remote start sensor.""" """Get a dictionary with info about the remote start sensor."""
return { return {
ATTR_DEVICE: self, ATTR_DEVICE: self,
ATTR_BSH_KEY: BSH_REMOTE_START_ALLOWANCE_STATE,
ATTR_DESC: "Remote Start", ATTR_DESC: "Remote Start",
ATTR_SENSOR_TYPE: "remote_start", ATTR_SENSOR_TYPE: "remote_start",
} }

View File

@ -39,7 +39,7 @@ _LOGGER = logging.getLogger(__name__)
class HomeConnectBinarySensorEntityDescription(BinarySensorEntityDescription): class HomeConnectBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Entity Description class for binary sensors.""" """Entity Description class for binary sensors."""
state_key: str | None desc: str
device_class: BinarySensorDeviceClass | None = BinarySensorDeviceClass.DOOR device_class: BinarySensorDeviceClass | None = BinarySensorDeviceClass.DOOR
boolean_map: dict[str, bool] = field( boolean_map: dict[str, bool] = field(
default_factory=lambda: { default_factory=lambda: {
@ -51,16 +51,16 @@ class HomeConnectBinarySensorEntityDescription(BinarySensorEntityDescription):
BINARY_SENSORS: tuple[HomeConnectBinarySensorEntityDescription, ...] = ( BINARY_SENSORS: tuple[HomeConnectBinarySensorEntityDescription, ...] = (
HomeConnectBinarySensorEntityDescription( HomeConnectBinarySensorEntityDescription(
key="Chiller Door", key=REFRIGERATION_STATUS_DOOR_CHILLER,
state_key=REFRIGERATION_STATUS_DOOR_CHILLER, desc="Chiller Door",
), ),
HomeConnectBinarySensorEntityDescription( HomeConnectBinarySensorEntityDescription(
key="Freezer Door", key=REFRIGERATION_STATUS_DOOR_FREEZER,
state_key=REFRIGERATION_STATUS_DOOR_FREEZER, desc="Freezer Door",
), ),
HomeConnectBinarySensorEntityDescription( HomeConnectBinarySensorEntityDescription(
key="Refrigerator Door", key=REFRIGERATION_STATUS_DOOR_REFRIGERATOR,
state_key=REFRIGERATION_STATUS_DOOR_REFRIGERATOR, desc="Refrigerator Door",
), ),
) )
@ -85,7 +85,7 @@ async def async_setup_entry(
device=device, entity_description=description device=device, entity_description=description
) )
for description in BINARY_SENSORS for description in BINARY_SENSORS
if description.state_key in device.appliance.status if description.key in device.appliance.status
) )
return entities return entities
@ -98,12 +98,13 @@ class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity):
def __init__( def __init__(
self, self,
device: HomeConnectDevice, device: HomeConnectDevice,
bsh_key: str,
desc: str, desc: str,
sensor_type: str, sensor_type: str,
device_class: BinarySensorDeviceClass | None = None, device_class: BinarySensorDeviceClass | None = None,
) -> None: ) -> None:
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(device, desc) super().__init__(device, bsh_key, desc)
self._attr_device_class = device_class self._attr_device_class = device_class
self._type = sensor_type self._type = sensor_type
self._false_value_list = None self._false_value_list = None
@ -162,7 +163,7 @@ class HomeConnectFridgeDoorBinarySensor(HomeConnectEntity, BinarySensorEntity):
) -> None: ) -> None:
"""Initialize the entity.""" """Initialize the entity."""
self.entity_description = entity_description 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: async def async_update(self) -> None:
"""Update the binary sensor's status.""" """Update the binary sensor's status."""
@ -172,9 +173,7 @@ class HomeConnectFridgeDoorBinarySensor(HomeConnectEntity, BinarySensorEntity):
self.state, self.state,
) )
self._attr_is_on = self.entity_description.boolean_map.get( self._attr_is_on = self.entity_description.boolean_map.get(
self.device.appliance.status.get(self.entity_description.state_key, {}).get( self.device.appliance.status.get(self.bsh_key, {}).get(ATTR_VALUE)
ATTR_VALUE
)
) )
self._attr_available = self._attr_is_on is not None self._attr_available = self._attr_is_on is not None
_LOGGER.debug( _LOGGER.debug(

View File

@ -14,6 +14,8 @@ class OAuth2FlowHandler(
DOMAIN = DOMAIN DOMAIN = DOMAIN
MINOR_VERSION = 2
@property @property
def logger(self) -> logging.Logger: def logger(self) -> logging.Logger:
"""Return logger.""" """Return logger."""

View File

@ -14,6 +14,10 @@ BSH_REMOTE_CONTROL_ACTIVATION_STATE = "BSH.Common.Status.RemoteControlActive"
BSH_REMOTE_START_ALLOWANCE_STATE = "BSH.Common.Status.RemoteControlStartAllowed" BSH_REMOTE_START_ALLOWANCE_STATE = "BSH.Common.Status.RemoteControlStartAllowed"
BSH_CHILD_LOCK_STATE = "BSH.Common.Setting.ChildLock" 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_PRESENT = "BSH.Common.EnumType.EventPresentState.Present"
BSH_EVENT_PRESENT_STATE_CONFIRMED = "BSH.Common.EnumType.EventPresentState.Confirmed" BSH_EVENT_PRESENT_STATE_CONFIRMED = "BSH.Common.EnumType.EventPresentState.Confirmed"
BSH_EVENT_PRESENT_STATE_OFF = "BSH.Common.EnumType.EventPresentState.Off" BSH_EVENT_PRESENT_STATE_OFF = "BSH.Common.EnumType.EventPresentState.Off"
@ -92,6 +96,7 @@ SERVICE_SETTING = "change_setting"
SERVICE_START_PROGRAM = "start_program" SERVICE_START_PROGRAM = "start_program"
ATTR_AMBIENT = "ambient" ATTR_AMBIENT = "ambient"
ATTR_BSH_KEY = "bsh_key"
ATTR_DESC = "desc" ATTR_DESC = "desc"
ATTR_DEVICE = "device" ATTR_DEVICE = "device"
ATTR_KEY = "key" ATTR_KEY = "key"
@ -100,3 +105,30 @@ ATTR_SENSOR_TYPE = "sensor_type"
ATTR_SIGN = "sign" ATTR_SIGN = "sign"
ATTR_UNIT = "unit" ATTR_UNIT = "unit"
ATTR_VALUE = "value" 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,
}

View File

@ -18,11 +18,12 @@ class HomeConnectEntity(Entity):
_attr_should_poll = False _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.""" """Initialize the entity."""
self.device = device self.device = device
self.bsh_key = bsh_key
self._attr_name = f"{device.appliance.name} {desc}" 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( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.appliance.haId)}, identifiers={(DOMAIN, device.appliance.haId)},
manufacturer=device.appliance.brand, manufacturer=device.appliance.brand,

View File

@ -27,8 +27,6 @@ from .const import (
BSH_AMBIENT_LIGHT_COLOR, BSH_AMBIENT_LIGHT_COLOR,
BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR, BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR,
BSH_AMBIENT_LIGHT_CUSTOM_COLOR, BSH_AMBIENT_LIGHT_CUSTOM_COLOR,
BSH_AMBIENT_LIGHT_ENABLED,
COOKING_LIGHTING,
COOKING_LIGHTING_BRIGHTNESS, COOKING_LIGHTING_BRIGHTNESS,
DOMAIN, DOMAIN,
REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS, REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS,
@ -45,19 +43,19 @@ _LOGGER = logging.getLogger(__name__)
class HomeConnectLightEntityDescription(LightEntityDescription): class HomeConnectLightEntityDescription(LightEntityDescription):
"""Light entity description.""" """Light entity description."""
on_key: str desc: str
brightness_key: str | None brightness_key: str | None
LIGHTS: tuple[HomeConnectLightEntityDescription, ...] = ( LIGHTS: tuple[HomeConnectLightEntityDescription, ...] = (
HomeConnectLightEntityDescription( HomeConnectLightEntityDescription(
key="Internal Light", key=REFRIGERATION_INTERNAL_LIGHT_POWER,
on_key=REFRIGERATION_INTERNAL_LIGHT_POWER, desc="Internal Light",
brightness_key=REFRIGERATION_INTERNAL_LIGHT_BRIGHTNESS, brightness_key=REFRIGERATION_INTERNAL_LIGHT_BRIGHTNESS,
), ),
HomeConnectLightEntityDescription( HomeConnectLightEntityDescription(
key="External Light", key=REFRIGERATION_EXTERNAL_LIGHT_POWER,
on_key=REFRIGERATION_EXTERNAL_LIGHT_POWER, desc="External Light",
brightness_key=REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS, brightness_key=REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS,
), ),
) )
@ -86,7 +84,7 @@ async def async_setup_entry(
entity_description=description, entity_description=description,
) )
for description in LIGHTS for description in LIGHTS
if description.on_key in device.appliance.status if description.key in device.appliance.status
) )
entities.extend(entity_list) entities.extend(entity_list)
return entities return entities
@ -97,9 +95,11 @@ async def async_setup_entry(
class HomeConnectLight(HomeConnectEntity, LightEntity): class HomeConnectLight(HomeConnectEntity, LightEntity):
"""Light for Home Connect.""" """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.""" """Initialize the entity."""
super().__init__(device, desc) super().__init__(device, bsh_key, desc)
self._ambient = ambient self._ambient = ambient
self._percentage_scale = (10, 100) self._percentage_scale = (10, 100)
self._brightness_key: str | None self._brightness_key: str | None
@ -107,14 +107,12 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
self._color_key: str | None self._color_key: str | None
if ambient: if ambient:
self._brightness_key = BSH_AMBIENT_LIGHT_BRIGHTNESS self._brightness_key = BSH_AMBIENT_LIGHT_BRIGHTNESS
self._key = BSH_AMBIENT_LIGHT_ENABLED
self._custom_color_key = BSH_AMBIENT_LIGHT_CUSTOM_COLOR self._custom_color_key = BSH_AMBIENT_LIGHT_CUSTOM_COLOR
self._color_key = BSH_AMBIENT_LIGHT_COLOR self._color_key = BSH_AMBIENT_LIGHT_COLOR
self._attr_color_mode = ColorMode.HS self._attr_color_mode = ColorMode.HS
self._attr_supported_color_modes = {ColorMode.HS} self._attr_supported_color_modes = {ColorMode.HS}
else: else:
self._brightness_key = COOKING_LIGHTING_BRIGHTNESS self._brightness_key = COOKING_LIGHTING_BRIGHTNESS
self._key = COOKING_LIGHTING
self._custom_color_key = None self._custom_color_key = None
self._color_key = None self._color_key = None
self._attr_color_mode = ColorMode.BRIGHTNESS self._attr_color_mode = ColorMode.BRIGHTNESS
@ -126,7 +124,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
_LOGGER.debug("Switching ambient light on for: %s", self.name) _LOGGER.debug("Switching ambient light on for: %s", self.name)
try: try:
await self.hass.async_add_executor_job( 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: except HomeConnectError as err:
_LOGGER.error("Error while trying to turn on ambient light: %s", 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) _LOGGER.debug("Switching light on for: %s", self.name)
try: try:
await self.hass.async_add_executor_job( 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: except HomeConnectError as err:
_LOGGER.error("Error while trying to turn on light: %s", 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) _LOGGER.debug("Switching light off for: %s", self.name)
try: try:
await self.hass.async_add_executor_job( 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: except HomeConnectError as err:
_LOGGER.error("Error while trying to turn off light: %s", 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: async def async_update(self) -> None:
"""Update the light's status.""" """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 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 self._attr_is_on = False
else: else:
self._attr_is_on = None self._attr_is_on = None
@ -255,8 +255,9 @@ class HomeConnectCoolingLight(HomeConnectLight):
entity_description: HomeConnectLightEntityDescription, entity_description: HomeConnectLightEntityDescription,
) -> None: ) -> None:
"""Initialize Cooling Light Entity.""" """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.entity_description = entity_description
self._key = entity_description.on_key
self._brightness_key = entity_description.brightness_key self._brightness_key = entity_description.brightness_key
self._percentage_scale = (1, 100) self._percentage_scale = (1, 100)

View File

@ -46,45 +46,39 @@ class HomeConnectSensorEntityDescription(SensorEntityDescription):
options: list[str] | None = field( options: list[str] | None = field(
default_factory=lambda: ["confirmed", "off", "present"] default_factory=lambda: ["confirmed", "off", "present"]
) )
state_key: str desc: str
appliance_types: tuple[str, ...] appliance_types: tuple[str, ...]
SENSORS: tuple[HomeConnectSensorEntityDescription, ...] = ( SENSORS: tuple[HomeConnectSensorEntityDescription, ...] = (
HomeConnectSensorEntityDescription( HomeConnectSensorEntityDescription(
key="Door Alarm Freezer", key=REFRIGERATION_EVENT_DOOR_ALARM_FREEZER,
translation_key="alarm_sensor_freezer", desc="Door Alarm Freezer",
state_key=REFRIGERATION_EVENT_DOOR_ALARM_FREEZER,
appliance_types=("FridgeFreezer", "Freezer"), appliance_types=("FridgeFreezer", "Freezer"),
), ),
HomeConnectSensorEntityDescription( HomeConnectSensorEntityDescription(
key="Door Alarm Refrigerator", key=REFRIGERATION_EVENT_DOOR_ALARM_REFRIGERATOR,
translation_key="alarm_sensor_fridge", desc="Door Alarm Refrigerator",
state_key=REFRIGERATION_EVENT_DOOR_ALARM_REFRIGERATOR,
appliance_types=("FridgeFreezer", "Refrigerator"), appliance_types=("FridgeFreezer", "Refrigerator"),
), ),
HomeConnectSensorEntityDescription( HomeConnectSensorEntityDescription(
key="Temperature Alarm Freezer", key=REFRIGERATION_EVENT_TEMP_ALARM_FREEZER,
translation_key="alarm_sensor_temp", desc="Temperature Alarm Freezer",
state_key=REFRIGERATION_EVENT_TEMP_ALARM_FREEZER,
appliance_types=("FridgeFreezer", "Freezer"), appliance_types=("FridgeFreezer", "Freezer"),
), ),
HomeConnectSensorEntityDescription( HomeConnectSensorEntityDescription(
key="Bean Container Empty", key=COFFEE_EVENT_BEAN_CONTAINER_EMPTY,
translation_key="alarm_sensor_coffee_bean_container", desc="Bean Container Empty",
state_key=COFFEE_EVENT_BEAN_CONTAINER_EMPTY,
appliance_types=("CoffeeMaker",), appliance_types=("CoffeeMaker",),
), ),
HomeConnectSensorEntityDescription( HomeConnectSensorEntityDescription(
key="Water Tank Empty", key=COFFEE_EVENT_WATER_TANK_EMPTY,
translation_key="alarm_sensor_coffee_water_tank", desc="Water Tank Empty",
state_key=COFFEE_EVENT_WATER_TANK_EMPTY,
appliance_types=("CoffeeMaker",), appliance_types=("CoffeeMaker",),
), ),
HomeConnectSensorEntityDescription( HomeConnectSensorEntityDescription(
key="Drip Tray Full", key=COFFEE_EVENT_DRIP_TRAY_FULL,
translation_key="alarm_sensor_coffee_drip_tray", desc="Drip Tray Full",
state_key=COFFEE_EVENT_DRIP_TRAY_FULL,
appliance_types=("CoffeeMaker",), appliance_types=("CoffeeMaker",),
), ),
) )
@ -128,16 +122,15 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity):
def __init__( def __init__(
self, self,
device: HomeConnectDevice, device: HomeConnectDevice,
bsh_key: str,
desc: str, desc: str,
key: str,
unit: str, unit: str,
icon: str, icon: str,
device_class: SensorDeviceClass, device_class: SensorDeviceClass,
sign: int = 1, sign: int = 1,
) -> None: ) -> None:
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(device, desc) super().__init__(device, bsh_key, desc)
self._key = key
self._sign = sign self._sign = sign
self._attr_native_unit_of_measurement = unit self._attr_native_unit_of_measurement = unit
self._attr_icon = icon self._attr_icon = icon
@ -151,10 +144,10 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update the sensor's status.""" """Update the sensor's status."""
status = self.device.appliance.status status = self.device.appliance.status
if self._key not in status: if self.bsh_key not in status:
self._attr_native_value = None self._attr_native_value = None
elif self.device_class == SensorDeviceClass.TIMESTAMP: 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 self._attr_native_value = None
elif ( elif (
self._attr_native_value is not None self._attr_native_value is not None
@ -175,13 +168,13 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity):
BSH_OPERATION_STATE_FINISHED, 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) self._attr_native_value = dt_util.utcnow() + timedelta(seconds=seconds)
else: else:
self._attr_native_value = None self._attr_native_value = None
else: else:
self._attr_native_value = status[self._key].get(ATTR_VALUE) self._attr_native_value = status[self.bsh_key].get(ATTR_VALUE)
if self._key == BSH_OPERATION_STATE: if self.bsh_key == BSH_OPERATION_STATE:
# Value comes back as an enum, we only really care about the # Value comes back as an enum, we only really care about the
# last part, so split it off # last part, so split it off
# https://developer.home-connect.com/docs/status/operation_state # https://developer.home-connect.com/docs/status/operation_state
@ -203,7 +196,9 @@ class HomeConnectAlarmSensor(HomeConnectEntity, SensorEntity):
) -> None: ) -> None:
"""Initialize the entity.""" """Initialize the entity."""
self.entity_description = entity_description self.entity_description = entity_description
super().__init__(device, self.entity_description.key) super().__init__(
device, self.entity_description.key, self.entity_description.desc
)
@property @property
def available(self) -> bool: def available(self) -> bool:
@ -213,7 +208,7 @@ class HomeConnectAlarmSensor(HomeConnectEntity, SensorEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update the sensor's status.""" """Update the sensor's status."""
self._attr_native_value = ( 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) .get(ATTR_VALUE, BSH_EVENT_PRESENT_STATE_OFF)
.rsplit(".", maxsplit=1)[-1] .rsplit(".", maxsplit=1)[-1]
.lower() .lower()

View File

@ -34,22 +34,21 @@ _LOGGER = logging.getLogger(__name__)
class HomeConnectSwitchEntityDescription(SwitchEntityDescription): class HomeConnectSwitchEntityDescription(SwitchEntityDescription):
"""Switch entity description.""" """Switch entity description."""
on_key: str desc: str
SWITCHES: tuple[HomeConnectSwitchEntityDescription, ...] = ( SWITCHES: tuple[HomeConnectSwitchEntityDescription, ...] = (
HomeConnectSwitchEntityDescription( HomeConnectSwitchEntityDescription(
key="Supermode Freezer", key=REFRIGERATION_SUPERMODEFREEZER,
on_key=REFRIGERATION_SUPERMODEFREEZER, desc="Supermode Freezer",
), ),
HomeConnectSwitchEntityDescription( HomeConnectSwitchEntityDescription(
key="Supermode Refrigerator", key=REFRIGERATION_SUPERMODEREFRIGERATOR,
on_key=REFRIGERATION_SUPERMODEREFRIGERATOR, desc="Supermode Refrigerator",
), ),
HomeConnectSwitchEntityDescription( HomeConnectSwitchEntityDescription(
key="Dispenser Enabled", key=REFRIGERATION_DISPENSER,
on_key=REFRIGERATION_DISPENSER, desc="Dispenser Enabled",
translation_key="refrigeration_dispenser",
), ),
) )
@ -75,7 +74,7 @@ async def async_setup_entry(
entities.extend( entities.extend(
HomeConnectSwitch(device=hc_device, entity_description=description) HomeConnectSwitch(device=hc_device, entity_description=description)
for description in SWITCHES for description in SWITCHES
if description.on_key in hc_device.appliance.status if description.key in hc_device.appliance.status
) )
return entities return entities
@ -96,7 +95,7 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
"""Initialize the entity.""" """Initialize the entity."""
self.entity_description = entity_description self.entity_description = entity_description
self._attr_available = False 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: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on setting.""" """Turn on setting."""
@ -104,7 +103,7 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
_LOGGER.debug("Turning on %s", self.entity_description.key) _LOGGER.debug("Turning on %s", self.entity_description.key)
try: try:
await self.hass.async_add_executor_job( 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: except HomeConnectError as err:
_LOGGER.error("Error while trying to turn on: %s", 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) _LOGGER.debug("Turning off %s", self.entity_description.key)
try: try:
await self.hass.async_add_executor_job( 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: except HomeConnectError as err:
_LOGGER.error("Error while trying to turn off: %s", err) _LOGGER.error("Error while trying to turn off: %s", err)
@ -134,7 +133,7 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
"""Update the switch's status.""" """Update the switch's status."""
self._attr_is_on = self.device.appliance.status.get( self._attr_is_on = self.device.appliance.status.get(
self.entity_description.on_key, {} self.entity_description.key, {}
).get(ATTR_VALUE) ).get(ATTR_VALUE)
self._attr_available = True self._attr_available = True
_LOGGER.debug( _LOGGER.debug(
@ -154,7 +153,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
desc = " ".join( desc = " ".join(
["Program", program_name.split(".")[-3], program_name.split(".")[-1]] ["Program", program_name.split(".")[-3], program_name.split(".")[-1]]
) )
super().__init__(device, desc) super().__init__(device, desc, desc)
self.program_name = program_name self.program_name = program_name
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
@ -192,7 +191,7 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
def __init__(self, device: HomeConnectDevice) -> None: def __init__(self, device: HomeConnectDevice) -> None:
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(device, "Power") super().__init__(device, BSH_POWER_STATE, "Power")
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Switch the device on.""" """Switch the device on."""
@ -259,7 +258,7 @@ class HomeConnectChildLockSwitch(HomeConnectEntity, SwitchEntity):
def __init__(self, device: HomeConnectDevice) -> None: def __init__(self, device: HomeConnectDevice) -> None:
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(device, "ChildLock") super().__init__(device, BSH_CHILD_LOCK_STATE, "ChildLock")
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Switch child lock on.""" """Switch child lock on."""

View File

@ -67,6 +67,20 @@ def mock_config_entry(token_entry: dict[str, Any]) -> MockConfigEntry:
"auth_implementation": FAKE_AUTH_IMPL, "auth_implementation": FAKE_AUTH_IMPL,
"token": token_entry, "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,
) )

View File

@ -2,18 +2,31 @@
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from typing import Any from typing import Any
from unittest.mock import MagicMock, Mock from unittest.mock import MagicMock, Mock, patch
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from requests import HTTPError from requests import HTTPError
import requests_mock 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 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.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant 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 ( from .conftest import (
CLIENT_ID, CLIENT_ID,
@ -294,3 +307,68 @@ async def test_services_exception(
with pytest.raises(ValueError): with pytest.raises(ValueError):
await hass.services.async_call(**service_call) 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