Allow LevelControl Cluster for Matter Pump devices (#145004)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Ludovic BOUÉ 2025-07-02 21:48:15 +02:00 committed by GitHub
parent 8ca1fe83b7
commit a748525e03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 149 additions and 0 deletions

View File

@ -8,6 +8,7 @@ from typing import Any, cast
from chip.clusters import Objects as clusters from chip.clusters import Objects as clusters
from chip.clusters.ClusterObjects import ClusterAttributeDescriptor, ClusterCommand from chip.clusters.ClusterObjects import ClusterAttributeDescriptor, ClusterCommand
from matter_server.client.models import device_types
from matter_server.common import custom_clusters from matter_server.common import custom_clusters
from homeassistant.components.number import ( from homeassistant.components.number import (
@ -18,6 +19,7 @@ from homeassistant.components.number import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
PERCENTAGE,
EntityCategory, EntityCategory,
Platform, Platform,
UnitOfLength, UnitOfLength,
@ -123,6 +125,31 @@ class MatterRangeNumber(MatterEntity, NumberEntity):
) )
class MatterLevelControlNumber(MatterEntity, NumberEntity):
"""Representation of a Matter Attribute as a Number entity."""
entity_description: MatterNumberEntityDescription
async def async_set_native_value(self, value: float) -> None:
"""Set level value."""
send_value = int(value)
if value_convert := self.entity_description.ha_to_native_value:
send_value = value_convert(value)
await self.send_device_command(
clusters.LevelControl.Commands.MoveToLevel(
level=send_value,
)
)
@callback
def _update_from_device(self) -> None:
"""Update from device."""
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
if value_convert := self.entity_description.measurement_to_ha:
value = value_convert(value)
self._attr_native_value = value
# Discovery schema(s) to map Matter Attributes to HA entities # Discovery schema(s) to map Matter Attributes to HA entities
DISCOVERY_SCHEMAS = [ DISCOVERY_SCHEMAS = [
MatterDiscoverySchema( MatterDiscoverySchema(
@ -239,6 +266,26 @@ DISCOVERY_SCHEMAS = [
), ),
vendor_id=(4874,), vendor_id=(4874,),
), ),
MatterDiscoverySchema(
platform=Platform.NUMBER,
entity_description=MatterNumberEntityDescription(
key="pump_setpoint",
native_unit_of_measurement=PERCENTAGE,
translation_key="pump_setpoint",
native_max_value=100,
native_min_value=0.5,
native_step=0.5,
measurement_to_ha=(
lambda x: None if x is None else x / 2 # Matter range (1-200)
),
ha_to_native_value=lambda x: round(x * 2), # HA range 0.5100.0%
mode=NumberMode.SLIDER,
),
entity_class=MatterLevelControlNumber,
required_attributes=(clusters.LevelControl.Attributes.CurrentLevel,),
device_type=(device_types.Pump,),
allow_multi=True,
),
MatterDiscoverySchema( MatterDiscoverySchema(
platform=Platform.NUMBER, platform=Platform.NUMBER,
entity_description=MatterNumberEntityDescription( entity_description=MatterNumberEntityDescription(

View File

@ -180,6 +180,9 @@
"altitude": { "altitude": {
"name": "Altitude above sea level" "name": "Altitude above sea level"
}, },
"pump_setpoint": {
"name": "Setpoint"
},
"temperature_offset": { "temperature_offset": {
"name": "Temperature offset" "name": "Temperature offset"
}, },

View File

@ -1961,6 +1961,64 @@
'state': '0', 'state': '0',
}) })
# --- # ---
# name: test_numbers[pump][number.mock_pump_setpoint-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0.5,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 0.5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.mock_pump_setpoint',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Setpoint',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'pump_setpoint',
'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-pump_setpoint-8-0',
'unit_of_measurement': '%',
})
# ---
# name: test_numbers[pump][number.mock_pump_setpoint-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Pump Setpoint',
'max': 100,
'min': 0.5,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 0.5,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.mock_pump_setpoint',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '127.0',
})
# ---
# name: test_numbers[silabs_laundrywasher][number.laundrywasher_temperature_setpoint-entry] # name: test_numbers[silabs_laundrywasher][number.laundrywasher_temperature_setpoint-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

@ -160,3 +160,44 @@ async def test_matter_exception_on_write_attribute(
}, },
blocking=True, blocking=True,
) )
@pytest.mark.parametrize("node_fixture", ["pump"])
async def test_pump_level(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test level control for pump."""
# CurrentLevel on LevelControl cluster
state = hass.states.get("number.mock_pump_setpoint")
assert state
assert state.state == "127.0"
set_node_attribute(matter_node, 1, 8, 0, 100)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("number.mock_pump_setpoint")
assert state
assert state.state == "50.0"
# test set value
await hass.services.async_call(
"number",
"set_value",
{
"entity_id": "number.mock_pump_setpoint",
"value": 75,
},
blocking=True,
)
assert matter_client.send_device_command.call_count == 1
assert (
matter_client.send_device_command.call_args
== call(
node_id=matter_node.node_id,
endpoint_id=1,
command=clusters.LevelControl.Commands.MoveToLevel(
level=150
), # 75 * 2 = 150, as the value is multiplied by 2 in the HA to native value conversion
)
)