From 82df49869e7fef7c18999d595bef22529a406547 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Tue, 16 Jun 2026 10:21:08 +1000 Subject: [PATCH 1/4] tools: cloud: apps: optional CoAP info Add an option to display CoAP file information when listing versions. Signed-off-by: Jordan Yates --- src/infuse_iot/tools/cloud.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/infuse_iot/tools/cloud.py b/src/infuse_iot/tools/cloud.py index 4e015ed..9023f91 100644 --- a/src/infuse_iot/tools/cloud.py +++ b/src/infuse_iot/tools/cloud.py @@ -349,6 +349,7 @@ def add_parser(cls, parser): info_parser = tool_parser.add_parser("info", help="Display summary of application releases") info_parser.add_argument("--org", "-o", type=str, required=True, help="Organisation ID") info_parser.add_argument("--app", "-a", type=lambda x: int(x, 16), required=True, help="Application ID (hex)") + info_parser.add_argument("--coap", action="store_true", help="Display CoAP file information") info_parser.set_defaults(command_fn=cls.info) upload_parser = tool_parser.add_parser("upload", help="Upload application release") @@ -398,19 +399,27 @@ def info(self, client: Client): for release in releases: version = release.version version_str = f"{version.major}.{version.minor}.{version.revision}+{version.build_num:08x}" - release_list.append( - [ - f"{release.board_target}", - version_str, - f"{release.id}", + info = [ + f"{release.board_target}", + version_str, + f"{release.id}", + ] + if self.args.coap: + info += [release.file.coap_path, str(release.file.len_), str(release.file.crc)] + else: + info += [ f"{release.file.len_ / 1024:.2f} kB", str(release.created_at), ] - ) + release_list.append(info) + if self.args.coap: + headers = ["Board Target", "Version", "ID", "Path", "Length", "CRC"] + else: + headers = ["Board Target", "Version", "ID", "Full OTA", "Created"] print( tabulate( release_list, - headers=["Board Target", "Version", "ID", "Full OTA", "Created"], + headers=headers, ) ) From 6c0924d7ca68a31340ece6b3153e30d391afed45 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Tue, 16 Jun 2026 10:22:12 +1000 Subject: [PATCH 2/4] tools: cloud: apps: fix new application creation Properly handle the case where an application does not already exist on the cloud. Signed-off-by: Jordan Yates --- src/infuse_iot/tools/cloud.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/infuse_iot/tools/cloud.py b/src/infuse_iot/tools/cloud.py index 9023f91..a0092e1 100644 --- a/src/infuse_iot/tools/cloud.py +++ b/src/infuse_iot/tools/cloud.py @@ -474,11 +474,11 @@ def upload(self, client: Client): client=client, id=self._org, application_id=app_id ) - if application is None: - dialog = f"Application 0x{app_id:08x} does not exist in organisation {self.args.org}, create?" + if application is None or (isinstance(application, models.Error) and application.code == 404): + dialog = f"Application 0x{app_id:08x} does not exist in organisation {self._org_name}, create?" if not user_confirm(dialog): return - print(f"Creating application 0x{app_id:08x} in organisation {self.args.org}") + print(f"Creating application 0x{app_id:08x} in organisation {self._org_name}") description = user_response("Application description:") body = models.NewApplication(id=app_id, name=name, description=description) application = create_application.sync(client=client, id=self._org, body=body) From f2e2e20f3c0b0d32a7cf7997a7b529bee0f5e6ba Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Tue, 16 Jun 2026 10:48:23 +1000 Subject: [PATCH 3/4] tools: cloud: apps: inspect diffs Extend the `apps info` command to inspect available diffs if a release ID is provided. Signed-off-by: Jordan Yates --- src/infuse_iot/tools/cloud.py | 68 ++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/src/infuse_iot/tools/cloud.py b/src/infuse_iot/tools/cloud.py index a0092e1..662cc7d 100644 --- a/src/infuse_iot/tools/cloud.py +++ b/src/infuse_iot/tools/cloud.py @@ -20,6 +20,8 @@ create_release_diff, get_application_by_organisation_id_and_application_id, get_applications_by_organisation_id, + get_diffs_by_organisation_id_and_application_id_and_release_id, + get_release_by_organisation_id_and_application_id_and_release_id, get_releases_by_organisation_id_and_application_id, ) from infuse_iot.api_client.api.board import ( @@ -349,6 +351,7 @@ def add_parser(cls, parser): info_parser = tool_parser.add_parser("info", help="Display summary of application releases") info_parser.add_argument("--org", "-o", type=str, required=True, help="Organisation ID") info_parser.add_argument("--app", "-a", type=lambda x: int(x, 16), required=True, help="Application ID (hex)") + info_parser.add_argument("--rel", "-r", type=str, help="Release ID") info_parser.add_argument("--coap", action="store_true", help="Display CoAP file information") info_parser.set_defaults(command_fn=cls.info) @@ -385,7 +388,64 @@ def list(self, client: Client): ) ) - def info(self, client: Client): + def _info_one(self, client: Client): + application = get_application_by_organisation_id_and_application_id.sync( + client=client, + id=UUID(self.args.org), + application_id=self.args.app, + ) + if application is None: + sys.exit("Get application: No response") + elif isinstance(application, models.Error): + sys.exit(f"<{application.code}>: {application.message}") + release = get_release_by_organisation_id_and_application_id_and_release_id.sync( + client=client, id=UUID(self.args.org), application_id=self.args.app, release_id=self.args.rel + ) + if release is None: + sys.exit("Get release: No response") + elif isinstance(release, models.Error): + sys.exit(f"<{release.code}>: {release.message}") + diffs = get_diffs_by_organisation_id_and_application_id_and_release_id.sync( + client=client, id=UUID(self.args.org), application_id=self.args.app, release_id=self.args.rel + ) + if diffs is None: + sys.exit("Get diffs: No response") + elif isinstance(diffs, models.Error): + sys.exit(f"<{diffs.code}>: {diffs.message}") + + version = release.version + version_str = f"{version.major}.{version.minor}.{version.revision}+{version.build_num:08x}" + + print( + tabulate( + [ + ["Application Name", application.name], + ["Application Description", application.description], + ["Board Target", release.board_target], + ["Version", version_str], + ], + tablefmt="simple", + ) + ) + diff_info = [] + for diff in diffs: + from_release = get_release_by_organisation_id_and_application_id_and_release_id.sync( + client=client, id=UUID(self.args.org), application_id=self.args.app, release_id=diff.from_release_id + ) + if not isinstance(from_release, models.ApplicationRelease): + print(f"Failed to query information about source release {diff.from_release_id}") + continue + from_version = from_release.version + from_version_str = ( + f"{from_version.major}.{from_version.minor}.{from_version.revision}+{from_version.build_num:08x}" + ) + diff_info.append([from_version_str, diff.file.coap_path, diff.file.len_, diff.file.crc]) + + if len(diff_info) > 0: + print("~~~ Diffs ~~~") + print(tabulate(diff_info, headers=["From Version", "Path", "Length", "CRC"])) + + def _info_all(self, client: Client): releases = get_releases_by_organisation_id_and_application_id.sync( client=client, id=UUID(self.args.org), application_id=self.args.app ) @@ -423,6 +483,12 @@ def info(self, client: Client): ) ) + def info(self, client: Client): + if self.args.rel: + self._info_one(client) + else: + self._info_all(client) + def upload(self, client: Client): try: self._board = UUID(self.args.board) if self.args.board else None From 28064a9ac6b3610a21049a273f6e077288c168a0 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Tue, 16 Jun 2026 10:51:47 +1000 Subject: [PATCH 4/4] tools: cloud: optional CLI API key Add the option to provide the Infuse-IoT cloud API key from the command line arguments, instead of using the stored credentials. Signed-off-by: Jordan Yates --- src/infuse_iot/tools/cloud.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/infuse_iot/tools/cloud.py b/src/infuse_iot/tools/cloud.py index 662cc7d..012ff5a 100644 --- a/src/infuse_iot/tools/cloud.py +++ b/src/infuse_iot/tools/cloud.py @@ -59,7 +59,8 @@ def run(self): def client(self): """Get API client object ready to use""" - return Client(base_url="https://api.infuse-iot.com").with_headers({"x-api-key": f"Bearer {get_api_key()}"}) + bearer = self.args.api_key if self.args.api_key else get_api_key() + return Client(base_url="https://api.infuse-iot.com").with_headers({"x-api-key": f"Bearer {bearer}"}) class Organisations(CloudSubCommand): @@ -649,6 +650,7 @@ class SubCommand(InfuseCommand): @classmethod def add_parser(cls, parser): + parser.add_argument("--api-key", type=str, help="Cloud API key to use instead of stored credentials") subparser = parser.add_subparsers(title="commands", metavar="", required=True) Organisations.add_parser(subparser)