Catch and convert MatterError when sending device commands (#136635)

This commit is contained in:
Marcel van der Veldt 2025-01-27 19:36:16 +01:00 committed by GitHub
parent 3984565084
commit 557b9d88b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 170 additions and 176 deletions

View File

@ -49,11 +49,7 @@ class MatterCommandButton(MatterEntity, ButtonEntity):
"""Handle the button press leveraging a Matter command.""" """Handle the button press leveraging a Matter command."""
if TYPE_CHECKING: if TYPE_CHECKING:
assert self.entity_description.command is not None assert self.entity_description.command is not None
await self.matter_client.send_device_command( await self.send_device_command(self.entity_description.command())
node_id=self._endpoint.node.node_id,
endpoint_id=self._endpoint.endpoint_id,
command=self.entity_description.command(),
)
# Discovery schema(s) to map Matter Attributes to HA entities # Discovery schema(s) to map Matter Attributes to HA entities

View File

@ -212,57 +212,45 @@ class MatterClimate(MatterEntity, ClimateEntity):
matter_attribute = ( matter_attribute = (
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
) )
await self.matter_client.write_attribute( await self.write_attribute(
node_id=self._endpoint.node.node_id,
attribute_path=create_attribute_path_from_attribute(
self._endpoint.endpoint_id,
matter_attribute,
),
value=int(target_temperature * TEMPERATURE_SCALING_FACTOR), value=int(target_temperature * TEMPERATURE_SCALING_FACTOR),
matter_attribute=matter_attribute,
) )
return return
if target_temperature_low is not None: if target_temperature_low is not None:
# multi setpoint control - low setpoint (heat) # multi setpoint control - low setpoint (heat)
if self.target_temperature_low != target_temperature_low: if self.target_temperature_low != target_temperature_low:
await self.matter_client.write_attribute( await self.write_attribute(
node_id=self._endpoint.node.node_id,
attribute_path=create_attribute_path_from_attribute(
self._endpoint.endpoint_id,
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint,
),
value=int(target_temperature_low * TEMPERATURE_SCALING_FACTOR), value=int(target_temperature_low * TEMPERATURE_SCALING_FACTOR),
matter_attribute=clusters.Thermostat.Attributes.OccupiedHeatingSetpoint,
) )
if target_temperature_high is not None: if target_temperature_high is not None:
# multi setpoint control - high setpoint (cool) # multi setpoint control - high setpoint (cool)
if self.target_temperature_high != target_temperature_high: if self.target_temperature_high != target_temperature_high:
await self.matter_client.write_attribute( await self.write_attribute(
node_id=self._endpoint.node.node_id,
attribute_path=create_attribute_path_from_attribute(
self._endpoint.endpoint_id,
clusters.Thermostat.Attributes.OccupiedCoolingSetpoint,
),
value=int(target_temperature_high * TEMPERATURE_SCALING_FACTOR), 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: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode.""" """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) system_mode_value = HVAC_SYSTEM_MODE_MAP.get(hvac_mode)
if system_mode_value is None: if system_mode_value is None:
raise ValueError(f"Unsupported hvac mode {hvac_mode} in Matter") raise ValueError(f"Unsupported hvac mode {hvac_mode} in Matter")
await self.matter_client.write_attribute( await self.write_attribute(
node_id=self._endpoint.node.node_id,
attribute_path=system_mode_path,
value=system_mode_value, value=system_mode_value,
matter_attribute=clusters.Thermostat.Attributes.SystemMode,
) )
# we need to optimistically update the attribute's value here # we need to optimistically update the attribute's value here
# to prevent a race condition when adjusting the mode and temperature # to prevent a race condition when adjusting the mode and temperature
# in the same call # 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._endpoint.set_attribute_value(system_mode_path, system_mode_value)
self._update_from_device() self._update_from_device()

View File

@ -102,14 +102,6 @@ class MatterCover(MatterEntity, CoverEntity):
clusters.WindowCovering.Commands.GoToTiltPercentage((100 - position) * 100) 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 @callback
def _update_from_device(self) -> None: def _update_from_device(self) -> None:
"""Update from device.""" """Update from device."""

View File

@ -2,18 +2,24 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
import functools
import logging 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 import Objects as clusters
from chip.clusters.Objects import ClusterAttributeDescriptor, NullValue from chip.clusters.Objects import ClusterAttributeDescriptor, ClusterCommand, NullValue
from matter_server.common.helpers.util import create_attribute_path 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 matter_server.common.models import EventType, ServerInfoMessage
from propcache.api import cached_property from propcache.api import cached_property
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity import Entity, EntityDescription
import homeassistant.helpers.entity_registry as er import homeassistant.helpers.entity_registry as er
@ -31,6 +37,23 @@ if TYPE_CHECKING:
LOGGER = logging.getLogger(__name__) 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) @dataclass(frozen=True)
class MatterEntityDescription(EntityDescription): class MatterEntityDescription(EntityDescription):
"""Describe the Matter entity.""" """Describe the Matter entity."""
@ -218,3 +241,38 @@ class MatterEntity(Entity):
return create_attribute_path( return create_attribute_path(
self._endpoint.endpoint_id, attribute.cluster_id, attribute.attribute_id 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,
)

View File

@ -5,7 +5,6 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from chip.clusters import Objects as clusters from chip.clusters import Objects as clusters
from matter_server.common.helpers.util import create_attribute_path_from_attribute
from homeassistant.components.fan import ( from homeassistant.components.fan import (
DIRECTION_FORWARD, DIRECTION_FORWARD,
@ -97,24 +96,16 @@ class MatterFan(MatterEntity, FanEntity):
# clear the wind setting if its currently set # clear the wind setting if its currently set
if self._attr_preset_mode in [PRESET_NATURAL_WIND, PRESET_SLEEP_WIND]: if self._attr_preset_mode in [PRESET_NATURAL_WIND, PRESET_SLEEP_WIND]:
await self._set_wind_mode(None) await self._set_wind_mode(None)
await self.matter_client.write_attribute( await self.write_attribute(
node_id=self._endpoint.node.node_id,
attribute_path=create_attribute_path_from_attribute(
self._endpoint.endpoint_id,
clusters.FanControl.Attributes.FanMode,
),
value=clusters.FanControl.Enums.FanModeEnum.kOff, value=clusters.FanControl.Enums.FanModeEnum.kOff,
matter_attribute=clusters.FanControl.Attributes.FanMode,
) )
async def async_set_percentage(self, percentage: int) -> None: async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage.""" """Set the speed of the fan, as a percentage."""
await self.matter_client.write_attribute( await self.write_attribute(
node_id=self._endpoint.node.node_id,
attribute_path=create_attribute_path_from_attribute(
self._endpoint.endpoint_id,
clusters.FanControl.Attributes.PercentSetting,
),
value=percentage, value=percentage,
matter_attribute=clusters.FanControl.Attributes.PercentSetting,
) )
async def async_set_preset_mode(self, preset_mode: str) -> None: 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]: if self._attr_preset_mode in [PRESET_NATURAL_WIND, PRESET_SLEEP_WIND]:
await self._set_wind_mode(None) await self._set_wind_mode(None)
await self.matter_client.write_attribute( await self.write_attribute(
node_id=self._endpoint.node.node_id,
attribute_path=create_attribute_path_from_attribute(
self._endpoint.endpoint_id,
clusters.FanControl.Attributes.FanMode,
),
value=FAN_MODE_MAP[preset_mode], value=FAN_MODE_MAP[preset_mode],
matter_attribute=clusters.FanControl.Attributes.FanMode,
) )
async def async_oscillate(self, oscillating: bool) -> None: async def async_oscillate(self, oscillating: bool) -> None:
"""Oscillate the fan.""" """Oscillate the fan."""
await self.matter_client.write_attribute( await self.write_attribute(
node_id=self._endpoint.node.node_id, value=(
attribute_path=create_attribute_path_from_attribute( self.get_matter_attribute_value(
self._endpoint.endpoint_id,
clusters.FanControl.Attributes.RockSetting,
),
value=self.get_matter_attribute_value(
clusters.FanControl.Attributes.RockSupport clusters.FanControl.Attributes.RockSupport
) )
if oscillating if oscillating
else 0, else 0
),
matter_attribute=clusters.FanControl.Attributes.RockSetting,
) )
async def async_set_direction(self, direction: str) -> None: async def async_set_direction(self, direction: str) -> None:
"""Set the direction of the fan.""" """Set the direction of the fan."""
await self.matter_client.write_attribute( await self.write_attribute(
node_id=self._endpoint.node.node_id, value=(
attribute_path=create_attribute_path_from_attribute( clusters.FanControl.Enums.AirflowDirectionEnum.kReverse
self._endpoint.endpoint_id,
clusters.FanControl.Attributes.AirflowDirection,
),
value=clusters.FanControl.Enums.AirflowDirectionEnum.kReverse
if direction == DIRECTION_REVERSE if direction == DIRECTION_REVERSE
else clusters.FanControl.Enums.AirflowDirectionEnum.kForward, else clusters.FanControl.Enums.AirflowDirectionEnum.kForward
),
matter_attribute=clusters.FanControl.Attributes.AirflowDirection,
) )
async def _set_wind_mode(self, wind_mode: str | None) -> None: async def _set_wind_mode(self, wind_mode: str | None) -> None:
@ -173,13 +156,9 @@ class MatterFan(MatterEntity, FanEntity):
wind_setting = WindBitmap.kSleepWind wind_setting = WindBitmap.kSleepWind
else: else:
wind_setting = 0 wind_setting = 0
await self.matter_client.write_attribute( await self.write_attribute(
node_id=self._endpoint.node.node_id,
attribute_path=create_attribute_path_from_attribute(
self._endpoint.endpoint_id,
clusters.FanControl.Attributes.WindSetting,
),
value=wind_setting, value=wind_setting,
matter_attribute=clusters.FanControl.Attributes.WindSetting,
) )
@callback @callback

View File

@ -282,14 +282,6 @@ class MatterLight(MatterEntity, LightEntity):
return ha_color_mode 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: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn light on.""" """Turn light on."""

View File

@ -62,19 +62,6 @@ class MatterLock(MatterEntity, LockEntity):
return None 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: async def async_lock(self, **kwargs: Any) -> None:
"""Lock the lock with pin if needed.""" """Lock the lock with pin if needed."""
if not self._attr_is_locked: if not self._attr_is_locked:
@ -89,7 +76,8 @@ class MatterLock(MatterEntity, LockEntity):
code: str | None = kwargs.get(ATTR_CODE) code: str | None = kwargs.get(ATTR_CODE)
code_bytes = code.encode() if code else None code_bytes = code.encode() if code else None
await self.send_device_command( 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: 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 # the unlock command should unbolt only on the unlock command
# and unlatch on the HA 'open' command. # and unlatch on the HA 'open' command.
await self.send_device_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: else:
await self.send_device_command( 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: async def async_open(self, **kwargs: Any) -> None:
@ -130,7 +120,8 @@ class MatterLock(MatterEntity, LockEntity):
code: str | None = kwargs.get(ATTR_CODE) code: str | None = kwargs.get(ATTR_CODE)
code_bytes = code.encode() if code else None code_bytes = code.encode() if code else None
await self.send_device_command( 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 @callback

View File

@ -6,7 +6,6 @@ from dataclasses import dataclass
from chip.clusters import Objects as clusters from chip.clusters import Objects as clusters
from matter_server.common import custom_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 ( from homeassistant.components.number import (
NumberDeviceClass, NumberDeviceClass,
@ -52,16 +51,10 @@ class MatterNumber(MatterEntity, NumberEntity):
async def async_set_native_value(self, value: float) -> None: async def async_set_native_value(self, value: float) -> None:
"""Update the current value.""" """Update the current value."""
matter_attribute = self._entity_info.primary_attribute
sendvalue = int(value) sendvalue = int(value)
if value_convert := self.entity_description.ha_to_native_value: if value_convert := self.entity_description.ha_to_native_value:
sendvalue = value_convert(value) sendvalue = value_convert(value)
await self.matter_client.write_attribute( await self.write_attribute(
node_id=self._endpoint.node.node_id,
attribute_path=create_attribute_path_from_attribute(
self._endpoint.endpoint_id,
matter_attribute,
),
value=sendvalue, value=sendvalue,
) )

View File

@ -9,7 +9,6 @@ from typing import TYPE_CHECKING, 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 chip.clusters.Types import Nullable 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.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -70,11 +69,7 @@ class MatterAttributeSelectEntity(MatterEntity, SelectEntity):
value_convert = self.entity_description.ha_to_native_value value_convert = self.entity_description.ha_to_native_value
if TYPE_CHECKING: if TYPE_CHECKING:
assert value_convert is not None assert value_convert is not None
await self.matter_client.write_attribute( await self.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
),
value=value_convert(option), value=value_convert(option),
) )
@ -101,10 +96,8 @@ class MatterModeSelectEntity(MatterAttributeSelectEntity):
for mode in cluster.supportedModes: for mode in cluster.supportedModes:
if mode.label != option: if mode.label != option:
continue continue
await self.matter_client.send_device_command( await self.send_device_command(
node_id=self._endpoint.node.node_id, cluster.Commands.ChangeToMode(newMode=mode.mode),
endpoint_id=self._endpoint.endpoint_id,
command=cluster.Commands.ChangeToMode(newMode=mode.mode),
) )
break break
@ -132,10 +125,8 @@ class MatterListSelectEntity(MatterEntity, SelectEntity):
async def async_select_option(self, option: str) -> None: async def async_select_option(self, option: str) -> None:
"""Change the selected option.""" """Change the selected option."""
option_id = self._attr_options.index(option) option_id = self._attr_options.index(option)
await self.matter_client.send_device_command( await self.send_device_command(
node_id=self._endpoint.node.node_id, self.entity_description.command(option_id),
endpoint_id=self._endpoint.endpoint_id,
command=self.entity_description.command(option_id),
) )
@callback @callback

View File

@ -7,7 +7,6 @@ from typing import Any
from chip.clusters import Objects as clusters from chip.clusters import Objects as clusters
from matter_server.client.models import device_types 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 ( from homeassistant.components.switch import (
SwitchDeviceClass, SwitchDeviceClass,
@ -41,18 +40,14 @@ class MatterSwitch(MatterEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn switch on.""" """Turn switch on."""
await self.matter_client.send_device_command( await self.send_device_command(
node_id=self._endpoint.node.node_id, clusters.OnOff.Commands.On(),
endpoint_id=self._endpoint.endpoint_id,
command=clusters.OnOff.Commands.On(),
) )
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn switch off.""" """Turn switch off."""
await self.matter_client.send_device_command( await self.send_device_command(
node_id=self._endpoint.node.node_id, clusters.OnOff.Commands.Off(),
endpoint_id=self._endpoint.endpoint_id,
command=clusters.OnOff.Commands.Off(),
) )
@callback @callback
@ -77,15 +72,9 @@ class MatterNumericSwitch(MatterSwitch):
async def _async_set_native_value(self, value: bool) -> None: async def _async_set_native_value(self, value: bool) -> None:
"""Update the current value.""" """Update the current value."""
matter_attribute = self._entity_info.primary_attribute
if value_convert := self.entity_description.ha_to_native_value: if value_convert := self.entity_description.ha_to_native_value:
send_value = value_convert(value) send_value = value_convert(value)
await self.matter_client.write_attribute( await self.write_attribute(
node_id=self._endpoint.node.node_id,
attribute_path=create_attribute_path_from_attribute(
self._endpoint.endpoint_id,
matter_attribute,
),
value=send_value, value=send_value,
) )

View File

@ -69,15 +69,15 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
async def async_stop(self, **kwargs: Any) -> None: async def async_stop(self, **kwargs: Any) -> None:
"""Stop the vacuum cleaner.""" """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: async def async_return_to_base(self, **kwargs: Any) -> None:
"""Set the vacuum cleaner to return to the dock.""" """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: async def async_locate(self, **kwargs: Any) -> None:
"""Locate the vacuum cleaner.""" """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: async def async_start(self) -> None:
"""Start or resume the cleaning task.""" """Start or resume the cleaning task."""
@ -87,26 +87,15 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
clusters.RvcOperationalState.Commands.Resume.command_id clusters.RvcOperationalState.Commands.Resume.command_id
in self._last_accepted_commands in self._last_accepted_commands
): ):
await self._send_device_command( await self.send_device_command(
clusters.RvcOperationalState.Commands.Resume() clusters.RvcOperationalState.Commands.Resume()
) )
else: 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: async def async_pause(self) -> None:
"""Pause the cleaning task.""" """Pause the cleaning task."""
await self._send_device_command(clusters.OperationalState.Commands.Pause()) 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,
)
@callback @callback
def _update_from_device(self) -> None: def _update_from_device(self) -> None:

View File

@ -42,17 +42,6 @@ class MatterValve(MatterEntity, ValveEntity):
entity_description: ValveEntityDescription entity_description: ValveEntityDescription
_platform_translation_key = "valve" _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: async def async_open_valve(self) -> None:
"""Open the valve.""" """Open the valve."""
await self.send_device_command(ValveConfigurationAndControl.Commands.Open()) await self.send_device_command(ValveConfigurationAndControl.Commands.Open())

View File

@ -4,12 +4,14 @@ from unittest.mock import MagicMock, call
from matter_server.client.models.node import MatterNode from matter_server.client.models.node import MatterNode
from matter_server.common import custom_clusters 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 from matter_server.common.helpers.util import create_attribute_path_from_attribute
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .common import ( from .common import (
@ -97,3 +99,25 @@ async def test_eve_weather_sensor_altitude(
), ),
value=500, 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,
)

View File

@ -4,12 +4,14 @@ from unittest.mock import MagicMock, call
from chip.clusters import Objects as clusters from chip.clusters import Objects as clusters
from matter_server.client.models.node import MatterNode 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 from matter_server.common.helpers.util import create_attribute_path_from_attribute
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .common import ( from .common import (
@ -165,3 +167,24 @@ async def test_numeric_switch(
), ),
value=0, 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,
)