diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 87f4bfa7799..53dffda7798 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -10,7 +10,7 @@ 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.const import ATTR_DEVICE_ID, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_entry_oauth2_flow, @@ -87,8 +87,7 @@ def _get_appliance_by_device_id( ) -> api.HomeConnectDevice: """Return a Home Connect appliance instance given an device_id.""" for hc_api in hass.data[DOMAIN].values(): - for dev_dict in hc_api.devices: - device = dev_dict[CONF_DEVICE] + for device in hc_api.devices: if device.device_id == device_id: return device.appliance raise ValueError(f"Appliance for device id {device_id} not found") @@ -255,9 +254,7 @@ async def update_all_devices(hass: HomeAssistant, entry: ConfigEntry) -> None: device_registry = dr.async_get(hass) try: await hass.async_add_executor_job(hc_api.get_devices) - for device_dict in hc_api.devices: - device = device_dict["device"] - + for device in hc_api.devices: device_entry = device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, device.appliance.haId)}, diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index 4324edc8c1e..453f926c402 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -1,50 +1,17 @@ """API for Home Connect bound to HASS OAuth.""" -from abc import abstractmethod from asyncio import run_coroutine_threadsafe import logging -from typing import Any import homeconnect from homeconnect.api import HomeConnectAppliance, HomeConnectError -from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, - CONF_DEVICE, - CONF_ENTITIES, - PERCENTAGE, - UnitOfTime, -) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.dispatcher import dispatcher_send -from .const import ( - ATTR_AMBIENT, - ATTR_BSH_KEY, - ATTR_DESC, - ATTR_DEVICE, - ATTR_KEY, - ATTR_SENSOR_TYPE, - ATTR_SIGN, - 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, -) +from .const import ATTR_KEY, ATTR_VALUE, BSH_ACTIVE_PROGRAM, SIGNAL_UPDATE_ENTITIES _LOGGER = logging.getLogger(__name__) @@ -65,7 +32,7 @@ class ConfigEntryAuth(homeconnect.HomeConnectAPI): hass, config_entry, implementation ) super().__init__(self.session.token) - self.devices: list[dict[str, Any]] = [] + self.devices: list[HomeConnectDevice] = [] def refresh_tokens(self) -> dict: """Refresh and return new Home Connect tokens using Home Assistant OAuth2 session.""" @@ -75,55 +42,16 @@ class ConfigEntryAuth(homeconnect.HomeConnectAPI): return self.session.token - def get_devices(self) -> list[dict[str, Any]]: + def get_devices(self) -> list[HomeConnectAppliance]: """Get a dictionary of devices.""" - appl = self.get_appliances() - devices = [] - for app in appl: - device: HomeConnectDevice - if app.type == "Dryer": - device = Dryer(self.hass, app) - elif app.type == "Washer": - device = Washer(self.hass, app) - elif app.type == "WasherDryer": - device = WasherDryer(self.hass, app) - elif app.type == "Dishwasher": - device = Dishwasher(self.hass, app) - elif app.type == "FridgeFreezer": - device = FridgeFreezer(self.hass, app) - elif app.type == "Refrigerator": - device = Refrigerator(self.hass, app) - elif app.type == "Freezer": - device = Freezer(self.hass, app) - elif app.type == "Oven": - device = Oven(self.hass, app) - elif app.type == "CoffeeMaker": - device = CoffeeMaker(self.hass, app) - elif app.type == "Hood": - device = Hood(self.hass, app) - elif app.type == "Hob": - device = Hob(self.hass, app) - elif app.type == "CookProcessor": - device = CookProcessor(self.hass, app) - else: - _LOGGER.warning("Appliance type %s not implemented", app.type) - continue - devices.append( - {CONF_DEVICE: device, CONF_ENTITIES: device.get_entity_info()} - ) - self.devices = devices - return devices + appl: list[HomeConnectAppliance] = self.get_appliances() + self.devices = [HomeConnectDevice(self.hass, app) for app in appl] + return self.devices class HomeConnectDevice: """Generic Home Connect device.""" - # for some devices, this is instead BSH_POWER_STANDBY - # see https://developer.home-connect.com/docs/settings/power_state - power_off_state = BSH_POWER_OFF - hass: HomeAssistant - appliance: HomeConnectAppliance - def __init__(self, hass: HomeAssistant, appliance: HomeConnectAppliance) -> None: """Initialize the device class.""" self.hass = hass @@ -155,378 +83,3 @@ class HomeConnectDevice: _LOGGER.debug("Update triggered on %s", appliance.name) _LOGGER.debug(self.appliance.status) dispatcher_send(self.hass, SIGNAL_UPDATE_ENTITIES, appliance.haId) - - @abstractmethod - def get_entity_info(self) -> dict[str, list[dict[str, Any]]]: - """Get a dictionary with info about the associated entities.""" - raise NotImplementedError - - -class DeviceWithPrograms(HomeConnectDevice): - """Device with programs.""" - - def get_programs_available(self) -> list: - """Get the available programs.""" - try: - programs_available = self.appliance.get_programs_available() - except (HomeConnectError, ValueError): - _LOGGER.debug("Unable to fetch available programs. Probably offline") - programs_available = [] - return programs_available - - def get_program_switches(self) -> list[dict[str, Any]]: - """Get a dictionary with info about program switches. - - There will be one switch for each program. - """ - programs = self.get_programs_available() - return [{ATTR_DEVICE: self, "program_name": p} for p in programs] - - def get_program_sensors(self) -> list[dict[str, Any]]: - """Get a dictionary with info about program sensors. - - There will be one of the four types of sensors for each - device. - """ - sensors = { - 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_BSH_KEY: k, - ATTR_DESC: desc, - ATTR_UNIT: unit, - ATTR_ICON: icon, - ATTR_DEVICE_CLASS: device_class, - ATTR_SIGN: sign, - } - for k, (desc, unit, icon, device_class, sign) in sensors.items() - ] - - -class DeviceWithOpState(HomeConnectDevice): - """Device that has an operation state sensor.""" - - def get_opstate_sensor(self) -> list[dict[str, Any]]: - """Get a list with info about operation state sensors.""" - - return [ - { - ATTR_DEVICE: self, - ATTR_BSH_KEY: BSH_OPERATION_STATE, - ATTR_DESC: "Operation State", - ATTR_UNIT: None, - ATTR_ICON: "mdi:state-machine", - ATTR_DEVICE_CLASS: None, - ATTR_SIGN: 1, - } - ] - - -class DeviceWithDoor(HomeConnectDevice): - """Device that has a door sensor.""" - - def get_door_entity(self) -> dict[str, Any]: - """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", - } - - -class DeviceWithLight(HomeConnectDevice): - """Device that has lighting.""" - - def get_light_entity(self) -> dict[str, Any]: - """Get a dictionary with info about the lighting.""" - return { - ATTR_DEVICE: self, - ATTR_BSH_KEY: COOKING_LIGHTING, - ATTR_DESC: "Light", - ATTR_AMBIENT: None, - } - - -class DeviceWithAmbientLight(HomeConnectDevice): - """Device that has ambient lighting.""" - - def get_ambientlight_entity(self) -> dict[str, Any]: - """Get a dictionary with info about the ambient lighting.""" - return { - ATTR_DEVICE: self, - ATTR_BSH_KEY: BSH_AMBIENT_LIGHT_ENABLED, - ATTR_DESC: "AmbientLight", - ATTR_AMBIENT: True, - } - - -class DeviceWithRemoteControl(HomeConnectDevice): - """Device that has Remote Control binary sensor.""" - - def get_remote_control(self) -> dict[str, Any]: - """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", - } - - -class DeviceWithRemoteStart(HomeConnectDevice): - """Device that has a Remote Start binary sensor.""" - - def get_remote_start(self) -> dict[str, Any]: - """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", - } - - -class Dryer( - DeviceWithDoor, - DeviceWithOpState, - DeviceWithPrograms, - DeviceWithRemoteControl, - DeviceWithRemoteStart, -): - """Dryer class.""" - - def get_entity_info(self) -> dict[str, list[dict[str, Any]]]: - """Get a dictionary with infos about the associated entities.""" - door_entity = self.get_door_entity() - remote_control = self.get_remote_control() - remote_start = self.get_remote_start() - op_state_sensor = self.get_opstate_sensor() - program_sensors = self.get_program_sensors() - program_switches = self.get_program_switches() - return { - "binary_sensor": [door_entity, remote_control, remote_start], - "switch": program_switches, - "sensor": program_sensors + op_state_sensor, - } - - -class Dishwasher( - DeviceWithDoor, - DeviceWithAmbientLight, - DeviceWithOpState, - DeviceWithPrograms, - DeviceWithRemoteControl, - DeviceWithRemoteStart, -): - """Dishwasher class.""" - - def get_entity_info(self) -> dict[str, list[dict[str, Any]]]: - """Get a dictionary with infos about the associated entities.""" - door_entity = self.get_door_entity() - remote_control = self.get_remote_control() - remote_start = self.get_remote_start() - op_state_sensor = self.get_opstate_sensor() - program_sensors = self.get_program_sensors() - program_switches = self.get_program_switches() - return { - "binary_sensor": [door_entity, remote_control, remote_start], - "switch": program_switches, - "sensor": program_sensors + op_state_sensor, - } - - -class Oven( - DeviceWithDoor, - DeviceWithOpState, - DeviceWithPrograms, - DeviceWithRemoteControl, - DeviceWithRemoteStart, -): - """Oven class.""" - - power_off_state = BSH_POWER_STANDBY - - def get_entity_info(self) -> dict[str, list[dict[str, Any]]]: - """Get a dictionary with infos about the associated entities.""" - door_entity = self.get_door_entity() - remote_control = self.get_remote_control() - remote_start = self.get_remote_start() - op_state_sensor = self.get_opstate_sensor() - program_sensors = self.get_program_sensors() - program_switches = self.get_program_switches() - return { - "binary_sensor": [door_entity, remote_control, remote_start], - "switch": program_switches, - "sensor": program_sensors + op_state_sensor, - } - - -class Washer( - DeviceWithDoor, - DeviceWithOpState, - DeviceWithPrograms, - DeviceWithRemoteControl, - DeviceWithRemoteStart, -): - """Washer class.""" - - def get_entity_info(self) -> dict[str, list[dict[str, Any]]]: - """Get a dictionary with infos about the associated entities.""" - door_entity = self.get_door_entity() - remote_control = self.get_remote_control() - remote_start = self.get_remote_start() - op_state_sensor = self.get_opstate_sensor() - program_sensors = self.get_program_sensors() - program_switches = self.get_program_switches() - return { - "binary_sensor": [door_entity, remote_control, remote_start], - "switch": program_switches, - "sensor": program_sensors + op_state_sensor, - } - - -class WasherDryer( - DeviceWithDoor, - DeviceWithOpState, - DeviceWithPrograms, - DeviceWithRemoteControl, - DeviceWithRemoteStart, -): - """WasherDryer class.""" - - def get_entity_info(self) -> dict[str, list[dict[str, Any]]]: - """Get a dictionary with infos about the associated entities.""" - door_entity = self.get_door_entity() - remote_control = self.get_remote_control() - remote_start = self.get_remote_start() - op_state_sensor = self.get_opstate_sensor() - program_sensors = self.get_program_sensors() - program_switches = self.get_program_switches() - return { - "binary_sensor": [door_entity, remote_control, remote_start], - "switch": program_switches, - "sensor": program_sensors + op_state_sensor, - } - - -class CoffeeMaker(DeviceWithOpState, DeviceWithPrograms, DeviceWithRemoteStart): - """Coffee maker class.""" - - power_off_state = BSH_POWER_STANDBY - - def get_entity_info(self): - """Get a dictionary with infos about the associated entities.""" - remote_start = self.get_remote_start() - op_state_sensor = self.get_opstate_sensor() - program_sensors = self.get_program_sensors() - program_switches = self.get_program_switches() - return { - "binary_sensor": [remote_start], - "switch": program_switches, - "sensor": program_sensors + op_state_sensor, - } - - -class Hood( - DeviceWithLight, - DeviceWithAmbientLight, - DeviceWithOpState, - DeviceWithPrograms, - DeviceWithRemoteControl, - DeviceWithRemoteStart, -): - """Hood class.""" - - def get_entity_info(self) -> dict[str, list[dict[str, Any]]]: - """Get a dictionary with infos about the associated entities.""" - remote_control = self.get_remote_control() - remote_start = self.get_remote_start() - light_entity = self.get_light_entity() - ambientlight_entity = self.get_ambientlight_entity() - op_state_sensor = self.get_opstate_sensor() - program_sensors = self.get_program_sensors() - program_switches = self.get_program_switches() - return { - "binary_sensor": [remote_control, remote_start], - "switch": program_switches, - "sensor": program_sensors + op_state_sensor, - "light": [light_entity, ambientlight_entity], - } - - -class FridgeFreezer(DeviceWithDoor): - """Fridge/Freezer class.""" - - def get_entity_info(self) -> dict[str, list[dict[str, Any]]]: - """Get a dictionary with infos about the associated entities.""" - door_entity = self.get_door_entity() - return {"binary_sensor": [door_entity]} - - -class Refrigerator(DeviceWithDoor): - """Refrigerator class.""" - - def get_entity_info(self) -> dict[str, list[dict[str, Any]]]: - """Get a dictionary with infos about the associated entities.""" - door_entity = self.get_door_entity() - return {"binary_sensor": [door_entity]} - - -class Freezer(DeviceWithDoor): - """Freezer class.""" - - def get_entity_info(self) -> dict[str, list[dict[str, Any]]]: - """Get a dictionary with infos about the associated entities.""" - door_entity = self.get_door_entity() - return {"binary_sensor": [door_entity]} - - -class Hob(DeviceWithOpState, DeviceWithPrograms, DeviceWithRemoteControl): - """Hob class.""" - - def get_entity_info(self) -> dict[str, list[dict[str, Any]]]: - """Get a dictionary with infos about the associated entities.""" - remote_control = self.get_remote_control() - op_state_sensor = self.get_opstate_sensor() - program_sensors = self.get_program_sensors() - program_switches = self.get_program_switches() - return { - "binary_sensor": [remote_control], - "switch": program_switches, - "sensor": program_sensors + op_state_sensor, - } - - -class CookProcessor(DeviceWithOpState): - """CookProcessor class.""" - - power_off_state = BSH_POWER_STANDBY - - def get_entity_info(self) -> dict[str, list[dict[str, Any]]]: - """Get a dictionary with infos about the associated entities.""" - op_state_sensor = self.get_opstate_sensor() - return {"sensor": op_state_sensor} diff --git a/homeassistant/components/home_connect/binary_sensor.py b/homeassistant/components/home_connect/binary_sensor.py index 7c99ee5421f..1919b2e4d3f 100644 --- a/homeassistant/components/home_connect/binary_sensor.py +++ b/homeassistant/components/home_connect/binary_sensor.py @@ -1,6 +1,6 @@ """Provides a binary sensor for Home Connect.""" -from dataclasses import dataclass, field +from dataclasses import dataclass import logging from homeassistant.components.binary_sensor import ( @@ -9,13 +9,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ENTITIES from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .api import HomeConnectDevice from .const import ( - ATTR_DEVICE, ATTR_VALUE, BSH_DOOR_STATE, BSH_DOOR_STATE_CLOSED, @@ -33,34 +31,80 @@ from .const import ( from .entity import HomeConnectEntity _LOGGER = logging.getLogger(__name__) +REFRIGERATION_DOOR_BOOLEAN_MAP = { + REFRIGERATION_STATUS_DOOR_CLOSED: False, + REFRIGERATION_STATUS_DOOR_OPEN: True, +} @dataclass(frozen=True, kw_only=True) class HomeConnectBinarySensorEntityDescription(BinarySensorEntityDescription): """Entity Description class for binary sensors.""" - desc: str device_class: BinarySensorDeviceClass | None = BinarySensorDeviceClass.DOOR - boolean_map: dict[str, bool] = field( - default_factory=lambda: { - REFRIGERATION_STATUS_DOOR_CLOSED: False, - REFRIGERATION_STATUS_DOOR_OPEN: True, - } - ) + boolean_map: dict[str, bool] | None = None -BINARY_SENSORS: tuple[HomeConnectBinarySensorEntityDescription, ...] = ( +BINARY_SENSORS = ( + BinarySensorEntityDescription( + key=BSH_REMOTE_CONTROL_ACTIVATION_STATE, + translation_key="remote_control", + ), + BinarySensorEntityDescription( + key=BSH_REMOTE_START_ALLOWANCE_STATE, + translation_key="remote_start", + ), + BinarySensorEntityDescription( + key="BSH.Common.Status.LocalControlActive", + translation_key="local_control", + ), + HomeConnectBinarySensorEntityDescription( + key="BSH.Common.Status.BatteryChargingState", + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, + boolean_map={ + "BSH.Common.EnumType.BatteryChargingState.Charging": True, + "BSH.Common.EnumType.BatteryChargingState.Discharging": False, + }, + translation_key="battery_charging_state", + ), + HomeConnectBinarySensorEntityDescription( + key="BSH.Common.Status.ChargingConnection", + device_class=BinarySensorDeviceClass.PLUG, + boolean_map={ + "BSH.Common.EnumType.ChargingConnection.Connected": True, + "BSH.Common.EnumType.ChargingConnection.Disconnected": False, + }, + translation_key="charging_connection", + ), + BinarySensorEntityDescription( + key="ConsumerProducts.CleaningRobot.Status.DustBoxInserted", + translation_key="dust_box_inserted", + ), + BinarySensorEntityDescription( + key="ConsumerProducts.CleaningRobot.Status.Lifted", + translation_key="lifted", + ), + BinarySensorEntityDescription( + key="ConsumerProducts.CleaningRobot.Status.Lost", + translation_key="lost", + ), HomeConnectBinarySensorEntityDescription( key=REFRIGERATION_STATUS_DOOR_CHILLER, - desc="Chiller Door", + boolean_map=REFRIGERATION_DOOR_BOOLEAN_MAP, + device_class=BinarySensorDeviceClass.DOOR, + translation_key="chiller_door", ), HomeConnectBinarySensorEntityDescription( key=REFRIGERATION_STATUS_DOOR_FREEZER, - desc="Freezer Door", + boolean_map=REFRIGERATION_DOOR_BOOLEAN_MAP, + device_class=BinarySensorDeviceClass.DOOR, + translation_key="freezer_door", ), HomeConnectBinarySensorEntityDescription( key=REFRIGERATION_STATUS_DOOR_REFRIGERATOR, - desc="Refrigerator Door", + boolean_map=REFRIGERATION_DOOR_BOOLEAN_MAP, + device_class=BinarySensorDeviceClass.DOOR, + translation_key="refrigerator_door", ), ) @@ -75,18 +119,14 @@ async def async_setup_entry( def get_entities() -> list[BinarySensorEntity]: entities: list[BinarySensorEntity] = [] hc_api = hass.data[DOMAIN][config_entry.entry_id] - for device_dict in hc_api.devices: - entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("binary_sensor", []) - entities += [HomeConnectBinarySensor(**d) for d in entity_dicts] - device: HomeConnectDevice = device_dict[ATTR_DEVICE] - # Auto-discover entities + for device in hc_api.devices: entities.extend( - HomeConnectFridgeDoorBinarySensor( - device=device, entity_description=description - ) + HomeConnectBinarySensor(device, description) for description in BINARY_SENSORS if description.key in device.appliance.status ) + if BSH_DOOR_STATE in device.appliance.status: + entities.append(HomeConnectDoorBinarySensor(device)) return entities async_add_entities(await hass.async_add_executor_job(get_entities), True) @@ -95,28 +135,7 @@ async def async_setup_entry( class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity): """Binary sensor for Home Connect.""" - 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, bsh_key, desc) - self._attr_device_class = device_class - self._type = sensor_type - self._false_value_list = None - self._true_value_list = None - if self._type == "door": - self._update_key = BSH_DOOR_STATE - self._false_value_list = [BSH_DOOR_STATE_CLOSED, BSH_DOOR_STATE_LOCKED] - self._true_value_list = [BSH_DOOR_STATE_OPEN] - elif self._type == "remote_control": - self._update_key = BSH_REMOTE_CONTROL_ACTIVATION_STATE - elif self._type == "remote_start": - self._update_key = BSH_REMOTE_START_ALLOWANCE_STATE + entity_description: HomeConnectBinarySensorEntityDescription @property def available(self) -> bool: @@ -125,59 +144,41 @@ class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity): async def async_update(self) -> None: """Update the binary sensor's status.""" - state = self.device.appliance.status.get(self._update_key, {}) - if not state: + if not self.device.appliance.status or not ( + status := self.device.appliance.status.get(self.bsh_key, {}).get(ATTR_VALUE) + ): self._attr_is_on = None return - - value = state.get(ATTR_VALUE) - if self._false_value_list and self._true_value_list: - if value in self._false_value_list: - self._attr_is_on = False - elif value in self._true_value_list: - self._attr_is_on = True - else: - _LOGGER.warning( - "Unexpected value for HomeConnect %s state: %s", self._type, state - ) - self._attr_is_on = None - elif isinstance(value, bool): - self._attr_is_on = value - else: - _LOGGER.warning( - "Unexpected value for HomeConnect %s state: %s", self._type, state - ) + if self.entity_description.boolean_map: + self._attr_is_on = self.entity_description.boolean_map.get(status) + elif status not in [True, False]: self._attr_is_on = None + else: + self._attr_is_on = status _LOGGER.debug("Updated, new state: %s", self._attr_is_on) -class HomeConnectFridgeDoorBinarySensor(HomeConnectEntity, BinarySensorEntity): - """Binary sensor for Home Connect Fridge Doors.""" +class HomeConnectDoorBinarySensor(HomeConnectBinarySensor): + """Binary sensor for Home Connect Generic Door.""" - entity_description: HomeConnectBinarySensorEntityDescription + _attr_has_entity_name = False def __init__( self, device: HomeConnectDevice, - entity_description: HomeConnectBinarySensorEntityDescription, ) -> None: """Initialize the entity.""" - self.entity_description = entity_description - super().__init__(device, entity_description.key, entity_description.desc) - - async def async_update(self) -> None: - """Update the binary sensor's status.""" - _LOGGER.debug( - "Updating: %s, cur state: %s", - self._attr_unique_id, - self.state, - ) - self._attr_is_on = self.entity_description.boolean_map.get( - self.device.appliance.status.get(self.bsh_key, {}).get(ATTR_VALUE) - ) - self._attr_available = self._attr_is_on is not None - _LOGGER.debug( - "Updated: %s, new state: %s", - self._attr_unique_id, - self.state, + super().__init__( + device, + HomeConnectBinarySensorEntityDescription( + key=BSH_DOOR_STATE, + device_class=BinarySensorDeviceClass.DOOR, + boolean_map={ + BSH_DOOR_STATE_CLOSED: False, + BSH_DOOR_STATE_LOCKED: False, + BSH_DOOR_STATE_OPEN: True, + }, + ), ) + self._attr_unique_id = f"{device.appliance.haId}-Door" + self._attr_name = f"{device.appliance.name} Door" diff --git a/homeassistant/components/home_connect/entity.py b/homeassistant/components/home_connect/entity.py index 6cad310f76a..0ae4a28b8d4 100644 --- a/homeassistant/components/home_connect/entity.py +++ b/homeassistant/components/home_connect/entity.py @@ -5,7 +5,7 @@ import logging from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from .api import HomeConnectDevice from .const import DOMAIN, SIGNAL_UPDATE_ENTITIES @@ -17,13 +17,13 @@ class HomeConnectEntity(Entity): """Generic Home Connect entity (base class).""" _attr_should_poll = False + _attr_has_entity_name = True - def __init__(self, device: HomeConnectDevice, bsh_key: str, desc: str) -> None: + def __init__(self, device: HomeConnectDevice, desc: EntityDescription) -> 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}-{bsh_key}" + self.entity_description = desc + self._attr_unique_id = f"{device.appliance.haId}-{self.bsh_key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device.appliance.haId)}, manufacturer=device.appliance.brand, @@ -50,3 +50,8 @@ class HomeConnectEntity(Entity): """Update the entity.""" _LOGGER.debug("Entity update triggered on %s", self) self.async_schedule_update_ha_state(True) + + @property + def bsh_key(self) -> str: + """Return the BSH key.""" + return self.entity_description.key diff --git a/homeassistant/components/home_connect/icons.json b/homeassistant/components/home_connect/icons.json index 949b30919b5..92ed72c142f 100644 --- a/homeassistant/components/home_connect/icons.json +++ b/homeassistant/components/home_connect/icons.json @@ -23,43 +23,127 @@ } }, "entity": { + "binary_sensor": { + "remote_control": { + "default": "mdi:remote", + "state": { + "off": "mdi:remote-off" + } + }, + "remote_start": { + "default": "mdi:remote", + "state": { + "off": "mdi:remote-off" + } + }, + "dust_box_inserted": { + "default": "mdi:download" + }, + "lifted": { + "default": "mdi:arrow-up-right-bold" + }, + "lost": { + "default": "mdi:map-marker-remove-variant" + } + }, "sensor": { - "alarm_sensor_fridge": { + "operation_state": { + "default": "mdi:state-machine", + "state": { + "inactive": "mdi:stop", + "ready": "mdi:check-circle", + "delayedstart": "mdi:progress-clock", + "run": "mdi:play", + "pause": "mdi:pause", + "actionrequired": "mdi:gesture-tap", + "finished": "mdi:flag-checkered", + "error": "mdi:alert-circle", + "aborting": "mdi:close-circle" + } + }, + "program_progress": { + "default": "mdi:progress-clock" + }, + "coffee_counter": { + "default": "mdi:coffee" + }, + "powder_coffee_counter": { + "default": "mdi:coffee" + }, + "hot_water_counter": { + "default": "mdi:cup-water" + }, + "hot_water_cups_counter": { + "default": "mdi:cup" + }, + "hot_milk_counter": { + "default": "mdi:cup" + }, + "frothy_milk_counter": { + "default": "mdi:cup" + }, + "milk_counter": { + "default": "mdi:cup" + }, + "coffee_and_milk": { + "default": "mdi:coffee" + }, + "ristretto_espresso_counter": { + "default": "mdi:coffee" + }, + "camera_state": { + "default": "mdi:camera", + "state": { + "disabled": "mdi:camera-off", + "sleeping": "mdi:sleep", + "error": "mdi:alert-circle-outline" + } + }, + "last_selected_map": { + "default": "mdi:map", + "state": { + "tempmap": "mdi:map-clock-outline", + "map1": "mdi:numeric-1", + "map2": "mdi:numeric-2", + "map3": "mdi:numeric-3" + } + }, + "refrigerator_door_alarm": { "default": "mdi:fridge", "state": { "confirmed": "mdi:fridge-alert-outline", "present": "mdi:fridge-alert" } }, - "alarm_sensor_freezer": { + "freezer_door_alarm": { "default": "mdi:snowflake", "state": { "confirmed": "mdi:snowflake-check", "present": "mdi:snowflake-alert" } }, - "alarm_sensor_temp": { + "freezer_temperature_alarm": { "default": "mdi:thermometer", "state": { "confirmed": "mdi:thermometer-check", "present": "mdi:thermometer-alert" } }, - "alarm_sensor_coffee_bean_container": { + "bean_container_empty": { "default": "mdi:coffee-maker", "state": { "confirmed": "mdi:coffee-maker-check", "present": "mdi:coffee-maker-outline" } }, - "alarm_sensor_coffee_water_tank": { + "water_tank_empty": { "default": "mdi:water", "state": { "confirmed": "mdi:water-check", "present": "mdi:water-alert" } }, - "alarm_sensor_coffee_drip_tray": { + "drip_tray_full": { "default": "mdi:tray", "state": { "confirmed": "mdi:tray-full", @@ -68,11 +152,51 @@ } }, "switch": { - "refrigeration_dispenser": { + "power": { + "default": "mdi:power" + }, + "child_lock": { + "default": "mdi:lock", + "state": { + "on": "mdi:lock", + "off": "mdi:lock-off" + } + }, + "cup_warmer": { + "default": "mdi:heat-wave" + }, + "refrigerator_super_mode": { + "default": "mdi:speedometer" + }, + "freezer_super_mode": { + "default": "mdi:speedometer" + }, + "eco_mode": { + "default": "mdi:sprout" + }, + "cooking-oven-setting-sabbath_mode": { + "default": "mdi:volume-mute" + }, + "sabbath_mode": { + "default": "mdi:volume-mute" + }, + "vacation_mode": { + "default": "mdi:beach" + }, + "fresh_mode": { + "default": "mdi:leaf" + }, + "dispenser_enabled": { "default": "mdi:snowflake", "state": { "off": "mdi:snowflake-off" } + }, + "door-assistant_fridge": { + "default": "mdi:door" + }, + "door-assistant_freezer": { + "default": "mdi:door" } } } diff --git a/homeassistant/components/home_connect/light.py b/homeassistant/components/home_connect/light.py index 7f6ea1bb4be..0308c6fcfbb 100644 --- a/homeassistant/components/home_connect/light.py +++ b/homeassistant/components/home_connect/light.py @@ -15,7 +15,6 @@ from homeassistant.components.light import ( LightEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEVICE, CONF_ENTITIES from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util @@ -27,6 +26,8 @@ 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, @@ -43,20 +44,19 @@ _LOGGER = logging.getLogger(__name__) class HomeConnectLightEntityDescription(LightEntityDescription): """Light entity description.""" - desc: str brightness_key: str | None LIGHTS: tuple[HomeConnectLightEntityDescription, ...] = ( HomeConnectLightEntityDescription( key=REFRIGERATION_INTERNAL_LIGHT_POWER, - desc="Internal Light", brightness_key=REFRIGERATION_INTERNAL_LIGHT_BRIGHTNESS, + translation_key="internal_light", ), HomeConnectLightEntityDescription( key=REFRIGERATION_EXTERNAL_LIGHT_POWER, - desc="External Light", brightness_key=REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS, + translation_key="external_light", ), ) @@ -72,11 +72,29 @@ async def async_setup_entry( """Get a list of entities.""" entities: list[LightEntity] = [] hc_api = hass.data[DOMAIN][config_entry.entry_id] - for device_dict in hc_api.devices: - entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("light", []) - entity_list = [HomeConnectLight(**d) for d in entity_dicts] - device: HomeConnectDevice = device_dict[CONF_DEVICE] - # Auto-discover entities + for device in hc_api.devices: + if COOKING_LIGHTING in device.appliance.status: + entities.append( + HomeConnectLight( + device, + LightEntityDescription( + key=COOKING_LIGHTING, + translation_key="cooking_lighting", + ), + False, + ) + ) + if BSH_AMBIENT_LIGHT_ENABLED in device.appliance.status: + entities.append( + HomeConnectLight( + device, + LightEntityDescription( + key=BSH_AMBIENT_LIGHT_ENABLED, + translation_key="ambient_light", + ), + True, + ) + ) entities.extend( HomeConnectCoolingLight( device=device, @@ -86,7 +104,6 @@ async def async_setup_entry( for description in LIGHTS if description.key in device.appliance.status ) - entities.extend(entity_list) return entities async_add_entities(await hass.async_add_executor_job(get_entities), True) @@ -95,11 +112,16 @@ async def async_setup_entry( class HomeConnectLight(HomeConnectEntity, LightEntity): """Light for Home Connect.""" + entity_description: LightEntityDescription + def __init__( - self, device: HomeConnectDevice, bsh_key: str, desc: str, ambient: bool + self, + device: HomeConnectDevice, + desc: LightEntityDescription, + ambient: bool, ) -> None: """Initialize the entity.""" - super().__init__(device, bsh_key, desc) + super().__init__(device, desc) self._ambient = ambient self._percentage_scale = (10, 100) self._brightness_key: str | None @@ -255,9 +277,7 @@ class HomeConnectCoolingLight(HomeConnectLight): entity_description: HomeConnectLightEntityDescription, ) -> None: """Initialize Cooling Light Entity.""" - super().__init__( - device, entity_description.key, entity_description.desc, ambient - ) + super().__init__(device, entity_description, ambient) self.entity_description = entity_description 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 599156a6b3a..f241ec0f265 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -1,26 +1,29 @@ """Provides a sensor for Home Connect.""" -from dataclasses import dataclass, field +import contextlib +from dataclasses import dataclass from datetime import datetime, timedelta import logging from typing import cast +from homeconnect.api import HomeConnectError + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ENTITIES +from homeassistant.const import PERCENTAGE, UnitOfTime, UnitOfVolume from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify import homeassistant.util.dt as dt_util -from .api import ConfigEntryAuth, HomeConnectDevice +from .api import ConfigEntryAuth from .const import ( - ATTR_DEVICE, ATTR_VALUE, - BSH_EVENT_PRESENT_STATE_OFF, BSH_OPERATION_STATE, BSH_OPERATION_STATE_FINISHED, BSH_OPERATION_STATE_PAUSE, @@ -38,47 +41,182 @@ from .entity import HomeConnectEntity _LOGGER = logging.getLogger(__name__) +EVENT_OPTIONS = ["confirmed", "off", "present"] + + @dataclass(frozen=True, kw_only=True) class HomeConnectSensorEntityDescription(SensorEntityDescription): """Entity Description class for sensors.""" - device_class: SensorDeviceClass | None = SensorDeviceClass.ENUM - options: list[str] | None = field( - default_factory=lambda: ["confirmed", "off", "present"] - ) - desc: str - appliance_types: tuple[str, ...] + default_value: str | None = None + appliance_types: tuple[str, ...] | None = None + sign: int = 1 -SENSORS: tuple[HomeConnectSensorEntityDescription, ...] = ( +BSH_PROGRAM_SENSORS = ( + HomeConnectSensorEntityDescription( + key="BSH.Common.Option.RemainingProgramTime", + device_class=SensorDeviceClass.TIMESTAMP, + sign=1, + translation_key="program_finish_time", + ), + HomeConnectSensorEntityDescription( + key="BSH.Common.Option.Duration", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + sign=1, + ), + HomeConnectSensorEntityDescription( + key="BSH.Common.Option.ProgramProgress", + native_unit_of_measurement=PERCENTAGE, + sign=1, + translation_key="program_progress", + ), +) + +SENSORS = ( + HomeConnectSensorEntityDescription( + key=BSH_OPERATION_STATE, + device_class=SensorDeviceClass.ENUM, + options=[ + "inactive", + "ready", + "delayedstart", + "run", + "pause", + "actionrequired", + "finished", + "error", + "aborting", + ], + translation_key="operation_state", + ), + HomeConnectSensorEntityDescription( + key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterCoffee", + state_class=SensorStateClass.TOTAL_INCREASING, + translation_key="coffee_counter", + ), + HomeConnectSensorEntityDescription( + key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterPowderCoffee", + state_class=SensorStateClass.TOTAL_INCREASING, + translation_key="powder_coffee_counter", + ), + HomeConnectSensorEntityDescription( + key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterHotWater", + native_unit_of_measurement=UnitOfVolume.MILLILITERS, + device_class=SensorDeviceClass.VOLUME, + state_class=SensorStateClass.TOTAL_INCREASING, + translation_key="hot_water_counter", + ), + HomeConnectSensorEntityDescription( + key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterHotWaterCups", + state_class=SensorStateClass.TOTAL_INCREASING, + translation_key="hot_water_cups_counter", + ), + HomeConnectSensorEntityDescription( + key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterHotMilk", + state_class=SensorStateClass.TOTAL_INCREASING, + translation_key="hot_milk_counter", + ), + HomeConnectSensorEntityDescription( + key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterFrothyMilk", + state_class=SensorStateClass.TOTAL_INCREASING, + translation_key="frothy_milk_counter", + ), + HomeConnectSensorEntityDescription( + key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterMilk", + state_class=SensorStateClass.TOTAL_INCREASING, + translation_key="milk_counter", + ), + HomeConnectSensorEntityDescription( + key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterCoffeeAndMilk", + state_class=SensorStateClass.TOTAL_INCREASING, + translation_key="coffee_and_milk_counter", + ), + HomeConnectSensorEntityDescription( + key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterRistrettoEspresso", + state_class=SensorStateClass.TOTAL_INCREASING, + translation_key="ristretto_espresso_counter", + ), + HomeConnectSensorEntityDescription( + key="BSH.Common.Status.BatteryLevel", + device_class=SensorDeviceClass.BATTERY, + translation_key="battery_level", + ), + HomeConnectSensorEntityDescription( + key="BSH.Common.Status.Video.CameraState", + device_class=SensorDeviceClass.ENUM, + options=[ + "disabled", + "sleeping", + "ready", + "streaminglocal", + "streamingcloud", + "streaminglocalancloud", + "error", + ], + translation_key="camera_state", + ), + HomeConnectSensorEntityDescription( + key="ConsumerProducts.CleaningRobot.Status.LastSelectedMap", + device_class=SensorDeviceClass.ENUM, + options=[ + "tempmap", + "map1", + "map2", + "map3", + ], + translation_key="last_selected_map", + ), +) + +EVENT_SENSORS = ( HomeConnectSensorEntityDescription( key=REFRIGERATION_EVENT_DOOR_ALARM_FREEZER, - desc="Door Alarm Freezer", + device_class=SensorDeviceClass.ENUM, + options=EVENT_OPTIONS, + default_value="off", + translation_key="freezer_door_alarm", appliance_types=("FridgeFreezer", "Freezer"), ), HomeConnectSensorEntityDescription( key=REFRIGERATION_EVENT_DOOR_ALARM_REFRIGERATOR, - desc="Door Alarm Refrigerator", + device_class=SensorDeviceClass.ENUM, + options=EVENT_OPTIONS, + default_value="off", + translation_key="refrigerator_door_alarm", appliance_types=("FridgeFreezer", "Refrigerator"), ), HomeConnectSensorEntityDescription( key=REFRIGERATION_EVENT_TEMP_ALARM_FREEZER, - desc="Temperature Alarm Freezer", + device_class=SensorDeviceClass.ENUM, + options=EVENT_OPTIONS, + default_value="off", + translation_key="freezer_temperature_alarm", appliance_types=("FridgeFreezer", "Freezer"), ), HomeConnectSensorEntityDescription( key=COFFEE_EVENT_BEAN_CONTAINER_EMPTY, - desc="Bean Container Empty", + device_class=SensorDeviceClass.ENUM, + options=EVENT_OPTIONS, + default_value="off", + translation_key="bean_container_empty", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=COFFEE_EVENT_WATER_TANK_EMPTY, - desc="Water Tank Empty", + device_class=SensorDeviceClass.ENUM, + options=EVENT_OPTIONS, + default_value="off", + translation_key="water_tank_empty", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=COFFEE_EVENT_DRIP_TRAY_FULL, - desc="Drip Tray Full", + device_class=SensorDeviceClass.ENUM, + options=EVENT_OPTIONS, + default_value="off", + translation_key="drip_tray_full", appliance_types=("CoffeeMaker",), ), ) @@ -95,18 +233,25 @@ async def async_setup_entry( """Get a list of entities.""" entities: list[SensorEntity] = [] hc_api: ConfigEntryAuth = hass.data[DOMAIN][config_entry.entry_id] - for device_dict in hc_api.devices: - entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("sensor", []) - entities += [HomeConnectSensor(**d) for d in entity_dicts] - device: HomeConnectDevice = device_dict[ATTR_DEVICE] - # Auto-discover entities + for device in hc_api.devices: entities.extend( - HomeConnectAlarmSensor( + HomeConnectSensor( device, - entity_description=description, + description, ) + for description in EVENT_SENSORS + if description.appliance_types + and device.appliance.type in description.appliance_types + ) + with contextlib.suppress(HomeConnectError): + if device.appliance.get_programs_available(): + entities.extend( + HomeConnectSensor(device, desc) for desc in BSH_PROGRAM_SENSORS + ) + entities.extend( + HomeConnectSensor(device, description) for description in SENSORS - if device.appliance.type in description.appliance_types + if description.key in device.appliance.status ) return entities @@ -116,25 +261,7 @@ async def async_setup_entry( class HomeConnectSensor(HomeConnectEntity, SensorEntity): """Sensor class for Home Connect.""" - _key: str - _sign: int - - def __init__( - self, - device: HomeConnectDevice, - bsh_key: str, - desc: str, - unit: str, - icon: str, - device_class: SensorDeviceClass, - sign: int = 1, - ) -> None: - """Initialize the entity.""" - super().__init__(device, bsh_key, desc) - self._sign = sign - self._attr_native_unit_of_measurement = unit - self._attr_icon = icon - self._attr_device_class = device_class + entity_description: HomeConnectSensorEntityDescription @property def available(self) -> bool: @@ -143,78 +270,52 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity): async def async_update(self) -> None: """Update the sensor's status.""" - status = self.device.appliance.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.bsh_key]: - self._attr_native_value = None - elif ( - self._attr_native_value is not None - and self._sign == 1 - and isinstance(self._attr_native_value, datetime) - and self._attr_native_value < dt_util.utcnow() - ): - # if the date is supposed to be in the future but we're - # already past it, set state to None. - self._attr_native_value = None - elif ( - BSH_OPERATION_STATE in status - and ATTR_VALUE in status[BSH_OPERATION_STATE] - and status[BSH_OPERATION_STATE][ATTR_VALUE] - in [ - BSH_OPERATION_STATE_RUN, - BSH_OPERATION_STATE_PAUSE, - BSH_OPERATION_STATE_FINISHED, - ] - ): - 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.bsh_key].get(ATTR_VALUE) - if self.bsh_key == BSH_OPERATION_STATE: + appliance_status = self.device.appliance.status + if ( + self.bsh_key not in appliance_status + or ATTR_VALUE not in appliance_status[self.bsh_key] + ): + self._attr_native_value = self.entity_description.default_value + _LOGGER.debug("Updated, new state: %s", self._attr_native_value) + return + status = appliance_status[self.bsh_key] + match self.device_class: + case SensorDeviceClass.TIMESTAMP: + if ATTR_VALUE not in status: + self._attr_native_value = None + elif ( + self._attr_native_value is not None + and self.entity_description.sign == 1 + and isinstance(self._attr_native_value, datetime) + and self._attr_native_value < dt_util.utcnow() + ): + # if the date is supposed to be in the future but we're + # already past it, set state to None. + self._attr_native_value = None + elif ( + BSH_OPERATION_STATE + in (appliance_status := self.device.appliance.status) + and ATTR_VALUE in appliance_status[BSH_OPERATION_STATE] + and appliance_status[BSH_OPERATION_STATE][ATTR_VALUE] + in [ + BSH_OPERATION_STATE_RUN, + BSH_OPERATION_STATE_PAUSE, + BSH_OPERATION_STATE_FINISHED, + ] + ): + seconds = self.entity_description.sign * float(status[ATTR_VALUE]) + self._attr_native_value = dt_util.utcnow() + timedelta( + seconds=seconds + ) + else: + self._attr_native_value = None + case SensorDeviceClass.ENUM: # 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 - self._attr_native_value = cast(str, self._attr_native_value).split(".")[ - -1 - ] + self._attr_native_value = slugify( + cast(str, status.get(ATTR_VALUE)).split(".")[-1] + ) + case _: + self._attr_native_value = status.get(ATTR_VALUE) _LOGGER.debug("Updated, new state: %s", self._attr_native_value) - - -class HomeConnectAlarmSensor(HomeConnectEntity, SensorEntity): - """Sensor entity setup using SensorEntityDescription.""" - - entity_description: HomeConnectSensorEntityDescription - - def __init__( - self, - device: HomeConnectDevice, - entity_description: HomeConnectSensorEntityDescription, - ) -> None: - """Initialize the entity.""" - self.entity_description = entity_description - super().__init__( - device, self.entity_description.key, self.entity_description.desc - ) - - @property - def available(self) -> bool: - """Return true if the sensor is available.""" - return self._attr_native_value is not None - - async def async_update(self) -> None: - """Update the sensor's status.""" - self._attr_native_value = ( - self.device.appliance.status.get(self.bsh_key, {}) - .get(ATTR_VALUE, BSH_EVENT_PRESENT_STATE_OFF) - .rsplit(".", maxsplit=1)[-1] - .lower() - ) - _LOGGER.debug( - "Updated: %s, new state: %s", - self._attr_unique_id, - self._attr_native_value, - ) diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 1fcd95e9cb2..9fe967fb5d1 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -135,43 +135,220 @@ } }, "entity": { + "binary_sensor": { + "remote_control": { + "name": "Remote control" + }, + "remote_start": { + "name": "Remote start" + }, + "local_control": { + "name": "Local control" + }, + "battery_charging_state": { + "name": "Battery charging state" + }, + "charging_connection": { + "name": "Charging connection" + }, + "dust_box_inserted": { + "name": "Dust box", + "state": { + "on": "Inserted", + "off": "Not inserted" + } + }, + "lifted": { + "name": "Lifted" + }, + "lost": { + "name": "Lost" + }, + "chiller_door": { + "name": "Chiller door" + }, + "freezer_door": { + "name": "Freezer door" + }, + "refrigerator_door": { + "name": "Refrigerator door" + } + }, + "light": { + "cooking_lighting": { + "name": "Functional light" + }, + "ambient_light": { + "name": "Ambient light" + }, + "external_light": { + "name": "External light" + }, + "internal_light": { + "name": "Internal light" + } + }, "sensor": { - "alarm_sensor_fridge": { + "program_progress": { + "name": "Program progress" + }, + "program_finish_time": { + "name": "Program finish time" + }, + "operation_state": { + "name": "Operation state", + "state": { + "inactive": "Inactive", + "ready": "Ready", + "delayedstart": "Delayed start", + "run": "Run", + "pause": "[%key:common::state::paused%]", + "actionrequired": "Action required", + "finished": "Finished", + "error": "Error", + "aborting": "Aborting" + } + }, + "coffee_counter": { + "name": "Coffees" + }, + "powder_coffee_counter": { + "name": "Powder coffees" + }, + "hot_water_counter": { + "name": "Hot water" + }, + "hot_water_cups_counter": { + "name": "Hot water cups" + }, + "hot_milk_counter": { + "name": "Hot milk cups" + }, + "frothy_milk_counter": { + "name": "Frothy milk cups" + }, + "milk_counter": { + "name": "Milk cups" + }, + "coffee_and_milk_counter": { + "name": "Coffee and milk cups" + }, + "ristretto_espresso_counter": { + "name": "Ristretto espresso cups" + }, + "battery_level": { + "name": "Battery level" + }, + "camera_state": { + "name": "Camera state", + "state": { + "disabled": "[%key:common::state::disabled%]", + "sleeping": "Sleeping", + "ready": "Ready", + "streaminglocal": "Streaming local", + "streamingcloud": "Streaming cloud", + "streaminglocal_and_cloud": "Streaming local and cloud", + "error": "Error" + } + }, + "last_selected_map": { + "name": "Last selected map", + "state": { + "tempmap": "Temporary map", + "map1": "Map 1", + "map2": "Map 2", + "map3": "Map 3" + } + }, + "freezer_door_alarm": { + "name": "Freezer door alarm", "state": { "confirmed": "[%key:component::home_connect::common::confirmed%]", "present": "[%key:component::home_connect::common::present%]" } }, - "alarm_sensor_freezer": { + "refrigerator_door_alarm": { + "name": "Refrigerator door alarm", "state": { + "off": "[%key:common::state::off%]", "confirmed": "[%key:component::home_connect::common::confirmed%]", "present": "[%key:component::home_connect::common::present%]" } }, - "alarm_sensor_temp": { + "freezer_temperature_alarm": { + "name": "Freezer temperature alarm", "state": { + "off": "[%key:common::state::off%]", "confirmed": "[%key:component::home_connect::common::confirmed%]", "present": "[%key:component::home_connect::common::present%]" } }, - "alarm_sensor_coffee_bean_container": { + "bean_container_empty": { + "name": "Bean container empty", "state": { + "off": "[%key:common::state::off%]", "confirmed": "[%key:component::home_connect::common::confirmed%]", "present": "[%key:component::home_connect::common::present%]" } }, - "alarm_sensor_coffee_water_tank": { + "water_tank_empty": { + "name": "Water tank empty", "state": { + "off": "[%key:common::state::off%]", "confirmed": "[%key:component::home_connect::common::confirmed%]", "present": "[%key:component::home_connect::common::present%]" } }, - "alarm_sensor_coffee_drip_tray": { + "drip_tray_full": { + "name": "Drip tray full", "state": { + "off": "[%key:common::state::off%]", "confirmed": "[%key:component::home_connect::common::confirmed%]", "present": "[%key:component::home_connect::common::present%]" } } + }, + "switch": { + "power": { + "name": "Power" + }, + "child_lock": { + "name": "Child lock" + }, + "cup_warmer": { + "name": "Cup warmer" + }, + "refrigerator_super_mode": { + "name": "Refrigerator super mode" + }, + "freezer_super_mode": { + "name": "Freezer super mode" + }, + "eco_mode": { + "name": "Eco mode" + }, + "sabbath_mode": { + "name": "Sabbath mode" + }, + "vacation_mode": { + "name": "Vacation mode" + }, + "fresh_mode": { + "name": "Fresh mode" + }, + "dispenser_enabled": { + "name": "Dispenser", + "state": { + "off": "[%key:common::state::disabled%]", + "on": "[%key:common::state::enabled%]" + } + }, + "door_assistant_fridge": { + "name": "Fridge door assistant" + }, + "door_assistant_freezer": { + "name": "Freezer door assistant" + } } } } diff --git a/homeassistant/components/home_connect/switch.py b/homeassistant/components/home_connect/switch.py index 6e96b371b82..536c82c4454 100644 --- a/homeassistant/components/home_connect/switch.py +++ b/homeassistant/components/home_connect/switch.py @@ -1,6 +1,6 @@ """Provides a switch for Home Connect.""" -from dataclasses import dataclass +import contextlib import logging from typing import Any @@ -8,7 +8,6 @@ from homeconnect.api import HomeConnectError from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEVICE, CONF_ENTITIES from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -18,7 +17,9 @@ from .const import ( BSH_ACTIVE_PROGRAM, BSH_CHILD_LOCK_STATE, BSH_OPERATION_STATE, + BSH_POWER_OFF, BSH_POWER_ON, + BSH_POWER_STANDBY, BSH_POWER_STATE, DOMAIN, REFRIGERATION_DISPENSER, @@ -29,26 +30,71 @@ from .entity import HomeConnectDevice, HomeConnectEntity _LOGGER = logging.getLogger(__name__) - -@dataclass(frozen=True, kw_only=True) -class HomeConnectSwitchEntityDescription(SwitchEntityDescription): - """Switch entity description.""" - - desc: str +APPLIANCES_WITH_PROGRAMS = ( + "CleaningRobot", + "CoffeeMachine", + "Dishwasher", + "Dryer", + "Hood", + "Oven", + "WarmingDrawer", + "Washer", + "WasherDryer", +) -SWITCHES: tuple[HomeConnectSwitchEntityDescription, ...] = ( - HomeConnectSwitchEntityDescription( - key=REFRIGERATION_SUPERMODEFREEZER, - desc="Supermode Freezer", +SWITCHES = ( + SwitchEntityDescription( + key=BSH_CHILD_LOCK_STATE, + translation_key="child_lock", ), - HomeConnectSwitchEntityDescription( + SwitchEntityDescription( + key="ConsumerProducts.CoffeeMaker.Setting.CupWarmer", + translation_key="cup_warmer", + ), + SwitchEntityDescription( key=REFRIGERATION_SUPERMODEREFRIGERATOR, - desc="Supermode Refrigerator", + translation_key="cup_warmer", ), - HomeConnectSwitchEntityDescription( + SwitchEntityDescription( + key=REFRIGERATION_SUPERMODEFREEZER, + translation_key="freezer_super_mode", + ), + SwitchEntityDescription( + key=REFRIGERATION_SUPERMODEREFRIGERATOR, + translation_key="refrigerator_super_mode", + ), + SwitchEntityDescription( + key="Refrigeration.Common.Setting.EcoMode", + translation_key="eco_mode", + ), + SwitchEntityDescription( + key="Cooking.Oven.Setting.SabbathMode", + translation_key="sabbath_mode", + ), + SwitchEntityDescription( + key="Refrigeration.Common.Setting.SabbathMode", + translation_key="sabbath_mode", + ), + SwitchEntityDescription( + key="Refrigeration.Common.Setting.VacationMode", + translation_key="vacation_mode", + ), + SwitchEntityDescription( + key="Refrigeration.Common.Setting.FreshMode", + translation_key="fresh_mode", + ), + SwitchEntityDescription( key=REFRIGERATION_DISPENSER, - desc="Dispenser Enabled", + translation_key="dispenser_enabled", + ), + SwitchEntityDescription( + key="Refrigeration.Common.Setting.Door.AssistantFridge", + translation_key="door_assistant_fridge", + ), + SwitchEntityDescription( + key="Refrigeration.Common.Setting.Door.AssistantFreezer", + translation_key="door_assistant_freezer", ), ) @@ -64,17 +110,20 @@ async def async_setup_entry( """Get a list of entities.""" entities: list[SwitchEntity] = [] hc_api: ConfigEntryAuth = hass.data[DOMAIN][config_entry.entry_id] - for device_dict in hc_api.devices: - entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("switch", []) - entities.extend(HomeConnectProgramSwitch(**d) for d in entity_dicts) - entities.append(HomeConnectPowerSwitch(device_dict[CONF_DEVICE])) - entities.append(HomeConnectChildLockSwitch(device_dict[CONF_DEVICE])) - # Auto-discover entities - hc_device: HomeConnectDevice = device_dict[CONF_DEVICE] + for device in hc_api.devices: + if device.appliance.type in APPLIANCES_WITH_PROGRAMS: + with contextlib.suppress(HomeConnectError): + programs = device.appliance.get_programs_available() + if programs: + entities.extend( + HomeConnectProgramSwitch(device, program) + for program in programs + ) + entities.append(HomeConnectPowerSwitch(device)) entities.extend( - HomeConnectSwitch(device=hc_device, entity_description=description) + HomeConnectSwitch(device, description) for description in SWITCHES - if description.key in hc_device.appliance.status + if description.key in device.appliance.status ) return entities @@ -85,18 +134,6 @@ async def async_setup_entry( class HomeConnectSwitch(HomeConnectEntity, SwitchEntity): """Generic switch class for Home Connect Binary Settings.""" - entity_description: HomeConnectSwitchEntityDescription - - def __init__( - self, - device: HomeConnectDevice, - entity_description: HomeConnectSwitchEntityDescription, - ) -> None: - """Initialize the entity.""" - self.entity_description = entity_description - self._attr_available = False - super().__init__(device, entity_description.key, entity_description.desc) - async def async_turn_on(self, **kwargs: Any) -> None: """Turn on setting.""" @@ -153,7 +190,9 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity): desc = " ".join( ["Program", program_name.split(".")[-3], program_name.split(".")[-1]] ) - super().__init__(device, desc, desc) + super().__init__(device, SwitchEntityDescription(key=program_name)) + self._attr_name = f"{device.appliance.name} {desc}" + self._attr_has_entity_name = False self.program_name = program_name async def async_turn_on(self, **kwargs: Any) -> None: @@ -189,9 +228,27 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity): class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): """Power switch class for Home Connect.""" + power_off_state: str | None + def __init__(self, device: HomeConnectDevice) -> None: """Initialize the entity.""" - super().__init__(device, BSH_POWER_STATE, "Power") + super().__init__( + device, + SwitchEntityDescription(key=BSH_POWER_STATE, translation_key="power"), + ) + match device.appliance.type: + case "Dishwasher" | "Cooktop" | "Hood": + self.power_off_state = BSH_POWER_OFF + case ( + "Oven" + | "WarmDrawer" + | "CoffeeMachine" + | "CleaningRobot" + | "CookProcessor" + ): + self.power_off_state = BSH_POWER_STANDBY + case _: + self.power_off_state = None async def async_turn_on(self, **kwargs: Any) -> None: """Switch the device on.""" @@ -207,12 +264,15 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Switch the device off.""" + if self.power_off_state is None: + _LOGGER.debug("This appliance type does not support turning off") + return _LOGGER.debug("tried to switch off %s", self.name) try: await self.hass.async_add_executor_job( self.device.appliance.set_setting, BSH_POWER_STATE, - self.device.power_off_state, + self.power_off_state, ) except HomeConnectError as err: _LOGGER.error("Error while trying to turn off device: %s", err) @@ -228,7 +288,7 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): self._attr_is_on = True elif ( self.device.appliance.status.get(BSH_POWER_STATE, {}).get(ATTR_VALUE) - == self.device.power_off_state + == self.power_off_state ): self._attr_is_on = False elif self.device.appliance.status.get(BSH_OPERATION_STATE, {}).get( @@ -251,44 +311,3 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): else: self._attr_is_on = None _LOGGER.debug("Updated, new state: %s", self._attr_is_on) - - -class HomeConnectChildLockSwitch(HomeConnectEntity, SwitchEntity): - """Child lock switch class for Home Connect.""" - - def __init__(self, device: HomeConnectDevice) -> None: - """Initialize the entity.""" - super().__init__(device, BSH_CHILD_LOCK_STATE, "ChildLock") - - async def async_turn_on(self, **kwargs: Any) -> None: - """Switch child lock on.""" - _LOGGER.debug("Tried to switch child lock on device: %s", self.name) - try: - await self.hass.async_add_executor_job( - self.device.appliance.set_setting, BSH_CHILD_LOCK_STATE, True - ) - except HomeConnectError as err: - _LOGGER.error("Error while trying to turn on child lock on device: %s", err) - self._attr_is_on = False - self.async_entity_update() - - async def async_turn_off(self, **kwargs: Any) -> None: - """Switch child lock off.""" - _LOGGER.debug("Tried to switch off child lock on device: %s", self.name) - try: - await self.hass.async_add_executor_job( - self.device.appliance.set_setting, BSH_CHILD_LOCK_STATE, False - ) - except HomeConnectError as err: - _LOGGER.error( - "Error while trying to turn off child lock on device: %s", err - ) - self._attr_is_on = True - self.async_entity_update() - - async def async_update(self) -> None: - """Update the switch's status.""" - self._attr_is_on = False - if self.device.appliance.status.get(BSH_CHILD_LOCK_STATE, {}).get(ATTR_VALUE): - self._attr_is_on = True - _LOGGER.debug("Updated child lock, new state: %s", self._attr_is_on) diff --git a/tests/components/home_connect/test_binary_sensor.py b/tests/components/home_connect/test_binary_sensor.py index de4263f6345..990943a34e6 100644 --- a/tests/components/home_connect/test_binary_sensor.py +++ b/tests/components/home_connect/test_binary_sensor.py @@ -68,9 +68,9 @@ async def test_binary_sensors_door_states( entity_id = "binary_sensor.washer_door" get_appliances.return_value = [appliance] assert config_entry.state == ConfigEntryState.NOT_LOADED + appliance.status.update({BSH_DOOR_STATE: {"value": state}}) assert await integration_setup() assert config_entry.state == ConfigEntryState.LOADED - appliance.status.update({BSH_DOOR_STATE: {"value": state}}) await async_update_entity(hass, entity_id) await hass.async_block_till_done() assert hass.states.is_state(entity_id, expected) diff --git a/tests/components/home_connect/test_light.py b/tests/components/home_connect/test_light.py index 7d375ce0b62..70c23f73c0a 100644 --- a/tests/components/home_connect/test_light.py +++ b/tests/components/home_connect/test_light.py @@ -67,7 +67,7 @@ async def test_light( ("entity_id", "status", "service", "service_data", "state", "appliance"), [ ( - "light.hood_light", + "light.hood_functional_light", { COOKING_LIGHTING: { "value": True, @@ -79,7 +79,7 @@ async def test_light( "Hood", ), ( - "light.hood_light", + "light.hood_functional_light", { COOKING_LIGHTING: { "value": True, @@ -92,7 +92,7 @@ async def test_light( "Hood", ), ( - "light.hood_light", + "light.hood_functional_light", { COOKING_LIGHTING: {"value": False}, COOKING_LIGHTING_BRIGHTNESS: {"value": 70}, @@ -103,7 +103,7 @@ async def test_light( "Hood", ), ( - "light.hood_light", + "light.hood_functional_light", { COOKING_LIGHTING: { "value": None, @@ -116,7 +116,7 @@ async def test_light( "Hood", ), ( - "light.hood_ambientlight", + "light.hood_ambient_light", { BSH_AMBIENT_LIGHT_ENABLED: { "value": True, @@ -129,7 +129,7 @@ async def test_light( "Hood", ), ( - "light.hood_ambientlight", + "light.hood_ambient_light", { BSH_AMBIENT_LIGHT_ENABLED: {"value": False}, BSH_AMBIENT_LIGHT_BRIGHTNESS: {"value": 70}, @@ -140,7 +140,7 @@ async def test_light( "Hood", ), ( - "light.hood_ambientlight", + "light.hood_ambient_light", { BSH_AMBIENT_LIGHT_ENABLED: {"value": True}, BSH_AMBIENT_LIGHT_CUSTOM_COLOR: {}, @@ -218,7 +218,7 @@ async def test_light_functionality( ), [ ( - "light.hood_light", + "light.hood_functional_light", { COOKING_LIGHTING: { "value": False, @@ -231,7 +231,7 @@ async def test_light_functionality( "Hood", ), ( - "light.hood_light", + "light.hood_functional_light", { COOKING_LIGHTING: { "value": True, @@ -245,7 +245,7 @@ async def test_light_functionality( "Hood", ), ( - "light.hood_light", + "light.hood_functional_light", { COOKING_LIGHTING: {"value": False}, }, @@ -256,7 +256,7 @@ async def test_light_functionality( "Hood", ), ( - "light.hood_ambientlight", + "light.hood_ambient_light", { BSH_AMBIENT_LIGHT_ENABLED: { "value": True, @@ -270,7 +270,7 @@ async def test_light_functionality( "Hood", ), ( - "light.hood_ambientlight", + "light.hood_ambient_light", { BSH_AMBIENT_LIGHT_ENABLED: { "value": True, diff --git a/tests/components/home_connect/test_sensor.py b/tests/components/home_connect/test_sensor.py index f0565c178fe..d98311ac5e5 100644 --- a/tests/components/home_connect/test_sensor.py +++ b/tests/components/home_connect/test_sensor.py @@ -26,14 +26,14 @@ TEST_HC_APP = "Dishwasher" EVENT_PROG_DELAYED_START = { "BSH.Common.Status.OperationState": { - "value": "BSH.Common.EnumType.OperationState.Delayed" + "value": "BSH.Common.EnumType.OperationState.DelayedStart" }, } EVENT_PROG_REMAIN_NO_VALUE = { "BSH.Common.Option.RemainingProgramTime": {}, "BSH.Common.Status.OperationState": { - "value": "BSH.Common.EnumType.OperationState.Delayed" + "value": "BSH.Common.EnumType.OperationState.DelayedStart" }, } @@ -103,13 +103,13 @@ PROGRAM_SEQUENCE_EVENTS = ( # Entity mapping to expected state at each program sequence. ENTITY_ID_STATES = { "sensor.dishwasher_operation_state": ( - "Delayed", - "Run", - "Run", - "Run", - "Ready", + "delayedstart", + "run", + "run", + "run", + "ready", ), - "sensor.dishwasher_remaining_program_time": ( + "sensor.dishwasher_program_finish_time": ( "unavailable", "2021-01-09T12:00:00+00:00", "2021-01-09T12:00:00+00:00", @@ -158,6 +158,8 @@ async def test_event_sensors( get_appliances.return_value = [appliance] assert config_entry.state == ConfigEntryState.NOT_LOADED + appliance.get_programs_available = MagicMock(return_value=["dummy_program"]) + appliance.status.update(EVENT_PROG_DELAYED_START) assert await integration_setup() assert config_entry.state == ConfigEntryState.LOADED @@ -198,11 +200,13 @@ async def test_remaining_prog_time_edge_cases( ) -> None: """Run program sequence to test edge cases for the remaining_prog_time entity.""" get_appliances.return_value = [appliance] - entity_id = "sensor.dishwasher_remaining_program_time" + entity_id = "sensor.dishwasher_program_finish_time" time_to_freeze = "2021-01-09 12:00:00+00:00" freezer.move_to(time_to_freeze) assert config_entry.state == ConfigEntryState.NOT_LOADED + appliance.get_programs_available = MagicMock(return_value=["dummy_program"]) + appliance.status.update(EVENT_PROG_REMAIN_NO_VALUE) assert await integration_setup() assert config_entry.state == ConfigEntryState.LOADED @@ -221,28 +225,28 @@ async def test_remaining_prog_time_edge_cases( ("entity_id", "status_key", "event_value_update", "expected", "appliance"), [ ( - "sensor.fridgefreezer_door_alarm_freezer", + "sensor.fridgefreezer_freezer_door_alarm", "EVENT_NOT_IN_STATUS_YET_SO_SET_TO_OFF", "", "off", "FridgeFreezer", ), ( - "sensor.fridgefreezer_door_alarm_freezer", + "sensor.fridgefreezer_freezer_door_alarm", REFRIGERATION_EVENT_DOOR_ALARM_FREEZER, BSH_EVENT_PRESENT_STATE_OFF, "off", "FridgeFreezer", ), ( - "sensor.fridgefreezer_door_alarm_freezer", + "sensor.fridgefreezer_freezer_door_alarm", REFRIGERATION_EVENT_DOOR_ALARM_FREEZER, BSH_EVENT_PRESENT_STATE_PRESENT, "present", "FridgeFreezer", ), ( - "sensor.fridgefreezer_door_alarm_freezer", + "sensor.fridgefreezer_freezer_door_alarm", REFRIGERATION_EVENT_DOOR_ALARM_FREEZER, BSH_EVENT_PRESENT_STATE_CONFIRMED, "confirmed", diff --git a/tests/components/home_connect/test_switch.py b/tests/components/home_connect/test_switch.py index d16a4626e59..1f1da1cd790 100644 --- a/tests/components/home_connect/test_switch.py +++ b/tests/components/home_connect/test_switch.py @@ -34,7 +34,7 @@ from tests.common import MockConfigEntry, load_json_object_fixture SETTINGS_STATUS = { setting.pop("key"): setting for setting in load_json_object_fixture("home_connect/settings.json") - .get("Washer") + .get("Dishwasher") .get("data") .get("settings") } @@ -64,34 +64,38 @@ async def test_switches( @pytest.mark.parametrize( - ("entity_id", "status", "service", "state"), + ("entity_id", "status", "service", "state", "appliance"), [ ( - "switch.washer_program_mix", + "switch.dishwasher_program_mix", {BSH_ACTIVE_PROGRAM: {"value": PROGRAM}}, SERVICE_TURN_ON, STATE_ON, + "Dishwasher", ), ( - "switch.washer_program_mix", + "switch.dishwasher_program_mix", {BSH_ACTIVE_PROGRAM: {"value": ""}}, SERVICE_TURN_OFF, STATE_OFF, + "Dishwasher", ), ( - "switch.washer_power", + "switch.dishwasher_power", {BSH_POWER_STATE: {"value": BSH_POWER_ON}}, SERVICE_TURN_ON, STATE_ON, + "Dishwasher", ), ( - "switch.washer_power", + "switch.dishwasher_power", {BSH_POWER_STATE: {"value": BSH_POWER_OFF}}, SERVICE_TURN_OFF, STATE_OFF, + "Dishwasher", ), ( - "switch.washer_power", + "switch.dishwasher_power", { BSH_POWER_STATE: {"value": ""}, BSH_OPERATION_STATE: { @@ -100,20 +104,24 @@ async def test_switches( }, SERVICE_TURN_OFF, STATE_OFF, + "Dishwasher", ), ( - "switch.washer_childlock", + "switch.dishwasher_child_lock", {BSH_CHILD_LOCK_STATE: {"value": True}}, SERVICE_TURN_ON, STATE_ON, + "Dishwasher", ), ( - "switch.washer_childlock", + "switch.dishwasher_child_lock", {BSH_CHILD_LOCK_STATE: {"value": False}}, SERVICE_TURN_OFF, STATE_OFF, + "Dishwasher", ), ], + indirect=["appliance"], ) async def test_switch_functionality( entity_id: str, @@ -145,45 +153,52 @@ async def test_switch_functionality( @pytest.mark.parametrize( - ("entity_id", "status", "service", "mock_attr"), + ("entity_id", "status", "service", "mock_attr", "problematic_appliance"), [ ( - "switch.washer_program_mix", + "switch.dishwasher_program_mix", {BSH_ACTIVE_PROGRAM: {"value": PROGRAM}}, SERVICE_TURN_ON, "start_program", + "Dishwasher", ), ( - "switch.washer_program_mix", + "switch.dishwasher_program_mix", {BSH_ACTIVE_PROGRAM: {"value": PROGRAM}}, SERVICE_TURN_OFF, "stop_program", + "Dishwasher", ), ( - "switch.washer_power", + "switch.dishwasher_power", {BSH_POWER_STATE: {"value": ""}}, SERVICE_TURN_ON, "set_setting", + "Dishwasher", ), ( - "switch.washer_power", + "switch.dishwasher_power", {BSH_POWER_STATE: {"value": ""}}, SERVICE_TURN_OFF, "set_setting", + "Dishwasher", ), ( - "switch.washer_childlock", + "switch.dishwasher_child_lock", {BSH_CHILD_LOCK_STATE: {"value": ""}}, SERVICE_TURN_ON, "set_setting", + "Dishwasher", ), ( - "switch.washer_childlock", + "switch.dishwasher_child_lock", {BSH_CHILD_LOCK_STATE: {"value": ""}}, SERVICE_TURN_OFF, "set_setting", + "Dishwasher", ), ], + indirect=["problematic_appliance"], ) async def test_switch_exception_handling( entity_id: str, @@ -204,6 +219,7 @@ async def test_switch_exception_handling( get_appliances.return_value = [problematic_appliance] assert config_entry.state == ConfigEntryState.NOT_LOADED + problematic_appliance.status.update(status) assert await integration_setup() assert config_entry.state == ConfigEntryState.LOADED @@ -211,7 +227,6 @@ async def test_switch_exception_handling( with pytest.raises(HomeConnectError): getattr(problematic_appliance, mock_attr)() - problematic_appliance.status.update(status) await hass.services.async_call( SWITCH_DOMAIN, service, {"entity_id": entity_id}, blocking=True ) @@ -222,14 +237,14 @@ async def test_switch_exception_handling( ("entity_id", "status", "service", "state", "appliance"), [ ( - "switch.fridgefreezer_supermode_freezer", + "switch.fridgefreezer_freezer_super_mode", {REFRIGERATION_SUPERMODEFREEZER: {"value": True}}, SERVICE_TURN_ON, STATE_ON, "FridgeFreezer", ), ( - "switch.fridgefreezer_supermode_freezer", + "switch.fridgefreezer_freezer_super_mode", {REFRIGERATION_SUPERMODEFREEZER: {"value": False}}, SERVICE_TURN_OFF, STATE_OFF, @@ -277,14 +292,14 @@ async def test_ent_desc_switch_functionality( ("entity_id", "status", "service", "mock_attr", "problematic_appliance"), [ ( - "switch.fridgefreezer_supermode_freezer", + "switch.fridgefreezer_freezer_super_mode", {REFRIGERATION_SUPERMODEFREEZER: {"value": ""}}, SERVICE_TURN_ON, "set_setting", "FridgeFreezer", ), ( - "switch.fridgefreezer_supermode_freezer", + "switch.fridgefreezer_freezer_super_mode", {REFRIGERATION_SUPERMODEFREEZER: {"value": ""}}, SERVICE_TURN_OFF, "set_setting",