Compare commits
2 commits
dev
...
matter-fea
Author | SHA1 | Date | |
---|---|---|---|
|
bb0ecffbbe | ||
|
de4a14903c |
7 changed files with 78 additions and 8 deletions
|
@ -45,6 +45,7 @@ class MatterAdapter:
|
|||
self.hass = hass
|
||||
self.config_entry = config_entry
|
||||
self.platform_handlers: dict[Platform, AddEntitiesCallback] = {}
|
||||
self.discovered_entities: set[str] = set()
|
||||
|
||||
def register_platform_handler(
|
||||
self, platform: Platform, add_entities: AddEntitiesCallback
|
||||
|
@ -54,23 +55,19 @@ class MatterAdapter:
|
|||
|
||||
async def setup_nodes(self) -> None:
|
||||
"""Set up all existing nodes and subscribe to new nodes."""
|
||||
initialized_nodes: set[int] = set()
|
||||
for node in self.matter_client.get_nodes():
|
||||
initialized_nodes.add(node.node_id)
|
||||
self._setup_node(node)
|
||||
|
||||
def node_added_callback(event: EventType, node: MatterNode) -> None:
|
||||
"""Handle node added event."""
|
||||
initialized_nodes.add(node.node_id)
|
||||
self._setup_node(node)
|
||||
|
||||
def node_updated_callback(event: EventType, node: MatterNode) -> None:
|
||||
"""Handle node updated event."""
|
||||
if node.node_id in initialized_nodes:
|
||||
return
|
||||
if not node.available:
|
||||
return
|
||||
initialized_nodes.add(node.node_id)
|
||||
# We always run the discovery logic again,
|
||||
# because the firmware version could have been changed or features added.
|
||||
self._setup_node(node)
|
||||
|
||||
def endpoint_added_callback(event: EventType, data: dict[str, int]) -> None:
|
||||
|
@ -78,6 +75,10 @@ class MatterAdapter:
|
|||
node = self.matter_client.get_node(data["node_id"])
|
||||
self._setup_endpoint(node.endpoints[data["endpoint_id"]])
|
||||
|
||||
def node_event_callback(event: EventType, data: dict[str, int]) -> None:
|
||||
"""Handle endpoint added event."""
|
||||
LOGGER.warning("Node event: %s %s", event, data)
|
||||
|
||||
def endpoint_removed_callback(event: EventType, data: dict[str, int]) -> None:
|
||||
"""Handle endpoint removed event."""
|
||||
server_info = cast(ServerInfoMessage, self.matter_client.server_info)
|
||||
|
@ -135,6 +136,11 @@ class MatterAdapter:
|
|||
callback=node_updated_callback, event_filter=EventType.NODE_UPDATED
|
||||
)
|
||||
)
|
||||
self.config_entry.async_on_unload(
|
||||
self.matter_client.subscribe_events(
|
||||
callback=node_event_callback, event_filter=EventType.NODE_EVENT
|
||||
)
|
||||
)
|
||||
|
||||
def _setup_node(self, node: MatterNode) -> None:
|
||||
"""Set up an node."""
|
||||
|
@ -237,11 +243,19 @@ class MatterAdapter:
|
|||
self._create_device_registry(endpoint)
|
||||
# run platform discovery from device type instances
|
||||
for entity_info in async_discover_entities(endpoint):
|
||||
discovery_key = (
|
||||
f"{entity_info.platform}_{endpoint.node.node_id}_{endpoint.endpoint_id}_"
|
||||
f"{entity_info.primary_attribute.cluster_id}_"
|
||||
f"{entity_info.primary_attribute.attribute_id}"
|
||||
)
|
||||
if discovery_key in self.discovered_entities:
|
||||
continue
|
||||
LOGGER.debug(
|
||||
"Creating %s entity for %s",
|
||||
entity_info.platform,
|
||||
entity_info.primary_attribute,
|
||||
)
|
||||
self.discovered_entities.add(discovery_key)
|
||||
new_entity = entity_info.entity_class(
|
||||
self.matter_client, endpoint, entity_info
|
||||
)
|
||||
|
|
|
@ -159,6 +159,7 @@ DISCOVERY_SCHEMAS = [
|
|||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
required_attributes=(clusters.DoorLock.Attributes.DoorState,),
|
||||
featuremap_contains=clusters.DoorLock.Bitmaps.Feature.kDoorPositionSensor,
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
|
|
|
@ -13,3 +13,5 @@ LOGGER = logging.getLogger(__package__)
|
|||
# prefixes to identify device identifier id types
|
||||
ID_TYPE_DEVICE_ID = "deviceid"
|
||||
ID_TYPE_SERIAL = "serial"
|
||||
|
||||
FEATUREMAP_ATTRIBUTE_ID = 65532
|
||||
|
|
|
@ -13,6 +13,7 @@ from homeassistant.core import callback
|
|||
from .binary_sensor import DISCOVERY_SCHEMAS as BINARY_SENSOR_SCHEMAS
|
||||
from .button import DISCOVERY_SCHEMAS as BUTTON_SCHEMAS
|
||||
from .climate import DISCOVERY_SCHEMAS as CLIMATE_SENSOR_SCHEMAS
|
||||
from .const import FEATUREMAP_ATTRIBUTE_ID
|
||||
from .cover import DISCOVERY_SCHEMAS as COVER_SCHEMAS
|
||||
from .event import DISCOVERY_SCHEMAS as EVENT_SCHEMAS
|
||||
from .fan import DISCOVERY_SCHEMAS as FAN_SCHEMAS
|
||||
|
@ -128,6 +129,21 @@ def async_discover_entities(
|
|||
):
|
||||
continue
|
||||
|
||||
# check for required value in cluster featuremap
|
||||
if schema.featuremap_contains is not None and (
|
||||
(primary_attribute := next((x for x in schema.required_attributes), None))
|
||||
is None
|
||||
or not bool(
|
||||
int(
|
||||
endpoint.get_attribute_value(
|
||||
primary_attribute.cluster_id, FEATUREMAP_ATTRIBUTE_ID
|
||||
)
|
||||
)
|
||||
& schema.featuremap_contains
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
# all checks passed, this value belongs to an entity
|
||||
|
||||
attributes_to_watch = list(schema.required_attributes)
|
||||
|
@ -145,6 +161,7 @@ def async_discover_entities(
|
|||
attributes_to_watch=attributes_to_watch,
|
||||
entity_description=schema.entity_description,
|
||||
entity_class=schema.entity_class,
|
||||
discovery_schema=schema,
|
||||
)
|
||||
|
||||
# prevent re-discovery of the primary attribute if not allowed
|
||||
|
|
|
@ -16,9 +16,10 @@ from propcache import cached_property
|
|||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
from homeassistant.helpers.typing import UndefinedType
|
||||
|
||||
from .const import DOMAIN, ID_TYPE_DEVICE_ID
|
||||
from .const import DOMAIN, FEATUREMAP_ATTRIBUTE_ID, ID_TYPE_DEVICE_ID
|
||||
from .helpers import get_device_id
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -140,6 +141,19 @@ class MatterEntity(Entity):
|
|||
node_filter=self._endpoint.node.node_id,
|
||||
)
|
||||
)
|
||||
# subscribe to FeatureMap attribute (as that can dynamically change)
|
||||
self._unsubscribes.append(
|
||||
self.matter_client.subscribe_events(
|
||||
callback=self._on_featuremap_update,
|
||||
event_filter=EventType.ATTRIBUTE_UPDATED,
|
||||
node_filter=self._endpoint.node.node_id,
|
||||
attr_path_filter=create_attribute_path(
|
||||
endpoint=self._endpoint.endpoint_id,
|
||||
cluster_id=self._entity_info.primary_attribute.cluster_id,
|
||||
attribute_id=FEATUREMAP_ATTRIBUTE_ID,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def name(self) -> str | UndefinedType | None:
|
||||
|
@ -159,6 +173,22 @@ class MatterEntity(Entity):
|
|||
self._update_from_device()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _on_featuremap_update(self, event: EventType, data: int) -> None:
|
||||
"""Handle FeatureMap attribute updates."""
|
||||
# handle edge case where a Feature is removed from a cluster
|
||||
if (
|
||||
self._entity_info.discovery_schema.featuremap_contains is not None
|
||||
and not bool(data & self._entity_info.discovery_schema.featuremap_contains)
|
||||
):
|
||||
# this entity is no longer supported by the device
|
||||
ent_reg = er.async_get(self.hass)
|
||||
ent_reg.async_remove(self.entity_id)
|
||||
|
||||
return
|
||||
# all other cases, just update the entity
|
||||
self._on_matter_event(event, data)
|
||||
|
||||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update data from Matter device."""
|
||||
|
|
|
@ -206,6 +206,5 @@ DISCOVERY_SCHEMAS = [
|
|||
),
|
||||
entity_class=MatterLock,
|
||||
required_attributes=(clusters.DoorLock.Attributes.LockState,),
|
||||
optional_attributes=(clusters.DoorLock.Attributes.DoorState,),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -51,6 +51,9 @@ class MatterEntityInfo:
|
|||
# entity class to use to instantiate the entity
|
||||
entity_class: type
|
||||
|
||||
# the original discovery schema used to create this entity
|
||||
discovery_schema: MatterDiscoverySchema
|
||||
|
||||
@property
|
||||
def primary_attribute(self) -> type[ClusterAttributeDescriptor]:
|
||||
"""Return Primary Attribute belonging to the entity."""
|
||||
|
@ -113,6 +116,10 @@ class MatterDiscoverySchema:
|
|||
# NOTE: only works for list values
|
||||
value_contains: Any | None = None
|
||||
|
||||
# [optional] the primary attribute's cluster featuremap must contain this value
|
||||
# for example for the DoorSensor on a DoorLock Cluster
|
||||
featuremap_contains: int | None = None
|
||||
|
||||
# [optional] bool to specify if this primary value may be discovered
|
||||
# by multiple platforms
|
||||
allow_multi: bool = False
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue