-
-
Notifications
You must be signed in to change notification settings - Fork 34.4k
Discovery of Miele temperature sensors #144585
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8357d2e
b367f14
3adacb9
c70c651
59d43f9
0003fad
27ce811
716c7cc
01b2d0a
5b7a8aa
554256c
b9b8971
2350f85
a580dac
11adcdb
9e218ee
607a930
fbce94a
5957633
ad47cbb
bf302de
b14de8a
3928355
4b4eff6
1e64035
9cb2ce0
17a2cdc
557a8d6
fa8b61e
c22d22c
16a1154
17b3424
82a1ead
785f906
f2c1286
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,7 @@ | |
import logging | ||
from typing import Final, cast | ||
|
||
from pymiele import MieleDevice | ||
from pymiele import MieleDevice, MieleTemperature | ||
|
||
from homeassistant.components.sensor import ( | ||
SensorDeviceClass, | ||
|
@@ -25,10 +25,13 @@ | |
UnitOfVolume, | ||
) | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers import entity_registry as er | ||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||
from homeassistant.helpers.typing import StateType | ||
|
||
from .const import ( | ||
DISABLED_TEMP_ENTITIES, | ||
DOMAIN, | ||
STATE_PROGRAM_ID, | ||
STATE_PROGRAM_PHASE, | ||
STATE_STATUS_TAGS, | ||
|
@@ -45,8 +48,6 @@ | |
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
DISABLED_TEMPERATURE = -32768 | ||
|
||
DEFAULT_PLATE_COUNT = 4 | ||
|
||
PLATE_COUNT = { | ||
|
@@ -75,12 +76,25 @@ | |
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 | ||
|
||
|
||
@dataclass(frozen=True, kw_only=True) | ||
class MieleSensorDescription(SensorEntityDescription): | ||
"""Class describing Miele sensor entities.""" | ||
|
||
value_fn: Callable[[MieleDevice], StateType] | ||
zone: int = 1 | ||
zone: int | None = None | ||
unique_id_fn: Callable[[str, MieleSensorDescription], str] | None = None | ||
|
||
|
||
@dataclass | ||
|
@@ -404,32 +418,20 @@ | |
), | ||
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: cast(int, value.state_temperatures[0].temperature) | ||
/ 100.0, | ||
value_fn=lambda value: _convert_temperature(value.state_temperatures, 0), | ||
), | ||
), | ||
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, | ||
farmio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
), | ||
description=MieleSensorDescription( | ||
key="state_temperature_2", | ||
|
@@ -438,7 +440,24 @@ | |
translation_key="temperature_zone_2", | ||
native_unit_of_measurement=UnitOfTemperature.CELSIUS, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
value_fn=lambda value: value.state_temperatures[1].temperature / 100.0, # type: ignore [operator] | ||
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( | ||
|
@@ -454,11 +473,8 @@ | |
device_class=SensorDeviceClass.TEMPERATURE, | ||
native_unit_of_measurement=UnitOfTemperature.CELSIUS, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
value_fn=( | ||
lambda value: cast( | ||
int, value.state_core_target_temperature[0].temperature | ||
) | ||
/ 100.0 | ||
value_fn=lambda value: _convert_temperature( | ||
value.state_core_target_temperature, 0 | ||
), | ||
), | ||
), | ||
|
@@ -479,9 +495,8 @@ | |
device_class=SensorDeviceClass.TEMPERATURE, | ||
native_unit_of_measurement=UnitOfTemperature.CELSIUS, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
value_fn=( | ||
lambda value: cast(int, value.state_target_temperature[0].temperature) | ||
/ 100.0 | ||
value_fn=lambda value: _convert_temperature( | ||
value.state_target_temperature, 0 | ||
), | ||
), | ||
), | ||
|
@@ -497,9 +512,8 @@ | |
device_class=SensorDeviceClass.TEMPERATURE, | ||
native_unit_of_measurement=UnitOfTemperature.CELSIUS, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
value_fn=( | ||
lambda value: cast(int, value.state_core_temperature[0].temperature) | ||
/ 100.0 | ||
value_fn=lambda value: _convert_temperature( | ||
value.state_core_temperature, 0 | ||
), | ||
), | ||
), | ||
|
@@ -518,6 +532,8 @@ | |
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) | ||
|
@@ -559,51 +575,88 @@ | |
) -> None: | ||
"""Set up the sensor platform.""" | ||
coordinator = config_entry.runtime_data | ||
added_devices: set[str] = set() | ||
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, | ||
}.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_new_devices() -> None: | ||
nonlocal added_devices | ||
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: | ||
if ( | ||
device_id in new_devices_set | ||
and device.device_type in definition.types | ||
): | ||
match definition.description.key: | ||
case "state_status": | ||
entity_class = MieleStatusSensor | ||
case "state_program_id": | ||
entity_class = MieleProgramIdSensor | ||
case "state_program_phase": | ||
entity_class = MielePhaseSensor | ||
case "state_plate_step": | ||
entity_class = MielePlateSensor | ||
case _: | ||
entity_class = MieleSensor | ||
if ( | ||
definition.description.device_class | ||
== SensorDeviceClass.TEMPERATURE | ||
and definition.description.value_fn(device) | ||
== DISABLED_TEMPERATURE / 100 | ||
) or ( | ||
definition.description.key == "state_plate_step" | ||
and definition.description.zone | ||
> _get_plate_count(device.tech_type) | ||
): | ||
# Don't create entity if API signals that datapoint is disabled | ||
continue | ||
entities.append( | ||
entity_class(coordinator, device_id, definition.description) | ||
# 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 | ||
Comment on lines
+622
to
+638
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay so one thing is, we should only check this on creation, not every time we get an update, do we keep track of all data we already kept track off? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
But in this way, I would recognize new temperature sensors only at creation. We need that the temperature sensor is created when it provides a valid value for the first time because of API limitations.
Sorry, I don't get the point here :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But you mentioned you don't know what values are available at boot right? But do we make sure we don't check every datapoint at every update? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the API is always reporting something, valid or not valid value. Due to how the Miele API works, temperature sensors are not exposed at initial device discovery and only start reporting values later. So we need to re-evaluate their presence when the data becomes available. The first time a temperature becomes valid, we need to create the sensor and keep it afterwards, reporting "unknown" as the API gets back to the invalid state (appliance is off or idle for example). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm adding some details to be more clear. The check for
To avoid checking all datapoints every time, we track both:
This ensures we don’t process already-added sensors again, and we skip entirely the devices already handled. Open to suggestions on improving performance :) What about an explicit check for |
||
) | ||
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_new_devices)) | ||
_async_add_new_devices() | ||
config_entry.async_on_unload(coordinator.async_add_listener(_async_add_devices)) | ||
_async_add_devices() | ||
|
||
|
||
APPLIANCE_ICONS = { | ||
|
@@ -641,6 +694,17 @@ | |
|
||
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: | ||
"""Return the state of the sensor.""" | ||
|
@@ -652,16 +716,6 @@ | |
|
||
entity_description: MieleSensorDescription | ||
|
||
def __init__( | ||
self, | ||
coordinator: MieleDataUpdateCoordinator, | ||
device_id: str, | ||
description: MieleSensorDescription, | ||
) -> None: | ||
"""Initialize the plate sensor.""" | ||
super().__init__(coordinator, device_id, description) | ||
self._attr_unique_id = f"{device_id}-{description.key}-{description.zone}" | ||
|
||
@property | ||
def native_value(self) -> StateType: | ||
"""Return the state of the plate sensor.""" | ||
|
@@ -672,7 +726,7 @@ | |
cast( | ||
int, | ||
self.device.state_plate_step[ | ||
self.entity_description.zone - 1 | ||
cast(int, self.entity_description.zone) - 1 | ||
].value_raw, | ||
) | ||
).name | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to move the entity_class assignment to the description instead of doing it by logic in
_async_add_new_devices()
?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes a lot of sense, but it requires moving around items inside
sensor.py
and it messes up the PR. I already prepared it on a separate branch and there will be a follow-up PR (already drafted: #145578)