From f9575a3b2f3bbff2d14f7d10571891dba448e8d5 Mon Sep 17 00:00:00 2001 From: Andrea Turri Date: Thu, 21 Aug 2025 10:19:13 +0200 Subject: [PATCH] Add "profile" extra attribute to Miele program sensor on coffee machines (#145073) --- homeassistant/components/miele/const.py | 8 + homeassistant/components/miele/sensor.py | 42 ++- homeassistant/components/miele/strings.json | 12 + .../miele/fixtures/coffee_system.json | 126 +++++++ .../miele/snapshots/test_sensor.ambr | 350 ++++++++++++++++++ tests/components/miele/test_sensor.py | 15 + 6 files changed, 550 insertions(+), 3 deletions(-) create mode 100644 tests/components/miele/fixtures/coffee_system.json diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index 3b5b13398a5..fb5e04fbff0 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -850,6 +850,14 @@ COFFEE_SYSTEM_PROGRAM_ID: dict[int, str] = { 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] = { 8: "steam_cooking", 19: "microwave", diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index cc108841aae..982a1198dab 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping from dataclasses import dataclass import logging -from typing import Final, cast +from typing import Any, Final, cast from pymiele import MieleDevice, MieleTemperature @@ -30,6 +30,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType from .const import ( + COFFEE_SYSTEM_PROFILE, DISABLED_TEMP_ENTITIES, DOMAIN, STATE_PROGRAM_ID, @@ -61,6 +62,8 @@ PLATE_COUNT = { "KMX": 6, } +ATTRIBUTE_PROFILE = "profile" + def _get_plate_count(tech_type: str) -> int: """Get number of zones for hob.""" @@ -88,11 +91,21 @@ def _convert_temperature( 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) class MieleSensorDescription(SensorEntityDescription): """Class describing Miele sensor entities.""" value_fn: Callable[[MieleDevice], StateType] + extra_attributes: dict[str, Callable[[MieleDevice], StateType]] | None = None zone: int | None = None unique_id_fn: Callable[[str, MieleSensorDescription], str] | None = None @@ -157,7 +170,6 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( MieleAppliance.OVEN_MICROWAVE, MieleAppliance.STEAM_OVEN, MieleAppliance.MICROWAVE, - MieleAppliance.COFFEE_SYSTEM, MieleAppliance.ROBOT_VACUUM_CLEANER, MieleAppliance.WASHER_DRYER, MieleAppliance.STEAM_OVEN_COMBI, @@ -172,6 +184,18 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( 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, @@ -710,6 +734,16 @@ class MieleSensor(MieleEntity, SensorEntity): """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 MielePlateSensor(MieleSensor): """Representation of a Sensor.""" @@ -792,6 +826,8 @@ class MielePhaseSensor(MieleSensor): 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.""" diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index cb9861e0246..4f0fa48e724 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -991,6 +991,18 @@ "yom_tov": "Yom tov", "yorkshire_pudding": "Yorkshire pudding", "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": { diff --git a/tests/components/miele/fixtures/coffee_system.json b/tests/components/miele/fixtures/coffee_system.json new file mode 100644 index 00000000000..36039e7be7f --- /dev/null +++ b/tests/components/miele/fixtures/coffee_system.json @@ -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 + } + } +} diff --git a/tests/components/miele/snapshots/test_sensor.ambr b/tests/components/miele/snapshots/test_sensor.ambr index 5d941550f41..33ec2013936 100644 --- a/tests/components/miele/snapshots/test_sensor.ambr +++ b/tests/components/miele/snapshots/test_sensor.ambr @@ -1,4 +1,354 @@ # 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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.coffee_system', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + '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': , + 'entity_id': 'sensor.coffee_system', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.coffee_system_program', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + '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': , + 'entity_id': 'sensor.coffee_system_program', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + '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': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + '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': , + 'entity_id': 'sensor.coffee_system_program_phase', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.coffee_system_program_type', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + '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': , + 'entity_id': 'sensor.coffee_system_program_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'own_program', + }) +# --- # name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/miele/test_sensor.py b/tests/components/miele/test_sensor.py index e5051a683c9..0fc7a891509 100644 --- a/tests/components/miele/test_sensor.py +++ b/tests/components/miele/test_sensor.py @@ -271,3 +271,18 @@ async def test_fan_hob_sensor_states( """Test robot fan / hob sensor state.""" 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)