Matter handle FeatureMap update (#122544)
This commit is contained in:
parent
f1084a57df
commit
8a4206da99
6 changed files with 93 additions and 74 deletions
|
@ -3,7 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from enum import IntEnum
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
from matter_server.client.models import device_types
|
||||
|
@ -30,12 +30,6 @@ 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,
|
||||
|
@ -105,46 +99,9 @@ class MatterClimate(MatterEntity, ClimateEntity):
|
|||
|
||||
_attr_temperature_unit: str = UnitOfTemperature.CELSIUS
|
||||
_attr_hvac_mode: HVACMode = HVACMode.OFF
|
||||
_feature_map: int | None = None
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
matter_client: MatterClient,
|
||||
endpoint: MatterEndpoint,
|
||||
entity_info: MatterEntityInfo,
|
||||
) -> None:
|
||||
"""Initialize the Matter climate entity."""
|
||||
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
|
||||
self._attr_hvac_modes: list[HVACMode] = [HVACMode.OFF]
|
||||
feature_map = int(
|
||||
self.get_matter_attribute_value(clusters.Thermostat.Attributes.FeatureMap)
|
||||
)
|
||||
self._attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF
|
||||
)
|
||||
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 (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:
|
||||
self._attr_hvac_modes.append(HVACMode.HEAT_COOL)
|
||||
# only enable temperature_range feature if the device actually supports that
|
||||
|
||||
if (vendor_id, product_id) not in SINGLE_SETPOINT_DEVICES:
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
)
|
||||
if any(mode for mode in self.hvac_modes if mode != HVACMode.OFF):
|
||||
self._attr_supported_features |= ClimateEntityFeature.TURN_ON
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
target_hvac_mode: HVACMode | None = kwargs.get(ATTR_HVAC_MODE)
|
||||
|
@ -224,6 +181,7 @@ class MatterClimate(MatterEntity, ClimateEntity):
|
|||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
self._calculate_features()
|
||||
self._attr_current_temperature = self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.LocalTemperature
|
||||
)
|
||||
|
@ -319,6 +277,46 @@ class MatterClimate(MatterEntity, ClimateEntity):
|
|||
else:
|
||||
self._attr_max_temp = DEFAULT_MAX_TEMP
|
||||
|
||||
@callback
|
||||
def _calculate_features(
|
||||
self,
|
||||
) -> None:
|
||||
"""Calculate features for HA Thermostat platform from Matter FeatureMap."""
|
||||
feature_map = int(
|
||||
self.get_matter_attribute_value(clusters.Thermostat.Attributes.FeatureMap)
|
||||
)
|
||||
# NOTE: the featuremap can dynamically change, so we need to update the
|
||||
# supported features if the featuremap changes.
|
||||
# work out supported features and presets from matter featuremap
|
||||
if self._feature_map == feature_map:
|
||||
return
|
||||
self._feature_map = feature_map
|
||||
product_id = self._endpoint.node.device_info.productID
|
||||
vendor_id = self._endpoint.node.device_info.vendorID
|
||||
self._attr_hvac_modes: list[HVACMode] = [HVACMode.OFF]
|
||||
self._attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF
|
||||
)
|
||||
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 (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:
|
||||
self._attr_hvac_modes.append(HVACMode.HEAT_COOL)
|
||||
# only enable temperature_range feature if the device actually supports that
|
||||
|
||||
if (vendor_id, product_id) not in SINGLE_SETPOINT_DEVICES:
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
)
|
||||
if any(mode for mode in self.hvac_modes if mode != HVACMode.OFF):
|
||||
self._attr_supported_features |= ClimateEntityFeature.TURN_ON
|
||||
|
||||
@callback
|
||||
def _get_temperature_in_degrees(
|
||||
self, attribute: type[clusters.ClusterAttributeDescriptor]
|
||||
) -> float | None:
|
||||
|
|
|
@ -59,6 +59,7 @@ class MatterFan(MatterEntity, FanEntity):
|
|||
_last_known_preset_mode: str | None = None
|
||||
_last_known_percentage: int = 0
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
_feature_map: int | None = None
|
||||
|
||||
async def async_turn_on(
|
||||
self,
|
||||
|
@ -183,8 +184,7 @@ class MatterFan(MatterEntity, FanEntity):
|
|||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
if not hasattr(self, "_attr_preset_modes"):
|
||||
self._calculate_features()
|
||||
self._calculate_features()
|
||||
|
||||
if self.get_matter_attribute_value(clusters.OnOff.Attributes.OnOff) is False:
|
||||
# special case: the appliance has a dedicated Power switch on the OnOff cluster
|
||||
|
@ -257,11 +257,17 @@ class MatterFan(MatterEntity, FanEntity):
|
|||
def _calculate_features(
|
||||
self,
|
||||
) -> None:
|
||||
"""Calculate features and preset modes for HA Fan platform from Matter attributes.."""
|
||||
# work out supported features and presets from matter featuremap
|
||||
"""Calculate features for HA Fan platform from Matter FeatureMap."""
|
||||
feature_map = int(
|
||||
self.get_matter_attribute_value(clusters.FanControl.Attributes.FeatureMap)
|
||||
)
|
||||
# NOTE: the featuremap can dynamically change, so we need to update the
|
||||
# supported features if the featuremap changes.
|
||||
# work out supported features and presets from matter featuremap
|
||||
if self._feature_map == feature_map:
|
||||
return
|
||||
self._feature_map = feature_map
|
||||
self._attr_supported_features = FanEntityFeature(0)
|
||||
if feature_map & FanControlFeature.kMultiSpeed:
|
||||
self._attr_supported_features |= FanEntityFeature.SET_SPEED
|
||||
self._attr_speed_count = int(
|
||||
|
|
|
@ -38,7 +38,7 @@ async def async_setup_entry(
|
|||
class MatterLock(MatterEntity, LockEntity):
|
||||
"""Representation of a Matter lock."""
|
||||
|
||||
features: int | None = None
|
||||
_feature_map: int | None = None
|
||||
_optimistic_timer: asyncio.TimerHandle | None = None
|
||||
|
||||
@property
|
||||
|
@ -61,22 +61,6 @@ class MatterLock(MatterEntity, LockEntity):
|
|||
|
||||
return None
|
||||
|
||||
@property
|
||||
def supports_door_position_sensor(self) -> bool:
|
||||
"""Return True if the lock supports door position sensor."""
|
||||
if self.features is None:
|
||||
return False
|
||||
|
||||
return bool(self.features & DoorLockFeature.kDoorPositionSensor)
|
||||
|
||||
@property
|
||||
def supports_unbolt(self) -> bool:
|
||||
"""Return True if the lock supports unbolt."""
|
||||
if self.features is None:
|
||||
return False
|
||||
|
||||
return bool(self.features & DoorLockFeature.kUnbolt)
|
||||
|
||||
async def send_device_command(
|
||||
self,
|
||||
command: clusters.ClusterCommand,
|
||||
|
@ -120,7 +104,7 @@ class MatterLock(MatterEntity, LockEntity):
|
|||
)
|
||||
code: str | None = kwargs.get(ATTR_CODE)
|
||||
code_bytes = code.encode() if code else None
|
||||
if self.supports_unbolt:
|
||||
if self._attr_supported_features & LockEntityFeature.OPEN:
|
||||
# if the lock reports it has separate unbolt support,
|
||||
# the unlock command should unbolt only on the unlock command
|
||||
# and unlatch on the HA 'open' command.
|
||||
|
@ -151,13 +135,8 @@ class MatterLock(MatterEntity, LockEntity):
|
|||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update the entity from the device."""
|
||||
|
||||
if self.features is None:
|
||||
self.features = int(
|
||||
self.get_matter_attribute_value(clusters.DoorLock.Attributes.FeatureMap)
|
||||
)
|
||||
if self.supports_unbolt:
|
||||
self._attr_supported_features = LockEntityFeature.OPEN
|
||||
# always calculate the features as they can dynamically change
|
||||
self._calculate_features()
|
||||
|
||||
lock_state = self.get_matter_attribute_value(
|
||||
clusters.DoorLock.Attributes.LockState
|
||||
|
@ -197,6 +176,25 @@ class MatterLock(MatterEntity, LockEntity):
|
|||
if write_state:
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _calculate_features(
|
||||
self,
|
||||
) -> None:
|
||||
"""Calculate features for HA Lock platform from Matter FeatureMap."""
|
||||
feature_map = int(
|
||||
self.get_matter_attribute_value(clusters.DoorLock.Attributes.FeatureMap)
|
||||
)
|
||||
# NOTE: the featuremap can dynamically change, so we need to update the
|
||||
# supported features if the featuremap changes.
|
||||
if self._feature_map == feature_map:
|
||||
return
|
||||
self._feature_map = feature_map
|
||||
supported_features = LockEntityFeature(0)
|
||||
# determine if lock supports optional open/unbolt feature
|
||||
if bool(feature_map & DoorLockFeature.kUnbolt):
|
||||
supported_features |= LockEntityFeature.OPEN
|
||||
self._attr_supported_features = supported_features
|
||||
|
||||
|
||||
DISCOVERY_SCHEMAS = [
|
||||
MatterDiscoverySchema(
|
||||
|
|
|
@ -350,3 +350,9 @@ async def test_room_airconditioner(
|
|||
state = hass.states.get("climate.room_airconditioner_thermostat")
|
||||
assert state
|
||||
assert state.state == HVACMode.DRY
|
||||
|
||||
# test featuremap update
|
||||
set_node_attribute(room_airconditioner, 1, 513, 65532, 1)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("climate.room_airconditioner_thermostat")
|
||||
assert state.attributes["supported_features"] & ClimateEntityFeature.TURN_ON
|
||||
|
|
|
@ -107,6 +107,11 @@ async def test_fan_base(
|
|||
state = hass.states.get(entity_id)
|
||||
assert state.attributes["preset_mode"] is None
|
||||
assert state.attributes["percentage"] == 0
|
||||
# test featuremap update
|
||||
set_node_attribute(air_purifier, 1, 514, 65532, 1)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes["supported_features"] & FanEntityFeature.SET_SPEED
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
|
|
|
@ -97,6 +97,12 @@ async def test_lock(
|
|||
assert state
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
# test featuremap update
|
||||
set_node_attribute(door_lock, 1, 257, 65532, 4096)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
state = hass.states.get("lock.mock_door_lock_lock")
|
||||
assert state.attributes["supported_features"] & LockEntityFeature.OPEN
|
||||
|
||||
|
||||
# This tests needs to be adjusted to remove lingering tasks
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
|
|
Loading…
Add table
Reference in a new issue