span-panel-api 3.6.2__tar.gz → 3.8.0b1__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.
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/CHANGELOG.md +32 -2
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/PKG-INFO +8 -5
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/README.md +5 -2
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/pyproject.toml +13 -4
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/__init__.py +24 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/auth.py +74 -23
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/exceptions.py +19 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/models.py +215 -14
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/mqtt/client.py +40 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/mqtt/connection.py +1 -2
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/mqtt/control.py +1 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/protocol.py +13 -0
- span_panel_api-3.8.0b1/tests/adapter_fidelity/__init__.py +5 -0
- span_panel_api-3.8.0b1/tests/adapter_fidelity/harness.py +869 -0
- span_panel_api-3.8.0b1/tests/adapter_fidelity/rules.py +543 -0
- span_panel_api-3.8.0b1/tests/adapter_fidelity/rules_captured.py +75 -0
- span_panel_api-3.8.0b1/tests/adapter_fidelity/test_capture_fidelity.py +153 -0
- span_panel_api-3.8.0b1/tests/adapter_fidelity/test_harness.py +194 -0
- span_panel_api-3.8.0b1/tests/adapter_fidelity/test_main32_fidelity.py +181 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/conftest.py +55 -1
- span_panel_api-3.8.0b1/tests/expected_failures.py +48 -0
- span_panel_api-3.8.0b1/tests/fixtures/captures/LICENSE +21 -0
- span_panel_api-3.8.0b1/tests/fixtures/captures/README.md +17 -0
- span_panel_api-3.8.0b1/tests/fixtures/captures/SHA256SUMS +6 -0
- span_panel_api-3.8.0b1/tests/fixtures/captures/r202639-a-tree-v1.json +1048 -0
- span_panel_api-3.8.0b1/tests/fixtures/captures/r202639-b-tree-v1.json +4039 -0
- span_panel_api-3.8.0b1/tests/fixtures/captures/r202639-c-tree-v1.json +1863 -0
- span_panel_api-3.8.0b1/tests/fixtures/captures/r202639-d-tree-v1.json +2372 -0
- span_panel_api-3.8.0b1/tests/fixtures/captures/r202639-e-tree-v1.json +10577 -0
- span_panel_api-3.8.0b1/tests/fixtures/distribution-enclosure-simulator.LICENSE +21 -0
- span_panel_api-3.8.0b1/tests/fixtures/main32-tree-v1.README.md +9 -0
- span_panel_api-3.8.0b1/tests/fixtures/main32-tree-v1.json +3748 -0
- span_panel_api-3.8.0b1/tests/fixtures/main32_r202639-tree-v1.README.md +8 -0
- span_panel_api-3.8.0b1/tests/fixtures/main32_r202639-tree-v1.json +3292 -0
- span_panel_api-3.8.0b1/tests/fixtures/other_model_tree.json +276 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/reference_payloads/README.md +6 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/reference_payloads/__init__.py +10 -0
- span_panel_api-3.8.0b1/tests/reference_payloads/captures.py +249 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/reference_payloads/schema_one.py +61 -2
- span_panel_api-3.8.0b1/tests/reference_payloads/synthetic_trees.py +457 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_absent_readings_are_not_zero.py +0 -15
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_accumulator.py +3 -2
- span_panel_api-3.8.0b1/tests/test_capture_replay.py +166 -0
- span_panel_api-3.8.0b1/tests/test_captures_unchanged.py +53 -0
- span_panel_api-3.8.0b1/tests/test_circuit_fed_battery_captures.py +21 -0
- span_panel_api-3.8.0b1/tests/test_declared_unvalued_captures.py +126 -0
- span_panel_api-3.8.0b1/tests/test_evse_lock_captures.py +59 -0
- span_panel_api-3.8.0b1/tests/test_evse_lock_control.py +112 -0
- span_panel_api-3.8.0b1/tests/test_expected_failures_registry.py +89 -0
- span_panel_api-3.8.0b1/tests/test_feeds_role_captures.py +30 -0
- span_panel_api-3.8.0b1/tests/test_homie_schema_hash.py +87 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_mqtt_client_connection.py +1 -2
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_mqtt_homie.py +65 -196
- span_panel_api-3.8.0b1/tests/test_panel_properties_captures.py +87 -0
- span_panel_api-3.8.0b1/tests/test_positions_captures.py +107 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_protocol_conformance.py +5 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_public_api_unchanged.py +21 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_register_passphrase_unavailable.py +5 -2
- span_panel_api-3.8.0b1/tests/test_register_rate_limit.py +87 -0
- span_panel_api-3.8.0b1/tests/test_register_rate_limit_companions.py +92 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_migration_delta.py +34 -7
- span_panel_api-3.8.0b1/tests/test_schema_one_acceptance_companions.py +57 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_adapter.py +5 -5
- span_panel_api-3.8.0b1/tests/test_schema_one_circuit_fed_battery.py +61 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_circuits.py +7 -7
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_conformance.py +37 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_connection_health.py +1 -1
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_control_refusal.py +2 -2
- span_panel_api-3.8.0b1/tests/test_schema_one_declared_unvalued.py +125 -0
- span_panel_api-3.8.0b1/tests/test_schema_one_device_display_name.py +91 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_discovery.py +64 -1
- span_panel_api-3.8.0b1/tests/test_schema_one_evse_lock.py +57 -0
- span_panel_api-3.8.0b1/tests/test_schema_one_feeds_role.py +65 -0
- span_panel_api-3.8.0b1/tests/test_schema_one_other_models.py +589 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_panel.py +2 -40
- span_panel_api-3.8.0b1/tests/test_schema_one_panel_properties.py +168 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_pcs.py +0 -10
- span_panel_api-3.8.0b1/tests/test_schema_one_positions.py +125 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_service_entrance.py +28 -4
- span_panel_api-3.8.0b1/tests/test_schema_one_shared_meters.py +115 -0
- span_panel_api-3.8.0b1/tests/test_schema_one_sheddable.py +46 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_snapshot.py +21 -22
- span_panel_api-3.8.0b1/tests/test_schema_one_space_less_meters.py +175 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_undeclared_devices.py +1 -1
- span_panel_api-3.8.0b1/tests/test_shared_meters_captures.py +60 -0
- span_panel_api-3.8.0b1/tests/test_space_less_meters_captures.py +64 -0
- span_panel_api-3.8.0b1/tests/test_synthetic_trees.py +295 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_v2_status_parser.py +36 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/.gitignore +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/LICENSE +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/_http.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/_ssl.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/adapters.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/const.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/detection.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/dispatch.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/factory.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/mqtt/__init__.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/mqtt/async_client.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/mqtt/const.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/mqtt/models.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/phase_validation.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/py.typed +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/src/span_panel_api/schema_drift.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/fixtures/flat_wire.json +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/fixtures/panelbench_unvalued_by_both.json +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/fixtures/panelbench_wire.json +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/fixtures/v2/README.md +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/fixtures/v2/status.json +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/reference_payloads/bootstrap.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/simulation_fixtures/circuits.response.txt +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/simulation_fixtures/panel.response.txt +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/simulation_fixtures/soe.response.txt +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/simulation_fixtures/status.response.txt +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_adapters_discovery.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_adopted_control.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_adoption.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_async_mqtt_client.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_auth_and_homie_helpers.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_auth_redaction.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_ca_pinning.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_catalog_divergence.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_control_interceptor.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_detection_auth.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_exceptions.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_factory_dispatch.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_field_metadata.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_https_transport.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_leaf_name_mismatch.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_live_flat_differential.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_mqtt_bridge.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_mqtt_connect_flow.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_mqtt_debounce.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_packaging.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_phase_validation_configs.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_phase_validation_errors.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_plaintext_warning.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_protocol_models.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_publish_outcome.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_redispatch_on_reconnect.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_reference_tree_values.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_rest_transport_contract.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_fetch_transport_split.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_generation_cross_check.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_charge_limit.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_devices.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_extension.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_firmware.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_shed_forecast.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_one_transport.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_provenance.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_schema_zero_adapter.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_shared_http_client.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_snapshot_readiness_gate.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/test_ssl_context.py +0 -0
- {span_panel_api-3.6.2 → span_panel_api-3.8.0b1}/tests/tls_fixtures.py +0 -0
|
@@ -7,6 +7,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
7
7
|
Pre-releases are not listed separately. A beta is a step towards the next public version, so its changes are folded into that version's entry as they land and are described against the **last public release**, never against the beta before it. What one
|
|
8
8
|
beta corrected in an earlier beta does not appear at all: from the point of view of somebody upgrading between released versions, it never happened.
|
|
9
9
|
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
Snapshot fields that held a fabricated constant can now be unknown, and with `span-panel-api-schema-0` and `span-panel-api-schema-1` 1.3.0 or newer the parsers report them as unknown and stop synthesising unoccupied positions.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **spanos3/r202639/03 batch 2: reads more of the data this firmware publishes.**
|
|
17
|
+
- **`V2StatusInfo.hardware_version` reports the hardware version string from `GET /api/v2/status`.**
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **BREAKING FOR CONSUMERS: `SpanPanelSnapshot.proximity_proven` becomes `bool | None`**, and with `span-panel-api-schema-0` and `span-panel-api-schema-1` 1.3.0 or newer it is always `None`, where it was effectively a constant `True`, because neither MQTT
|
|
22
|
+
schema publishes a proximity property and the panel's own answer is `V2StatusInfo.proximity_proven`.
|
|
23
|
+
- **BREAKING FOR CONSUMERS: `SpanPanelSnapshot.uptime_s` becomes `int | None`**, and with those parsers it is `None` on a parent/child panel and before a flat panel first reports ready, where it was a constant `0`.
|
|
24
|
+
- **BREAKING FOR CONSUMERS: `SpanCircuitSnapshot.instant_power_update_time_s` and `energy_accum_update_time_s` become `int | None` and default to `None`**, and with those parsers they are `None` until the reading has arrived and always on a parent/child
|
|
25
|
+
panel, where they were `0`.
|
|
26
|
+
- **BREAKING FOR CONSUMERS: `SpanCircuitSnapshot.is_240v` becomes `bool | None`**, and with `span-panel-api-schema-1` 1.4.0 or newer it is `None` where the pole count is unpublished.
|
|
27
|
+
- **BREAKING FOR CONSUMERS: `SpanPanelSnapshot.lugs_at_service_entrance` becomes `bool | None`**, and with `span-panel-api-schema-1` 1.4.0 or newer it is `None` while the upstream lugs declare their feed but leave it unpublished.
|
|
28
|
+
- **The `schema-0` extra requires `span-panel-api-schema-0` 1.3.0 or newer and the `schema-1` extra requires `span-panel-api-schema-1` 1.4.0 or newer**, the adapters that report these values as unknown; schema-1 1.4.0 also fills the snapshot fields this
|
|
29
|
+
release adds.
|
|
30
|
+
|
|
31
|
+
### Removed
|
|
32
|
+
|
|
33
|
+
- **BREAKING: with `span-panel-api-schema-0` and `span-panel-api-schema-1` 1.3.0 or newer, snapshots no longer include synthesised `unmapped_tab_N` entries for unoccupied breaker positions, which carried no measured data**, so `SpanPanelSnapshot.circuits`
|
|
34
|
+
holds only real circuits and `panel_size` alone states the panel's total positions.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **`V2HomieSchema.types_schema_hash` on data model 1.0 is the panel's published `deviceClassesSchemaHash`, or a hash of `deviceClasses` when none is published, where it was one constant for every such panel**; a flat panel's value is unchanged, and a
|
|
39
|
+
value stored from 3.6.x differs once on a data model 1.0 panel.
|
|
40
|
+
|
|
10
41
|
## [3.6.2]
|
|
11
42
|
|
|
12
43
|
A reconnect waits for the panel's labels before handing out a snapshot, as connecting always has.
|
|
@@ -480,8 +511,7 @@ Everything below is additive. Each field is `None` or empty on a panel that publ
|
|
|
480
511
|
`MQTT_FULL_REBUILD_AFTER_FAILURES` (3) consecutive failures, or immediately on any `ssl.SSLError`. The previous behavior pinned the panel's CA certificate into the paho client once at `connect()` time and re-used it across all reconnect attempts; if the
|
|
481
512
|
panel rotated its private CA — most plausibly during a firmware upgrade — every subsequent reconnect raised `ssl.SSLCertVerificationError` (caught by the broad `OSError` clause and silently retried) and the bridge could not recover without a config-entry
|
|
482
513
|
reload. The rebuild mirrors what a manual reload does without going through HA's `config_entry` teardown, so entities stay registered and the integration's grace-period logic continues to apply unchanged. The threshold-cadence design (counter reset on
|
|
483
|
-
every rebuild attempt, success or fail) keeps the recovery path active throughout extended outages — multi-day disconnections recover whenever the panel becomes usable again, including if the CA rotates a second time mid-outage.
|
|
484
|
-
`SpanPanel_Docs/span-panel-api/2026-05-17-mqtt-ca-refresh-on-reconnect-design.md` for the full design.
|
|
514
|
+
every rebuild attempt, success or fail) keeps the recovery path active throughout extended outages — multi-day disconnections recover whenever the panel becomes usable again, including if the CA rotates a second time mid-outage.
|
|
485
515
|
|
|
486
516
|
### Added
|
|
487
517
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: span-panel-api
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.8.0b1
|
|
4
4
|
Summary: A client library for SPAN Panel API
|
|
5
5
|
Project-URL: Homepage, https://github.com/SpanPanel/span-panel-api
|
|
6
6
|
Project-URL: Issues, https://github.com/SpanPanel/span-panel-api/issues
|
|
@@ -20,9 +20,9 @@ Requires-Dist: httpx<1.0,>=0.28.1
|
|
|
20
20
|
Requires-Dist: paho-mqtt<3.0.0,>=2.0.0
|
|
21
21
|
Requires-Dist: pyyaml>=6.0.0
|
|
22
22
|
Provides-Extra: schema-0
|
|
23
|
-
Requires-Dist: span-panel-api-schema-0>=1.
|
|
23
|
+
Requires-Dist: span-panel-api-schema-0>=1.3.0b1; extra == 'schema-0'
|
|
24
24
|
Provides-Extra: schema-1
|
|
25
|
-
Requires-Dist: span-panel-api-schema-1>=1.
|
|
25
|
+
Requires-Dist: span-panel-api-schema-1>=1.4.0b1; extra == 'schema-1'
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
|
|
28
28
|
# SPAN Panel API
|
|
@@ -175,8 +175,8 @@ transport-specific classes.
|
|
|
175
175
|
|
|
176
176
|
All panel state is represented as immutable, frozen dataclasses:
|
|
177
177
|
|
|
178
|
-
- **`SpanPanelSnapshot`**: complete panel state. Identity (serial, firmware, vendor, model, hardware version), panel size, main breaker rating, main relay, door
|
|
179
|
-
feedthrough power and energy, grid/DSM state, run configuration, dominant power source, grid islandability, per-leg voltages, power flows, lugs current and whether the lugs are the service entrance, shed policy and forecast. It holds `circuits`,
|
|
178
|
+
- **`SpanPanelSnapshot`**: complete panel state. Identity (serial, firmware, vendor, model, hardware version), panel size, main breaker rating, main relay, door state, connection uptime on a flat panel, network links with the Wi-Fi SSID and vendor cloud,
|
|
179
|
+
grid and feedthrough power and energy, grid/DSM state, run configuration, dominant power source, grid islandability, per-leg voltages, power flows, lugs current and whether the lugs are the service entrance, shed policy and forecast. It holds `circuits`,
|
|
180
180
|
`battery`, `pv`, `pv_inverters`, `evse`, `mid`, `pcs`, `adopted_devices` and `extension_properties`.
|
|
181
181
|
- **`SpanCircuitSnapshot`**: one circuit. Id, name, relay state and requester, power, energy and their update times, tabs, 240 V, breaker rating, current, priority, user controllability, sheddable / never-backup / always-on flags, device type, relative
|
|
182
182
|
position, PCS management and priority, and the `$target` pending state for relay and priority.
|
|
@@ -195,6 +195,9 @@ see the changelog, because `battery.model` changes value for existing flat users
|
|
|
195
195
|
|
|
196
196
|
`mid`, `pcs`, `adopted_devices`, `extension_properties` and the per-DER link-health fields exist only under the parent/child schema. They are `None` or empty on a flat panel rather than absent, so a consumer reads the same snapshot type either way.
|
|
197
197
|
|
|
198
|
+
`uptime_s` and a circuit's update times are the reverse: only the flat parser measures them, and they are `None` under the parent/child schema. Neither schema publishes proximity, so `SpanPanelSnapshot.proximity_proven` is always `None`; the panel's own
|
|
199
|
+
answer is `V2StatusInfo.proximity_proven`, from `GET /api/v2/status`.
|
|
200
|
+
|
|
198
201
|
## Usage
|
|
199
202
|
|
|
200
203
|
### Factory Pattern (Recommended)
|
|
@@ -148,8 +148,8 @@ transport-specific classes.
|
|
|
148
148
|
|
|
149
149
|
All panel state is represented as immutable, frozen dataclasses:
|
|
150
150
|
|
|
151
|
-
- **`SpanPanelSnapshot`**: complete panel state. Identity (serial, firmware, vendor, model, hardware version), panel size, main breaker rating, main relay, door
|
|
152
|
-
feedthrough power and energy, grid/DSM state, run configuration, dominant power source, grid islandability, per-leg voltages, power flows, lugs current and whether the lugs are the service entrance, shed policy and forecast. It holds `circuits`,
|
|
151
|
+
- **`SpanPanelSnapshot`**: complete panel state. Identity (serial, firmware, vendor, model, hardware version), panel size, main breaker rating, main relay, door state, connection uptime on a flat panel, network links with the Wi-Fi SSID and vendor cloud,
|
|
152
|
+
grid and feedthrough power and energy, grid/DSM state, run configuration, dominant power source, grid islandability, per-leg voltages, power flows, lugs current and whether the lugs are the service entrance, shed policy and forecast. It holds `circuits`,
|
|
153
153
|
`battery`, `pv`, `pv_inverters`, `evse`, `mid`, `pcs`, `adopted_devices` and `extension_properties`.
|
|
154
154
|
- **`SpanCircuitSnapshot`**: one circuit. Id, name, relay state and requester, power, energy and their update times, tabs, 240 V, breaker rating, current, priority, user controllability, sheddable / never-backup / always-on flags, device type, relative
|
|
155
155
|
position, PCS management and priority, and the `$target` pending state for relay and priority.
|
|
@@ -168,6 +168,9 @@ see the changelog, because `battery.model` changes value for existing flat users
|
|
|
168
168
|
|
|
169
169
|
`mid`, `pcs`, `adopted_devices`, `extension_properties` and the per-DER link-health fields exist only under the parent/child schema. They are `None` or empty on a flat panel rather than absent, so a consumer reads the same snapshot type either way.
|
|
170
170
|
|
|
171
|
+
`uptime_s` and a circuit's update times are the reverse: only the flat parser measures them, and they are `None` under the parent/child schema. Neither schema publishes proximity, so `SpanPanelSnapshot.proximity_proven` is always `None`; the panel's own
|
|
172
|
+
answer is `V2StatusInfo.proximity_proven`, from `GET /api/v2/status`.
|
|
173
|
+
|
|
171
174
|
## Usage
|
|
172
175
|
|
|
173
176
|
### Factory Pattern (Recommended)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "span-panel-api"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.8.0b1"
|
|
4
4
|
description = "A client library for SPAN Panel API"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "SpanPanel"}
|
|
@@ -62,11 +62,16 @@ dependencies = [
|
|
|
62
62
|
# so a 1.1.x adapter is still accepted, but it fills neither `pv_inverters` nor
|
|
63
63
|
# the inverter identity fields 3.6.0 adds. Upgrading through the extra is what
|
|
64
64
|
# brings the adapters that do.
|
|
65
|
-
|
|
65
|
+
# Raised to 1.3.0b1 for 3.7.0b1: that adapter reports the unknown values this
|
|
66
|
+
# bootstrap's snapshot fields now accept, and stops synthesising unoccupied positions.
|
|
67
|
+
schema-0 = ["span-panel-api-schema-0>=1.3.0b1"]
|
|
66
68
|
# Raised to 1.2.1 for 3.6.1: that adapter builds the `pv` that `SpanPanelSnapshot.pv` now documents.
|
|
67
69
|
# Raised to 1.2.2 for 3.6.2: the readiness gate every snapshot passes asks that
|
|
68
70
|
# adapter what is still missing, and 1.2.2 restarts its feed grace correctly.
|
|
69
|
-
|
|
71
|
+
# Raised to 1.3.0b1 for 3.7.0b1, for the same reason as schema-0's.
|
|
72
|
+
# Raised to 1.4.0b1 for 3.8.0b1: that adapter fills the snapshot fields this
|
|
73
|
+
# bootstrap adds.
|
|
74
|
+
schema-1 = ["span-panel-api-schema-1>=1.4.0b1"]
|
|
70
75
|
|
|
71
76
|
[project.urls]
|
|
72
77
|
Homepage = "https://github.com/SpanPanel/span-panel-api"
|
|
@@ -245,7 +250,11 @@ warn_return_any = true
|
|
|
245
250
|
warn_unused_configs = true
|
|
246
251
|
disallow_untyped_defs = true
|
|
247
252
|
explicit_package_bases = true
|
|
248
|
-
|
|
253
|
+
# `tests` because the suite imports its support packages top-level
|
|
254
|
+
# (`from reference_payloads.schema_one import ...`), the way pytest's rootdir puts
|
|
255
|
+
# `tests/` on `sys.path`; the support packages the mypy hook checks resolve the same
|
|
256
|
+
# way here.
|
|
257
|
+
mypy_path = "src:tests"
|
|
249
258
|
# Excluded by name for the reason `[tool.ruff]` gives above: the capture script is
|
|
250
259
|
# checked like the library, and the hand-run tools written before that was expected
|
|
251
260
|
# are not.
|
|
@@ -29,6 +29,7 @@ from .exceptions import (
|
|
|
29
29
|
SpanPanelError,
|
|
30
30
|
SpanPanelInsufficientPrivilegeError,
|
|
31
31
|
SpanPanelPassphraseUnavailableError,
|
|
32
|
+
SpanPanelRateLimitError,
|
|
32
33
|
SpanPanelSchemaVersionError,
|
|
33
34
|
SpanPanelServerError,
|
|
34
35
|
SpanPanelStaleDataError,
|
|
@@ -41,15 +42,19 @@ from .models import (
|
|
|
41
42
|
ADOPTION_IDENTITY_NODE,
|
|
42
43
|
ADOPTION_TOPOLOGY_NODE,
|
|
43
44
|
DISCOVERY_NAMESPACE,
|
|
45
|
+
FEEDS_ROLES,
|
|
46
|
+
PROTECTION_FUNCTIONS,
|
|
44
47
|
AdoptedDevice,
|
|
45
48
|
AdoptedProperty,
|
|
46
49
|
ControlTarget,
|
|
47
50
|
DiscoveredMetadata,
|
|
48
51
|
ExtensionProperty,
|
|
49
52
|
ExtensionSubject,
|
|
53
|
+
FeedsRole,
|
|
50
54
|
FieldMetadata,
|
|
51
55
|
HomieSchemaTypes,
|
|
52
56
|
PassphraseRotation,
|
|
57
|
+
ProtectionFunction,
|
|
53
58
|
SpanBatterySnapshot,
|
|
54
59
|
SpanCircuitSnapshot,
|
|
55
60
|
SpanEvseSnapshot,
|
|
@@ -61,6 +66,7 @@ from .models import (
|
|
|
61
66
|
V2HomieSchema,
|
|
62
67
|
V2StatusInfo,
|
|
63
68
|
is_discovery_path,
|
|
69
|
+
shared_meter_groups,
|
|
64
70
|
)
|
|
65
71
|
from .mqtt import (
|
|
66
72
|
ControlCommand,
|
|
@@ -84,6 +90,7 @@ from .protocol import (
|
|
|
84
90
|
CircuitControlProtocol,
|
|
85
91
|
ControlInterceptionProtocol,
|
|
86
92
|
EvseControlProtocol,
|
|
93
|
+
EvseLockControlProtocol,
|
|
87
94
|
PanelCapability,
|
|
88
95
|
PanelControlProtocol,
|
|
89
96
|
SpanPanelClientProtocol,
|
|
@@ -106,6 +113,9 @@ __all__ = [ # noqa: RUF022
|
|
|
106
113
|
# snapshot rather than by its arguments -- a device the adapter models
|
|
107
114
|
# produces no AdoptedDevice and so cannot be addressed through it.
|
|
108
115
|
"AdoptedControlProtocol",
|
|
116
|
+
# A charger's connector lock, authorised by `SpanEvseSnapshot.lock_control`.
|
|
117
|
+
# A protocol of its own so `EvseControlProtocol`'s implementers are unchanged.
|
|
118
|
+
"EvseLockControlProtocol",
|
|
109
119
|
# Added 2026-08-25 (3.1.0): one veto/observe point for every control
|
|
110
120
|
# command. A protocol of its own rather than a member on the four control
|
|
111
121
|
# protocols, which would break their implementers a second time in one
|
|
@@ -145,6 +155,16 @@ __all__ = [ # noqa: RUF022
|
|
|
145
155
|
"SpanPVSnapshot",
|
|
146
156
|
"SpanPanelSnapshot",
|
|
147
157
|
"SpanPcsSnapshot",
|
|
158
|
+
# What a circuit's `connection/feeds-role` declares, read into
|
|
159
|
+
# `SpanCircuitSnapshot.feeds_role`.
|
|
160
|
+
"FEEDS_ROLES",
|
|
161
|
+
"FeedsRole",
|
|
162
|
+
# What a circuit's `breaker/protection-functions` lists, read into
|
|
163
|
+
# `SpanCircuitSnapshot.protection_functions`.
|
|
164
|
+
"PROTECTION_FUNCTIONS",
|
|
165
|
+
"ProtectionFunction",
|
|
166
|
+
# The circuits one meter measures, from `SpanCircuitSnapshot.meter_shared_with`.
|
|
167
|
+
"shared_meter_groups",
|
|
148
168
|
# Factory
|
|
149
169
|
"create_span_client",
|
|
150
170
|
# Detection
|
|
@@ -216,6 +236,10 @@ __all__ = [ # noqa: RUF022
|
|
|
216
236
|
# keep their meaning; deliberately not a SpanPanelAuthError, because the
|
|
217
237
|
# passphrase the user supplied may be correct.
|
|
218
238
|
"SpanPanelPassphraseUnavailableError",
|
|
239
|
+
# Registration refused with HTTP 429, carrying the panel's `Retry-After` in
|
|
240
|
+
# seconds. A subclass of SpanPanelAPIError, so existing except clauses keep
|
|
241
|
+
# their meaning; not a SpanPanelAuthError, because no credential was judged.
|
|
242
|
+
"SpanPanelRateLimitError",
|
|
219
243
|
"SpanPanelServerError",
|
|
220
244
|
"SpanPanelStaleDataError",
|
|
221
245
|
# Added 2026-08-31 (3.4.0): a bootstrap REST call that failed verification
|
|
@@ -9,10 +9,13 @@ client (which only covers v1 endpoints).
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import asyncio
|
|
12
|
-
from collections.abc import Collection
|
|
12
|
+
from collections.abc import Collection, Mapping
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from email.utils import parsedate_to_datetime
|
|
13
15
|
import hashlib
|
|
14
16
|
import json
|
|
15
17
|
import logging
|
|
18
|
+
import math
|
|
16
19
|
import ssl
|
|
17
20
|
import uuid
|
|
18
21
|
|
|
@@ -24,6 +27,7 @@ from .exceptions import (
|
|
|
24
27
|
SpanPanelAuthError,
|
|
25
28
|
SpanPanelInsufficientPrivilegeError,
|
|
26
29
|
SpanPanelPassphraseUnavailableError,
|
|
30
|
+
SpanPanelRateLimitError,
|
|
27
31
|
SpanPanelServerError,
|
|
28
32
|
)
|
|
29
33
|
from .models import HomieSchemaTypes, PassphraseRotation, V2AuthResponse, V2HomieSchema, V2StatusInfo
|
|
@@ -216,21 +220,37 @@ CA_CERT_MAX_ATTEMPTS = 5
|
|
|
216
220
|
CA_CERT_BACKOFF_S = 1.5
|
|
217
221
|
|
|
218
222
|
|
|
219
|
-
def
|
|
220
|
-
"""
|
|
223
|
+
def _retry_after_s(retry_after: str | None, now: datetime) -> float | None:
|
|
224
|
+
"""The wait a ``Retry-After`` header asks for, in seconds, or None if it asks for none.
|
|
221
225
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
226
|
+
Both forms RFC 9110 allows: delta-seconds, taken as given, and an HTTP-date,
|
|
227
|
+
taken as the seconds from `now` until then (0.0 once it has passed). A
|
|
228
|
+
negative, infinite or unparseable value is no answer, so a bad header can
|
|
229
|
+
never stall or skip a retry.
|
|
225
230
|
"""
|
|
226
|
-
fallback = backoff_s * (2.0 ** (attempt - 1))
|
|
227
231
|
if retry_after is None:
|
|
228
|
-
return
|
|
232
|
+
return None
|
|
229
233
|
try:
|
|
230
|
-
|
|
231
|
-
except
|
|
232
|
-
|
|
233
|
-
|
|
234
|
+
seconds = float(retry_after)
|
|
235
|
+
except ValueError:
|
|
236
|
+
try:
|
|
237
|
+
until = parsedate_to_datetime(retry_after)
|
|
238
|
+
except (TypeError, ValueError):
|
|
239
|
+
return None
|
|
240
|
+
if until.tzinfo is None:
|
|
241
|
+
return None
|
|
242
|
+
return max((until - now).total_seconds(), 0.0)
|
|
243
|
+
return seconds if math.isfinite(seconds) and seconds >= 0 else None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _retry_delay(retry_after: str | None, attempt: int, backoff_s: float) -> float:
|
|
247
|
+
"""Return how long to wait before retrying a rate-limited request.
|
|
248
|
+
|
|
249
|
+
Prefers the panel's ``Retry-After`` header and falls back to exponential
|
|
250
|
+
backoff where it gives no usable wait.
|
|
251
|
+
"""
|
|
252
|
+
parsed = _retry_after_s(retry_after, datetime.now(UTC))
|
|
253
|
+
return backoff_s * (2.0 ** (attempt - 1)) if parsed is None else parsed
|
|
234
254
|
|
|
235
255
|
|
|
236
256
|
async def register_v2(
|
|
@@ -276,6 +296,7 @@ async def register_v2(
|
|
|
276
296
|
Raises:
|
|
277
297
|
SpanPanelAuthError: Invalid passphrase or auth failure
|
|
278
298
|
SpanPanelPassphraseUnavailableError: The panel cannot read its own passphrase
|
|
299
|
+
SpanPanelRateLimitError: HTTP 429; the panel is limiting registrations. Not retried
|
|
279
300
|
SpanPanelServerError: The panel is not ready to register clients (any 5xx,
|
|
280
301
|
including the 503 it answers before its serial number is known); retryable
|
|
281
302
|
SpanPanelConnectionError: Cannot reach panel
|
|
@@ -314,6 +335,16 @@ async def register_v2(
|
|
|
314
335
|
status_code=reply.status_code,
|
|
315
336
|
)
|
|
316
337
|
|
|
338
|
+
if reply.status_code == HTTP_TOO_MANY_REQUESTS:
|
|
339
|
+
# Not retried: the panel limits registrations per client address, and a
|
|
340
|
+
# retry here would spend the caller's next attempt on its behalf.
|
|
341
|
+
_log_auth_failure(reply.endpoint, reply.response, sent)
|
|
342
|
+
raise SpanPanelRateLimitError(
|
|
343
|
+
f"Panel is rate-limiting registration (HTTP {reply.status_code})",
|
|
344
|
+
status_code=reply.status_code,
|
|
345
|
+
retry_after_s=_retry_after_s(reply.headers.get("retry-after"), datetime.now(UTC)),
|
|
346
|
+
)
|
|
347
|
+
|
|
317
348
|
if reply.status_code == 422 and _error_detail(reply.response) == REGISTRATION_UNAVAILABLE_DETAIL:
|
|
318
349
|
# Checked before the general 422 below, which means "credential not
|
|
319
350
|
# accepted". This one is the panel failing to read its own passphrase,
|
|
@@ -454,6 +485,32 @@ async def download_ca_cert(
|
|
|
454
485
|
raise SpanPanelAPIError(f"Failed to download CA cert: HTTP {last_status}")
|
|
455
486
|
|
|
456
487
|
|
|
488
|
+
def _schema_hash(data: Mapping[str, object]) -> str:
|
|
489
|
+
"""The schema hash ``V2HomieSchema.types_schema_hash`` reports.
|
|
490
|
+
|
|
491
|
+
A parent/child panel publishes its schema as ``deviceClasses``, with its own
|
|
492
|
+
``deviceClassesSchemaHash`` over that block, in place of flat firmware's
|
|
493
|
+
``types``. Hashing ``types`` there hashed an absent block, so every such
|
|
494
|
+
panel got one constant key. The published hash is taken as is when it is a
|
|
495
|
+
non-empty string; otherwise ``deviceClasses`` is hashed here.
|
|
496
|
+
|
|
497
|
+
SPAN's public API changelog states that ``deviceClassesSchemaHash`` is
|
|
498
|
+
computed over the new structure, so it differs from a ``typesSchemaHash``
|
|
499
|
+
even where the property content is unchanged, and that a cached
|
|
500
|
+
``typesSchemaHash`` is not to be compared against it. A key held from a
|
|
501
|
+
release that hashed ``types`` therefore differs once on such a panel.
|
|
502
|
+
|
|
503
|
+
A flat panel's key is exactly what earlier releases computed, so a value
|
|
504
|
+
stored for a flat panel still matches.
|
|
505
|
+
"""
|
|
506
|
+
published = data.get("deviceClassesSchemaHash")
|
|
507
|
+
if isinstance(published, str) and published:
|
|
508
|
+
return published
|
|
509
|
+
hashed = data["deviceClasses"] if "deviceClasses" in data else data.get("types", {})
|
|
510
|
+
canonical = json.dumps(hashed, sort_keys=True)
|
|
511
|
+
return "sha256:" + hashlib.sha256(canonical.encode()).hexdigest()[:16]
|
|
512
|
+
|
|
513
|
+
|
|
457
514
|
async def get_homie_schema(
|
|
458
515
|
host: str,
|
|
459
516
|
timeout: float = 10.0,
|
|
@@ -527,15 +584,9 @@ async def get_homie_schema(
|
|
|
527
584
|
if isinstance(props, dict):
|
|
528
585
|
types[str(type_name)] = {str(k): v for k, v in props.items()}
|
|
529
586
|
|
|
530
|
-
# Compute schema hash from types key names for change detection
|
|
531
|
-
# The panel provides this implicitly via the firmware version + types structure
|
|
532
|
-
# We derive a hash for caching; the fixture README documents the expected value
|
|
533
|
-
types_json = json.dumps(data.get("types", {}), sort_keys=True)
|
|
534
|
-
schema_hash = "sha256:" + hashlib.sha256(types_json.encode()).hexdigest()[:16]
|
|
535
|
-
|
|
536
587
|
# Read before anything else interprets the payload. A parent/child response
|
|
537
|
-
# carries `deviceClasses` where this one reads `types`, so
|
|
538
|
-
#
|
|
588
|
+
# carries `deviceClasses` where this one reads `types`, so `types` degrades
|
|
589
|
+
# to empty for such a panel — which is harmless only because this
|
|
539
590
|
# value routes it to a different parser before those fields are used.
|
|
540
591
|
# Absence is the flat signal and must stay distinct from an empty string.
|
|
541
592
|
raw_data_model_version = data.get("dataModelVersion")
|
|
@@ -543,7 +594,7 @@ async def get_homie_schema(
|
|
|
543
594
|
|
|
544
595
|
return V2HomieSchema(
|
|
545
596
|
firmware_version=str(data.get("firmwareVersion", "")),
|
|
546
|
-
types_schema_hash=
|
|
597
|
+
types_schema_hash=_schema_hash(data),
|
|
547
598
|
types=types,
|
|
548
599
|
data_model_version=data_model_version,
|
|
549
600
|
)
|
|
@@ -593,8 +644,8 @@ async def rotate_passphrase(
|
|
|
593
644
|
Raises:
|
|
594
645
|
SpanPanelInsufficientPrivilegeError: HTTP 403; the token is reduced-privilege
|
|
595
646
|
SpanPanelAuthError: HTTP 401 (token invalid or stale) or 412 (no bearer token)
|
|
596
|
-
SpanPanelServerError: HTTP 5xx. After a 500 the outcome is unknown:
|
|
597
|
-
passphrase may or may not have changed.
|
|
647
|
+
SpanPanelServerError: HTTP 5xx. After a 500 or a 504 the outcome is unknown:
|
|
648
|
+
the passphrase may or may not have changed.
|
|
598
649
|
SpanPanelConnectionError: Cannot reach panel
|
|
599
650
|
SpanPanelTimeoutError: Request timed out
|
|
600
651
|
SpanPanelAPIError: Unexpected response
|
|
@@ -68,6 +68,25 @@ class SpanPanelServerError(SpanPanelAPIError):
|
|
|
68
68
|
"""
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
class SpanPanelRateLimitError(SpanPanelAPIError):
|
|
72
|
+
"""The panel is limiting the client's requests and refused this one (HTTP 429).
|
|
73
|
+
|
|
74
|
+
Raised by ``register_v2``, which does not retry: the caller decides when to
|
|
75
|
+
try again. ``retry_after_s`` is the wait the panel asked for, in seconds,
|
|
76
|
+
from its ``Retry-After`` header: a delta-seconds value as given, an HTTP-date
|
|
77
|
+
as the seconds until that time. ``None`` when the header is absent or holds
|
|
78
|
+
neither.
|
|
79
|
+
|
|
80
|
+
A `SpanPanelAPIError`, so an existing clause catching that class still
|
|
81
|
+
catches this, and not a `SpanPanelAuthError`: the credential was never
|
|
82
|
+
judged.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, message: str, status_code: int | None = None, retry_after_s: float | None = None) -> None:
|
|
86
|
+
super().__init__(message, status_code)
|
|
87
|
+
self.retry_after_s = retry_after_s
|
|
88
|
+
|
|
89
|
+
|
|
71
90
|
class SpanPanelPassphraseUnavailableError(SpanPanelAPIError):
|
|
72
91
|
"""The panel cannot read its own passphrase, so it cannot issue broker credentials.
|
|
73
92
|
|