Several fixes for the Matter climate platform (#118322)
* extend hvacmode mapping with extra modes * Fix climate platform * adjust tests * fix reversed test * cleanup * dry and fan hvac mode test
This commit is contained in:
parent
7f1a616c9a
commit
5f5288d8b9
3 changed files with 194 additions and 237 deletions
|
@ -42,7 +42,33 @@ HVAC_SYSTEM_MODE_MAP = {
|
||||||
HVACMode.HEAT_COOL: 1,
|
HVACMode.HEAT_COOL: 1,
|
||||||
HVACMode.COOL: 3,
|
HVACMode.COOL: 3,
|
||||||
HVACMode.HEAT: 4,
|
HVACMode.HEAT: 4,
|
||||||
|
HVACMode.DRY: 8,
|
||||||
|
HVACMode.FAN_ONLY: 7,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SINGLE_SETPOINT_DEVICES: set[tuple[int, int]] = {
|
||||||
|
# Some devices only have a single setpoint while the matter spec
|
||||||
|
# assumes that you need separate setpoints for heating and cooling.
|
||||||
|
# We were told this is just some legacy inheritance from zigbee specs.
|
||||||
|
# In the list below specify tuples of (vendorid, productid) of devices for
|
||||||
|
# which we just need a single setpoint to control both heating and cooling.
|
||||||
|
(0x1209, 0x8007),
|
||||||
|
}
|
||||||
|
|
||||||
|
SUPPORT_DRY_MODE_DEVICES: set[tuple[int, int]] = {
|
||||||
|
# The Matter spec is missing a feature flag if the device supports a dry mode.
|
||||||
|
# In the list below specify tuples of (vendorid, productid) of devices that
|
||||||
|
# support dry mode.
|
||||||
|
(0x1209, 0x8007),
|
||||||
|
}
|
||||||
|
|
||||||
|
SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = {
|
||||||
|
# The Matter spec is missing a feature flag if the device supports a fan-only mode.
|
||||||
|
# In the list below specify tuples of (vendorid, productid) of devices that
|
||||||
|
# support fan-only mode.
|
||||||
|
(0x1209, 0x8007),
|
||||||
|
}
|
||||||
|
|
||||||
SystemModeEnum = clusters.Thermostat.Enums.ThermostatSystemMode
|
SystemModeEnum = clusters.Thermostat.Enums.ThermostatSystemMode
|
||||||
ControlSequenceEnum = clusters.Thermostat.Enums.ThermostatControlSequence
|
ControlSequenceEnum = clusters.Thermostat.Enums.ThermostatControlSequence
|
||||||
ThermostatFeature = clusters.Thermostat.Bitmaps.Feature
|
ThermostatFeature = clusters.Thermostat.Bitmaps.Feature
|
||||||
|
@ -85,80 +111,91 @@ class MatterClimate(MatterEntity, ClimateEntity):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Matter climate entity."""
|
"""Initialize the Matter climate entity."""
|
||||||
super().__init__(matter_client, endpoint, entity_info)
|
super().__init__(matter_client, endpoint, entity_info)
|
||||||
|
product_id = self._endpoint.node.device_info.productID
|
||||||
|
vendor_id = self._endpoint.node.device_info.vendorID
|
||||||
|
|
||||||
# set hvac_modes based on feature map
|
# set hvac_modes based on feature map
|
||||||
self._attr_hvac_modes: list[HVACMode] = [HVACMode.OFF]
|
self._attr_hvac_modes: list[HVACMode] = [HVACMode.OFF]
|
||||||
feature_map = int(
|
feature_map = int(
|
||||||
self.get_matter_attribute_value(clusters.Thermostat.Attributes.FeatureMap)
|
self.get_matter_attribute_value(clusters.Thermostat.Attributes.FeatureMap)
|
||||||
)
|
)
|
||||||
|
self._attr_supported_features = (
|
||||||
|
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF
|
||||||
|
)
|
||||||
if feature_map & ThermostatFeature.kHeating:
|
if feature_map & ThermostatFeature.kHeating:
|
||||||
self._attr_hvac_modes.append(HVACMode.HEAT)
|
self._attr_hvac_modes.append(HVACMode.HEAT)
|
||||||
if feature_map & ThermostatFeature.kCooling:
|
if feature_map & ThermostatFeature.kCooling:
|
||||||
self._attr_hvac_modes.append(HVACMode.COOL)
|
self._attr_hvac_modes.append(HVACMode.COOL)
|
||||||
|
if (vendor_id, product_id) in SUPPORT_DRY_MODE_DEVICES:
|
||||||
|
self._attr_hvac_modes.append(HVACMode.DRY)
|
||||||
|
if (vendor_id, product_id) in SUPPORT_FAN_MODE_DEVICES:
|
||||||
|
self._attr_hvac_modes.append(HVACMode.FAN_ONLY)
|
||||||
if feature_map & ThermostatFeature.kAutoMode:
|
if feature_map & ThermostatFeature.kAutoMode:
|
||||||
self._attr_hvac_modes.append(HVACMode.HEAT_COOL)
|
self._attr_hvac_modes.append(HVACMode.HEAT_COOL)
|
||||||
self._attr_supported_features = (
|
# only enable temperature_range feature if the device actually supports that
|
||||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
|
||||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
if (vendor_id, product_id) not in SINGLE_SETPOINT_DEVICES:
|
||||||
| ClimateEntityFeature.TURN_OFF
|
self._attr_supported_features |= (
|
||||||
)
|
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||||
|
)
|
||||||
if any(mode for mode in self.hvac_modes if mode != HVACMode.OFF):
|
if any(mode for mode in self.hvac_modes if mode != HVACMode.OFF):
|
||||||
self._attr_supported_features |= ClimateEntityFeature.TURN_ON
|
self._attr_supported_features |= ClimateEntityFeature.TURN_ON
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
target_hvac_mode: HVACMode | None = kwargs.get(ATTR_HVAC_MODE)
|
target_hvac_mode: HVACMode | None = kwargs.get(ATTR_HVAC_MODE)
|
||||||
|
target_temperature: float | None = kwargs.get(ATTR_TEMPERATURE)
|
||||||
|
target_temperature_low: float | None = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||||
|
target_temperature_high: float | None = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||||
|
|
||||||
if target_hvac_mode is not None:
|
if target_hvac_mode is not None:
|
||||||
await self.async_set_hvac_mode(target_hvac_mode)
|
await self.async_set_hvac_mode(target_hvac_mode)
|
||||||
|
|
||||||
current_mode = target_hvac_mode or self.hvac_mode
|
current_mode = target_hvac_mode or self.hvac_mode
|
||||||
command = None
|
|
||||||
if current_mode in (HVACMode.HEAT, HVACMode.COOL):
|
if target_temperature is not None:
|
||||||
# when current mode is either heat or cool, the temperature arg must be provided.
|
# single setpoint control
|
||||||
temperature: float | None = kwargs.get(ATTR_TEMPERATURE)
|
if self.target_temperature != target_temperature:
|
||||||
if temperature is None:
|
if current_mode == HVACMode.COOL:
|
||||||
raise ValueError("Temperature must be provided")
|
matter_attribute = (
|
||||||
if self.target_temperature is None:
|
clusters.Thermostat.Attributes.OccupiedCoolingSetpoint
|
||||||
raise ValueError("Current target_temperature should not be None")
|
)
|
||||||
command = self._create_optional_setpoint_command(
|
else:
|
||||||
clusters.Thermostat.Enums.SetpointAdjustMode.kCool
|
matter_attribute = (
|
||||||
if current_mode == HVACMode.COOL
|
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
|
||||||
else clusters.Thermostat.Enums.SetpointAdjustMode.kHeat,
|
)
|
||||||
temperature,
|
await self.matter_client.write_attribute(
|
||||||
self.target_temperature,
|
node_id=self._endpoint.node.node_id,
|
||||||
)
|
attribute_path=create_attribute_path_from_attribute(
|
||||||
elif current_mode == HVACMode.HEAT_COOL:
|
self._endpoint.endpoint_id,
|
||||||
temperature_low: float | None = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
matter_attribute,
|
||||||
temperature_high: float | None = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
),
|
||||||
if temperature_low is None or temperature_high is None:
|
value=int(target_temperature * TEMPERATURE_SCALING_FACTOR),
|
||||||
raise ValueError(
|
|
||||||
"temperature_low and temperature_high must be provided"
|
|
||||||
)
|
)
|
||||||
if (
|
return
|
||||||
self.target_temperature_low is None
|
|
||||||
or self.target_temperature_high is None
|
if target_temperature_low is not None:
|
||||||
):
|
# multi setpoint control - low setpoint (heat)
|
||||||
raise ValueError(
|
if self.target_temperature_low != target_temperature_low:
|
||||||
"current target_temperature_low and target_temperature_high should not be None"
|
await self.matter_client.write_attribute(
|
||||||
|
node_id=self._endpoint.node.node_id,
|
||||||
|
attribute_path=create_attribute_path_from_attribute(
|
||||||
|
self._endpoint.endpoint_id,
|
||||||
|
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint,
|
||||||
|
),
|
||||||
|
value=int(target_temperature_low * TEMPERATURE_SCALING_FACTOR),
|
||||||
)
|
)
|
||||||
# due to ha send both high and low temperature, we need to check which one is changed
|
|
||||||
command = self._create_optional_setpoint_command(
|
if target_temperature_high is not None:
|
||||||
clusters.Thermostat.Enums.SetpointAdjustMode.kHeat,
|
# multi setpoint control - high setpoint (cool)
|
||||||
temperature_low,
|
if self.target_temperature_high != target_temperature_high:
|
||||||
self.target_temperature_low,
|
await self.matter_client.write_attribute(
|
||||||
)
|
node_id=self._endpoint.node.node_id,
|
||||||
if command is None:
|
attribute_path=create_attribute_path_from_attribute(
|
||||||
command = self._create_optional_setpoint_command(
|
self._endpoint.endpoint_id,
|
||||||
clusters.Thermostat.Enums.SetpointAdjustMode.kCool,
|
clusters.Thermostat.Attributes.OccupiedCoolingSetpoint,
|
||||||
temperature_high,
|
),
|
||||||
self.target_temperature_high,
|
value=int(target_temperature_high * TEMPERATURE_SCALING_FACTOR),
|
||||||
)
|
)
|
||||||
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:
|
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||||
"""Set new target hvac mode."""
|
"""Set new target hvac mode."""
|
||||||
|
@ -201,6 +238,10 @@ class MatterClimate(MatterEntity, ClimateEntity):
|
||||||
self._attr_hvac_mode = HVACMode.COOL
|
self._attr_hvac_mode = HVACMode.COOL
|
||||||
case SystemModeEnum.kHeat | SystemModeEnum.kEmergencyHeat:
|
case SystemModeEnum.kHeat | SystemModeEnum.kEmergencyHeat:
|
||||||
self._attr_hvac_mode = HVACMode.HEAT
|
self._attr_hvac_mode = HVACMode.HEAT
|
||||||
|
case SystemModeEnum.kFanOnly:
|
||||||
|
self._attr_hvac_mode = HVACMode.FAN_ONLY
|
||||||
|
case SystemModeEnum.kDry:
|
||||||
|
self._attr_hvac_mode = HVACMode.DRY
|
||||||
case _:
|
case _:
|
||||||
self._attr_hvac_mode = HVACMode.OFF
|
self._attr_hvac_mode = HVACMode.OFF
|
||||||
# running state is an optional attribute
|
# running state is an optional attribute
|
||||||
|
@ -271,24 +312,6 @@ class MatterClimate(MatterEntity, ClimateEntity):
|
||||||
return float(value) / TEMPERATURE_SCALING_FACTOR
|
return float(value) / TEMPERATURE_SCALING_FACTOR
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _create_optional_setpoint_command(
|
|
||||||
mode: clusters.Thermostat.Enums.SetpointAdjustMode | int,
|
|
||||||
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 schema(s) to map Matter Attributes to HA entities
|
||||||
DISCOVERY_SCHEMAS = [
|
DISCOVERY_SCHEMAS = [
|
||||||
|
|
|
@ -43,9 +43,9 @@
|
||||||
"0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
|
"0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
|
||||||
"0/40/0": 17,
|
"0/40/0": 17,
|
||||||
"0/40/1": "TEST_VENDOR",
|
"0/40/1": "TEST_VENDOR",
|
||||||
"0/40/2": 65521,
|
"0/40/2": 4617,
|
||||||
"0/40/3": "Room AirConditioner",
|
"0/40/3": "Room AirConditioner",
|
||||||
"0/40/4": 32774,
|
"0/40/4": 32775,
|
||||||
"0/40/5": "",
|
"0/40/5": "",
|
||||||
"0/40/6": "**REDACTED**",
|
"0/40/6": "**REDACTED**",
|
||||||
"0/40/7": 0,
|
"0/40/7": 0,
|
||||||
|
|
|
@ -8,6 +8,7 @@ from matter_server.common.helpers.util import create_attribute_path_from_attribu
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.climate import HVACAction, HVACMode
|
from homeassistant.components.climate import HVACAction, HVACMode
|
||||||
|
from homeassistant.components.climate.const import ClimateEntityFeature
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .common import (
|
from .common import (
|
||||||
|
@ -37,67 +38,30 @@ async def room_airconditioner(
|
||||||
|
|
||||||
# This tests needs to be adjusted to remove lingering tasks
|
# This tests needs to be adjusted to remove lingering tasks
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
async def test_thermostat(
|
async def test_thermostat_base(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
matter_client: MagicMock,
|
matter_client: MagicMock,
|
||||||
thermostat: MatterNode,
|
thermostat: MatterNode,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test thermostat."""
|
"""Test thermostat base attributes and state updates."""
|
||||||
# test default temp range
|
# test entity attributes
|
||||||
state = hass.states.get("climate.longan_link_hvac")
|
state = hass.states.get("climate.longan_link_hvac")
|
||||||
assert state
|
assert state
|
||||||
assert state.attributes["min_temp"] == 7
|
assert state.attributes["min_temp"] == 7
|
||||||
assert state.attributes["max_temp"] == 35
|
assert state.attributes["max_temp"] == 35
|
||||||
|
|
||||||
# test set temperature when target temp is None
|
|
||||||
assert state.attributes["temperature"] is None
|
assert state.attributes["temperature"] is None
|
||||||
assert state.state == HVACMode.COOL
|
assert state.state == HVACMode.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
|
# test supported features correctly parsed
|
||||||
set_node_attribute(thermostat, 1, 513, 28, 1)
|
# including temperature_range support
|
||||||
await trigger_subscription_callback(hass, matter_client)
|
mask = (
|
||||||
with pytest.raises(
|
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
ValueError,
|
| ClimateEntityFeature.TURN_OFF
|
||||||
match="current target_temperature_low and target_temperature_high should not be None",
|
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||||
):
|
)
|
||||||
state = hass.states.get("climate.longan_link_hvac")
|
assert state.attributes["supported_features"] & mask == mask
|
||||||
assert state
|
|
||||||
assert state.state == HVACMode.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
|
# test common state updates from device
|
||||||
set_node_attribute(thermostat, 1, 513, 3, 1600)
|
set_node_attribute(thermostat, 1, 513, 3, 1600)
|
||||||
set_node_attribute(thermostat, 1, 513, 4, 3000)
|
set_node_attribute(thermostat, 1, 513, 4, 3000)
|
||||||
set_node_attribute(thermostat, 1, 513, 5, 1600)
|
set_node_attribute(thermostat, 1, 513, 5, 1600)
|
||||||
|
@ -121,18 +85,6 @@ async def test_thermostat(
|
||||||
assert state
|
assert state
|
||||||
assert state.state == HVACMode.OFF
|
assert state.state == HVACMode.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 == HVACMode.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 == HVACMode.DRY
|
|
||||||
|
|
||||||
# test running state update from device
|
# test running state update from device
|
||||||
set_node_attribute(thermostat, 1, 513, 41, 1)
|
set_node_attribute(thermostat, 1, 513, 41, 1)
|
||||||
await trigger_subscription_callback(hass, matter_client)
|
await trigger_subscription_callback(hass, matter_client)
|
||||||
|
@ -198,6 +150,19 @@ async def test_thermostat(
|
||||||
assert state
|
assert state
|
||||||
assert state.attributes["temperature"] == 20
|
assert state.attributes["temperature"] == 20
|
||||||
|
|
||||||
|
|
||||||
|
# This tests needs to be adjusted to remove lingering tasks
|
||||||
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
|
async def test_thermostat_service_calls(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
matter_client: MagicMock,
|
||||||
|
thermostat: MatterNode,
|
||||||
|
) -> None:
|
||||||
|
"""Test climate platform service calls."""
|
||||||
|
# test single-setpoint temperature adjustment when cool mode is active
|
||||||
|
state = hass.states.get("climate.longan_link_hvac")
|
||||||
|
assert state
|
||||||
|
assert state.state == HVACMode.COOL
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"climate",
|
"climate",
|
||||||
"set_temperature",
|
"set_temperature",
|
||||||
|
@ -208,133 +173,87 @@ async def test_thermostat(
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert matter_client.send_device_command.call_count == 1
|
assert matter_client.write_attribute.call_count == 1
|
||||||
assert matter_client.send_device_command.call_args == call(
|
assert matter_client.write_attribute.call_args == call(
|
||||||
node_id=thermostat.node_id,
|
node_id=thermostat.node_id,
|
||||||
endpoint_id=1,
|
attribute_path="1/513/17",
|
||||||
command=clusters.Thermostat.Commands.SetpointRaiseLower(
|
value=2500,
|
||||||
clusters.Thermostat.Enums.SetpointAdjustMode.kHeat,
|
|
||||||
50,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
matter_client.send_device_command.reset_mock()
|
matter_client.write_attribute.reset_mock()
|
||||||
|
|
||||||
# change system mode to cool
|
# ensure that no command is executed when the temperature is the same
|
||||||
set_node_attribute(thermostat, 1, 513, 28, 3)
|
set_node_attribute(thermostat, 1, 513, 17, 2500)
|
||||||
await trigger_subscription_callback(hass, matter_client)
|
await trigger_subscription_callback(hass, matter_client)
|
||||||
|
await hass.services.async_call(
|
||||||
|
"climate",
|
||||||
|
"set_temperature",
|
||||||
|
{
|
||||||
|
"entity_id": "climate.longan_link_hvac",
|
||||||
|
"temperature": 25,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matter_client.write_attribute.call_count == 0
|
||||||
|
matter_client.write_attribute.reset_mock()
|
||||||
|
|
||||||
|
# test single-setpoint temperature adjustment when heat mode is active
|
||||||
|
set_node_attribute(thermostat, 1, 513, 28, 4)
|
||||||
|
await trigger_subscription_callback(hass, matter_client)
|
||||||
state = hass.states.get("climate.longan_link_hvac")
|
state = hass.states.get("climate.longan_link_hvac")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == HVACMode.COOL
|
assert state.state == HVACMode.HEAT
|
||||||
|
|
||||||
# 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(
|
await hass.services.async_call(
|
||||||
"climate",
|
"climate",
|
||||||
"set_temperature",
|
"set_temperature",
|
||||||
{
|
{
|
||||||
"entity_id": "climate.longan_link_hvac",
|
"entity_id": "climate.longan_link_hvac",
|
||||||
"temperature": 16,
|
"temperature": 20,
|
||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert matter_client.send_device_command.call_count == 1
|
assert matter_client.write_attribute.call_count == 1
|
||||||
assert matter_client.send_device_command.call_args == call(
|
assert matter_client.write_attribute.call_args == call(
|
||||||
node_id=thermostat.node_id,
|
node_id=thermostat.node_id,
|
||||||
endpoint_id=1,
|
attribute_path="1/513/18",
|
||||||
command=clusters.Thermostat.Commands.SetpointRaiseLower(
|
value=2000,
|
||||||
clusters.Thermostat.Enums.SetpointAdjustMode.kCool, -20
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
matter_client.send_device_command.reset_mock()
|
matter_client.write_attribute.reset_mock()
|
||||||
|
|
||||||
# change system mode to heat_cool
|
# test dual setpoint temperature adjustments when heat_cool mode is active
|
||||||
set_node_attribute(thermostat, 1, 513, 28, 1)
|
set_node_attribute(thermostat, 1, 513, 28, 1)
|
||||||
await trigger_subscription_callback(hass, matter_client)
|
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")
|
state = hass.states.get("climate.longan_link_hvac")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == HVACMode.HEAT_COOL
|
assert state.state == HVACMode.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(
|
await hass.services.async_call(
|
||||||
"climate",
|
"climate",
|
||||||
"set_temperature",
|
"set_temperature",
|
||||||
{
|
{
|
||||||
"entity_id": "climate.longan_link_hvac",
|
"entity_id": "climate.longan_link_hvac",
|
||||||
"target_temp_low": 18,
|
"target_temp_low": 10,
|
||||||
"target_temp_high": 25,
|
"target_temp_high": 30,
|
||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert matter_client.send_device_command.call_count == 1
|
assert matter_client.write_attribute.call_count == 2
|
||||||
assert matter_client.send_device_command.call_args == call(
|
assert matter_client.write_attribute.call_args_list[0] == call(
|
||||||
node_id=thermostat.node_id,
|
node_id=thermostat.node_id,
|
||||||
endpoint_id=1,
|
attribute_path="1/513/18",
|
||||||
command=clusters.Thermostat.Commands.SetpointRaiseLower(
|
value=1000,
|
||||||
clusters.Thermostat.Enums.SetpointAdjustMode.kHeat, 10
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
matter_client.send_device_command.reset_mock()
|
assert matter_client.write_attribute.call_args_list[1] == call(
|
||||||
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,
|
node_id=thermostat.node_id,
|
||||||
endpoint_id=1,
|
attribute_path="1/513/17",
|
||||||
command=clusters.Thermostat.Commands.SetpointRaiseLower(
|
value=3000,
|
||||||
clusters.Thermostat.Enums.SetpointAdjustMode.kCool, 10
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
matter_client.send_device_command.reset_mock()
|
matter_client.write_attribute.reset_mock()
|
||||||
set_node_attribute(thermostat, 1, 513, 17, 2600)
|
|
||||||
await trigger_subscription_callback(hass, matter_client)
|
|
||||||
|
|
||||||
|
# test change HAVC mode to heat
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"climate",
|
"climate",
|
||||||
"set_hvac_mode",
|
"set_hvac_mode",
|
||||||
|
@ -356,17 +275,6 @@ async def test_thermostat(
|
||||||
)
|
)
|
||||||
matter_client.send_device_command.reset_mock()
|
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
|
# change target_temp and hvac_mode in the same call
|
||||||
matter_client.send_device_command.reset_mock()
|
matter_client.send_device_command.reset_mock()
|
||||||
matter_client.write_attribute.reset_mock()
|
matter_client.write_attribute.reset_mock()
|
||||||
|
@ -380,8 +288,8 @@ async def test_thermostat(
|
||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
assert matter_client.write_attribute.call_count == 1
|
assert matter_client.write_attribute.call_count == 2
|
||||||
assert matter_client.write_attribute.call_args == call(
|
assert matter_client.write_attribute.call_args_list[0] == call(
|
||||||
node_id=thermostat.node_id,
|
node_id=thermostat.node_id,
|
||||||
attribute_path=create_attribute_path_from_attribute(
|
attribute_path=create_attribute_path_from_attribute(
|
||||||
endpoint_id=1,
|
endpoint_id=1,
|
||||||
|
@ -389,14 +297,12 @@ async def test_thermostat(
|
||||||
),
|
),
|
||||||
value=3,
|
value=3,
|
||||||
)
|
)
|
||||||
assert matter_client.send_device_command.call_count == 1
|
assert matter_client.write_attribute.call_args_list[1] == call(
|
||||||
assert matter_client.send_device_command.call_args == call(
|
|
||||||
node_id=thermostat.node_id,
|
node_id=thermostat.node_id,
|
||||||
endpoint_id=1,
|
attribute_path="1/513/17",
|
||||||
command=clusters.Thermostat.Commands.SetpointRaiseLower(
|
value=2200,
|
||||||
clusters.Thermostat.Enums.SetpointAdjustMode.kCool, -40
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
matter_client.write_attribute.reset_mock()
|
||||||
|
|
||||||
|
|
||||||
# This tests needs to be adjusted to remove lingering tasks
|
# This tests needs to be adjusted to remove lingering tasks
|
||||||
|
@ -412,3 +318,31 @@ async def test_room_airconditioner(
|
||||||
assert state.attributes["current_temperature"] == 20
|
assert state.attributes["current_temperature"] == 20
|
||||||
assert state.attributes["min_temp"] == 16
|
assert state.attributes["min_temp"] == 16
|
||||||
assert state.attributes["max_temp"] == 32
|
assert state.attributes["max_temp"] == 32
|
||||||
|
|
||||||
|
# test supported features correctly parsed
|
||||||
|
# WITHOUT temperature_range support
|
||||||
|
mask = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF
|
||||||
|
assert state.attributes["supported_features"] & mask == mask
|
||||||
|
|
||||||
|
# test supported HVAC modes include fan and dry modes
|
||||||
|
assert state.attributes["hvac_modes"] == [
|
||||||
|
HVACMode.OFF,
|
||||||
|
HVACMode.HEAT,
|
||||||
|
HVACMode.COOL,
|
||||||
|
HVACMode.DRY,
|
||||||
|
HVACMode.FAN_ONLY,
|
||||||
|
HVACMode.HEAT_COOL,
|
||||||
|
]
|
||||||
|
# test fan-only hvac mode
|
||||||
|
set_node_attribute(room_airconditioner, 1, 513, 28, 7)
|
||||||
|
await trigger_subscription_callback(hass, matter_client)
|
||||||
|
state = hass.states.get("climate.room_airconditioner")
|
||||||
|
assert state
|
||||||
|
assert state.state == HVACMode.FAN_ONLY
|
||||||
|
|
||||||
|
# test dry hvac mode
|
||||||
|
set_node_attribute(room_airconditioner, 1, 513, 28, 8)
|
||||||
|
await trigger_subscription_callback(hass, matter_client)
|
||||||
|
state = hass.states.get("climate.room_airconditioner")
|
||||||
|
assert state
|
||||||
|
assert state.state == HVACMode.DRY
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue