Add Matter Pump device type (#145335)

* Pump status

* Pump speed

* PumpStatusRunning

* ControlModeEnum

* Add tests

* Clean code

* Update tests and sensors

* Review fixes

* Add RPM unit

* Fix for unknown value

* Update snapshot

* OperationMode

* Update snapshots

* Update snapshot

* Update tests/components/matter/test_select.py

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>

* Handle SupplyFault bit enabled too

* Review fix

* Unmove

* Remove pump_operation_mode

* Update snapshot

---------

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
This commit is contained in:
Ludovic BOUÉ 2025-05-23 17:20:27 +02:00 committed by GitHub
parent ed0ff93d1e
commit e22ea85e84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 501 additions and 1 deletions

View File

@ -334,4 +334,47 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterBinarySensor,
required_attributes=(clusters.WaterHeaterManagement.Attributes.BoostState,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="PumpFault",
translation_key="pump_fault",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
# DeviceFault or SupplyFault bit enabled
measurement_to_ha={
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kDeviceFault: True,
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kSupplyFault: True,
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kSpeedLow: False,
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kSpeedHigh: False,
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kLocalOverride: False,
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kRunning: False,
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kRemotePressure: False,
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kRemoteFlow: False,
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kRemoteTemperature: False,
}.get,
),
entity_class=MatterBinarySensor,
required_attributes=(
clusters.PumpConfigurationAndControl.Attributes.PumpStatus,
),
allow_multi=True,
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="PumpStatusRunning",
translation_key="pump_running",
device_class=BinarySensorDeviceClass.RUNNING,
measurement_to_ha=lambda x: (
x
== clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kRunning
),
),
entity_class=MatterBinarySensor,
required_attributes=(
clusters.PumpConfigurationAndControl.Attributes.PumpStatus,
),
allow_multi=True,
),
]

View File

@ -57,6 +57,9 @@
"bat_replacement_description": {
"default": "mdi:battery-sync"
},
"flow": {
"default": "mdi:pipe"
},
"hepa_filter_condition": {
"default": "mdi:filter-check"
},
@ -86,6 +89,15 @@
},
"evse_fault_state": {
"default": "mdi:ev-station"
},
"pump_control_mode": {
"default": "mdi:pipe-wrench"
},
"pump_speed": {
"default": "mdi:speedometer"
},
"pump_status": {
"default": "mdi:pump"
}
},
"switch": {

View File

@ -30,6 +30,13 @@ NUMBER_OF_RINSES_STATE_MAP = {
NUMBER_OF_RINSES_STATE_MAP_REVERSE = {
v: k for k, v in NUMBER_OF_RINSES_STATE_MAP.items()
}
PUMP_OPERATION_MODE_MAP = {
clusters.PumpConfigurationAndControl.Enums.OperationModeEnum.kNormal: "normal",
clusters.PumpConfigurationAndControl.Enums.OperationModeEnum.kMinimum: "minimum",
clusters.PumpConfigurationAndControl.Enums.OperationModeEnum.kMaximum: "maximum",
clusters.PumpConfigurationAndControl.Enums.OperationModeEnum.kLocal: "local",
}
PUMP_OPERATION_MODE_MAP_REVERSE = {v: k for k, v in PUMP_OPERATION_MODE_MAP.items()}
type SelectCluster = (
clusters.ModeSelect
@ -459,4 +466,18 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterAttributeSelectEntity,
required_attributes=(clusters.DoorLock.Attributes.SoundVolume,),
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="PumpConfigurationAndControlOperationMode",
translation_key="pump_operation_mode",
options=list(PUMP_OPERATION_MODE_MAP.values()),
measurement_to_ha=PUMP_OPERATION_MODE_MAP.get,
ha_to_native_value=PUMP_OPERATION_MODE_MAP_REVERSE.get,
),
entity_class=MatterAttributeSelectEntity,
required_attributes=(
clusters.PumpConfigurationAndControl.Attributes.OperationMode,
),
),
]

View File

@ -29,6 +29,7 @@ from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
EntityCategory,
Platform,
UnitOfElectricCurrent,
@ -110,6 +111,16 @@ EVSE_FAULT_STATE_MAP = {
clusters.EnergyEvse.Enums.FaultStateEnum.kOther: "other",
}
PUMP_CONTROL_MODE_MAP = {
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kConstantSpeed: "constant_speed",
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kConstantPressure: "constant_pressure",
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kProportionalPressure: "proportional_pressure",
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kConstantFlow: "constant_flow",
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kConstantTemperature: "constant_temperature",
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kAutomatic: "automatic",
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kUnknownEnumValue: None,
}
async def async_setup_entry(
hass: HomeAssistant,
@ -1118,4 +1129,31 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterSensor,
required_attributes=(clusters.DeviceEnergyManagement.Attributes.ESAState,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="PumpControlMode",
translation_key="pump_control_mode",
device_class=SensorDeviceClass.ENUM,
options=[
mode for mode in PUMP_CONTROL_MODE_MAP.values() if mode is not None
],
measurement_to_ha=PUMP_CONTROL_MODE_MAP.get,
),
entity_class=MatterSensor,
required_attributes=(
clusters.PumpConfigurationAndControl.Attributes.ControlMode,
),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="PumpSpeed",
translation_key="pump_speed",
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(clusters.PumpConfigurationAndControl.Attributes.Speed,),
),
]

View File

@ -239,6 +239,15 @@
"laundry_washer_spin_speed": {
"name": "Spin speed"
},
"pump_operation_mode": {
"name": "mode",
"state": {
"local": "Local",
"maximum": "Maximum",
"minimum": "Minimum",
"normal": "[%key:common::state::normal%]"
}
},
"water_heater_mode": {
"name": "Water heater mode"
},
@ -352,6 +361,20 @@
"other": "Other fault"
}
},
"pump_control_mode": {
"name": "Control mode",
"state": {
"constant_flow": "Constant flow",
"constant_pressure": "Constant pressure",
"constant_speed": "Constant speed",
"constant_temperature": "Constant temp",
"proportional_pressure": "Proportional pressure",
"automatic": "Automatic"
}
},
"pump_speed": {
"name": "Rotation speed"
},
"evse_circuit_capacity": {
"name": "Circuit capacity"
},

View File

@ -228,7 +228,7 @@
"1/512/0": 32767,
"1/512/1": 65534,
"1/512/2": 65534,
"1/512/16": 5,
"1/512/16": 32,
"1/512/17": 0,
"1/512/18": 5,
"1/512/19": null,

View File

