"""Provides a sensor for Home Connect.""" from dataclasses import dataclass from datetime import timedelta import logging from typing import cast from aiohomeconnect.model import EventKey, StatusKey from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfVolume from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import dt as dt_util, slugify from .common import setup_home_connect_entry from .const import ( APPLIANCES_WITH_PROGRAMS, BSH_OPERATION_STATE_FINISHED, BSH_OPERATION_STATE_PAUSE, BSH_OPERATION_STATE_RUN, UNIT_MAP, ) from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry from .entity import HomeConnectEntity, constraint_fetcher _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 EVENT_OPTIONS = ["confirmed", "off", "present"] @dataclass(frozen=True, kw_only=True) class HomeConnectSensorEntityDescription( SensorEntityDescription, ): """Entity Description class for sensors.""" default_value: str | None = None appliance_types: tuple[str, ...] | None = None fetch_unit: bool = False BSH_PROGRAM_SENSORS = ( HomeConnectSensorEntityDescription( key=EventKey.BSH_COMMON_OPTION_REMAINING_PROGRAM_TIME, device_class=SensorDeviceClass.TIMESTAMP, translation_key="program_finish_time", appliance_types=( "CoffeeMaker", "CookProcessor", "Dishwasher", "Dryer", "Hood", "Oven", "Washer", "WasherDryer", ), ), HomeConnectSensorEntityDescription( key=EventKey.BSH_COMMON_OPTION_PROGRAM_PROGRESS, native_unit_of_measurement=PERCENTAGE, translation_key="program_progress", appliance_types=APPLIANCES_WITH_PROGRAMS, ), ) SENSORS = ( HomeConnectSensorEntityDescription( key=StatusKey.BSH_COMMON_OPERATION_STATE, device_class=SensorDeviceClass.ENUM, options=[ "inactive", "ready", "delayedstart", "run", "pause", "actionrequired", "finished", "error", "aborting", ], translation_key="operation_state", ), HomeConnectSensorEntityDescription( key=StatusKey.BSH_COMMON_DOOR_STATE, device_class=SensorDeviceClass.ENUM, options=[ "closed", "locked", "open", ], translation_key="door", ), HomeConnectSensorEntityDescription( key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_COFFEE, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL_INCREASING, translation_key="coffee_counter", ), HomeConnectSensorEntityDescription( key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_POWDER_COFFEE, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL_INCREASING, translation_key="powder_coffee_counter", ), HomeConnectSensorEntityDescription( key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_HOT_WATER, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfVolume.MILLILITERS, device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, translation_key="hot_water_counter", ), HomeConnectSensorEntityDescription( key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_HOT_WATER_CUPS, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL_INCREASING, translation_key="hot_water_cups_counter", ), HomeConnectSensorEntityDescription( key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_HOT_MILK, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL_INCREASING, translation_key="hot_milk_counter", ), HomeConnectSensorEntityDescription( key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_FROTHY_MILK, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL_INCREASING, translation_key="frothy_milk_counter", ), HomeConnectSensorEntityDescription( key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_MILK, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL_INCREASING, translation_key="milk_counter", ), HomeConnectSensorEntityDescription( key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_COFFEE_AND_MILK, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL_INCREASING, translation_key="coffee_and_milk_counter", ), HomeConnectSensorEntityDescription( key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_RISTRETTO_ESPRESSO, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL_INCREASING, translation_key="ristretto_espresso_counter", ), HomeConnectSensorEntityDescription( key=StatusKey.BSH_COMMON_BATTERY_LEVEL, device_class=SensorDeviceClass.BATTERY, ), HomeConnectSensorEntityDescription( key=StatusKey.BSH_COMMON_VIDEO_CAMERA_STATE, device_class=SensorDeviceClass.ENUM, options=[ "disabled", "sleeping", "ready", "streaminglocal", "streamingcloud", "streaminglocalancloud", "error", ], translation_key="camera_state", ), HomeConnectSensorEntityDescription( key=StatusKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_LAST_SELECTED_MAP, device_class=SensorDeviceClass.ENUM, options=[ "tempmap", "map1", "map2", "map3", ], translation_key="last_selected_map", ), HomeConnectSensorEntityDescription( key=StatusKey.COOKING_OVEN_CURRENT_CAVITY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, translation_key="oven_current_cavity_temperature", fetch_unit=True, ), ) EVENT_SENSORS = ( HomeConnectSensorEntityDescription( key=EventKey.BSH_COMMON_EVENT_PROGRAM_ABORTED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="program_aborted", appliance_types=("Dishwasher", "CleaningRobot", "CookProcessor"), ), HomeConnectSensorEntityDescription( key=EventKey.BSH_COMMON_EVENT_PROGRAM_FINISHED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="program_finished", appliance_types=( "Oven", "Dishwasher", "Washer", "Dryer", "WasherDryer", "CleaningRobot", "CookProcessor", ), ), HomeConnectSensorEntityDescription( key=EventKey.BSH_COMMON_EVENT_ALARM_CLOCK_ELAPSED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="alarm_clock_elapsed", appliance_types=("Oven", "Cooktop"), ), HomeConnectSensorEntityDescription( key=EventKey.COOKING_OVEN_EVENT_PREHEAT_FINISHED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="preheat_finished", appliance_types=("Oven", "Cooktop"), ), HomeConnectSensorEntityDescription( key=EventKey.COOKING_OVEN_EVENT_REGULAR_PREHEAT_FINISHED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="regular_preheat_finished", appliance_types=("Oven",), ), HomeConnectSensorEntityDescription( key=EventKey.LAUNDRY_CARE_DRYER_EVENT_DRYING_PROCESS_FINISHED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="drying_process_finished", appliance_types=("Dryer",), ), HomeConnectSensorEntityDescription( key=EventKey.DISHCARE_DISHWASHER_EVENT_SALT_NEARLY_EMPTY, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="salt_nearly_empty", appliance_types=("Dishwasher",), ), HomeConnectSensorEntityDescription( key=EventKey.DISHCARE_DISHWASHER_EVENT_RINSE_AID_NEARLY_EMPTY, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="rinse_aid_nearly_empty", appliance_types=("Dishwasher",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_BEAN_CONTAINER_EMPTY, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="bean_container_empty", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_WATER_TANK_EMPTY, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="water_tank_empty", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DRIP_TRAY_FULL, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="drip_tray_full", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_KEEP_MILK_TANK_COOL, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="keep_milk_tank_cool", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DESCALING_IN_20_CUPS, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="descaling_in_20_cups", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DESCALING_IN_15_CUPS, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="descaling_in_15_cups", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DESCALING_IN_10_CUPS, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="descaling_in_10_cups", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DESCALING_IN_5_CUPS, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="descaling_in_5_cups", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_SHOULD_BE_DESCALED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="device_should_be_descaled", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_DESCALING_OVERDUE, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="device_descaling_overdue", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_DESCALING_BLOCKAGE, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="device_descaling_blockage", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_SHOULD_BE_CLEANED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="device_should_be_cleaned", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_CLEANING_OVERDUE, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="device_cleaning_overdue", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_CALC_N_CLEAN_IN20CUPS, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="calc_n_clean_in20cups", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_CALC_N_CLEAN_IN15CUPS, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="calc_n_clean_in15cups", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_CALC_N_CLEAN_IN10CUPS, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="calc_n_clean_in10cups", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_CALC_N_CLEAN_IN5CUPS, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="calc_n_clean_in5cups", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_SHOULD_BE_CALC_N_CLEANED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="device_should_be_calc_n_cleaned", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_CALC_N_CLEAN_OVERDUE, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="device_calc_n_clean_overdue", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_CALC_N_CLEAN_BLOCKAGE, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="device_calc_n_clean_blockage", appliance_types=("CoffeeMaker",), ), HomeConnectSensorEntityDescription( key=EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_FREEZER, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="freezer_door_alarm", appliance_types=("FridgeFreezer", "Freezer"), ), HomeConnectSensorEntityDescription( key=EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_REFRIGERATOR, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="refrigerator_door_alarm", appliance_types=("FridgeFreezer", "Refrigerator"), ), HomeConnectSensorEntityDescription( key=EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_TEMPERATURE_ALARM_FREEZER, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="freezer_temperature_alarm", appliance_types=("FridgeFreezer", "Freezer"), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_EVENT_EMPTY_DUST_BOX_AND_CLEAN_FILTER, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="empty_dust_box_and_clean_filter", appliance_types=("CleaningRobot",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_EVENT_ROBOT_IS_STUCK, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="robot_is_stuck", appliance_types=("CleaningRobot",), ), HomeConnectSensorEntityDescription( key=EventKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_EVENT_DOCKING_STATION_NOT_FOUND, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="docking_station_not_found", appliance_types=("CleaningRobot",), ), HomeConnectSensorEntityDescription( key=EventKey.LAUNDRY_CARE_WASHER_EVENT_I_DOS_1_FILL_LEVEL_POOR, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="poor_i_dos_1_fill_level", appliance_types=("Washer", "WasherDryer"), ), HomeConnectSensorEntityDescription( key=EventKey.LAUNDRY_CARE_WASHER_EVENT_I_DOS_2_FILL_LEVEL_POOR, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="poor_i_dos_2_fill_level", appliance_types=("Washer", "WasherDryer"), ), HomeConnectSensorEntityDescription( key=EventKey.COOKING_COMMON_EVENT_HOOD_GREASE_FILTER_MAX_SATURATION_NEARLY_REACHED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="grease_filter_max_saturation_nearly_reached", appliance_types=("Hood",), ), HomeConnectSensorEntityDescription( key=EventKey.COOKING_COMMON_EVENT_HOOD_GREASE_FILTER_MAX_SATURATION_REACHED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, default_value="off", translation_key="grease_filter_max_saturation_reached", appliance_types=("Hood",), ), ) def _get_entities_for_appliance( entry: HomeConnectConfigEntry, appliance: HomeConnectApplianceData, ) -> list[HomeConnectEntity]: """Get a list of entities.""" return [ *[ HomeConnectEventSensor(entry.runtime_data, appliance, description) for description in EVENT_SENSORS if description.appliance_types and appliance.info.type in description.appliance_types ], *[ HomeConnectProgramSensor(entry.runtime_data, appliance, desc) for desc in BSH_PROGRAM_SENSORS if desc.appliance_types and appliance.info.type in desc.appliance_types ], *[ HomeConnectSensor(entry.runtime_data, appliance, description) for description in SENSORS if description.key in appliance.status ], ] async def async_setup_entry( hass: HomeAssistant, entry: HomeConnectConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Home Connect sensor.""" setup_home_connect_entry( entry, _get_entities_for_appliance, async_add_entities, ) class HomeConnectSensor(HomeConnectEntity, SensorEntity): """Sensor class for Home Connect.""" entity_description: HomeConnectSensorEntityDescription def update_native_value(self) -> None: """Set the value of the sensor.""" status = self.appliance.status[cast(StatusKey, self.bsh_key)].value self._update_native_value(status) def _update_native_value(self, status: str | float) -> None: """Set the value of the sensor based on the given value.""" match self.device_class: case SensorDeviceClass.TIMESTAMP: self._attr_native_value = dt_util.utcnow() + timedelta( seconds=cast(float, status) ) 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 = slugify(cast(str, status).split(".")[-1]) case _: self._attr_native_value = status async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() if self.entity_description.fetch_unit: data = self.appliance.status[cast(StatusKey, self.bsh_key)] if data.unit: self._attr_native_unit_of_measurement = UNIT_MAP.get( data.unit, data.unit ) else: await self.fetch_unit() @constraint_fetcher async def fetch_unit(self) -> None: """Fetch the unit of measurement.""" data = await self.coordinator.client.get_status_value( self.appliance.info.ha_id, status_key=cast(StatusKey, self.bsh_key) ) if data.unit: self._attr_native_unit_of_measurement = UNIT_MAP.get(data.unit, data.unit) class HomeConnectProgramSensor(HomeConnectSensor): """Sensor class for Home Connect sensors that reports information related to the running program.""" program_running: bool = False async def async_added_to_hass(self) -> None: """Register listener.""" await super().async_added_to_hass() self.async_on_remove( self.coordinator.async_add_listener( self._handle_operation_state_event, (self.appliance.info.ha_id, EventKey.BSH_COMMON_STATUS_OPERATION_STATE), ) ) @callback def _handle_operation_state_event(self) -> None: """Update status when an event for the entity is received.""" self.program_running = ( status := self.appliance.status.get(StatusKey.BSH_COMMON_OPERATION_STATE) ) is not None and status.value in [ BSH_OPERATION_STATE_RUN, BSH_OPERATION_STATE_PAUSE, BSH_OPERATION_STATE_FINISHED, ] if not self.program_running: # reset the value when the program is not running, paused or finished self._attr_native_value = None self.async_write_ha_state() @property def available(self) -> bool: """Return true if the sensor is available.""" # These sensors are only available if the program is running, paused or finished. # Otherwise, some sensors report erroneous values. return super().available and self.program_running def update_native_value(self) -> None: """Update the program sensor's status.""" self.program_running = ( status := self.appliance.status.get(StatusKey.BSH_COMMON_OPERATION_STATE) ) is not None and status.value in [ BSH_OPERATION_STATE_RUN, BSH_OPERATION_STATE_PAUSE, BSH_OPERATION_STATE_FINISHED, ] event = self.appliance.events.get(cast(EventKey, self.bsh_key)) if event: self._update_native_value(event.value) class HomeConnectEventSensor(HomeConnectSensor): """Sensor class for Home Connect events.""" _attr_entity_registry_enabled_default = False def update_native_value(self) -> None: """Update the sensor's status.""" event = self.appliance.events.get(cast(EventKey, self.bsh_key)) if event: self._update_native_value(event.value) elif self._attr_native_value is None: self._attr_native_value = self.entity_description.default_value