diff --git a/homeassistant/components/matter/icons.json b/homeassistant/components/matter/icons.json index bd8665eb18b..4f3e532d877 100644 --- a/homeassistant/components/matter/icons.json +++ b/homeassistant/components/matter/icons.json @@ -45,6 +45,9 @@ "contamination_state": { "default": "mdi:air-filter" }, + "current_phase": { + "default": "mdi:state-machine" + }, "air_quality": { "default": "mdi:air-filter" }, diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index 39e11a683f5..40b25d14c46 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -7,6 +7,7 @@ from datetime import datetime from typing import TYPE_CHECKING, cast from chip.clusters import Objects as clusters +from chip.clusters.ClusterObjects import ClusterAttributeDescriptor from chip.clusters.Types import Nullable, NullValue from matter_server.client.models import device_types from matter_server.common.custom_clusters import ( @@ -89,6 +90,14 @@ class MatterSensorEntityDescription(SensorEntityDescription, MatterEntityDescrip """Describe Matter sensor entities.""" +@dataclass(frozen=True, kw_only=True) +class MatterListSensorEntityDescription(MatterSensorEntityDescription): + """Describe Matter sensor entities from MatterListSensor.""" + + # list attribute: the attribute descriptor to get the list of values (= list of strings) + list_attribute: type[ClusterAttributeDescriptor] + + class MatterSensor(MatterEntity, SensorEntity): """Representation of a Matter sensor.""" @@ -171,6 +180,28 @@ class MatterOperationalStateSensor(MatterSensor): ) +class MatterListSensor(MatterSensor): + """Representation of a sensor entity from Matter list from Cluster attribute(s).""" + + entity_description: MatterListSensorEntityDescription + _attr_device_class = SensorDeviceClass.ENUM + + @callback + def _update_from_device(self) -> None: + """Update from device.""" + self._attr_options = list_values = cast( + list[str], + self.get_matter_attribute_value(self.entity_description.list_attribute), + ) + current_value: int = self.get_matter_attribute_value( + self._entity_info.primary_attribute + ) + try: + self._attr_native_value = list_values[current_value] + except IndexError: + self._attr_native_value = None + + # Discovery schema(s) to map Matter Attributes to HA entities DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( @@ -762,6 +793,19 @@ DISCOVERY_SCHEMAS = [ clusters.OperationalState.Attributes.OperationalStateList, ), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterListSensorEntityDescription( + key="OperationalStateCurrentPhase", + translation_key="current_phase", + list_attribute=clusters.OperationalState.Attributes.PhaseList, + ), + entity_class=MatterListSensor, + required_attributes=( + clusters.OperationalState.Attributes.CurrentPhase, + clusters.OperationalState.Attributes.PhaseList, + ), + ), MatterDiscoverySchema( platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index 4054adba530..8bac67a4ca7 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -257,6 +257,9 @@ }, "battery_replacement_description": { "name": "Battery type" + }, + "current_phase": { + "name": "Current phase" } }, "switch": { diff --git a/tests/components/matter/fixtures/nodes/silabs_laundrywasher.json b/tests/components/matter/fixtures/nodes/silabs_laundrywasher.json index 4d26dfb03aa..a91584d7212 100644 --- a/tests/components/matter/fixtures/nodes/silabs_laundrywasher.json +++ b/tests/components/matter/fixtures/nodes/silabs_laundrywasher.json @@ -673,8 +673,8 @@ "1/86/65528": [], "1/86/65529": [0], "1/86/65531": [4, 5, 65528, 65529, 65531, 65532, 65533], - "1/96/0": null, - "1/96/1": null, + "1/96/0": ["pre-soak", "rinse", "spin"], + "1/96/1": 0, "1/96/3": [ { "0": 0 diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 0215abf47c6..d9bc0bdf1fc 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -2870,6 +2870,64 @@ 'state': '0.0', }) # --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_current_phase-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'pre-soak', + 'rinse', + 'spin', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.laundrywasher_current_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': 'Current phase', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_phase', + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-OperationalStateCurrentPhase-96-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_current_phase-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'LaundryWasher Current phase', + 'options': list([ + 'pre-soak', + 'rinse', + 'spin', + ]), + }), + 'context': , + 'entity_id': 'sensor.laundrywasher_current_phase', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'pre-soak', + }) +# --- # name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/test_sensor.py b/tests/components/matter/test_sensor.py index 8a5fbf48a49..251aab73e3b 100644 --- a/tests/components/matter/test_sensor.py +++ b/tests/components/matter/test_sensor.py @@ -332,7 +332,7 @@ async def test_operational_state_sensor( matter_client: MagicMock, matter_node: MatterNode, ) -> None: - """Test dishwasher sensor.""" + """Test Operational State sensor, using a dishwasher fixture.""" # OperationalState Cluster / OperationalState attribute (1/96/4) state = hass.states.get("sensor.dishwasher_operational_state") assert state @@ -379,3 +379,23 @@ async def test_draft_electrical_measurement_sensor( state = hass.states.get("sensor.yndx_00540_power") assert state assert state.state == "unknown" + + +@pytest.mark.parametrize("node_fixture", ["silabs_laundrywasher"]) +async def test_list_sensor( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test Matter List sensor.""" + # OperationalState Cluster / CurrentPhase attribute (1/96/1) + state = hass.states.get("sensor.laundrywasher_current_phase") + assert state + assert state.state == "pre-soak" + + set_node_attribute(matter_node, 1, 96, 1, 1) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.laundrywasher_current_phase") + assert state + assert state.state == "rinse"