Skip to content

Commit 7d06aec

Browse files
aturrijoostlek
andauthored
Discovery of Miele temperature sensors (home-assistant#144585)
Co-authored-by: Joost Lekkerkerker <[email protected]>
1 parent ee4325a commit 7d06aec

File tree

14 files changed

+4479
-544
lines changed

14 files changed

+4479
-544
lines changed

homeassistant/components/miele/entity.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ class MieleEntity(CoordinatorEntity[MieleDataUpdateCoordinator]):
1616

1717
_attr_has_entity_name = True
1818

19+
@staticmethod
20+
def get_unique_id(device_id: str, description: EntityDescription) -> str:
21+
"""Generate a unique ID for the entity."""
22+
return f"{device_id}-{description.key}"
23+
1924
def __init__(
2025
self,
2126
coordinator: MieleDataUpdateCoordinator,
@@ -26,7 +31,7 @@ def __init__(
2631
super().__init__(coordinator)
2732
self._device_id = device_id
2833
self.entity_description = description
29-
self._attr_unique_id = f"{device_id}-{description.key}"
34+
self._attr_unique_id = MieleEntity.get_unique_id(device_id, description)
3035

3136
device = self.device
3237
appliance_type = DEVICE_TYPE_TAGS.get(MieleAppliance(device.device_type))

homeassistant/components/miele/sensor.py

Lines changed: 129 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import logging
88
from typing import Final, cast
99

10-
from pymiele import MieleDevice
10+
from pymiele import MieleDevice, MieleTemperature
1111

1212
from homeassistant.components.sensor import (
1313
SensorDeviceClass,
@@ -25,10 +25,13 @@
2525
UnitOfVolume,
2626
)
2727
from homeassistant.core import HomeAssistant
28+
from homeassistant.helpers import entity_registry as er
2829
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
2930
from homeassistant.helpers.typing import StateType
3031

3132
from .const import (
33+
DISABLED_TEMP_ENTITIES,
34+
DOMAIN,
3235
STATE_PROGRAM_ID,
3336
STATE_PROGRAM_PHASE,
3437
STATE_STATUS_TAGS,
@@ -45,8 +48,6 @@
4548

4649
_LOGGER = logging.getLogger(__name__)
4750

48-
DISABLED_TEMPERATURE = -32768
49-
5051
DEFAULT_PLATE_COUNT = 4
5152

5253
PLATE_COUNT = {
@@ -75,12 +76,25 @@ def _convert_duration(value_list: list[int]) -> int | None:
7576
return value_list[0] * 60 + value_list[1] if value_list else None
7677

7778

79+
def _convert_temperature(
80+
value_list: list[MieleTemperature], index: int
81+
) -> float | None:
82+
"""Convert temperature object to readable value."""
83+
if index >= len(value_list):
84+
return None
85+
raw_value = cast(int, value_list[index].temperature) / 100.0
86+
if raw_value in DISABLED_TEMP_ENTITIES:
87+
return None
88+
return raw_value
89+
90+
7891
@dataclass(frozen=True, kw_only=True)
7992
class MieleSensorDescription(SensorEntityDescription):
8093
"""Class describing Miele sensor entities."""
8194

8295
value_fn: Callable[[MieleDevice], StateType]
83-
zone: int = 1
96+
zone: int | None = None
97+
unique_id_fn: Callable[[str, MieleSensorDescription], str] | None = None
8498

8599

86100
@dataclass
@@ -404,32 +418,20 @@ class MieleSensorDefinition:
404418
),
405419
description=MieleSensorDescription(
406420
key="state_temperature_1",
421+
zone=1,
407422
device_class=SensorDeviceClass.TEMPERATURE,
408423
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
409424
state_class=SensorStateClass.MEASUREMENT,
410-
value_fn=lambda value: cast(int, value.state_temperatures[0].temperature)
411-
/ 100.0,
425+
value_fn=lambda value: _convert_temperature(value.state_temperatures, 0),
412426
),
413427
),
414428
MieleSensorDefinition(
415429
types=(
416-
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
417-
MieleAppliance.OVEN,
418-
MieleAppliance.OVEN_MICROWAVE,
419-
MieleAppliance.DISH_WARMER,
420-
MieleAppliance.STEAM_OVEN,
421-
MieleAppliance.MICROWAVE,
422-
MieleAppliance.FRIDGE,
423-
MieleAppliance.FREEZER,
424430
MieleAppliance.FRIDGE_FREEZER,
425-
MieleAppliance.STEAM_OVEN_COMBI,
426431
MieleAppliance.WINE_CABINET,
427432
MieleAppliance.WINE_CONDITIONING_UNIT,
428433
MieleAppliance.WINE_STORAGE_CONDITIONING_UNIT,
429-
MieleAppliance.STEAM_OVEN_MICRO,
430-
MieleAppliance.DIALOG_OVEN,
431434
MieleAppliance.WINE_CABINET_FREEZER,
432-
MieleAppliance.STEAM_OVEN_MK2,
433435
),
434436
description=MieleSensorDescription(
435437
key="state_temperature_2",
@@ -438,7 +440,24 @@ class MieleSensorDefinition:
438440
translation_key="temperature_zone_2",
439441
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
440442
state_class=SensorStateClass.MEASUREMENT,
441-
value_fn=lambda value: value.state_temperatures[1].temperature / 100.0, # type: ignore [operator]
443+
value_fn=lambda value: _convert_temperature(value.state_temperatures, 1),
444+
),
445+
),
446+
MieleSensorDefinition(
447+
types=(
448+
MieleAppliance.WINE_CABINET,
449+
MieleAppliance.WINE_CONDITIONING_UNIT,
450+
MieleAppliance.WINE_STORAGE_CONDITIONING_UNIT,
451+
MieleAppliance.WINE_CABINET_FREEZER,
452+
),
453+
description=MieleSensorDescription(
454+
key="state_temperature_3",
455+
zone=3,
456+
device_class=SensorDeviceClass.TEMPERATURE,
457+
translation_key="temperature_zone_3",
458+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
459+
state_class=SensorStateClass.MEASUREMENT,
460+
value_fn=lambda value: _convert_temperature(value.state_temperatures, 2),
442461
),
443462
),
444463
MieleSensorDefinition(
@@ -454,11 +473,8 @@ class MieleSensorDefinition:
454473
device_class=SensorDeviceClass.TEMPERATURE,
455474
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
456475
state_class=SensorStateClass.MEASUREMENT,
457-
value_fn=(
458-
lambda value: cast(
459-
int, value.state_core_target_temperature[0].temperature
460-
)
461-
/ 100.0
476+
value_fn=lambda value: _convert_temperature(
477+
value.state_core_target_temperature, 0
462478
),
463479
),
464480
),
@@ -479,9 +495,8 @@ class MieleSensorDefinition:
479495
device_class=SensorDeviceClass.TEMPERATURE,
480496
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
481497
state_class=SensorStateClass.MEASUREMENT,
482-
value_fn=(
483-
lambda value: cast(int, value.state_target_temperature[0].temperature)
484-
/ 100.0
498+
value_fn=lambda value: _convert_temperature(
499+
value.state_target_temperature, 0
485500
),
486501
),
487502
),
@@ -497,9 +512,8 @@ class MieleSensorDefinition:
497512
device_class=SensorDeviceClass.TEMPERATURE,
498513
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
499514
state_class=SensorStateClass.MEASUREMENT,
500-
value_fn=(
501-
lambda value: cast(int, value.state_core_temperature[0].temperature)
502-
/ 100.0
515+
value_fn=lambda value: _convert_temperature(
516+
value.state_core_temperature, 0
503517
),
504518
),
505519
),
@@ -518,6 +532,8 @@ class MieleSensorDefinition:
518532
device_class=SensorDeviceClass.ENUM,
519533
options=sorted(PlatePowerStep.keys()),
520534
value_fn=lambda value: None,
535+
unique_id_fn=lambda device_id,
536+
description: f"{device_id}-{description.key}-{description.zone}",
521537
),
522538
)
523539
for i in range(1, 7)
@@ -559,51 +575,88 @@ async def async_setup_entry(
559575
) -> None:
560576
"""Set up the sensor platform."""
561577
coordinator = config_entry.runtime_data
562-
added_devices: set[str] = set()
578+
added_devices: set[str] = set() # device_id
579+
added_entities: set[str] = set() # unique_id
580+
581+
def _get_entity_class(definition: MieleSensorDefinition) -> type[MieleSensor]:
582+
"""Get the entity class for the sensor."""
583+
return {
584+
"state_status": MieleStatusSensor,
585+
"state_program_id": MieleProgramIdSensor,
586+
"state_program_phase": MielePhaseSensor,
587+
"state_plate_step": MielePlateSensor,
588+
}.get(definition.description.key, MieleSensor)
589+
590+
def _is_entity_registered(unique_id: str) -> bool:
591+
"""Check if the entity is already registered."""
592+
entity_registry = er.async_get(hass)
593+
return any(
594+
entry.platform == DOMAIN and entry.unique_id == unique_id
595+
for entry in entity_registry.entities.values()
596+
)
597+
598+
def _is_sensor_enabled(
599+
definition: MieleSensorDefinition,
600+
device: MieleDevice,
601+
unique_id: str,
602+
) -> bool:
603+
"""Check if the sensor is enabled."""
604+
if (
605+
definition.description.device_class == SensorDeviceClass.TEMPERATURE
606+
and definition.description.value_fn(device) is None
607+
and definition.description.zone != 1
608+
):
609+
# all appliances supporting temperature have at least zone 1, for other zones
610+
# don't create entity if API signals that datapoint is disabled, unless the sensor
611+
# already appeared in the past (= it provided a valid value)
612+
return _is_entity_registered(unique_id)
613+
if (
614+
definition.description.key == "state_plate_step"
615+
and definition.description.zone is not None
616+
and definition.description.zone > _get_plate_count(device.tech_type)
617+
):
618+
# don't create plate entity if not expected by the appliance tech type
619+
return False
620+
return True
563621

