Add support for OperationalState Attribute from Matter OperationalState cluster (#125627)

This commit is contained in:
Ludovic BOUÉ 2024-09-24 21:30:30 +02:00 committed by GitHub
parent 86f8901c96
commit b370893e58
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 766 additions and 2 deletions

View file

@ -35,6 +35,9 @@
"activated_carbon_filter_condition": {
"default": "mdi:filter-check"
},
"operational_state": {
"default": "mdi:play-pause"
},
"valve_position": {
"default": "mdi:valve"
}

View file

@ -4,6 +4,7 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from typing import TYPE_CHECKING, cast
from chip.clusters import Objects as clusters
from chip.clusters.Types import Nullable, NullValue
@ -37,6 +38,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
from .entity import MatterEntity, MatterEntityDescription
from .helpers import get_matter
@ -61,6 +63,15 @@ CONTAMINATION_STATE_MAP = {
}
OPERATIONAL_STATE_MAP = {
# enum with known Operation state values which we can translate
clusters.OperationalState.Enums.OperationalStateEnum.kStopped: "stopped",
clusters.OperationalState.Enums.OperationalStateEnum.kRunning: "running",
clusters.OperationalState.Enums.OperationalStateEnum.kPaused: "paused",
clusters.OperationalState.Enums.OperationalStateEnum.kError: "error",
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
@ -93,6 +104,42 @@ class MatterSensor(MatterEntity, SensorEntity):
self._attr_native_value = value
class MatterOperationalStateSensor(MatterSensor):
"""Representation of a sensor for Matter Operational State."""
states_map: dict[int, str]
@callback
def _update_from_device(self) -> None:
"""Update from device."""
# the operational state list is a list of the possible operational states
# this is a dynamic list and is condition, device and manufacturer specific
# therefore it is not possible to provide a fixed list of options
# or to provide a mapping to a translateable string for all options
operational_state_list = self.get_matter_attribute_value(
clusters.OperationalState.Attributes.OperationalStateList
)
if TYPE_CHECKING:
operational_state_list = cast(
list[clusters.OperationalState.Structs.OperationalStateStruct],
operational_state_list,
)
states_map: dict[int, str] = {}
for state in operational_state_list:
# prefer translateable (known) state from mapping,
# fallback to the raw state label as given by the device/manufacturer
states_map[state.operationalStateID] = OPERATIONAL_STATE_MAP.get(
state.operationalStateID, slugify(state.operationalStateLabel)
)
self.states_map = states_map
self._attr_options = list(states_map.values())
self._attr_native_value = states_map.get(
self.get_matter_attribute_value(
clusters.OperationalState.Attributes.OperationalState
)
)
# Discovery schema(s) to map Matter Attributes to HA entities
DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
@ -582,8 +629,7 @@ DISCOVERY_SCHEMAS = [
key="SmokeCOAlarmContaminationState",
translation_key="contamination_state",
device_class=SensorDeviceClass.ENUM,
# convert to set first to remove the duplicate unknown value
options=list(set(CONTAMINATION_STATE_MAP.values())),
options=list(CONTAMINATION_STATE_MAP.values()),
measurement_to_ha=CONTAMINATION_STATE_MAP.get,
),
entity_class=MatterSensor,
@ -601,4 +647,17 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterSensor,
required_attributes=(clusters.SmokeCoAlarm.Attributes.ExpiryDate,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="OperationalState",
device_class=SensorDeviceClass.ENUM,
translation_key="operational_state",
),
entity_class=MatterOperationalStateSensor,
required_attributes=(
clusters.OperationalState.Attributes.OperationalState,
clusters.OperationalState.Attributes.OperationalStateList,
),
),
]

View file

@ -210,6 +210,15 @@
"hepa_filter_condition": {
"name": "Hepa filter condition"
},
"operational_state": {
"name": "Operational state",
"state": {
"stopped": "Stopped",
"running": "Running",
"paused": "[%key:common::state::paused%]",
"error": "Error"
}
},
"switch_current_position": {
"name": "Current switch position"
},

