Fix Matter unique_id generation (#86046)

* bae entity unique id on Operational Instance Name standard

* Update homeassistant/components/matter/entity.py

Co-authored-by: Stefan Agner <stefan@agner.ch>

* also adjust unique id for devices

* final adjustment

* remove assert on server_info

* move device info to init

* fabric_id_hex

* use DeviceInfo instead of dict

* fix test

Co-authored-by: Stefan Agner <stefan@agner.ch>
pull/86087/head
Marcel van der Veldt 2023-01-17 15:39:42 +01:00 committed by GitHub
parent 3cd6bd87a7
commit 072517f17e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 81 additions and 39 deletions

View File

@ -1,11 +1,15 @@
"""Matter to Home Assistant adapter."""
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, cast
from chip.clusters import Objects as all_clusters
from matter_server.common.models.events import EventType
from matter_server.common.models.node_device import AbstractMatterNodeDevice
from matter_server.common.models.node_device import (
AbstractMatterNodeDevice,
MatterBridgedNodeDevice,
)
from matter_server.common.models.server_information import ServerInfo
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
@ -15,6 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, LOGGER
from .device_platform import DEVICE_PLATFORM
from .helpers import get_device_id
if TYPE_CHECKING:
from matter_server.client import MatterClient
@ -66,31 +71,49 @@ class MatterAdapter:
bridge_unique_id: str | None = None
if node.aggregator_device_type_instance is not None and (
node_info := node.root_device_type_instance.get_cluster(all_clusters.Basic)
node.root_device_type_instance.get_cluster(all_clusters.Basic)
):
self._create_device_registry(
node_info, node_info.nodeLabel or "Hub device", None
# create virtual (parent) device for bridge node device
bridge_device = MatterBridgedNodeDevice(
node.aggregator_device_type_instance
)
bridge_unique_id = node_info.uniqueID
self._create_device_registry(bridge_device)
server_info = cast(ServerInfo, self.matter_client.server_info)
bridge_unique_id = get_device_id(server_info, bridge_device)
for node_device in node.node_devices:
self._setup_node_device(node_device, bridge_unique_id)
def _create_device_registry(
self,
info: all_clusters.Basic | all_clusters.BridgedDeviceBasic,
name: str,
bridge_unique_id: str | None,
node_device: AbstractMatterNodeDevice,
bridge_unique_id: str | None = None,
) -> None:
"""Create a device registry entry."""
server_info = cast(ServerInfo, self.matter_client.server_info)
node_unique_id = get_device_id(
server_info,
node_device,
)
basic_info = node_device.device_info()
device_type_instances = node_device.device_type_instances()
name = basic_info.nodeLabel
if not name and isinstance(node_device, MatterBridgedNodeDevice):
# fallback name for Bridge
name = "Hub device"
elif not name and device_type_instances:
# fallback name based on device type
name = f"{device_type_instances[0].device_type.__doc__[:-1]} {node_device.node().node_id}"
dr.async_get(self.hass).async_get_or_create(
name=name,
config_entry_id=self.config_entry.entry_id,
identifiers={(DOMAIN, info.uniqueID)},
hw_version=info.hardwareVersionString,
sw_version=info.softwareVersionString,
manufacturer=info.vendorName,
model=info.productName,
identifiers={(DOMAIN, node_unique_id)},
hw_version=basic_info.hardwareVersionString,
sw_version=basic_info.softwareVersionString,
manufacturer=basic_info.vendorName,
model=basic_info.productName,
via_device=(DOMAIN, bridge_unique_id) if bridge_unique_id else None,
)
@ -98,17 +121,9 @@ class MatterAdapter:
self, node_device: AbstractMatterNodeDevice, bridge_unique_id: str | None
) -> None:
"""Set up a node device."""
node = node_device.node()
basic_info = node_device.device_info()
device_type_instances = node_device.device_type_instances()
name = basic_info.nodeLabel
if not name and device_type_instances:
name = f"{device_type_instances[0].device_type.__doc__[:-1]} {node.node_id}"
self._create_device_registry(basic_info, name, bridge_unique_id)
for instance in device_type_instances:
self._create_device_registry(node_device, bridge_unique_id)
# run platform discovery from device type instances
for instance in node_device.device_type_instances():
created = False
for platform, devices in DEVICE_PLATFORM.items():

View File

@ -5,16 +5,18 @@ from abc import abstractmethod
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast
from matter_server.common.models.device_type_instance import MatterDeviceTypeInstance
from matter_server.common.models.events import EventType
from matter_server.common.models.node_device import AbstractMatterNodeDevice
from matter_server.common.models.server_information import ServerInfo
from homeassistant.core import callback
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
from .const import DOMAIN
from .helpers import get_device_id, get_operational_instance_id
if TYPE_CHECKING:
from matter_server.client import MatterClient
@ -55,24 +57,20 @@ class MatterEntity(Entity):
self._node_device = node_device
self._device_type_instance = device_type_instance
self.entity_description = entity_description
node = device_type_instance.node
self._unsubscribes: list[Callable] = []
# for fast lookups we create a mapping to the attribute paths
self._attributes_map: dict[type, str] = {}
server_info = matter_client.server_info
# The server info is set when the client connects to the server.
assert server_info is not None
self._attributes_map: dict[type, str] = {}
server_info = cast(ServerInfo, self.matter_client.server_info)
# create unique_id based on "Operational Instance Name" and endpoint/device type
self._attr_unique_id = (
f"{server_info.compressed_fabric_id}-"
f"{node.unique_id}-"
f"{get_operational_instance_id(server_info, self._node_device.node())}-"
f"{device_type_instance.endpoint}-"
f"{device_type_instance.device_type.device_type}"
)
@property
def device_info(self) -> DeviceInfo | None:
"""Return device info for device registry."""
return {"identifiers": {(DOMAIN, self._node_device.device_info().uniqueID)}}
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, get_device_id(server_info, node_device))}
)
async def async_added_to_hass(self) -> None:
"""Handle being added to Home Assistant."""