@ -383,6 +383,102 @@
'state': 'off',
})
# ---
# name: test_binary_sensors[pump][binary_sensor.mock_pump_problem-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.mock_pump_problem',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Problem',
'platform': 'matter',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'pump_fault',
'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PumpFault-512-16',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[pump][binary_sensor.mock_pump_problem-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Mock Pump Problem',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.mock_pump_problem',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensors[pump][binary_sensor.mock_pump_running-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.mock_pump_running',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.RUNNING: 'running'>,
'original_icon': None,
'original_name': 'Running',
'platform': 'matter',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'pump_running',
'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PumpStatusRunning-512-16',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[pump][binary_sensor.mock_pump_running-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'running',
'friendly_name': 'Mock Pump Running',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.mock_pump_running',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_binary_sensors[silabs_evse_charging][binary_sensor.evse_charging_status-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -1967,6 +1967,66 @@
'state': 'Low',
})
# ---
# name: test_selects[pump][select.mock_pump_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'normal',
'minimum',
'maximum',
'local',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.mock_pump_mode',
'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': 'mode',
'platform': 'matter',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'pump_operation_mode',
'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PumpConfigurationAndControlOperationMode-512-32',
'unit_of_measurement': None,
})
# ---
# name: test_selects[pump][select.mock_pump_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Pump mode',
'options': list([
'normal',
'minimum',
'maximum',
'local',
]),
}),
'context': <ANY>,
'entity_id': 'select.mock_pump_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'normal',
})
# ---
# name: test_selects[silabs_evse_charging][select.evse_energy_management_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -3101,6 +3101,71 @@
'state': '0.0',
})
# ---
# name: test_sensors[pump][sensor.mock_pump_control_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'constant_speed',
'constant_pressure',
'proportional_pressure',
'constant_flow',
'constant_temperature',
'automatic',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.mock_pump_control_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Control mode',
'platform': 'matter',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'pump_control_mode',
'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PumpControlMode-512-33',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[pump][sensor.mock_pump_control_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Mock Pump Control mode',
'options': list([
'constant_speed',
'constant_pressure',
'proportional_pressure',
'constant_flow',
'constant_temperature',
'automatic',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.mock_pump_control_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'constant_temperature',
})
# ---
# name: test_sensors[pump][sensor.mock_pump_flow-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -3204,6 +3269,57 @@
'state': '10.0',
})
# ---
# name: test_sensors[pump][sensor.mock_pump_rotation_speed-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.mock_pump_rotation_speed',
'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': 'Rotation speed',
'platform': 'matter',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'pump_speed',
'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PumpSpeed-512-20',
'unit_of_measurement': 'rpm',
})
# ---
# name: test_sensors[pump][sensor.mock_pump_rotation_speed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock Pump Rotation speed',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'rpm',
}),
'context': <ANY>,
'entity_id': 'sensor.mock_pump_rotation_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1000',
})
# ---
# name: test_sensors[pump][sensor.mock_pump_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -217,3 +217,43 @@ async def test_water_heater(
state = hass.states.get("binary_sensor.water_heater_boost_state")
assert state
assert state.state == "on"
@pytest.mark.parametrize("node_fixture", ["pump"])
async def test_pump(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test pump sensors."""
# PumpStatus
state = hass.states.get("binary_sensor.mock_pump_running")
assert state
assert state.state == "on"
set_node_attribute(matter_node, 1, 512, 16, 0)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("binary_sensor.mock_pump_running")
assert state
assert state.state == "off"
# PumpStatus --> DeviceFault bit
state = hass.states.get("binary_sensor.mock_pump_problem")
assert state
assert state.state == "unknown"
set_node_attribute(matter_node, 1, 512, 16, 1)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("binary_sensor.mock_pump_problem")
assert state
assert state.state == "on"
# PumpStatus --> SupplyFault bit
set_node_attribute(matter_node, 1, 512, 16, 2)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("binary_sensor.mock_pump_problem")
assert state
assert state.state == "on"

View File

@ -216,3 +216,22 @@ async def test_map_select_entities(
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("select.laundrywasher_number_of_rinses")
assert state.state == "normal"
@pytest.mark.parametrize("node_fixture", ["pump"])
async def test_pump(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test MatterAttributeSelectEntity entities are discovered and working from a pump fixture."""
# OperationMode
state = hass.states.get("select.mock_pump_mode")
assert state
assert state.state == "normal"
assert state.attributes["options"] == ["normal", "minimum", "maximum", "local"]
set_node_attribute(matter_node, 1, 512, 32, 3)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("select.mock_pump_mode")
assert state.state == "local"

View File

@ -523,3 +523,35 @@ async def test_water_heater(
state = hass.states.get("sensor.water_heater_appliance_energy_state")
assert state
assert state.state == "offline"
@pytest.mark.parametrize("node_fixture", ["pump"])
async def test_pump(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test pump sensors."""
# ControlMode
state = hass.states.get("sensor.mock_pump_control_mode")
assert state
assert state.state == "constant_temperature"
set_node_attribute(matter_node, 1, 512, 33, 7)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("sensor.mock_pump_control_mode")
assert state
assert state.state == "automatic"
# Speed
state = hass.states.get("sensor.mock_pump_rotation_speed")
assert state
assert state.state == "1000"
set_node_attribute(matter_node, 1, 512, 20, 500)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("sensor.mock_pump_rotation_speed")
assert state
assert state.state == "500"