Files
core/homeassistant/components/miele/sensor.py
Åke Strandberg e7e13ecc74 Refactor miele program id codes part 3(3) (#144196)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-11-20 09:59:28 +01:00

1118 lines
40 KiB
Python

"""Sensor platform for Miele integration."""
from __future__ import annotations
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
from typing import Any, Final, cast
from pymiele import MieleDevice, MieleTemperature
from homeassistant.components.sensor import (
RestoreSensor,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
EntityCategory,
UnitOfEnergy,
UnitOfTemperature,
UnitOfTime,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util import dt as dt_util
from .const import (
COFFEE_SYSTEM_PROFILE,
DISABLED_TEMP_ENTITIES,
DOMAIN,
PROGRAM_IDS,
PROGRAM_PHASE,
STATE_STATUS_TAGS,
MieleAppliance,
PlatePowerStep,
StateDryingStep,
StateProgramType,
StateStatus,
)
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
from .entity import MieleEntity
PARALLEL_UPDATES = 0
_LOGGER = logging.getLogger(__name__)
DEFAULT_PLATE_COUNT = 4
PLATE_COUNT = {
"KM7575": 6,
"KM7678": 6,
"KM7697": 6,
"KM7878": 6,
"KM7897": 6,
"KMDA7633": 5,
"KMDA7634": 5,
"KMDA7774": 5,
"KMX": 6,
}
ATTRIBUTE_PROFILE = "profile"
def _get_plate_count(tech_type: str) -> int:
"""Get number of zones for hob."""
stripped = tech_type.replace(" ", "")
for prefix, plates in PLATE_COUNT.items():
if stripped.startswith(prefix):
return plates
return DEFAULT_PLATE_COUNT
def _convert_duration(value_list: list[int]) -> int | None:
"""Convert duration to minutes."""
return value_list[0] * 60 + value_list[1] if value_list else None
def _convert_temperature(
value_list: list[MieleTemperature], index: int
) -> float | None:
"""Convert temperature object to readable value."""
if index >= len(value_list):
return None
raw_value = cast(int, value_list[index].temperature) / 100.0
if raw_value in DISABLED_TEMP_ENTITIES:
return None
return raw_value
def _get_coffee_profile(value: MieleDevice) -> str | None:
"""Get coffee profile from value."""
if value.state_program_id is not None:
for key_range, profile in COFFEE_SYSTEM_PROFILE.items():
if value.state_program_id in key_range:
return profile
return None
def _convert_start_timestamp(
elapsed_time_list: list[int], start_time_list: list[int]
) -> datetime | None:
"""Convert raw values representing time into start timestamp."""
now = dt_util.utcnow()
elapsed_duration = _convert_duration(elapsed_time_list)
delayed_start_duration = _convert_duration(start_time_list)
if (elapsed_duration is None or elapsed_duration == 0) and (
delayed_start_duration is None or delayed_start_duration == 0
):
return None
if elapsed_duration is not None and elapsed_duration > 0:
duration = -elapsed_duration
elif delayed_start_duration is not None and delayed_start_duration > 0:
duration = delayed_start_duration
delta = timedelta(minutes=duration)
return (now + delta).replace(second=0, microsecond=0)
def _convert_finish_timestamp(
remaining_time_list: list[int], start_time_list: list[int]
) -> datetime | None:
"""Convert raw values representing time into finish timestamp."""
now = dt_util.utcnow()
program_duration = _convert_duration(remaining_time_list)
delayed_start_duration = _convert_duration(start_time_list)
if program_duration is None or program_duration == 0:
return None
duration = program_duration + (
delayed_start_duration if delayed_start_duration is not None else 0
)
delta = timedelta(minutes=duration)
return (now + delta).replace(second=0, microsecond=0)
@dataclass(frozen=True, kw_only=True)
class MieleSensorDescription(SensorEntityDescription):
"""Class describing Miele sensor entities."""
value_fn: Callable[[MieleDevice], StateType | datetime]
end_value_fn: Callable[[StateType | datetime], StateType | datetime] | None = None
extra_attributes: dict[str, Callable[[MieleDevice], StateType]] | None = None
zone: int | None = None
unique_id_fn: Callable[[str, MieleSensorDescription], str] | None = None
@dataclass
class MieleSensorDefinition:
"""Class for defining sensor entities."""
types: tuple[MieleAppliance, ...]
description: MieleSensorDescription
SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
MieleAppliance.TUMBLE_DRYER,
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
MieleAppliance.DISHWASHER,
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.HOB_HIGHLIGHT,
MieleAppliance.STEAM_OVEN,
MieleAppliance.MICROWAVE,
MieleAppliance.COFFEE_SYSTEM,
MieleAppliance.HOOD,
MieleAppliance.FRIDGE,
MieleAppliance.FREEZER,
MieleAppliance.FRIDGE_FREEZER,
MieleAppliance.ROBOT_VACUUM_CLEANER,
MieleAppliance.WASHER_DRYER,
MieleAppliance.DISH_WARMER,
MieleAppliance.HOB_INDUCTION,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.WINE_CABINET,
MieleAppliance.WINE_CONDITIONING_UNIT,
MieleAppliance.WINE_STORAGE_CONDITIONING_UNIT,
MieleAppliance.STEAM_OVEN_MICRO,
MieleAppliance.DIALOG_OVEN,
MieleAppliance.WINE_CABINET_FREEZER,
MieleAppliance.STEAM_OVEN_MK2,
MieleAppliance.HOB_INDUCT_EXTR,
),
description=MieleSensorDescription(
key="state_status",
translation_key="status",
value_fn=lambda value: value.state_status,
device_class=SensorDeviceClass.ENUM,
options=sorted(set(STATE_STATUS_TAGS.values())),
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
MieleAppliance.TUMBLE_DRYER,
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
MieleAppliance.DISHWASHER,
MieleAppliance.DISH_WARMER,
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN,
MieleAppliance.MICROWAVE,
MieleAppliance.ROBOT_VACUUM_CLEANER,
MieleAppliance.WASHER_DRYER,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.STEAM_OVEN_MICRO,
MieleAppliance.DIALOG_OVEN,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_program_id",
translation_key="program_id",
device_class=SensorDeviceClass.ENUM,
value_fn=lambda value: value.state_program_id,
),
),
MieleSensorDefinition(
types=(MieleAppliance.COFFEE_SYSTEM,),
description=MieleSensorDescription(
key="state_program_id",
translation_key="program_id",
device_class=SensorDeviceClass.ENUM,
value_fn=lambda value: value.state_program_id,
extra_attributes={
ATTRIBUTE_PROFILE: _get_coffee_profile,
},
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
MieleAppliance.TUMBLE_DRYER,
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
MieleAppliance.DISHWASHER,
MieleAppliance.DISH_WARMER,
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN,
MieleAppliance.MICROWAVE,
MieleAppliance.COFFEE_SYSTEM,
MieleAppliance.WASHER_DRYER,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.STEAM_OVEN_MICRO,
MieleAppliance.DIALOG_OVEN,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_program_phase",
translation_key="program_phase",
value_fn=lambda value: value.state_program_phase,
device_class=SensorDeviceClass.ENUM,
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
MieleAppliance.TUMBLE_DRYER,
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
MieleAppliance.DISHWASHER,
MieleAppliance.DISH_WARMER,
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN,
MieleAppliance.MICROWAVE,
MieleAppliance.ROBOT_VACUUM_CLEANER,
MieleAppliance.WASHER_DRYER,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.STEAM_OVEN_MICRO,
MieleAppliance.DIALOG_OVEN,
MieleAppliance.COFFEE_SYSTEM,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_program_type",
translation_key="program_type",
value_fn=lambda value: StateProgramType(value.state_program_type).name,
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
options=sorted(set(StateProgramType.keys())),
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
MieleAppliance.TUMBLE_DRYER,
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
MieleAppliance.DISHWASHER,
MieleAppliance.WASHER_DRYER,
),
description=MieleSensorDescription(
key="current_energy_consumption",
translation_key="energy_consumption",
value_fn=lambda value: value.current_energy_consumption,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_display_precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
MieleAppliance.TUMBLE_DRYER,
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
MieleAppliance.DISHWASHER,
MieleAppliance.WASHER_DRYER,
),
description=MieleSensorDescription(
key="energy_forecast",
translation_key="energy_forecast",
value_fn=(
lambda value: value.energy_forecast * 100
if value.energy_forecast is not None
else None
),
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.DISHWASHER,
MieleAppliance.WASHER_DRYER,
),
description=MieleSensorDescription(
key="current_water_consumption",
translation_key="water_consumption",
value_fn=lambda value: value.current_water_consumption,
device_class=SensorDeviceClass.WATER,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfVolume.LITERS,
suggested_display_precision=0,
entity_category=EntityCategory.DIAGNOSTIC,
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.DISHWASHER,
MieleAppliance.WASHER_DRYER,
),
description=MieleSensorDescription(
key="water_forecast",
translation_key="water_forecast",
value_fn=(
lambda value: value.water_forecast * 100
if value.water_forecast is not None
else None
),
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
MieleAppliance.WASHER_DRYER,
),
description=MieleSensorDescription(
key="state_spinning_speed",
translation_key="spin_speed",
value_fn=lambda value: value.state_spinning_speed,
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
entity_category=EntityCategory.DIAGNOSTIC,
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
MieleAppliance.TUMBLE_DRYER,
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
MieleAppliance.DISHWASHER,
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN,
MieleAppliance.MICROWAVE,
MieleAppliance.ROBOT_VACUUM_CLEANER,
MieleAppliance.WASHER_DRYER,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.STEAM_OVEN_MICRO,
MieleAppliance.DIALOG_OVEN,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_remaining_time",
translation_key="remaining_time",
value_fn=lambda value: _convert_duration(value.state_remaining_time),
end_value_fn=lambda last_value: 0,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
entity_category=EntityCategory.DIAGNOSTIC,
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.TUMBLE_DRYER,
MieleAppliance.DISHWASHER,
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN,
MieleAppliance.MICROWAVE,
MieleAppliance.WASHER_DRYER,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.STEAM_OVEN_MICRO,
MieleAppliance.DIALOG_OVEN,
MieleAppliance.ROBOT_VACUUM_CLEANER,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_elapsed_time",
translation_key="elapsed_time",
value_fn=lambda value: _convert_duration(value.state_elapsed_time),
end_value_fn=lambda last_value: last_value,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
entity_category=EntityCategory.DIAGNOSTIC,
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
MieleAppliance.TUMBLE_DRYER,
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
MieleAppliance.DISHWASHER,
MieleAppliance.DISH_WARMER,
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN,
MieleAppliance.MICROWAVE,
MieleAppliance.WASHER_DRYER,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.STEAM_OVEN_MICRO,
MieleAppliance.DIALOG_OVEN,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_start_time",
translation_key="start_time",
value_fn=lambda value: _convert_duration(value.state_start_time),
end_value_fn=lambda last_value: None,
native_unit_of_measurement=UnitOfTime.MINUTES,
device_class=SensorDeviceClass.DURATION,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=2,
suggested_unit_of_measurement=UnitOfTime.HOURS,
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
MieleAppliance.TUMBLE_DRYER,
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
MieleAppliance.DISHWASHER,
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN,
MieleAppliance.MICROWAVE,
MieleAppliance.ROBOT_VACUUM_CLEANER,
MieleAppliance.WASHER_DRYER,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.STEAM_OVEN_MICRO,
MieleAppliance.DIALOG_OVEN,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_finish_timestamp",
translation_key="finish",
value_fn=lambda value: _convert_finish_timestamp(
value.state_remaining_time, value.state_start_time
),
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.TUMBLE_DRYER,
MieleAppliance.DISHWASHER,
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN,
MieleAppliance.MICROWAVE,
MieleAppliance.WASHER_DRYER,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.STEAM_OVEN_MICRO,
MieleAppliance.DIALOG_OVEN,
MieleAppliance.ROBOT_VACUUM_CLEANER,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_start_timestamp",
translation_key="start",
value_fn=lambda value: _convert_start_timestamp(
value.state_elapsed_time, value.state_start_time
),
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
),
),
MieleSensorDefinition(
types=(
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.DISH_WARMER,
MieleAppliance.STEAM_OVEN,
MieleAppliance.MICROWAVE,
MieleAppliance.FRIDGE,
MieleAppliance.FREEZER,
MieleAppliance.FRIDGE_FREEZER,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.WINE_CABINET,
MieleAppliance.WINE_CONDITIONING_UNIT,
MieleAppliance.WINE_STORAGE_CONDITIONING_UNIT,
MieleAppliance.STEAM_OVEN_MICRO,
MieleAppliance.DIALOG_OVEN,
MieleAppliance.WINE_CABINET_FREEZER,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_temperature_1",
zone=1,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda value: _convert_temperature(value.state_temperatures, 0),
),
),
MieleSensorDefinition(
types=(
MieleAppliance.FRIDGE_FREEZER,
MieleAppliance.WINE_CABINET,
MieleAppliance.WINE_CONDITIONING_UNIT,
MieleAppliance.WINE_STORAGE_CONDITIONING_UNIT,
MieleAppliance.WINE_CABINET_FREEZER,
),
description=MieleSensorDescription(
key="state_temperature_2",
zone=2,
device_class=SensorDeviceClass.TEMPERATURE,
translation_key="temperature_zone_2",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda value: _convert_temperature(value.state_temperatures, 1),
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WINE_CABINET,
MieleAppliance.WINE_CONDITIONING_UNIT,
MieleAppliance.WINE_STORAGE_CONDITIONING_UNIT,
MieleAppliance.WINE_CABINET_FREEZER,
),
description=MieleSensorDescription(
key="state_temperature_3",
zone=3,
device_class=SensorDeviceClass.TEMPERATURE,
translation_key="temperature_zone_3",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda value: _convert_temperature(value.state_temperatures, 2),
),
),
MieleSensorDefinition(
types=(
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_core_target_temperature",
translation_key="core_target_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda value: _convert_temperature(
value.state_core_target_temperature, 0
),
),
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHING_MACHINE,
MieleAppliance.WASHER_DRYER,
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN_MICRO,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_target_temperature",
translation_key="target_temperature",
zone=1,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda value: _convert_temperature(
value.state_target_temperature, 0
),
),
),
MieleSensorDefinition(
types=(
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN_COMBI,
),
description=MieleSensorDescription(
key="state_core_temperature",
translation_key="core_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda value: _convert_temperature(
value.state_core_temperature, 0
),
),
),
*(
MieleSensorDefinition(
types=(
MieleAppliance.HOB_HIGHLIGHT,
MieleAppliance.HOB_INDUCT_EXTR,
MieleAppliance.HOB_INDUCTION,
),
description=MieleSensorDescription(
key="state_plate_step",
translation_key="plate",
translation_placeholders={"plate_no": str(i)},
zone=i,
device_class=SensorDeviceClass.ENUM,
options=sorted(PlatePowerStep.keys()),
value_fn=lambda value: None,
unique_id_fn=lambda device_id,
description: f"{device_id}-{description.key}-{description.zone}",
),
)
for i in range(1, 7)
),
MieleSensorDefinition(
types=(
MieleAppliance.WASHER_DRYER,
MieleAppliance.TUMBLE_DRYER,
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
),
description=MieleSensorDescription(
key="state_drying_step",
translation_key="drying_step",
value_fn=lambda value: StateDryingStep(
cast(int, value.state_drying_step)
).name,
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
options=sorted(StateDryingStep.keys()),
),
),
MieleSensorDefinition(
types=(MieleAppliance.ROBOT_VACUUM_CLEANER,),
description=MieleSensorDescription(
key="state_battery",
value_fn=lambda value: value.state_battery_level,
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.BATTERY,
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: MieleConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
coordinator = config_entry.runtime_data
added_devices: set[str] = set() # device_id
added_entities: set[str] = set() # unique_id
def _get_entity_class(definition: MieleSensorDefinition) -> type[MieleSensor]:
"""Get the entity class for the sensor."""
return {
"state_status": MieleStatusSensor,
"state_program_id": MieleProgramIdSensor,
"state_program_phase": MielePhaseSensor,
"state_plate_step": MielePlateSensor,
"state_elapsed_time": MieleTimeSensor,
"state_remaining_time": MieleTimeSensor,
"state_start_time": MieleTimeSensor,
"state_start_timestamp": MieleAbsoluteTimeSensor,
"state_finish_timestamp": MieleAbsoluteTimeSensor,
"current_energy_consumption": MieleConsumptionSensor,
"current_water_consumption": MieleConsumptionSensor,
}.get(definition.description.key, MieleSensor)
def _is_entity_registered(unique_id: str) -> bool:
"""Check if the entity is already registered."""
entity_registry = er.async_get(hass)
return any(
entry.platform == DOMAIN and entry.unique_id == unique_id
for entry in entity_registry.entities.values()
)
def _is_sensor_enabled(
definition: MieleSensorDefinition,
device: MieleDevice,
unique_id: str,
) -> bool:
"""Check if the sensor is enabled."""
if (
definition.description.device_class == SensorDeviceClass.TEMPERATURE
and definition.description.value_fn(device) is None
and definition.description.zone != 1
):
# all appliances supporting temperature have at least zone 1, for other zones
# don't create entity if API signals that datapoint is disabled, unless the sensor
# already appeared in the past (= it provided a valid value)
return _is_entity_registered(unique_id)
if (
definition.description.key == "state_plate_step"
and definition.description.zone is not None
and definition.description.zone > _get_plate_count(device.tech_type)
):
# don't create plate entity if not expected by the appliance tech type
return False
return True
def _async_add_devices() -> None:
nonlocal added_devices, added_entities
entities: list = []
entity_class: type[MieleSensor]
new_devices_set, current_devices = coordinator.async_add_devices(added_devices)
added_devices = current_devices
for device_id, device in coordinator.data.devices.items():
for definition in SENSOR_TYPES:
# device is not supported, skip
if device.device_type not in definition.types:
continue
entity_class = _get_entity_class(definition)
unique_id = (
definition.description.unique_id_fn(
device_id, definition.description
)
if definition.description.unique_id_fn is not None
else MieleEntity.get_unique_id(device_id, definition.description)
)
# entity was already added, skip
if device_id not in new_devices_set and unique_id in added_entities:
continue
# sensors is not enabled, skip
if not _is_sensor_enabled(definition, device, unique_id):
continue
added_entities.add(unique_id)
entities.append(
entity_class(coordinator, device_id, definition.description)
)
async_add_entities(entities)
config_entry.async_on_unload(coordinator.async_add_listener(_async_add_devices))
_async_add_devices()
APPLIANCE_ICONS = {
MieleAppliance.WASHING_MACHINE: "mdi:washing-machine",
MieleAppliance.TUMBLE_DRYER: "mdi:tumble-dryer",
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL: "mdi:tumble-dryer",
MieleAppliance.DISHWASHER: "mdi:dishwasher",
MieleAppliance.OVEN: "mdi:chef-hat",
MieleAppliance.OVEN_MICROWAVE: "mdi:chef-hat",
MieleAppliance.HOB_HIGHLIGHT: "mdi:pot-steam-outline",
MieleAppliance.STEAM_OVEN: "mdi:chef-hat",
MieleAppliance.MICROWAVE: "mdi:microwave",
MieleAppliance.COFFEE_SYSTEM: "mdi:coffee-maker",
MieleAppliance.HOOD: "mdi:turbine",
MieleAppliance.FRIDGE: "mdi:fridge-industrial-outline",
MieleAppliance.FREEZER: "mdi:fridge-industrial-outline",
MieleAppliance.FRIDGE_FREEZER: "mdi:fridge-outline",
MieleAppliance.ROBOT_VACUUM_CLEANER: "mdi:robot-vacuum",
MieleAppliance.WASHER_DRYER: "mdi:washing-machine",
MieleAppliance.DISH_WARMER: "mdi:heat-wave",
MieleAppliance.HOB_INDUCTION: "mdi:pot-steam-outline",
MieleAppliance.STEAM_OVEN_COMBI: "mdi:chef-hat",
MieleAppliance.WINE_CABINET: "mdi:glass-wine",
MieleAppliance.WINE_CONDITIONING_UNIT: "mdi:glass-wine",
MieleAppliance.WINE_STORAGE_CONDITIONING_UNIT: "mdi:glass-wine",
MieleAppliance.STEAM_OVEN_MICRO: "mdi:chef-hat",
MieleAppliance.DIALOG_OVEN: "mdi:chef-hat",
MieleAppliance.WINE_CABINET_FREEZER: "mdi:glass-wine",
MieleAppliance.HOB_INDUCT_EXTR: "mdi:pot-steam-outline",
}
class MieleSensor(MieleEntity, SensorEntity):
"""Representation of a Sensor."""
entity_description: MieleSensorDescription
def __init__(
self,
coordinator: MieleDataUpdateCoordinator,
device_id: str,
description: MieleSensorDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, device_id, description)
if description.unique_id_fn is not None:
self._attr_unique_id = description.unique_id_fn(device_id, description)
@property
def native_value(self) -> StateType | datetime:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.device)
@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return extra_state_attributes."""
if self.entity_description.extra_attributes is None:
return None
attr = {}
for key, value in self.entity_description.extra_attributes.items():
attr[key] = value(self.device)
return attr
class MieleRestorableSensor(MieleSensor, RestoreSensor):
"""Representation of a Sensor whose internal state can be restored."""
_attr_native_value: StateType | datetime
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
# recover last value from cache when adding entity
last_data = await self.async_get_last_sensor_data()
if last_data:
self._attr_native_value = last_data.native_value # type: ignore[assignment]
@property
def native_value(self) -> StateType | datetime:
"""Return the state of the sensor.
It is necessary to override `native_value` to fall back to the default
attribute-based implementation, instead of the function-based
implementation in `MieleSensor`.
"""
return self._attr_native_value
def _update_native_value(self) -> None:
"""Update the native value attribute of the sensor."""
self._attr_native_value = self.entity_description.value_fn(self.device)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_native_value()
super()._handle_coordinator_update()
class MielePlateSensor(MieleSensor):
"""Representation of a Sensor."""
entity_description: MieleSensorDescription
@property
def native_value(self) -> StateType:
"""Return the state of the plate sensor."""
# state_plate_step is [] if all zones are off
return (
PlatePowerStep(
cast(
int,
self.device.state_plate_step[
cast(int, self.entity_description.zone) - 1
].value_raw,
)
).name
if self.device.state_plate_step
else PlatePowerStep.plate_step_0.name
)
class MieleStatusSensor(MieleSensor):
"""Representation of the status sensor."""
def __init__(
self,
coordinator: MieleDataUpdateCoordinator,
device_id: str,
description: MieleSensorDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, device_id, description)
self._attr_name = None
self._attr_icon = APPLIANCE_ICONS.get(
MieleAppliance(self.device.device_type),
"mdi:state-machine",
)
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return STATE_STATUS_TAGS.get(StateStatus(self.device.state_status))
@property
def available(self) -> bool:
"""Return the availability of the entity."""
# This sensor should always be available
return True
# Some phases have names that are not valid python identifiers, so we need to translate
# them in order to avoid breaking changes
PROGRAM_PHASE_TRANSLATION = {
"second_espresso": "2nd_espresso",
"second_grinding": "2nd_grinding",
"second_pre_brewing": "2nd_pre_brewing",
}
class MielePhaseSensor(MieleSensor):
"""Representation of the program phase sensor."""
@property
def native_value(self) -> StateType:
"""Return the state of the phase sensor."""
program_phase = PROGRAM_PHASE[self.device.device_type](
self.device.state_program_phase
).name
return (
PROGRAM_PHASE_TRANSLATION.get(program_phase, program_phase)
if program_phase is not None
else None
)
@property
def options(self) -> list[str]:
"""Return the options list for the actual device type."""
phases = PROGRAM_PHASE[self.device.device_type].keys()
return sorted([PROGRAM_PHASE_TRANSLATION.get(phase, phase) for phase in phases])
class MieleProgramIdSensor(MieleSensor):
"""Representation of the program id sensor."""
_unrecorded_attributes = frozenset({ATTRIBUTE_PROFILE})
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return (
PROGRAM_IDS[self.device.device_type](self.device.state_program_id).name
if self.device.device_type in PROGRAM_IDS
else None
)
@property
def options(self) -> list[str]:
"""Return the options list for the actual device type."""
return sorted(PROGRAM_IDS.get(self.device.device_type, {}).keys())
class MieleTimeSensor(MieleRestorableSensor):
"""Representation of time sensors keeping state from cache."""
def _update_native_value(self) -> None:
"""Update the last value of the sensor."""
current_value = self.entity_description.value_fn(self.device)
current_status = StateStatus(self.device.state_status)
# report end-specific value when program ends (some devices are immediately reporting 0...)
if (
current_status == StateStatus.PROGRAM_ENDED
and self.entity_description.end_value_fn is not None
):
self._attr_native_value = self.entity_description.end_value_fn(
self._attr_native_value
)
# keep value when program ends if no function is specified
elif current_status == StateStatus.PROGRAM_ENDED:
pass
# force unknown when appliance is not working (some devices are keeping last value until a new cycle starts)
elif current_status in (StateStatus.OFF, StateStatus.ON, StateStatus.IDLE):
self._attr_native_value = None
# otherwise, cache value and return it
else:
self._attr_native_value = current_value
class MieleAbsoluteTimeSensor(MieleRestorableSensor):
"""Representation of absolute time sensors handling precision correctness."""
_previous_value: StateType | datetime = None
def _update_native_value(self) -> None:
"""Update the last value of the sensor."""
current_value = self.entity_description.value_fn(self.device)
current_status = StateStatus(self.device.state_status)
# The API reports with minute precision, to avoid changing
# the value too often, we keep the cached value if it differs
# less than 90s from the new value
if (
isinstance(self._previous_value, datetime)
and isinstance(current_value, datetime)
and (
self._previous_value - timedelta(seconds=90)
< current_value
< self._previous_value + timedelta(seconds=90)
)
) or current_status == StateStatus.PROGRAM_ENDED:
return
# force unknown when appliance is not working (some devices are keeping last value until a new cycle starts)
if current_status in (StateStatus.OFF, StateStatus.ON, StateStatus.IDLE):
self._attr_native_value = None
# otherwise, cache value and return it
else:
self._attr_native_value = current_value
self._previous_value = current_value
class MieleConsumptionSensor(MieleRestorableSensor):
"""Representation of consumption sensors keeping state from cache."""
_is_reporting: bool = False
def _update_native_value(self) -> None:
"""Update the last value of the sensor."""
current_value = self.entity_description.value_fn(self.device)
current_status = StateStatus(self.device.state_status)
# Guard for corrupt restored value
restored_value = (
self._attr_native_value
if isinstance(self._attr_native_value, (int, float))
else 0
)
last_value = (
float(cast(str, restored_value))
if self._attr_native_value is not None
else 0
)
# Force unknown when appliance is not able to report consumption
if current_status in (
StateStatus.ON,
StateStatus.OFF,
StateStatus.PROGRAMMED,
StateStatus.WAITING_TO_START,
StateStatus.IDLE,
StateStatus.SERVICE,
):
self._is_reporting = False
self._attr_native_value = None
# appliance might report the last value for consumption of previous cycle and it will report 0
# only after a while, so it is necessary to force 0 until we see the 0 value coming from API, unless
# we already saw a valid value in this cycle from cache
elif (
current_status in (StateStatus.IN_USE, StateStatus.PAUSE)
and not self._is_reporting
and last_value > 0
):
self._attr_native_value = current_value
self._is_reporting = True
elif (
current_status in (StateStatus.IN_USE, StateStatus.PAUSE)
and not self._is_reporting
and current_value is not None
and cast(int, current_value) > 0
):
self._attr_native_value = 0
# keep value when program ends
elif current_status == StateStatus.PROGRAM_ENDED:
pass
else:
self._attr_native_value = current_value
self._is_reporting = True