mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 01:37:08 +00:00
Fix sending commands to Matter vacuum (#147567)
This commit is contained in:
parent
f28d6582c6
commit
d523f85404
@ -62,14 +62,25 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
|||||||
|
|
||||||
_last_accepted_commands: list[int] | None = None
|
_last_accepted_commands: list[int] | None = None
|
||||||
_supported_run_modes: (
|
_supported_run_modes: (
|
||||||
dict[int, clusters.RvcCleanMode.Structs.ModeOptionStruct] | None
|
dict[int, clusters.RvcRunMode.Structs.ModeOptionStruct] | None
|
||||||
) = None
|
) = None
|
||||||
entity_description: StateVacuumEntityDescription
|
entity_description: StateVacuumEntityDescription
|
||||||
_platform_translation_key = "vacuum"
|
_platform_translation_key = "vacuum"
|
||||||
|
|
||||||
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())
|
# We simply set the RvcRunMode to the first runmode
|
||||||
|
# that has the idle tag to stop the vacuum cleaner.
|
||||||
|
# this is compatible with both Matter 1.2 and 1.3+ devices.
|
||||||
|
supported_run_modes = self._supported_run_modes or {}
|
||||||
|
for mode in supported_run_modes.values():
|
||||||
|
for tag in mode.modeTags:
|
||||||
|
if tag.value == ModeTag.IDLE:
|
||||||
|
# stop the vacuum by changing the run mode to idle
|
||||||
|
await self.send_device_command(
|
||||||
|
clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
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."""
|
||||||
@ -83,15 +94,30 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
|||||||
"""Start or resume the cleaning task."""
|
"""Start or resume the cleaning task."""
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
assert self._last_accepted_commands is not None
|
assert self._last_accepted_commands is not None
|
||||||
|
|
||||||
|
accepted_operational_commands = self._last_accepted_commands
|
||||||
if (
|
if (
|
||||||
clusters.RvcOperationalState.Commands.Resume.command_id
|
clusters.RvcOperationalState.Commands.Resume.command_id
|
||||||
in self._last_accepted_commands
|
in accepted_operational_commands
|
||||||
|
and self.state == VacuumActivity.PAUSED
|
||||||
):
|
):
|
||||||
|
# vacuum is paused and supports resume command
|
||||||
await self.send_device_command(
|
await self.send_device_command(
|
||||||
clusters.RvcOperationalState.Commands.Resume()
|
clusters.RvcOperationalState.Commands.Resume()
|
||||||
)
|
)
|
||||||
else:
|
return
|
||||||
await self.send_device_command(clusters.OperationalState.Commands.Start())
|
|
||||||
|
# We simply set the RvcRunMode to the first runmode
|
||||||
|
# that has the cleaning tag to start the vacuum cleaner.
|
||||||
|
# this is compatible with both Matter 1.2 and 1.3+ devices.
|
||||||
|
supported_run_modes = self._supported_run_modes or {}
|
||||||
|
for mode in supported_run_modes.values():
|
||||||
|
for tag in mode.modeTags:
|
||||||
|
if tag.value == ModeTag.CLEANING:
|
||||||
|
await self.send_device_command(
|
||||||
|
clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
async def async_pause(self) -> None:
|
async def async_pause(self) -> None:
|
||||||
"""Pause the cleaning task."""
|
"""Pause the cleaning task."""
|
||||||
@ -130,6 +156,8 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
|||||||
state = VacuumActivity.CLEANING
|
state = VacuumActivity.CLEANING
|
||||||
elif ModeTag.IDLE in tags:
|
elif ModeTag.IDLE in tags:
|
||||||
state = VacuumActivity.IDLE
|
state = VacuumActivity.IDLE
|
||||||
|
elif ModeTag.MAPPING in tags:
|
||||||
|
state = VacuumActivity.CLEANING
|
||||||
self._attr_activity = state
|
self._attr_activity = state
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -143,7 +171,10 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
|||||||
return
|
return
|
||||||
self._last_accepted_commands = accepted_operational_commands
|
self._last_accepted_commands = accepted_operational_commands
|
||||||
supported_features: VacuumEntityFeature = VacuumEntityFeature(0)
|
supported_features: VacuumEntityFeature = VacuumEntityFeature(0)
|
||||||
|
supported_features |= VacuumEntityFeature.START
|
||||||
supported_features |= VacuumEntityFeature.STATE
|
supported_features |= VacuumEntityFeature.STATE
|
||||||
|
supported_features |= VacuumEntityFeature.STOP
|
||||||
|
|
||||||
# optional battery attribute = battery feature
|
# optional battery attribute = battery feature
|
||||||
if self.get_matter_attribute_value(
|
if self.get_matter_attribute_value(
|
||||||
clusters.PowerSource.Attributes.BatPercentRemaining
|
clusters.PowerSource.Attributes.BatPercentRemaining
|
||||||
@ -153,7 +184,7 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
|||||||
if self.get_matter_attribute_value(clusters.Identify.Attributes.IdentifyType):
|
if self.get_matter_attribute_value(clusters.Identify.Attributes.IdentifyType):
|
||||||
supported_features |= VacuumEntityFeature.LOCATE
|
supported_features |= VacuumEntityFeature.LOCATE
|
||||||
# create a map of supported run modes
|
# create a map of supported run modes
|
||||||
run_modes: list[clusters.RvcCleanMode.Structs.ModeOptionStruct] = (
|
run_modes: list[clusters.RvcRunMode.Structs.ModeOptionStruct] = (
|
||||||
self.get_matter_attribute_value(
|
self.get_matter_attribute_value(
|
||||||
clusters.RvcRunMode.Attributes.SupportedModes
|
clusters.RvcRunMode.Attributes.SupportedModes
|
||||||
)
|
)
|
||||||
@ -165,22 +196,6 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
|||||||
in accepted_operational_commands
|
in accepted_operational_commands
|
||||||
):
|
):
|
||||||
supported_features |= VacuumEntityFeature.PAUSE
|
supported_features |= VacuumEntityFeature.PAUSE
|
||||||
if (
|
|
||||||
clusters.OperationalState.Commands.Stop.command_id
|
|
||||||
in accepted_operational_commands
|
|
||||||
):
|
|
||||||
supported_features |= VacuumEntityFeature.STOP
|
|
||||||
if (
|
|
||||||
clusters.OperationalState.Commands.Start.command_id
|
|
||||||
in accepted_operational_commands
|
|
||||||
):
|
|
||||||
# note that start has been replaced by resume in rev2 of the spec
|
|
||||||
supported_features |= VacuumEntityFeature.START
|
|
||||||
if (
|
|
||||||
clusters.RvcOperationalState.Commands.Resume.command_id
|
|
||||||
in accepted_operational_commands
|
|
||||||
):
|
|
||||||
supported_features |= VacuumEntityFeature.START
|
|
||||||
if (
|
if (
|
||||||
clusters.RvcOperationalState.Commands.GoHome.command_id
|
clusters.RvcOperationalState.Commands.GoHome.command_id
|
||||||
in accepted_operational_commands
|
in accepted_operational_commands
|
||||||
@ -202,10 +217,7 @@ DISCOVERY_SCHEMAS = [
|
|||||||
clusters.RvcRunMode.Attributes.CurrentMode,
|
clusters.RvcRunMode.Attributes.CurrentMode,
|
||||||
clusters.RvcOperationalState.Attributes.OperationalState,
|
clusters.RvcOperationalState.Attributes.OperationalState,
|
||||||
),
|
),
|
||||||
optional_attributes=(
|
optional_attributes=(clusters.PowerSource.Attributes.BatPercentRemaining,),
|
||||||
clusters.RvcCleanMode.Attributes.CurrentMode,
|
|
||||||
clusters.PowerSource.Attributes.BatPercentRemaining,
|
|
||||||
),
|
|
||||||
device_type=(device_types.RoboticVacuumCleaner,),
|
device_type=(device_types.RoboticVacuumCleaner,),
|
||||||
allow_none_value=True,
|
allow_none_value=True,
|
||||||
),
|
),
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
'platform': 'matter',
|
'platform': 'matter',
|
||||||
'previous_unique_id': None,
|
'previous_unique_id': None,
|
||||||
'suggested_object_id': None,
|
'suggested_object_id': None,
|
||||||
'supported_features': <VacuumEntityFeature: 12308>,
|
'supported_features': <VacuumEntityFeature: 12316>,
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
'unique_id': '00000000000004D2-0000000000000042-MatterNodeDevice-1-MatterVacuumCleaner-84-1',
|
'unique_id': '00000000000004D2-0000000000000042-MatterNodeDevice-1-MatterVacuumCleaner-84-1',
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
@ -38,7 +38,7 @@
|
|||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
'friendly_name': 'Mock Vacuum',
|
'friendly_name': 'Mock Vacuum',
|
||||||
'supported_features': <VacuumEntityFeature: 12308>,
|
'supported_features': <VacuumEntityFeature: 12316>,
|
||||||
}),
|
}),
|
||||||
'context': <ANY>,
|
'context': <ANY>,
|
||||||
'entity_id': 'vacuum.mock_vacuum',
|
'entity_id': 'vacuum.mock_vacuum',
|
||||||
|
@ -9,7 +9,6 @@ from syrupy.assertion 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 ServiceNotSupported
|
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
@ -61,7 +60,29 @@ async def test_vacuum_actions(
|
|||||||
)
|
)
|
||||||
matter_client.send_device_command.reset_mock()
|
matter_client.send_device_command.reset_mock()
|
||||||
|
|
||||||
# test start/resume action
|
# test start action (from idle state)
|
||||||
|
await hass.services.async_call(
|
||||||
|
"vacuum",
|
||||||
|
"start",
|
||||||
|
{
|
||||||
|
"entity_id": entity_id,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matter_client.send_device_command.call_count == 1
|
||||||
|
assert matter_client.send_device_command.call_args == call(
|
||||||
|
node_id=matter_node.node_id,
|
||||||
|
endpoint_id=1,
|
||||||
|
command=clusters.RvcRunMode.Commands.ChangeToMode(newMode=1),
|
||||||
|
)
|
||||||
|
matter_client.send_device_command.reset_mock()
|
||||||
|
|
||||||
|
# test resume action (from paused state)
|
||||||
|
# first set the operational state to paused
|
||||||
|
set_node_attribute(matter_node, 1, 97, 4, 0x02)
|
||||||
|
await trigger_subscription_callback(hass, matter_client)
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"vacuum",
|
"vacuum",
|
||||||
"start",
|
"start",
|
||||||
@ -98,25 +119,6 @@ async def test_vacuum_actions(
|
|||||||
matter_client.send_device_command.reset_mock()
|
matter_client.send_device_command.reset_mock()
|
||||||
|
|
||||||
# test stop action
|
# test stop action
|
||||||
# stop command is not supported by the vacuum fixture
|
|
||||||
with pytest.raises(
|
|
||||||
ServiceNotSupported,
|
|
||||||
match="Entity vacuum.mock_vacuum does not support action vacuum.stop",
|
|
||||||
):
|
|
||||||
await hass.services.async_call(
|
|
||||||
"vacuum",
|
|
||||||
"stop",
|
|
||||||
{
|
|
||||||
"entity_id": entity_id,
|
|
||||||
},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# update accepted command list to add support for stop command
|
|
||||||
set_node_attribute(
|
|
||||||
matter_node, 1, 97, 65529, [clusters.OperationalState.Commands.Stop.command_id]
|
|
||||||
)
|
|
||||||
await trigger_subscription_callback(hass, matter_client)
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"vacuum",
|
"vacuum",
|
||||||
"stop",
|
"stop",
|
||||||
@ -129,7 +131,7 @@ async def test_vacuum_actions(
|
|||||||
assert matter_client.send_device_command.call_args == call(
|
assert matter_client.send_device_command.call_args == call(
|
||||||
node_id=matter_node.node_id,
|
node_id=matter_node.node_id,
|
||||||
endpoint_id=1,
|
endpoint_id=1,
|
||||||
command=clusters.OperationalState.Commands.Stop(),
|
command=clusters.RvcRunMode.Commands.ChangeToMode(newMode=0),
|
||||||
)
|
)
|
||||||
matter_client.send_device_command.reset_mock()
|
matter_client.send_device_command.reset_mock()
|
||||||
|
|
||||||
@ -209,11 +211,21 @@ async def test_vacuum_updates(
|
|||||||
assert state
|
assert state
|
||||||
assert state.state == "idle"
|
assert state.state == "idle"
|
||||||
|
|
||||||
|
# confirm state is 'cleaning' by setting;
|
||||||
|
# - the operational state to 0x00
|
||||||
|
# - the run mode is set to a mode which has mapping tag
|
||||||
|
set_node_attribute(matter_node, 1, 97, 4, 0)
|
||||||
|
set_node_attribute(matter_node, 1, 84, 1, 2)
|
||||||
|
await trigger_subscription_callback(hass, matter_client)
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == "cleaning"
|
||||||
|
|
||||||
# confirm state is 'unknown' by setting;
|
# confirm state is 'unknown' by setting;
|
||||||
# - the operational state to 0x00
|
# - the operational state to 0x00
|
||||||
# - the run mode is set to a mode which has neither cleaning or idle tag
|
# - the run mode is set to a mode which has neither cleaning or idle tag
|
||||||
set_node_attribute(matter_node, 1, 97, 4, 0)
|
set_node_attribute(matter_node, 1, 97, 4, 0)
|
||||||
set_node_attribute(matter_node, 1, 84, 1, 2)
|
set_node_attribute(matter_node, 1, 84, 1, 5)
|
||||||
await trigger_subscription_callback(hass, matter_client)
|
await trigger_subscription_callback(hass, matter_client)
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
assert state
|
assert state
|
||||||
|
Loading…
x
Reference in New Issue
Block a user