From 557b9d88b5cdef26ccff63a70affc8b4e5d205ec Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 27 Jan 2025 19:36:16 +0100 Subject: [PATCH] Catch and convert MatterError when sending device commands (#136635) --- homeassistant/components/matter/button.py | 6 +- homeassistant/components/matter/climate.py | 38 +++++-------- homeassistant/components/matter/cover.py | 8 --- homeassistant/components/matter/entity.py | 66 ++++++++++++++++++++-- homeassistant/components/matter/fan.py | 65 ++++++++------------- homeassistant/components/matter/light.py | 8 --- homeassistant/components/matter/lock.py | 25 +++----- homeassistant/components/matter/number.py | 9 +-- homeassistant/components/matter/select.py | 19 ++----- homeassistant/components/matter/switch.py | 21 ++----- homeassistant/components/matter/vacuum.py | 23 ++------ homeassistant/components/matter/valve.py | 11 ---- tests/components/matter/test_number.py | 24 ++++++++ tests/components/matter/test_switch.py | 23 ++++++++ 14 files changed, 170 insertions(+), 176 deletions(-) diff --git a/homeassistant/components/matter/button.py b/homeassistant/components/matter/button.py index 2c5e641e640..634406d18eb 100644 --- a/homeassistant/components/matter/button.py +++ b/homeassistant/components/matter/button.py @@ -49,11 +49,7 @@ class MatterCommandButton(MatterEntity, ButtonEntity): """Handle the button press leveraging a Matter command.""" if TYPE_CHECKING: assert self.entity_description.command is not None - await self.matter_client.send_device_command( - node_id=self._endpoint.node.node_id, - endpoint_id=self._endpoint.endpoint_id, - command=self.entity_description.command(), - ) + await self.send_device_command(self.entity_description.command()) # Discovery schema(s) to map Matter Attributes to HA entities diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index 8f6cd92d31f..25419c34e42 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -212,57 +212,45 @@ class MatterClimate(MatterEntity, ClimateEntity): matter_attribute = ( clusters.Thermostat.Attributes.OccupiedHeatingSetpoint ) - await self.matter_client.write_attribute( - node_id=self._endpoint.node.node_id, - attribute_path=create_attribute_path_from_attribute( - self._endpoint.endpoint_id, - matter_attribute, - ), + await self.write_attribute( value=int(target_temperature * TEMPERATURE_SCALING_FACTOR), + matter_attribute=matter_attribute, ) return if target_temperature_low is not None: # multi setpoint control - low setpoint (heat) if self.target_temperature_low != target_temperature_low: - await self.matter_client.write_attribute( - node_id=self._endpoint.node.node_id, - attribute_path=create_attribute_path_from_attribute( - self._endpoint.endpoint_id, - clusters.Thermostat.Attributes.OccupiedHeatingSetpoint, - ), + await self.write_attribute( value=int(target_temperature_low * TEMPERATURE_SCALING_FACTOR), + matter_attribute=clusters.Thermostat.Attributes.OccupiedHeatingSetpoint, ) if target_temperature_high is not None: # multi setpoint control - high setpoint (cool) if self.target_temperature_high != target_temperature_high: - await self.matter_client.write_attribute( - node_id=self._endpoint.node.node_id, - attribute_path=create_attribute_path_from_attribute( - self._endpoint.endpoint_id, - clusters.Thermostat.Attributes.OccupiedCoolingSetpoint, - ), + await self.write_attribute( value=int(target_temperature_high * TEMPERATURE_SCALING_FACTOR), + matter_attribute=clusters.Thermostat.Attributes.OccupiedCoolingSetpoint, ) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" - system_mode_path = create_attribute_path_from_attribute( - endpoint_id=self._endpoint.endpoint_id, - attribute=clusters.Thermostat.Attributes.SystemMode, - ) + system_mode_value = HVAC_SYSTEM_MODE_MAP.get(hvac_mode) if system_mode_value is None: raise ValueError(f"Unsupported hvac mode {hvac_mode} in Matter") - await self.matter_client.write_attribute( - node_id=self._endpoint.node.node_id, - attribute_path=system_mode_path, + await self.write_attribute( value=system_mode_value, + matter_attribute=clusters.Thermostat.Attributes.SystemMode, ) # we need to optimistically update the attribute's value here # to prevent a race condition when adjusting the mode and temperature # in the same call + system_mode_path = create_attribute_path_from_attribute( + endpoint_id=self._endpoint.endpoint_id, + attribute=clusters.Thermostat.Attributes.SystemMode, + ) self._endpoint.set_attribute_value(system_mode_path, system_mode_value) self._update_from_device() diff --git a/homeassistant/components/matter/cover.py b/homeassistant/components/matter/cover.py index ba9c3afbdee..5b109d52189 100644 --- a/homeassistant/components/matter/cover.py +++ b/homeassistant/components/matter/cover.py @@ -102,14 +102,6 @@ class MatterCover(MatterEntity, CoverEntity): clusters.WindowCovering.Commands.GoToTiltPercentage((100 - position) * 100) ) - async def send_device_command(self, command: Any) -> None: - """Send device command.""" - await self.matter_client.send_device_command( - node_id=self._endpoint.node.node_id, - endpoint_id=self._endpoint.endpoint_id, - command=command, - ) - @callback def _update_from_device(self) -> None: """Update from device.""" diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index 61c62d8b564..a6d0dbb08d8 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -2,18 +2,24 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass +import functools import logging -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, Concatenate, cast from chip.clusters import Objects as clusters -from chip.clusters.Objects import ClusterAttributeDescriptor, NullValue -from matter_server.common.helpers.util import create_attribute_path +from chip.clusters.Objects import ClusterAttributeDescriptor, ClusterCommand, NullValue +from matter_server.common.errors import MatterError +from matter_server.common.helpers.util import ( + create_attribute_path, + create_attribute_path_from_attribute, +) from matter_server.common.models import EventType, ServerInfoMessage from propcache.api import cached_property from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity, EntityDescription import homeassistant.helpers.entity_registry as er @@ -31,6 +37,23 @@ if TYPE_CHECKING: LOGGER = logging.getLogger(__name__) +def catch_matter_error[_R, **P]( + func: Callable[Concatenate[MatterEntity, P], Coroutine[Any, Any, _R]], +) -> Callable[Concatenate[MatterEntity, P], Coroutine[Any, Any, _R]]: + """Catch Matter errors and convert to Home Assistant error.""" + + @functools.wraps(func) + async def wrapper(self: MatterEntity, *args: P.args, **kwargs: P.kwargs) -> _R: + """Catch Matter errors and convert to Home Assistant error.""" + try: + return await func(self, *args, **kwargs) + except MatterError as err: + error_msg = str(err) or err.__class__.__name__ + raise HomeAssistantError(error_msg) from err + + return wrapper + + @dataclass(frozen=True) class MatterEntityDescription(EntityDescription): """Describe the Matter entity.""" @@ -218,3 +241,38 @@ class MatterEntity(Entity): return create_attribute_path( self._endpoint.endpoint_id, attribute.cluster_id, attribute.attribute_id ) + + @catch_matter_error + async def send_device_command( + self, + command: ClusterCommand, + **kwargs: Any, + ) -> None: + """Send device command on the primary attribute's endpoint.""" + await self.matter_client.send_device_command( + node_id=self._endpoint.node.node_id, + endpoint_id=self._endpoint.endpoint_id, + command=command, + **kwargs, + ) + + @catch_matter_error + async def write_attribute( + self, + value: Any, + matter_attribute: type[ClusterAttributeDescriptor] | None = None, + ) -> Any: + """Write an attribute(value) on the primary endpoint. + + If matter_attribute is not provided, the primary attribute of the entity is used. + """ + if matter_attribute is None: + matter_attribute = self._entity_info.primary_attribute + return await self.matter_client.write_attribute( + node_id=self._endpoint.node.node_id, + attribute_path=create_attribute_path_from_attribute( + self._endpoint.endpoint_id, + matter_attribute, + ), + value=value, + ) diff --git a/homeassistant/components/matter/fan.py b/homeassistant/components/matter/fan.py index 593693dbbf9..8b8ebee619d 100644 --- a/homeassistant/components/matter/fan.py +++ b/homeassistant/components/matter/fan.py @@ -5,7 +5,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any from chip.clusters import Objects as clusters -from matter_server.common.helpers.util import create_attribute_path_from_attribute from homeassistant.components.fan import ( DIRECTION_FORWARD, @@ -97,24 +96,16 @@ class MatterFan(MatterEntity, FanEntity): # clear the wind setting if its currently set if self._attr_preset_mode in [PRESET_NATURAL_WIND, PRESET_SLEEP_WIND]: await self._set_wind_mode(None) - await self.matter_client.write_attribute( - node_id=self._endpoint.node.node_id, - attribute_path=create_attribute_path_from_attribute( - self._endpoint.endpoint_id, - clusters.FanControl.Attributes.FanMode, - ), + await self.write_attribute( value=clusters.FanControl.Enums.FanModeEnum.kOff, + matter_attribute=clusters.FanControl.Attributes.FanMode, ) async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" - await self.matter_client.write_attribute( - node_id=self._endpoint.node.node_id, - attribute_path=create_attribute_path_from_attribute( - self._endpoint.endpoint_id, - clusters.FanControl.Attributes.PercentSetting, - ), + await self.write_attribute( value=percentage, + matter_attribute=clusters.FanControl.Attributes.PercentSetting, ) async def async_set_preset_mode(self, preset_mode: str) -> None: @@ -128,41 +119,33 @@ class MatterFan(MatterEntity, FanEntity): if self._attr_preset_mode in [PRESET_NATURAL_WIND, PRESET_SLEEP_WIND]: await self._set_wind_mode(None) - await self.matter_client.write_attribute( - node_id=self._endpoint.node.node_id, - attribute_path=create_attribute_path_from_attribute( - self._endpoint.endpoint_id, - clusters.FanControl.Attributes.FanMode, - ), + await self.write_attribute( value=FAN_MODE_MAP[preset_mode], + matter_attribute=clusters.FanControl.Attributes.FanMode, ) async def async_oscillate(self, oscillating: bool) -> None: """Oscillate the fan.""" - await self.matter_client.write_attribute( - node_id=self._endpoint.node.node_id, - attribute_path=create_attribute_path_from_attribute( - self._endpoint.endpoint_id, - clusters.FanControl.Attributes.RockSetting, + await self.write_attribute( + value=( + self.get_matter_attribute_value( + clusters.FanControl.Attributes.RockSupport + ) + if oscillating + else 0 ), - value=self.get_matter_attribute_value( - clusters.FanControl.Attributes.RockSupport - ) - if oscillating - else 0, + matter_attribute=clusters.FanControl.Attributes.RockSetting, ) async def async_set_direction(self, direction: str) -> None: """Set the direction of the fan.""" - await self.matter_client.write_attribute( - node_id=self._endpoint.node.node_id, - attribute_path=create_attribute_path_from_attribute( - self._endpoint.endpoint_id, - clusters.FanControl.Attributes.AirflowDirection, + await self.write_attribute( + value=( + clusters.FanControl.Enums.AirflowDirectionEnum.kReverse + if direction == DIRECTION_REVERSE + else clusters.FanControl.Enums.AirflowDirectionEnum.kForward ), - value=clusters.FanControl.Enums.AirflowDirectionEnum.kReverse - if direction == DIRECTION_REVERSE - else clusters.FanControl.Enums.AirflowDirectionEnum.kForward, + matter_attribute=clusters.FanControl.Attributes.AirflowDirection, ) async def _set_wind_mode(self, wind_mode: str | None) -> None: @@ -173,13 +156,9 @@ class MatterFan(MatterEntity, FanEntity): wind_setting = WindBitmap.kSleepWind else: wind_setting = 0 - await self.matter_client.write_attribute( - node_id=self._endpoint.node.node_id, - attribute_path=create_attribute_path_from_attribute( - self._endpoint.endpoint_id, - clusters.FanControl.Attributes.WindSetting, - ), + await self.write_attribute( value=wind_setting, + matter_attribute=clusters.FanControl.Attributes.WindSetting, ) @callback diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 5a2768d1d50..5c20554f065 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -282,14 +282,6 @@ class MatterLight(MatterEntity, LightEntity): return ha_color_mode - async def send_device_command(self, command: Any) -> None: - """Send device command.""" - await self.matter_client.send_device_command( - node_id=self._endpoint.node.node_id, - endpoint_id=self._endpoint.endpoint_id, - command=command, - ) - async def async_turn_on(self, **kwargs: Any) -> None: """Turn light on.""" diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index d69d0fd3dab..8524b39d584 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -62,19 +62,6 @@ class MatterLock(MatterEntity, LockEntity): return None - async def send_device_command( - self, - command: clusters.ClusterCommand, - timed_request_timeout_ms: int = 1000, - ) -> None: - """Send a command to the device.""" - await self.matter_client.send_device_command( - node_id=self._endpoint.node.node_id, - endpoint_id=self._endpoint.endpoint_id, - command=command, - timed_request_timeout_ms=timed_request_timeout_ms, - ) - async def async_lock(self, **kwargs: Any) -> None: """Lock the lock with pin if needed.""" if not self._attr_is_locked: @@ -89,7 +76,8 @@ class MatterLock(MatterEntity, LockEntity): code: str | None = kwargs.get(ATTR_CODE) code_bytes = code.encode() if code else None await self.send_device_command( - command=clusters.DoorLock.Commands.LockDoor(code_bytes) + command=clusters.DoorLock.Commands.LockDoor(code_bytes), + timed_request_timeout_ms=1000, ) async def async_unlock(self, **kwargs: Any) -> None: @@ -110,11 +98,13 @@ class MatterLock(MatterEntity, LockEntity): # the unlock command should unbolt only on the unlock command # and unlatch on the HA 'open' command. await self.send_device_command( - command=clusters.DoorLock.Commands.UnboltDoor(code_bytes) + command=clusters.DoorLock.Commands.UnboltDoor(code_bytes), + timed_request_timeout_ms=1000, ) else: await self.send_device_command( - command=clusters.DoorLock.Commands.UnlockDoor(code_bytes) + command=clusters.DoorLock.Commands.UnlockDoor(code_bytes), + timed_request_timeout_ms=1000, ) async def async_open(self, **kwargs: Any) -> None: @@ -130,7 +120,8 @@ class MatterLock(MatterEntity, LockEntity): code: str | None = kwargs.get(ATTR_CODE) code_bytes = code.encode() if code else None await self.send_device_command( - command=clusters.DoorLock.Commands.UnlockDoor(code_bytes) + command=clusters.DoorLock.Commands.UnlockDoor(code_bytes), + timed_request_timeout_ms=1000, ) @callback diff --git a/homeassistant/components/matter/number.py b/homeassistant/components/matter/number.py index 22929c60b89..4518e83e9d0 100644 --- a/homeassistant/components/matter/number.py +++ b/homeassistant/components/matter/number.py @@ -6,7 +6,6 @@ from dataclasses import dataclass from chip.clusters import Objects as clusters from matter_server.common import custom_clusters -from matter_server.common.helpers.util import create_attribute_path_from_attribute from homeassistant.components.number import ( NumberDeviceClass, @@ -52,16 +51,10 @@ class MatterNumber(MatterEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Update the current value.""" - matter_attribute = self._entity_info.primary_attribute sendvalue = int(value) if value_convert := self.entity_description.ha_to_native_value: sendvalue = value_convert(value) - await self.matter_client.write_attribute( - node_id=self._endpoint.node.node_id, - attribute_path=create_attribute_path_from_attribute( - self._endpoint.endpoint_id, - matter_attribute, - ), + await self.write_attribute( value=sendvalue, ) diff --git a/homeassistant/components/matter/select.py b/homeassistant/components/matter/select.py index 317c8515d4b..1018bed6af0 100644 --- a/homeassistant/components/matter/select.py +++ b/homeassistant/components/matter/select.py @@ -9,7 +9,6 @@ from typing import TYPE_CHECKING, cast from chip.clusters import Objects as clusters from chip.clusters.ClusterObjects import ClusterAttributeDescriptor, ClusterCommand from chip.clusters.Types import Nullable -from matter_server.common.helpers.util import create_attribute_path_from_attribute from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry @@ -70,11 +69,7 @@ class MatterAttributeSelectEntity(MatterEntity, SelectEntity): value_convert = self.entity_description.ha_to_native_value if TYPE_CHECKING: assert value_convert is not None - await self.matter_client.write_attribute( - node_id=self._endpoint.node.node_id, - attribute_path=create_attribute_path_from_attribute( - self._endpoint.endpoint_id, self._entity_info.primary_attribute - ), + await self.write_attribute( value=value_convert(option), ) @@ -101,10 +96,8 @@ class MatterModeSelectEntity(MatterAttributeSelectEntity): for mode in cluster.supportedModes: if mode.label != option: continue - await self.matter_client.send_device_command( - node_id=self._endpoint.node.node_id, - endpoint_id=self._endpoint.endpoint_id, - command=cluster.Commands.ChangeToMode(newMode=mode.mode), + await self.send_device_command( + cluster.Commands.ChangeToMode(newMode=mode.mode), ) break @@ -132,10 +125,8 @@ class MatterListSelectEntity(MatterEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" option_id = self._attr_options.index(option) - await self.matter_client.send_device_command( - node_id=self._endpoint.node.node_id, - endpoint_id=self._endpoint.endpoint_id, - command=self.entity_description.command(option_id), + await self.send_device_command( + self.entity_description.command(option_id), ) @callback diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index 2a1e6d59a06..890ca662295 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -7,7 +7,6 @@ from typing import Any from chip.clusters import Objects as clusters from matter_server.client.models import device_types -from matter_server.common.helpers.util import create_attribute_path_from_attribute from homeassistant.components.switch import ( SwitchDeviceClass, @@ -41,18 +40,14 @@ class MatterSwitch(MatterEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn switch on.""" - await self.matter_client.send_device_command( - node_id=self._endpoint.node.node_id, - endpoint_id=self._endpoint.endpoint_id, - command=clusters.OnOff.Commands.On(), + await self.send_device_command( + clusters.OnOff.Commands.On(), ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn switch off.""" - await self.matter_client.send_device_command( - node_id=self._endpoint.node.node_id, - endpoint_id=self._endpoint.endpoint_id, - command=clusters.OnOff.Commands.Off(), + await self.send_device_command( + clusters.OnOff.Commands.Off(), ) @callback @@ -77,15 +72,9 @@ class MatterNumericSwitch(MatterSwitch): async def _async_set_native_value(self, value: bool) -> None: """Update the current value.""" - matter_attribute = self._entity_info.primary_attribute if value_convert := self.entity_description.ha_to_native_value: send_value = value_convert(value) - await self.matter_client.write_attribute( - node_id=self._endpoint.node.node_id, - attribute_path=create_attribute_path_from_attribute( - self._endpoint.endpoint_id, - matter_attribute, - ), + await self.write_attribute( value=send_value, ) diff --git a/homeassistant/components/matter/vacuum.py b/homeassistant/components/matter/vacuum.py index e98e1ad0bbd..511b32d3182 100644 --- a/homeassistant/components/matter/vacuum.py +++ b/homeassistant/components/matter/vacuum.py @@ -69,15 +69,15 @@ class MatterVacuum(MatterEntity, StateVacuumEntity): async def async_stop(self, **kwargs: Any) -> None: """Stop the vacuum cleaner.""" - await self._send_device_command(clusters.OperationalState.Commands.Stop()) + await self.send_device_command(clusters.OperationalState.Commands.Stop()) async def async_return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock.""" - await self._send_device_command(clusters.RvcOperationalState.Commands.GoHome()) + await self.send_device_command(clusters.RvcOperationalState.Commands.GoHome()) async def async_locate(self, **kwargs: Any) -> None: """Locate the vacuum cleaner.""" - await self._send_device_command(clusters.Identify.Commands.Identify()) + await self.send_device_command(clusters.Identify.Commands.Identify()) async def async_start(self) -> None: """Start or resume the cleaning task.""" @@ -87,26 +87,15 @@ class MatterVacuum(MatterEntity, StateVacuumEntity): clusters.RvcOperationalState.Commands.Resume.command_id in self._last_accepted_commands ): - await self._send_device_command( + await self.send_device_command( clusters.RvcOperationalState.Commands.Resume() ) else: - await self._send_device_command(clusters.OperationalState.Commands.Start()) + await self.send_device_command(clusters.OperationalState.Commands.Start()) async def async_pause(self) -> None: """Pause the cleaning task.""" - await self._send_device_command(clusters.OperationalState.Commands.Pause()) - - async def _send_device_command( - self, - command: clusters.ClusterCommand, - ) -> None: - """Send a command to the device.""" - await self.matter_client.send_device_command( - node_id=self._endpoint.node.node_id, - endpoint_id=self._endpoint.endpoint_id, - command=command, - ) + await self.send_device_command(clusters.OperationalState.Commands.Pause()) @callback def _update_from_device(self) -> None: diff --git a/homeassistant/components/matter/valve.py b/homeassistant/components/matter/valve.py index ccb4e89da17..29946621853 100644 --- a/homeassistant/components/matter/valve.py +++ b/homeassistant/components/matter/valve.py @@ -42,17 +42,6 @@ class MatterValve(MatterEntity, ValveEntity): entity_description: ValveEntityDescription _platform_translation_key = "valve" - async def send_device_command( - self, - command: clusters.ClusterCommand, - ) -> None: - """Send a command to the device.""" - await self.matter_client.send_device_command( - node_id=self._endpoint.node.node_id, - endpoint_id=self._endpoint.endpoint_id, - command=command, - ) - async def async_open_valve(self) -> None: """Open the valve.""" await self.send_device_command(ValveConfigurationAndControl.Commands.Open()) diff --git a/tests/components/matter/test_number.py b/tests/components/matter/test_number.py index 86e1fbbf419..2a4eea1c324 100644 --- a/tests/components/matter/test_number.py +++ b/tests/components/matter/test_number.py @@ -4,12 +4,14 @@ from unittest.mock import MagicMock, call from matter_server.client.models.node import MatterNode from matter_server.common import custom_clusters +from matter_server.common.errors import MatterError from matter_server.common.helpers.util import create_attribute_path_from_attribute import pytest from syrupy import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from .common import ( @@ -97,3 +99,25 @@ async def test_eve_weather_sensor_altitude( ), value=500, ) + + +@pytest.mark.parametrize("node_fixture", ["dimmable_light"]) +async def test_matter_exception_on_write_attribute( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test if a MatterError gets converted to HomeAssistantError by using a dimmable_light fixture.""" + state = hass.states.get("number.mock_dimmable_light_on_level") + assert state + matter_client.write_attribute.side_effect = MatterError("Boom") + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "number", + "set_value", + { + "entity_id": "number.mock_dimmable_light_on_level", + "value": 500, + }, + blocking=True, + ) diff --git a/tests/components/matter/test_switch.py b/tests/components/matter/test_switch.py index 11451c715c3..e82848fcc3a 100644 --- a/tests/components/matter/test_switch.py +++ b/tests/components/matter/test_switch.py @@ -4,12 +4,14 @@ from unittest.mock import MagicMock, call from chip.clusters import Objects as clusters from matter_server.client.models.node import MatterNode +from matter_server.common.errors import MatterError from matter_server.common.helpers.util import create_attribute_path_from_attribute import pytest from syrupy import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from .common import ( @@ -165,3 +167,24 @@ async def test_numeric_switch( ), value=0, ) + + +@pytest.mark.parametrize("node_fixture", ["on_off_plugin_unit"]) +async def test_matter_exception_on_command( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test if a MatterError gets converted to HomeAssistantError by using a switch fixture.""" + state = hass.states.get("switch.mock_onoffpluginunit") + assert state + matter_client.send_device_command.side_effect = MatterError("Boom") + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "switch", + "turn_on", + { + "entity_id": "switch.mock_onoffpluginunit", + }, + blocking=True, + )