Add Matter Climate support (#95434)
* Add Matter Climate support * update set target temp and update callback * remove print * remove optional property * Adjust the code to improve readability. * add thermostat test * Remove irrelevant cases in setting the target temperature. * add temp range support * update hvac action * support adjust low high setpoint.. * support set hvac mode * address some review feedback * move some methods around * dont discover climate in switch platform * set some default values * fix some of the tests * fix some typos * Update thermostat.json * Update homeassistant/components/matter/climate.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/matter/climate.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * support heat_cool in hvac_modes * address some review feedback * handle hvac mode param in set temp service * check hvac modes by featuremap * add comment to thermostat feature class * make ruff happy.. * use enum to enhance readability. * use builtin feature bitmap * fix target temp range and address some feedback * use instance attribute instead of class attr * make ruff happy... * address feedback about single case * add init docstring * more test * fix typo in tests * make ruff happy * fix hvac modes test * test case for update callback * remove optional check * more tests * more tests * update all attributes in the update callback * Update climate.py * fix missing test --------- Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
367acd0433
commit
3bd8955e0e
5 changed files with 1085 additions and 0 deletions
313
homeassistant/components/matter/climate.py
Normal file
313
homeassistant/components/matter/climate.py
Normal file
|
@ -0,0 +1,313 @@
|
|||
"""Matter climate 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 matter_server.common.helpers.util import create_attribute_path_from_attribute
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_HVAC_MODE,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
DEFAULT_MAX_TEMP,
|
||||
DEFAULT_MIN_TEMP,
|
||||
ClimateEntity,
|
||||
ClimateEntityDescription,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, Platform, UnitOfTemperature
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from matter_server.client import MatterClient
|
||||
from matter_server.client.models.node import MatterEndpoint
|
||||
|
||||
from .discovery import MatterEntityInfo
|
||||
|
||||
TEMPERATURE_SCALING_FACTOR = 100
|
||||
HVAC_SYSTEM_MODE_MAP = {
|
||||
HVACMode.OFF: 0,
|
||||
HVACMode.HEAT_COOL: 1,
|
||||
HVACMode.COOL: 3,
|
||||
HVACMode.HEAT: 4,
|
||||
}
|
||||
SystemModeEnum = clusters.Thermostat.Enums.ThermostatSystemMode
|
||||
ControlSequenceEnum = clusters.Thermostat.Enums.ThermostatControlSequence
|
||||
ThermostatFeature = clusters.Thermostat.Bitmaps.ThermostatFeature
|
||||
|
||||
|
||||
class ThermostatRunningState(IntEnum):
|
||||
"""Thermostat Running State, Matter spec Thermostat 7.33."""
|
||||
|
||||
Heat = 1 # 1 << 0 = 1
|
||||
Cool = 2 # 1 << 1 = 2
|
||||
Fan = 4 # 1 << 2 = 4
|
||||
HeatStage2 = 8 # 1 << 3 = 8
|
||||
CoolStage2 = 16 # 1 << 4 = 16
|
||||
FanStage2 = 32 # 1 << 5 = 32
|
||||
FanStage3 = 64 # 1 << 6 = 64
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Matter climate platform from Config Entry."""
|
||||
matter = get_matter(hass)
|
||||
matter.register_platform_handler(Platform.CLIMATE, async_add_entities)
|
||||
|
||||
|
||||
class MatterClimate(MatterEntity, ClimateEntity):
|
||||
"""Representation of a Matter climate entity."""
|
||||
|
||||
_attr_temperature_unit: str = UnitOfTemperature.CELSIUS
|
||||
_attr_supported_features: ClimateEntityFeature = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
)
|
||||
_attr_hvac_mode: HVACMode = HVACMode.OFF
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
matter_client: MatterClient,
|
||||
endpoint: MatterEndpoint,
|
||||
entity_info: MatterEntityInfo,
|
||||
) -> None:
|
||||
"""Initialize the Matter climate entity."""
|
||||
super().__init__(matter_client, endpoint, entity_info)
|
||||
|
||||
# set hvac_modes based on feature map
|
||||
self._attr_hvac_modes: list[HVACMode] = [HVACMode.OFF]
|
||||
feature_map = int(
|
||||
self.get_matter_attribute_value(clusters.Thermostat.Attributes.FeatureMap)
|
||||
)
|
||||
if feature_map & ThermostatFeature.kHeating:
|
||||
self._attr_hvac_modes.append(HVACMode.HEAT)
|
||||
if feature_map & ThermostatFeature.kCooling:
|
||||
self._attr_hvac_modes.append(HVACMode.COOL)
|
||||
if feature_map & ThermostatFeature.kAutoMode:
|
||||
self._attr_hvac_modes.append(HVACMode.HEAT_COOL)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
target_hvac_mode: HVACMode | None = kwargs.get(ATTR_HVAC_MODE)
|
||||
if target_hvac_mode is not None:
|
||||
await self.async_set_hvac_mode(target_hvac_mode)
|
||||
|
||||
current_mode = target_hvac_mode or self.hvac_mode
|
||||
command = None
|
||||
if current_mode in (HVACMode.HEAT, HVACMode.COOL):
|
||||
# when current mode is either heat or cool, the temperature arg must be provided.
|
||||
temperature: float | None = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
raise ValueError("Temperature must be provided")
|
||||
if self.target_temperature is None:
|
||||
raise ValueError("Current target_temperature should not be None")
|
||||
command = self._create_optional_setpoint_command(
|
||||
clusters.Thermostat.Enums.SetpointAdjustMode.kCool
|
||||
if current_mode == HVACMode.COOL
|
||||
else clusters.Thermostat.Enums.SetpointAdjustMode.kHeat,
|
||||
temperature,
|
||||
self.target_temperature,
|
||||
)
|
||||
elif current_mode == HVACMode.HEAT_COOL:
|
||||
temperature_low: float | None = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
temperature_high: float | None = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if temperature_low is None or temperature_high is None:
|
||||
raise ValueError(
|
||||
"temperature_low and temperature_high must be provided"
|
||||
)
|
||||
if (
|
||||
self.target_temperature_low is None
|
||||
or self.target_temperature_high is None
|
||||
):
|
||||
raise ValueError(
|
||||
"current target_temperature_low and target_temperature_high should not be None"
|
||||
)
|
||||
# due to ha send both high and low temperature, we need to check which one is changed
|
||||
command = self._create_optional_setpoint_command(
|
||||
clusters.Thermostat.Enums.SetpointAdjustMode.kHeat,
|
||||
temperature_low,
|
||||
self.target_temperature_low,
|
||||
)
|
||||
if command is None:
|
||||
command = self._create_optional_setpoint_command(
|
||||
clusters.Thermostat.Enums.SetpointAdjustMode.kCool,
|
||||
temperature_high,
|
||||
self.target_temperature_high,
|
||||
)
|
||||
if 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_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,
|
||||
value=system_mode_value,
|
||||
)
|
||||
# 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
|
||||
self._endpoint.set_attribute_value(system_mode_path, system_mode_value)
|
||||
self._update_from_device()
|
||||
|
||||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
self._attr_current_temperature = self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.LocalTemperature
|
||||
)
|
||||
# update hvac_mode from SystemMode
|
||||
system_mode_value = int(
|
||||
self.get_matter_attribute_value(clusters.Thermostat.Attributes.SystemMode)
|
||||
)
|
||||
match system_mode_value:
|
||||
case SystemModeEnum.kAuto:
|
||||
self._attr_hvac_mode = HVACMode.HEAT_COOL
|
||||
case SystemModeEnum.kDry:
|
||||
self._attr_hvac_mode = HVACMode.DRY
|
||||
case SystemModeEnum.kFanOnly:
|
||||
self._attr_hvac_mode = HVACMode.FAN_ONLY
|
||||
case SystemModeEnum.kCool | SystemModeEnum.kPrecooling:
|
||||
self._attr_hvac_mode = HVACMode.COOL
|
||||
case SystemModeEnum.kHeat | SystemModeEnum.kEmergencyHeat:
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
case _:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
# running state is an optional attribute
|
||||
# which we map to hvac_action if it exists (its value is not None)
|
||||
self._attr_hvac_action = None
|
||||
if running_state_value := self.get_matter_attribute_value(
|
||||
clusters.Thermostat.Attributes.ThermostatRunningState
|
||||
):
|
||||
match running_state_value:
|
||||
case ThermostatRunningState.Heat | ThermostatRunningState.HeatStage2:
|
||||
self._attr_hvac_action = HVACAction.HEATING
|
||||
case ThermostatRunningState.Cool | ThermostatRunningState.CoolStage2:
|
||||
self._attr_hvac_action = HVACAction.COOLING
|
||||
case (
|
||||
ThermostatRunningState.Fan
|
||||
| ThermostatRunningState.FanStage2
|
||||
| ThermostatRunningState.FanStage3
|
||||
):
|
||||
self._attr_hvac_action = HVACAction.FAN
|
||||
case _:
|
||||
self._attr_hvac_action = HVACAction.OFF
|
||||
# update target_temperature
|
||||
if self._attr_hvac_mode == HVACMode.HEAT_COOL:
|
||||
self._attr_target_temperature = None
|
||||
elif self._attr_hvac_mode == HVACMode.COOL:
|
||||
self._attr_target_temperature = self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.OccupiedCoolingSetpoint
|
||||
)
|
||||
else:
|
||||
self._attr_target_temperature = self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
|
||||
)
|
||||
# update target temperature high/low
|
||||
if self._attr_hvac_mode == HVACMode.HEAT_COOL:
|
||||
self._attr_target_temperature_high = self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.OccupiedCoolingSetpoint
|
||||
)
|
||||
self._attr_target_temperature_low = self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
|
||||
)
|
||||
else:
|
||||
self._attr_target_temperature_high = None
|
||||
self._attr_target_temperature_low = None
|
||||
# update min_temp
|
||||
if self._attr_hvac_mode == HVACMode.COOL:
|
||||
attribute = clusters.Thermostat.Attributes.AbsMinCoolSetpointLimit
|
||||
else:
|
||||
attribute = clusters.Thermostat.Attributes.AbsMinHeatSetpointLimit
|
||||
if (value := self._get_temperature_in_degrees(attribute)) is not None:
|
||||
self._attr_min_temp = value
|
||||
else:
|
||||
self._attr_min_temp = DEFAULT_MIN_TEMP
|
||||
# update max_temp
|
||||
if self._attr_hvac_mode in (HVACMode.COOL, HVACMode.HEAT_COOL):
|
||||
attribute = clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit
|
||||
else:
|
||||
attribute = clusters.Thermostat.Attributes.AbsMaxCoolSetpointLimit
|
||||
if (value := self._get_temperature_in_degrees(attribute)) is not None:
|
||||
self._attr_max_temp = value
|
||||
else:
|
||||
self._attr_max_temp = DEFAULT_MAX_TEMP
|
||||
|
||||
def _get_temperature_in_degrees(
|
||||
self, attribute: type[clusters.ClusterAttributeDescriptor]
|
||||
) -> float | None:
|
||||
"""Return the scaled temperature value for the given attribute."""
|
||||
if value := self.get_matter_attribute_value(attribute):
|
||||
return float(value) / TEMPERATURE_SCALING_FACTOR
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _create_optional_setpoint_command(
|
||||
mode: clusters.Thermostat.Enums.SetpointAdjustMode,
|
||||
target_temp: float,
|
||||
current_target_temp: float,
|
||||
) -> clusters.Thermostat.Commands.SetpointRaiseLower | None:
|
||||
"""Create a setpoint command if the target temperature is different from the current one."""
|
||||
|
||||
temp_diff = int((target_temp - current_target_temp) * 10)
|
||||
|
||||
if temp_diff == 0:
|
||||
return None
|
||||
|
||||
return clusters.Thermostat.Commands.SetpointRaiseLower(
|
||||
mode,
|
||||
temp_diff,
|
||||
)
|
||||
|
||||
|
||||
# Discovery schema(s) to map Matter Attributes to HA entities
|
||||
DISCOVERY_SCHEMAS = [
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.CLIMATE,
|
||||
entity_description=ClimateEntityDescription(
|
||||
key="MatterThermostat",
|
||||
name=None,
|
||||
),
|
||||
entity_class=MatterClimate,
|
||||
required_attributes=(clusters.Thermostat.Attributes.LocalTemperature,),
|
||||
optional_attributes=(
|
||||
clusters.Thermostat.Attributes.FeatureMap,
|
||||
clusters.Thermostat.Attributes.ControlSequenceOfOperation,
|
||||
clusters.Thermostat.Attributes.Occupancy,
|
||||
clusters.Thermostat.Attributes.OccupiedCoolingSetpoint,
|
||||
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint,
|
||||
clusters.Thermostat.Attributes.SystemMode,
|
||||
clusters.Thermostat.Attributes.ThermostatRunningMode,
|
||||
clusters.Thermostat.Attributes.ThermostatRunningState,
|
||||
clusters.Thermostat.Attributes.TemperatureSetpointHold,
|
||||
clusters.Thermostat.Attributes.UnoccupiedCoolingSetpoint,
|
||||
clusters.Thermostat.Attributes.UnoccupiedHeatingSetpoint,
|
||||
),
|
||||
device_type=(device_types.Thermostat,),
|
||||
),
|
||||
]
|
|
@ -10,6 +10,7 @@ from homeassistant.const import Platform
|
|||
from homeassistant.core import callback
|
||||
|
||||
from .binary_sensor import DISCOVERY_SCHEMAS as BINARY_SENSOR_SCHEMAS
|
||||
from .climate import DISCOVERY_SCHEMAS as CLIMATE_SENSOR_SCHEMAS
|
||||
from .cover import DISCOVERY_SCHEMAS as COVER_SCHEMAS
|
||||
from .light import DISCOVERY_SCHEMAS as LIGHT_SCHEMAS
|
||||
from .lock import DISCOVERY_SCHEMAS as LOCK_SCHEMAS
|
||||
|
@ -19,6 +20,7 @@ from .switch import DISCOVERY_SCHEMAS as SWITCH_SCHEMAS
|
|||
|
||||
DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = {
|
||||
Platform.BINARY_SENSOR: BINARY_SENSOR_SCHEMAS,
|
||||
Platform.CLIMATE: CLIMATE_SENSOR_SCHEMAS,
|
||||
Platform.COVER: COVER_SCHEMAS,
|
||||
Platform.LIGHT: LIGHT_SCHEMAS,
|
||||
Platform.LOCK: LOCK_SCHEMAS,
|
||||
|
|
|
@ -77,6 +77,7 @@ DISCOVERY_SCHEMAS = [
|
|||
device_types.ColorDimmerSwitch,
|
||||
device_types.DimmerSwitch,
|
||||
device_types.OnOffLightSwitch,
|
||||
device_types.Thermostat,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
370
tests/components/matter/fixtures/nodes/thermostat.json
Normal file
370
tests/components/matter/fixtures/nodes/thermostat.json
Normal file
|
@ -0,0 +1,370 @@
|
|||
{
|
||||
"node_id": 4,
|
||||
"date_commissioned": "2023-06-28T16:26:35.525058",
|
||||
"last_interview": "2023-06-28T16:26:35.525060",
|
||||
"interview_version": 4,
|
||||
"available": true,
|
||||
"is_bridge": false,
|
||||
"attributes": {
|
||||
"0/29/0": [
|
||||
{
|
||||
"deviceType": 22,
|
||||
"revision": 1
|
||||
}
|
||||
],
|
||||
"0/29/1": [29, 31, 40, 42, 48, 49, 50, 51, 54, 60, 62, 63, 64],
|
||||
"0/29/2": [41],
|
||||
"0/29/3": [1],
|
||||
"0/29/65532": 0,
|
||||
"0/29/65533": 1,
|
||||
"0/29/65528": [],
|
||||
"0/29/65529": [],
|
||||
"0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/31/0": [
|
||||
{
|
||||
"privilege": 0,
|
||||
"authMode": 0,
|
||||
"subjects": null,
|
||||
"targets": null,
|
||||
"fabricIndex": 1
|
||||
},
|
||||
{
|
||||
"privilege": 5,
|
||||
"authMode": 2,
|
||||
"subjects": [112233],
|
||||
"targets": null,
|
||||
"fabricIndex": 2
|
||||
}
|
||||
],
|
||||
"0/31/1": [],
|
||||
"0/31/2": 4,
|
||||
"0/31/3": 3,
|
||||
"0/31/4": 3,
|
||||
"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": 1,
|
||||
"0/40/1": "LONGAN-LINK",
|
||||
"0/40/2": 4895,
|
||||
"0/40/3": "Longan link HVAC",
|
||||
"0/40/4": 8192,
|
||||
"0/40/5": "",
|
||||
"0/40/6": "XX",
|
||||
"0/40/7": 1,
|
||||
"0/40/8": "1.0",
|
||||
"0/40/9": 2,
|
||||
"0/40/10": "v2.0",
|
||||
"0/40/11": "20200101",
|
||||
"0/40/12": "",
|
||||
"0/40/14": "",
|
||||
"0/40/15": "5a1fd2d040f23cf66e3a9d2a88e11f78",
|
||||
"0/40/16": false,
|
||||
"0/40/17": true,
|
||||
"0/40/18": "3D06D025F9E026A0",
|
||||
"0/40/19": {
|
||||
"caseSessionsPerFabric": 3,
|
||||
"subscriptionsPerFabric": 3
|
||||
},
|
||||
"0/40/65532": 0,
|
||||
"0/40/65533": 1,
|
||||
"0/40/65528": [],
|
||||
"0/40/65529": [],
|
||||
"0/40/65531": [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 17, 18, 19, 65528,
|
||||
65529, 65531, 65532, 65533
|
||||
],
|
||||
"0/42/0": [],
|
||||
"0/42/1": true,
|
||||
"0/42/2": 1,
|
||||
"0/42/3": null,
|
||||
"0/42/65532": 0,
|
||||
"0/42/65533": 1,
|
||||
"0/42/65528": [],
|
||||
"0/42/65529": [0],
|
||||
"0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/48/0": 0,
|
||||
"0/48/1": {
|
||||
"failSafeExpiryLengthSeconds": 60,
|
||||
"maxCumulativeFailsafeSeconds": 900
|
||||
},
|
||||
"0/48/2": 0,
|
||||
"0/48/3": 0,
|
||||
"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": [
|
||||
{
|
||||
"networkID": "TE9OR0FOLUlPVA==",
|
||||
"connected": true
|
||||
}
|
||||
],
|
||||
"0/49/2": 10,
|
||||
"0/49/3": 30,
|
||||
"0/49/4": true,
|
||||
"0/49/5": 0,
|
||||
"0/49/6": "TE9OR0FOLUlPVA==",
|
||||
"0/49/7": null,
|
||||
"0/49/65532": 1,
|
||||
"0/49/65533": 1,
|
||||
"0/49/65528": [1, 5, 7],
|
||||
"0/49/65529": [0, 2, 4, 6, 8],
|
||||
"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": [
|
||||
{
|
||||
"name": "WIFI_STA_DEF",
|
||||
"isOperational": true,
|
||||
"offPremiseServicesReachableIPv4": null,
|
||||
"offPremiseServicesReachableIPv6": null,
|
||||
"hardwareAddress": "3FR1X7qs",
|
||||
"IPv4Addresses": ["wKgI7g=="],
|
||||
"IPv6Addresses": [
|
||||
"/oAAAAAAAADeVHX//l+6rA==",
|
||||
"JA4DsgZ9jUDeVHX//l+6rA==",
|
||||
"/UgvJAe/AADeVHX//l+6rA=="
|
||||
],
|
||||
"type": 1
|
||||
}
|
||||
],
|
||||
"0/51/1": 4,
|
||||
"0/51/2": 30,
|
||||
"0/51/3": 0,
|
||||
"0/51/4": 0,
|
||||
"0/51/5": [],
|
||||
"0/51/6": [],
|
||||
"0/51/7": [],
|
||||
"0/51/8": true,
|
||||
"0/51/65532": 0,
|
||||
"0/51/65533": 1,
|
||||
"0/51/65528": [],
|
||||
"0/51/65529": [0],
|
||||
"0/51/65531": [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533
|
||||
],
|
||||
"0/54/0": "aHckDXAk",
|
||||
"0/54/1": 0,
|
||||
"0/54/2": 3,
|
||||
"0/54/3": 1,
|
||||
"0/54/4": -61,
|
||||
"0/54/5": null,
|
||||
"0/54/6": null,
|
||||
"0/54/7": null,
|
||||
"0/54/8": null,
|
||||
"0/54/9": null,
|
||||
"0/54/10": null,
|
||||
"0/54/11": null,
|
||||
"0/54/12": null,
|
||||
"0/54/65532": 3,
|
||||
"0/54/65533": 1,
|
||||
"0/54/65528": [],
|
||||
"0/54/65529": [0],
|
||||
"0/54/65531": [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 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, 1, 2],
|
||||
"0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/62/0": [
|
||||
{
|
||||
"noc": "",
|
||||
"icac": null,
|
||||
"fabricIndex": 1
|
||||
},
|
||||
{
|
||||
"noc": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVASQRBBgkBwEkCAEwCUEETaqdhs6MRkbh8fdh4EEImZaziiE6anaVp6Mu3P/zIJUB0fHUMxydKRTAC8bIn7vUhBCM47OYlYTkX0zFhoKYrzcKNQEoARgkAgE2AwQCBAEYMAQUrouBLuksQTkLrFhNVAbTHkNvMSEwBRTPlgMACvPdpqPOzuvR0OfPgfUcxBgwC0AcUInETXp/2gIFGDQF2+u+9WtYtvIfo6C3MhoOIV1SrRBZWYxY3CVjPGK7edTibQrVA4GccZKnHhNSBjxktrPiGA==",
|
||||
"icac": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEE+rI5XQyifTZbZRK1Z2DOuXdQkmdUkWklTv+G1x4ZfbSupbUDo4l7i/iFdyu//uJThAw1GPEkWe6i98IFKCOQpzcKNQEpARgkAmAwBBTPlgMACvPdpqPOzuvR0OfPgfUcxDAFFJQo6UEBWTLtZVYFZwRBgn+qstpTGDALQK3jYiaxwnYJMwTBQlcVNrGxPtuVTZrp5foZtQCp/JEX2ZWqVxKypilx0ES/CfMHZ0Lllv9QsLs8xV/HNLidllkY",
|
||||
"fabricIndex": 2
|
||||
}
|
||||
],
|
||||
"0/62/1": [
|
||||
{
|
||||
"rootPublicKey": "BAP9BJt5aQ9N98ClPTdNxpMZ1/Vh8r9usw6C8Ygi79AImsJq4UjAaYad0UI9Lh0OmRA9sWE2aSPbHjf409i/970=",
|
||||
"vendorID": 4996,
|
||||
"fabricID": 1,
|
||||
"nodeID": 1425709672,
|
||||
"label": "",
|
||||
"fabricIndex": 1
|
||||
},
|
||||
{
|
||||
"rootPublicKey": "BJXfyipMp+Jx4pkoTnvYoAYODis4xJktKdQXu8MSpBLIwII58BD0KkIG9NmuHcp0xUQKzqlfyB/bkAanevO73ZI=",
|
||||
"vendorID": 65521,
|
||||
"fabricID": 1,
|
||||
"nodeID": 4,
|
||||
"label": "",
|
||||
"fabricIndex": 2
|
||||
}
|
||||
],
|
||||
"0/62/2": 5,
|
||||
"0/62/3": 2,
|
||||
"0/62/4": [
|
||||
"FTABAQAkAgE3AycULlZRw4lgwKgkFQEYJgT1e7grJgV1r5ktNwYnFC5WUcOJYMCoJBUBGCQHASQIATAJQQQD/QSbeWkPTffApT03TcaTGdf1YfK/brMOgvGIIu/QCJrCauFIwGmGndFCPS4dDpkQPbFhNmkj2x43+NPYv/e9Nwo1ASkBGCQCYDAEFCqZHzimE2c+jPoEuJoM1rQaAPFRMAUUKpkfOKYTZz6M+gS4mgzWtBoA8VEYMAtANu49PfywV8aJmtxNYZa7SJXGlK1EciiF6vhZsoqdDCwx1VQX8FdyVunw0H3ljzbvucU6o8aY6HwBsPJKCQVHzhg=",
|
||||
"FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEld/KKkyn4nHimShOe9igBg4OKzjEmS0p1Be7wxKkEsjAgjnwEPQqQgb02a4dynTFRArOqV/IH9uQBqd687vdkjcKNQEpARgkAmAwBBSUKOlBAVky7WVWBWcEQYJ/qrLaUzAFFJQo6UEBWTLtZVYFZwRBgn+qstpTGDALQCNU8W3im+pmCBR5A4e15ByjPq2msE05NI9eeFI6BO0p/whhaBSGtjI7Tb1onNNu9AH6AQoji8XDDa7Nj/1w9KoY"
|
||||
],
|
||||
"0/62/5": 2,
|
||||
"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": 3,
|
||||
"0/63/3": 3,
|
||||
"0/63/65532": 0,
|
||||
"0/63/65533": 1,
|
||||
"0/63/65528": [2, 5],
|
||||
"0/63/65529": [0, 1, 3, 4],
|
||||
"0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
|
||||
"0/64/0": [
|
||||
{
|
||||
"label": "room",
|
||||
"value": "bedroom 2"
|
||||
},
|
||||
{
|
||||
"label": "orientation",
|
||||
"value": "North"
|
||||
},
|
||||
{
|
||||
"label": "floor",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"label": "direction",
|
||||
"value": "up"
|
||||
}
|
||||
],
|
||||
"0/64/65532": 0,
|
||||
"0/64/65533": 1,
|
||||
"0/64/65528": [],
|
||||
"0/64/65529": [],
|
||||
"0/64/65531": [0, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/3/0": 0,
|
||||
"1/3/1": 4,
|
||||
"1/3/65532": 0,
|
||||
"1/3/65533": 4,
|
||||
"1/3/65528": [],
|
||||
"1/3/65529": [0],
|
||||
"1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/4/0": 128,
|
||||
"1/4/65532": 1,
|
||||
"1/4/65533": 4,
|
||||
"1/4/65528": [0, 1, 2, 3],
|
||||
"1/4/65529": [0, 1, 2, 3, 4, 5],
|
||||
"1/4/65531": [0, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/6/0": true,
|
||||
"1/6/65532": 0,
|
||||
"1/6/65533": 4,
|
||||
"1/6/65528": [],
|
||||
"1/6/65529": [0, 1, 2],
|
||||
"1/6/65531": [0, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/29/0": [
|
||||
{
|
||||
"deviceType": 769,
|
||||
"revision": 1
|
||||
}
|
||||
],
|
||||
"1/29/1": [3, 4, 6, 29, 30, 64, 513, 514, 516],
|
||||
"1/29/2": [],
|
||||
"1/29/3": [],
|
||||
"1/29/65532": 0,
|
||||
"1/29/65533": 1,
|
||||
"1/29/65528": [],
|
||||
"1/29/65529": [],
|
||||
"1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/30/0": [],
|
||||
"1/30/65532": 0,
|
||||
"1/30/65533": 1,
|
||||
"1/30/65528": [],
|
||||
"1/30/65529": [],
|
||||
"1/30/65531": [0, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/64/0": [
|
||||
{
|
||||
"label": "room",
|
||||
"value": "bedroom 2"
|
||||
},
|
||||
{
|
||||
"label": "orientation",
|
||||
"value": "North"
|
||||
},
|
||||
{
|
||||
"label": "floor",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"label": "direction",
|
||||
"value": "up"
|
||||
}
|
||||
],
|
||||
"1/64/65532": 0,
|
||||
"1/64/65533": 1,
|
||||
"1/64/65528": [],
|
||||
"1/64/65529": [],
|
||||
"1/64/65531": [0, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/513/0": 2830,
|
||||
"1/513/3": null,
|
||||
"1/513/4": null,
|
||||
"1/513/5": null,
|
||||
"1/513/6": null,
|
||||
"1/513/9": 0,
|
||||
"1/513/17": null,
|
||||
"1/513/18": null,
|
||||
"1/513/21": 1600,
|
||||
"1/513/22": 3000,
|
||||
"1/513/23": 1600,
|
||||
"1/513/24": 3000,
|
||||
"1/513/25": 5,
|
||||
"1/513/27": 4,
|
||||
"1/513/28": 3,
|
||||
"1/513/30": 0,
|
||||
"1/513/65532": 35,
|
||||
"1/513/65533": 5,
|
||||
"1/513/65528": [],
|
||||
"1/513/65529": [0],
|
||||
"1/513/65531": [
|
||||
0, 3, 4, 5, 6, 9, 17, 18, 21, 22, 23, 24, 25, 27, 28, 30, 65528, 65529,
|
||||
65531, 65532, 65533
|
||||
],
|
||||
"1/514/0": 0,
|
||||
"1/514/1": 2,
|
||||
"1/514/2": 0,
|
||||
"1/514/3": 0,
|
||||
"1/514/65532": 0,
|
||||
"1/514/65533": 1,
|
||||
"1/514/65528": [],
|
||||
"1/514/65529": [],
|
||||
"1/514/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
|
||||
"1/516/0": 0,
|
||||
"1/516/1": 0,
|
||||
"1/516/65532": 0,
|
||||
"1/516/65533": 1,
|
||||
"1/516/65528": [],
|
||||
"1/516/65529": [],
|
||||
"1/516/65531": [0, 1, 65528, 65529, 65531, 65532, 65533]
|
||||
},
|
||||
"attribute_subscriptions": [
|
||||
[1, 513, 17],
|
||||
[1, 6, 0],
|
||||
[1, 513, 0],
|
||||
[1, 513, 28],
|
||||
[1, 513, 65532],
|
||||
[1, 513, 18],
|
||||
[1, 513, 30],
|
||||
[1, 513, 27]
|
||||
]
|
||||
}
|
399
tests/components/matter/test_climate.py
Normal file
399
tests/components/matter/test_climate.py
Normal file
|
@ -0,0 +1,399 @@
|
|||
"""Test Matter locks."""
|
||||
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.helpers.util import create_attribute_path_from_attribute
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.components.climate.const import (
|
||||
HVAC_MODE_DRY,
|
||||
HVAC_MODE_FAN_ONLY,
|
||||
HVAC_MODE_OFF,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .common import (
|
||||
set_node_attribute,
|
||||
setup_integration_with_node_fixture,
|
||||
trigger_subscription_callback,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="thermostat")
|
||||
async def thermostat_fixture(
|
||||
hass: HomeAssistant, matter_client: MagicMock
|
||||
) -> MatterNode:
|
||||
"""Fixture for a thermostat node."""
|
||||
return await setup_integration_with_node_fixture(hass, "thermostat", matter_client)
|
||||
|
||||
|
||||
# This tests needs to be adjusted to remove lingering tasks
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
async def test_thermostat(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
thermostat: MatterNode,
|
||||
) -> None:
|
||||
"""Test thermostat."""
|
||||
# test default temp range
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.attributes["min_temp"] == 7
|
||||
assert state.attributes["max_temp"] == 35
|
||||
|
||||
# test set temperature when target temp is None
|
||||
assert state.attributes["temperature"] is None
|
||||
assert state.state == HVAC_MODE_COOL
|
||||
with pytest.raises(
|
||||
ValueError, match="Current target_temperature should not be None"
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.longan_link_hvac",
|
||||
"temperature": 22.5,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
with pytest.raises(ValueError, match="Temperature must be provided"):
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.longan_link_hvac",
|
||||
"target_temp_low": 18,
|
||||
"target_temp_high": 26,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# change system mode to heat_cool
|
||||
set_node_attribute(thermostat, 1, 513, 28, 1)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="current target_temperature_low and target_temperature_high should not be None",
|
||||
):
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.state == HVAC_MODE_HEAT_COOL
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.longan_link_hvac",
|
||||
"target_temp_low": 18,
|
||||
"target_temp_high": 26,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# initial state
|
||||
set_node_attribute(thermostat, 1, 513, 3, 1600)
|
||||
set_node_attribute(thermostat, 1, 513, 4, 3000)
|
||||
set_node_attribute(thermostat, 1, 513, 5, 1600)
|
||||
set_node_attribute(thermostat, 1, 513, 6, 3000)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.attributes["min_temp"] == 16
|
||||
assert state.attributes["max_temp"] == 30
|
||||
assert state.attributes["hvac_modes"] == [
|
||||
HVACMode.OFF,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.COOL,
|
||||
HVACMode.HEAT_COOL,
|
||||
]
|
||||
|
||||
# test system mode update from device
|
||||
set_node_attribute(thermostat, 1, 513, 28, 0)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.state == HVAC_MODE_OFF
|
||||
|
||||
set_node_attribute(thermostat, 1, 513, 28, 7)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.state == HVAC_MODE_FAN_ONLY
|
||||
|
||||
set_node_attribute(thermostat, 1, 513, 28, 8)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.state == HVAC_MODE_DRY
|
||||
|
||||
# test running state update from device
|
||||
set_node_attribute(thermostat, 1, 513, 41, 1)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.attributes["hvac_action"] == HVACAction.HEATING
|
||||
|
||||
set_node_attribute(thermostat, 1, 513, 41, 8)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.attributes["hvac_action"] == HVACAction.HEATING
|
||||
|
||||
set_node_attribute(thermostat, 1, 513, 41, 2)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.attributes["hvac_action"] == HVACAction.COOLING
|
||||
|
||||
set_node_attribute(thermostat, 1, 513, 41, 16)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.attributes["hvac_action"] == HVACAction.COOLING
|
||||
|
||||
set_node_attribute(thermostat, 1, 513, 41, 4)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.attributes["hvac_action"] == HVACAction.FAN
|
||||
|
||||
set_node_attribute(thermostat, 1, 513, 41, 32)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.attributes["hvac_action"] == HVACAction.FAN
|
||||
|
||||
set_node_attribute(thermostat, 1, 513, 41, 64)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.attributes["hvac_action"] == HVACAction.FAN
|
||||
|
||||
set_node_attribute(thermostat, 1, 513, 41, 66)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.attributes["hvac_action"] == HVACAction.OFF
|
||||
|
||||
# change system mode to heat
|
||||
set_node_attribute(thermostat, 1, 513, 28, 4)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.state == HVAC_MODE_HEAT
|
||||
|
||||
# change occupied heating setpoint to 20
|
||||
set_node_attribute(thermostat, 1, 513, 18, 2000)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.attributes["temperature"] == 20
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.longan_link_hvac",
|
||||
"temperature": 25,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert matter_client.send_device_command.call_count == 1
|
||||
assert matter_client.send_device_command.call_args == call(
|
||||
node_id=thermostat.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.Thermostat.Commands.SetpointRaiseLower(
|
||||
clusters.Thermostat.Enums.SetpointAdjustMode.kHeat,
|
||||
50,
|
||||
),
|
||||
)
|
||||
matter_client.send_device_command.reset_mock()
|
||||
|
||||
# change system mode to cool
|
||||
set_node_attribute(thermostat, 1, 513, 28, 3)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.state == HVAC_MODE_COOL
|
||||
|
||||
# change occupied cooling setpoint to 18
|
||||
set_node_attribute(thermostat, 1, 513, 17, 1800)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.attributes["temperature"] == 18
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.longan_link_hvac",
|
||||
"temperature": 16,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert matter_client.send_device_command.call_count == 1
|
||||
assert matter_client.send_device_command.call_args == call(
|
||||
node_id=thermostat.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.Thermostat.Commands.SetpointRaiseLower(
|
||||
clusters.Thermostat.Enums.SetpointAdjustMode.kCool, -20
|
||||
),
|
||||
)
|
||||
matter_client.send_device_command.reset_mock()
|
||||
|
||||
# change system mode to heat_cool
|
||||
set_node_attribute(thermostat, 1, 513, 28, 1)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
with pytest.raises(
|
||||
ValueError, match="temperature_low and temperature_high must be provided"
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.longan_link_hvac",
|
||||
"temperature": 18,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.state == HVAC_MODE_HEAT_COOL
|
||||
|
||||
# change occupied cooling setpoint to 18
|
||||
set_node_attribute(thermostat, 1, 513, 17, 2500)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
# change occupied heating setpoint to 18
|
||||
set_node_attribute(thermostat, 1, 513, 18, 1700)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get("climate.longan_link_hvac")
|
||||
assert state
|
||||
assert state.attributes["target_temp_low"] == 17
|
||||
assert state.attributes["target_temp_high"] == 25
|
||||
|
||||
# change target_temp_low to 18
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.longan_link_hvac",
|
||||
"target_temp_low": 18,
|
||||
"target_temp_high": 25,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert matter_client.send_device_command.call_count == 1
|
||||
assert matter_client.send_device_command.call_args == call(
|
||||
node_id=thermostat.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.Thermostat.Commands.SetpointRaiseLower(
|
||||
clusters.Thermostat.Enums.SetpointAdjustMode.kHeat, 10
|
||||
),
|
||||
)
|
||||
matter_client.send_device_command.reset_mock()
|
||||
set_node_attribute(thermostat, 1, 513, 18, 1800)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
# change target_temp_high to 26
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.longan_link_hvac",
|
||||
"target_temp_low": 18,
|
||||
"target_temp_high": 26,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert matter_client.send_device_command.call_count == 1
|
||||
assert matter_client.send_device_command.call_args == call(
|
||||
node_id=thermostat.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.Thermostat.Commands.SetpointRaiseLower(
|
||||
clusters.Thermostat.Enums.SetpointAdjustMode.kCool, 10
|
||||
),
|
||||
)
|
||||
matter_client.send_device_command.reset_mock()
|
||||
set_node_attribute(thermostat, 1, 513, 17, 2600)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_hvac_mode",
|
||||
{
|
||||
"entity_id": "climate.longan_link_hvac",
|
||||
"hvac_mode": HVAC_MODE_HEAT,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert matter_client.write_attribute.call_count == 1
|
||||
assert matter_client.write_attribute.call_args == call(
|
||||
node_id=thermostat.node_id,
|
||||
attribute_path=create_attribute_path_from_attribute(
|
||||
endpoint_id=1,
|
||||
attribute=clusters.Thermostat.Attributes.SystemMode,
|
||||
),
|
||||
value=4,
|
||||
)
|
||||
matter_client.send_device_command.reset_mock()
|
||||
|
||||
with pytest.raises(ValueError, match="Unsupported hvac mode dry in Matter"):
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_hvac_mode",
|
||||
{
|
||||
"entity_id": "climate.longan_link_hvac",
|
||||
"hvac_mode": HVACMode.DRY,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# change target_temp and hvac_mode in the same call
|
||||
matter_client.send_device_command.reset_mock()
|
||||
matter_client.write_attribute.reset_mock()
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{
|
||||
"entity_id": "climate.longan_link_hvac",
|
||||
"temperature": 22,
|
||||
"hvac_mode": HVACMode.COOL,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert matter_client.write_attribute.call_count == 1
|
||||
assert matter_client.write_attribute.call_args == call(
|
||||
node_id=thermostat.node_id,
|
||||
attribute_path=create_attribute_path_from_attribute(
|
||||
endpoint_id=1,
|
||||
attribute=clusters.Thermostat.Attributes.SystemMode,
|
||||
),
|
||||
value=3,
|
||||
)
|
||||
assert matter_client.send_device_command.call_count == 1
|
||||
assert matter_client.send_device_command.call_args == call(
|
||||
node_id=thermostat.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.Thermostat.Commands.SetpointRaiseLower(
|
||||
clusters.Thermostat.Enums.SetpointAdjustMode.kCool, -40
|
||||
),
|
||||
)
|
Loading…
Add table
Reference in a new issue