Skip to content

Commit b43b024

Browse files
committed
Check if DCL software updates are indeed applicable
Verify that the DCL software update is indeed applicable to the currently software running on the device. Add test coverage as well.
1 parent 880c430 commit b43b024

File tree

3 files changed

+114
-21
lines changed

3 files changed

+114
-21
lines changed

matter_server/server/device_controller.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from matter_server.common.models import CommissionableNodeData, CommissioningParameters
2828
from matter_server.server.helpers.attributes import parse_attributes_from_read_result
2929
from matter_server.server.helpers.utils import ping_ip
30-
from matter_server.server.ota.dcl import check_updates
30+
from matter_server.server.ota.dcl import check_for_update
3131
from matter_server.server.ota.provider import ExternalOtaProvider
3232

3333
from ..common.errors import (
@@ -971,7 +971,7 @@ async def update_node(self, node_id: int) -> dict | None:
971971
BASIC_INFORMATION_SOFTWARE_VERSION_STRING_ATTRIBUTE_PATH
972972
)
973973

974-
update = await check_updates(node_id, vid, pid, software_version)
974+
update = await check_for_update(vid, pid, software_version)
975975
if not update:
976976
node_logger.info("No new update found.")
977977
return None

matter_server/server/ota/dcl.py

+37-19
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Handle OTA software version endpoints of the DCL."""
22

33
import logging
4-
from typing import Any
4+
from typing import Any, cast
55

66
from aiohttp import ClientError, ClientSession
77

@@ -10,7 +10,7 @@
1010
LOGGER = logging.getLogger(__name__)
1111

1212

13-
async def get_software_versions(node_id: int, vid: int, pid: int) -> Any:
13+
async def get_software_versions(vid: int, pid: int) -> Any:
1414
"""Check DCL if there are updates available for a particular node."""
1515
async with ClientSession(raise_for_status=True) as http_session:
1616
# fetch the paa certificates list
@@ -20,9 +20,7 @@ async def get_software_versions(node_id: int, vid: int, pid: int) -> Any:
2020
return await response.json()
2121

2222

23-
async def get_software_version(
24-
node_id: int, vid: int, pid: int, software_version: int
25-
) -> Any:
23+
async def get_software_version(vid: int, pid: int, software_version: int) -> Any:
2624
"""Check DCL if there are updates available for a particular node."""
2725
async with ClientSession(raise_for_status=True) as http_session:
2826
# fetch the paa certificates list
@@ -32,27 +30,47 @@ async def get_software_version(
3230
return await response.json()
3331

3432

35-
async def check_updates(
36-
node_id: int, vid: int, pid: int, current_software_version: int
33+
async def check_for_update(
34+
vid: int, pid: int, current_software_version: int
3735
) -> None | dict:
3836
"""Check if there is a newer software version available on the DCL."""
3937
try:
40-
versions = await get_software_versions(node_id, vid, pid)
38+
versions = await get_software_versions(vid, pid)
4139

42-
software_versions: list[int] = versions["modelVersions"]["softwareVersions"]
43-
latest_software_version = max(software_versions)
44-
if latest_software_version <= current_software_version:
40+
all_software_versions: list[int] = versions["modelVersions"]["softwareVersions"]
41+
newer_software_versions = [
42+
version
43+
for version in all_software_versions
44+
if version > current_software_version
45+
]
46+
47+
# Check if there is a newer software version available
48+
if not newer_software_versions:
49+
LOGGER.info("No newer software version available.")
4550
return None
4651

47-
version: dict = await get_software_version(
48-
node_id, vid, pid, latest_software_version
49-
)
50-
if isinstance(version, dict) and "modelVersion" in version:
51-
result: Any = version["modelVersion"]
52-
if isinstance(result, dict):
53-
return result
52+
# Check if latest firmware is applicable, and backtrack from there
53+
for version in sorted(newer_software_versions, reverse=True):
54+
version_res: dict = await get_software_version(vid, pid, version)
55+
if not isinstance(version_res, dict):
56+
raise TypeError("Unexpected DCL response.")
57+
58+
if "modelVersion" not in version_res:
59+
raise ValueError("Unexpected DCL response.")
60+
61+
version_candidate: dict = cast(dict, version_res["modelVersion"])
62+
63+
# Check minApplicableSoftwareVersion/maxApplicableSoftwareVersion
64+
min_sw_version = version_candidate["minApplicableSoftwareVersion"]
65+
max_sw_version = version_candidate["maxApplicableSoftwareVersion"]
66+
if (
67+
current_software_version < min_sw_version
68+
or current_software_version > max_sw_version
69+
):
70+
LOGGER.debug("Software version %d not applicable.", version)
71+
continue
5472

55-
logging.error("Unexpected DCL response.")
73+
return version_candidate
5674
return None
5775

5876
except (ClientError, TimeoutError) as err:

tests/server/ota/test_dcl.py

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Test DCL OTA updates."""
2+
3+
from unittest.mock import AsyncMock, patch
4+
5+
from matter_server.server.ota.dcl import check_for_update
6+
7+
# Mock the DCL responses (sample from https://on.dcl.csa-iot.org/dcl/model/versions/4447/8194)
8+
DCL_RESPONSE_SOFTWARE_VERSIONS = {
9+
"modelVersions": {
10+
"vid": 4447,
11+
"pid": 8194,
12+
"softwareVersions": [1000, 1011],
13+
}
14+
}
15+
16+
# Mock the DCL responses (sample from https://on.dcl.csa-iot.org/dcl/model/versions/4447/8194/1011)
17+
DCL_RESPONSE_SOFTWARE_VERSION_1011 = {
18+
"modelVersion": {
19+
"vid": 4447,
20+
"pid": 8194,
21+
"softwareVersion": 1011,
22+
"softwareVersionString": "1.0.1.1",
23+
"cdVersionNumber": 1,
24+
"firmwareInformation": "",
25+
"softwareVersionValid": True,
26+
"otaUrl": "https://cdn.aqara.com/cdn/opencloud-product/mainland/product-firmware/prd/aqara.matter.4447_8194/20240306154144_rel_up_to_enc_ota_sbl_app_aqara.matter.4447_8194_1.0.1.1_115F_2002_20240115195007_7a9b91.ota",
27+
"otaFileSize": "615708",
28+
"otaChecksum": "rFZ6WdH0DuuCf7HVoRmNjCF73mYZ98DGYpHoDKmf0Bw=",
29+
"otaChecksumType": 1,
30+
"minApplicableSoftwareVersion": 1000,
31+
"maxApplicableSoftwareVersion": 1010,
32+
"releaseNotesUrl": "",
33+
"creator": "cosmos1qpz3ghnqj6my7gzegkftzav9hpxymkx6zdk73v",
34+
}
35+
}
36+
37+
38+
async def test_check_updates():
39+
"""Test the case where the latest software version is applicable."""
40+
with (
41+
patch(
42+
"matter_server.server.ota.dcl.get_software_versions",
43+
new_callable=AsyncMock,
44+
return_value=DCL_RESPONSE_SOFTWARE_VERSIONS,
45+
),
46+
patch(
47+
"matter_server.server.ota.dcl.get_software_version",
48+
new_callable=AsyncMock,
49+
return_value=DCL_RESPONSE_SOFTWARE_VERSION_1011,
50+
),
51+
):
52+
# Call the function with a current software version of 1000
53+
result = await check_for_update(4447, 8194, 1000)
54+
55+
assert result == DCL_RESPONSE_SOFTWARE_VERSION_1011["modelVersion"]
56+
57+
58+
async def test_check_updates_not_applicable():
59+
"""Test the case where the latest software version is not applicable."""
60+
with (
61+
patch(
62+
"matter_server.server.ota.dcl.get_software_versions",
63+
new_callable=AsyncMock,
64+
return_value=DCL_RESPONSE_SOFTWARE_VERSIONS,
65+
),
66+
patch(
67+
"matter_server.server.ota.dcl.get_software_version",
68+
new_callable=AsyncMock,
69+
return_value=DCL_RESPONSE_SOFTWARE_VERSION_1011,
70+
),
71+
):
72+
# Call the function with a current software version of 1
73+
result = await check_for_update(4447, 8194, 1)
74+
75+
assert result is None

0 commit comments

Comments
 (0)