Skip to content

Commit 3c1a10c

Browse files
Fix lint and test issues on main (#247)
1 parent 6d0c415 commit 3c1a10c

File tree

4 files changed

+73
-32
lines changed

4 files changed

+73
-32
lines changed

matter_server/server/const.py

+14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
11
"""Server-only constants for the Python Matter Server."""
2+
import pathlib
3+
from typing import Final
24

35
# The minimum schema version (of a client) the server can support
46
MIN_SCHEMA_VERSION = 2
7+
8+
# the paa-root-certs path is hardcoded in the sdk at this time
9+
# and always uses the development subfolder
10+
# regardless of anything you pass into instantiating the controller
11+
# revisit this once matter 1.1 is released
12+
PAA_ROOT_CERTS_DIR: Final[pathlib.Path] = (
13+
pathlib.Path(__file__)
14+
.parent.resolve()
15+
.parent.resolve()
16+
.parent.resolve()
17+
.joinpath("credentials/development/paa-root-certs")
18+
)

matter_server/server/device_controller.py

+41-24
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,13 @@
77
from datetime import datetime
88
from functools import partial
99
import logging
10-
import pathlib
11-
from typing import TYPE_CHECKING, Any, Callable, Deque, Final, Type, TypeVar, cast
10+
from typing import TYPE_CHECKING, Any, Callable, Deque, Type, TypeVar, cast
1211

1312
from chip.ChipDeviceCtrl import CommissionableNode
1413
from chip.clusters import Attribute, Objects as Clusters
1514
from chip.clusters.ClusterObjects import ALL_CLUSTERS, Cluster
1615
from chip.exceptions import ChipStackError
1716

18-
from matter_server.server.helpers.paa_certificates import fetch_certificates
19-
2017
from ..common.const import SCHEMA_VERSION
2118
from ..common.errors import (
2219
NodeCommissionFailed,
@@ -31,30 +28,21 @@
3128
dataclass_from_dict,
3229
)
3330
from ..common.models import APICommand, EventType, MatterNodeData
31+
from .const import PAA_ROOT_CERTS_DIR
32+
from .helpers.paa_certificates import fetch_certificates
3433

3534
if TYPE_CHECKING:
36-
from .server import MatterServer
3735
from chip.ChipDeviceCtrl import ChipDeviceController
3836

37+
from .server import MatterServer
38+
3939
_T = TypeVar("_T")
4040

4141
DATA_KEY_NODES = "nodes"
4242
DATA_KEY_LAST_NODE_ID = "last_node_id"
4343

4444
LOGGER = logging.getLogger(__name__)
4545

46-
# the paa-root-certs path is hardcoded in the sdk at this time
47-
# and always uses the development subfolder
48-
# regardless of anything you pass into instantiating the controller
49-
# revisit this once matter 1.1 is released
50-
PAA_ROOT_CERTS_DIR: Final[pathlib.Path] = (
51-
pathlib.Path(__file__)
52-
.parent.resolve()
53-
.parent.resolve()
54-
.parent.resolve()
55-
.joinpath("credentials/development/paa-root-certs")
56-
)
57-
5846

5947
class MatterDeviceController:
6048
"""Class that manages the Matter devices."""
@@ -67,10 +55,6 @@ def __init__(
6755
):
6856
"""Initialize the device controller."""
6957
self.server = server
70-
# Instantiate the underlying ChipDeviceController instance on the Fabric
71-
if not PAA_ROOT_CERTS_DIR.is_dir():
72-
raise RuntimeError("PAA certificates directory not found")
73-
7458
# we keep the last events in memory so we can include them in the diagnostics dump
7559
self.event_history: Deque[Attribute.EventReadResult] = deque(maxlen=25)
7660
self._subscriptions: dict[int, Attribute.SubscriptionTransaction] = {}
@@ -84,7 +68,8 @@ async def initialize(self) -> None:
8468
"""Async initialize of controller."""
8569
# (re)fetch all PAA certificates once at startup
8670
# NOTE: this must be done before initializing the controller
87-
await fetch_certificates(PAA_ROOT_CERTS_DIR)
71+
await fetch_certificates()
72+
# Instantiate the underlying ChipDeviceController instance on the Fabric
8873
self.chip_controller = self.server.stack.fabric_admin.NewController(
8974
paaTrustStorePath=str(PAA_ROOT_CERTS_DIR)
9075
)
@@ -117,6 +102,9 @@ async def start(self) -> None:
117102

118103
async def stop(self) -> None:
119104
"""Handle logic on server stop."""
105+
if self.chip_controller is None:
106+
raise RuntimeError("Device Controller not initialized.")
107+
120108
# unsubscribe all node subscriptions
121109
for sub in self._subscriptions.values():
122110
await self._call_sdk(sub.Shutdown)
@@ -147,9 +135,12 @@ async def commission_with_code(self, code: str) -> MatterNodeData:
147135
148136
Returns full NodeInfo once complete.
149137
"""
138+
if self.chip_controller is None:
139+
raise RuntimeError("Device Controller not initialized.")
140+
150141
# perform a quick delta sync of certificates to make sure
151142
# we have the latest paa root certs
152-
await fetch_certificates(PAA_ROOT_CERTS_DIR)
143+
await fetch_certificates()
153144
node_id = self._get_next_node_id()
154145

155146
success = await self._call_sdk(
@@ -184,11 +175,14 @@ async def commission_on_network(
184175
a string or None depending on the actual type of selected filter.
185176
Returns full NodeInfo once complete.
186177
"""
178+
if self.chip_controller is None:
179+
raise RuntimeError("Device Controller not initialized.")
180+
187181
# perform a quick delta sync of certificates to make sure
188182
# we have the latest paa root certs
189183
# NOTE: Its not very clear if the newly fetched certificates can be used without
190184
# restarting the device controller
191-
await fetch_certificates(PAA_ROOT_CERTS_DIR)
185+
await fetch_certificates()
192186

193187
node_id = self._get_next_node_id()
194188

@@ -214,6 +208,9 @@ async def commission_on_network(
214208
@api_command(APICommand.SET_WIFI_CREDENTIALS)
215209
async def set_wifi_credentials(self, ssid: str, credentials: str) -> None:
216210
"""Set WiFi credentials for commissioning to a (new) device."""
211+
if self.chip_controller is None:
212+
raise RuntimeError("Device Controller not initialized.")
213+
217214
await self._call_sdk(
218215
self.chip_controller.SetWiFiCredentials,
219216
ssid=ssid,
@@ -225,6 +222,9 @@ async def set_wifi_credentials(self, ssid: str, credentials: str) -> None:
225222
@api_command(APICommand.SET_THREAD_DATASET)
226223
async def set_thread_operational_dataset(self, dataset: str) -> None:
227224
"""Set Thread Operational dataset in the stack."""
225+
if self.chip_controller is None:
226+
raise RuntimeError("Device Controller not initialized.")
227+
228228
await self._call_sdk(
229229
self.chip_controller.SetThreadOperationalDataset,
230230
threadOperationalDataset=bytes.fromhex(dataset),
@@ -246,6 +246,9 @@ async def open_commissioning_window(
246246
247247
Returns code to use as discriminator.
248248
"""
249+
if self.chip_controller is None:
250+
raise RuntimeError("Device Controller not initialized.")
251+
249252
if discriminator is None:
250253
discriminator = 3840 # TODO generate random one
251254

@@ -264,6 +267,8 @@ async def discover_commissionable_nodes(
264267
self,
265268
) -> CommissionableNode | list[CommissionableNode] | None:
266269
"""Discover Commissionable Nodes (discovered on BLE or mDNS)."""
270+
if self.chip_controller is None:
271+
raise RuntimeError("Device Controller not initialized.")
267272

268273
result = await self._call_sdk(
269274
self.chip_controller.DiscoverCommissionableNodes,
@@ -273,6 +278,9 @@ async def discover_commissionable_nodes(
273278
@api_command(APICommand.INTERVIEW_NODE)
274279
async def interview_node(self, node_id: int) -> None:
275280
"""Interview a node."""
281+
if self.chip_controller is None:
282+
raise RuntimeError("Device Controller not initialized.")
283+
276284
LOGGER.debug("Interviewing node: %s", node_id)
277285
try:
278286
await self._call_sdk(self.chip_controller.ResolveNode, nodeid=node_id)
@@ -328,6 +336,9 @@ async def send_device_command(
328336
interaction_timeout_ms: int | None = None,
329337
) -> Any:
330338
"""Send a command to a Matter node/device."""
339+
if self.chip_controller is None:
340+
raise RuntimeError("Device Controller not initialized.")
341+
331342
cluster_cls: Cluster = ALL_CLUSTERS[cluster_id]
332343
command_cls = getattr(cluster_cls.Commands, command_name)
333344
command = dataclass_from_dict(command_cls, payload)
@@ -343,6 +354,9 @@ async def send_device_command(
343354
@api_command(APICommand.REMOVE_NODE)
344355
async def remove_node(self, node_id: int) -> None:
345356
"""Remove a Matter node/device from the fabric."""
357+
if self.chip_controller is None:
358+
raise RuntimeError("Device Controller not initialized.")
359+
346360
if node_id not in self._nodes:
347361
raise NodeNotExists(
348362
f"Node {node_id} does not exist or has not been interviewed."
@@ -378,6 +392,9 @@ async def subscribe_node(self, node_id: int) -> None:
378392
379393
Note that by using the listen command at server level, you will receive all node events.
380394
"""
395+
if self.chip_controller is None:
396+
raise RuntimeError("Device Controller not initialized.")
397+
381398
if node_id not in self._nodes:
382399
raise NodeNotExists(
383400
f"Node {node_id} does not exist or has not been interviewed."

matter_server/server/helpers/paa_certificates.py

+9-8
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,30 @@
99

1010
import asyncio
1111
import logging
12-
import pathlib
12+
from os import makedirs
1313
import re
1414

15-
from aiohttp import ClientSession, ClientError
15+
from aiohttp import ClientError, ClientSession
1616
from cryptography import x509
1717
from cryptography.hazmat.primitives import serialization
1818

19+
from matter_server.server.const import PAA_ROOT_CERTS_DIR
20+
1921
LOGGER = logging.getLogger(__name__)
2022
PRODUCTION_URL = "https://on.dcl.csa-iot.org"
2123
TEST_URL = "https://on.test-net.dcl.csa-iot.org"
2224

2325
LAST_CERT_IDS: set[str] = set()
2426

2527

26-
async def write_paa_root_cert(
27-
certificate: str, subject: str, root_path: pathlib.Path
28-
) -> None:
28+
async def write_paa_root_cert(certificate: str, subject: str) -> None:
2929
"""Write certificate from string to file."""
3030

3131
def _write() -> None:
3232
filename_base = "dcld_mirror_" + re.sub(
3333
"[^a-zA-Z0-9_-]", "", re.sub("[=, ]", "_", subject)
3434
)
35-
filepath_base = root_path.joinpath(filename_base)
35+
filepath_base = PAA_ROOT_CERTS_DIR.joinpath(filename_base)
3636
# handle PEM certificate file
3737
file_path_pem = f"{filepath_base}.pem"
3838
LOGGER.debug("Writing certificate %s", file_path_pem)
@@ -50,12 +50,14 @@ def _write() -> None:
5050

5151

5252
async def fetch_certificates(
53-
paa_trust_store_path: pathlib.Path,
5453
fetch_test_certificates: bool = True,
5554
fetch_production_certificates: bool = True,
5655
) -> int:
5756
"""Fetch PAA Certificates."""
5857
LOGGER.info("Fetching the latest PAA root certificates from DCL.")
58+
if not PAA_ROOT_CERTS_DIR.is_dir():
59+
loop = asyncio.get_running_loop()
60+
await loop.run_in_executor(None, makedirs, PAA_ROOT_CERTS_DIR)
5961
fetch_count: int = 0
6062
base_urls = set()
6163
# determine which url's need to be queried.
@@ -94,7 +96,6 @@ async def fetch_certificates(
9496
await write_paa_root_cert(
9597
certificate,
9698
subject,
97-
paa_trust_store_path,
9899
)
99100
LAST_CERT_IDS.add(paa["subjectKeyId"])
100101
fetch_count += 1

tests/server/test_server.py

+9
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ def storage_controller_fixture() -> Generator[MagicMock, None, None]:
8787
yield storage_controller
8888

8989

90+
@pytest.fixture(name="fetch_certificates", autouse=True)
91+
def fetch_certificates_fixture() -> Generator[MagicMock, None, None]:
92+
"""Return a mocked fetch certificates."""
93+
with patch(
94+
"matter_server.server.device_controller.fetch_certificates", autospec=True
95+
) as fetch_certificates:
96+
yield fetch_certificates
97+
98+
9099
@pytest.fixture(name="server")
91100
async def server_fixture() -> AsyncGenerator[MatterServer, None]:
92101
"""Yield a server."""

0 commit comments

Comments
 (0)