Home Assistant Unofficial Reference 2024.12.1
adapter.py
Go to the documentation of this file.
1 """Matter to Home Assistant adapter."""
2 
3 from __future__ import annotations
4 
5 from typing import TYPE_CHECKING, cast
6 
7 from chip.clusters import Objects as clusters
8 from matter_server.client.models.device_types import BridgedDevice
9 from matter_server.common.models import EventType, ServerInfoMessage
10 
11 from homeassistant.config_entries import ConfigEntry
12 from homeassistant.const import Platform
13 from homeassistant.core import HomeAssistant
14 from homeassistant.helpers import device_registry as dr
15 from homeassistant.helpers.entity_platform import AddEntitiesCallback
16 
17 from .const import DOMAIN, ID_TYPE_DEVICE_ID, ID_TYPE_SERIAL, LOGGER
18 from .discovery import async_discover_entities
19 from .helpers import get_device_id
20 
21 if TYPE_CHECKING:
22  from matter_server.client import MatterClient
23  from matter_server.client.models.node import MatterEndpoint, MatterNode
24 
25 
26 def get_clean_name(name: str | None) -> str | None:
27  """Strip spaces and null char from the name."""
28  if name is None:
29  return name
30  name = name.replace("\x00", "")
31  return name.strip() or None
32 
33 
35  """Connect Matter into Home Assistant."""
36 
37  def __init__(
38  self,
39  hass: HomeAssistant,
40  matter_client: MatterClient,
41  config_entry: ConfigEntry,
42  ) -> None:
43  """Initialize the adapter."""
44  self.matter_clientmatter_client = matter_client
45  self.hasshass = hass
46  self.config_entryconfig_entry = config_entry
47  self.platform_handlers: dict[Platform, AddEntitiesCallback] = {}
48  self.discovered_entities: set[str] = set()
49 
51  self, platform: Platform, add_entities: AddEntitiesCallback
52  ) -> None:
53  """Register a platform handler."""
54  self.platform_handlers[platform] = add_entities
55 
56  async def setup_nodes(self) -> None:
57  """Set up all existing nodes and subscribe to new nodes."""
58  for node in self.matter_clientmatter_client.get_nodes():
59  self._setup_node_setup_node(node)
60 
61  def node_added_callback(event: EventType, node: MatterNode) -> None:
62  """Handle node added event."""
63  self._setup_node_setup_node(node)
64 
65  def node_updated_callback(event: EventType, node: MatterNode) -> None:
66  """Handle node updated event."""
67  if not node.available:
68  return
69  # We always run the discovery logic again,
70  # because the firmware version could have been changed or features added.
71  self._setup_node_setup_node(node)
72 
73  def endpoint_added_callback(event: EventType, data: dict[str, int]) -> None:
74  """Handle endpoint added event."""
75  node = self.matter_clientmatter_client.get_node(data["node_id"])
76  self._setup_endpoint_setup_endpoint(node.endpoints[data["endpoint_id"]])
77 
78  def endpoint_removed_callback(event: EventType, data: dict[str, int]) -> None:
79  """Handle endpoint removed event."""
80  server_info = cast(ServerInfoMessage, self.matter_clientmatter_client.server_info)
81  try:
82  node = self.matter_clientmatter_client.get_node(data["node_id"])
83  except KeyError:
84  return # race condition
85  device_registry = dr.async_get(self.hasshass)
86  endpoint = node.endpoints.get(data["endpoint_id"])
87  if not endpoint:
88  return # race condition
89  node_device_id = get_device_id(
90  server_info,
91  node.endpoints[data["endpoint_id"]],
92  )
93  identifier = (DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}")
94  if device := device_registry.async_get_device(identifiers={identifier}):
95  device_registry.async_remove_device(device.id)
96 
97  def node_removed_callback(event: EventType, node_id: int) -> None:
98  """Handle node removed event."""
99  try:
100  node = self.matter_clientmatter_client.get_node(node_id)
101  except KeyError:
102  return # race condition
103  for endpoint_id in node.endpoints:
104  endpoint_removed_callback(
105  EventType.ENDPOINT_REMOVED,
106  {"node_id": node_id, "endpoint_id": endpoint_id},
107  )
108 
109  self.config_entryconfig_entry.async_on_unload(
110  self.matter_clientmatter_client.subscribe_events(
111  callback=endpoint_added_callback, event_filter=EventType.ENDPOINT_ADDED
112  )
113  )
114  self.config_entryconfig_entry.async_on_unload(
115  self.matter_clientmatter_client.subscribe_events(
116  callback=endpoint_removed_callback,
117  event_filter=EventType.ENDPOINT_REMOVED,
118  )
119  )
120  self.config_entryconfig_entry.async_on_unload(
121  self.matter_clientmatter_client.subscribe_events(
122  callback=node_removed_callback, event_filter=EventType.NODE_REMOVED
123  )
124  )
125  self.config_entryconfig_entry.async_on_unload(
126  self.matter_clientmatter_client.subscribe_events(
127  callback=node_added_callback, event_filter=EventType.NODE_ADDED
128  )
129  )
130  self.config_entryconfig_entry.async_on_unload(
131  self.matter_clientmatter_client.subscribe_events(
132  callback=node_updated_callback, event_filter=EventType.NODE_UPDATED
133  )
134  )
135 
136  def _setup_node(self, node: MatterNode) -> None:
137  """Set up an node."""
138  LOGGER.debug("Setting up entities for node %s", node.node_id)
139  try:
140  for endpoint in node.endpoints.values():
141  # Node endpoints are translated into HA devices
142  self._setup_endpoint_setup_endpoint(endpoint)
143  except Exception as err: # noqa: BLE001
144  # We don't want to crash the whole setup when a single node fails to setup
145  # for whatever reason, so we catch all exceptions here.
146  LOGGER.exception(
147  "Error setting up node %s: %s",
148  node.node_id,
149  err,
150  )
151 
153  self,
154  endpoint: MatterEndpoint,
155  ) -> None:
156  """Create a device registry entry for a MatterNode."""
157  server_info = cast(ServerInfoMessage, self.matter_clientmatter_client.server_info)
158 
159  basic_info = endpoint.device_info
160  # use (first) DeviceType of the endpoint as fallback product name
161  device_type = next(
162  (
163  x
164  for x in endpoint.device_types
165  if x.device_type != BridgedDevice.device_type
166  ),
167  None,
168  )
169  name = (
170  get_clean_name(basic_info.nodeLabel)
171  or get_clean_name(basic_info.productLabel)
172  or get_clean_name(basic_info.productName)
173  or (device_type.__name__ if device_type else None)
174  )
175 
176  # handle bridged devices
177  bridge_device_id = None
178  if endpoint.is_bridged_device and endpoint.node.endpoints[0] != endpoint:
179  bridge_device_id = get_device_id(
180  server_info,
181  endpoint.node.endpoints[0],
182  )
183  bridge_device_id = f"{ID_TYPE_DEVICE_ID}_{bridge_device_id}"
184 
185  node_device_id = get_device_id(
186  server_info,
187  endpoint,
188  )
189  identifiers = {(DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}")}
190  serial_number: str | None = None
191  # if available, we also add the serialnumber as identifier
192  if (
193  basic_info_serial_number := basic_info.serialNumber
194  ) and "test" not in basic_info_serial_number.lower():
195  # prefix identifier with 'serial_' to be able to filter it
196  identifiers.add((DOMAIN, f"{ID_TYPE_SERIAL}_{basic_info_serial_number}"))
197  serial_number = basic_info_serial_number
198 
199  # Model name is the human readable name of the model/product name
200  model_name = (
201  # productLabel is optional but preferred (e.g. Hue Bloom)
202  get_clean_name(basic_info.productLabel)
203  # alternative is the productName (e.g. LCT001)
204  or get_clean_name(basic_info.productName)
205  # if no product name, use the device type name
206  or device_type.__name__
207  if device_type
208  else None
209  )
210  # Model ID is the non-human readable product ID
211  # we prefer the matter product ID so we can look it up in Matter DCL
212  if isinstance(basic_info, clusters.BridgedDeviceBasicInformation):
213  # On bridged devices, the productID is not available
214  model_id = None
215  else:
216  model_id = str(product_id) if (product_id := basic_info.productID) else None
217 
218  dr.async_get(self.hasshass).async_get_or_create(
219  name=name,
220  config_entry_id=self.config_entryconfig_entry.entry_id,
221  identifiers=identifiers,
222  hw_version=basic_info.hardwareVersionString,
223  sw_version=basic_info.softwareVersionString,
224  manufacturer=basic_info.vendorName or endpoint.node.device_info.vendorName,
225  model=model_name,
226  model_id=model_id,
227  serial_number=serial_number,
228  via_device=(DOMAIN, bridge_device_id) if bridge_device_id else None,
229  )
230 
231  def _setup_endpoint(self, endpoint: MatterEndpoint) -> None:
232  """Set up a MatterEndpoint as HA Device."""
233  # pre-create device registry entry
234  self._create_device_registry_create_device_registry(endpoint)
235  # run platform discovery from device type instances
236  for entity_info in async_discover_entities(endpoint):
237  discovery_key = (
238  f"{entity_info.platform}_{endpoint.node.node_id}_{endpoint.endpoint_id}_"
239  f"{entity_info.primary_attribute.cluster_id}_"
240  f"{entity_info.primary_attribute.attribute_id}_"
241  f"{entity_info.entity_description.key}"
242  )
243  if discovery_key in self.discovered_entities:
244  continue
245  LOGGER.debug(
246  "Creating %s entity for %s",
247  entity_info.platform,
248  entity_info.primary_attribute,
249  )
250  self.discovered_entities.add(discovery_key)
251  new_entity = entity_info.entity_class(
252  self.matter_clientmatter_client, endpoint, entity_info
253  )
254  self.platform_handlers[entity_info.platform]([new_entity])
None _setup_endpoint(self, MatterEndpoint endpoint)
Definition: adapter.py:231
None __init__(self, HomeAssistant hass, MatterClient matter_client, ConfigEntry config_entry)
Definition: adapter.py:42
None register_platform_handler(self, Platform platform, AddEntitiesCallback add_entities)
Definition: adapter.py:52
None _create_device_registry(self, MatterEndpoint endpoint)
Definition: adapter.py:155
bool add(self, _T matcher)
Definition: match.py:185
str|None get_clean_name(str|None name)
Definition: adapter.py:26
Generator[MatterEntityInfo] async_discover_entities(MatterEndpoint endpoint)
Definition: discovery.py:61
str get_device_id(ServerInfoMessage server_info, MatterEndpoint endpoint)
Definition: helpers.py:59