span-panel-api 3.7.0b1__tar.gz → 3.8.0b2__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.7.0b1 → span_panel_api-3.8.0b2}/.gitignore +1 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/CHANGELOG.md +8 -1
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/PKG-INFO +5 -2
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/README.md +3 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/pyproject.toml +12 -3
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/__init__.py +38 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/auth.py +44 -13
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/exceptions.py +19 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/models.py +275 -5
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/mqtt/client.py +70 -4
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/mqtt/control.py +1 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/protocol.py +29 -1
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/adapter_fidelity/harness.py +16 -5
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/adapter_fidelity/rules.py +142 -13
- span_panel_api-3.8.0b2/tests/adapter_fidelity/rules_captured.py +85 -0
- span_panel_api-3.8.0b2/tests/adapter_fidelity/test_capture_fidelity.py +153 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/adapter_fidelity/test_harness.py +47 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/adapter_fidelity/test_main32_fidelity.py +13 -4
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/conftest.py +55 -1
- span_panel_api-3.8.0b2/tests/expected_failures.py +48 -0
- span_panel_api-3.8.0b2/tests/fixtures/captures/README.md +17 -0
- span_panel_api-3.8.0b2/tests/fixtures/captures/SHA256SUMS +6 -0
- span_panel_api-3.8.0b2/tests/fixtures/captures/r202639-a-tree-v1.json +1048 -0
- span_panel_api-3.8.0b2/tests/fixtures/captures/r202639-b-tree-v1.json +4039 -0
- span_panel_api-3.8.0b2/tests/fixtures/captures/r202639-c-tree-v1.json +1863 -0
- span_panel_api-3.8.0b2/tests/fixtures/captures/r202639-d-tree-v1.json +2372 -0
- span_panel_api-3.8.0b2/tests/fixtures/captures/r202639-e-tree-v1.json +10577 -0
- span_panel_api-3.8.0b2/tests/fixtures/distribution-enclosure-simulator.LICENSE +21 -0
- span_panel_api-3.8.0b2/tests/fixtures/other_model_tree.json +276 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/fixtures/panelbench_unvalued_by_both.json +2 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/reference_payloads/README.md +6 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/reference_payloads/__init__.py +10 -0
- span_panel_api-3.8.0b2/tests/reference_payloads/captures.py +253 -0
- span_panel_api-3.8.0b2/tests/reference_payloads/reidentify.py +207 -0
- span_panel_api-3.8.0b2/tests/reference_payloads/schema_one.py +174 -0
- span_panel_api-3.8.0b2/tests/reference_payloads/synthetic_trees.py +457 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_adoption.py +215 -1
- span_panel_api-3.8.0b2/tests/test_capture_replay.py +166 -0
- span_panel_api-3.8.0b2/tests/test_captures_unchanged.py +53 -0
- span_panel_api-3.8.0b2/tests/test_circuit_fed_battery_captures.py +21 -0
- span_panel_api-3.8.0b2/tests/test_declared_unvalued_captures.py +126 -0
- span_panel_api-3.8.0b2/tests/test_evse_lock_captures.py +59 -0
- span_panel_api-3.8.0b2/tests/test_evse_lock_control.py +112 -0
- span_panel_api-3.8.0b2/tests/test_expected_failures_registry.py +89 -0
- span_panel_api-3.8.0b2/tests/test_feeds_role_captures.py +30 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_mqtt_client_connection.py +33 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_mqtt_homie.py +17 -0
- span_panel_api-3.8.0b2/tests/test_panel_properties_captures.py +87 -0
- span_panel_api-3.8.0b2/tests/test_positions_captures.py +107 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_protocol_conformance.py +5 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_public_api_unchanged.py +30 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_publish_outcome.py +19 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_reference_tree_values.py +25 -14
- span_panel_api-3.8.0b2/tests/test_register_rate_limit.py +87 -0
- span_panel_api-3.8.0b2/tests/test_register_rate_limit_companions.py +92 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_migration_delta.py +6 -0
- span_panel_api-3.8.0b2/tests/test_schema_one_acceptance_companions.py +57 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_adapter.py +183 -6
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_charge_limit.py +359 -28
- span_panel_api-3.8.0b2/tests/test_schema_one_circuit_fed_battery.py +61 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_circuits.py +1 -1
- span_panel_api-3.8.0b2/tests/test_schema_one_cleared_retained.py +253 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_conformance.py +37 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_connection_health.py +1 -1
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_control_refusal.py +20 -2
- span_panel_api-3.8.0b2/tests/test_schema_one_declared_unvalued.py +125 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_devices.py +104 -47
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_discovery.py +64 -1
- span_panel_api-3.8.0b2/tests/test_schema_one_evse_lock.py +57 -0
- span_panel_api-3.8.0b2/tests/test_schema_one_feeds_role.py +65 -0
- span_panel_api-3.8.0b2/tests/test_schema_one_other_models.py +589 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_panel.py +12 -10
- span_panel_api-3.8.0b2/tests/test_schema_one_panel_properties.py +168 -0
- span_panel_api-3.8.0b2/tests/test_schema_one_positions.py +125 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_service_entrance.py +28 -4
- span_panel_api-3.8.0b2/tests/test_schema_one_shared_meters.py +115 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_snapshot.py +22 -0
- span_panel_api-3.8.0b2/tests/test_schema_one_space_less_meters.py +175 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_transport.py +114 -0
- span_panel_api-3.8.0b2/tests/test_schema_one_tree_export.py +2201 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_undeclared_devices.py +1 -1
- span_panel_api-3.8.0b2/tests/test_shared_meters_captures.py +60 -0
- span_panel_api-3.8.0b2/tests/test_space_less_meters_captures.py +64 -0
- span_panel_api-3.8.0b2/tests/test_synthetic_trees.py +295 -0
- span_panel_api-3.8.0b2/tests/test_tree_export_protocol.py +23 -0
- span_panel_api-3.7.0b1/tests/reference_payloads/schema_one.py +0 -83
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/LICENSE +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/_http.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/_ssl.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/adapters.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/const.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/detection.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/dispatch.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/factory.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/mqtt/__init__.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/mqtt/async_client.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/mqtt/connection.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/mqtt/const.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/mqtt/models.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/phase_validation.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/py.typed +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/src/span_panel_api/schema_drift.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/adapter_fidelity/__init__.py +0 -0
- /span_panel_api-3.7.0b1/tests/fixtures/distribution-enclosure-simulator.LICENSE → /span_panel_api-3.8.0b2/tests/fixtures/captures/LICENSE +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/fixtures/flat_wire.json +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/fixtures/main32-tree-v1.README.md +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/fixtures/main32-tree-v1.json +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/fixtures/main32_r202639-tree-v1.README.md +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/fixtures/main32_r202639-tree-v1.json +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/fixtures/panelbench_wire.json +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/fixtures/v2/README.md +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/fixtures/v2/status.json +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/reference_payloads/bootstrap.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/simulation_fixtures/circuits.response.txt +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/simulation_fixtures/panel.response.txt +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/simulation_fixtures/soe.response.txt +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/simulation_fixtures/status.response.txt +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_absent_readings_are_not_zero.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_accumulator.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_adapters_discovery.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_adopted_control.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_async_mqtt_client.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_auth_and_homie_helpers.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_auth_redaction.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_ca_pinning.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_catalog_divergence.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_control_interceptor.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_detection_auth.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_exceptions.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_factory_dispatch.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_field_metadata.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_homie_schema_hash.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_https_transport.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_leaf_name_mismatch.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_live_flat_differential.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_mqtt_bridge.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_mqtt_connect_flow.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_mqtt_debounce.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_packaging.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_phase_validation_configs.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_phase_validation_errors.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_plaintext_warning.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_protocol_models.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_redispatch_on_reconnect.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_register_passphrase_unavailable.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_rest_transport_contract.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_fetch_transport_split.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_generation_cross_check.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_device_display_name.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_extension.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_firmware.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_pcs.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_shed_forecast.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_one_sheddable.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_provenance.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_schema_zero_adapter.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_shared_http_client.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_snapshot_readiness_gate.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_ssl_context.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/test_v2_status_parser.py +0 -0
- {span_panel_api-3.7.0b1 → span_panel_api-3.8.0b2}/tests/tls_fixtures.py +0 -0
|
@@ -13,7 +13,10 @@ Snapshot fields that held a fabricated constant can now be unknown, and with `sp
|
|
|
13
13
|
|
|
14
14
|
### Added
|
|
15
15
|
|
|
16
|
+
- **spanos3/r202639/03 batch 2: reads more of the data this firmware publishes.**
|
|
17
|
+
- **`SpanMqttClient.masked_tree()` returns the panel's device tree with identifiers and site details masked**, for a consumer's diagnostics; `None` on a flat panel.
|
|
16
18
|
- **`V2StatusInfo.hardware_version` reports the hardware version string from `GET /api/v2/status`.**
|
|
19
|
+
- **`SpanEvseSnapshot.charge_current_limit_minimum_a` and `charge_current_limit_maximum_a` report the range a SPAN Drive declares for its charge-current limit**, `None` where undeclared.
|
|
17
20
|
|
|
18
21
|
### Changed
|
|
19
22
|
|
|
@@ -22,7 +25,10 @@ Snapshot fields that held a fabricated constant can now be unknown, and with `sp
|
|
|
22
25
|
- **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`.
|
|
23
26
|
- **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
|
|
24
27
|
panel, where they were `0`.
|
|
25
|
-
- **
|
|
28
|
+
- **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.
|
|
29
|
+
- **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.
|
|
30
|
+
- **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
|
|
31
|
+
release adds.
|
|
26
32
|
|
|
27
33
|
### Removed
|
|
28
34
|
|
|
@@ -33,6 +39,7 @@ Snapshot fields that held a fabricated constant can now be unknown, and with `sp
|
|
|
33
39
|
|
|
34
40
|
- **`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
|
|
35
41
|
value stored from 3.6.x differs once on a data model 1.0 panel.
|
|
42
|
+
- **A property the panel clears reads as unpublished** on a parent/child panel, where it read as an empty value until the panel published it again.
|
|
36
43
|
|
|
37
44
|
## [3.6.2]
|
|
38
45
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: span-panel-api
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.8.0b2
|
|
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
|
|
@@ -22,7 +22,7 @@ Requires-Dist: pyyaml>=6.0.0
|
|
|
22
22
|
Provides-Extra: schema-0
|
|
23
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.0b4; extra == 'schema-1'
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
|
|
28
28
|
# SPAN Panel API
|
|
@@ -171,6 +171,9 @@ because a transport could reasonably offer one and not the other.
|
|
|
171
171
|
One further protocol, `SchemaAdapter`, is the bootstrap-to-parser contract rather than a consumer-facing one; it is what an adapter distribution implements and what discovery checks. Integration code programs against the protocols above, not against
|
|
172
172
|
transport-specific classes.
|
|
173
173
|
|
|
174
|
+
`TreeExportingAdapter` sits beside `SchemaAdapter` as an optional protocol, not one every adapter must implement: an adapter that offers `export_masked_tree()` lets `SpanMqttClient.masked_tree()` return the panel's device tree with serials, device ids and
|
|
175
|
+
site details masked, and `None` otherwise.
|
|
176
|
+
|
|
174
177
|
### Snapshots
|
|
175
178
|
|
|
176
179
|
All panel state is represented as immutable, frozen dataclasses:
|
|
@@ -144,6 +144,9 @@ because a transport could reasonably offer one and not the other.
|
|
|
144
144
|
One further protocol, `SchemaAdapter`, is the bootstrap-to-parser contract rather than a consumer-facing one; it is what an adapter distribution implements and what discovery checks. Integration code programs against the protocols above, not against
|
|
145
145
|
transport-specific classes.
|
|
146
146
|
|
|
147
|
+
`TreeExportingAdapter` sits beside `SchemaAdapter` as an optional protocol, not one every adapter must implement: an adapter that offers `export_masked_tree()` lets `SpanMqttClient.masked_tree()` return the panel's device tree with serials, device ids and
|
|
148
|
+
site details masked, and `None` otherwise.
|
|
149
|
+
|
|
147
150
|
### Snapshots
|
|
148
151
|
|
|
149
152
|
All panel state is represented as immutable, frozen dataclasses:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "span-panel-api"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.8.0b2"
|
|
4
4
|
description = "A client library for SPAN Panel API"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "SpanPanel"}
|
|
@@ -69,7 +69,10 @@ schema-0 = ["span-panel-api-schema-0>=1.3.0b1"]
|
|
|
69
69
|
# Raised to 1.2.2 for 3.6.2: the readiness gate every snapshot passes asks that
|
|
70
70
|
# adapter what is still missing, and 1.2.2 restarts its feed grace correctly.
|
|
71
71
|
# Raised to 1.3.0b1 for 3.7.0b1, for the same reason as schema-0's.
|
|
72
|
-
|
|
72
|
+
# Raised to 1.4.0b1 for 3.8.0b1: that adapter fills the snapshot fields this
|
|
73
|
+
# bootstrap adds.
|
|
74
|
+
# Raised to 1.4.0b4 for 3.8.0b2: that adapter exports the masked device tree.
|
|
75
|
+
schema-1 = ["span-panel-api-schema-1>=1.4.0b4"]
|
|
73
76
|
|
|
74
77
|
[project.urls]
|
|
75
78
|
Homepage = "https://github.com/SpanPanel/span-panel-api"
|
|
@@ -93,7 +96,7 @@ dev = [
|
|
|
93
96
|
# known. Dependabot raises the bump on its own; the bump PR re-runs
|
|
94
97
|
# `scripts/capture_parent_child_reference.py` and the suite then says whether
|
|
95
98
|
# the wire moved.
|
|
96
|
-
"ebus-panel-sim==0.
|
|
99
|
+
"ebus-panel-sim==0.9.0",
|
|
97
100
|
"pytest>=9.0.2",
|
|
98
101
|
"pytest-asyncio>=1.3.0",
|
|
99
102
|
"pytest-cov",
|
|
@@ -253,6 +256,12 @@ explicit_package_bases = true
|
|
|
253
256
|
# `tests/` on `sys.path`; the support packages the mypy hook checks resolve the same
|
|
254
257
|
# way here.
|
|
255
258
|
mypy_path = "src:tests"
|
|
259
|
+
# paho ships `py.typed` but leaves its topic matcher unannotated, so every call
|
|
260
|
+
# into it would be an untyped call. The schema-1 route table resolves what the
|
|
261
|
+
# matcher yields through a typed dict at once, which keeps its `Any` from going
|
|
262
|
+
# further; `warn_return_any` does not police that for you (a union or a
|
|
263
|
+
# `yield from` passes it), so keep it to that shape.
|
|
264
|
+
untyped_calls_exclude = ["paho.mqtt.matcher"]
|
|
256
265
|
# Excluded by name for the reason `[tool.ruff]` gives above: the capture script is
|
|
257
266
|
# checked like the library, and the hand-run tools written before that was expected
|
|
258
267
|
# 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,22 @@ 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,
|
|
56
|
+
MaskedTree,
|
|
57
|
+
MaskedTreeDevice,
|
|
58
|
+
MaskedTreeMetadata,
|
|
52
59
|
PassphraseRotation,
|
|
60
|
+
ProtectionFunction,
|
|
53
61
|
SpanBatterySnapshot,
|
|
54
62
|
SpanCircuitSnapshot,
|
|
55
63
|
SpanEvseSnapshot,
|
|
@@ -57,10 +65,12 @@ from .models import (
|
|
|
57
65
|
SpanPanelSnapshot,
|
|
58
66
|
SpanPcsSnapshot,
|
|
59
67
|
SpanPVSnapshot,
|
|
68
|
+
UndescribedDevice,
|
|
60
69
|
V2AuthResponse,
|
|
61
70
|
V2HomieSchema,
|
|
62
71
|
V2StatusInfo,
|
|
63
72
|
is_discovery_path,
|
|
73
|
+
shared_meter_groups,
|
|
64
74
|
)
|
|
65
75
|
from .mqtt import (
|
|
66
76
|
ControlCommand,
|
|
@@ -84,10 +94,12 @@ from .protocol import (
|
|
|
84
94
|
CircuitControlProtocol,
|
|
85
95
|
ControlInterceptionProtocol,
|
|
86
96
|
EvseControlProtocol,
|
|
97
|
+
EvseLockControlProtocol,
|
|
87
98
|
PanelCapability,
|
|
88
99
|
PanelControlProtocol,
|
|
89
100
|
SpanPanelClientProtocol,
|
|
90
101
|
StreamingCapableProtocol,
|
|
102
|
+
TreeExportingAdapter,
|
|
91
103
|
)
|
|
92
104
|
|
|
93
105
|
__version__ = _pkg_version("span-panel-api")
|
|
@@ -106,6 +118,9 @@ __all__ = [ # noqa: RUF022
|
|
|
106
118
|
# snapshot rather than by its arguments -- a device the adapter models
|
|
107
119
|
# produces no AdoptedDevice and so cannot be addressed through it.
|
|
108
120
|
"AdoptedControlProtocol",
|
|
121
|
+
# A charger's connector lock, authorised by `SpanEvseSnapshot.lock_control`.
|
|
122
|
+
# A protocol of its own so `EvseControlProtocol`'s implementers are unchanged.
|
|
123
|
+
"EvseLockControlProtocol",
|
|
109
124
|
# Added 2026-08-25 (3.1.0): one veto/observe point for every control
|
|
110
125
|
# command. A protocol of its own rather than a member on the four control
|
|
111
126
|
# protocols, which would break their implementers a second time in one
|
|
@@ -137,6 +152,15 @@ __all__ = [ # noqa: RUF022
|
|
|
137
152
|
"ControlTarget",
|
|
138
153
|
"ExtensionProperty",
|
|
139
154
|
"ExtensionSubject",
|
|
155
|
+
# Added 2026-10-31: the masked device tree a consumer forwards in diagnostics,
|
|
156
|
+
# and the optional protocol an adapter offers it through. Deliberately not a
|
|
157
|
+
# SchemaAdapter member: the protocol derives its required set from itself, so a
|
|
158
|
+
# member there would invalidate every built adapter wheel.
|
|
159
|
+
"MaskedTree",
|
|
160
|
+
"MaskedTreeDevice",
|
|
161
|
+
"MaskedTreeMetadata",
|
|
162
|
+
"UndescribedDevice",
|
|
163
|
+
"TreeExportingAdapter",
|
|
140
164
|
# Snapshots
|
|
141
165
|
"SpanBatterySnapshot",
|
|
142
166
|
"SpanCircuitSnapshot",
|
|
@@ -145,6 +169,16 @@ __all__ = [ # noqa: RUF022
|
|
|
145
169
|
"SpanPVSnapshot",
|
|
146
170
|
"SpanPanelSnapshot",
|
|
147
171
|
"SpanPcsSnapshot",
|
|
172
|
+
# What a circuit's `connection/feeds-role` declares, read into
|
|
173
|
+
# `SpanCircuitSnapshot.feeds_role`.
|
|
174
|
+
"FEEDS_ROLES",
|
|
175
|
+
"FeedsRole",
|
|
176
|
+
# What a circuit's `breaker/protection-functions` lists, read into
|
|
177
|
+
# `SpanCircuitSnapshot.protection_functions`.
|
|
178
|
+
"PROTECTION_FUNCTIONS",
|
|
179
|
+
"ProtectionFunction",
|
|
180
|
+
# The circuits one meter measures, from `SpanCircuitSnapshot.meter_shared_with`.
|
|
181
|
+
"shared_meter_groups",
|
|
148
182
|
# Factory
|
|
149
183
|
"create_span_client",
|
|
150
184
|
# Detection
|
|
@@ -216,6 +250,10 @@ __all__ = [ # noqa: RUF022
|
|
|
216
250
|
# keep their meaning; deliberately not a SpanPanelAuthError, because the
|
|
217
251
|
# passphrase the user supplied may be correct.
|
|
218
252
|
"SpanPanelPassphraseUnavailableError",
|
|
253
|
+
# Registration refused with HTTP 429, carrying the panel's `Retry-After` in
|
|
254
|
+
# seconds. A subclass of SpanPanelAPIError, so existing except clauses keep
|
|
255
|
+
# their meaning; not a SpanPanelAuthError, because no credential was judged.
|
|
256
|
+
"SpanPanelRateLimitError",
|
|
219
257
|
"SpanPanelServerError",
|
|
220
258
|
"SpanPanelStaleDataError",
|
|
221
259
|
# Added 2026-08-31 (3.4.0): a bootstrap REST call that failed verification
|
|
@@ -10,9 +10,12 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import asyncio
|
|
12
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,
|
|
@@ -613,8 +644,8 @@ async def rotate_passphrase(
|
|
|
613
644
|
Raises:
|
|
614
645
|
SpanPanelInsufficientPrivilegeError: HTTP 403; the token is reduced-privilege
|
|
615
646
|
SpanPanelAuthError: HTTP 401 (token invalid or stale) or 412 (no bearer token)
|
|
616
|
-
SpanPanelServerError: HTTP 5xx. After a 500 the outcome is unknown:
|
|
617
|
-
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.
|
|
618
649
|
SpanPanelConnectionError: Cannot reach panel
|
|
619
650
|
SpanPanelTimeoutError: Request timed out
|
|
620
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
|
|