Skip to content

Commit dc35816

Browse files
authored
Add software update capability (#709)
1 parent 28906d6 commit dc35816

15 files changed

+952
-11
lines changed

Dockerfile

+16
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ RUN \
99
set -x \
1010
&& apt-get update \
1111
&& apt-get install -y --no-install-recommends \
12+
curl \
1213
libuv1 \
1314
zlib1g \
1415
libjson-c5 \
@@ -25,6 +26,21 @@ RUN \
2526

2627
ARG PYTHON_MATTER_SERVER
2728

29+
ENV chip_example_url "https://github.com/home-assistant-libs/matter-linux-ota-provider/releases/download/2024.7.1"
30+
ARG TARGETPLATFORM
31+
32+
RUN \
33+
set -x \
34+
&& echo "${TARGETPLATFORM}" \
35+
&& if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
36+
curl -Lo /usr/local/bin/chip-ota-provider-app "${chip_example_url}/chip-ota-provider-app-x86-64"; \
37+
elif [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
38+
curl -Lo /usr/local/bin/chip-ota-provider-app "${chip_example_url}/chip-ota-provider-app-aarch64"; \
39+
else \
40+
exit 1; \
41+
fi \
42+
&& chmod +x /usr/local/bin/chip-ota-provider-app
43+
2844
# hadolint ignore=DL3013
2945
RUN \
3046
pip3 install --no-cache-dir "python-matter-server[server]==${PYTHON_MATTER_SERVER}"

matter_server/client/client.py

+31
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
EventType,
3030
MatterNodeData,
3131
MatterNodeEvent,
32+
MatterSoftwareVersion,
3233
MessageType,
3334
NodePingResult,
3435
ResultMessageBase,
@@ -509,6 +510,36 @@ async def interview_node(self, node_id: int) -> None:
509510
"""Interview a node."""
510511
await self.send_command(APICommand.INTERVIEW_NODE, node_id=node_id)
511512

513+
async def check_node_update(self, node_id: int) -> MatterSoftwareVersion | None:
514+
"""Check Node for updates.
515+
516+
Return a dict with the available update information. Most notable
517+
"softwareVersion" contains the integer value of the update version which then
518+
can be used for the update_node command to trigger the update.
519+
520+
The "softwareVersionString" is a human friendly version string.
521+
"""
522+
data = await self.send_command(
523+
APICommand.CHECK_NODE_UPDATE, node_id=node_id, require_schema=10
524+
)
525+
if data is None:
526+
return None
527+
528+
return dataclass_from_dict(MatterSoftwareVersion, data)
529+
530+
async def update_node(
531+
self,
532+
node_id: int,
533+
software_version: int | str,
534+
) -> None:
535+
"""Start node update to a particular version."""
536+
await self.send_command(
537+
APICommand.UPDATE_NODE,
538+
node_id=node_id,
539+
software_version=software_version,
540+
require_schema=10,
541+
)
542+
512543
def _prepare_message(
513544
self,
514545
command: str,

matter_server/common/const.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# schema version is used to determine compatibility between server and client
44
# bump schema if we add new features and/or make other (breaking) changes
5-
SCHEMA_VERSION = 9
5+
SCHEMA_VERSION = 10
66

77

88
VERBOSE_LOG_LEVEL = 5

matter_server/common/errors.py

+12
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ class InvalidCommand(MatterError):
7777
error_code = 9
7878

7979

80+
class UpdateCheckError(MatterError):
81+
"""Error raised when there was an error during searching for updates."""
82+
83+
error_code = 10
84+
85+
86+
class UpdateError(MatterError):
87+
"""Error raised when there was an error during applying updates."""
88+
89+
error_code = 11
90+
91+
8092
def exception_from_error_code(error_code: int) -> type[MatterError]:
8193
"""Return correct Exception class from error_code."""
8294
return ERROR_MAP.get(error_code, MatterError)

matter_server/common/models.py

+58
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ class APICommand(str, Enum):
4747
PING_NODE = "ping_node"
4848
GET_NODE_IP_ADDRESSES = "get_node_ip_addresses"
4949
IMPORT_TEST_NODE = "import_test_node"
50+
CHECK_NODE_UPDATE = "check_node_update"
51+
UPDATE_NODE = "update_node"
5052

5153

5254
EventCallBackType = Callable[[EventType, Any], None]
@@ -209,3 +211,59 @@ class CommissioningParameters:
209211
setup_pin_code: int
210212
setup_manual_code: str
211213
setup_qr_code: str
214+
215+
216+
class UpdateSource(Enum):
217+
"""Enum with possible sources for a software update."""
218+
219+
MAIN_NET_DCL = "main-net-dcl"
220+
TEST_NET_DCL = "test-net-dcl"
221+
LOCAL = "local"
222+
223+
224+
@dataclass
225+
class MatterSoftwareVersion:
226+
"""Representation of a Matter software version. Return by the check_node_update command.
227+
228+
This holds Matter software version information similar to what is available from the CSA DCL.
229+
https://on.dcl.csa-iot.org/#/Query/ModelVersion.
230+
"""
231+
232+
vid: int
233+
pid: int
234+
software_version: int
235+
software_version_string: str
236+
firmware_information: str | None
237+
min_applicable_software_version: int
238+
max_applicable_software_version: int
239+
release_notes_url: str | None
240+
update_source: UpdateSource
241+
242+
@classmethod
243+
def from_dict(cls, data: dict) -> MatterSoftwareVersion:
244+
"""Initialize from dict."""
245+
return cls(
246+
vid=data["vid"],
247+
pid=data["pid"],
248+
software_version=data["software_version"],
249+
software_version_string=data["software_version_string"],
250+
firmware_information=data["firmware_information"],
251+
min_applicable_software_version=data["min_applicable_software_version"],
252+
max_applicable_software_version=data["max_applicable_software_version"],
253+
release_notes_url=data["release_notes_url"],
254+
update_source=UpdateSource(data["update_source"]),
255+
)
256+
257+
def as_dict(self) -> dict:
258+
"""Return dict representation of the object."""
259+
return {
260+
"vid": self.vid,
261+
"pid": self.pid,
262+
"software_version": self.software_version,
263+
"software_version_string": self.software_version_string,
264+
"firmware_information": self.firmware_information,
265+
"min_applicable_software_version": self.min_applicable_software_version,
266+
"max_applicable_software_version": self.max_applicable_software_version,
267+
"release_notes_url": self.release_notes_url,
268+
"update_source": str(self.update_source),
269+
}

matter_server/server/__main__.py

+7
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@
116116
nargs="+",
117117
help="List of node IDs to show logs from (applies only to server logs).",
118118
)
119+
parser.add_argument(
120+
"--ota-provider-dir",
121+
type=str,
122+
default=None,
123+
help="Directory where OTA Provider stores software updates and configuration.",
124+
)
119125

120126
args = parser.parse_args()
121127

@@ -216,6 +222,7 @@ def main() -> None:
216222
args.paa_root_cert_dir,
217223
args.enable_test_net_dcl,
218224
args.bluetooth_adapter,
225+
args.ota_provider_dir,
219226
)
220227

221228
async def handle_stop(loop: asyncio.AbstractEventLoop) -> None:

matter_server/server/const.py

+2
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@
2020
.parent.resolve()
2121
.joinpath("credentials/development/paa-root-certs")
2222
)
23+
24+
DEFAULT_OTA_PROVIDER_DIR: Final[pathlib.Path] = pathlib.Path().cwd().joinpath("updates")

0 commit comments

Comments
 (0)