View File

@ -10,6 +10,10 @@ from homeassistant.core import HomeAssistant, callback
from .const import DOMAIN
if TYPE_CHECKING:
from matter_server.common.models.node import MatterNode
from matter_server.common.models.node_device import AbstractMatterNodeDevice
from matter_server.common.models.server_information import ServerInfo
from .adapter import MatterAdapter
@ -29,3 +33,27 @@ def get_matter(hass: HomeAssistant) -> MatterAdapter:
# In case of the config entry we need to fix this.
matter_entry_data: MatterEntryData = next(iter(hass.data[DOMAIN].values()))
return matter_entry_data.adapter
def get_operational_instance_id(
server_info: ServerInfo,
node: MatterNode,
) -> str:
"""Return `Operational Instance Name` for given MatterNode."""
fabric_id_hex = f"{server_info.compressed_fabric_id:016X}"
node_id_hex = f"{node.node_id:016X}"
# operational instance id matches the mdns advertisement for the node
# this is the recommended ID to recognize a unique matter node (within a fabric)
return f"{fabric_id_hex}-{node_id_hex}"
def get_device_id(
server_info: ServerInfo,
node_device: AbstractMatterNodeDevice,
) -> str:
"""Return HA device_id for the given MatterNodeDevice."""
operational_instance_id = get_operational_instance_id(
server_info, node_device.node()
)
# append nodedevice(type) to differentiate between a root node and bridge within HA devices.
return f"{operational_instance_id}-{node_device.__class__.__name__}"

View File

@ -27,8 +27,9 @@ async def test_device_registry_single_node_device(
)
dev_reg = dr.async_get(hass)
entry = dev_reg.async_get_device({(DOMAIN, "mock-onoff-light")})
entry = dev_reg.async_get_device(
{(DOMAIN, "00000000000004D2-0000000000000001-MatterNodeDevice")}
)
assert entry is not None
assert entry.name == "Mock OnOff Light"