diff --git a/AGENTS.md b/AGENTS.md index 15af053..b1f3d2a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,8 @@ evidence. ## Non-negotiable safety boundaries -- Default to passive or read-only operations. +- Default to non-mutating discovery or read-only operations. CoreBluetooth BLE + scanning on macOS is active discovery, not passive radio sniffing. - Do not send BLE writes until the exact service, characteristic, framing, payload semantics, rollback, and expected state transition are documented. - BLE notification subscription may perform the standard temporary CCCD write; diff --git a/README.md b/README.md index 7f5868e..2d1498d 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,20 @@ uv run pytest and reports external tools; it does not request Bluetooth permission, scan the LAN, touch the K1, alter Homebrew, or change capture permissions. +The currently implemented laboratory commands are: + +```bash +uv run k1link ble scan --duration 30 --out sessions//captures/ble.json +uv run k1link ble gatt-dump --device \ + --out sessions//captures/gatt.json +uv run k1link net snapshot --out sessions//captures/network.json +``` + +On macOS the scan is active CoreBluetooth discovery, but it does not connect to +or modify devices. `gatt-dump` connects and performs service discovery only; it +does not read characteristic values, subscribe to notifications or write +configuration. Session output is sensitive and ignored by Git. + ## Documentation - [Technical audit](docs/00_TECHNICAL_AUDIT.md) @@ -65,7 +79,7 @@ provenance remains clear. ## Safety boundary -Allowed initial work is passive discovery, standard device-information reads, +Allowed initial work is non-mutating discovery, standard device-information reads, controlled notification listening, autonomous button operation, targeted capture of traffic to or from the confirmed K1 address, and offline analysis of owned artifacts. diff --git a/docs/01_IMPLEMENTATION_PLAN.md b/docs/01_IMPLEMENTATION_PLAN.md index 4a4dcbf..6641c04 100644 --- a/docs/01_IMPLEMENTATION_PLAN.md +++ b/docs/01_IMPLEMENTATION_PLAN.md @@ -43,7 +43,7 @@ documenting the missing ground truth. STOP: serious fault, overheating, activation lock, or destructive/ambiguous device state. -## Stage 1 — BLE read-only toolkit +## Stage 1 — BLE non-mutating discovery and metadata toolkit Minimal dependencies: `bleak` plus the existing CLI stack. Add them only in the repository-local environment. @@ -59,6 +59,8 @@ k1link ble listen Requirements: - preserve advertisement snapshots, manufacturer/service data and RSSI; +- describe macOS scanning as active discovery: CoreBluetooth does not support + passive scan mode, although discovery does not change K1 configuration; - identify by observed name + macOS UUID + advertisement fingerprint; - inspect standby, scanning and USB states separately; - start with standard Device Information/Battery services; diff --git a/docs/02_FIRST_LAB_RUNBOOK.md b/docs/02_FIRST_LAB_RUNBOOK.md index ee4681c..3eac962 100644 --- a/docs/02_FIRST_LAB_RUNBOOK.md +++ b/docs/02_FIRST_LAB_RUNBOOK.md @@ -49,7 +49,7 @@ Decision: - PAUSE USB analysis if recording works but USB is unavailable. - STOP on activation lock or device fault. -## Experiment C — passive BLE surface +## Experiment C — BLE discovery and metadata surface Prerequisite: Stage 1 BLE commands implemented and reviewed. @@ -131,4 +131,3 @@ GO / PAUSE / BLOCKED: Evidence: Next smallest experiment: ``` - diff --git a/pyproject.toml b/pyproject.toml index 393b920..fdff046 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ requires-python = ">=3.12,<3.13" license = { text = "Proprietary" } authors = [{ name = "NODE.DC" }] dependencies = [ + "bleak==3.0.2", "rich>=13.9,<15", "typer>=0.15,<1", ] diff --git a/src/k1link/artifacts.py b/src/k1link/artifacts.py new file mode 100644 index 0000000..abf523b --- /dev/null +++ b/src/k1link/artifacts.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import json +import os +import tempfile +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + + +def utc_now_iso() -> str: + """Return a stable UTC timestamp for manifests and capture artifacts.""" + return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z") + + +def write_json_atomic(path: Path, payload: Any) -> None: + """Write JSON without exposing a partially written artifact.""" + path = path.expanduser() + path.parent.mkdir(parents=True, exist_ok=True) + serialized = json.dumps(payload, ensure_ascii=False, indent=2) + "\n" + + temp_name: str | None = None + try: + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + dir=path.parent, + prefix=f".{path.name}.", + suffix=".tmp", + delete=False, + ) as stream: + temp_name = stream.name + stream.write(serialized) + stream.flush() + os.fsync(stream.fileno()) + Path(temp_name).replace(path) + finally: + if temp_name is not None: + Path(temp_name).unlink(missing_ok=True) diff --git a/src/k1link/ble/__init__.py b/src/k1link/ble/__init__.py new file mode 100644 index 0000000..a8c49e1 --- /dev/null +++ b/src/k1link/ble/__init__.py @@ -0,0 +1 @@ +"""Bluetooth Low Energy discovery tools.""" diff --git a/src/k1link/ble/gatt.py b/src/k1link/ble/gatt.py new file mode 100644 index 0000000..3c9a58a --- /dev/null +++ b/src/k1link/ble/gatt.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +import asyncio +from importlib.metadata import version +from typing import TypedDict + +from bleak import BleakClient, BleakScanner +from bleak.exc import BleakDeviceNotFoundError + +from k1link.artifacts import utc_now_iso + + +class DescriptorRecord(TypedDict): + uuid: str + handle: int + description: str + + +class CharacteristicRecord(TypedDict): + uuid: str + handle: int + description: str + properties: list[str] + descriptors: list[DescriptorRecord] + + +class ServiceRecord(TypedDict): + uuid: str + handle: int + description: str + characteristics: list[CharacteristicRecord] + + +class GattDumpResult(TypedDict): + schema_version: int + started_at_utc: str + completed_at_utc: str + adapter: str + bleak_version: str + device_macos_uuid: str + device_name: str + metadata_only: bool + services: list[ServiceRecord] + + +async def dump_metadata(device_macos_uuid: str, timeout_seconds: float) -> GattDumpResult: + """Connect and enumerate GATT metadata without characteristic reads or writes.""" + if timeout_seconds <= 0: + raise ValueError("timeout_seconds must be positive") + + started_at = utc_now_iso() + async with asyncio.timeout(timeout_seconds): + device = await BleakScanner.find_device_by_address( + device_macos_uuid, + timeout=min(20.0, timeout_seconds), + ) + if device is None: + raise BleakDeviceNotFoundError( + device_macos_uuid, + "Device was not rediscovered; keep the K1 powered and nearby.", + ) + + async with BleakClient(device, timeout=timeout_seconds, pair=False) as client: + services: list[ServiceRecord] = [] + for service in client.services: + characteristics: list[CharacteristicRecord] = [] + for characteristic in service.characteristics: + descriptors: list[DescriptorRecord] = [] + for descriptor in characteristic.descriptors: + descriptors.append( + { + "uuid": descriptor.uuid, + "handle": descriptor.handle, + "description": descriptor.description, + } + ) + characteristics.append( + { + "uuid": characteristic.uuid, + "handle": characteristic.handle, + "description": characteristic.description, + "properties": sorted(characteristic.properties), + "descriptors": descriptors, + } + ) + services.append( + { + "uuid": service.uuid, + "handle": service.handle, + "description": service.description, + "characteristics": characteristics, + } + ) + return { + "schema_version": 1, + "started_at_utc": started_at, + "completed_at_utc": utc_now_iso(), + "adapter": "CoreBluetooth", + "bleak_version": version("bleak"), + "device_macos_uuid": device_macos_uuid, + "device_name": client.name, + "metadata_only": True, + "services": services, + } diff --git a/src/k1link/ble/scanner.py b/src/k1link/ble/scanner.py new file mode 100644 index 0000000..62c23b5 --- /dev/null +++ b/src/k1link/ble/scanner.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from importlib.metadata import version +from typing import TypedDict + +from bleak import BleakScanner +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from k1link.artifacts import utc_now_iso + + +class BleDeviceRecord(TypedDict): + macos_uuid: str + id_kind: str + name: str | None + local_name: str | None + rssi: int + tx_power: int | None + service_uuids: list[str] + manufacturer_data_hex: dict[str, str] + service_data_hex: dict[str, str] + k1_name_candidate: bool + + +class BleScanResult(TypedDict): + schema_version: int + started_at_utc: str + completed_at_utc: str + duration_seconds: float + adapter: str + bleak_version: str + device_count: int + devices: list[BleDeviceRecord] + + +def advertisement_record(device: BLEDevice, advertisement: AdvertisementData) -> BleDeviceRecord: + name = advertisement.local_name or device.name + normalized_name = (name or "").casefold() + return { + "macos_uuid": device.address, + "id_kind": "corebluetooth_uuid", + "name": device.name, + "local_name": advertisement.local_name, + "rssi": advertisement.rssi, + "tx_power": advertisement.tx_power, + "service_uuids": sorted(advertisement.service_uuids), + "manufacturer_data_hex": { + str(company_id): data.hex() + for company_id, data in sorted(advertisement.manufacturer_data.items()) + }, + "service_data_hex": { + service_uuid: data.hex() + for service_uuid, data in sorted(advertisement.service_data.items()) + }, + "k1_name_candidate": any(marker in normalized_name for marker in ("lixel", "xgrids", "k1")), + } + + +async def scan(duration_seconds: float) -> BleScanResult: + if duration_seconds <= 0: + raise ValueError("duration_seconds must be positive") + + started_at = utc_now_iso() + discovered = await BleakScanner.discover(timeout=duration_seconds, return_adv=True) + devices = [ + advertisement_record(device, advertisement) for device, advertisement in discovered.values() + ] + devices.sort( + key=lambda item: (not item["k1_name_candidate"], -item["rssi"], item["macos_uuid"]) + ) + return { + "schema_version": 1, + "started_at_utc": started_at, + "completed_at_utc": utc_now_iso(), + "duration_seconds": duration_seconds, + "adapter": "CoreBluetooth", + "bleak_version": version("bleak"), + "device_count": len(devices), + "devices": devices, + } diff --git a/src/k1link/cli.py b/src/k1link/cli.py index 3f1e0ed..8adb5e3 100644 --- a/src/k1link/cli.py +++ b/src/k1link/cli.py @@ -1,18 +1,24 @@ from __future__ import annotations +import asyncio import json import platform import shutil import subprocess import sys from pathlib import Path -from typing import TypedDict +from typing import Annotated, TypedDict import typer +from bleak.exc import BleakError from rich.console import Console from rich.table import Table from k1link import __version__ +from k1link.artifacts import write_json_atomic +from k1link.ble.gatt import dump_metadata +from k1link.ble.scanner import scan +from k1link.net.snapshot import snapshot app = typer.Typer( name="k1link", @@ -20,6 +26,10 @@ app = typer.Typer( no_args_is_help=True, ) console = Console() +ble_app = typer.Typer(help="Bluetooth LE discovery and metadata commands.", no_args_is_help=True) +net_app = typer.Typer(help="Passive local network observation commands.", no_args_is_help=True) +app.add_typer(ble_app, name="ble") +app.add_typer(net_app, name="net") class ToolStatus(TypedDict): @@ -168,5 +178,86 @@ def doctor( console.print(f"- {note}") +@ble_app.command("scan") +def ble_scan( + out: Annotated[ + Path, + typer.Option(help="Ignored session JSON path; parent directories are created."), + ], + duration: Annotated[ + float, + typer.Option(min=1.0, max=300.0, help="Scan duration in seconds."), + ] = 30.0, +) -> None: + """Discover BLE advertisements without connecting or changing device configuration.""" + try: + result = asyncio.run(scan(duration)) + except (BleakError, OSError, ValueError) as exc: + console.print(f"[red]BLE scan failed:[/red] {type(exc).__name__}: {exc}") + console.print("Check System Settings → Privacy & Security → Bluetooth.") + raise typer.Exit(code=2) from exc + + write_json_atomic(out, result) + table = Table(title=f"BLE devices ({result['device_count']})") + table.add_column("Candidate") + table.add_column("Name") + table.add_column("RSSI") + table.add_column("macOS UUID") + for device in result["devices"]: + table.add_row( + "K1?" if device["k1_name_candidate"] else "", + device["local_name"] or device["name"] or "-", + str(device["rssi"]), + device["macos_uuid"], + ) + console.print(table) + console.print(f"Saved: {out}") + + +@ble_app.command("gatt-dump") +def ble_gatt_dump( + device: Annotated[str, typer.Option(help="CoreBluetooth/macOS UUID from ble scan.")], + out: Annotated[ + Path, + typer.Option(help="Ignored session JSON path; parent directories are created."), + ], + timeout: Annotated[ + float, + typer.Option(min=5.0, max=120.0, help="Connection timeout in seconds."), + ] = 45.0, +) -> None: + """Enumerate GATT metadata only: no characteristic reads, subscriptions or writes.""" + console.print( + "Connecting for service discovery only; no characteristic values will be read or written." + ) + try: + result = asyncio.run(dump_metadata(device, timeout)) + except (BleakError, OSError, ValueError) as exc: + console.print(f"[red]GATT metadata failed:[/red] {type(exc).__name__}: {exc}") + raise typer.Exit(code=2) from exc + + write_json_atomic(out, result) + characteristic_count = sum(len(service["characteristics"]) for service in result["services"]) + console.print( + f"Device: {result['device_name']}; services: {len(result['services'])}; " + f"characteristics: {characteristic_count}" + ) + console.print(f"Saved: {out}") + + +@net_app.command("snapshot") +def net_snapshot( + out: Annotated[ + Path, + typer.Option(help="Ignored session JSON path; parent directories are created."), + ], +) -> None: + """Save routes, interfaces and the existing neighbor table without scanning the LAN.""" + result = snapshot() + write_json_atomic(out, result) + console.print("Saved a sensitive local network snapshot; do not commit the output.") + console.print(f"Saved: {out}") + + if __name__ == "__main__": app() diff --git a/src/k1link/net/__init__.py b/src/k1link/net/__init__.py new file mode 100644 index 0000000..f906f59 --- /dev/null +++ b/src/k1link/net/__init__.py @@ -0,0 +1 @@ +"""Passive network observation tools.""" diff --git a/src/k1link/net/snapshot.py b/src/k1link/net/snapshot.py new file mode 100644 index 0000000..b857672 --- /dev/null +++ b/src/k1link/net/snapshot.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import subprocess +from typing import TypedDict + +from k1link.artifacts import utc_now_iso + + +class CommandRecord(TypedDict): + argv: list[str] + returncode: int | None + stdout: str + stderr: str + error: str | None + + +class NetworkSnapshot(TypedDict): + schema_version: int + created_at_utc: str + sensitivity: str + commands: list[CommandRecord] + + +def command_record(argv: list[str]) -> CommandRecord: + try: + result = subprocess.run( + argv, + check=False, + capture_output=True, + text=True, + timeout=10, + ) + except (OSError, subprocess.SubprocessError) as exc: + return { + "argv": argv, + "returncode": None, + "stdout": "", + "stderr": "", + "error": f"{type(exc).__name__}: {exc}", + } + return { + "argv": argv, + "returncode": result.returncode, + "stdout": result.stdout, + "stderr": result.stderr, + "error": None, + } + + +def snapshot() -> NetworkSnapshot: + """Collect a local read-only network snapshot; no packets are transmitted intentionally.""" + commands = [ + ["route", "-n", "get", "default"], + ["networksetup", "-listallhardwareports"], + ["scutil", "--nwi"], + ["arp", "-an"], + ] + return { + "schema_version": 1, + "created_at_utc": utc_now_iso(), + "sensitivity": ( + "contains local interface, route, IP/MAC and neighbor metadata; do not commit" + ), + "commands": [command_record(command) for command in commands], + } diff --git a/tests/test_artifacts.py b/tests/test_artifacts.py new file mode 100644 index 0000000..5bc7387 --- /dev/null +++ b/tests/test_artifacts.py @@ -0,0 +1,11 @@ +import json +from pathlib import Path + +from k1link.artifacts import write_json_atomic + + +def test_write_json_atomic(tmp_path: Path) -> None: + output = tmp_path / "nested" / "artifact.json" + write_json_atomic(output, {"value": "тест"}) + assert json.loads(output.read_text(encoding="utf-8")) == {"value": "тест"} + assert not list(output.parent.glob("*.tmp")) diff --git a/tests/test_ble_scanner.py b/tests/test_ble_scanner.py new file mode 100644 index 0000000..4bd6957 --- /dev/null +++ b/tests/test_ble_scanner.py @@ -0,0 +1,23 @@ +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from k1link.ble.scanner import advertisement_record + + +def test_advertisement_record_marks_k1_candidate() -> None: + device = BLEDevice("TEST-UUID", "Unknown", details=None) + advertisement = AdvertisementData( + local_name="Lixel-K1-1234", + manufacturer_data={123: b"\x01\x02"}, + service_data={"service": b"\xaa"}, + service_uuids=["B", "a"], + tx_power=-4, + rssi=-52, + platform_data=(), + ) + record = advertisement_record(device, advertisement) + assert record["k1_name_candidate"] is True + assert record["id_kind"] == "corebluetooth_uuid" + assert record["manufacturer_data_hex"] == {"123": "0102"} + assert record["service_data_hex"] == {"service": "aa"} + assert record["service_uuids"] == ["B", "a"] diff --git a/tests/test_network_snapshot.py b/tests/test_network_snapshot.py new file mode 100644 index 0000000..1952d7e --- /dev/null +++ b/tests/test_network_snapshot.py @@ -0,0 +1,14 @@ +from k1link.net.snapshot import command_record, snapshot + + +def test_command_record_captures_success() -> None: + result = command_record(["/usr/bin/true"]) + assert result["returncode"] == 0 + assert result["error"] is None + + +def test_snapshot_is_explicitly_sensitive() -> None: + result = snapshot() + assert result["schema_version"] == 1 + assert "do not commit" in result["sensitivity"] + assert [record["argv"] for record in result["commands"]][-1] == ["arp", "-an"] diff --git a/uv.lock b/uv.lock index 9e85b5e..a99304b 100644 --- a/uv.lock +++ b/uv.lock @@ -11,6 +11,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, ] +[[package]] +name = "bleak" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dbus-fast", marker = "sys_platform == 'linux'" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corebluetooth", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-libdispatch", marker = "sys_platform == 'darwin'" }, + { name = "winrt-runtime", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-devices-bluetooth", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-devices-bluetooth-advertisement", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-devices-bluetooth-genericattributeprofile", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-devices-enumeration", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-devices-radios", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-foundation", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-foundation-collections", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-storage-streams", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/df/05a3f80ca8e3f7f5b0dba68a9e618147c909ccdba1468f07487dc8d72a9d/bleak-3.0.2.tar.gz", hash = "sha256:c2229cb8238d5876b4bd05c74bf7a1aea1f88da39d2e51ac9dfd5cc319d5265f", size = 125293, upload-time = "2026-05-02T23:01:04.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/54/05aceb9cd80073805b3ed8522e3196e8cb22f70e741873fa51406c31f4e7/bleak-3.0.2-py3-none-any.whl", hash = "sha256:39092feb9e83f1df5ad2f88e837723c7211c982ce9e9cda6235104bc2ebe0d0d", size = 146490, upload-time = "2026-05-02T23:01:02.592Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -20,6 +44,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "dbus-fast" +version = "5.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/db/b621610e50b1bc46ff63534d75239553c1bf33256de6096b58214fd9808a/dbus_fast-5.0.22.tar.gz", hash = "sha256:34dc67d7d21a12399828dd13e63b352750580beea54ea7c729e708f2d2905fef", size = 83224, upload-time = "2026-06-05T18:47:59.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/06/233b0bc13919474f70320bf389cb81ce02811956d6cf86c45e84679b63c3/dbus_fast-5.0.22-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f0bcad7f71d2304a68a5b0bc0d24c3fcc14710a2ffcf5f2a27521e3aece71ca", size = 799464, upload-time = "2026-06-05T18:56:02.617Z" }, + { url = "https://files.pythonhosted.org/packages/68/e9/77bc23a6f5aebfb8f2c34489795e8517aed7eca31738438e1a4c4a4891d3/dbus_fast-5.0.22-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ffcf16034f71a801bd2108aeffb6337d104c9459e8b1a218d16a917c8a2d2e9", size = 852687, upload-time = "2026-06-05T18:56:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/ae/04/56769d0936d1273d1801ef574ec426ccb3f61f4b0a7a0eeb9eb2b8ccafa5/dbus_fast-5.0.22-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98de6d2c200d8182e1fd0bdde3206fa556b8fa14ebb752a044cd8daa87b4658c", size = 833814, upload-time = "2026-06-05T18:56:05.87Z" }, + { url = "https://files.pythonhosted.org/packages/06/ca/964f0d39a3be03b12a98f39519d34ad95b74360c7e3adf4cd4907dc25fd6/dbus_fast-5.0.22-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b013437b66dc22b8d9aca5e0b0d46bf1980208a143409469fe482d9684a2a717", size = 806891, upload-time = "2026-06-05T18:56:07.623Z" }, + { url = "https://files.pythonhosted.org/packages/f4/25/57fe6ab509ad9da2e190498fa9c37f868e38ac521d940a9edb9ba6b6c657/dbus_fast-5.0.22-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:855f15b7f7805171da2b82de1c317d01cfbb9fb8ac61fcc1e8dec54d8c69fab7", size = 830867, upload-time = "2026-06-05T18:56:09.473Z" }, + { url = "https://files.pythonhosted.org/packages/be/2b/da036e9f4aeb776833139575fe0774544aa6cd13ba997eea7fbc4ab99852/dbus_fast-5.0.22-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7d1c42963235cfc015a2d2b8c5fe42b65387493b4ad4ce0ec122601c805e6742", size = 860651, upload-time = "2026-06-05T18:56:10.952Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -107,6 +145,7 @@ name = "ndc-xgrids-k1-connector" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "bleak" }, { name = "rich" }, { name = "typer" }, ] @@ -120,6 +159,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "bleak", specifier = "==3.0.2" }, { name = "rich", specifier = ">=13.9,<15" }, { name = "typer", specifier = ">=0.15,<1" }, ] @@ -167,6 +207,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyobjc-core" +version = "12.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/b1/729f7458a63758bd21716648a8abcd9a0c8f2d2e9897763c8a1a1c7fd31b/pyobjc_core-12.2.1.tar.gz", hash = "sha256:7a7b9b018402342cf32bf1956366896350fbe5c0478cb3ef59778f77abed7f07", size = 1063383, upload-time = "2026-06-19T16:19:39.357Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/88/300ad283bed0c971c52dcac6f70113e138169d4ce6d856ddd03d16081e51/pyobjc_core-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a64232bb27ed101d4adc7d42b0e64a6d3331aac7bee7861c037a6777a163f10b", size = 6433347, upload-time = "2026-06-19T16:04:49.341Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/34/fbe38a204643aa4e1b91391cdce07a34da565a69171ebcad08de7438a556/pyobjc_framework_cocoa-12.2.1.tar.gz", hash = "sha256:b94b37fe5730e5ae1fb0052912cd174e6ec329b0bfba4a012ae5db1014b5864b", size = 3125751, upload-time = "2026-06-19T16:20:05.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/cf/1b3b32b2f28f66cc053c3438ef4e6df36a1591945bf05e7399da18d74553/pyobjc_framework_cocoa-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:28b9b8bab1c36efb94744786918752d0c1842f5fbb67e7d5ca97b5f736512080", size = 388113, upload-time = "2026-06-19T16:07:38.9Z" }, +] + +[[package]] +name = "pyobjc-framework-corebluetooth" +version = "12.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/91/c76f3c5e8e80c7047e43c4c05b3e6fda9a7cefad5aae85487007674c966c/pyobjc_framework_corebluetooth-12.2.1.tar.gz", hash = "sha256:7dbb285295097205bebbcb11f55161e5faa02111108fb7b17536176e31971eb0", size = 37568, upload-time = "2026-06-19T16:20:12.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/c4/7938016860850e28c001dc9b7c653352c43f7aebfffc6d3c5fd087281f22/pyobjc_framework_corebluetooth-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2f8d2ed65c0e98ba044b6a7fc2f9a290a5c579a28d33a57c642f8617ccbcca0f", size = 13217, upload-time = "2026-06-19T16:08:27.802Z" }, +] + +[[package]] +name = "pyobjc-framework-libdispatch" +version = "12.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/3f/561653aff3f19873457c95c053f0298da517be89fdfc0ec35115ed5b7030/pyobjc_framework_libdispatch-12.2.1.tar.gz", hash = "sha256:0d24eda41c6c258135077f60d410e704bc7b5a67adcb2ca463918896c7363795", size = 40336, upload-time = "2026-06-19T16:20:56.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/8f/42cfa987c07a2b5ce8c236a42b0fb388b8807dac72c25e004cd4905ea9a3/pyobjc_framework_libdispatch-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8f41b5021ff70bc51220a79b41ebd1eacb55fe3ceb67448594f30e491a2c42a5", size = 15656, upload-time = "2026-06-19T16:12:50.361Z" }, +] + [[package]] name = "pytest" version = "8.4.2" @@ -253,3 +340,129 @@ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3 wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] + +[[package]] +name = "winrt-runtime" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/dd/acdd527c1d890c8f852cc2af644aa6c160974e66631289420aa871b05e65/winrt_runtime-3.2.1.tar.gz", hash = "sha256:c8dca19e12b234ae6c3dadf1a4d0761b51e708457492c13beb666556958801ea", size = 21721, upload-time = "2025-06-06T14:40:27.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/54/3dd06f2341fab6abb06588a16b30e0b213b0125be7b79dafc3bdba3b334a/winrt_runtime-3.2.1-cp312-cp312-win32.whl", hash = "sha256:762b3d972a2f7037f7db3acbaf379dd6d8f6cda505f71f66c6b425d1a1eae2f1", size = 210090, upload-time = "2025-06-06T06:44:08.151Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a1/1d7248d5c62ccbea5f3e0da64ca4529ce99c639c3be2485b6ed709f5c740/winrt_runtime-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:06510db215d4f0dc45c00fbb1251c6544e91742a0ad928011db33b30677e1576", size = 241391, upload-time = "2025-06-06T06:44:09.442Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ae/6a205d8dafc79f7c242be7f940b1e0c1971fd64ab3079bda4b514aa3d714/winrt_runtime-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:14562c29a087ccad38e379e585fef333e5c94166c807bdde67b508a6261aa195", size = 415242, upload-time = "2025-06-06T06:44:10.407Z" }, +] + +[[package]] +name = "winrt-windows-devices-bluetooth" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/a0/1c8a0c469abba7112265c6cb52f0090d08a67c103639aee71fc690e614b8/winrt_windows_devices_bluetooth-3.2.1.tar.gz", hash = "sha256:db496d2d92742006d5a052468fc355bf7bb49e795341d695c374746113d74505", size = 23732, upload-time = "2025-06-06T14:41:20.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/ff/c4a3de909a875b46fad5e9f4fd412bba48571405bfa802b878954abf128c/winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win32.whl", hash = "sha256:18c833ec49e7076127463679e85efc59f61785ade0dc185c852586b21be1f31c", size = 105752, upload-time = "2025-06-06T07:00:10.684Z" }, + { url = "https://files.pythonhosted.org/packages/e7/78/bfee1f0c8d188c561c5b946ab21f6a0037e60dea110e80b1d6a1d529639f/winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:9b6702c462b216c91e32388023a74d0f87210cef6fd5d93b7191e9427ce2faca", size = 113356, upload-time = "2025-06-06T07:00:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1b/d9da9c29d36cabadef4e19c3e9ba6d2692f6a28224c81fcff757132ea0da/winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:419fd1078c7749119f6b4bbf6be4e586e03a0ed544c03b83178f1d85f1b3d148", size = 104724, upload-time = "2025-06-06T07:00:12.406Z" }, +] + +[[package]] +name = "winrt-windows-devices-bluetooth-advertisement" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/fc/7ffe66ca4109b9e994b27c00f3d2d506e6e549e268791f755287ad9106d8/winrt_windows_devices_bluetooth_advertisement-3.2.1.tar.gz", hash = "sha256:0223852a7b7fa5c8dea3c6a93473bd783df4439b1ed938d9871f947933e574cc", size = 16906, upload-time = "2025-06-06T14:41:21.448Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/15/ad05c28e049208c97011728e2debdb45439175f75efe357b6faa4c9ba099/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win32.whl", hash = "sha256:901933cc40de5eb7e5f4188897c899dd0b0f577cb2c13eab1a63c7dfe89b08c4", size = 90033, upload-time = "2025-06-06T07:00:23.421Z" }, + { url = "https://files.pythonhosted.org/packages/26/48/074779081841f6eba4987930c4e7adcec38a5985b7dffd9fecc41f39a89c/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:e6c66e7d4f4ca86d2c801d30efd2b9673247b59a2b4c365d9e11650303d68d89", size = 95824, upload-time = "2025-06-06T07:00:24.238Z" }, + { url = "https://files.pythonhosted.org/packages/aa/25/e01966033a02b2d0718710bb47ef4f6b9b5a619ca2c857e06eb5c8e3ed13/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:447d19defd8982d39944642eb7ebe89e4e20259ec9734116cf88879fb2c514ff", size = 89311, upload-time = "2025-06-06T07:00:25.029Z" }, +] + +[[package]] +name = "winrt-windows-devices-bluetooth-genericattributeprofile" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/21/aeeddc0eccdfbd25e543360b5cc093233e2eab3cdfb53ad3cabae1b5d04d/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1.tar.gz", hash = "sha256:cdf6ddc375e9150d040aca67f5a17c41ceaf13a63f3668f96608bc1d045dde71", size = 38896, upload-time = "2025-06-06T14:41:22.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/a1/75ac783a5faee9b455fef2f53b7fef97b21ed60d52401b44c690202141e4/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win32.whl", hash = "sha256:ef894d21e0a805f3e114940254636a8045335fa9de766c7022af5d127dfad557", size = 183326, upload-time = "2025-06-06T07:00:52.662Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d9/a9dcc15322d2f5c7dfd491bd7ab121e36437caf78ebfa92bc0dd0546e2ca/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:db05de95cd1b24a51abb69cb936a8b17e9214e015757d0b37e3a5e207ddceb3d", size = 187810, upload-time = "2025-06-06T07:00:53.594Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fc/47d00af076f558267097af3050910beda6bf8a21ceaa5830bbd26fcaf85e/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d4e131cf3d15fc5ad81c1bcde3509ac171298217381abed6bdf687f29871984", size = 184516, upload-time = "2025-06-06T07:00:55.24Z" }, +] + +[[package]] +name = "winrt-windows-devices-enumeration" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/dd/75835bfbd063dffa152109727dedbd80f6e92ea284855f7855d48cdf31c9/winrt_windows_devices_enumeration-3.2.1.tar.gz", hash = "sha256:df316899e39bfc0ffc1f3cb0f5ee54d04e1d167fbbcc1484d2d5121449a935cf", size = 23538, upload-time = "2025-06-06T14:41:26.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/3e/81642208ecd6c6c936f35a39a433c54e3f68e09d316546b8f953581ae334/winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win32.whl", hash = "sha256:1db22b0292b93b0688d11ad932ad1f3629d4f471310281a2fbfe187530c2c1f3", size = 130249, upload-time = "2025-06-06T07:02:02.237Z" }, + { url = "https://files.pythonhosted.org/packages/00/f4/a9ede5f3f0d86abfc7590726cf711133d97419b49ced372fca532e4f0696/winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:a73bc88d7f510af454f2b392985501c96f39b89fd987140708ccaec1588ceebc", size = 141512, upload-time = "2025-06-06T07:02:03.424Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/4fad07c03124bdc3acd64f80f3bd3cc4417ea641e07bb16a9503afd3e554/winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:2853d687803f0dd76ae1afe3648abc0453e09dff0e7eddbb84b792eddb0473ca", size = 135383, upload-time = "2025-06-06T07:02:04.312Z" }, +] + +[[package]] +name = "winrt-windows-devices-radios" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/02/9704ea359ad8b0d6faa1011f98fb477e8fb6eac5201f39d19e73c2407e7b/winrt_windows_devices_radios-3.2.1.tar.gz", hash = "sha256:4dc9b9d1501846049eb79428d64ec698d6476c27a357999b78a8331072e18a0b", size = 5908, upload-time = "2025-06-06T14:41:44.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/e0/4731a3c412318b2c5e74a8803a32e2fb9afc2c98368c6b61a422eb359e7e/winrt_windows_devices_radios-3.2.1-cp312-cp312-win32.whl", hash = "sha256:c3e683ce682338a5a5ed465f735e223ba7a22f16d0bbea2d070962bc7657edbb", size = 38606, upload-time = "2025-06-06T07:08:01.477Z" }, + { url = "https://files.pythonhosted.org/packages/37/8e/91464854dfc9e0be9ce8dcbe2bd6a67c19b68ab91584fc5de0f4f13e78f8/winrt_windows_devices_radios-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:a116e552a3f38607b9be558fb2e7de9b4450d1f9080069944d74d80cdda1873e", size = 40172, upload-time = "2025-06-06T07:08:02.214Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0d/1bd62f606b6c4dfa936fccc4712be5506a40fc5d1b7177c3d3cbcaf30972/winrt_windows_devices_radios-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4c28822f9251c9d547324f596b5c2581f050254ded05e5b786c650a3502744c1", size = 36989, upload-time = "2025-06-06T07:08:03.295Z" }, +] + +[[package]] +name = "winrt-windows-foundation" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/55/098ce7ea0679efcc1298b269c48768f010b6c68f90c588f654ec874c8a74/winrt_windows_foundation-3.2.1.tar.gz", hash = "sha256:ad2f1fcaa6c34672df45527d7c533731fdf65b67c4638c2b4aca949f6eec0656", size = 30485, upload-time = "2025-06-06T14:41:53.344Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/f8/495e304ddedd5ff2f196efbde906265cb75ade4d79e2937837f72ef654a0/winrt_windows_foundation-3.2.1-cp312-cp312-win32.whl", hash = "sha256:867642ccf629611733db482c4288e17b7919f743a5873450efb6d69ae09fdc2b", size = 112169, upload-time = "2025-06-06T07:11:01.438Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5e/b5059e4ece095351c496c9499783130c302d25e353c18031d5231b1b3b3c/winrt_windows_foundation-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:45550c5b6c2125cde495c409633e6b1ea5aa1677724e3b95eb8140bfccbe30c9", size = 118668, upload-time = "2025-06-06T07:11:02.475Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/acbcb3ef07b1b67e2de4afab9176a5282cfd775afd073efe6828dfc65ace/winrt_windows_foundation-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:94f4661d71cb35ebc52be7af112f2eeabdfa02cb05e0243bf9d6bd2cafaa6f37", size = 109671, upload-time = "2025-06-06T07:11:03.538Z" }, +] + +[[package]] +name = "winrt-windows-foundation-collections" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/62/d21e3f1eeb8d47077887bbf0c3882c49277a84d8f98f7c12bda64d498a07/winrt_windows_foundation_collections-3.2.1.tar.gz", hash = "sha256:0eff1ad0d8d763ad17e9e7bbd0c26a62b27215016393c05b09b046d6503ae6d5", size = 16043, upload-time = "2025-06-06T14:41:53.983Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/0b/7802349391466d3f7e8f62f588f36a1a0b6560abfcdbdaa426fe21d322b4/winrt_windows_foundation_collections-3.2.1-cp312-cp312-win32.whl", hash = "sha256:15704eef3125788f846f269cf54a3d89656fa09a1dc8428b70871f717d595ad6", size = 60060, upload-time = "2025-06-06T07:11:16.173Z" }, + { url = "https://files.pythonhosted.org/packages/37/94/5b888713e472746635a382e523513ab1b8200af55c5b56bc70e1e4369115/winrt_windows_foundation_collections-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:550dfb8c82fe74d9e0728a2a16a9175cc9e34ca2b8ef758d69b2a398894b698b", size = 69058, upload-time = "2025-06-06T07:11:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3c/829273622c9b37c67b97f187b92be318404f7d33db045e31d72b7d50f54c/winrt_windows_foundation_collections-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:810ad4bd11ab4a74fdbcd3ed33b597ef7c0b03af73fc9d7986c22bcf3bd24f84", size = 58793, upload-time = "2025-06-06T07:11:17.837Z" }, +] + +[[package]] +name = "winrt-windows-storage-streams" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/50/f4488b07281566e3850fcae1021f0285c9653992f60a915e15567047db63/winrt_windows_storage_streams-3.2.1.tar.gz", hash = "sha256:476f522722751eb0b571bc7802d85a82a3cae8b1cce66061e6e758f525e7b80f", size = 34335, upload-time = "2025-06-06T14:43:23.905Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/e7/7d3f2a4a442f264e05cab2bdf20ed1b95cb3f753bd1b0f277f2b49fb8335/winrt_windows_storage_streams-3.2.1-cp312-cp312-win32.whl", hash = "sha256:77c1f0e004b84347b5bd705e8f0fc63be8cd29a6093be13f1d0869d0d97b7d78", size = 127787, upload-time = "2025-06-06T14:02:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2f/cc36f475f8af293f40e2c2a5d6c2e75a189c2c2d4d01ecb3551578518c79/winrt_windows_storage_streams-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4508ee135af53e4fc142876abbf4bc7c2a95edfc7d19f52b291a8499cacd6dc", size = 131849, upload-time = "2025-06-06T14:02:03.09Z" }, + { url = "https://files.pythonhosted.org/packages/94/84/896fb734f7456910ec412f3f3adfdc3f0dc3134864a496d5b120592f3bfd/winrt_windows_storage_streams-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:040cb94e6fb26b0d00a00e8b88b06fadf29dfe18cf24ed6cb3e69709c3613307", size = 128144, upload-time = "2025-06-06T14:02:03.946Z" }, +]