564-
def _async_add_new_devices() -> None:
565-
nonlocal added_devices
622+
def _async_add_devices() -> None:
623+
nonlocal added_devices, added_entities
566624
entities: list = []
567625
entity_class: type[MieleSensor]
568626
new_devices_set, current_devices = coordinator.async_add_devices(added_devices)
569627
added_devices = current_devices
570628

571629
for device_id, device in coordinator.data.devices.items():
572630
for definition in SENSOR_TYPES:
573-
if (
574-
device_id in new_devices_set
575-
and device.device_type in definition.types
576-
):
577-
match definition.description.key:
578-
case "state_status":
579-
entity_class = MieleStatusSensor
580-
case "state_program_id":
581-
entity_class = MieleProgramIdSensor
582-
case "state_program_phase":
583-
entity_class = MielePhaseSensor
584-
case "state_plate_step":
585-
entity_class = MielePlateSensor
586-
case _:
587-
entity_class = MieleSensor
588-
if (
589-
definition.description.device_class
590-
== SensorDeviceClass.TEMPERATURE
591-
and definition.description.value_fn(device)
592-
== DISABLED_TEMPERATURE / 100
593-
) or (
594-
definition.description.key == "state_plate_step"
595-
and definition.description.zone
596-
> _get_plate_count(device.tech_type)
597-
):
598-
# Don't create entity if API signals that datapoint is disabled
599-
continue
600-
entities.append(
601-
entity_class(coordinator, device_id, definition.description)
631+
# device is not supported, skip
632+
if device.device_type not in definition.types:
633+
continue
634+
635+
entity_class = _get_entity_class(definition)
636+
unique_id = (
637+
definition.description.unique_id_fn(
638+
device_id, definition.description
602639
)
640+
if definition.description.unique_id_fn is not None
641+
else MieleEntity.get_unique_id(device_id, definition.description)
642+
)
643+
644+
# entity was already added, skip
645+
if device_id not in new_devices_set and unique_id in added_entities:
646+
continue
647+
648+
# sensors is not enabled, skip
649+
if not _is_sensor_enabled(definition, device, unique_id):
650+
continue
651+
652+
added_entities.add(unique_id)
653+
entities.append(
654+
entity_class(coordinator, device_id, definition.description)
655+
)
603656
async_add_entities(entities)
604657

605-
config_entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
606-
_async_add_new_devices()
658+
config_entry.async_on_unload(coordinator.async_add_listener(_async_add_devices))
659+
_async_add_devices()
607660

608661

609662
APPLIANCE_ICONS = {
@@ -641,6 +694,17 @@ class MieleSensor(MieleEntity, SensorEntity):
641694

642695
entity_description: MieleSensorDescription
643696

697+
def __init__(
698+
self,
699+
coordinator: MieleDataUpdateCoordinator,
700+
device_id: str,
701+
description: MieleSensorDescription,
702+
) -> None:
703+
"""Initialize the sensor."""
704+
super().__init__(coordinator, device_id, description)
705+
if description.unique_id_fn is not None:
706+
self._attr_unique_id = description.unique_id_fn(device_id, description)
707+
644708
@property
645709
def native_value(self) -> StateType:
646710
"""Return the state of the sensor."""
@@ -652,16 +716,6 @@ class MielePlateSensor(MieleSensor):
652716

653717
entity_description: MieleSensorDescription
654718

655-
def __init__(
656-
self,
657-
coordinator: MieleDataUpdateCoordinator,
658-
device_id: str,
659-
description: MieleSensorDescription,
660-
) -> None:
661-
"""Initialize the plate sensor."""
662-
super().__init__(coordinator, device_id, description)
663-
self._attr_unique_id = f"{device_id}-{description.key}-{description.zone}"
664-
665719
@property
666720
def native_value(self) -> StateType:
667721
"""Return the state of the plate sensor."""
@@ -672,7 +726,7 @@ def native_value(self) -> StateType:
672726
cast(
673727
int,
674728
self.device.state_plate_step[
675-
self.entity_description.zone - 1
729+
cast(int, self.entity_description.zone) - 1
676730
].value_raw,
677731
)
678732
).name

tests/components/miele/fixtures/4_actions.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,20 @@
8282
"colors": [],
8383
"modes": [],
8484
"runOnTime": []
85+
},
86+
"DummyAppliance_12": {
87+
"processAction": [],
88+
"light": [2],
89+
"ambientLight": [],
90+
"startTime": [],
91+
"ventilationStep": [],
92+
"programId": [],
93+
"targetTemperature": [],
94+
"deviceName": true,
95+
"powerOn": false,
96+
"powerOff": true,
97+
"colors": [],
98+
"modes": [],
99+
"runOnTime": []
85100
}
86101
}

0 commit comments

Comments
 (0)