From e22ea85e844e156ee7ea6e3dc7610dc792f14f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Fri, 23 May 2025 17:20:27 +0200 Subject: [PATCH] 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 * Handle SupplyFault bit enabled too * Review fix * Unmove * Remove pump_operation_mode * Update snapshot --------- Co-authored-by: TheJulianJES --- .../components/matter/binary_sensor.py | 43 +++++++ homeassistant/components/matter/icons.json | 12 ++ homeassistant/components/matter/select.py | 21 ++++ homeassistant/components/matter/sensor.py | 38 ++++++ homeassistant/components/matter/strings.json | 23 ++++ .../matter/fixtures/nodes/pump.json | 2 +- .../matter/snapshots/test_binary_sensor.ambr | 96 +++++++++++++++ .../matter/snapshots/test_select.ambr | 60 +++++++++ .../matter/snapshots/test_sensor.ambr | 116 ++++++++++++++++++ tests/components/matter/test_binary_sensor.py | 40 ++++++ tests/components/matter/test_select.py | 19 +++ tests/components/matter/test_sensor.py | 32 +++++ 12 files changed, 501 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/matter/binary_sensor.py b/homeassistant/components/matter/binary_sensor.py index 95375d5fc49..2d04a936ee5 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -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, + ), ] diff --git a/homeassistant/components/matter/icons.json b/homeassistant/components/matter/icons.json index 82e45e0383a..ac3e70dcfc8 100644 --- a/homeassistant/components/matter/icons.json +++ b/homeassistant/components/matter/icons.json @@ -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": { diff --git a/homeassistant/components/matter/select.py b/homeassistant/components/matter/select.py index 39e1db3bf6f..ac1bc2d1f8f 100644 --- a/homeassistant/components/matter/select.py +++ b/homeassistant/components/matter/select.py @@ -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, + ), + ), ] diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index 381ecc480da..2197f81e134 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -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,), + ), ] diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index 325e8d1f26c..a04f1d86880 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -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" }, diff --git a/tests/components/matter/fixtures/nodes/pump.json b/tests/components/matter/fixtures/nodes/pump.json index 39579f4448c..e4afc0b4f33 100644 --- a/tests/components/matter/fixtures/nodes/pump.json +++ b/tests/components/matter/fixtures/nodes/pump.json @@ -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, diff --git a/tests/components/matter/snapshots/test_binary_sensor.ambr b/tests/components/matter/snapshots/test_binary_sensor.ambr index feca62ffa31..e91ea9f7ba9 100644 --- a/tests/components/matter/snapshots/test_binary_sensor.ambr +++ b/tests/components/matter/snapshots/test_binary_sensor.ambr @@ -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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mock_pump_problem', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + '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': , + 'entity_id': 'binary_sensor.mock_pump_problem', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[pump][binary_sensor.mock_pump_running-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + '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': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + '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': , + 'entity_id': 'binary_sensor.mock_pump_running', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_binary_sensors[silabs_evse_charging][binary_sensor.evse_charging_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_select.ambr b/tests/components/matter/snapshots/test_select.ambr index edd0224ccac..0ab50d7a7fc 100644 --- a/tests/components/matter/snapshots/test_select.ambr +++ b/tests/components/matter/snapshots/test_select.ambr @@ -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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.mock_pump_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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': , + 'entity_id': 'select.mock_pump_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'normal', + }) +# --- # name: test_selects[silabs_evse_charging][select.evse_energy_management_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index bf22986d6df..424511f286e 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + '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': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + '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': , + 'entity_id': 'sensor.mock_pump_control_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + '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': , + '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': , + 'unit_of_measurement': 'rpm', + }), + 'context': , + 'entity_id': 'sensor.mock_pump_rotation_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1000', + }) +# --- # name: test_sensors[pump][sensor.mock_pump_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/test_binary_sensor.py b/tests/components/matter/test_binary_sensor.py index bea9c1ad237..e221140b85b 100644 --- a/tests/components/matter/test_binary_sensor.py +++ b/tests/components/matter/test_binary_sensor.py @@ -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" diff --git a/tests/components/matter/test_select.py b/tests/components/matter/test_select.py index 456558d983d..7045b60a24e 100644 --- a/tests/components/matter/test_select.py +++ b/tests/components/matter/test_select.py @@ -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" diff --git a/tests/components/matter/test_sensor.py b/tests/components/matter/test_sensor.py index feb604bd365..19697efab71 100644 --- a/tests/components/matter/test_sensor.py +++ b/tests/components/matter/test_sensor.py @@ -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"