Skip to content

Commit ab4dc0f

Browse files
authored
Convert to asyncio Matter SDK API (#765)
1 parent 190411a commit ab4dc0f

File tree

3 files changed

+68
-89
lines changed

3 files changed

+68
-89
lines changed

matter_server/server/device_controller.py

+38-51
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,6 @@
6262
from collections.abc import Iterable
6363
from pathlib import Path
6464

65-
from chip.native import PyChipError
66-
6765
from .server import MatterServer
6866

6967
DATA_KEY_NODES = "nodes"
@@ -73,7 +71,6 @@
7371
NODE_SUBSCRIPTION_CEILING_WIFI = 60
7472
NODE_SUBSCRIPTION_CEILING_THREAD = 60
7573
NODE_SUBSCRIPTION_CEILING_BATTERY_POWERED = 600
76-
MAX_COMMISSION_RETRIES = 3
7774
NODE_RESUBSCRIBE_ATTEMPTS_UNAVAILABLE = 3
7875
NODE_RESUBSCRIBE_TIMEOUT_OFFLINE = 30 * 60 * 1000
7976
NODE_PING_TIMEOUT = 10
@@ -262,34 +259,29 @@ async def commission_with_code(
262259
"""
263260
node_id = self._get_next_node_id()
264261

265-
attempts = 0
266-
# we retry commissioning a few times as we've seen devices in the wild
267-
# that are a bit unstable.
268-
# by retrying, we increase the chances of a successful commission
269-
while attempts <= MAX_COMMISSION_RETRIES:
270-
attempts += 1
271-
LOGGER.info(
272-
"Starting Matter commissioning with code using Node ID %s (attempt %s/%s).",
273-
node_id,
274-
attempts,
275-
MAX_COMMISSION_RETRIES,
276-
)
277-
result: (
278-
PyChipError | None
279-
) = await self._chip_device_controller.commission_with_code(
280-
node_id,
281-
code,
282-
DiscoveryType.DISCOVERY_NETWORK_ONLY
283-
if network_only
284-
else DiscoveryType.DISCOVERY_ALL,
285-
)
286-
if result and result.is_success:
287-
break
288-
if attempts >= MAX_COMMISSION_RETRIES:
289-
raise NodeCommissionFailed(
290-
f"Commission with code failed for node {node_id}."
262+
LOGGER.info(
263+
"Starting Matter commissioning with code using Node ID %s.",
264+
node_id,
265+
)
266+
try:
267+
commissioned_node_id: int = (
268+
await self._chip_device_controller.commission_with_code(
269+
node_id,
270+
code,
271+
DiscoveryType.DISCOVERY_NETWORK_ONLY
272+
if network_only
273+
else DiscoveryType.DISCOVERY_ALL,
291274
)
292-
await asyncio.sleep(5)
275+
)
276+
# We use SDK default behavior which always uses the commissioning Node ID in the
277+
# generated NOC. So this should be the same really.
278+
LOGGER.info("Commissioned Node ID: %s vs %s", commissioned_node_id, node_id)
279+
if commissioned_node_id != node_id:
280+
raise RuntimeError("Returned Node ID must match requested Node ID")
281+
except ChipStackError as err:
282+
raise NodeCommissionFailed(
283+
f"Commission with code failed for node {node_id}."
284+
) from err
293285

294286
LOGGER.info("Matter commissioning of Node ID %s successful.", node_id)
295287

@@ -340,40 +332,35 @@ async def commission_on_network(
340332
if ip_addr is not None:
341333
ip_addr = self.server.scope_ipv6_lla(ip_addr)
342334

343-
attempts = 0
344-
# we retry commissioning a few times as we've seen devices in the wild
345-
# that are a bit unstable.
346-
# by retrying, we increase the chances of a successful commission
347-
while attempts <= MAX_COMMISSION_RETRIES:
348-
attempts += 1
349-
result: PyChipError | None
335+
try:
350336
if ip_addr is None:
351337
# regular CommissionOnNetwork if no IP address provided
352338
LOGGER.info(
353-
"Starting Matter commissioning on network using Node ID %s (attempt %s/%s).",
339+
"Starting Matter commissioning on network using Node ID %s.",
354340
node_id,
355-
attempts,
356-
MAX_COMMISSION_RETRIES,
357341
)
358-
result = await self._chip_device_controller.commission_on_network(
359-
node_id, setup_pin_code, filter_type, filter
342+
commissioned_node_id = (
343+
await self._chip_device_controller.commission_on_network(
344+
node_id, setup_pin_code, filter_type, filter
345+
)
360346
)
361347
else:
362348
LOGGER.info(
363-
"Starting Matter commissioning using Node ID %s and IP %s (attempt %s/%s).",
349+
"Starting Matter commissioning using Node ID %s and IP %s.",
364350
node_id,
365351
ip_addr,
366-
attempts,
367-
MAX_COMMISSION_RETRIES,
368352
)
369-
result = await self._chip_device_controller.commission_ip(
353+
commissioned_node_id = await self._chip_device_controller.commission_ip(
370354
node_id, setup_pin_code, ip_addr
371355
)
372-
if result and result.is_success:
373-
break
374-
if attempts >= MAX_COMMISSION_RETRIES:
375-
raise NodeCommissionFailed(f"Commissioning failed for node {node_id}.")
376-
await asyncio.sleep(5)
356+
# We use SDK default behavior which always uses the commissioning Node ID in the
357+
# generated NOC. So this should be the same really.
358+
if commissioned_node_id != node_id:
359+
raise RuntimeError("Returned Node ID must match requested Node ID")
360+
except ChipStackError as err:
361+
raise NodeCommissionFailed(
362+
f"Commissioning failed for node {node_id}."
363+
) from err
377364

378365
LOGGER.info("Matter commissioning of Node ID %s successful.", node_id)
379366

matter_server/server/sdk.py

+28-36
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from __future__ import annotations
99

1010
import asyncio
11-
from concurrent.futures import ThreadPoolExecutor
1211
from functools import partial
1312
import logging
1413
import time
@@ -25,6 +24,7 @@
2524

2625
if TYPE_CHECKING:
2726
from collections.abc import Callable
27+
from concurrent.futures import ThreadPoolExecutor
2828
from pathlib import Path
2929

3030
from chip.ChipDeviceCtrl import (
@@ -59,7 +59,6 @@ def __init__(self, server: MatterServer, paa_root_cert_dir: Path):
5959

6060
self._node_lock: dict[int, asyncio.Lock] = {}
6161
self._subscriptions: dict[int, Attribute.SubscriptionTransaction] = {}
62-
self._sdk_non_entrant_executor = ThreadPoolExecutor(max_workers=1)
6362

6463
# Instantiate the underlying ChipDeviceController instance on the Fabric
6564
self._chip_controller = self.server.stack.fabric_admin.NewController(
@@ -100,16 +99,6 @@ async def _call_sdk(
10099
) -> _T:
101100
return await self._call_sdk_executor(None, target, *args, **kwargs)
102101

103-
async def _call_sdk_non_reentrant(
104-
self,
105-
target: Callable[..., _T],
106-
*args: Any,
107-
**kwargs: Any,
108-
) -> _T:
109-
return await self._call_sdk_executor(
110-
self._sdk_non_entrant_executor, target, *args, **kwargs
111-
)
112-
113102
async def get_compressed_fabric_id(self) -> int:
114103
"""Get the compressed fabric id."""
115104
return await self._call_sdk(self._chip_controller.GetCompressedFabricId)
@@ -128,13 +117,15 @@ async def commission_with_code(
128117
node_id: int,
129118
setup_payload: str,
130119
discovery_type: DiscoveryType,
131-
) -> PyChipError:
120+
) -> int:
132121
"""Commission a device using a QR Code or Manual Pairing Code."""
133-
return await self._call_sdk_non_reentrant(
134-
self._chip_controller.CommissionWithCode,
135-
setupPayload=setup_payload,
136-
nodeid=node_id,
137-
discoveryType=discovery_type,
122+
return cast(
123+
int,
124+
await self._chip_controller.CommissionWithCode(
125+
setupPayload=setup_payload,
126+
nodeid=node_id,
127+
discoveryType=discovery_type,
128+
),
138129
)
139130

140131
async def commission_on_network(
@@ -143,25 +134,29 @@ async def commission_on_network(
143134
setup_pin_code: int,
144135
disc_filter_type: FilterType = FilterType.NONE,
145136
disc_filter: Any = None,
146-
) -> PyChipError:
137+
) -> int:
147138
"""Commission a device on the network."""
148-
return await self._call_sdk_non_reentrant(
149-
self._chip_controller.CommissionOnNetwork,
150-
nodeId=node_id,
151-
setupPinCode=setup_pin_code,
152-
filterType=disc_filter_type,
153-
filter=disc_filter,
139+
return cast(
140+
int,
141+
await self._chip_controller.CommissionOnNetwork(
142+
nodeId=node_id,
143+
setupPinCode=setup_pin_code,
144+
filterType=disc_filter_type,
145+
filter=disc_filter,
146+
),
154147
)
155148

156149
async def commission_ip(
157150
self, node_id: int, setup_pin_code: int, ip_addr: str
158-
) -> PyChipError:
151+
) -> int:
159152
"""Commission a device using an IP address."""
160-
return await self._call_sdk_non_reentrant(
161-
self._chip_controller.CommissionIP,
162-
nodeid=node_id,
163-
setupPinCode=setup_pin_code,
164-
ipaddr=ip_addr,
153+
return cast(
154+
int,
155+
await self._chip_controller.CommissionIP(
156+
nodeid=node_id,
157+
setupPinCode=setup_pin_code,
158+
ipaddr=ip_addr,
159+
),
165160
)
166161

167162
async def set_wifi_credentials(self, ssid: str, credentials: str) -> None:
@@ -185,9 +180,7 @@ async def unpair_device(self, node_id: int) -> PyChipError:
185180
Tries to look up the device attached to our controller with the given
186181
remote node id and ask it to remove Fabric.
187182
"""
188-
return await self._call_sdk_non_reentrant(
189-
self._chip_controller.UnpairDevice, nodeid=node_id
190-
)
183+
return await self._chip_controller.UnpairDevice(nodeid=node_id)
191184

192185
async def open_commissioning_window(
193186
self,
@@ -199,8 +192,7 @@ async def open_commissioning_window(
199192
) -> CommissioningParameters:
200193
"""Open a commissioning window to commission a device present on this controller to another."""
201194
async with self._get_node_lock(node_id):
202-
return await self._call_sdk_non_reentrant(
203-
self._chip_controller.OpenCommissioningWindow,
195+
return await self._chip_controller.OpenCommissioningWindow(
204196
nodeid=node_id,
205197
timeout=timeout,
206198
iteration=iteration,

pyproject.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ dependencies = [
2020
"async-timeout",
2121
"coloredlogs",
2222
"orjson",
23-
"home-assistant-chip-clusters==2024.6.1",
23+
"home-assistant-chip-clusters==2024.6.2",
2424
]
2525
description = "Python Matter WebSocket Server"
2626
license = {text = "Apache-2.0"}
@@ -39,7 +39,7 @@ server = [
3939
"cryptography==42.0.8",
4040
"orjson==3.10.5",
4141
"zeroconf==0.132.2",
42-
"home-assistant-chip-core==2024.6.1",
42+
"home-assistant-chip-core==2024.6.2",
4343
]
4444
test = [
4545
"codespell==2.3.0",

0 commit comments

Comments
 (0)