Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add software update capability #709

Merged
merged 39 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
612b867
Implement Update check using DCL software information
agners May 17, 2024
17f9f54
Initial implementation of OTA provider
agners May 17, 2024
2bab7e3
Implement update using OTA Provider app
agners May 17, 2024
9d7717f
Setup OTA Provider App automatically when necessary
agners May 17, 2024
ee82e39
Deploy chip-ota-provider-app in container
agners May 23, 2024
1cf634b
Check if DCL software updates are indeed applicable
agners May 24, 2024
f698b51
Introduce hardcoded updates
agners May 24, 2024
93f3894
Split update WebSocket command into two commands
agners May 24, 2024
09a4469
Introduce Update logic specific exceptions
agners May 24, 2024
e1a5941
Implement OTA checksum verification
agners May 24, 2024
116077d
Add client commands for updates
agners May 27, 2024
5b41888
Improve DCL error message when download fails
agners May 27, 2024
4b0911c
Improve OTA Provider handling
agners May 28, 2024
70e9b60
Move almost all update logic into ExternalOtaProvider
agners May 28, 2024
c67c850
Update implementation to work with latest refactoring
agners May 29, 2024
e4bbc47
Simplify ExternalOtaProvider
agners May 30, 2024
be9ee65
Support specific version by string
agners Jun 4, 2024
3c33d5f
Use ephemeral OTA Provider instances
agners Jun 5, 2024
07b8254
Raise update error if the node moves from querying to idle
agners Jun 5, 2024
02d43d6
Improve logging and use Future to mark completion
agners Jun 5, 2024
56d5b06
Make sure that only one updates is running at a time
agners Jun 5, 2024
2f535aa
Use new commissioning API
agners Jun 20, 2024
683b33f
Ignore when there is no software version info on DCL
agners Jun 24, 2024
76ed950
Add MatterSoftwareVersion model for check_node_update
agners Jun 24, 2024
475a1dc
Bump Server schema
agners Jun 24, 2024
23a6e6b
Use OTA Provider from dedicated repository
agners Jul 11, 2024
b0dca4b
Bump OTA Provider to 2024.7.1
agners Jul 11, 2024
87cd0a4
Use new node logger
agners Jul 11, 2024
7e7537b
Complete future only once on error
agners Jul 11, 2024
7a30700
Apply suggestions from code review
agners Jul 11, 2024
07e20dd
Share client session for update check
agners Jul 11, 2024
b057eae
Provide methods to convert dataclass as dict
agners Jul 11, 2024
09c92f7
Log with node logger when checking for updates
agners Jul 11, 2024
57fb7d2
Fix trailing whitespace
agners Jul 11, 2024
9ad2348
Fix tests
agners Jul 12, 2024
507a429
ruff format
agners Jul 12, 2024
39f025e
Support loading updates from local json file
agners Jul 15, 2024
51bec82
Check if update directory exists
agners Jul 15, 2024
d4162fe
Add software update source information
agners Jul 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ RUN \
set -x \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
curl \
libuv1 \
zlib1g \
libjson-c5 \
Expand All @@ -25,6 +26,21 @@ RUN \

ARG PYTHON_MATTER_SERVER

ENV chip_example_url "https://github.com/home-assistant-libs/matter-linux-ota-provider/releases/download/2024.7.1"
ARG TARGETPLATFORM

