diff --git a/homeassistant/components/matter/__init__.py b/homeassistant/components/matter/__init__.py index a2aa2c5ceff..b58c4562994 100644 --- a/homeassistant/components/matter/__init__.py +++ b/homeassistant/components/matter/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from contextlib import suppress +from functools import cache from matter_server.client import MatterClient from matter_server.client.exceptions import CannotConnect, InvalidServerVersion @@ -28,12 +29,34 @@ from .addon import get_addon_manager from .api import async_register_api from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON, DOMAIN, LOGGER from .discovery import SUPPORTED_PLATFORMS -from .helpers import MatterEntryData, get_matter, get_node_from_device_entry +from .helpers import ( + MatterEntryData, + get_matter, + get_node_from_device_entry, + node_from_ha_device_id, +) +from .models import MatterDeviceInfo CONNECT_TIMEOUT = 10 LISTEN_READY_TIMEOUT = 30 +@callback +@cache +def get_matter_device_info( + hass: HomeAssistant, device_id: str +) -> MatterDeviceInfo | None: + """Return Matter device info or None if device does not exist.""" + if not (node := node_from_ha_device_id(hass, device_id)): + return None + + return MatterDeviceInfo( + unique_id=node.device_info.uniqueID, + vendor_id=hex(node.device_info.vendorID), + product_id=hex(node.device_info.productID), + ) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Matter from a config entry.""" if use_addon := entry.data.get(CONF_USE_ADDON): @@ -190,7 +213,7 @@ async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry ) -> bool: """Remove a config entry from a device.""" - node = await get_node_from_device_entry(hass, device_entry) + node = get_node_from_device_entry(hass, device_entry) if node is None: return True @@ -218,21 +241,11 @@ async def async_remove_config_entry_device( def _async_init_services(hass: HomeAssistant) -> None: """Init services.""" - async def _node_id_from_ha_device_id(ha_device_id: str) -> int | None: - """Get node id from ha device id.""" - dev_reg = dr.async_get(hass) - device = dev_reg.async_get(ha_device_id) - if device is None: - return None - if node := await get_node_from_device_entry(hass, device): - return node.node_id - return None - async def open_commissioning_window(call: ServiceCall) -> None: """Open commissioning window on specific node.""" - node_id = await _node_id_from_ha_device_id(call.data["device_id"]) + node = node_from_ha_device_id(hass, call.data["device_id"]) - if node_id is None: + if node is None: raise HomeAssistantError("This is not a Matter device") matter_client = get_matter(hass).matter_client @@ -240,7 +253,7 @@ def _async_init_services(hass: HomeAssistant) -> None: # We are sending device ID . try: - await matter_client.open_commissioning_window(node_id) + await matter_client.open_commissioning_window(node.node_id) except NodeCommissionFailed as err: raise HomeAssistantError(str(err)) from err diff --git a/homeassistant/components/matter/diagnostics.py b/homeassistant/components/matter/diagnostics.py index bcb41cc0462..8846a75b42a 100644 --- a/homeassistant/components/matter/diagnostics.py +++ b/homeassistant/components/matter/diagnostics.py @@ -58,7 +58,7 @@ async def async_get_device_diagnostics( """Return diagnostics for a device.""" matter = get_matter(hass) server_diagnostics = await matter.matter_client.get_diagnostics() - node = await get_node_from_device_entry(hass, device) + node = get_node_from_device_entry(hass, device) return { "server_info": dataclass_to_dict(server_diagnostics.info), diff --git a/homeassistant/components/matter/helpers.py b/homeassistant/components/matter/helpers.py index 0274c80edf8..dcd6a30ee1f 100644 --- a/homeassistant/components/matter/helpers.py +++ b/homeassistant/components/matter/helpers.py @@ -66,7 +66,18 @@ def get_device_id( return f"{operational_instance_id}-{postfix}" -async def get_node_from_device_entry( +@callback +def node_from_ha_device_id(hass: HomeAssistant, ha_device_id: str) -> MatterNode | None: + """Get node id from ha device id.""" + dev_reg = dr.async_get(hass) + device = dev_reg.async_get(ha_device_id) + if device is None: + raise ValueError("Invalid device ID") + return get_node_from_device_entry(hass, device) + + +@callback +def get_node_from_device_entry( hass: HomeAssistant, device: dr.DeviceEntry ) -> MatterNode | None: """Return MatterNode from device entry.""" diff --git a/homeassistant/components/matter/models.py b/homeassistant/components/matter/models.py index 3ac7f66b83f..34447751797 100644 --- a/homeassistant/components/matter/models.py +++ b/homeassistant/components/matter/models.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import TypedDict from chip.clusters import Objects as clusters from chip.clusters.Objects import ClusterAttributeDescriptor @@ -16,6 +17,20 @@ SensorValueTypes = type[ ] +class MatterDeviceInfo(TypedDict): + """Dictionary with Matter Device info. + + Used to send to other Matter controllers, + such as Google Home to prevent duplicated devices. + + Reference: https://developers.home.google.com/matter/device-deduplication + """ + + unique_id: str + vendor_id: str # vendorId hex string + product_id: str # productId hex string + + @dataclass class MatterEntityInfo: """Info discovered from (primary) Matter Attribute to create entity.""" diff --git a/tests/components/matter/test_helpers.py b/tests/components/matter/test_helpers.py index 36761362618..f7399d6aaf1 100644 --- a/tests/components/matter/test_helpers.py +++ b/tests/components/matter/test_helpers.py @@ -56,7 +56,7 @@ async def test_get_node_from_device_entry( device_registry, config_entry.entry_id )[0] assert device_entry - node_from_device_entry = await get_node_from_device_entry(hass, device_entry) + node_from_device_entry = get_node_from_device_entry(hass, device_entry) assert node_from_device_entry is node