Add support for vacuum cleaners to the Matter integration (#129420)
parent
cce925c06c
commit
cbb8d76da7
|
@ -24,6 +24,7 @@ from .select import DISCOVERY_SCHEMAS as SELECT_SCHEMAS
|
|||
from .sensor import DISCOVERY_SCHEMAS as SENSOR_SCHEMAS
|
||||
from .switch import DISCOVERY_SCHEMAS as SWITCH_SCHEMAS
|
||||
from .update import DISCOVERY_SCHEMAS as UPDATE_SCHEMAS
|
||||
from .vacuum import DISCOVERY_SCHEMAS as VACUUM_SCHEMAS
|
||||
from .valve import DISCOVERY_SCHEMAS as VALVE_SCHEMAS
|
||||
|
||||
DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = {
|
||||
|
@ -40,6 +41,7 @@ DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = {
|
|||
Platform.SENSOR: SENSOR_SCHEMAS,
|
||||
Platform.SWITCH: SWITCH_SCHEMAS,
|
||||
Platform.UPDATE: UPDATE_SCHEMAS,
|
||||
Platform.VACUUM: VACUUM_SCHEMAS,
|
||||
Platform.VALVE: VALVE_SCHEMAS,
|
||||
}
|
||||
SUPPORTED_PLATFORMS = tuple(DISCOVERY_SCHEMAS)
|
||||
|
|
|
@ -162,23 +162,11 @@ DISCOVERY_SCHEMAS = [
|
|||
clusters.RefrigeratorAndTemperatureControlledCabinetMode.Attributes.SupportedModes,
|
||||
),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SELECT,
|
||||
entity_description=MatterSelectEntityDescription(
|
||||
key="MatterRvcRunMode",
|
||||
translation_key="mode",
|
||||
),
|
||||
entity_class=MatterModeSelectEntity,
|
||||
required_attributes=(
|
||||
clusters.RvcRunMode.Attributes.CurrentMode,
|
||||
clusters.RvcRunMode.Attributes.SupportedModes,
|
||||
),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SELECT,
|
||||
entity_description=MatterSelectEntityDescription(
|
||||
key="MatterRvcCleanMode",
|
||||
translation_key="mode",
|
||||
translation_key="clean_mode",
|
||||
),
|
||||
entity_class=MatterModeSelectEntity,
|
||||
required_attributes=(
|
||||
|
|
|
@ -174,6 +174,9 @@
|
|||
}
|
||||
},
|
||||
"select": {
|
||||
"clean_mode": {
|
||||
"name": "Clean mode"
|
||||
},
|
||||
"mode": {
|
||||
"name": "Mode"
|
||||
},
|
||||
|
@ -252,6 +255,11 @@
|
|||
"name": "Power"
|
||||
}
|
||||
},
|
||||
"vacuum": {
|
||||
"vacuum": {
|
||||
"name": "[%key:component::vacuum::title%]"
|
||||
}
|
||||
},
|
||||
"valve": {
|
||||
"valve": {
|
||||
"name": "[%key:component::valve::title%]"
|
||||
|
|
|
@ -0,0 +1,226 @@
|
|||
"""Matter vacuum platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import IntEnum
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
from matter_server.client.models import device_types
|
||||
|
||||
from homeassistant.components.vacuum import (
|
||||
STATE_CLEANING,
|
||||
STATE_DOCKED,
|
||||
STATE_ERROR,
|
||||
STATE_RETURNING,
|
||||
StateVacuumEntity,
|
||||
StateVacuumEntityDescription,
|
||||
VacuumEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_IDLE, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .entity import MatterEntity
|
||||
from .helpers import get_matter
|
||||
from .models import MatterDiscoverySchema
|
||||
|
||||
|
||||
class OperationalState(IntEnum):
|
||||
"""Operational State of the vacuum cleaner.
|
||||
|
||||
Combination of generic OperationalState and RvcOperationalState.
|
||||
"""
|
||||
|
||||
NO_ERROR = 0x00
|
||||
UNABLE_TO_START_OR_RESUME = 0x01
|
||||
UNABLE_TO_COMPLETE_OPERATION = 0x02
|
||||
COMMAND_INVALID_IN_STATE = 0x03
|
||||
SEEKING_CHARGER = 0x40
|
||||
CHARGING = 0x41
|
||||
DOCKED = 0x42
|
||||
|
||||
|
||||
class ModeTag(IntEnum):
|
||||
"""Enum with available ModeTag values."""
|
||||
|
||||
IDLE = 0x4000 # 16384 decimal
|
||||
CLEANING = 0x4001 # 16385 decimal
|
||||
MAPPING = 0x4002 # 16386 decimal
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Matter vacuum platform from Config Entry."""
|
||||
matter = get_matter(hass)
|
||||
matter.register_platform_handler(Platform.VACUUM, async_add_entities)
|
||||
|
||||
|
||||
class MatterVacuum(MatterEntity, StateVacuumEntity):
|
||||
"""Representation of a Matter Vacuum cleaner entity."""
|
||||
|
||||
_last_accepted_commands: list[int] | None = None
|
||||
_supported_run_modes: (
|
||||
dict[int, clusters.RvcCleanMode.Structs.ModeOptionStruct] | None
|
||||
) = None
|
||||
entity_description: StateVacuumEntityDescription
|
||||
_platform_translation_key = "vacuum"
|
||||
|
||||
async def async_stop(self, **kwargs: Any) -> None:
|
||||
"""Stop the vacuum cleaner."""
|
||||
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())
|
||||
|
||||
async def async_locate(self, **kwargs: Any) -> None:
|
||||
"""Locate the vacuum cleaner."""
|
||||
await self._send_device_command(clusters.Identify.Commands.Identify())
|
||||
|
||||
async def async_start(self) -> None:
|
||||
"""Start or resume the cleaning task."""
|
||||
if TYPE_CHECKING:
|
||||
assert self._last_accepted_commands is not None
|
||||
if (
|
||||
clusters.RvcOperationalState.Commands.Resume.command_id
|
||||
in self._last_accepted_commands
|
||||
):
|
||||
await self._send_device_command(
|
||||
clusters.RvcOperationalState.Commands.Resume()
|
||||
)
|
||||
else:
|
||||
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,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
self._calculate_features()
|
||||
# optional battery level
|
||||
if VacuumEntityFeature.BATTERY & self._attr_supported_features:
|
||||
self._attr_battery_level = self.get_matter_attribute_value(
|
||||
clusters.PowerSource.Attributes.BatPercentRemaining
|
||||
)
|
||||
# derive state from the run mode + operational state
|
||||
run_mode_raw: int = self.get_matter_attribute_value(
|
||||
clusters.RvcRunMode.Attributes.CurrentMode
|
||||
)
|
||||
operational_state: int = self.get_matter_attribute_value(
|
||||
clusters.RvcOperationalState.Attributes.OperationalState
|
||||
)
|
||||
state: str | None = None
|
||||
if TYPE_CHECKING:
|
||||
assert self._supported_run_modes is not None
|
||||
if operational_state in (OperationalState.CHARGING, OperationalState.DOCKED):
|
||||
state = STATE_DOCKED
|
||||
elif operational_state == OperationalState.SEEKING_CHARGER:
|
||||
state = STATE_RETURNING
|
||||
elif operational_state in (
|
||||
OperationalState.UNABLE_TO_COMPLETE_OPERATION,
|
||||
OperationalState.UNABLE_TO_START_OR_RESUME,
|
||||
):
|
||||
state = STATE_ERROR
|
||||
elif (run_mode := self._supported_run_modes.get(run_mode_raw)) is not None:
|
||||
tags = {x.value for x in run_mode.modeTags}
|
||||
if ModeTag.CLEANING in tags:
|
||||
state = STATE_CLEANING
|
||||
elif ModeTag.IDLE in tags:
|
||||
state = STATE_IDLE
|
||||
self._attr_state = state
|
||||
|
||||
@callback
|
||||
def _calculate_features(self) -> None:
|
||||
"""Calculate features for HA Vacuum platform."""
|
||||
accepted_operational_commands: list[int] = self.get_matter_attribute_value(
|
||||
clusters.RvcOperationalState.Attributes.AcceptedCommandList
|
||||
)
|
||||
# in principle the feature set should not change, except for the accepted commands
|
||||
if self._last_accepted_commands == accepted_operational_commands:
|
||||
return
|
||||
self._last_accepted_commands = accepted_operational_commands
|
||||
supported_features: VacuumEntityFeature = VacuumEntityFeature(0)
|
||||
supported_features |= VacuumEntityFeature.STATE
|
||||
# optional battery attribute = battery feature
|
||||
if self.get_matter_attribute_value(
|
||||
clusters.PowerSource.Attributes.BatPercentRemaining
|
||||
):
|
||||
supported_features |= VacuumEntityFeature.BATTERY
|
||||
# optional identify cluster = locate feature (value must be not None or 0)
|
||||
if self.get_matter_attribute_value(clusters.Identify.Attributes.IdentifyType):
|
||||
supported_features |= VacuumEntityFeature.LOCATE
|
||||
# create a map of supported run modes
|
||||
run_modes: list[clusters.RvcCleanMode.Structs.ModeOptionStruct] = (
|
||||
self.get_matter_attribute_value(
|
||||
clusters.RvcRunMode.Attributes.SupportedModes
|
||||
)
|
||||
)
|
||||
self._supported_run_modes = {mode.mode: mode for mode in run_modes}
|
||||
# map operational state commands to vacuum features
|
||||
if (
|
||||
clusters.RvcOperationalState.Commands.Pause.command_id
|
||||
in accepted_operational_commands
|
||||
):
|
||||
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 (
|
||||
clusters.RvcOperationalState.Commands.GoHome.command_id
|
||||
in accepted_operational_commands
|
||||
):
|
||||
supported_features |= VacuumEntityFeature.RETURN_HOME
|
||||
|
||||
self._attr_supported_features = supported_features
|
||||
|
||||
|
||||
# Discovery schema(s) to map Matter Attributes to HA entities
|
||||
DISCOVERY_SCHEMAS = [
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.VACUUM,
|
||||
entity_description=StateVacuumEntityDescription(
|
||||
key="MatterVacuumCleaner", name=None
|
||||
),
|
||||
entity_class=MatterVacuum,
|
||||
required_attributes=(
|
||||
clusters.RvcRunMode.Attributes.CurrentMode,
|
||||
clusters.RvcOperationalState.Attributes.CurrentPhase,
|
||||
),
|
||||
optional_attributes=(
|
||||
clusters.RvcCleanMode.Attributes.CurrentMode,
|
||||
clusters.PowerSource.Attributes.BatPercentRemaining,
|
||||
),
|
||||
device_type=(device_types.RoboticVacuumCleaner,),
|
||||
),
|
||||
]
|
|
@ -108,6 +108,7 @@ async def integration_fixture(
|
|||
"switch_unit",
|
||||
"temperature_sensor",
|
||||
"thermostat",
|
||||
"vacuum_cleaner",
|
||||
"valve",
|
||||
"window_covering_full",
|
||||
"window_covering_lift",
|
||||
|
|
|
@ -0,0 +1,309 @@
|
|||
{
|
||||
"node_id": 66,
|
||||
"date_commissioned": "2024-10-29T08:27:39.860951",
|
||||
"last_interview": "2024-10-29T08:27:39.860959",
|
||||
"interview_version": 6,
|
||||
"available": true,
|
||||
"is_bridge": false,
|
||||
"attributes": {
|
||||
"0/29/0": [
|
||||
{
|
||||
"0": 22,
|
||||
"1": 1
|
||||
}
|
||||
],
|
||||
"0/29/1": [29, 31, 40, 48, 49, 50, 51, 60, 62, 63],
|
||||
"0/29/2": [],
|
||||
"0/29/3": [1],
|
||||
"0/29/65532": 0,
|
||||
"0/29/65533": 2,
|
||||
"0/29/65528": [],
|
||||
"0/29/65529": [],
|
||||
"0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/31/0": [
|
||||
{
|
||||
"1": 5,
|
||||
"2": 2,
|
||||
"3": [112233],
|
||||
"4": null,
|
||||
"254": 1
|
||||
}
|
||||
],
|
||||
"0/31/1": [],
|
||||
"0/31/2": 4,
|
||||
"0/31/3": 3,
|
||||
"0/31/4": 4,
|
||||
"0/31/65532": 0,
|
||||
"0/31/65533": 1,
|
||||
"0/31/65528": [],
|
||||
"0/31/65529": [],
|
||||
"0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/40/0": 17,
|
||||
"0/40/1": "TEST_VENDOR",
|
||||
"0/40/2": 65521,
|
||||
"0/40/3": "Mock Vacuum",
|
||||
"0/40/4": 32769,
|
||||
"0/40/5": "Mock Vacuum",
|
||||
"0/40/6": "**REDACTED**",
|
||||
"0/40/7": 0,
|
||||
"0/40/8": "TEST_VERSION",
|
||||
"0/40/9": 1,
|
||||
"0/40/10": "1.0",
|
||||
"0/40/11": "20200101",
|
||||
"0/40/12": "",
|
||||
"0/40/13": "",
|
||||
"0/40/14": "",
|
||||
"0/40/15": "TEST_SN",
|
||||
"0/40/16": false,
|
||||
"0/40/18": "F0D59DFAAEAD6E76",
|
||||
"0/40/19": {
|
||||
"0": 3,
|
||||
"1": 65535
|
||||
},
|
||||
"0/40/21": 16973824,
|
||||
"0/40/22": 1,
|
||||
"0/40/65532": 0,
|
||||
"0/40/65533": 3,
|
||||
"0/40/65528": [],
|
||||
"0/40/65529": [],
|
||||
"0/40/65531": [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 21, 22,
|
||||
65528, 65529, 65531, 65532, 65533
|
||||
],
|
||||
"0/48/0": 0,
|
||||
"0/48/1": {
|
||||
"0": 60,
|
||||
"1": 900
|
||||
},
|
||||
"0/48/2": 0,
|
||||
"0/48/3": 2,
|
||||
"0/48/4": true,
|
||||
"0/48/65532": 0,
|
||||
"0/48/65533": 1,
|
||||
"0/48/65528": [1, 3, 5],
|
||||
"0/48/65529": [0, 2, 4],
|
||||
"0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/49/0": 1,
|
||||
"0/49/1": [
|
||||
{
|
||||
"0": "ZW5kMA==",
|
||||
"1": true
|
||||
}
|
||||
],
|
||||
"0/49/2": 0,
|
||||
"0/49/3": 0,
|
||||
"0/49/4": true,
|
||||
"0/49/5": null,
|
||||
"0/49/6": null,
|
||||
"0/49/7": null,
|
||||
"0/49/65532": 4,
|
||||
"0/49/65533": 2,
|
||||
"0/49/65528": [],
|
||||
"0/49/65529": [],
|
||||
"0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/50/65532": 0,
|
||||
"0/50/65533": 1,
|
||||
"0/50/65528": [1],
|
||||
"0/50/65529": [0],
|
||||
"0/50/65531": [65528, 65529, 65531, 65532, 65533],
|
||||
"0/51/0": [],
|
||||
"0/51/1": 1,
|
||||
"0/51/2": 47,
|
||||
"0/51/3": 0,
|
||||
"0/51/4": 0,
|
||||
"0/51/5": [],
|
||||
"0/51/6": [],
|
||||
"0/51/7": [],
|
||||
"0/51/8": false,
|
||||
"0/51/65532": 0,
|
||||
"0/51/65533": 2,
|
||||
"0/51/65528": [2],
|
||||
"0/51/65529": [0, 1],
|
||||
"0/51/65531": [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533
|
||||
],
|
||||
"0/60/0": 0,
|
||||
"0/60/1": null,
|
||||
"0/60/2": null,
|
||||
"0/60/65532": 0,
|
||||
"0/60/65533": 1,
|
||||
"0/60/65528": [],
|
||||
"0/60/65529": [0, 2],
|
||||
"0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/62/0": [],
|
||||
"0/62/1": [],
|
||||
"0/62/2": 16,
|
||||
"0/62/3": 1,
|
||||
"0/62/4": [],
|
||||
"0/62/5": 1,
|
||||
"0/62/65532": 0,
|
||||
"0/62/65533": 1,
|
||||
"0/62/65528": [1, 3, 5, 8],
|
||||
"0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11],
|
||||
"0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/63/0": [],
|
||||
"0/63/1": [],
|
||||
"0/63/2": 4,
|
||||
"0/63/3": 3,
|
||||
"0/63/65532": 0,
|
||||
"0/63/65533": 2,
|
||||
"0/63/65528": [2, 5],
|
||||
"0/63/65529": [0, 1, 3, 4],
|
||||
"0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/29/0": [
|
||||
{
|
||||
"0": 116,
|
||||
"1": 1
|
||||
}
|
||||
],
|
||||
"1/29/1": [3, 29, 84, 85, 97],
|
||||
"1/29/2": [],
|
||||
"1/29/3": [],
|
||||
"1/29/65532": 0,
|
||||
"1/29/65533": 2,
|
||||
"1/29/65528": [],
|
||||
"1/29/65529": [],
|
||||
"1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/84/0": [
|
||||
{
|
||||
"0": "Idle",
|
||||
"1": 0,
|
||||
"2": [
|
||||
{
|
||||
"1": 16384
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"0": "Cleaning",
|
||||
"1": 1,
|
||||
"2": [
|
||||
{
|
||||
"1": 16385
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"0": "Mapping",
|
||||
"1": 2,
|
||||
"2": [
|
||||
{
|
||||
"1": 16386
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"1/84/1": 0,
|
||||
"1/84/65532": 0,
|
||||
"1/84/65533": 2,
|
||||
"1/84/65528": [1],
|
||||
"1/84/65529": [0],
|
||||
"1/84/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/85/0": [
|
||||
{
|
||||
"0": "Quick",
|
||||
"1": 0,
|
||||
"2": [
|
||||
{
|
||||
"1": 16385
|
||||
},
|
||||
{
|
||||
"1": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"0": "Auto",
|
||||
"1": 1,
|
||||
"2": [
|
||||
{
|
||||
"1": 0
|
||||
},
|
||||
{
|
||||
"1": 16385
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"0": "Deep Clean",
|
||||
"1": 2,
|
||||
"2": [
|
||||
{
|
||||
"1": 16386
|
||||
},
|
||||
{
|
||||
"1": 16384
|
||||
},
|
||||
{
|
||||
"1": 16385
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"0": "Quiet",
|
||||
"1": 3,
|
||||
"2": [
|
||||
{
|
||||
"1": 2
|
||||
},
|
||||
{
|
||||
"1": 16385
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"0": "Max Vac",
|
||||
"1": 4,
|
||||
"2": [
|
||||
{
|
||||
"1": 16385
|
||||
},
|
||||
{
|
||||
"1": 16384
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"1/85/1": 0,
|
||||
"1/85/65532": 0,
|
||||
"1/85/65533": 2,
|
||||
"1/85/65528": [1],
|
||||
"1/85/65529": [0],
|
||||
"1/85/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/97/0": null,
|
||||
"1/97/1": null,
|
||||
"1/97/3": [
|
||||
{
|
||||
"0": 0
|
||||
},
|
||||
{
|
||||
"0": 1
|
||||
},
|
||||
{
|
||||
"0": 2
|
||||
},
|
||||
{
|
||||
"0": 3
|
||||
},
|
||||
{
|
||||
"0": 64
|
||||
},
|
||||
{
|
||||
"0": 65
|
||||
},
|
||||
{
|
||||
"0": 66
|
||||
}
|
||||
],
|
||||
"1/97/4": 0,
|
||||
"1/97/5": {
|
||||
"0": 0
|
||||
},
|
||||
"1/97/65532": 0,
|
||||
"1/97/65533": 1,
|
||||
"1/97/65528": [4],
|
||||
"1/97/65529": [0, 3, 128],
|
||||
"1/97/65531": [0, 1, 3, 4, 5, 65528, 65529, 65531, 65532, 65533]
|
||||
},
|
||||
"attribute_subscriptions": []
|
||||
}
|
|
@ -1573,3 +1573,64 @@
|
|||
'state': 'previous',
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[vacuum_cleaner][select.mock_vacuum_clean_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'Quick',
|
||||
'Auto',
|
||||
'Deep Clean',
|
||||
'Quiet',
|
||||
'Max Vac',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.mock_vacuum_clean_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': 'Clean mode',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'clean_mode',
|
||||
'unique_id': '00000000000004D2-0000000000000042-MatterNodeDevice-1-MatterRvcCleanMode-85-1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_selects[vacuum_cleaner][select.mock_vacuum_clean_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Vacuum Clean mode',
|
||||
'options': list([
|
||||
'Quick',
|
||||
'Auto',
|
||||
'Deep Clean',
|
||||
'Quiet',
|
||||
'Max Vac',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.mock_vacuum_clean_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Quick',
|
||||
})
|
||||
# ---
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# serializer version: 1
|
||||
# name: test_vacuum[vacuum_cleaner][vacuum.mock_vacuum-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'vacuum',
|
||||
'entity_category': None,
|
||||
'entity_id': 'vacuum.mock_vacuum',
|
||||
'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': None,
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <VacuumEntityFeature: 12308>,
|
||||
'translation_key': None,
|
||||
'unique_id': '00000000000004D2-0000000000000042-MatterNodeDevice-1-MatterVacuumCleaner-84-1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_vacuum[vacuum_cleaner][vacuum.mock_vacuum-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Vacuum',
|
||||
'supported_features': <VacuumEntityFeature: 12308>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'vacuum.mock_vacuum',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'idle',
|
||||
})
|
||||
# ---
|
|
@ -0,0 +1,209 @@
|
|||
"""Test Matter vacuum."""
|
||||
|
||||
from unittest.mock import MagicMock, call
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
from matter_server.client.models.node import MatterNode
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .common import (
|
||||
set_node_attribute,
|
||||
snapshot_matter_entities,
|
||||
trigger_subscription_callback,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("matter_devices")
|
||||
async def test_vacuum(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test that the correct entities get created for a vacuum device."""
|
||||
snapshot_matter_entities(hass, entity_registry, snapshot, Platform.VACUUM)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["vacuum_cleaner"])
|
||||
async def test_vacuum_actions(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test vacuum entity actions."""
|
||||
entity_id = "vacuum.mock_vacuum"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
|
||||
# test return_to_base action
|
||||
await hass.services.async_call(
|
||||
"vacuum",
|
||||
"return_to_base",
|
||||
{
|
||||
"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.RvcOperationalState.Commands.GoHome(),
|
||||
)
|
||||
matter_client.send_device_command.reset_mock()
|
||||
|
||||
# test start/resume action
|
||||
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.RvcOperationalState.Commands.Resume(),
|
||||
)
|
||||
matter_client.send_device_command.reset_mock()
|
||||
|
||||
# test pause action
|
||||
await hass.services.async_call(
|
||||
"vacuum",
|
||||
"pause",
|
||||
{
|
||||
"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.OperationalState.Commands.Pause(),
|
||||
)
|
||||
matter_client.send_device_command.reset_mock()
|
||||
|
||||
# test stop action
|
||||
# stop command is not supported by the vacuum fixture
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Entity vacuum.mock_vacuum does not support this service.",
|
||||
):
|
||||
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(
|
||||
"vacuum",
|
||||
"stop",
|
||||
{
|
||||
"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.OperationalState.Commands.Stop(),
|
||||
)
|
||||
matter_client.send_device_command.reset_mock()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["vacuum_cleaner"])
|
||||
async def test_vacuum_updates(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test vacuum entity updates."""
|
||||
entity_id = "vacuum.mock_vacuum"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
# confirm initial state is idle (as stored in the fixture)
|
||||
assert state.state == "idle"
|
||||
|
||||
# confirm state is 'docked' by setting the operational state to 0x42
|
||||
set_node_attribute(matter_node, 1, 97, 4, 0x42)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "docked"
|
||||
|
||||
# confirm state is 'docked' by setting the operational state to 0x41
|
||||
set_node_attribute(matter_node, 1, 97, 4, 0x41)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "docked"
|
||||
|
||||
# confirm state is 'returning' by setting the operational state to 0x40
|
||||
set_node_attribute(matter_node, 1, 97, 4, 0x40)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "returning"
|
||||
|
||||
# confirm state is 'error' by setting the operational state to 0x01
|
||||
set_node_attribute(matter_node, 1, 97, 4, 0x01)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "error"
|
||||
|
||||
# confirm state is 'error' by setting the operational state to 0x02
|
||||
set_node_attribute(matter_node, 1, 97, 4, 0x02)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "error"
|
||||
|
||||
# confirm state is 'cleaning' by setting;
|
||||
# - the operational state to 0x00
|
||||
# - the run mode is set to a mode which has cleaning tag
|
||||
set_node_attribute(matter_node, 1, 97, 4, 0)
|
||||
set_node_attribute(matter_node, 1, 84, 1, 1)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "cleaning"
|
||||
|
||||
# confirm state is 'idle' by setting;
|
||||
# - the operational state to 0x00
|
||||
# - the run mode is set to a mode which has idle tag
|
||||
set_node_attribute(matter_node, 1, 97, 4, 0)
|
||||
set_node_attribute(matter_node, 1, 84, 1, 0)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "idle"
|
||||
|
||||
# confirm state is 'unknown' by setting;
|
||||
# - the operational state to 0x00
|
||||
# - 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, 84, 1, 2)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "unknown"
|
Loading…
Reference in New Issue