matter-python-client 1.1.8a0.dev20260706__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matter-python-client
3
- Version: 1.1.8a0.dev20260706
3
+ Version: 1.2.0
4
4
  Summary: Python Client for the OHF Matter Server
5
5
  Author-email: Open Home Foundation <hello@openhomefoundation.io>
6
6
  License-Expression: Apache-2.0
@@ -155,7 +155,7 @@ chip/tlv/tlvlist.py,sha256=ZpT6Hr6t3qDHiUZWkY4WnioVP2icv5i5LIKNUIgOOps,2469
155
155
  matter_server/__init__.py,sha256=lJ6xyVLSMGtiHvq36VPSDb75ksxfNKYmKEEE-ZwJdhQ,47
156
156
  matter_server/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
157
157
  matter_server/client/__init__.py,sha256=pZzjJp-7C5BmYGZJjIo5SpyjGLU0FcqBa2nJ558y-Is,97
158
- matter_server/client/client.py,sha256=3EAH4_qpSpGNB9jaVa3tL24SitgA_YPDo-2vky8LFAU,31401
158
+ matter_server/client/client.py,sha256=ACyJ0JjEiM7brGa18RBeF-6iJRzp1QKcGZuPEo3r5Ak,36917
159
159
  matter_server/client/connection.py,sha256=oMZ3Ia8Z5GxnYawBJDkBKJX-icTwd0vX1vgfvouFaTk,6246
160
160
  matter_server/client/exceptions.py,sha256=741JkGMBQFJYZ70zX80B-Dn_NtgHffb6jDHC83tCL2U,1901
161
161
  matter_server/client/models/__init__.py,sha256=pT16UC2evZVw2trjun2xJZpaOR55ky7i_8znlA3Mreo,63
@@ -164,13 +164,13 @@ matter_server/client/models/node.py,sha256=j7pKZCLkZS8FRMxSDyGlLu7it7_9DRLNIHTJt
164
164
  matter_server/common/__init__.py,sha256=y4EeCH05sUYifS2b624oweA6omnY5k-rLNHHv4t4p7Q,50
165
165
  matter_server/common/const.py,sha256=hTu0siAgpQvAz6oYavTGuqgkXaQH2glQxUQuGyPnn1o,256
166
166
  matter_server/common/custom_clusters.py,sha256=Zr9zLdijaMbPGZ0879oB1NxwYj_oXNM6xKtZB_dcTug,1227
167
- matter_server/common/errors.py,sha256=ryZQXg3cS8g0aBt-6r1y0q4LO7D-gl-GZFRlv0IIuNo,2154
168
- matter_server/common/models.py,sha256=D1cFSxyzAvhEeyKsbHlVMumtA49pmKygBBPKbkBitY4,8757
167
+ matter_server/common/errors.py,sha256=idK30d48jZPm6zw9TJjHbBccz3oOgpB_W3SJaGNsj5g,3461
168
+ matter_server/common/models.py,sha256=hpR30aXn8ON2rpCjsPCo6eicifImdEr3pSj0KcyluWg,9635
169
169
  matter_server/common/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
170
170
  matter_server/common/helpers/api.py,sha256=9maToXRnef6SLGnecaTSBlWU7wDSG3elil-HtffQ2TA,1993
171
171
  matter_server/common/helpers/json.py,sha256=FktcqarAhmdI0AdiBwo7gCdJp6rA-uVdeNUglQv6HhA,1307
172
172
  matter_server/common/helpers/util.py,sha256=6vi945T2X14XQndhWW03Snj3yHbKALJHGU2qXCLvVFk,12948
173
- matter_python_client-1.1.8a0.dev20260706.dist-info/METADATA,sha256=gOGvk2P0ftmixuRI3oWb5TmXiMc2c3NItleArSi-H-k,4412
174
- matter_python_client-1.1.8a0.dev20260706.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
175
- matter_python_client-1.1.8a0.dev20260706.dist-info/top_level.txt,sha256=qqEVT8dTHhSn15gf6otxpB5dYM48Sqsucdx9W2Lq-So,19
176
- matter_python_client-1.1.8a0.dev20260706.dist-info/RECORD,,
173
+ matter_python_client-1.2.0.dist-info/METADATA,sha256=xX8pX3aplugGiF8neg7TmL2zY3zBNdD33fDRAwFtpNg,4398
174
+ matter_python_client-1.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
175
+ matter_python_client-1.2.0.dist-info/top_level.txt,sha256=qqEVT8dTHhSn15gf6otxpB5dYM48Sqsucdx9W2Lq-So,19
176
+ matter_python_client-1.2.0.dist-info/RECORD,,
@@ -25,6 +25,7 @@ from matter_server.common.models import (
25
25
  ErrorResultMessage,
26
26
  EventMessage,
27
27
  EventType,
28
+ IcdStateData,
28
29
  MatterNodeData,
29
30
  MatterNodeEvent,
30
31
  MatterSoftwareVersion,
@@ -127,32 +128,45 @@ class MatterClient:
127
128
 
128
129
  async def set_default_fabric_label(self, label: str | None) -> None:
129
130
  """Set the default fabric label."""
130
- await self.send_command(
131
- APICommand.SET_DEFAULT_FABRIC_LABEL, require_schema=11, label=label
132
- )
131
+ await self.send_command(APICommand.SET_DEFAULT_FABRIC_LABEL, require_schema=11, label=label)
132
+
133
+ async def get_fabric_label(self) -> str | None:
134
+ """Return the currently configured fabric label."""
135
+ result = await self.send_command(APICommand.GET_FABRIC_LABEL, require_schema=12)
136
+ return cast("str | None", result["fabric_label"])
133
137
 
134
138
  async def commission_with_code(
135
- self, code: str, network_only: bool = False
139
+ self,
140
+ code: str,
141
+ network_only: bool = False,
142
+ wifi_credentials_id: str | None = None,
143
+ thread_dataset_id: str | None = None,
136
144
  ) -> MatterNodeData:
137
145
  """
138
146
  Commission a device using a QR Code or Manual Pairing Code.
139
147
 
140
148
  :param code: The QR Code or Manual Pairing Code for device commissioning.
141
149
  :param network_only: If True, restricts device discovery to network only.
150
+ :param wifi_credentials_id: Optional id of the WiFi credentials entry to use.
151
+ :param thread_dataset_id: Optional id of the Thread dataset entry to use.
142
152
 
143
153
  :return: The NodeInfo of the commissioned device.
144
154
  """
155
+ extra: dict[str, str] = {}
156
+ if wifi_credentials_id is not None:
157
+ extra["wifi_credentials_id"] = wifi_credentials_id
158
+ if thread_dataset_id is not None:
159
+ extra["thread_dataset_id"] = thread_dataset_id
145
160
  data = await self.send_command(
146
161
  APICommand.COMMISSION_WITH_CODE,
147
- require_schema=6 if network_only else None,
162
+ require_schema=12 if extra else (6 if network_only else None),
148
163
  code=code,
149
164
  network_only=network_only,
165
+ **extra,
150
166
  )
151
167
  return dataclass_from_dict(MatterNodeData, data)
152
168
 
153
- async def commission_on_network(
154
- self, setup_pin_code: int, ip_addr: str | None = None
155
- ) -> MatterNodeData:
169
+ async def commission_on_network(self, setup_pin_code: int, ip_addr: str | None = None) -> MatterNodeData:
156
170
  """
157
171
  Do the routine for OnNetworkCommissioning.
158
172
 
@@ -169,23 +183,130 @@ class MatterClient:
169
183
  )
170
184
  return dataclass_from_dict(MatterNodeData, data)
171
185
 
172
- async def set_wifi_credentials(self, ssid: str, credentials: str) -> None:
186
+ async def set_wifi_credentials(self, ssid: str, credentials: str, entry_id: str = "default") -> None:
173
187
  """Set WiFi credentials for commissioning to a (new) device."""
174
188
  await self.send_command(
175
- APICommand.SET_WIFI_CREDENTIALS, ssid=ssid, credentials=credentials
189
+ APICommand.SET_WIFI_CREDENTIALS,
190
+ require_schema=12 if entry_id != "default" else None,
191
+ ssid=ssid,
192
+ credentials=credentials,
193
+ id=entry_id,
176
194
  )
177
195
 
178
- async def set_thread_operational_dataset(self, dataset: str) -> None:
196
+ async def set_thread_operational_dataset(self, dataset: str, entry_id: str = "default") -> None:
179
197
  """Set Thread Operational dataset in the stack."""
180
- await self.send_command(APICommand.SET_THREAD_DATASET, dataset=dataset)
198
+ await self.send_command(
199
+ APICommand.SET_THREAD_DATASET,
200
+ require_schema=12 if entry_id != "default" else None,
201
+ dataset=dataset,
202
+ id=entry_id,
203
+ )
204
+
205
+ async def remove_wifi_credentials(self, entry_id: str = "default") -> None:
206
+ """Remove stored WiFi credentials from the server.
207
+
208
+ :param entry_id: Id of the entry to remove; "default" removes the default entry.
209
+ """
210
+ await self.send_command(
211
+ APICommand.REMOVE_WIFI_CREDENTIALS,
212
+ require_schema=12 if entry_id != "default" else None,
213
+ id=entry_id,
214
+ )
215
+
216
+ async def remove_thread_dataset(self, entry_id: str = "default") -> None:
217
+ """Remove a stored Thread Operational dataset from the server.
218
+
219
+ :param entry_id: Id of the entry to remove; "default" removes the default entry.
220
+ """
221
+ await self.send_command(
222
+ APICommand.REMOVE_THREAD_DATASET,
223
+ require_schema=12 if entry_id != "default" else None,
224
+ id=entry_id,
225
+ )
226
+
227
+ async def get_all_credentials(self) -> dict[str, Any]:
228
+ """Return summaries (no secrets) of all stored WiFi and Thread credentials."""
229
+ result: dict[str, Any] = await self.send_command(APICommand.GET_ALL_CREDENTIALS, require_schema=12)
230
+ return result
231
+
232
+ async def get_thread_border_routers(self) -> list[dict[str, Any]]:
233
+ """Return mDNS-discovered Thread Border Routers (passive discovery).
234
+
235
+ Each entry describes a discovered Border Router (extended address, addresses,
236
+ network name, extended PAN ID, mDNS TXT metadata, …). Requires schema 12.
237
+ """
238
+ result: list[dict[str, Any]] = await self.send_command(APICommand.GET_THREAD_BORDER_ROUTERS, require_schema=12)
239
+ return result
240
+
241
+ async def get_thread_diagnostics(self, ext_pan_id: str | None = None, force: bool = False) -> Any:
242
+ """Return per-Thread-network diagnostics collected from Border Routers.
243
+
244
+ Also streamed via the ``thread_diagnostics_updated`` event. Requires schema 12.
245
+
246
+ :param ext_pan_id: 16-char hex extended PAN ID to fetch a single network (awaits a
247
+ fresh collection and returns the batch, or ``None`` when nothing is cached);
248
+ omitted returns the *current* cache for all known networks immediately (a list) and
249
+ triggers a background refresh whose fresh batches arrive via ``thread_diagnostics_updated``.
250
+ :param force: Bypass the server-side cache and re-fetch. For the all-networks form the refresh
251
+ is fire-and-forget (the call still returns the current cache at once); pass ``ext_pan_id``
252
+ when you need to await synchronously fresh data for one network.
253
+ """
254
+ args: dict[str, Any] = {}
255
+ if ext_pan_id is not None:
256
+ args["ext_pan_id"] = ext_pan_id
257
+ if force:
258
+ args["force"] = True
259
+ return await self.send_command(APICommand.GET_THREAD_DIAGNOSTICS, require_schema=12, **args)
260
+
261
+ async def get_icd_state(self, node_id: int) -> IcdStateData:
262
+ """Return the controller-side ICD (Intermittently Connected Device) state for a node.
263
+
264
+ Requires schema 12.
265
+ """
266
+ data = await self.send_command(APICommand.GET_ICD_STATE, require_schema=12, node_id=node_id)
267
+ return dataclass_from_dict(IcdStateData, data)
268
+
269
+ async def register_icd(
270
+ self,
271
+ node_id: int,
272
+ allow_multi_admin: bool | None = None,
273
+ ignored_vendors: list[int] | None = None,
274
+ ) -> IcdStateData:
275
+ """Register this controller as an ICD monitor for a node.
276
+
277
+ Requires schema 12.
278
+
279
+ :param allow_multi_admin: Proceed even when other admin fabrics may not support LIT
280
+ (raises IcdMultiAdmin otherwise).
281
+ :param ignored_vendors: Vendor ids to skip during the multi-admin check.
282
+ """
283
+ args: dict[str, Any] = {}
284
+ if allow_multi_admin is not None:
285
+ args["allow_multi_admin"] = allow_multi_admin
286
+ if ignored_vendors is not None:
287
+ args["ignored_vendors"] = ignored_vendors
288
+ data = await self.send_command(APICommand.REGISTER_ICD, require_schema=12, node_id=node_id, **args)
289
+ return dataclass_from_dict(IcdStateData, data)
181
290
 
182
- async def remove_wifi_credentials(self) -> None:
183
- """Remove any stored WiFi credentials from the server."""
184
- await self.send_command(APICommand.REMOVE_WIFI_CREDENTIALS)
291
+ async def unregister_icd(self, node_id: int, force: bool = False) -> IcdStateData:
292
+ """Unregister this controller as an ICD monitor for a node.
185
293
 
186
- async def remove_thread_dataset(self) -> None:
187
- """Remove the stored Thread Operational dataset from the server."""
188
- await self.send_command(APICommand.REMOVE_THREAD_DATASET)
294
+ Requires schema 12.
295
+
296
+ :param force: Skip the peer round-trip and only clear local registration state
297
+ (for an unreachable peer).
298
+ """
299
+ data = await self.send_command(
300
+ APICommand.UNREGISTER_ICD, require_schema=12, node_id=node_id, force=force
301
+ )
302
+ return dataclass_from_dict(IcdStateData, data)
303
+
304
+ async def resync_icd(self, node_id: int) -> None:
305
+ """Drop the local ICD registration for a node and reconnect (the peer re-registers).
306
+
307
+ Requires schema 12.
308
+ """
309
+ await self.send_command(APICommand.RESYNC_ICD, require_schema=12, node_id=node_id)
189
310
 
190
311
  async def open_commissioning_window(
191
312
  self,
@@ -233,14 +354,10 @@ class MatterClient:
233
354
  if node.available:
234
355
  await self.refresh_attribute(
235
356
  node_id,
236
- create_attribute_path_from_attribute(
237
- 0, Clusters.OperationalCredentials.Attributes.Fabrics
238
- ),
357
+ create_attribute_path_from_attribute(0, Clusters.OperationalCredentials.Attributes.Fabrics),
239
358
  )
240
359
 
241
- fabrics: list[
242
- Clusters.OperationalCredentials.Structs.FabricDescriptorStruct
243
- ] = node.get_attribute_value(
360
+ fabrics: list[Clusters.OperationalCredentials.Structs.FabricDescriptorStruct] = node.get_attribute_value(
244
361
  0, None, Clusters.OperationalCredentials.Attributes.Fabrics
245
362
  )
246
363
 
@@ -278,9 +395,7 @@ class MatterClient:
278
395
  await self.send_command(APICommand.PING_NODE, node_id=node_id),
279
396
  )
280
397
 
281
- async def get_node_ip_addresses(
282
- self, node_id: int, prefer_cache: bool = True, scoped: bool = False
283
- ) -> list[str]:
398
+ async def get_node_ip_addresses(self, node_id: int, prefer_cache: bool = True, scoped: bool = False) -> list[str]:
284
399
  """Return the currently known (scoped) IP-address(es)."""
285
400
  if TYPE_CHECKING:
286
401
  assert self.server_info is not None
@@ -300,9 +415,7 @@ class MatterClient:
300
415
  attribute = Clusters.GeneralDiagnostics.Attributes.NetworkInterfaces
301
416
  network_interface: Clusters.GeneralDiagnostics.Structs.NetworkInterface
302
417
  ip_addresses: list[str] = []
303
- for network_interface in node.get_attribute_value(
304
- 0, cluster=None, attribute=attribute
305
- ):
418
+ for network_interface in node.get_attribute_value(0, cluster=None, attribute=attribute):
306
419
  # ignore invalid/non-operational interfaces
307
420
  if not network_interface.isOperational:
308
421
  continue
@@ -326,26 +439,15 @@ class MatterClient:
326
439
  mac_address = None
327
440
  attribute = Clusters.GeneralDiagnostics.Attributes.NetworkInterfaces
328
441
  network_interface: Clusters.GeneralDiagnostics.Structs.NetworkInterface
329
- for network_interface in (
330
- node.get_attribute_value(0, cluster=None, attribute=attribute) or []
331
- ):
442
+ for network_interface in node.get_attribute_value(0, cluster=None, attribute=attribute) or []:
332
443
  # ignore invalid/non-operational interfaces
333
444
  if not network_interface.isOperational:
334
445
  continue
335
- if (
336
- network_interface.type
337
- == Clusters.GeneralDiagnostics.Enums.InterfaceTypeEnum.kThread
338
- ):
446
+ if network_interface.type == Clusters.GeneralDiagnostics.Enums.InterfaceTypeEnum.kThread:
339
447
  network_type = NetworkType.THREAD
340
- elif (
341
- network_interface.type
342
- == Clusters.GeneralDiagnostics.Enums.InterfaceTypeEnum.kWiFi
343
- ):
448
+ elif network_interface.type == Clusters.GeneralDiagnostics.Enums.InterfaceTypeEnum.kWiFi:
344
449
  network_type = NetworkType.WIFI
345
- elif (
346
- network_interface.type
347
- == Clusters.GeneralDiagnostics.Enums.InterfaceTypeEnum.kEthernet
348
- ):
450
+ elif network_interface.type == Clusters.GeneralDiagnostics.Enums.InterfaceTypeEnum.kEthernet:
349
451
  network_type = NetworkType.ETHERNET
350
452
  else:
351
453
  # unknown interface: ignore
@@ -362,14 +464,10 @@ class MatterClient:
362
464
  node_type = NodeType.UNKNOWN
363
465
  network_name = None
364
466
  if network_type == NetworkType.THREAD:
365
- thread_cluster: Clusters.ThreadNetworkDiagnostics = node.get_cluster(
366
- 0, Clusters.ThreadNetworkDiagnostics
367
- )
467
+ thread_cluster: Clusters.ThreadNetworkDiagnostics = node.get_cluster(0, Clusters.ThreadNetworkDiagnostics)
368
468
  if thread_cluster:
369
469
  if isinstance(thread_cluster.networkName, bytes):
370
- network_name = thread_cluster.networkName.decode(
371
- "utf-8", errors="replace"
372
- )
470
+ network_name = thread_cluster.networkName.decode("utf-8", errors="replace")
373
471
  elif thread_cluster.networkName != NullValue:
374
472
  network_name = thread_cluster.networkName
375
473
 
@@ -428,9 +526,7 @@ class MatterClient:
428
526
  # get active fabrics for this node
429
527
  active_fabrics = await self.get_matter_fabrics(node_id)
430
528
  # get active fabric index
431
- fabric_index = node.get_attribute_value(
432
- 0, None, Clusters.OperationalCredentials.Attributes.CurrentFabricIndex
433
- )
529
+ fabric_index = node.get_attribute_value(0, None, Clusters.OperationalCredentials.Attributes.CurrentFabricIndex)
434
530
  return NodeDiagnostics(
435
531
  node_id=node_id,
436
532
  network_type=network_type,
@@ -526,9 +622,7 @@ class MatterClient:
526
622
 
527
623
  The "softwareVersionString" is a human friendly version string.
528
624
  """
529
- data = await self.send_command(
530
- APICommand.CHECK_NODE_UPDATE, node_id=node_id, require_schema=10
531
- )
625
+ data = await self.send_command(APICommand.CHECK_NODE_UPDATE, node_id=node_id, require_schema=10)
532
626
  if data is None:
533
627
  return None
534
628
 
@@ -561,6 +655,7 @@ class MatterClient:
561
655
  """
562
656
  response = await self.send_command(
563
657
  APICommand.SEND_WEBRTC_PROVIDER_COMMAND,
658
+ require_schema=12,
564
659
  node_id=node_id,
565
660
  endpoint_id=endpoint_id,
566
661
  command_name=command_name,
@@ -643,19 +738,12 @@ class MatterClient:
643
738
  await self.connect()
644
739
 
645
740
  try:
646
- message = CommandMessage(
647
- message_id=uuid.uuid4().hex, command=APICommand.START_LISTENING
648
- )
741
+ message = CommandMessage(message_id=uuid.uuid4().hex, command=APICommand.START_LISTENING)
649
742
  await self.connection.send_message(message)
650
- nodes_msg = cast(
651
- SuccessResultMessage, await self.connection.receive_message_or_raise()
652
- )
743
+ nodes_msg = cast(SuccessResultMessage, await self.connection.receive_message_or_raise())
653
744
  # a full dump of all nodes will be the result of the start_listening command
654
745
  # create MatterNode objects from the basic MatterNodeData objects
655
- nodes = [
656
- MatterNode(dataclass_from_dict(MatterNodeData, x))
657
- for x in nodes_msg.result
658
- ]
746
+ nodes = [MatterNode(dataclass_from_dict(MatterNodeData, x)) for x in nodes_msg.result]
659
747
  self._nodes = {node.node_id: node for node in nodes}
660
748
  # once we've hit this point we're all set
661
749
  self.logger.info("Matter client initialized.")
@@ -741,9 +829,7 @@ class MatterClient:
741
829
  node_id = msg.data["node_id"]
742
830
  endpoint_id = msg.data["endpoint_id"]
743
831
  self.logger.debug("Endpoint removed: %s/%s", node_id, endpoint_id)
744
- self._signal_event(
745
- EventType.ENDPOINT_REMOVED, data=msg.data, node_id=node_id
746
- )
832
+ self._signal_event(EventType.ENDPOINT_REMOVED, data=msg.data, node_id=node_id)
747
833
  # cleanup endpoint only after signalling subscribers
748
834
  if node := self._nodes.get(node_id):
749
835
  node.endpoints.pop(endpoint_id, None)
@@ -783,6 +869,12 @@ class MatterClient:
783
869
  node_id=node_event.node_id,
784
870
  )
785
871
  return
872
+ # An event type unknown to this (older) client is passed through by parse_value as a raw
873
+ # string; forwarding it would crash on `event.value` in _signal_event. Drop it instead so a
874
+ # newer server emitting a new event type can never disconnect the client.
875
+ if not isinstance(msg.event, EventType):
876
+ self.logger.debug("Ignoring unknown event type: %s", msg.event)
877
+ return
786
878
  # simply forward all other events as-is
787
879
  if self.logger.isEnabledFor(logging.DEBUG):
788
880
  self.logger.debug("Received event: %s", msg)
@@ -814,9 +906,7 @@ class MatterClient:
814
906
  await self.connect()
815
907
  return self
816
908
 
817
- async def __aexit__(
818
- self, exc_type: Exception, exc_value: str, traceback: TracebackType
819
- ) -> None:
909
+ async def __aexit__(self, exc_type: Exception, exc_value: str, traceback: TracebackType) -> None:
820
910
  """Disconnect from the websocket."""
821
911
  await self.disconnect()
822
912
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
6
+
5
7
  # mapping from error_code to Exception class
6
8
  ERROR_MAP: dict[int, type] = {}
7
9
 
@@ -89,6 +91,39 @@ class UpdateError(MatterError):
89
91
  error_code = 11
90
92
 
91
93
 
94
+ class IcdMultiAdmin(MatterError):
95
+ """Error raised when ICD registration is rejected due to other admin fabrics.
96
+
97
+ OHF extension (python-matter-server codes stop at 11); `details` is a JSON string
98
+ `{"message": str, "admin_vendor_ids": list[int]}`.
99
+ """
100
+
101
+ error_code = 100
102
+
103
+ _FALLBACK_MESSAGE = "ICD registration rejected: the peer has administrator fabrics from other vendors"
104
+
105
+ def __init__(self, details: str | None = None) -> None:
106
+ """Parse `details` and expose `admin_vendor_ids`."""
107
+ self.admin_vendor_ids: list[int] = []
108
+ message = details if details else self._FALLBACK_MESSAGE
109
+ if details:
110
+ try:
111
+ parsed = json.loads(details)
112
+ except (json.JSONDecodeError, TypeError):
113
+ parsed = None
114
+ if isinstance(parsed, dict):
115
+ vendor_ids = parsed.get("admin_vendor_ids")
116
+ if isinstance(vendor_ids, list):
117
+ self.admin_vendor_ids = vendor_ids
118
+ parsed_message = parsed.get("message")
119
+ message = (
120
+ parsed_message
121
+ if isinstance(parsed_message, str) and parsed_message
122
+ else self._FALLBACK_MESSAGE
123
+ )
124
+ super().__init__(message)
125
+
126
+
92
127
  def exception_from_error_code(error_code: int) -> type[MatterError]:
93
128
  """Return correct Exception class from error_code."""
94
129
  return ERROR_MAP.get(error_code, MatterError)
@@ -24,6 +24,7 @@ class EventType(Enum):
24
24
  ENDPOINT_ADDED = "endpoint_added"
25
25
  ENDPOINT_REMOVED = "endpoint_removed"
26
26
  WEBRTC_CALLBACK = "webrtc_callback"
27
+ THREAD_DIAGNOSTICS_UPDATED = "thread_diagnostics_updated" # schema 12+
27
28
 
28
29
 
29
30
  class APICommand(str, Enum):
@@ -54,9 +55,17 @@ class APICommand(str, Enum):
54
55
  CHECK_NODE_UPDATE = "check_node_update"
55
56
  UPDATE_NODE = "update_node"
56
57
  SET_DEFAULT_FABRIC_LABEL = "set_default_fabric_label"
58
+ GET_FABRIC_LABEL = "get_fabric_label"
57
59
  SET_ACL_ENTRY = "set_acl_entry"
58
60
  SET_NODE_BINDING = "set_node_binding"
59
61
  SEND_WEBRTC_PROVIDER_COMMAND = "send_webrtc_provider_command"
62
+ GET_ALL_CREDENTIALS = "get_all_credentials"
63
+ GET_THREAD_BORDER_ROUTERS = "get_thread_border_routers"
64
+ GET_THREAD_DIAGNOSTICS = "get_thread_diagnostics"
65
+ GET_ICD_STATE = "get_icd_state"
66
+ REGISTER_ICD = "register_icd"
67
+ RESYNC_ICD = "resync_icd"
68
+ UNREGISTER_ICD = "unregister_icd"
60
69
 
61
70
 
62
71
  EventCallBackType = Callable[[EventType, Any], None]
@@ -91,9 +100,7 @@ class MatterNodeData:
91
100
  # all attribute subscriptions we need to persist for this node,
92
101
  # a set of tuples in format (endpoint_id, cluster_id, attribute_id)
93
102
  # where each value can also be a None for wildcard
94
- attribute_subscriptions: set[tuple[int | None, int | None, int | None]] = field(
95
- default_factory=set
96
- )
103
+ attribute_subscriptions: set[tuple[int | None, int | None, int | None]] = field(default_factory=set)
97
104
 
98
105
 
99
106
  @dataclass
@@ -192,7 +199,9 @@ class ErrorResultMessage(ResultMessageBase):
192
199
  class EventMessage:
193
200
  """Message sent when server or client signals a (stateless) event."""
194
201
 
195
- event: EventType
202
+ # An event type unknown to this client is passed through by parse_value as a raw string rather
203
+ # than an EventType, so consumers must narrow before relying on enum members.
204
+ event: EventType | str
196
205
  data: Any
197
206
 
198
207
 
@@ -212,13 +221,7 @@ class ServerInfoMessage:
212
221
  ble_proxy_enabled: bool = False
213
222
 
214
223
 
215
- MessageType = (
216
- CommandMessage
217
- | EventMessage
218
- | SuccessResultMessage
219
- | ErrorResultMessage
220
- | ServerInfoMessage
221
- )
224
+ MessageType = CommandMessage | EventMessage | SuccessResultMessage | ErrorResultMessage | ServerInfoMessage
222
225
 
223
226
 
224
227
  @dataclass
@@ -254,6 +257,19 @@ class CommissioningParameters:
254
257
  setup_qr_code: str
255
258
 
256
259
 
260
+ @dataclass
261
+ class IcdStateData:
262
+ """ICD controller-side state for a node. Note: Only available with OHF Matter Server."""
263
+
264
+ supported: bool
265
+ lit_supported: bool
266
+ registered: bool
267
+ operating_mode: str | None
268
+ awake: bool | None
269
+ available: bool | None
270
+ next_expected_checkin: int | None
271
+
272
+
257
273
  class UpdateSource(Enum):
258
274
  """Enum with possible sources for a software update."""
259
275