Add "profile" extra attribute to Miele program sensor on coffee machines (#145073)

This commit is contained in:
Andrea Turri
2025-08-21 10:19:13 +02:00
committed by GitHub
parent bbde98bc9f
commit f9575a3b2f
6 changed files with 550 additions and 3 deletions

View File

@@ -850,6 +850,14 @@ COFFEE_SYSTEM_PROGRAM_ID: dict[int, str] = {
24813: "appliance_settings", # modify profile name 24813: "appliance_settings", # modify profile name
} }
COFFEE_SYSTEM_PROFILE: dict[range, str] = {
range(24000, 24032): "profile_1",
range(24032, 24064): "profile_2",
range(24064, 24096): "profile_3",
range(24096, 24128): "profile_4",
range(24128, 24160): "profile_5",
}
STEAM_OVEN_MICRO_PROGRAM_ID: dict[int, str] = { STEAM_OVEN_MICRO_PROGRAM_ID: dict[int, str] = {
8: "steam_cooking", 8: "steam_cooking",
19: "microwave", 19: "microwave",

View File

@@ -2,10 +2,10 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable, Mapping
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
from typing import Final, cast from typing import Any, Final, cast
from pymiele import MieleDevice, MieleTemperature from pymiele import MieleDevice, MieleTemperature
@@ -30,6 +30,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from .const import ( from .const import (
COFFEE_SYSTEM_PROFILE,
DISABLED_TEMP_ENTITIES, DISABLED_TEMP_ENTITIES,
DOMAIN, DOMAIN,
STATE_PROGRAM_ID, STATE_PROGRAM_ID,
@@ -61,6 +62,8 @@ PLATE_COUNT = {
"KMX": 6, "KMX": 6,
} }
ATTRIBUTE_PROFILE = "profile"
def _get_plate_count(tech_type: str) -> int: def _get_plate_count(tech_type: str) -> int:
"""Get number of zones for hob.""" """Get number of zones for hob."""
@@ -88,11 +91,21 @@ def _convert_temperature(
return raw_value 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
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class MieleSensorDescription(SensorEntityDescription): class MieleSensorDescription(SensorEntityDescription):
"""Class describing Miele sensor entities.""" """Class describing Miele sensor entities."""
value_fn: Callable[[MieleDevice], StateType] value_fn: Callable[[MieleDevice], StateType]
extra_attributes: dict[str, Callable[[MieleDevice], StateType]] | None = None
zone: int | None = None zone: int | None = None
unique_id_fn: Callable[[str, MieleSensorDescription], str] | None = None unique_id_fn: Callable[[str, MieleSensorDescription], str] | None = None
@@ -157,7 +170,6 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
MieleAppliance.OVEN_MICROWAVE, MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN, MieleAppliance.STEAM_OVEN,
MieleAppliance.MICROWAVE, MieleAppliance.MICROWAVE,
MieleAppliance.COFFEE_SYSTEM,
MieleAppliance.ROBOT_VACUUM_CLEANER, MieleAppliance.ROBOT_VACUUM_CLEANER,
MieleAppliance.WASHER_DRYER, MieleAppliance.WASHER_DRYER,
MieleAppliance.STEAM_OVEN_COMBI, MieleAppliance.STEAM_OVEN_COMBI,
@@ -172,6 +184,18 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
value_fn=lambda value: value.state_program_id, 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( MieleSensorDefinition(
types=( types=(
MieleAppliance.WASHING_MACHINE, MieleAppliance.WASHING_MACHINE,
@@ -710,6 +734,16 @@ class MieleSensor(MieleEntity, SensorEntity):
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self.entity_description.value_fn(self.device) 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 MielePlateSensor(MieleSensor): class MielePlateSensor(MieleSensor):
"""Representation of a Sensor.""" """Representation of a Sensor."""
@@ -792,6 +826,8 @@ class MielePhaseSensor(MieleSensor):
class MieleProgramIdSensor(MieleSensor): class MieleProgramIdSensor(MieleSensor):
"""Representation of the program id sensor.""" """Representation of the program id sensor."""
_unrecorded_attributes = frozenset({ATTRIBUTE_PROFILE})
@property @property
def native_value(self) -> StateType: def native_value(self) -> StateType:
"""Return the state of the sensor.""" """Return the state of the sensor."""

View File

@@ -991,6 +991,18 @@
"yom_tov": "Yom tov", "yom_tov": "Yom tov",
"yorkshire_pudding": "Yorkshire pudding", "yorkshire_pudding": "Yorkshire pudding",
"zander_fillet": "Zander (fillet)" "zander_fillet": "Zander (fillet)"
},
"state_attributes": {
"profile": {
"name": "Profile",
"state": {
"profile_1": "Profile 1",
"profile_2": "Profile 2",
"profile_3": "Profile 3",
"profile_4": "Profile 4",
"profile_5": "Profile 5"
}
}
} }
}, },
"spin_speed": { "spin_speed": {

View File

@@ -0,0 +1,126 @@
{
"DummyAppliance_CoffeeSystem": {
"ident": {
"type": {
"key_localized": "Device type",
"value_raw": 17,
"value_localized": "Coffee machine"
},
"deviceName": "",
"protocolVersion": 4,
"deviceIdentLabel": {
"fabNumber": "**REDACTED**",
"fabIndex": "11",
"techType": "CM6160",
"matNumber": "11488670",
"swids": []
},
"xkmIdentLabel": {
"techType": "EK037",
"releaseVersion": "04.05"
}
},
"state": {
"ProgramID": {
"value_raw": 24001,
"value_localized": "espresso",
"key_localized": "Program name"
},
"status": {
"value_raw": 5,
"value_localized": "In use",
"key_localized": "status"
},
"programType": {
"value_raw": 1,
"value_localized": "Program",
"key_localized": "Program type"
},
"programPhase": {
"value_raw": 4353,
"value_localized": "Espresso",
"key_localized": "Program phase"
},
"remainingTime": [0, 0],
"startTime": [0, 0],
"targetTemperature": [
{
"value_raw": -32768,
"value_localized": null,
"unit": "Celsius"
},
{
"value_raw": -32768,
"value_localized": null,
"unit": "Celsius"
},
{
"value_raw": -32768,
"value_localized": null,
"unit": "Celsius"
}
],
"coreTargetTemperature": [
{
"value_raw": -32768,
"value_localized": null,
"unit": "Celsius"
}
],
"temperature": [
{
"value_raw": -32768,
"value_localized": null,
"unit": "Celsius"
},
{
"value_raw": -32768,
"value_localized": null,
"unit": "Celsius"
},
{
"value_raw": -32768,
"value_localized": null,
"unit": "Celsius"
}
],
"coreTemperature": [
{
"value_raw": -32768,
"value_localized": null,
"unit": "Celsius"
}
],
"signalInfo": false,
"signalFailure": false,
"signalDoor": false,
"remoteEnable": {
"fullRemoteControl": true,
"smartGrid": false,
"mobileStart": false
},
"ambientLight": null,
"light": 1,
"elapsedTime": [],
"spinningSpeed": {
"unit": "rpm",
"value_raw": null,
"value_localized": null,
"key_localized": "Spin speed"
},
"dryingStep": {
"value_raw": null,
"value_localized": "",
"key_localized": "Drying level"
},
"ventilationStep": {
"value_raw": null,
"value_localized": "",
"key_localized": "Fan level"
},
"plateStep": [],
"ecoFeedback": null,
"batteryLevel": null
}
}
}

View File

@@ -1,4 +1,354 @@
# serializer version: 1 # serializer version: 1
# name: test_coffee_system_sensor_states[platforms0-coffee_system.json][sensor.coffee_system-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'autocleaning',
'failure',
'idle',
'in_use',
'not_connected',
'off',
'on',
'pause',
'program_ended',
'program_interrupted',
'programmed',
'rinse_hold',
'service',
'supercooling',
'supercooling_superfreezing',
'superfreezing',
'superheating',
'waiting_to_start',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.coffee_system',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': 'mdi:coffee-maker',
'original_name': None,
'platform': 'miele',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'status',
'unique_id': 'DummyAppliance_CoffeeSystem-state_status',
'unit_of_measurement': None,
})
# ---
# name: test_coffee_system_sensor_states[platforms0-coffee_system.json][sensor.coffee_system-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Coffee system',
'icon': 'mdi:coffee-maker',
'options': list([
'autocleaning',
'failure',
'idle',
'in_use',
'not_connected',
'off',
'on',
'pause',
'program_ended',
'program_interrupted',
'programmed',
'rinse_hold',
'service',
'supercooling',
'supercooling_superfreezing',
'superfreezing',
'superheating',
'waiting_to_start',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.coffee_system',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'in_use',
})
# ---
# name: test_coffee_system_sensor_states[platforms0-coffee_system.json][sensor.coffee_system_program-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'appliance_rinse',
'appliance_settings',
'barista_assistant',
'black_tea',
'brewing_unit_degrease',
'cafe_au_lait',
'caffe_latte',
'cappuccino',
'cappuccino_italiano',
'check_appliance',
'coffee',
'coffee_pot',
'descaling',
'espresso',
'espresso_macchiato',
'flat_white',
'fruit_tea',
'green_tea',
'herbal_tea',
'hot_milk',
'hot_water',
'japanese_tea',
'latte_macchiato',
'long_coffee',
'milk_foam',
'milk_pipework_clean',
'milk_pipework_rinse',
'no_program',
'ristretto',
'very_hot_water',
'white_tea',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.coffee_system_program',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Program',
'platform': 'miele',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'program_id',
'unique_id': 'DummyAppliance_CoffeeSystem-state_program_id',
'unit_of_measurement': None,
})
# ---
# name: test_coffee_system_sensor_states[platforms0-coffee_system.json][sensor.coffee_system_program-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Coffee system Program',
'options': list([
'appliance_rinse',
'appliance_settings',
'barista_assistant',
'black_tea',
'brewing_unit_degrease',
'cafe_au_lait',
'caffe_latte',
'cappuccino',
'cappuccino_italiano',
'check_appliance',
'coffee',
'coffee_pot',
'descaling',
'espresso',
'espresso_macchiato',
'flat_white',
'fruit_tea',
'green_tea',
'herbal_tea',
'hot_milk',
'hot_water',
'japanese_tea',
'latte_macchiato',
'long_coffee',
'milk_foam',
'milk_pipework_clean',
'milk_pipework_rinse',
'no_program',
'ristretto',
'very_hot_water',
'white_tea',
]),
'profile': 'profile_1',
}),
'context': <ANY>,
'entity_id': 'sensor.coffee_system_program',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'espresso',
})
# ---
# name: test_coffee_system_sensor_states[platforms0-coffee_system.json][sensor.coffee_system_program_phase-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'2nd_espresso',
'2nd_grinding',
'2nd_pre_brewing',
'dispensing',
'espresso',
'grinding',
'heating_up',
'hot_milk',
'milk_foam',
'not_running',
'pre_brewing',
'rinse',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.coffee_system_program_phase',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Program phase',
'platform': 'miele',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'program_phase',
'unique_id': 'DummyAppliance_CoffeeSystem-state_program_phase',
'unit_of_measurement': None,
})
# ---
# name: test_coffee_system_sensor_states[platforms0-coffee_system.json][sensor.coffee_system_program_phase-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Coffee system Program phase',
'options': list([
'2nd_espresso',
'2nd_grinding',
'2nd_pre_brewing',
'dispensing',
'espresso',
'grinding',
'heating_up',
'hot_milk',
'milk_foam',
'not_running',
'pre_brewing',
'rinse',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.coffee_system_program_phase',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'espresso',
})
# ---
# name: test_coffee_system_sensor_states[platforms0-coffee_system.json][sensor.coffee_system_program_type-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'automatic_program',
'cleaning_care_program',
'maintenance_program',
'normal_operation_mode',
'own_program',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.coffee_system_program_type',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Program type',
'platform': 'miele',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'program_type',
'unique_id': 'DummyAppliance_CoffeeSystem-state_program_type',
'unit_of_measurement': None,
})
# ---
# name: test_coffee_system_sensor_states[platforms0-coffee_system.json][sensor.coffee_system_program_type-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Coffee system Program type',
'options': list([
'automatic_program',
'cleaning_care_program',
'maintenance_program',
'normal_operation_mode',
'own_program',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.coffee_system_program_type',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'own_program',
})
# ---
# name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction-entry] # name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

@@ -271,3 +271,18 @@ async def test_fan_hob_sensor_states(
"""Test robot fan / hob sensor state.""" """Test robot fan / hob sensor state."""
await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id)
@pytest.mark.parametrize("load_device_file", ["coffee_system.json"])
@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)])
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_coffee_system_sensor_states(
hass: HomeAssistant,
mock_miele_client: MagicMock,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
setup_platform: None,
) -> None:
"""Test coffee system sensor state."""
await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id)