quilt-hp-python 0.2.2__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quilt_hp_python-0.3.0/CHANGELOG.md +78 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/PKG-INFO +1 -1
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/how-to/automation-daemon.md +16 -12
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/how-to/configure-comfort-settings.md +7 -7
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/how-to/configure-schedules.md +32 -20
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/how-to/control-spaces.md +1 -1
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/how-to/stream-updates.md +55 -12
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/how-to/tui-app.md +18 -21
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/reference/client.md +21 -3
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/reference/models.md +186 -58
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/pyproject.toml +1 -1
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/__init__.py +3 -1
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/auth.py +14 -3
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/cli/main.py +61 -27
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/cli/settings.py +2 -1
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/cli/store.py +45 -6
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/cli/tui.py +55 -6
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/client.py +130 -95
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/models/__init__.py +2 -0
- quilt_hp_python-0.3.0/src/quilt_hp/models/_helpers.py +31 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/models/controller.py +7 -5
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/models/energy.py +11 -4
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/models/enums.py +11 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/models/indoor_unit.py +3 -1
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/models/outdoor_unit.py +30 -19
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/models/qsm.py +4 -4
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/models/schedule.py +3 -2
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/models/sensor.py +8 -4
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/models/space.py +9 -5
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/models/system.py +34 -3
- quilt_hp_python-0.3.0/src/quilt_hp/services/__init__.py +113 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/services/hds.py +41 -3
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/services/streaming.py +284 -84
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/services/system.py +20 -9
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/services/user.py +7 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/transport.py +21 -4
- quilt_hp_python-0.3.0/tests/conftest.py +87 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_auth_store_settings_edges.py +1 -1
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_cli_login.py +2 -1
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_cli_surfaces_extra.py +32 -1
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_client_service_error_paths.py +35 -0
- quilt_hp_python-0.3.0/tests/test_grpc_retry.py +113 -0
- quilt_hp_python-0.3.0/tests/test_hds_payloads.py +124 -0
- quilt_hp_python-0.3.0/tests/test_models_extra.py +285 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_models_from_proto.py +297 -15
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_streaming.py +17 -0
- quilt_hp_python-0.3.0/tests/test_streaming_concurrency.py +140 -0
- quilt_hp_python-0.3.0/tests/test_streaming_debounce.py +99 -0
- quilt_hp_python-0.3.0/tests/test_streaming_health.py +127 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_streaming_reconnect_dispatch_extra.py +7 -2
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_transport_interceptor_extra.py +33 -0
- quilt_hp_python-0.3.0/tests/test_tui_bindings.py +70 -0
- quilt_hp_python-0.2.2/CHANGELOG.md +0 -54
- quilt_hp_python-0.2.2/src/quilt_hp/services/__init__.py +0 -1
- quilt_hp_python-0.2.2/tests/conftest.py +0 -3
- quilt_hp_python-0.2.2/tests/test_tui_bindings.py +0 -38
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/.github/copilot-instructions.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/.github/workflows/ci.yml +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/.github/workflows/docs-deploy.yml +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/.github/workflows/release.yml +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/.gitignore +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/LICENSE +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/README.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/explanation/architecture.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/explanation/authentication.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/explanation/grpc-and-protobuf.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/explanation/snapshot-and-stream.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/explanation/streaming-protocol.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/how-to/authenticate.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/how-to/cli-scripting.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/how-to/contribute.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/how-to/home-assistant.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/how-to/regenerate-protos.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/index.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/reference/api-reference.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/reference/documentation-standards.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/reference/grpc-services-matrix.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/reference/hds-entities.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/reference/token-management.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/docs/tutorial/get-started.md +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/mkdocs.yml +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/proto/cleaned/quilt_device_pairing.proto +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/proto/cleaned/quilt_hds.proto +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/proto/cleaned/quilt_notifier.proto +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/proto/cleaned/quilt_services.proto +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/proto/cleaned/quilt_system.proto +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/scripts/bump_version.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/scripts/check_docs_nav.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/scripts/generate_public_api_reference.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/scripts/regen_protos.sh +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_paths.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_proto/__init__.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_proto/quilt_device_pairing_pb2.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_proto/quilt_device_pairing_pb2.pyi +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_proto/quilt_device_pairing_pb2_grpc.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_proto/quilt_hds_pb2.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_proto/quilt_hds_pb2.pyi +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_proto/quilt_hds_pb2_grpc.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_proto/quilt_notifier_pb2.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_proto/quilt_notifier_pb2.pyi +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_proto/quilt_notifier_pb2_grpc.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_proto/quilt_services_pb2.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_proto/quilt_services_pb2.pyi +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_proto/quilt_services_pb2_grpc.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_proto/quilt_system_pb2.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_proto/quilt_system_pb2.pyi +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/_proto/quilt_system_pb2_grpc.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/cli/__init__.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/const.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/exceptions.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/models/comfort.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/models/software_update.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/py.typed +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/src/quilt_hp/tokens.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/__init__.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_auth.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_cli_feature_completion.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_client_cache.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_hds_schedule_mapping.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_hds_service_branches.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_models.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_settings_store.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_tokens.py +0 -0
- {quilt_hp_python-0.2.2 → quilt_hp_python-0.3.0}/tests/test_transport.py +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.3.0] - 2026-05-12
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `NotifierStream` health properties: `is_connected`, `last_event_at`, `stream_state`
|
|
9
|
+
- `NotifierStream` `debounce_s` parameter to coalesce rapid update bursts
|
|
10
|
+
- `MetricBucketStatus` enum exposed on `EnergyBucket.status` (was untyped `int`)
|
|
11
|
+
- `grpc_call()` context manager in `services` for consistent gRPC error translation and optional retry
|
|
12
|
+
- Structured `logging.getLogger(__name__)` across all modules (auth, client, transport, services, CLI)
|
|
13
|
+
- `QuiltStreamError` re-exported from the top-level `quilt_hp` package
|
|
14
|
+
- Shared model helpers (`_helpers.py`) for WiFi signal parsing and hardware lookup
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `ScheduleEvent.hvac_mode` is now typed as `HVACMode` (was `int`)
|
|
18
|
+
- Token temp file created with `os.open(..., 0o600)` so permissions are secure from creation (no transient world-readable window)
|
|
19
|
+
- Signature cache in transport layer uses `weakref.WeakKeyDictionary` instead of `dict[int, bool]` — prevents unbounded growth and id-reuse bugs after GC
|
|
20
|
+
- `login()` clears the token cache so a re-login always fetches fresh credentials
|
|
21
|
+
- `_GrpcCallContext` avoids self-chaining when re-raising a `QuiltError` unchanged
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- `EnergyBucket.has_missing_energy_value` now treats `None` (absent proto field) as missing, not just `NaN`; prevents `TypeError` in `SpaceEnergyMetrics.total_kwh`
|
|
25
|
+
- `MetricBucketStatus()` conversion in `get_energy_metrics` catches `ValueError` for unknown server values and falls back to `UNSPECIFIED` instead of raising
|
|
26
|
+
- `WeakKeyDictionary.get()` for the refresh-callback signature cache is now guarded against `TypeError` for non-weakrefable callables
|
|
27
|
+
- AUTO mode setpoint deadband clamp now runs before setpoint selection, ensuring the correct (clamped) value is sent to the device
|
|
28
|
+
- CLI settings `bool` coercion uses `isinstance(v, bool)` to avoid `bool("false") == True`
|
|
29
|
+
- Zero-value proto3 fields (0 °C temperature, 0 dBm WiFi signal, 0% humidity) are now preserved instead of being dropped as falsy
|
|
30
|
+
- `NotifierStream` reconnect subscription is now protected by an `asyncio.Lock` to prevent concurrent subscribe/reconnect races
|
|
31
|
+
- CLI enum lookups raise a clear error showing valid options on invalid input
|
|
32
|
+
- `auth.py` narrows broad `except Exception` to `except (QuiltAuthError, ClientError)` to avoid swallowing unexpected errors
|
|
33
|
+
|
|
34
|
+
## [0.2.2] - 2026-05-11
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- Corrected mapping of outdoor units to indoor units in SystemSnapshot
|
|
38
|
+
- Fixed TUI interaction issues with button handling and bindings
|
|
39
|
+
|
|
40
|
+
## [0.2.1] - 2026-05-10
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
- Restored CI/release quality-gate stability by applying required `ruff format`
|
|
44
|
+
updates in model files.
|
|
45
|
+
|
|
46
|
+
## [0.2.0] - 2026-05-10
|
|
47
|
+
|
|
48
|
+
## [0.1.4] - 2026-05-08
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- `boto3.client()` was called synchronously inside async functions, causing a
|
|
52
|
+
blocking HTTP request to the EC2 instance metadata service (IMDS) at
|
|
53
|
+
`169.254.169.254` during credential resolution. This manifested as an
|
|
54
|
+
`HTTPClientError` in Home Assistant's async event loop. The client is now
|
|
55
|
+
created via `loop.run_in_executor()` like the subsequent API calls.
|
|
56
|
+
|
|
57
|
+
## [0.1.3] - 2026-05-08
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
- Regenerated gRPC stubs with `grpcio-tools==1.78.0` so the library works
|
|
61
|
+
inside Home Assistant, which hard-pins `grpcio==1.78.0` in its package
|
|
62
|
+
constraints. Previously the stubs were generated with 1.80.0 and raised
|
|
63
|
+
`RuntimeError` at import time on older grpcio versions.
|
|
64
|
+
|
|
65
|
+
## [0.1.2] - 2026-05-08
|
|
66
|
+
|
|
67
|
+
## [0.1.1] - 2026-05-08
|
|
68
|
+
|
|
69
|
+
## [0.1.0]
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
- GitHub Actions release automation for SemVer tags (`vX.Y.Z`) that enforces quality gates, creates a GitHub Release, and publishes distribution artifacts to PyPI via trusted publishing
|
|
73
|
+
- Initial async client for Quilt cloud gRPC API
|
|
74
|
+
- Cognito OTP authentication with token caching
|
|
75
|
+
- HomeDatastoreService: spaces, indoor units, comfort settings, schedules
|
|
76
|
+
- SystemInformationService: system listing, energy metrics
|
|
77
|
+
- NotifierService: real-time streaming subscriptions
|
|
78
|
+
- CLI for interactive use (`quilt` command)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quilt-hp-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Async Python client for Quilt mini-split HVAC systems
|
|
5
5
|
Project-URL: Repository, https://github.com/eman/quilt-hp-python
|
|
6
6
|
Project-URL: Issues, https://github.com/eman/quilt-hp-python/issues
|
|
@@ -56,19 +56,24 @@ _snapshot: SystemSnapshot | None = None
|
|
|
56
56
|
async def on_space_update(space: Space) -> None:
|
|
57
57
|
global _snapshot
|
|
58
58
|
if _snapshot is not None:
|
|
59
|
-
|
|
59
|
+
space = _snapshot.apply_space(space)
|
|
60
60
|
|
|
61
61
|
temp = (
|
|
62
|
-
f"{space.state.
|
|
63
|
-
if space.state.
|
|
62
|
+
f"{space.state.ambient_temperature_c:.1f}°C"
|
|
63
|
+
if space.state.ambient_temperature_c is not None
|
|
64
64
|
else "unknown"
|
|
65
65
|
)
|
|
66
|
-
LOG.info(
|
|
66
|
+
LOG.info(
|
|
67
|
+
"[space] %s — mode=%s temp=%s",
|
|
68
|
+
space.name,
|
|
69
|
+
space.controls.hvac_mode.value,
|
|
70
|
+
temp,
|
|
71
|
+
)
|
|
67
72
|
|
|
68
73
|
if (
|
|
69
|
-
space.state.
|
|
70
|
-
and space.state.
|
|
71
|
-
and space.controls.
|
|
74
|
+
space.state.ambient_temperature_c is not None
|
|
75
|
+
and space.state.ambient_temperature_c > 27.0
|
|
76
|
+
and space.controls.hvac_mode.value in ("auto", "cool")
|
|
72
77
|
):
|
|
73
78
|
LOG.warning("[space] %s is above 27°C — check cooling", space.name)
|
|
74
79
|
|
|
@@ -76,8 +81,8 @@ async def on_space_update(space: Space) -> None:
|
|
|
76
81
|
async def on_idu_update(idu: IndoorUnit) -> None:
|
|
77
82
|
global _snapshot
|
|
78
83
|
if _snapshot is not None:
|
|
79
|
-
|
|
80
|
-
LOG.debug("[idu] %s — fan=%s online=%s", idu.id, idu.controls.fan_speed.value, idu.
|
|
84
|
+
idu = _snapshot.apply_indoor_unit(idu)
|
|
85
|
+
LOG.debug("[idu] %s — fan=%s online=%s", idu.id, idu.controls.fan_speed.value, idu.is_online)
|
|
81
86
|
|
|
82
87
|
|
|
83
88
|
async def run() -> None:
|
|
@@ -91,7 +96,7 @@ async def run() -> None:
|
|
|
91
96
|
_snapshot = await client.get_snapshot()
|
|
92
97
|
LOG.info(
|
|
93
98
|
"Snapshot loaded: system=%s rooms=%d idus=%d",
|
|
94
|
-
|
|
99
|
+
client.system_name,
|
|
95
100
|
len(_snapshot.rooms),
|
|
96
101
|
len(_snapshot.indoor_units),
|
|
97
102
|
)
|
|
@@ -100,8 +105,7 @@ async def run() -> None:
|
|
|
100
105
|
stream = client.stream(topics, max_reconnects=-1, reconnect_delay_s=2.0)
|
|
101
106
|
stream.on_space_update(on_space_update)
|
|
102
107
|
stream.on_indoor_unit_update(on_idu_update)
|
|
103
|
-
stream.
|
|
104
|
-
stream.on_disconnected(lambda: LOG.warning("Stream disconnected; will reconnect automatically"))
|
|
108
|
+
stream.on_error(lambda exc: LOG.error("Stream stopped: %s", exc))
|
|
105
109
|
|
|
106
110
|
async with stream:
|
|
107
111
|
LOG.info("Daemon running. Send SIGINT or SIGTERM to stop.")
|
|
@@ -11,14 +11,14 @@ To retrieve all comfort presets:
|
|
|
11
11
|
```python
|
|
12
12
|
settings = await client.list_comfort_settings()
|
|
13
13
|
for s in settings:
|
|
14
|
-
print(f"{s.name}: mode={s.hvac_mode}, heat={s.
|
|
14
|
+
print(f"{s.name}: mode={s.hvac_mode}, heat={s.heating_setpoint_c}°C, cool={s.cooling_setpoint_c}°C, fan={s.fan_speed}")
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Alternatively, comfort settings are embedded in `SystemSnapshot`:
|
|
18
18
|
|
|
19
19
|
```python
|
|
20
20
|
snapshot = await client.get_snapshot()
|
|
21
|
-
for cs in snapshot.comfort_settings
|
|
21
|
+
for cs in snapshot.comfort_settings:
|
|
22
22
|
print(f"{cs.name}: {cs.hvac_mode}")
|
|
23
23
|
```
|
|
24
24
|
|
|
@@ -40,7 +40,7 @@ updated = await client.update_comfort_setting(
|
|
|
40
40
|
cool_setpoint_c=25.0,
|
|
41
41
|
fan_speed=FanSpeed.AUTO,
|
|
42
42
|
)
|
|
43
|
-
print(f"Updated '{updated.name}': heat={updated.
|
|
43
|
+
print(f"Updated '{updated.name}': heat={updated.heating_setpoint_c}°C cool={updated.cooling_setpoint_c}°C")
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Omit any parameter to keep its current value. You can also update by comfort setting ID string:
|
|
@@ -73,11 +73,11 @@ async def main() -> None:
|
|
|
73
73
|
snapshot = await client.get_snapshot()
|
|
74
74
|
|
|
75
75
|
preset = next(
|
|
76
|
-
(cs for cs in snapshot.comfort_settings
|
|
76
|
+
(cs for cs in snapshot.comfort_settings if cs.name == PRESET_NAME),
|
|
77
77
|
None,
|
|
78
78
|
)
|
|
79
79
|
if preset is None:
|
|
80
|
-
names = [cs.name for cs in snapshot.comfort_settings
|
|
80
|
+
names = [cs.name for cs in snapshot.comfort_settings]
|
|
81
81
|
print(f"Preset '{PRESET_NAME}' not found. Available: {names}")
|
|
82
82
|
return
|
|
83
83
|
|
|
@@ -85,8 +85,8 @@ async def main() -> None:
|
|
|
85
85
|
updated = await client.set_space(
|
|
86
86
|
space,
|
|
87
87
|
mode=preset.hvac_mode,
|
|
88
|
-
heat_setpoint_c=preset.
|
|
89
|
-
cool_setpoint_c=preset.
|
|
88
|
+
heat_setpoint_c=preset.heating_setpoint_c,
|
|
89
|
+
cool_setpoint_c=preset.cooling_setpoint_c,
|
|
90
90
|
)
|
|
91
91
|
print(f" {updated.name}: mode={updated.controls.hvac_mode}")
|
|
92
92
|
|
|
@@ -13,19 +13,31 @@ To create a day program with timed comfort-setting transitions:
|
|
|
13
13
|
```python
|
|
14
14
|
from quilt_hp.models.schedule import ScheduleEvent
|
|
15
15
|
|
|
16
|
-
# Get
|
|
16
|
+
# Get comfort settings for one room from the snapshot
|
|
17
17
|
snapshot = await client.get_snapshot()
|
|
18
18
|
space = snapshot.space_by_name("Bedroom")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
)
|
|
22
|
-
sleep_cs = next(
|
|
23
|
-
cs for cs in snapshot.comfort_settings.values() if cs.name == "Sleep"
|
|
24
|
-
)
|
|
19
|
+
assert space is not None
|
|
20
|
+
space_settings = snapshot.comfort_settings_for_space(space)
|
|
21
|
+
active_cs = next(cs for cs in space_settings if cs.name == "Active")
|
|
22
|
+
sleep_cs = next(cs for cs in space_settings if cs.name == "Sleep")
|
|
25
23
|
|
|
26
24
|
events = [
|
|
27
|
-
ScheduleEvent(
|
|
28
|
-
|
|
25
|
+
ScheduleEvent(
|
|
26
|
+
start_s=7 * 3600,
|
|
27
|
+
comfort_setting_id=active_cs.id,
|
|
28
|
+
hvac_mode=active_cs.hvac_mode,
|
|
29
|
+
heating_setpoint_c=active_cs.heating_setpoint_c,
|
|
30
|
+
cooling_setpoint_c=active_cs.cooling_setpoint_c,
|
|
31
|
+
precondition=False,
|
|
32
|
+
),
|
|
33
|
+
ScheduleEvent(
|
|
34
|
+
start_s=22 * 3600,
|
|
35
|
+
comfort_setting_id=sleep_cs.id,
|
|
36
|
+
hvac_mode=sleep_cs.hvac_mode,
|
|
37
|
+
heating_setpoint_c=sleep_cs.heating_setpoint_c,
|
|
38
|
+
cooling_setpoint_c=sleep_cs.cooling_setpoint_c,
|
|
39
|
+
precondition=False,
|
|
40
|
+
),
|
|
29
41
|
]
|
|
30
42
|
|
|
31
43
|
day = await client.create_schedule_day(
|
|
@@ -36,7 +48,7 @@ day = await client.create_schedule_day(
|
|
|
36
48
|
print(f"Created schedule day: {day.id} ({len(day.events)} events)")
|
|
37
49
|
```
|
|
38
50
|
|
|
39
|
-
`
|
|
51
|
+
`start_s` is the number of seconds from midnight (e.g., `7 * 3600` = 07:00).
|
|
40
52
|
|
|
41
53
|
---
|
|
42
54
|
|
|
@@ -47,17 +59,17 @@ To create a schedule week and assign day programs to each weekday:
|
|
|
47
59
|
```python
|
|
48
60
|
from quilt_hp.models.schedule import ScheduleWeekDay
|
|
49
61
|
|
|
50
|
-
#
|
|
62
|
+
# weekday: 1 = Monday, 7 = Sunday
|
|
51
63
|
week = await client.create_schedule_week(
|
|
52
64
|
space_id=space.id,
|
|
53
65
|
days=[
|
|
54
|
-
ScheduleWeekDay(
|
|
55
|
-
ScheduleWeekDay(
|
|
56
|
-
ScheduleWeekDay(
|
|
57
|
-
ScheduleWeekDay(
|
|
58
|
-
ScheduleWeekDay(
|
|
59
|
-
ScheduleWeekDay(
|
|
60
|
-
ScheduleWeekDay(
|
|
66
|
+
ScheduleWeekDay(weekday=1, day_id=weekday_program.id), # Mon
|
|
67
|
+
ScheduleWeekDay(weekday=2, day_id=weekday_program.id), # Tue
|
|
68
|
+
ScheduleWeekDay(weekday=3, day_id=weekday_program.id), # Wed
|
|
69
|
+
ScheduleWeekDay(weekday=4, day_id=weekday_program.id), # Thu
|
|
70
|
+
ScheduleWeekDay(weekday=5, day_id=weekday_program.id), # Fri
|
|
71
|
+
ScheduleWeekDay(weekday=6, day_id=weekend_program.id), # Sat
|
|
72
|
+
ScheduleWeekDay(weekday=7, day_id=weekend_program.id), # Sun
|
|
61
73
|
],
|
|
62
74
|
)
|
|
63
75
|
print(f"Created schedule week: {week.id}")
|
|
@@ -74,7 +86,7 @@ updated_week = await client.update_schedule_week(
|
|
|
74
86
|
schedule_week_id=week.id,
|
|
75
87
|
space_id=space.id,
|
|
76
88
|
days=[
|
|
77
|
-
ScheduleWeekDay(
|
|
89
|
+
ScheduleWeekDay(weekday=1, day_id=new_monday_program.id),
|
|
78
90
|
# ... include all 7 days; omitted days are cleared
|
|
79
91
|
],
|
|
80
92
|
)
|
|
@@ -114,4 +126,4 @@ To resume:
|
|
|
114
126
|
await client.set_schedule_execution(paused=False)
|
|
115
127
|
```
|
|
116
128
|
|
|
117
|
-
This is a global switch. It affects all schedule weeks across all spaces in the system. The current pause state is available as `snapshot.schedule_paused
|
|
129
|
+
This is a global switch. It affects all schedule weeks across all spaces in the system. The current pause state is available as `snapshot.primary_location.schedule_paused` when a location is present.
|
|
@@ -88,7 +88,7 @@ To set the fan speed on an indoor unit:
|
|
|
88
88
|
from quilt_hp.models.enums import FanSpeed
|
|
89
89
|
|
|
90
90
|
snapshot = await client.get_snapshot()
|
|
91
|
-
idu = snapshot.indoor_units[
|
|
91
|
+
idu = snapshot.indoor_units[0] # first IDU
|
|
92
92
|
|
|
93
93
|
updated = await client.set_indoor_unit(idu, fan_speed=FanSpeed.MEDIUM)
|
|
94
94
|
print(f"Fan speed: {updated.controls.fan_speed}")
|
|
@@ -23,7 +23,13 @@ def on_idu(idu: IndoorUnit) -> None:
|
|
|
23
23
|
|
|
24
24
|
async with client.stream(snapshot.stream_topics()) as stream:
|
|
25
25
|
stream.on_space_update(on_space)
|
|
26
|
-
stream.on_indoor_unit_update(
|
|
26
|
+
stream.on_indoor_unit_update(lambda idu: print(snapshot.apply_indoor_unit(idu).id))
|
|
27
|
+
stream.on_outdoor_unit_update(snapshot.apply_outdoor_unit)
|
|
28
|
+
stream.on_controller_update(snapshot.apply_controller)
|
|
29
|
+
stream.on_qsm_update(snapshot.apply_qsm)
|
|
30
|
+
stream.on_remote_sensor_update(snapshot.apply_remote_sensor)
|
|
31
|
+
stream.on_controller_remote_sensor_update(snapshot.apply_controller_remote_sensor)
|
|
32
|
+
stream.on_software_update_info(lambda info: print(f"Update info: {info.id}"))
|
|
27
33
|
stream.on_error(lambda e: print(f"Fatal error: {e}"))
|
|
28
34
|
await asyncio.sleep(3600) # run for 1 hour
|
|
29
35
|
```
|
|
@@ -54,24 +60,59 @@ For indoor units:
|
|
|
54
60
|
```python
|
|
55
61
|
def on_idu(idu: IndoorUnit) -> None:
|
|
56
62
|
merged = snapshot.apply_indoor_unit(idu)
|
|
57
|
-
print(f"{merged.id}: online={merged.
|
|
63
|
+
print(f"{merged.id}: online={merged.is_online}")
|
|
58
64
|
```
|
|
59
65
|
|
|
60
66
|
For background on why sparse diffs require merging, see [Snapshot and stream data model](../explanation/snapshot-and-stream.md).
|
|
61
67
|
|
|
62
68
|
---
|
|
63
69
|
|
|
64
|
-
##
|
|
70
|
+
## Callback registration methods
|
|
71
|
+
|
|
72
|
+
`NotifierStream` accepts both synchronous and async callbacks. Register whichever entity types you care about:
|
|
73
|
+
|
|
74
|
+
| Method | Callback argument | Typical use |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `on_space_update()` | `Space` | Merge room diffs with `snapshot.apply_space()` |
|
|
77
|
+
| `on_indoor_unit_update()` | `IndoorUnit` | Merge IDU diffs with `snapshot.apply_indoor_unit()` |
|
|
78
|
+
| `on_outdoor_unit_update()` | `OutdoorUnit` | Merge ODU diffs with `snapshot.apply_outdoor_unit()` |
|
|
79
|
+
| `on_controller_update()` | `Controller` | Merge Dial diffs with `snapshot.apply_controller()` |
|
|
80
|
+
| `on_qsm_update()` | `QuiltSmartModule` | Merge QSM diffs with `snapshot.apply_qsm()` |
|
|
81
|
+
| `on_remote_sensor_update()` | `RemoteSensor` | Merge standalone sensor diffs with `snapshot.apply_remote_sensor()` |
|
|
82
|
+
| `on_controller_remote_sensor_update()` | `ControllerRemoteSensor` | Merge Dial sensor diffs with `snapshot.apply_controller_remote_sensor()` |
|
|
83
|
+
| `on_software_update_info()` | `SoftwareUpdateInfo` | Observe firmware/software update records |
|
|
84
|
+
| `on_error()` | `Exception` | Handle fatal stream failure after reconnects are exhausted |
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Lifecycle methods
|
|
89
|
+
|
|
90
|
+
Use these methods to control the stream explicitly:
|
|
91
|
+
|
|
92
|
+
| Method / property | What it does |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| `await stream.start()` | Starts the listener in the background |
|
|
95
|
+
| `await stream.run_forever()` | Runs inline until cancelled or a fatal error stops it |
|
|
96
|
+
| `await stream.stop()` | Cancels the background task and closes the stream |
|
|
97
|
+
| `await stream.subscribe(topics)` | Adds topic subscriptions after startup |
|
|
98
|
+
| `await stream.unsubscribe(topics)` | Removes topic subscriptions |
|
|
99
|
+
| `stream.error` | Last fatal exception, or `None` while healthy |
|
|
100
|
+
|
|
101
|
+
### Run the stream as a background task
|
|
65
102
|
|
|
66
103
|
To run the stream while doing other work concurrently:
|
|
67
104
|
|
|
68
105
|
```python
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
106
|
+
stream = client.stream(snapshot.stream_topics())
|
|
107
|
+
stream.on_space_update(on_space)
|
|
108
|
+
await stream.start()
|
|
109
|
+
try:
|
|
72
110
|
result = await do_something_else()
|
|
73
111
|
await asyncio.sleep(3600)
|
|
74
|
-
|
|
112
|
+
finally:
|
|
113
|
+
await stream.stop()
|
|
114
|
+
if stream.error is not None:
|
|
115
|
+
print(f"Stream stopped with error: {stream.error}")
|
|
75
116
|
```
|
|
76
117
|
|
|
77
118
|
Use this pattern in integrations (Home Assistant, automation daemons) where the stream is just one part of a larger async application.
|
|
@@ -115,7 +156,7 @@ async with client.stream(snapshot.stream_topics()) as stream:
|
|
|
115
156
|
|
|
116
157
|
## Handle stream errors and reconnect
|
|
117
158
|
|
|
118
|
-
The stream reconnects automatically with exponential back-off (1 s, 2 s, 4 s, … up to a 60 s cap). Use
|
|
159
|
+
The stream reconnects automatically with exponential back-off (1 s, 2 s, 4 s, … up to a 60 s cap). Use `on_error()` or the `error` property to observe only fatal failures after the reconnect budget is exhausted. Configure the reconnect budget like this:
|
|
119
160
|
|
|
120
161
|
```python
|
|
121
162
|
# Unlimited reconnects (default: -1)
|
|
@@ -132,14 +173,16 @@ stream = client.stream(
|
|
|
132
173
|
)
|
|
133
174
|
```
|
|
134
175
|
|
|
135
|
-
To observe
|
|
176
|
+
To observe fatal stream failures:
|
|
136
177
|
|
|
137
178
|
```python
|
|
138
|
-
stream.on_connected(lambda: print("Stream connected"))
|
|
139
|
-
stream.on_disconnected(lambda: print("Stream disconnected; will reconnect"))
|
|
140
179
|
stream.on_error(lambda e: print(f"Fatal error (budget exhausted): {e}"))
|
|
180
|
+
|
|
181
|
+
await stream.run_forever()
|
|
182
|
+
if stream.error is not None:
|
|
183
|
+
print(f"Last fatal error: {stream.error}")
|
|
141
184
|
```
|
|
142
185
|
|
|
143
|
-
`on_error` is called only when the reconnect budget is exhausted. Until then, disconnects and errors trigger automatic reconnection without
|
|
186
|
+
`on_error()` is called only when the reconnect budget is exhausted. Until then, disconnects and transient errors trigger automatic reconnection without surfacing a fatal error to your callback.
|
|
144
187
|
|
|
145
188
|
For the full reconnect state machine, see [The streaming protocol](../explanation/streaming-protocol.md).
|
|
@@ -64,12 +64,10 @@ class IDUUpdate(Message):
|
|
|
64
64
|
self.idu = idu
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
class
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class StreamDisconnected(Message):
|
|
72
|
-
pass
|
|
67
|
+
class StreamError(Message):
|
|
68
|
+
def __init__(self, error: Exception) -> None:
|
|
69
|
+
super().__init__()
|
|
70
|
+
self.error = error
|
|
73
71
|
|
|
74
72
|
|
|
75
73
|
class QuiltApp(App):
|
|
@@ -99,6 +97,7 @@ class QuiltApp(App):
|
|
|
99
97
|
|
|
100
98
|
self._snapshot = await self._client.get_snapshot()
|
|
101
99
|
self._refresh_table()
|
|
100
|
+
self.query_one("#status-bar", Label).update("● Streaming")
|
|
102
101
|
|
|
103
102
|
self.run_worker(self._run_stream(), exclusive=True)
|
|
104
103
|
|
|
@@ -110,30 +109,26 @@ class QuiltApp(App):
|
|
|
110
109
|
stream = self._client.stream(topics, max_reconnects=-1)
|
|
111
110
|
stream.on_space_update(self._on_space)
|
|
112
111
|
stream.on_indoor_unit_update(self._on_idu)
|
|
113
|
-
stream.
|
|
114
|
-
stream.on_disconnected(lambda: self.post_message(StreamDisconnected()))
|
|
112
|
+
stream.on_error(lambda exc: self.post_message(StreamError(exc)))
|
|
115
113
|
|
|
116
114
|
async with stream:
|
|
117
115
|
await asyncio.Event().wait()
|
|
118
116
|
|
|
119
117
|
def _on_space(self, space: Space) -> None:
|
|
120
118
|
if self._snapshot is not None:
|
|
121
|
-
self._snapshot.
|
|
119
|
+
space = self._snapshot.apply_space(space)
|
|
122
120
|
self.post_message(SpaceUpdate(space))
|
|
123
121
|
|
|
124
122
|
def _on_idu(self, idu: IndoorUnit) -> None:
|
|
125
123
|
if self._snapshot is not None:
|
|
126
|
-
self._snapshot.
|
|
124
|
+
idu = self._snapshot.apply_indoor_unit(idu)
|
|
127
125
|
self.post_message(IDUUpdate(idu))
|
|
128
126
|
|
|
129
127
|
def on_space_update(self, msg: SpaceUpdate) -> None:
|
|
130
128
|
self._update_row(msg.space)
|
|
131
129
|
|
|
132
|
-
def
|
|
133
|
-
self.query_one("#status-bar", Label).update("
|
|
134
|
-
|
|
135
|
-
def on_stream_disconnected(self, msg: StreamDisconnected) -> None:
|
|
136
|
-
self.query_one("#status-bar", Label).update("○ Disconnected — reconnecting…")
|
|
130
|
+
def on_stream_error(self, msg: StreamError) -> None:
|
|
131
|
+
self.query_one("#status-bar", Label).update(f"✕ Stream stopped: {msg.error}")
|
|
137
132
|
|
|
138
133
|
def _refresh_table(self) -> None:
|
|
139
134
|
if self._snapshot is None:
|
|
@@ -144,15 +139,15 @@ class QuiltApp(App):
|
|
|
144
139
|
def _update_row(self, space: Space) -> None:
|
|
145
140
|
table = self.query_one("#spaces-table", DataTable)
|
|
146
141
|
temp = (
|
|
147
|
-
f"{space.state.
|
|
148
|
-
if space.state.
|
|
142
|
+
f"{space.state.ambient_temperature_c:.1f}°C"
|
|
143
|
+
if space.state.ambient_temperature_c is not None
|
|
149
144
|
else "—"
|
|
150
145
|
)
|
|
151
146
|
setpoints = (
|
|
152
|
-
f"{space.controls.
|
|
153
|
-
f"{space.controls.
|
|
147
|
+
f"{space.controls.heating_setpoint_c:.0f} / "
|
|
148
|
+
f"{space.controls.cooling_setpoint_c:.0f}°C"
|
|
154
149
|
)
|
|
155
|
-
row = (space.name, space.controls.
|
|
150
|
+
row = (space.name, space.controls.hvac_mode.value, temp, setpoints)
|
|
156
151
|
|
|
157
152
|
key = f"space-{space.id}"
|
|
158
153
|
if key in table.rows:
|
|
@@ -203,8 +198,10 @@ async def action_set_mode(self) -> None:
|
|
|
203
198
|
row_key = table.cursor_row_key
|
|
204
199
|
if row_key is None:
|
|
205
200
|
return
|
|
201
|
+
if self._snapshot is None:
|
|
202
|
+
return
|
|
206
203
|
space_id = row_key.removeprefix("space-")
|
|
207
|
-
space = self._snapshot.spaces.
|
|
204
|
+
space = next((s for s in self._snapshot.spaces if s.id == space_id), None)
|
|
208
205
|
if space is None:
|
|
209
206
|
return
|
|
210
207
|
|
|
@@ -69,7 +69,7 @@ Raised when a requested resource does not exist (gRPC `NOT_FOUND`).
|
|
|
69
69
|
### `__version__`
|
|
70
70
|
|
|
71
71
|
```python
|
|
72
|
-
__version__: str # e.g. "0.
|
|
72
|
+
__version__: str # e.g. "0.3.0"
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
---
|
|
@@ -112,7 +112,7 @@ async def __aenter__(self) -> QuiltClient: ...
|
|
|
112
112
|
async def __aexit__(self, *_: object) -> None: ...
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
`__aexit__`
|
|
115
|
+
`__aexit__` calls `close()` to close the gRPC channel. Prefer the async context manager, or call `await close()` yourself when managing lifecycle manually.
|
|
116
116
|
|
|
117
117
|
---
|
|
118
118
|
|
|
@@ -143,6 +143,16 @@ async def refresh_token(self, context: TokenRefreshContext | None = None) -> Non
|
|
|
143
143
|
|
|
144
144
|
Silently refreshes the auth token using the refresh token. Does not attempt OTP. Called automatically by the transport interceptor on `UNAUTHENTICATED`; rarely needed directly.
|
|
145
145
|
|
|
146
|
+
### `get_current_token`
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
def get_current_token(self) -> str
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Returns the current JWT access token held by the client.
|
|
153
|
+
|
|
154
|
+
**Raises:** `QuiltAuthError` if the client is not authenticated yet.
|
|
155
|
+
|
|
146
156
|
---
|
|
147
157
|
|
|
148
158
|
## System discovery
|
|
@@ -155,7 +165,7 @@ async def list_systems(self) -> list[SystemInfo]
|
|
|
155
165
|
|
|
156
166
|
Lists all Quilt systems the authenticated user has access to.
|
|
157
167
|
|
|
158
|
-
**Returns:** List of `SystemInfo` objects with `id`, `name`, `timezone
|
|
168
|
+
**Returns:** List of `SystemInfo` objects with `id`, `name`, and `timezone`.
|
|
159
169
|
|
|
160
170
|
**Raises:** `QuiltError` if the gRPC call fails.
|
|
161
171
|
|
|
@@ -205,6 +215,14 @@ def invalidate_snapshot(self) -> None
|
|
|
205
215
|
|
|
206
216
|
Discards the cached snapshot. The next `get_snapshot()` call fetches fresh data from the server.
|
|
207
217
|
|
|
218
|
+
### `close`
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
async def close(self) -> None
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Closes the underlying gRPC channel and clears the client's open channel reference. Safe to call multiple times.
|
|
225
|
+
|
|
208
226
|
---
|
|
209
227
|
|
|
210
228
|
## Space control
|