RUN \
set -x \
&& echo "${TARGETPLATFORM}" \
&& if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
curl -Lo /usr/local/bin/chip-ota-provider-app "${chip_example_url}/chip-ota-provider-app-x86-64"; \
elif [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
curl -Lo /usr/local/bin/chip-ota-provider-app "${chip_example_url}/chip-ota-provider-app-aarch64"; \
else \
exit 1; \
fi \
&& chmod +x /usr/local/bin/chip-ota-provider-app
marcelveldt marked this conversation as resolved.
Show resolved Hide resolved

# hadolint ignore=DL3013
RUN \
pip3 install --no-cache-dir "python-matter-server[server]==${PYTHON_MATTER_SERVER}"
Expand Down
31 changes: 31 additions & 0 deletions matter_server/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
EventType,
MatterNodeData,
MatterNodeEvent,
MatterSoftwareVersion,
MessageType,
NodePingResult,
ResultMessageBase,
Expand Down Expand Up @@ -509,6 +510,36 @@ async def interview_node(self, node_id: int) -> None:
"""Interview a node."""
await self.send_command(APICommand.INTERVIEW_NODE, node_id=node_id)

async def check_node_update(self, node_id: int) -> MatterSoftwareVersion | None:
"""Check Node for updates.

Return a dict with the available update information. Most notable
"softwareVersion" contains the integer value of the update version which then
can be used for the update_node command to trigger the update.

The "softwareVersionString" is a human friendly version string.
"""
data = await self.send_command(
APICommand.CHECK_NODE_UPDATE, node_id=node_id, require_schema=10
)
if data is None:
return None

return dataclass_from_dict(MatterSoftwareVersion, data)

async def update_node(
self,
node_id: int,
software_version: int | str,
) -> None:
"""Start node update to a particular version."""
await self.send_command(
APICommand.UPDATE_NODE,
node_id=node_id,
software_version=software_version,
require_schema=10,
)

def _prepare_message(
self,
command: str,
Expand Down
2 changes: 1 addition & 1 deletion matter_server/common/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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


VERBOSE_LOG_LEVEL = 5
12 changes: 12 additions & 0 deletions matter_server/common/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ class InvalidCommand(MatterError):
error_code = 9


class UpdateCheckError(MatterError):
"""Error raised when there was an error during searching for updates."""

error_code = 10


class UpdateError(MatterError):
"""Error raised when there was an error during applying updates."""

error_code = 11


def exception_from_error_code(error_code: int) -> type[MatterError]:
"""Return correct Exception class from error_code."""
return ERROR_MAP.get(error_code, MatterError)
58 changes: 58 additions & 0 deletions matter_server/common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ class APICommand(str, Enum):
PING_NODE = "ping_node"
GET_NODE_IP_ADDRESSES = "get_node_ip_addresses"
IMPORT_TEST_NODE = "import_test_node"
CHECK_NODE_UPDATE = "check_node_update"
UPDATE_NODE = "update_node"


EventCallBackType = Callable[[EventType, Any], None]
Expand Down Expand Up @@ -209,3 +211,59 @@ class CommissioningParameters:
setup_pin_code: int
setup_manual_code: str
setup_qr_code: str


class UpdateSource(Enum):
"""Enum with possible sources for a software update."""

MAIN_NET_DCL = "main-net-dcl"
TEST_NET_DCL = "test-net-dcl"
LOCAL = "local"


@dataclass
class MatterSoftwareVersion:
"""Representation of a Matter software version. Return by the check_node_update command.

This holds Matter software version information similar to what is available from the CSA DCL.
https://on.dcl.csa-iot.org/#/Query/ModelVersion.
"""

agners marked this conversation as resolved.
Show resolved Hide resolved
vid: int
pid: int
software_version: int
software_version_string: str
firmware_information: str | None
min_applicable_software_version: int
max_applicable_software_version: int
release_notes_url: str | None
update_source: UpdateSource

@classmethod
def from_dict(cls, data: dict) -> MatterSoftwareVersion:
"""Initialize from dict."""
return cls(
vid=data["vid"],
pid=data["pid"],
software_version=data["software_version"],
software_version_string=data["software_version_string"],
firmware_information=data["firmware_information"],
min_applicable_software_version=data["min_applicable_software_version"],
max_applicable_software_version=data["max_applicable_software_version"],
release_notes_url=data["release_notes_url"],
update_source=UpdateSource(data["update_source"]),
)

def as_dict(self) -> dict:
"""Return dict representation of the object."""
return {
"vid": self.vid,
"pid": self.pid,
"software_version": self.software_version,
"software_version_string": self.software_version_string,
"firmware_information": self.firmware_information,
"min_applicable_software_version": self.min_applicable_software_version,
"max_applicable_software_version": self.max_applicable_software_version,
"release_notes_url": self.release_notes_url,
"update_source": str(self.update_source),
}
7 changes: 7 additions & 0 deletions matter_server/server/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@
nargs="+",
help="List of node IDs to show logs from (applies only to server logs).",
)
parser.add_argument(
"--ota-provider-dir",
type=str,
default=None,
help="Directory where OTA Provider stores software updates and configuration.",
)

args = parser.parse_args()

Expand Down Expand Up @@ -216,6 +222,7 @@ def main() -> None:
args.paa_root_cert_dir,
args.enable_test_net_dcl,
args.bluetooth_adapter,
args.ota_provider_dir,
)

async def handle_stop(loop: asyncio.AbstractEventLoop) -> None:
Expand Down
2 changes: 2 additions & 0 deletions matter_server/server/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@
.parent.resolve()
.joinpath("credentials/development/paa-root-certs")
)

DEFAULT_OTA_PROVIDER_DIR: Final[pathlib.Path] = pathlib.Path().cwd().joinpath("updates")
Loading