View file

@ -0,0 +1,657 @@
{
"node_id": 54,
"date_commissioned": "2024-08-15T07:14:29.055273",
"last_interview": "2024-08-15T11:36:27.830863",
"interview_version": 6,
"available": true,
"is_bridge": false,
"attributes": {
"0/29/0": [
{
"0": 22,
"1": 1
}
],
"0/29/1": [
29, 31, 40, 42, 43, 44, 45, 48, 49, 50, 51, 52, 53, 60, 62, 63, 64, 65
],
"0/29/2": [41],
"0/29/3": [1, 2],
"0/29/65532": 0,
"0/29/65533": 2,
"0/29/65528": [],
"0/29/65529": [],
"0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"0/31/0": [
{
"254": 1
},
{
"254": 1
},
{
"254": 2
},
{
"1": 5,
"2": 2,
"3": [112233],
"4": null,
"254": 3
}
],
"0/31/1": [],
"0/31/2": 4,
"0/31/3": 3,
"0/31/4": 4,
"0/31/65532": 0,
"0/31/65533": 1,
"0/31/65528": [],
"0/31/65529": [],
"0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
"0/40/0": 17,
"0/40/1": "Silabs",
"0/40/2": 65521,
"0/40/3": "Dishwasher",
"0/40/4": 32773,
"0/40/5": "",
"0/40/6": "**REDACTED**",
"0/40/7": 1,
"0/40/8": "TEST_VERSION",
"0/40/9": 1,
"0/40/10": "1",
"0/40/11": "20200101",
"0/40/12": "Dishwasher",
"0/40/13": "Dishwasher",
"0/40/14": "",
"0/40/15": "",
"0/40/16": false,
"0/40/18": "**REDACTED**",
"0/40/19": {
"0": 3,
"1": 3
},
"0/40/21": 16973824,
"0/40/22": 1,
"0/40/65532": 0,
"0/40/65533": 3,
"0/40/65528": [],
"0/40/65529": [],
"0/40/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 21, 22,
65528, 65529, 65531, 65532, 65533
],
"0/42/0": [
{
"1": 556220604,
"2": 0,
"254": 1
}
],
"0/42/1": true,
"0/42/2": 1,
"0/42/3": null,
"0/42/65532": 0,
"0/42/65533": 1,
"0/42/65528": [],
"0/42/65529": [0],
"0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"0/43/0": "en-US",
"0/43/1": [
"en-US",
"de-DE",
"fr-FR",
"en-GB",
"es-ES",
"zh-CN",
"it-IT",
"ja-JP"
],
"0/43/65532": 0,
"0/43/65533": 1,
"0/43/65528": [],
"0/43/65529": [],
"0/43/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"0/44/0": 0,
"0/44/1": 0,
"0/44/2": [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 7],
"0/44/65532": 0,
"0/44/65533": 1,
"0/44/65528": [],
"0/44/65529": [],
"0/44/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533],
"0/45/0": 1,
"0/45/65532": 0,
"0/45/65533": 1,
"0/45/65528": [],
"0/45/65529": [],
"0/45/65531": [0, 65528, 65529, 65531, 65532, 65533],
"0/48/0": 0,
"0/48/1": {
"0": 60,
"1": 900
},
"0/48/2": 0,
"0/48/3": 0,
"0/48/4": true,
"0/48/65532": 0,
"0/48/65533": 1,
"0/48/65528": [1, 3, 5],
"0/48/65529": [0, 2, 4],
"0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
"0/49/0": 1,
"0/49/1": [],
"0/49/2": 10,
"0/49/3": 20,
"0/49/4": true,
"0/49/5": 0,
"0/49/6": "**REDACTED**",
"0/49/7": null,
"0/49/9": 10,
"0/49/10": 4,
"0/49/65532": 2,
"0/49/65533": 2,
"0/49/65528": [1, 5, 7],
"0/49/65529": [0, 3, 4, 6, 8],
"0/49/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 65528, 65529, 65531, 65532, 65533
],
"0/50/65532": 0,
"0/50/65533": 1,
"0/50/65528": [1],
"0/50/65529": [0],
"0/50/65531": [65528, 65529, 65531, 65532, 65533],
"0/51/0": [],
"0/51/1": 6,
"0/51/2": 10,
"0/51/3": 4,
"0/51/4": 1,
"0/51/5": [],
"0/51/6": [],
"0/51/7": [],
"0/51/8": false,
"0/51/65532": 0,
"0/51/65533": 2,
"0/51/65528": [2],
"0/51/65529": [0, 1],
"0/51/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533
],
"0/52/0": [
{
"0": 3,
"1": "UART",
"3": 128
},
{
"0": 9,
"1": "DishWash",
"3": 766
},
{
"0": 2,
"1": "OT Stack",
"3": 719
},
{
"0": 12,
"1": "Bluetoot",
"3": 40
},
{
"0": 1,
"1": "Bluetoot",
"3": 282
},
{
"0": 11,
"1": "Bluetoot",
"3": 210
},
{
"0": 8,
"1": "shell",
"3": 323
},
{
"0": 6,
"1": "Tmr Svc",
"3": 594
},
{
"0": 5,
"1": "IDLE",
"3": 266
},
{
"0": 7,
"1": "CHIP",
"3": 705
}
],
"0/52/1": 100824,
"0/52/2": 16984,
"0/52/3": 4294959062,
"0/52/65532": 1,
"0/52/65533": 1,
"0/52/65528": [],
"0/52/65529": [0],
"0/52/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"0/53/0": 25,
"0/53/1": 5,
"0/53/2": "**REDACTED**",
"0/53/3": 39055,
"0/53/4": 12054125955590472924,
"0/53/5": "**REDACTED**",
"0/53/6": 0,
"0/53/7": [],
"0/53/8": [],
"0/53/9": 1773502518,
"0/53/10": 64,
"0/53/11": 88,
"0/53/12": 225,
"0/53/13": 22,
"0/53/14": 1,
"0/53/15": 0,
"0/53/16": 1,
"0/53/17": 0,
"0/53/18": 0,
"0/53/19": 1,
"0/53/20": 0,
"0/53/21": 0,
"0/53/22": 693,
"0/53/23": 686,
"0/53/24": 7,
"0/53/25": 686,
"0/53/26": 686,
"0/53/27": 7,
"0/53/28": 693,
"0/53/29": 0,
"0/53/30": 0,
"0/53/31": 0,
"0/53/32": 0,
"0/53/33": 61,
"0/53/34": 0,
"0/53/35": 0,
"0/53/36": 2,
"0/53/37": 0,
"0/53/38": 0,
"0/53/39": 87,
"0/53/40": 87,
"0/53/41": 0,
"0/53/42": 86,
"0/53/43": 0,
"0/53/44": 0,
"0/53/45": 0,
"0/53/46": 0,
"0/53/47": 0,
"0/53/48": 0,
"0/53/49": 1,
"0/53/50": 0,
"0/53/51": 0,
"0/53/52": 0,
"0/53/53": 0,
"0/53/54": 0,
"0/53/55": 0,
"0/53/56": 0,
"0/53/57": 0,
"0/53/58": 0,
"0/53/59": {
"0": 672,
"1": 8335
},
"0/53/60": "AB//wA==",
"0/53/61": {
"0": true,
"1": false,
"2": true,
"3": true,
"4": true,
"5": true,
"6": false,
"7": true,
"8": true,
"9": true,
"10": true,
"11": true
},
"0/53/62": [],
"0/53/65532": 15,
"0/53/65533": 2,
"0/53/65528": [],
"0/53/65529": [0],
"0/53/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
57, 58, 59, 60, 61, 62, 65528, 65529, 65531, 65532, 65533
],
"0/60/0": 0,
"0/60/1": null,
"0/60/2": null,
"0/60/65532": 0,
"0/60/65533": 1,
"0/60/65528": [],
"0/60/65529": [0, 1, 2],
"0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533],
"0/62/0": [],
"0/62/1": [],
"0/62/2": 5,
"0/62/3": 3,
"0/62/4": [],
"0/62/5": 3,
"0/62/65532": 0,
"0/62/65533": 1,
"0/62/65528": [1, 3, 5, 8],
"0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11],
"0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533],
"0/63/0": [],
"0/63/1": [],
"0/63/2": 4,
"0/63/3": 3,
"0/63/65532": 0,
"0/63/65533": 2,
"0/63/65528": [2, 5],
"0/63/65529": [0, 1, 3, 4],
"0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"0/64/0": [
{
"0": "room",
"1": "bedroom 2"
},
{
"0": "orientation",
"1": "North"
},
{
"0": "floor",
"1": "2"
},
{
"0": "direction",
"1": "up"
}
],
"0/64/65532": 0,
"0/64/65533": 1,
"0/64/65528": [],
"0/64/65529": [],
"0/64/65531": [0, 65528, 65529, 65531, 65532, 65533],
"0/65/0": [],
"0/65/65532": 0,
"0/65/65533": 1,
"0/65/65528": [],
"0/65/65529": [],
"0/65/65531": [0, 65528, 65529, 65531, 65532, 65533],
"1/3/0": 0,
"1/3/1": 2,
"1/3/65532": 0,
"1/3/65533": 4,
"1/3/65528": [],
"1/3/65529": [0, 64],
"1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/29/0": [
{
"0": 117,
"1": 1
}
],
"1/29/1": [3, 29, 30, 89, 96],
"1/29/2": [],
"1/29/3": [],
"1/29/65532": 0,
"1/29/65533": 2,
"1/29/65528": [],
"1/29/65529": [],
"1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"1/30/0": [],
"1/30/65532": 0,
"1/30/65533": 1,
"1/30/65528": [],
"1/30/65529": [],
"1/30/65531": [0, 65528, 65529, 65531, 65532, 65533],
"1/89/0": null,
"1/89/1": null,
"1/89/65532": null,
"1/89/65533": 2,
"1/89/65528": [1],
"1/89/65529": [0],
"1/89/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/96/0": null,
"1/96/1": null,
"1/96/3": [
{
"0": 0
},
{
"0": 1
},
{
"0": 2
},
{
"0": 3
},
{
"0": 8,
"1": "Extra state"
}
],
"1/96/4": 0,
"1/96/5": {
"0": 0
},
"1/96/65532": 0,
"1/96/65533": 1,
"1/96/65528": [4],
"1/96/65529": [0, 1, 2, 3],
"1/96/65531": [0, 1, 3, 4, 5, 65528, 65529, 65531, 65532, 65533],
"2/29/0": [
{
"0": 1296,
"1": 1
}
],
"2/29/1": [29, 144, 145, 156],
"2/29/2": [],
"2/29/3": [],
"2/29/65532": 0,
"2/29/65533": 2,
"2/29/65528": [],
"2/29/65529": [],
"2/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"2/144/0": 2,
"2/144/1": 3,
"2/144/2": [
{
"0": 5,
"1": true,
"2": -50000000,
"3": 50000000,
"4": [
{
"0": -50000000,
"1": -10000000,
"2": 5000,
"3": 2000,
"4": 3000
},
{
"0": -9999999,
"1": 9999999,
"2": 1000,
"3": 100,
"4": 500
},
{
"0": 10000000,
"1": 50000000,
"2": 5000,
"3": 2000,
"4": 3000
}
]
},
{
"0": 2,
"1": true,
"2": -100000,
"3": 100000,
"4": [
{
"0": -100000,
"1": -5000,
"2": 5000,
"3": 2000,
"4": 3000
},
{
"0": -4999,
"1": 4999,
"2": 1000,
"3": 100,
"4": 500
},
{
"0": 5000,
"1": 100000,
"2": 5000,
"3": 2000,
"4": 3000
}
]
},
{
"0": 1,
"1": true,
"2": -500000,
"3": 500000,
"4": [
{
"0": -500000,
"1": -100000,
"2": 5000,
"3": 2000,
"4": 3000
},
{
"0": -99999,
"1": 99999,
"2": 1000,
"3": 100,
"4": 500
},
{
"0": 100000,
"1": 500000,
"2": 5000,
"3": 2000,
"4": 3000
}
]
}
],
"2/144/3": [
{
"0": 0,
"1": 0,
"2": 300,
"7": 101,
"8": 101,
"9": 101,
"10": 101
},
{
"0": 1,
"1": 0,
"2": 500,
"7": 101,
"8": 101,
"9": 101,
"10": 101
},
{
"0": 2,
"1": 0,
"2": 1000,
"7": 101,
"8": 101,
"9": 101,
"10": 101
}
],
"2/144/4": 120000,
"2/144/5": 0,
"2/144/6": 0,
"2/144/7": 0,
"2/144/8": 0,
"2/144/9": 0,
"2/144/10": 0,
"2/144/11": 120000,
"2/144/12": 0,
"2/144/13": 0,
"2/144/14": 60,
"2/144/15": [
{
"0": 1,
"1": 100000
}
],
"2/144/16": [
{
"0": 1,
"1": 100000
}
],
"2/144/17": 9800,
"2/144/18": 0,
"2/144/65532": 31,
"2/144/65533": 1,
"2/144/65528": [],
"2/144/65529": [],
"2/144/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 65528,
65529, 65531, 65532, 65533
],
"2/145/0": {
"0": 14,
"1": true,
"2": 0,
"3": 1000000000000000,
"4": [
{
"0": 0,
"1": 1000000000000000,
"2": 500,
"3": 50
}
]
},
"2/145/1": {
"0": 0,
"1": 9,
"2": 12,
"3": 9649,
"4": 12530
},
"2/145/5": {
"0": 0,
"1": 0,
"2": 0,
"3": 0
},
"2/145/65532": 5,
"2/145/65533": 1,
"2/145/65528": [],
"2/145/65529": [],
"2/145/65531": [0, 1, 5, 65528, 65529, 65531, 65532, 65533],
"2/156/0": [0, 1, 2],
"2/156/1": null,
"2/156/65532": 12,
"2/156/65533": 1,
"2/156/65528": [],
"2/156/65529": [],
"2/156/65531": [0, 1, 65528, 65529, 65531, 65532, 65533]
},
"attribute_subscriptions": []
}

