Skip to content

Commit a748525

Browse files
Allow LevelControl Cluster for Matter Pump devices (home-assistant#145004)
Co-authored-by: Martin Hjelmare <[email protected]>
1 parent 8ca1fe8 commit a748525

File tree

4 files changed

+149
-0
lines changed

4 files changed

+149
-0
lines changed

homeassistant/components/matter/number.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from chip.clusters import Objects as clusters
1010
from chip.clusters.ClusterObjects import ClusterAttributeDescriptor, ClusterCommand
11+
from matter_server.client.models import device_types
1112
from matter_server.common import custom_clusters
1213

1314
from homeassistant.components.number import (
@@ -18,6 +19,7 @@
1819
)
1920
from homeassistant.config_entries import ConfigEntry
2021
from homeassistant.const import (
22+
PERCENTAGE,
2123
EntityCategory,
2224
Platform,
2325
UnitOfLength,
@@ -123,6 +125,31 @@ def _update_from_device(self) -> None:
123125
)
124126

125127

128+
class MatterLevelControlNumber(MatterEntity, NumberEntity):
129+
"""Representation of a Matter Attribute as a Number entity."""
130+
131+
entity_description: MatterNumberEntityDescription
132+
133+
async def async_set_native_value(self, value: float) -> None:
134+
"""Set level value."""
135+
send_value = int(value)
136+
if value_convert := self.entity_description.ha_to_native_value:
137+
send_value = value_convert(value)
138+
await self.send_device_command(
139+
clusters.LevelControl.Commands.MoveToLevel(
140+
level=send_value,
141+
)
142+
)
143+
144+
@callback
145+
def _update_from_device(self) -> None:
146+
"""Update from device."""
147+
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
148+
if value_convert := self.entity_description.measurement_to_ha:
149+
value = value_convert(value)
150+
self._attr_native_value = value
151+
152+
126153
# Discovery schema(s) to map Matter Attributes to HA entities
127154
DISCOVERY_SCHEMAS = [
128155
MatterDiscoverySchema(
@@ -239,6 +266,26 @@ def _update_from_device(self) -> None:
239266
),
240267
vendor_id=(4874,),
241268
),
269+
MatterDiscoverySchema(
270+
platform=Platform.NUMBER,
271+
entity_description=MatterNumberEntityDescription(
272+
key="pump_setpoint",
273+
native_unit_of_measurement=PERCENTAGE,
274+
translation_key="pump_setpoint",
275+
native_max_value=100,
276+
native_min_value=0.5,
277+
native_step=0.5,
278+
measurement_to_ha=(
279+
lambda x: None if x is None else x / 2 # Matter range (1-200)
280+
),
281+
ha_to_native_value=lambda x: round(x * 2), # HA range 0.5–100.0%
282+
mode=NumberMode.SLIDER,
283+
),
284+
entity_class=MatterLevelControlNumber,
285+
required_attributes=(clusters.LevelControl.Attributes.CurrentLevel,),
286+
device_type=(device_types.Pump,),
287+
allow_multi=True,
288+
),
242289
MatterDiscoverySchema(
243290
platform=Platform.NUMBER,
244291
entity_description=MatterNumberEntityDescription(

homeassistant/components/matter/strings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@
180180
"altitude": {
181181
"name": "Altitude above sea level"
182182
},
183+
"pump_setpoint": {
184+
"name": "Setpoint"
185+
},
183186
"temperature_offset": {
184187
"name": "Temperature offset"
185188
},

tests/components/matter/snapshots/test_number.ambr

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1961,6 +1961,64 @@
19611961
'state': '0',
19621962
})
19631963
# ---
1964+
# name: test_numbers[pump][number.mock_pump_setpoint-entry]
1965+
EntityRegistryEntrySnapshot({
1966+
'aliases': set({
1967+
}),
1968+
'area_id': None,
1969+
'capabilities': dict({
1970+
'max': 100,
1971+
'min': 0.5,
1972+
'mode': <NumberMode.SLIDER: 'slider'>,
1973+
'step': 0.5,
1974+
}),
1975+
'config_entry_id': <ANY>,
1976+
'config_subentry_id': <ANY>,
1977+
'device_class': None,
1978+
'device_id': <ANY>,
1979+
'disabled_by': None,
1980+
'domain': 'number',
1981+
'entity_category': None,
1982+
'entity_id': 'number.mock_pump_setpoint',
1983+
'has_entity_name': True,
1984+
'hidden_by': None,
1985+
'icon': None,
1986+
'id': <ANY>,
1987+
'labels': set({
1988+
}),
1989+
'name': None,
1990+
'options': dict({
1991+
}),
1992+
'original_device_class': None,
1993+
'original_icon': None,
1994+
'original_name': 'Setpoint',
1995+
'platform': 'matter',
1996+
'previous_unique_id': None,
1997+
'suggested_object_id': None,
1998+
'supported_features': 0,
1999+
'translation_key': 'pump_setpoint',
2000+
'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-pump_setpoint-8-0',
2001+
'unit_of_measurement': '%',
2002+
})
2003+
# ---
2004+
# name: test_numbers[pump][number.mock_pump_setpoint-state]
2005+
StateSnapshot({
2006+
'attributes': ReadOnlyDict({
2007+
'friendly_name': 'Mock Pump Setpoint',
2008+
'max': 100,
2009+
'min': 0.5,
2010+
'mode': <NumberMode.SLIDER: 'slider'>,
2011+
'step': 0.5,
2012+
'unit_of_measurement': '%',
2013+
}),
2014+
'context': <ANY>,
2015+
'entity_id': 'number.mock_pump_setpoint',
2016+
'last_changed': <ANY>,
2017+
'last_reported': <ANY>,
2018+
'last_updated': <ANY>,
2019+
'state': '127.0',
2020+
})
2021+
# ---
19642022
# name: test_numbers[silabs_laundrywasher][number.laundrywasher_temperature_setpoint-entry]
19652023
EntityRegistryEntrySnapshot({
19662024
'aliases': set({

tests/components/matter/test_number.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,44 @@ async def test_matter_exception_on_write_attribute(
160160
},
161161
blocking=True,
162162
)
163+
164+
165+
@pytest.mark.parametrize("node_fixture", ["pump"])
166+
async def test_pump_level(
167+
hass: HomeAssistant,
168+
matter_client: MagicMock,
169+
matter_node: MatterNode,
170+
) -> None:
171+
"""Test level control for pump."""
172+
# CurrentLevel on LevelControl cluster
173+
state = hass.states.get("number.mock_pump_setpoint")
174+
assert state
175+
assert state.state == "127.0"
176+
177+
set_node_attribute(matter_node, 1, 8, 0, 100)
178+
await trigger_subscription_callback(hass, matter_client)
179+
state = hass.states.get("number.mock_pump_setpoint")
180+
assert state
181+
assert state.state == "50.0"
182+
183+
# test set value
184+
await hass.services.async_call(
185+
"number",
186+
"set_value",
187+
{
188+
"entity_id": "number.mock_pump_setpoint",
189+
"value": 75,
190+
},
191+
blocking=True,
192+
)
193+
assert matter_client.send_device_command.call_count == 1
194+
assert (
195+
matter_client.send_device_command.call_args
196+
== call(
197+
node_id=matter_node.node_id,
198+
endpoint_id=1,
199+
command=clusters.LevelControl.Commands.MoveToLevel(
200+
level=150
201+
), # 75 * 2 = 150, as the value is multiplied by 2 in the HA to native value conversion
202+
)
203+
)

0 commit comments

Comments
 (0)