Migrate device models to entity descriptions and add localization & icons at Home Connect (#127870)

* Delete device models and use entity descriptions

* Home Connect localization & icons

* Update homeassistant/components/home_connect/strings.json

* Update homeassistant/components/home_connect/icons.json

* Fix tests

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
pull/128177/head
J. Diego Rodríguez Royo 2024-10-11 17:52:06 +02:00 committed by GitHub
parent 1739647768
commit 6a12a24d73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 839 additions and 823 deletions

View File

@ -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)},

View File

@ -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}

View File

@ -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"

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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)

View File

@ -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,
)

View File

@ -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"
}
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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",

View File

@ -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",