View file

@ -122,6 +122,16 @@ async def air_purifier_node_fixture(
)
@pytest.fixture(name="dishwasher_node")
async def dishwasher_node_fixture(
hass: HomeAssistant, matter_client: MagicMock
) -> MatterNode:
"""Fixture for an dishwasher node."""
return await setup_integration_with_node_fixture(
hass, "silabs-dishwasher", matter_client
)
# This tests needs to be adjusted to remove lingering tasks
@pytest.mark.parametrize("expected_lingering_tasks", [True])
async def test_sensor_null_value(
@ -622,3 +632,29 @@ async def test_smoke_alarm(
state = hass.states.get("sensor.smoke_sensor_voltage")
assert state
assert state.state == "0.0"
async def test_operational_state_sensor(
hass: HomeAssistant,
matter_client: MagicMock,
dishwasher_node: MatterNode,
) -> None:
"""Test dishwasher sensor."""
# OperationalState Cluster / OperationalState attribute (1/96/4)
state = hass.states.get("sensor.dishwasher_operational_state")
assert state
assert state.state == "stopped"
assert state.attributes["options"] == [
"stopped",
"running",
"paused",
"error",
"extra_state",
]
set_node_attribute(dishwasher_node, 1, 96, 4, 8)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("sensor.dishwasher_operational_state")
assert state
assert state.state == "extra_state"