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 import voluptuous as vol
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_DEVICE_ID, CONF_DEVICE, Platform from homeassistant.const import ATTR_DEVICE_ID, Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import ( from homeassistant.helpers import (
config_entry_oauth2_flow, config_entry_oauth2_flow,
@ -87,8 +87,7 @@ def _get_appliance_by_device_id(
) -> api.HomeConnectDevice: ) -> api.HomeConnectDevice:
"""Return a Home Connect appliance instance given an device_id.""" """Return a Home Connect appliance instance given an device_id."""
for hc_api in hass.data[DOMAIN].values(): for hc_api in hass.data[DOMAIN].values():
for dev_dict in hc_api.devices: for device in hc_api.devices:
device = dev_dict[CONF_DEVICE]
if device.device_id == device_id: if device.device_id == device_id:
return device.appliance return device.appliance
raise ValueError(f"Appliance for device id {device_id} not found") 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) device_registry = dr.async_get(hass)
try: try:
await hass.async_add_executor_job(hc_api.get_devices) await hass.async_add_executor_job(hc_api.get_devices)
for device_dict in hc_api.devices: for device in hc_api.devices:
device = device_dict["device"]
device_entry = device_registry.async_get_or_create( device_entry = device_registry.async_get_or_create(
config_entry_id=entry.entry_id, config_entry_id=entry.entry_id,
identifiers={(DOMAIN, device.appliance.haId)}, identifiers={(DOMAIN, device.appliance.haId)},

View File

@ -1,50 +1,17 @@
"""API for Home Connect bound to HASS OAuth.""" """API for Home Connect bound to HASS OAuth."""
from abc import abstractmethod
from asyncio import run_coroutine_threadsafe from asyncio import run_coroutine_threadsafe
import logging import logging
from typing import Any
import homeconnect import homeconnect
from homeconnect.api import HomeConnectAppliance, HomeConnectError from homeconnect.api import HomeConnectAppliance, HomeConnectError
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.config_entries import ConfigEntry 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.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
from .const import ( from .const import ATTR_KEY, ATTR_VALUE, BSH_ACTIVE_PROGRAM, SIGNAL_UPDATE_ENTITIES
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,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -65,7 +32,7 @@ class ConfigEntryAuth(homeconnect.HomeConnectAPI):
hass, config_entry, implementation hass, config_entry, implementation
) )
super().__init__(self.session.token) super().__init__(self.session.token)
self.devices: list[dict[str, Any]] = [] self.devices: list[HomeConnectDevice] = []
def refresh_tokens(self) -> dict: def refresh_tokens(self) -> dict:
"""Refresh and return new Home Connect tokens using Home Assistant OAuth2 session.""" """Refresh and return new Home Connect tokens using Home Assistant OAuth2 session."""
@ -75,55 +42,16 @@ class ConfigEntryAuth(homeconnect.HomeConnectAPI):
return self.session.token return self.session.token
def get_devices(self) -> list[dict[str, Any]]: def get_devices(self) -> list[HomeConnectAppliance]:
"""Get a dictionary of devices.""" """Get a dictionary of devices."""
appl = self.get_appliances() appl: list[HomeConnectAppliance] = self.get_appliances()
devices = [] self.devices = [HomeConnectDevice(self.hass, app) for app in appl]
for app in appl: return self.devices
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
class HomeConnectDevice: class HomeConnectDevice:
"""Generic Home Connect device.""" """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: def __init__(self, hass: HomeAssistant, appliance: HomeConnectAppliance) -> None:
"""Initialize the device class.""" """Initialize the device class."""
self.hass = hass self.hass = hass
@ -155,378 +83,3 @@ class HomeConnectDevice:
_LOGGER.debug("Update triggered on %s", appliance.name) _LOGGER.debug("Update triggered on %s", appliance.name)
_LOGGER.debug(self.appliance.status) _LOGGER.debug(self.appliance.status)
dispatcher_send(self.hass, SIGNAL_UPDATE_ENTITIES, appliance.haId) 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.""" """Provides a binary sensor for Home Connect."""
from dataclasses import dataclass, field from dataclasses import dataclass
import logging import logging
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
@ -9,13 +9,11 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENTITIES
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .api import HomeConnectDevice from .api import HomeConnectDevice
from .const import ( from .const import (
ATTR_DEVICE,
ATTR_VALUE, ATTR_VALUE,
BSH_DOOR_STATE, BSH_DOOR_STATE,
BSH_DOOR_STATE_CLOSED, BSH_DOOR_STATE_CLOSED,
@ -33,34 +31,80 @@ from .const import (
from .entity import HomeConnectEntity from .entity import HomeConnectEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REFRIGERATION_DOOR_BOOLEAN_MAP = {
REFRIGERATION_STATUS_DOOR_CLOSED: False,
REFRIGERATION_STATUS_DOOR_OPEN: True,
}
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class HomeConnectBinarySensorEntityDescription(BinarySensorEntityDescription): class HomeConnectBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Entity Description class for binary sensors.""" """Entity Description class for binary sensors."""
desc: str
device_class: BinarySensorDeviceClass | None = BinarySensorDeviceClass.DOOR device_class: BinarySensorDeviceClass | None = BinarySensorDeviceClass.DOOR
boolean_map: dict[str, bool] = field( boolean_map: dict[str, bool] | None = None
default_factory=lambda: {
REFRIGERATION_STATUS_DOOR_CLOSED: False,
REFRIGERATION_STATUS_DOOR_OPEN: True,
}
)
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( HomeConnectBinarySensorEntityDescription(
key=REFRIGERATION_STATUS_DOOR_CHILLER, key=REFRIGERATION_STATUS_DOOR_CHILLER,
desc="Chiller Door", boolean_map=REFRIGERATION_DOOR_BOOLEAN_MAP,
device_class=BinarySensorDeviceClass.DOOR,
translation_key="chiller_door",
), ),
HomeConnectBinarySensorEntityDescription( HomeConnectBinarySensorEntityDescription(
key=REFRIGERATION_STATUS_DOOR_FREEZER, key=REFRIGERATION_STATUS_DOOR_FREEZER,
desc="Freezer Door", boolean_map=REFRIGERATION_DOOR_BOOLEAN_MAP,
device_class=BinarySensorDeviceClass.DOOR,
translation_key="freezer_door",
), ),
HomeConnectBinarySensorEntityDescription( HomeConnectBinarySensorEntityDescription(
key=REFRIGERATION_STATUS_DOOR_REFRIGERATOR, 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]: def get_entities() -> list[BinarySensorEntity]:
entities: list[BinarySensorEntity] = [] entities: list[BinarySensorEntity] = []
hc_api = hass.data[DOMAIN][config_entry.entry_id] hc_api = hass.data[DOMAIN][config_entry.entry_id]
for device_dict in hc_api.devices: for device 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
entities.extend( entities.extend(
HomeConnectFridgeDoorBinarySensor( HomeConnectBinarySensor(device, description)
device=device, entity_description=description
)
for description in BINARY_SENSORS for description in BINARY_SENSORS
if description.key in device.appliance.status if description.key in device.appliance.status
) )
if BSH_DOOR_STATE in device.appliance.status:
entities.append(HomeConnectDoorBinarySensor(device))
return entities return entities
async_add_entities(await hass.async_add_executor_job(get_entities), True) 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): class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity):
"""Binary sensor for Home Connect.""" """Binary sensor for Home Connect."""
def __init__( entity_description: HomeConnectBinarySensorEntityDescription
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
@property @property
def available(self) -> bool: def available(self) -> bool:
@ -125,59 +144,41 @@ class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update the binary sensor's status.""" """Update the binary sensor's status."""
state = self.device.appliance.status.get(self._update_key, {}) if not self.device.appliance.status or not (
if not state: status := self.device.appliance.status.get(self.bsh_key, {}).get(ATTR_VALUE)
):
self._attr_is_on = None self._attr_is_on = None
return return
if self.entity_description.boolean_map:
value = state.get(ATTR_VALUE) self._attr_is_on = self.entity_description.boolean_map.get(status)
if self._false_value_list and self._true_value_list: elif status not in [True, False]:
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
)
self._attr_is_on = None self._attr_is_on = None
else:
self._attr_is_on = status
_LOGGER.debug("Updated, new state: %s", self._attr_is_on) _LOGGER.debug("Updated, new state: %s", self._attr_is_on)
class HomeConnectFridgeDoorBinarySensor(HomeConnectEntity, BinarySensorEntity): class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
"""Binary sensor for Home Connect Fridge Doors.""" """Binary sensor for Home Connect Generic Door."""
entity_description: HomeConnectBinarySensorEntityDescription _attr_has_entity_name = False
def __init__( def __init__(
self, self,
device: HomeConnectDevice, device: HomeConnectDevice,
entity_description: HomeConnectBinarySensorEntityDescription,
) -> None: ) -> None:
"""Initialize the entity.""" """Initialize the entity."""
self.entity_description = entity_description super().__init__(
super().__init__(device, entity_description.key, entity_description.desc) device,
HomeConnectBinarySensorEntityDescription(
async def async_update(self) -> None: key=BSH_DOOR_STATE,
"""Update the binary sensor's status.""" device_class=BinarySensorDeviceClass.DOOR,
_LOGGER.debug( boolean_map={
"Updating: %s, cur state: %s", BSH_DOOR_STATE_CLOSED: False,
self._attr_unique_id, BSH_DOOR_STATE_LOCKED: False,
self.state, BSH_DOOR_STATE_OPEN: True,
) },
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,
) )
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.core import callback
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect 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 .api import HomeConnectDevice
from .const import DOMAIN, SIGNAL_UPDATE_ENTITIES from .const import DOMAIN, SIGNAL_UPDATE_ENTITIES
@ -17,13 +17,13 @@ class HomeConnectEntity(Entity):
"""Generic Home Connect entity (base class).""" """Generic Home Connect entity (base class)."""
_attr_should_poll = False _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.""" """Initialize the entity."""
self.device = device self.device = device
self.bsh_key = bsh_key self.entity_description = desc
self._attr_name = f"{device.appliance.name} {desc}" self._attr_unique_id = f"{device.appliance.haId}-{self.bsh_key}"
self._attr_unique_id = f"{device.appliance.haId}-{bsh_key}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.appliance.haId)}, identifiers={(DOMAIN, device.appliance.haId)},
manufacturer=device.appliance.brand, manufacturer=device.appliance.brand,
@ -50,3 +50,8 @@ class HomeConnectEntity(Entity):
"""Update the entity.""" """Update the entity."""
_LOGGER.debug("Entity update triggered on %s", self) _LOGGER.debug("Entity update triggered on %s", self)
self.async_schedule_update_ha_state(True) 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": { "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": { "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", "default": "mdi:fridge",
"state": { "state": {
"confirmed": "mdi:fridge-alert-outline", "confirmed": "mdi:fridge-alert-outline",
"present": "mdi:fridge-alert" "present": "mdi:fridge-alert"
} }
}, },
"alarm_sensor_freezer": { "freezer_door_alarm": {
"default": "mdi:snowflake", "default": "mdi:snowflake",
"state": { "state": {
"confirmed": "mdi:snowflake-check", "confirmed": "mdi:snowflake-check",
"present": "mdi:snowflake-alert" "present": "mdi:snowflake-alert"
} }
}, },
"alarm_sensor_temp": { "freezer_temperature_alarm": {
"default": "mdi:thermometer", "default": "mdi:thermometer",
"state": { "state": {
"confirmed": "mdi:thermometer-check", "confirmed": "mdi:thermometer-check",
"present": "mdi:thermometer-alert" "present": "mdi:thermometer-alert"
} }
}, },
"alarm_sensor_coffee_bean_container": { "bean_container_empty": {
"default": "mdi:coffee-maker", "default": "mdi:coffee-maker",
"state": { "state": {
"confirmed": "mdi:coffee-maker-check", "confirmed": "mdi:coffee-maker-check",
"present": "mdi:coffee-maker-outline" "present": "mdi:coffee-maker-outline"
} }
}, },
"alarm_sensor_coffee_water_tank": { "water_tank_empty": {
"default": "mdi:water", "default": "mdi:water",
"state": { "state": {
"confirmed": "mdi:water-check", "confirmed": "mdi:water-check",
"present": "mdi:water-alert" "present": "mdi:water-alert"
} }
}, },
"alarm_sensor_coffee_drip_tray": { "drip_tray_full": {
"default": "mdi:tray", "default": "mdi:tray",
"state": { "state": {
"confirmed": "mdi:tray-full", "confirmed": "mdi:tray-full",
@ -68,11 +152,51 @@
} }
}, },
"switch": { "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", "default": "mdi:snowflake",
"state": { "state": {
"off": "mdi:snowflake-off" "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, LightEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, CONF_ENTITIES
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
@ -27,6 +26,8 @@ from .const import (
BSH_AMBIENT_LIGHT_COLOR, BSH_AMBIENT_LIGHT_COLOR,
BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR, BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR,
BSH_AMBIENT_LIGHT_CUSTOM_COLOR, BSH_AMBIENT_LIGHT_CUSTOM_COLOR,
BSH_AMBIENT_LIGHT_ENABLED,
COOKING_LIGHTING,
COOKING_LIGHTING_BRIGHTNESS, COOKING_LIGHTING_BRIGHTNESS,
DOMAIN, DOMAIN,
REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS, REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS,
@ -43,20 +44,19 @@ _LOGGER = logging.getLogger(__name__)
class HomeConnectLightEntityDescription(LightEntityDescription): class HomeConnectLightEntityDescription(LightEntityDescription):
"""Light entity description.""" """Light entity description."""
desc: str
brightness_key: str | None brightness_key: str | None
LIGHTS: tuple[HomeConnectLightEntityDescription, ...] = ( LIGHTS: tuple[HomeConnectLightEntityDescription, ...] = (
HomeConnectLightEntityDescription( HomeConnectLightEntityDescription(
key=REFRIGERATION_INTERNAL_LIGHT_POWER, key=REFRIGERATION_INTERNAL_LIGHT_POWER,
desc="Internal Light",
brightness_key=REFRIGERATION_INTERNAL_LIGHT_BRIGHTNESS, brightness_key=REFRIGERATION_INTERNAL_LIGHT_BRIGHTNESS,
translation_key="internal_light",
), ),
HomeConnectLightEntityDescription( HomeConnectLightEntityDescription(
key=REFRIGERATION_EXTERNAL_LIGHT_POWER, key=REFRIGERATION_EXTERNAL_LIGHT_POWER,
desc="External Light",
brightness_key=REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS, brightness_key=REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS,
translation_key="external_light",
), ),
) )
@ -72,11 +72,29 @@ async def async_setup_entry(
"""Get a list of entities.""" """Get a list of entities."""
entities: list[LightEntity] = [] entities: list[LightEntity] = []
hc_api = hass.data[DOMAIN][config_entry.entry_id] hc_api = hass.data[DOMAIN][config_entry.entry_id]
for device_dict in hc_api.devices: for device in hc_api.devices:
entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("light", []) if COOKING_LIGHTING in device.appliance.status:
entity_list = [HomeConnectLight(**d) for d in entity_dicts] entities.append(
device: HomeConnectDevice = device_dict[CONF_DEVICE] HomeConnectLight(
# Auto-discover entities 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( entities.extend(
HomeConnectCoolingLight( HomeConnectCoolingLight(
device=device, device=device,
@ -86,7 +104,6 @@ async def async_setup_entry(
for description in LIGHTS for description in LIGHTS
if description.key in device.appliance.status if description.key in device.appliance.status
) )
entities.extend(entity_list)
return entities return entities
async_add_entities(await hass.async_add_executor_job(get_entities), True) 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): class HomeConnectLight(HomeConnectEntity, LightEntity):
"""Light for Home Connect.""" """Light for Home Connect."""
entity_description: LightEntityDescription
def __init__( def __init__(
self, device: HomeConnectDevice, bsh_key: str, desc: str, ambient: bool self,
device: HomeConnectDevice,
desc: LightEntityDescription,
ambient: bool,
) -> None: ) -> None:
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(device, bsh_key, desc) super().__init__(device, desc)
self._ambient = ambient self._ambient = ambient
self._percentage_scale = (10, 100) self._percentage_scale = (10, 100)
self._brightness_key: str | None self._brightness_key: str | None
@ -255,9 +277,7 @@ class HomeConnectCoolingLight(HomeConnectLight):
entity_description: HomeConnectLightEntityDescription, entity_description: HomeConnectLightEntityDescription,
) -> None: ) -> None:
"""Initialize Cooling Light Entity.""" """Initialize Cooling Light Entity."""
super().__init__( super().__init__(device, entity_description, ambient)
device, entity_description.key, entity_description.desc, ambient
)
self.entity_description = entity_description self.entity_description = entity_description
self._brightness_key = entity_description.brightness_key self._brightness_key = entity_description.brightness_key
self._percentage_scale = (1, 100) self._percentage_scale = (1, 100)

View File

@ -1,26 +1,29 @@
"""Provides a sensor for Home Connect.""" """Provides a sensor for Home Connect."""
from dataclasses import dataclass, field import contextlib
from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from typing import cast from typing import cast
from homeconnect.api import HomeConnectError
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
SensorEntityDescription, SensorEntityDescription,
SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry 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.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .api import ConfigEntryAuth, HomeConnectDevice from .api import ConfigEntryAuth
from .const import ( from .const import (
ATTR_DEVICE,
ATTR_VALUE, ATTR_VALUE,
BSH_EVENT_PRESENT_STATE_OFF,
BSH_OPERATION_STATE, BSH_OPERATION_STATE,
BSH_OPERATION_STATE_FINISHED, BSH_OPERATION_STATE_FINISHED,
BSH_OPERATION_STATE_PAUSE, BSH_OPERATION_STATE_PAUSE,
@ -38,47 +41,182 @@ from .entity import HomeConnectEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
EVENT_OPTIONS = ["confirmed", "off", "present"]
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class HomeConnectSensorEntityDescription(SensorEntityDescription): class HomeConnectSensorEntityDescription(SensorEntityDescription):
"""Entity Description class for sensors.""" """Entity Description class for sensors."""
device_class: SensorDeviceClass | None = SensorDeviceClass.ENUM default_value: str | None = None
options: list[str] | None = field( appliance_types: tuple[str, ...] | None = None
default_factory=lambda: ["confirmed", "off", "present"] sign: int = 1
)
desc: str
appliance_types: tuple[str, ...]
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( HomeConnectSensorEntityDescription(
key=REFRIGERATION_EVENT_DOOR_ALARM_FREEZER, 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"), appliance_types=("FridgeFreezer", "Freezer"),
), ),
HomeConnectSensorEntityDescription( HomeConnectSensorEntityDescription(
key=REFRIGERATION_EVENT_DOOR_ALARM_REFRIGERATOR, 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"), appliance_types=("FridgeFreezer", "Refrigerator"),
), ),
HomeConnectSensorEntityDescription( HomeConnectSensorEntityDescription(
key=REFRIGERATION_EVENT_TEMP_ALARM_FREEZER, 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"), appliance_types=("FridgeFreezer", "Freezer"),
), ),
HomeConnectSensorEntityDescription( HomeConnectSensorEntityDescription(
key=COFFEE_EVENT_BEAN_CONTAINER_EMPTY, 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",), appliance_types=("CoffeeMaker",),
), ),
HomeConnectSensorEntityDescription( HomeConnectSensorEntityDescription(
key=COFFEE_EVENT_WATER_TANK_EMPTY, 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",), appliance_types=("CoffeeMaker",),
), ),
HomeConnectSensorEntityDescription( HomeConnectSensorEntityDescription(
key=COFFEE_EVENT_DRIP_TRAY_FULL, 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",), appliance_types=("CoffeeMaker",),
), ),
) )
@ -95,18 +233,25 @@ async def async_setup_entry(
"""Get a list of entities.""" """Get a list of entities."""
entities: list[SensorEntity] = [] entities: list[SensorEntity] = []
hc_api: ConfigEntryAuth = hass.data[DOMAIN][config_entry.entry_id] hc_api: ConfigEntryAuth = hass.data[DOMAIN][config_entry.entry_id]
for device_dict in hc_api.devices: for device 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
entities.extend( entities.extend(
HomeConnectAlarmSensor( HomeConnectSensor(
device, 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 for description in SENSORS
if device.appliance.type in description.appliance_types if description.key in device.appliance.status
) )
return entities return entities
@ -116,25 +261,7 @@ async def async_setup_entry(
class HomeConnectSensor(HomeConnectEntity, SensorEntity): class HomeConnectSensor(HomeConnectEntity, SensorEntity):
"""Sensor class for Home Connect.""" """Sensor class for Home Connect."""
_key: str entity_description: HomeConnectSensorEntityDescription
_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
@property @property
def available(self) -> bool: def available(self) -> bool:
@ -143,78 +270,52 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update the sensor's status.""" """Update the sensor's status."""
status = self.device.appliance.status appliance_status = self.device.appliance.status
if self.bsh_key not in status: if (
self._attr_native_value = None self.bsh_key not in appliance_status
elif self.device_class == SensorDeviceClass.TIMESTAMP: or ATTR_VALUE not in appliance_status[self.bsh_key]
if ATTR_VALUE not in status[self.bsh_key]: ):
self._attr_native_value = None self._attr_native_value = self.entity_description.default_value
elif ( _LOGGER.debug("Updated, new state: %s", self._attr_native_value)
self._attr_native_value is not None return
and self._sign == 1 status = appliance_status[self.bsh_key]
and isinstance(self._attr_native_value, datetime) match self.device_class:
and self._attr_native_value < dt_util.utcnow() case SensorDeviceClass.TIMESTAMP:
): if ATTR_VALUE not in status:
# if the date is supposed to be in the future but we're self._attr_native_value = None
# already past it, set state to None. elif (
self._attr_native_value = None self._attr_native_value is not None
elif ( and self.entity_description.sign == 1
BSH_OPERATION_STATE in status and isinstance(self._attr_native_value, datetime)
and ATTR_VALUE in status[BSH_OPERATION_STATE] and self._attr_native_value < dt_util.utcnow()
and status[BSH_OPERATION_STATE][ATTR_VALUE] ):
in [ # if the date is supposed to be in the future but we're
BSH_OPERATION_STATE_RUN, # already past it, set state to None.
BSH_OPERATION_STATE_PAUSE, self._attr_native_value = None
BSH_OPERATION_STATE_FINISHED, elif (
] BSH_OPERATION_STATE
): in (appliance_status := self.device.appliance.status)
seconds = self._sign * float(status[self.bsh_key][ATTR_VALUE]) and ATTR_VALUE in appliance_status[BSH_OPERATION_STATE]
self._attr_native_value = dt_util.utcnow() + timedelta(seconds=seconds) and appliance_status[BSH_OPERATION_STATE][ATTR_VALUE]
else: in [
self._attr_native_value = None BSH_OPERATION_STATE_RUN,
else: BSH_OPERATION_STATE_PAUSE,
self._attr_native_value = status[self.bsh_key].get(ATTR_VALUE) BSH_OPERATION_STATE_FINISHED,
if self.bsh_key == BSH_OPERATION_STATE: ]
):
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 # Value comes back as an enum, we only really care about the
# last part, so split it off # last part, so split it off
# https://developer.home-connect.com/docs/status/operation_state # https://developer.home-connect.com/docs/status/operation_state
self._attr_native_value = cast(str, self._attr_native_value).split(".")[ self._attr_native_value = slugify(
-1 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) _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": { "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": { "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": { "state": {
"confirmed": "[%key:component::home_connect::common::confirmed%]", "confirmed": "[%key:component::home_connect::common::confirmed%]",
"present": "[%key:component::home_connect::common::present%]" "present": "[%key:component::home_connect::common::present%]"
} }
}, },
"alarm_sensor_freezer": { "refrigerator_door_alarm": {
"name": "Refrigerator door alarm",
"state": { "state": {
"off": "[%key:common::state::off%]",
"confirmed": "[%key:component::home_connect::common::confirmed%]", "confirmed": "[%key:component::home_connect::common::confirmed%]",
"present": "[%key:component::home_connect::common::present%]" "present": "[%key:component::home_connect::common::present%]"
} }
}, },
"alarm_sensor_temp": { "freezer_temperature_alarm": {
"name": "Freezer temperature alarm",
"state": { "state": {
"off": "[%key:common::state::off%]",
"confirmed": "[%key:component::home_connect::common::confirmed%]", "confirmed": "[%key:component::home_connect::common::confirmed%]",
"present": "[%key:component::home_connect::common::present%]" "present": "[%key:component::home_connect::common::present%]"
} }
}, },
"alarm_sensor_coffee_bean_container": { "bean_container_empty": {
"name": "Bean container empty",
"state": { "state": {
"off": "[%key:common::state::off%]",
"confirmed": "[%key:component::home_connect::common::confirmed%]", "confirmed": "[%key:component::home_connect::common::confirmed%]",
"present": "[%key:component::home_connect::common::present%]" "present": "[%key:component::home_connect::common::present%]"
} }
}, },
"alarm_sensor_coffee_water_tank": { "water_tank_empty": {
"name": "Water tank empty",
"state": { "state": {
"off": "[%key:common::state::off%]",
"confirmed": "[%key:component::home_connect::common::confirmed%]", "confirmed": "[%key:component::home_connect::common::confirmed%]",
"present": "[%key:component::home_connect::common::present%]" "present": "[%key:component::home_connect::common::present%]"
} }
}, },
"alarm_sensor_coffee_drip_tray": { "drip_tray_full": {
"name": "Drip tray full",
"state": { "state": {
"off": "[%key:common::state::off%]",
"confirmed": "[%key:component::home_connect::common::confirmed%]", "confirmed": "[%key:component::home_connect::common::confirmed%]",
"present": "[%key:component::home_connect::common::present%]" "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.""" """Provides a switch for Home Connect."""
from dataclasses import dataclass import contextlib
import logging import logging
from typing import Any from typing import Any
@ -8,7 +8,6 @@ from homeconnect.api import HomeConnectError
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, CONF_ENTITIES
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -18,7 +17,9 @@ from .const import (
BSH_ACTIVE_PROGRAM, BSH_ACTIVE_PROGRAM,
BSH_CHILD_LOCK_STATE, BSH_CHILD_LOCK_STATE,
BSH_OPERATION_STATE, BSH_OPERATION_STATE,
BSH_POWER_OFF,
BSH_POWER_ON, BSH_POWER_ON,
BSH_POWER_STANDBY,
BSH_POWER_STATE, BSH_POWER_STATE,
DOMAIN, DOMAIN,
REFRIGERATION_DISPENSER, REFRIGERATION_DISPENSER,
@ -29,26 +30,71 @@ from .entity import HomeConnectDevice, HomeConnectEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
APPLIANCES_WITH_PROGRAMS = (
@dataclass(frozen=True, kw_only=True) "CleaningRobot",
class HomeConnectSwitchEntityDescription(SwitchEntityDescription): "CoffeeMachine",
"""Switch entity description.""" "Dishwasher",
"Dryer",
desc: str "Hood",
"Oven",
"WarmingDrawer",
"Washer",
"WasherDryer",
)
SWITCHES: tuple[HomeConnectSwitchEntityDescription, ...] = ( SWITCHES = (
HomeConnectSwitchEntityDescription( SwitchEntityDescription(
key=REFRIGERATION_SUPERMODEFREEZER, key=BSH_CHILD_LOCK_STATE,
desc="Supermode Freezer", translation_key="child_lock",
), ),
HomeConnectSwitchEntityDescription( SwitchEntityDescription(
key="ConsumerProducts.CoffeeMaker.Setting.CupWarmer",
translation_key="cup_warmer",
),
SwitchEntityDescription(
key=REFRIGERATION_SUPERMODEREFRIGERATOR, 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, 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.""" """Get a list of entities."""
entities: list[SwitchEntity] = [] entities: list[SwitchEntity] = []
hc_api: ConfigEntryAuth = hass.data[DOMAIN][config_entry.entry_id] hc_api: ConfigEntryAuth = hass.data[DOMAIN][config_entry.entry_id]
for device_dict in hc_api.devices: for device in hc_api.devices:
entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("switch", []) if device.appliance.type in APPLIANCES_WITH_PROGRAMS:
entities.extend(HomeConnectProgramSwitch(**d) for d in entity_dicts) with contextlib.suppress(HomeConnectError):
entities.append(HomeConnectPowerSwitch(device_dict[CONF_DEVICE])) programs = device.appliance.get_programs_available()
entities.append(HomeConnectChildLockSwitch(device_dict[CONF_DEVICE])) if programs:
# Auto-discover entities entities.extend(
hc_device: HomeConnectDevice = device_dict[CONF_DEVICE] HomeConnectProgramSwitch(device, program)
for program in programs
)
entities.append(HomeConnectPowerSwitch(device))
entities.extend( entities.extend(
HomeConnectSwitch(device=hc_device, entity_description=description) HomeConnectSwitch(device, description)
for description in SWITCHES for description in SWITCHES
if description.key in hc_device.appliance.status if description.key in device.appliance.status
) )
return entities return entities
@ -85,18 +134,6 @@ async def async_setup_entry(
class HomeConnectSwitch(HomeConnectEntity, SwitchEntity): class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
"""Generic switch class for Home Connect Binary Settings.""" """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: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on setting.""" """Turn on setting."""
@ -153,7 +190,9 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
desc = " ".join( desc = " ".join(
["Program", program_name.split(".")[-3], program_name.split(".")[-1]] ["Program", program_name.split(".")[-3], program_name.split(".")[-1]]
) )
super().__init__(device, desc, 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 self.program_name = program_name
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
@ -189,9 +228,27 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
"""Power switch class for Home Connect.""" """Power switch class for Home Connect."""
power_off_state: str | None
def __init__(self, device: HomeConnectDevice) -> None: def __init__(self, device: HomeConnectDevice) -> None:
"""Initialize the entity.""" """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: async def async_turn_on(self, **kwargs: Any) -> None:
"""Switch the device on.""" """Switch the device on."""
@ -207,12 +264,15 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Switch the device off.""" """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) _LOGGER.debug("tried to switch off %s", self.name)
try: try:
await self.hass.async_add_executor_job( await self.hass.async_add_executor_job(
self.device.appliance.set_setting, self.device.appliance.set_setting,
BSH_POWER_STATE, BSH_POWER_STATE,
self.device.power_off_state, self.power_off_state,
) )
except HomeConnectError as err: except HomeConnectError as err:
_LOGGER.error("Error while trying to turn off device: %s", 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 self._attr_is_on = True
elif ( elif (
self.device.appliance.status.get(BSH_POWER_STATE, {}).get(ATTR_VALUE) 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 self._attr_is_on = False
elif self.device.appliance.status.get(BSH_OPERATION_STATE, {}).get( elif self.device.appliance.status.get(BSH_OPERATION_STATE, {}).get(
@ -251,44 +311,3 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
else: else:
self._attr_is_on = None self._attr_is_on = None
_LOGGER.debug("Updated, new state: %s", self._attr_is_on) _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" entity_id = "binary_sensor.washer_door"
get_appliances.return_value = [appliance] get_appliances.return_value = [appliance]
assert config_entry.state == ConfigEntryState.NOT_LOADED assert config_entry.state == ConfigEntryState.NOT_LOADED
appliance.status.update({BSH_DOOR_STATE: {"value": state}})
assert await integration_setup() assert await integration_setup()
assert config_entry.state == ConfigEntryState.LOADED assert config_entry.state == ConfigEntryState.LOADED
appliance.status.update({BSH_DOOR_STATE: {"value": state}})
await async_update_entity(hass, entity_id) await async_update_entity(hass, entity_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.is_state(entity_id, expected) 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"), ("entity_id", "status", "service", "service_data", "state", "appliance"),
[ [
( (
"light.hood_light", "light.hood_functional_light",
{ {
COOKING_LIGHTING: { COOKING_LIGHTING: {
"value": True, "value": True,
@ -79,7 +79,7 @@ async def test_light(
"Hood", "Hood",
), ),
( (
"light.hood_light", "light.hood_functional_light",
{ {
COOKING_LIGHTING: { COOKING_LIGHTING: {
"value": True, "value": True,
@ -92,7 +92,7 @@ async def test_light(
"Hood", "Hood",
), ),
( (
"light.hood_light", "light.hood_functional_light",
{ {
COOKING_LIGHTING: {"value": False}, COOKING_LIGHTING: {"value": False},
COOKING_LIGHTING_BRIGHTNESS: {"value": 70}, COOKING_LIGHTING_BRIGHTNESS: {"value": 70},
@ -103,7 +103,7 @@ async def test_light(
"Hood", "Hood",
), ),
( (
"light.hood_light", "light.hood_functional_light",
{ {
COOKING_LIGHTING: { COOKING_LIGHTING: {
"value": None, "value": None,
@ -116,7 +116,7 @@ async def test_light(
"Hood", "Hood",
), ),
( (
"light.hood_ambientlight", "light.hood_ambient_light",
{ {
BSH_AMBIENT_LIGHT_ENABLED: { BSH_AMBIENT_LIGHT_ENABLED: {
"value": True, "value": True,
@ -129,7 +129,7 @@ async def test_light(
"Hood", "Hood",
), ),
( (
"light.hood_ambientlight", "light.hood_ambient_light",
{ {
BSH_AMBIENT_LIGHT_ENABLED: {"value": False}, BSH_AMBIENT_LIGHT_ENABLED: {"value": False},
BSH_AMBIENT_LIGHT_BRIGHTNESS: {"value": 70}, BSH_AMBIENT_LIGHT_BRIGHTNESS: {"value": 70},
@ -140,7 +140,7 @@ async def test_light(
"Hood", "Hood",
), ),
( (
"light.hood_ambientlight", "light.hood_ambient_light",
{ {
BSH_AMBIENT_LIGHT_ENABLED: {"value": True}, BSH_AMBIENT_LIGHT_ENABLED: {"value": True},
BSH_AMBIENT_LIGHT_CUSTOM_COLOR: {}, BSH_AMBIENT_LIGHT_CUSTOM_COLOR: {},
@ -218,7 +218,7 @@ async def test_light_functionality(
), ),
[ [
( (
"light.hood_light", "light.hood_functional_light",
{ {
COOKING_LIGHTING: { COOKING_LIGHTING: {
"value": False, "value": False,
@ -231,7 +231,7 @@ async def test_light_functionality(
"Hood", "Hood",
), ),
( (
"light.hood_light", "light.hood_functional_light",
{ {
COOKING_LIGHTING: { COOKING_LIGHTING: {
"value": True, "value": True,
@ -245,7 +245,7 @@ async def test_light_functionality(
"Hood", "Hood",
), ),
( (
"light.hood_light", "light.hood_functional_light",
{ {
COOKING_LIGHTING: {"value": False}, COOKING_LIGHTING: {"value": False},
}, },
@ -256,7 +256,7 @@ async def test_light_functionality(
"Hood", "Hood",
), ),
( (
"light.hood_ambientlight", "light.hood_ambient_light",
{ {
BSH_AMBIENT_LIGHT_ENABLED: { BSH_AMBIENT_LIGHT_ENABLED: {
"value": True, "value": True,
@ -270,7 +270,7 @@ async def test_light_functionality(
"Hood", "Hood",
), ),
( (
"light.hood_ambientlight", "light.hood_ambient_light",
{ {
BSH_AMBIENT_LIGHT_ENABLED: { BSH_AMBIENT_LIGHT_ENABLED: {
"value": True, "value": True,

View File

@ -26,14 +26,14 @@ TEST_HC_APP = "Dishwasher"
EVENT_PROG_DELAYED_START = { EVENT_PROG_DELAYED_START = {
"BSH.Common.Status.OperationState": { "BSH.Common.Status.OperationState": {
"value": "BSH.Common.EnumType.OperationState.Delayed" "value": "BSH.Common.EnumType.OperationState.DelayedStart"
}, },
} }
EVENT_PROG_REMAIN_NO_VALUE = { EVENT_PROG_REMAIN_NO_VALUE = {
"BSH.Common.Option.RemainingProgramTime": {}, "BSH.Common.Option.RemainingProgramTime": {},
"BSH.Common.Status.OperationState": { "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 mapping to expected state at each program sequence.
ENTITY_ID_STATES = { ENTITY_ID_STATES = {
"sensor.dishwasher_operation_state": ( "sensor.dishwasher_operation_state": (
"Delayed", "delayedstart",
"Run", "run",
"Run", "run",
"Run", "run",
"Ready", "ready",
), ),
"sensor.dishwasher_remaining_program_time": ( "sensor.dishwasher_program_finish_time": (
"unavailable", "unavailable",
"2021-01-09T12:00:00+00:00", "2021-01-09T12:00:00+00:00",
"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] get_appliances.return_value = [appliance]
assert config_entry.state == ConfigEntryState.NOT_LOADED 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 await integration_setup()
assert config_entry.state == ConfigEntryState.LOADED assert config_entry.state == ConfigEntryState.LOADED
@ -198,11 +200,13 @@ async def test_remaining_prog_time_edge_cases(
) -> None: ) -> None:
"""Run program sequence to test edge cases for the remaining_prog_time entity.""" """Run program sequence to test edge cases for the remaining_prog_time entity."""
get_appliances.return_value = [appliance] 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" time_to_freeze = "2021-01-09 12:00:00+00:00"
freezer.move_to(time_to_freeze) freezer.move_to(time_to_freeze)
assert config_entry.state == ConfigEntryState.NOT_LOADED 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 await integration_setup()
assert config_entry.state == ConfigEntryState.LOADED 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"), ("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", "EVENT_NOT_IN_STATUS_YET_SO_SET_TO_OFF",
"", "",
"off", "off",
"FridgeFreezer", "FridgeFreezer",
), ),
( (
"sensor.fridgefreezer_door_alarm_freezer", "sensor.fridgefreezer_freezer_door_alarm",
REFRIGERATION_EVENT_DOOR_ALARM_FREEZER, REFRIGERATION_EVENT_DOOR_ALARM_FREEZER,
BSH_EVENT_PRESENT_STATE_OFF, BSH_EVENT_PRESENT_STATE_OFF,
"off", "off",
"FridgeFreezer", "FridgeFreezer",
), ),
( (
"sensor.fridgefreezer_door_alarm_freezer", "sensor.fridgefreezer_freezer_door_alarm",
REFRIGERATION_EVENT_DOOR_ALARM_FREEZER, REFRIGERATION_EVENT_DOOR_ALARM_FREEZER,
BSH_EVENT_PRESENT_STATE_PRESENT, BSH_EVENT_PRESENT_STATE_PRESENT,
"present", "present",
"FridgeFreezer", "FridgeFreezer",
), ),
( (
"sensor.fridgefreezer_door_alarm_freezer", "sensor.fridgefreezer_freezer_door_alarm",
REFRIGERATION_EVENT_DOOR_ALARM_FREEZER, REFRIGERATION_EVENT_DOOR_ALARM_FREEZER,
BSH_EVENT_PRESENT_STATE_CONFIRMED, BSH_EVENT_PRESENT_STATE_CONFIRMED,
"confirmed", "confirmed",

View File

@ -34,7 +34,7 @@ from tests.common import MockConfigEntry, load_json_object_fixture
SETTINGS_STATUS = { SETTINGS_STATUS = {
setting.pop("key"): setting setting.pop("key"): setting
for setting in load_json_object_fixture("home_connect/settings.json") for setting in load_json_object_fixture("home_connect/settings.json")
.get("Washer") .get("Dishwasher")
.get("data") .get("data")
.get("settings") .get("settings")
} }
@ -64,34 +64,38 @@ async def test_switches(
@pytest.mark.parametrize( @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}}, {BSH_ACTIVE_PROGRAM: {"value": PROGRAM}},
SERVICE_TURN_ON, SERVICE_TURN_ON,
STATE_ON, STATE_ON,
"Dishwasher",
), ),
( (
"switch.washer_program_mix", "switch.dishwasher_program_mix",
{BSH_ACTIVE_PROGRAM: {"value": ""}}, {BSH_ACTIVE_PROGRAM: {"value": ""}},
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
STATE_OFF, STATE_OFF,
"Dishwasher",
), ),
( (
"switch.washer_power", "switch.dishwasher_power",
{BSH_POWER_STATE: {"value": BSH_POWER_ON}}, {BSH_POWER_STATE: {"value": BSH_POWER_ON}},
SERVICE_TURN_ON, SERVICE_TURN_ON,
STATE_ON, STATE_ON,
"Dishwasher",
), ),
( (
"switch.washer_power", "switch.dishwasher_power",
{BSH_POWER_STATE: {"value": BSH_POWER_OFF}}, {BSH_POWER_STATE: {"value": BSH_POWER_OFF}},
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
STATE_OFF, STATE_OFF,
"Dishwasher",
), ),
( (
"switch.washer_power", "switch.dishwasher_power",
{ {
BSH_POWER_STATE: {"value": ""}, BSH_POWER_STATE: {"value": ""},
BSH_OPERATION_STATE: { BSH_OPERATION_STATE: {
@ -100,20 +104,24 @@ async def test_switches(
}, },
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
STATE_OFF, STATE_OFF,
"Dishwasher",
), ),
( (
"switch.washer_childlock", "switch.dishwasher_child_lock",
{BSH_CHILD_LOCK_STATE: {"value": True}}, {BSH_CHILD_LOCK_STATE: {"value": True}},
SERVICE_TURN_ON, SERVICE_TURN_ON,
STATE_ON, STATE_ON,
"Dishwasher",
), ),
( (
"switch.washer_childlock", "switch.dishwasher_child_lock",
{BSH_CHILD_LOCK_STATE: {"value": False}}, {BSH_CHILD_LOCK_STATE: {"value": False}},
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
STATE_OFF, STATE_OFF,
"Dishwasher",
), ),
], ],
indirect=["appliance"],
) )
async def test_switch_functionality( async def test_switch_functionality(
entity_id: str, entity_id: str,
@ -145,45 +153,52 @@ async def test_switch_functionality(
@pytest.mark.parametrize( @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}}, {BSH_ACTIVE_PROGRAM: {"value": PROGRAM}},
SERVICE_TURN_ON, SERVICE_TURN_ON,
"start_program", "start_program",
"Dishwasher",
), ),
( (
"switch.washer_program_mix", "switch.dishwasher_program_mix",
{BSH_ACTIVE_PROGRAM: {"value": PROGRAM}}, {BSH_ACTIVE_PROGRAM: {"value": PROGRAM}},
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
"stop_program", "stop_program",
"Dishwasher",
), ),
( (
"switch.washer_power", "switch.dishwasher_power",
{BSH_POWER_STATE: {"value": ""}}, {BSH_POWER_STATE: {"value": ""}},
SERVICE_TURN_ON, SERVICE_TURN_ON,
"set_setting", "set_setting",
"Dishwasher",
), ),
( (
"switch.washer_power", "switch.dishwasher_power",
{BSH_POWER_STATE: {"value": ""}}, {BSH_POWER_STATE: {"value": ""}},
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
"set_setting", "set_setting",
"Dishwasher",
), ),
( (
"switch.washer_childlock", "switch.dishwasher_child_lock",
{BSH_CHILD_LOCK_STATE: {"value": ""}}, {BSH_CHILD_LOCK_STATE: {"value": ""}},
SERVICE_TURN_ON, SERVICE_TURN_ON,
"set_setting", "set_setting",
"Dishwasher",
), ),
( (
"switch.washer_childlock", "switch.dishwasher_child_lock",
{BSH_CHILD_LOCK_STATE: {"value": ""}}, {BSH_CHILD_LOCK_STATE: {"value": ""}},
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
"set_setting", "set_setting",
"Dishwasher",
), ),
], ],
indirect=["problematic_appliance"],
) )
async def test_switch_exception_handling( async def test_switch_exception_handling(
entity_id: str, entity_id: str,
@ -204,6 +219,7 @@ async def test_switch_exception_handling(
get_appliances.return_value = [problematic_appliance] get_appliances.return_value = [problematic_appliance]
assert config_entry.state == ConfigEntryState.NOT_LOADED assert config_entry.state == ConfigEntryState.NOT_LOADED
problematic_appliance.status.update(status)
assert await integration_setup() assert await integration_setup()
assert config_entry.state == ConfigEntryState.LOADED assert config_entry.state == ConfigEntryState.LOADED
@ -211,7 +227,6 @@ async def test_switch_exception_handling(
with pytest.raises(HomeConnectError): with pytest.raises(HomeConnectError):
getattr(problematic_appliance, mock_attr)() getattr(problematic_appliance, mock_attr)()
problematic_appliance.status.update(status)
await hass.services.async_call( await hass.services.async_call(
SWITCH_DOMAIN, service, {"entity_id": entity_id}, blocking=True 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"), ("entity_id", "status", "service", "state", "appliance"),
[ [
( (
"switch.fridgefreezer_supermode_freezer", "switch.fridgefreezer_freezer_super_mode",
{REFRIGERATION_SUPERMODEFREEZER: {"value": True}}, {REFRIGERATION_SUPERMODEFREEZER: {"value": True}},
SERVICE_TURN_ON, SERVICE_TURN_ON,
STATE_ON, STATE_ON,
"FridgeFreezer", "FridgeFreezer",
), ),
( (
"switch.fridgefreezer_supermode_freezer", "switch.fridgefreezer_freezer_super_mode",
{REFRIGERATION_SUPERMODEFREEZER: {"value": False}}, {REFRIGERATION_SUPERMODEFREEZER: {"value": False}},
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
STATE_OFF, STATE_OFF,
@ -277,14 +292,14 @@ async def test_ent_desc_switch_functionality(
("entity_id", "status", "service", "mock_attr", "problematic_appliance"), ("entity_id", "status", "service", "mock_attr", "problematic_appliance"),
[ [
( (
"switch.fridgefreezer_supermode_freezer", "switch.fridgefreezer_freezer_super_mode",
{REFRIGERATION_SUPERMODEFREEZER: {"value": ""}}, {REFRIGERATION_SUPERMODEFREEZER: {"value": ""}},
SERVICE_TURN_ON, SERVICE_TURN_ON,
"set_setting", "set_setting",
"FridgeFreezer", "FridgeFreezer",
), ),
( (
"switch.fridgefreezer_supermode_freezer", "switch.fridgefreezer_freezer_super_mode",
{REFRIGERATION_SUPERMODEFREEZER: {"value": ""}}, {REFRIGERATION_SUPERMODEFREEZER: {"value": ""}},
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
"set_setting", "set_setting",