span-panel-api 3.8.0b1__tar.gz → 3.9.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.8.0b1 → span_panel_api-3.9.0b1}/.gitignore +1 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/CHANGELOG.md +16 -6
- span_panel_api-3.8.0b1/README.md → span_panel_api-3.9.0b1/PKG-INFO +62 -17
- span_panel_api-3.8.0b1/PKG-INFO → span_panel_api-3.9.0b1/README.md +35 -44
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/pyproject.toml +14 -8
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/__init__.py +14 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/models.py +111 -1
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/mqtt/client.py +30 -4
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/protocol.py +16 -1
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/adapter_fidelity/harness.py +5 -5
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/adapter_fidelity/rules.py +8 -7
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/adapter_fidelity/rules_captured.py +14 -2
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/adapter_fidelity/test_harness.py +47 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/panelbench_unvalued_by_both.json +2 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/reference_payloads/captures.py +8 -4
- span_panel_api-3.9.0b1/tests/reference_payloads/reidentify.py +207 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/reference_payloads/schema_one.py +37 -5
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_adoption.py +215 -1
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_mqtt_client_connection.py +33 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_mqtt_homie.py +17 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_public_api_unchanged.py +9 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_publish_outcome.py +19 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_reference_tree_values.py +25 -14
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_adapter.py +182 -5
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_charge_limit.py +359 -28
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_circuits.py +56 -0
- span_panel_api-3.9.0b1/tests/test_schema_one_cleared_retained.py +253 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_control_refusal.py +18 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_devices.py +104 -47
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_panel.py +12 -10
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_shared_meters.py +41 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_snapshot.py +22 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_transport.py +114 -0
- span_panel_api-3.9.0b1/tests/test_schema_one_tree_export.py +2201 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_shared_meters_captures.py +10 -0
- span_panel_api-3.9.0b1/tests/test_tree_export_protocol.py +23 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/LICENSE +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/_http.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/_ssl.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/adapters.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/auth.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/const.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/detection.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/dispatch.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/exceptions.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/factory.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/mqtt/__init__.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/mqtt/async_client.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/mqtt/connection.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/mqtt/const.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/mqtt/control.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/mqtt/models.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/phase_validation.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/py.typed +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/src/span_panel_api/schema_drift.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/adapter_fidelity/__init__.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/adapter_fidelity/test_capture_fidelity.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/adapter_fidelity/test_main32_fidelity.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/conftest.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/expected_failures.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/captures/LICENSE +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/captures/README.md +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/captures/SHA256SUMS +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/captures/r202639-a-tree-v1.json +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/captures/r202639-b-tree-v1.json +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/captures/r202639-c-tree-v1.json +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/captures/r202639-d-tree-v1.json +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/captures/r202639-e-tree-v1.json +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/distribution-enclosure-simulator.LICENSE +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/flat_wire.json +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/main32-tree-v1.README.md +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/main32-tree-v1.json +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/main32_r202639-tree-v1.README.md +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/main32_r202639-tree-v1.json +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/other_model_tree.json +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/panelbench_wire.json +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/v2/README.md +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/fixtures/v2/status.json +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/reference_payloads/README.md +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/reference_payloads/__init__.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/reference_payloads/bootstrap.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/reference_payloads/synthetic_trees.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/simulation_fixtures/circuits.response.txt +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/simulation_fixtures/panel.response.txt +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/simulation_fixtures/soe.response.txt +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/simulation_fixtures/status.response.txt +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_absent_readings_are_not_zero.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_accumulator.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_adapters_discovery.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_adopted_control.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_async_mqtt_client.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_auth_and_homie_helpers.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_auth_redaction.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_ca_pinning.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_capture_replay.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_captures_unchanged.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_catalog_divergence.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_circuit_fed_battery_captures.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_control_interceptor.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_declared_unvalued_captures.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_detection_auth.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_evse_lock_captures.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_evse_lock_control.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_exceptions.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_expected_failures_registry.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_factory_dispatch.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_feeds_role_captures.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_field_metadata.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_homie_schema_hash.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_https_transport.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_leaf_name_mismatch.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_live_flat_differential.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_mqtt_bridge.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_mqtt_connect_flow.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_mqtt_debounce.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_packaging.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_panel_properties_captures.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_phase_validation_configs.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_phase_validation_errors.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_plaintext_warning.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_positions_captures.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_protocol_conformance.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_protocol_models.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_redispatch_on_reconnect.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_register_passphrase_unavailable.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_register_rate_limit.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_register_rate_limit_companions.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_rest_transport_contract.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_fetch_transport_split.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_generation_cross_check.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_migration_delta.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_acceptance_companions.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_circuit_fed_battery.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_conformance.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_connection_health.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_declared_unvalued.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_device_display_name.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_discovery.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_evse_lock.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_extension.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_feeds_role.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_firmware.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_other_models.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_panel_properties.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_pcs.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_positions.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_service_entrance.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_shed_forecast.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_sheddable.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_space_less_meters.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_one_undeclared_devices.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_provenance.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_schema_zero_adapter.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_shared_http_client.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_snapshot_readiness_gate.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_space_less_meters_captures.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_ssl_context.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_synthetic_trees.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/test_v2_status_parser.py +0 -0
- {span_panel_api-3.8.0b1 → span_panel_api-3.9.0b1}/tests/tls_fixtures.py +0 -0
|
@@ -7,19 +7,28 @@ 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
|
-
## [
|
|
10
|
+
## [3.9.0]
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`SpanCircuitSnapshot.breaker_rating_declared`** says whether a circuit declares a breaker rating, so a consumer can tell a rating the panel does not publish from one that has not arrived yet; `None` where the adapter does not say.
|
|
15
|
+
- **`SpanPanelSnapshot.publishes_shared_meters`** says whether any circuit declares `meter/shared-with-device-ids`, so a consumer can tell a panel that describes its multi-load breakers from one, such as MAIN 32, that publishes each as a single circuit.
|
|
16
|
+
|
|
17
|
+
## [3.8.0]
|
|
18
|
+
|
|
19
|
+
Snapshot fields that held a fabricated constant can now be unknown, and with `span-panel-api-schema-0` 1.3.0 and `span-panel-api-schema-1` 1.4.0 or newer the parsers report them as unknown and stop synthesising unoccupied positions.
|
|
13
20
|
|
|
14
21
|
### Added
|
|
15
22
|
|
|
16
23
|
- **spanos3/r202639/03 batch 2: reads more of the data this firmware publishes.**
|
|
24
|
+
- **`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.
|
|
17
25
|
- **`V2StatusInfo.hardware_version` reports the hardware version string from `GET /api/v2/status`.**
|
|
26
|
+
- **`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.
|
|
18
27
|
|
|
19
28
|
### Changed
|
|
20
29
|
|
|
21
|
-
- **BREAKING FOR CONSUMERS: `SpanPanelSnapshot.proximity_proven` becomes `bool | None`**, and with `span-panel-api-schema-0` and `span-panel-api-schema-1` 1.
|
|
22
|
-
schema publishes a proximity property and the panel's own answer is `V2StatusInfo.proximity_proven`.
|
|
30
|
+
- **BREAKING FOR CONSUMERS: `SpanPanelSnapshot.proximity_proven` becomes `bool | None`**, and with `span-panel-api-schema-0` 1.3.0 and `span-panel-api-schema-1` 1.4.0 or newer it is always `None`, where it was effectively a constant `True`, because neither
|
|
31
|
+
MQTT schema publishes a proximity property and the panel's own answer is `V2StatusInfo.proximity_proven`.
|
|
23
32
|
- **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
33
|
- **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
34
|
panel, where they were `0`.
|
|
@@ -30,13 +39,14 @@ Snapshot fields that held a fabricated constant can now be unknown, and with `sp
|
|
|
30
39
|
|
|
31
40
|
### Removed
|
|
32
41
|
|
|
33
|
-
- **BREAKING: with `span-panel-api-schema-0` and `span-panel-api-schema-1` 1.
|
|
34
|
-
holds only real circuits and `panel_size` alone states the panel's total positions.
|
|
42
|
+
- **BREAKING: with `span-panel-api-schema-0` 1.3.0 and `span-panel-api-schema-1` 1.4.0 or newer, snapshots no longer include synthesised `unmapped_tab_N` entries for unoccupied breaker positions, which carried no measured data**, so
|
|
43
|
+
`SpanPanelSnapshot.circuits` holds only real circuits and `panel_size` alone states the panel's total positions.
|
|
35
44
|
|
|
36
45
|
### Fixed
|
|
37
46
|
|
|
38
47
|
- **`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
48
|
value stored from 3.6.x differs once on a data model 1.0 panel.
|
|
49
|
+
- **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.
|
|
40
50
|
|
|
41
51
|
## [3.6.2]
|
|
42
52
|
|
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: span-panel-api
|
|
3
|
+
Version: 3.9.0b1
|
|
4
|
+
Summary: A client library for SPAN Panel API
|
|
5
|
+
Project-URL: Homepage, https://github.com/SpanPanel/span-panel-api
|
|
6
|
+
Project-URL: Issues, https://github.com/SpanPanel/span-panel-api/issues
|
|
7
|
+
Author: SpanPanel
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
16
|
+
Classifier: Topic :: Home Automation
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: <4.0,>=3.14
|
|
19
|
+
Requires-Dist: httpx<1.0,>=0.28.1
|
|
20
|
+
Requires-Dist: paho-mqtt<3.0.0,>=2.0.0
|
|
21
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
22
|
+
Provides-Extra: schema-0
|
|
23
|
+
Requires-Dist: span-panel-api-schema-0>=1.3.0; extra == 'schema-0'
|
|
24
|
+
Provides-Extra: schema-1
|
|
25
|
+
Requires-Dist: span-panel-api-schema-1>=1.4.0; extra == 'schema-1'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
1
28
|
# SPAN Panel API
|
|
2
29
|
|
|
3
30
|
[](https://github.com/SpanPanel/span-panel-api/releases)
|
|
@@ -129,35 +156,42 @@ The library defines structural subtyping protocols (PEP 544). All are `runtime_c
|
|
|
129
156
|
| `CircuitControlProtocol` | Relay and shed-priority control: `set_circuit_relay`, `set_circuit_priority` |
|
|
130
157
|
| `PanelControlProtocol` | Panel-level control: `set_dominant_power_source` |
|
|
131
158
|
| `EvseControlProtocol` | Per-charger control: `set_evse_charge_limit(node_id, amps)` |
|
|
159
|
+
| `EvseLockControlProtocol` | Per-charger connector lock: `set_evse_lock(node_id, locked)` |
|
|
132
160
|
| `AdoptedControlProtocol` | Write to a settable property of a device this library models nothing for |
|
|
133
161
|
| `ControlInterceptionProtocol` | Install one veto-and-observe point for every control command: `set_control_interceptor` |
|
|
134
162
|
| `StreamingCapableProtocol` | Push-based updates: `register_snapshot_callback`, `start_streaming`, `stop_streaming` |
|
|
135
163
|
|
|
136
|
-
The first
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
164
|
+
The first six differ in subject, not just in name. `EvseControlProtocol` is separate from `PanelControlProtocol` because several chargers may be commissioned at once and every call names which one. `EvseLockControlProtocol` is separate from
|
|
165
|
+
`EvseControlProtocol` so that protocol's implementers are unchanged, and its write is authorised by the snapshot: only a charger whose `SpanEvseSnapshot.lock_control` is set can be addressed, and only with a value its `lock_state_options` lists.
|
|
166
|
+
`AdoptedControlProtocol` differs in kind: the curated setters name a control this library understands and translate or bound the value on the way out, while this one names a property by its wire address and passes the value through, because the declaration
|
|
167
|
+
is all anybody here knows about it. That write is authorised by the snapshot rather than by its arguments — the transport resolves the property against the current `adopted_devices` and refuses anything it does not find carrying a set topic, so a device
|
|
168
|
+
this library _does_ model cannot be addressed through it.
|
|
140
169
|
|
|
141
|
-
`ControlInterceptionProtocol` is separate from the
|
|
142
|
-
|
|
170
|
+
`ControlInterceptionProtocol` is separate from the control protocols rather than a member of them because adding it there would break every implementer of those protocols a second time in one release, and separate from `StreamingCapableProtocol` because a
|
|
171
|
+
transport could reasonably offer one and not the other.
|
|
143
172
|
|
|
144
173
|
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
174
|
transport-specific classes.
|
|
146
175
|
|
|
176
|
+
`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
|
|
177
|
+
site details masked, and `None` otherwise.
|
|
178
|
+
|
|
147
179
|
### Snapshots
|
|
148
180
|
|
|
149
181
|
All panel state is represented as immutable, frozen dataclasses:
|
|
150
182
|
|
|
151
183
|
- **`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,
|
|
153
|
-
`battery`, `pv`, `pv_inverters`, `evse`, `mid`, `pcs`,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
184
|
+
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, busbar current, line frequency, the rating of
|
|
185
|
+
the protection ahead of the upstream lugs, the first and last breaker positions, whether any circuit declares what it feeds or a shared meter, shed policy and forecast. It holds `circuits`, `battery`, `pv`, `pv_inverters`, `evse`, `mid`, `pcs`,
|
|
186
|
+
`adopted_devices` and `extension_properties`.
|
|
187
|
+
- **`SpanCircuitSnapshot`**: one circuit. Id, name, relay state and requester, power, energy and their update times, tabs, 240 V, breaker rating and whether it declares one, current, priority, user controllability, sheddable / never-backup / always-on
|
|
188
|
+
flags, device type, relative position, PCS management and priority, the `$target` pending state for relay and priority, nominal voltage, the breaker's protection functions, the role it declares for what it feeds, the other circuits its meter or relay is
|
|
189
|
+
shared with, and whether it is a meter outside the panel, which has no breaker space, relay or shed priority and reads import-positive.
|
|
190
|
+
- **`SpanBatterySnapshot`**: the BESS. Whether the panel declares one, SoE percentage and kWh, its own meter reading, communication state, link health, the circuit that feeds it, vendor / `model` / `part_number` / serial / version, nameplate capacity.
|
|
157
191
|
- **`SpanPVSnapshot`**: one PV inverter. Link health, vendor / `model` / serial / version, nameplate capacity (the array's DC size), feeding circuit, relative position, its wire `device_id` and its `node_id`, which is its key in `pv_inverters`.
|
|
158
192
|
`SpanPanelSnapshot.pv` is the lone inverter, or describes several together.
|
|
159
|
-
- **`SpanEvseSnapshot`**: one EV charger. `node_id`, feeding circuit, status, lock state, advertised current, link health, vendor / `model` / `part_number` / serial / version, charge-current limit, ceiling, pending target and settability,
|
|
160
|
-
limit it applies.
|
|
193
|
+
- **`SpanEvseSnapshot`**: one EV charger. `node_id`, feeding circuit, status, lock state, advertised current, link health, vendor / `model` / `part_number` / serial / version, charge-current limit, ceiling, pending target and settability, the effective
|
|
194
|
+
limit it applies, the range it declares for that limit, and where its connector lock is commanded with the values it accepts.
|
|
161
195
|
- **`SpanMidSnapshot`**: the Microgrid Interconnect Device. `node_id`, islanding state, grid state, grid-forming entity and its device name, vendor / `model` / serial / software and hardware version.
|
|
162
196
|
- **`SpanPcsSnapshot`**: the Power Control System. Enabled and active, the enforced import limit and its binding constraint, and each import constraint's limit, enablement and active state (feed, operator, off-grid, requested).
|
|
163
197
|
- **`AdoptedDevice`**: a device type this library models nothing for, carried whole. Identity (id, type, name, vendor, model, serial, versions), its declared parent, whether a peer proxies it, and its readings.
|
|
@@ -171,6 +205,10 @@ see the changelog, because `battery.model` changes value for existing flat users
|
|
|
171
205
|
`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
206
|
answer is `V2StatusInfo.proximity_proven`, from `GET /api/v2/status`.
|
|
173
207
|
|
|
208
|
+
Two booleans can be unknown under the parent/child schema: `SpanCircuitSnapshot.is_240v` is `None` where no pole count is published, and `SpanPanelSnapshot.lugs_at_service_entrance` is `None` while the upstream lugs declare their feed and have not
|
|
209
|
+
published it, so a consumer tests both against `None` before reading them as a boolean. `SpanPanelSnapshot.circuits` holds only circuits the panel publishes: no entry stands in for an unoccupied breaker position, and `panel_size`, with `first_position` and
|
|
210
|
+
`last_position` under the parent/child schema, says which positions exist.
|
|
211
|
+
|
|
174
212
|
## Usage
|
|
175
213
|
|
|
176
214
|
### Factory Pattern (Recommended)
|
|
@@ -195,8 +233,11 @@ async def main():
|
|
|
195
233
|
# The upstream lugs' own meter. That is grid flow only where the lugs are
|
|
196
234
|
# the utility connection point; a BESS wired ahead of them, or a panel fed
|
|
197
235
|
# by another panel, makes it this panel's feed instead. `power_flow_grid`
|
|
198
|
-
# is the site-level figure in every topology.
|
|
199
|
-
|
|
236
|
+
# is the site-level figure in every topology. `None` means the lugs have
|
|
237
|
+
# not yet published what feeds them, so neither reading is known.
|
|
238
|
+
if snapshot.lugs_at_service_entrance is None:
|
|
239
|
+
print(f"Grid power: {snapshot.power_flow_grid}W")
|
|
240
|
+
elif snapshot.lugs_at_service_entrance:
|
|
200
241
|
print(f"Grid power: {snapshot.instant_grid_power_w}W")
|
|
201
242
|
else:
|
|
202
243
|
print(f"Panel feed: {snapshot.instant_grid_power_w}W")
|
|
@@ -358,8 +399,9 @@ deliver it minutes later. `CONFIRMED` is strong evidence rather than proof: the
|
|
|
358
399
|
idempotent in its physical effect.
|
|
359
400
|
|
|
360
401
|
**A control the panel declares non-commandable raises rather than returning an outcome**, because there is no topic to publish to and so nothing to report on. `SpanPanelServerError` is raised for a relay on a circuit commissioned always-on, for the shed
|
|
361
|
-
priority of a circuit commissioned never-backup, and for a charger with no settable charge-current limit. The same facts are on the snapshot ahead of the call — `SpanCircuitSnapshot.is_user_controllable` and `.is_never_backup` — so a
|
|
362
|
-
the control only where the panel offers it will not meet this; it is the backstop for the case a setup-time gate cannot see, since re-commissioning a circuit in place changes its settability while an entity built from the earlier
|
|
402
|
+
priority of a circuit commissioned never-backup, and for a charger with no settable charge-current limit or connector lock. The same facts are on the snapshot ahead of the call — `SpanCircuitSnapshot.is_user_controllable` and `.is_never_backup` — so a
|
|
403
|
+
consumer that offers the control only where the panel offers it will not meet this; it is the backstop for the case a setup-time gate cannot see, since re-commissioning a circuit in place changes its settability while an entity built from the earlier
|
|
404
|
+
snapshot is still alive.
|
|
363
405
|
|
|
364
406
|
### Control Interception
|
|
365
407
|
|
|
@@ -505,6 +547,7 @@ All exceptions inherit from `SpanPanelError`:
|
|
|
505
547
|
| `SpanPanelValidationError` | Data validation failure |
|
|
506
548
|
| `SpanPanelAPIError` | Unexpected HTTP response from v2 endpoints |
|
|
507
549
|
| `SpanPanelServerError` | Panel answered 5xx, or answered `200` with a body that cannot be used — "not ready yet" |
|
|
550
|
+
| `SpanPanelRateLimitError` | Panel refused a registration with HTTP 429; `retry_after_s` carries the wait it asked for |
|
|
508
551
|
|
|
509
552
|
Three more are specific to the hot-loading model, and they are separate because the remedy differs:
|
|
510
553
|
|
|
@@ -584,6 +627,7 @@ One repository, three distributions. The bootstrap is at the root; each parser i
|
|
|
584
627
|
src/span_panel_api/ # distribution: span-panel-api (no parser)
|
|
585
628
|
├── __init__.py # Public API exports
|
|
586
629
|
├── _http.py # Shared httpx plumbing / client ownership rules
|
|
630
|
+
├── _ssl.py # The panel's trust anchor: SSL context, CA fingerprint, leaf-name probe
|
|
587
631
|
├── adapters.py # installed_adapter_keys(), resolve_adapter() — metadata, then lazy import
|
|
588
632
|
├── auth.py # v2 HTTP provisioning (register, cert, schema, passphrase)
|
|
589
633
|
├── const.py # Panel state constants (DSM, relay)
|
|
@@ -601,6 +645,7 @@ src/span_panel_api/ # distribution: span-panel-api (no parser)
|
|
|
601
645
|
├── client.py # SpanMqttClient (transport + control protocols)
|
|
602
646
|
├── connection.py # AsyncMqttBridge (event-loop-driven, no threads)
|
|
603
647
|
├── const.py # MQTT/Homie constants + UUID helpers
|
|
648
|
+
├── control.py # PublishOutcome, ControlCommand, ControlInterceptor
|
|
604
649
|
└── models.py # MqttClientConfig, MqttTransport
|
|
605
650
|
|
|
606
651
|
packages/schema-0/ # distribution: span-panel-api-schema-0
|
|
@@ -1,30 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: span-panel-api
|
|
3
|
-
Version: 3.8.0b1
|
|
4
|
-
Summary: A client library for SPAN Panel API
|
|
5
|
-
Project-URL: Homepage, https://github.com/SpanPanel/span-panel-api
|
|
6
|
-
Project-URL: Issues, https://github.com/SpanPanel/span-panel-api/issues
|
|
7
|
-
Author: SpanPanel
|
|
8
|
-
License-Expression: MIT
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
-
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
16
|
-
Classifier: Topic :: Home Automation
|
|
17
|
-
Classifier: Typing :: Typed
|
|
18
|
-
Requires-Python: <4.0,>=3.14
|
|
19
|
-
Requires-Dist: httpx<1.0,>=0.28.1
|
|
20
|
-
Requires-Dist: paho-mqtt<3.0.0,>=2.0.0
|
|
21
|
-
Requires-Dist: pyyaml>=6.0.0
|
|
22
|
-
Provides-Extra: schema-0
|
|
23
|
-
Requires-Dist: span-panel-api-schema-0>=1.3.0b1; extra == 'schema-0'
|
|
24
|
-
Provides-Extra: schema-1
|
|
25
|
-
Requires-Dist: span-panel-api-schema-1>=1.4.0b1; extra == 'schema-1'
|
|
26
|
-
Description-Content-Type: text/markdown
|
|
27
|
-
|
|
28
1
|
# SPAN Panel API
|
|
29
2
|
|
|
30
3
|
[](https://github.com/SpanPanel/span-panel-api/releases)
|
|
@@ -156,35 +129,42 @@ The library defines structural subtyping protocols (PEP 544). All are `runtime_c
|
|
|
156
129
|
| `CircuitControlProtocol` | Relay and shed-priority control: `set_circuit_relay`, `set_circuit_priority` |
|
|
157
130
|
| `PanelControlProtocol` | Panel-level control: `set_dominant_power_source` |
|
|
158
131
|
| `EvseControlProtocol` | Per-charger control: `set_evse_charge_limit(node_id, amps)` |
|
|
132
|
+
| `EvseLockControlProtocol` | Per-charger connector lock: `set_evse_lock(node_id, locked)` |
|
|
159
133
|
| `AdoptedControlProtocol` | Write to a settable property of a device this library models nothing for |
|
|
160
134
|
| `ControlInterceptionProtocol` | Install one veto-and-observe point for every control command: `set_control_interceptor` |
|
|
161
135
|
| `StreamingCapableProtocol` | Push-based updates: `register_snapshot_callback`, `start_streaming`, `stop_streaming` |
|
|
162
136
|
|
|
163
|
-
The first
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
137
|
+
The first six differ in subject, not just in name. `EvseControlProtocol` is separate from `PanelControlProtocol` because several chargers may be commissioned at once and every call names which one. `EvseLockControlProtocol` is separate from
|
|
138
|
+
`EvseControlProtocol` so that protocol's implementers are unchanged, and its write is authorised by the snapshot: only a charger whose `SpanEvseSnapshot.lock_control` is set can be addressed, and only with a value its `lock_state_options` lists.
|
|
139
|
+
`AdoptedControlProtocol` differs in kind: the curated setters name a control this library understands and translate or bound the value on the way out, while this one names a property by its wire address and passes the value through, because the declaration
|
|
140
|
+
is all anybody here knows about it. That write is authorised by the snapshot rather than by its arguments — the transport resolves the property against the current `adopted_devices` and refuses anything it does not find carrying a set topic, so a device
|
|
141
|
+
this library _does_ model cannot be addressed through it.
|
|
167
142
|
|
|
168
|
-
`ControlInterceptionProtocol` is separate from the
|
|
169
|
-
|
|
143
|
+
`ControlInterceptionProtocol` is separate from the control protocols rather than a member of them because adding it there would break every implementer of those protocols a second time in one release, and separate from `StreamingCapableProtocol` because a
|
|
144
|
+
transport could reasonably offer one and not the other.
|
|
170
145
|
|
|
171
146
|
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
147
|
transport-specific classes.
|
|
173
148
|
|
|
149
|
+
`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
|
|
150
|
+
site details masked, and `None` otherwise.
|
|
151
|
+
|
|
174
152
|
### Snapshots
|
|
175
153
|
|
|
176
154
|
All panel state is represented as immutable, frozen dataclasses:
|
|
177
155
|
|
|
178
156
|
- **`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,
|
|
180
|
-
`battery`, `pv`, `pv_inverters`, `evse`, `mid`, `pcs`,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
157
|
+
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, busbar current, line frequency, the rating of
|
|
158
|
+
the protection ahead of the upstream lugs, the first and last breaker positions, whether any circuit declares what it feeds or a shared meter, shed policy and forecast. It holds `circuits`, `battery`, `pv`, `pv_inverters`, `evse`, `mid`, `pcs`,
|
|
159
|
+
`adopted_devices` and `extension_properties`.
|
|
160
|
+
- **`SpanCircuitSnapshot`**: one circuit. Id, name, relay state and requester, power, energy and their update times, tabs, 240 V, breaker rating and whether it declares one, current, priority, user controllability, sheddable / never-backup / always-on
|
|
161
|
+
flags, device type, relative position, PCS management and priority, the `$target` pending state for relay and priority, nominal voltage, the breaker's protection functions, the role it declares for what it feeds, the other circuits its meter or relay is
|
|
162
|
+
shared with, and whether it is a meter outside the panel, which has no breaker space, relay or shed priority and reads import-positive.
|
|
163
|
+
- **`SpanBatterySnapshot`**: the BESS. Whether the panel declares one, SoE percentage and kWh, its own meter reading, communication state, link health, the circuit that feeds it, vendor / `model` / `part_number` / serial / version, nameplate capacity.
|
|
184
164
|
- **`SpanPVSnapshot`**: one PV inverter. Link health, vendor / `model` / serial / version, nameplate capacity (the array's DC size), feeding circuit, relative position, its wire `device_id` and its `node_id`, which is its key in `pv_inverters`.
|
|
185
165
|
`SpanPanelSnapshot.pv` is the lone inverter, or describes several together.
|
|
186
|
-
- **`SpanEvseSnapshot`**: one EV charger. `node_id`, feeding circuit, status, lock state, advertised current, link health, vendor / `model` / `part_number` / serial / version, charge-current limit, ceiling, pending target and settability,
|
|
187
|
-
limit it applies.
|
|
166
|
+
- **`SpanEvseSnapshot`**: one EV charger. `node_id`, feeding circuit, status, lock state, advertised current, link health, vendor / `model` / `part_number` / serial / version, charge-current limit, ceiling, pending target and settability, the effective
|
|
167
|
+
limit it applies, the range it declares for that limit, and where its connector lock is commanded with the values it accepts.
|
|
188
168
|
- **`SpanMidSnapshot`**: the Microgrid Interconnect Device. `node_id`, islanding state, grid state, grid-forming entity and its device name, vendor / `model` / serial / software and hardware version.
|
|
189
169
|
- **`SpanPcsSnapshot`**: the Power Control System. Enabled and active, the enforced import limit and its binding constraint, and each import constraint's limit, enablement and active state (feed, operator, off-grid, requested).
|
|
190
170
|
- **`AdoptedDevice`**: a device type this library models nothing for, carried whole. Identity (id, type, name, vendor, model, serial, versions), its declared parent, whether a peer proxies it, and its readings.
|
|
@@ -198,6 +178,10 @@ see the changelog, because `battery.model` changes value for existing flat users
|
|
|
198
178
|
`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
179
|
answer is `V2StatusInfo.proximity_proven`, from `GET /api/v2/status`.
|
|
200
180
|
|
|
181
|
+
Two booleans can be unknown under the parent/child schema: `SpanCircuitSnapshot.is_240v` is `None` where no pole count is published, and `SpanPanelSnapshot.lugs_at_service_entrance` is `None` while the upstream lugs declare their feed and have not
|
|
182
|
+
published it, so a consumer tests both against `None` before reading them as a boolean. `SpanPanelSnapshot.circuits` holds only circuits the panel publishes: no entry stands in for an unoccupied breaker position, and `panel_size`, with `first_position` and
|
|
183
|
+
`last_position` under the parent/child schema, says which positions exist.
|
|
184
|
+
|
|
201
185
|
## Usage
|
|
202
186
|
|
|
203
187
|
### Factory Pattern (Recommended)
|
|
@@ -222,8 +206,11 @@ async def main():
|
|
|
222
206
|
# The upstream lugs' own meter. That is grid flow only where the lugs are
|
|
223
207
|
# the utility connection point; a BESS wired ahead of them, or a panel fed
|
|
224
208
|
# by another panel, makes it this panel's feed instead. `power_flow_grid`
|
|
225
|
-
# is the site-level figure in every topology.
|
|
226
|
-
|
|
209
|
+
# is the site-level figure in every topology. `None` means the lugs have
|
|
210
|
+
# not yet published what feeds them, so neither reading is known.
|
|
211
|
+
if snapshot.lugs_at_service_entrance is None:
|
|
212
|
+
print(f"Grid power: {snapshot.power_flow_grid}W")
|
|
213
|
+
elif snapshot.lugs_at_service_entrance:
|
|
227
214
|
print(f"Grid power: {snapshot.instant_grid_power_w}W")
|
|
228
215
|
else:
|
|
229
216
|
print(f"Panel feed: {snapshot.instant_grid_power_w}W")
|
|
@@ -385,8 +372,9 @@ deliver it minutes later. `CONFIRMED` is strong evidence rather than proof: the
|
|
|
385
372
|
idempotent in its physical effect.
|
|
386
373
|
|
|
387
374
|
**A control the panel declares non-commandable raises rather than returning an outcome**, because there is no topic to publish to and so nothing to report on. `SpanPanelServerError` is raised for a relay on a circuit commissioned always-on, for the shed
|
|
388
|
-
priority of a circuit commissioned never-backup, and for a charger with no settable charge-current limit. The same facts are on the snapshot ahead of the call — `SpanCircuitSnapshot.is_user_controllable` and `.is_never_backup` — so a
|
|
389
|
-
the control only where the panel offers it will not meet this; it is the backstop for the case a setup-time gate cannot see, since re-commissioning a circuit in place changes its settability while an entity built from the earlier
|
|
375
|
+
priority of a circuit commissioned never-backup, and for a charger with no settable charge-current limit or connector lock. The same facts are on the snapshot ahead of the call — `SpanCircuitSnapshot.is_user_controllable` and `.is_never_backup` — so a
|
|
376
|
+
consumer that offers the control only where the panel offers it will not meet this; it is the backstop for the case a setup-time gate cannot see, since re-commissioning a circuit in place changes its settability while an entity built from the earlier
|
|
377
|
+
snapshot is still alive.
|
|
390
378
|
|
|
391
379
|
### Control Interception
|
|
392
380
|
|
|
@@ -532,6 +520,7 @@ All exceptions inherit from `SpanPanelError`:
|
|
|
532
520
|
| `SpanPanelValidationError` | Data validation failure |
|
|
533
521
|
| `SpanPanelAPIError` | Unexpected HTTP response from v2 endpoints |
|
|
534
522
|
| `SpanPanelServerError` | Panel answered 5xx, or answered `200` with a body that cannot be used — "not ready yet" |
|
|
523
|
+
| `SpanPanelRateLimitError` | Panel refused a registration with HTTP 429; `retry_after_s` carries the wait it asked for |
|
|
535
524
|
|
|
536
525
|
Three more are specific to the hot-loading model, and they are separate because the remedy differs:
|
|
537
526
|
|
|
@@ -611,6 +600,7 @@ One repository, three distributions. The bootstrap is at the root; each parser i
|
|
|
611
600
|
src/span_panel_api/ # distribution: span-panel-api (no parser)
|
|
612
601
|
├── __init__.py # Public API exports
|
|
613
602
|
├── _http.py # Shared httpx plumbing / client ownership rules
|
|
603
|
+
├── _ssl.py # The panel's trust anchor: SSL context, CA fingerprint, leaf-name probe
|
|
614
604
|
├── adapters.py # installed_adapter_keys(), resolve_adapter() — metadata, then lazy import
|
|
615
605
|
├── auth.py # v2 HTTP provisioning (register, cert, schema, passphrase)
|
|
616
606
|
├── const.py # Panel state constants (DSM, relay)
|
|
@@ -628,6 +618,7 @@ src/span_panel_api/ # distribution: span-panel-api (no parser)
|
|
|
628
618
|
├── client.py # SpanMqttClient (transport + control protocols)
|
|
629
619
|
├── connection.py # AsyncMqttBridge (event-loop-driven, no threads)
|
|
630
620
|
├── const.py # MQTT/Homie constants + UUID helpers
|
|
621
|
+
├── control.py # PublishOutcome, ControlCommand, ControlInterceptor
|
|
631
622
|
└── models.py # MqttClientConfig, MqttTransport
|
|
632
623
|
|
|
633
624
|
packages/schema-0/ # distribution: span-panel-api-schema-0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "span-panel-api"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.9.0b1"
|
|
4
4
|
description = "A client library for SPAN Panel API"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "SpanPanel"}
|
|
@@ -62,16 +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
|
-
# Raised to 1.3.
|
|
65
|
+
# Raised to 1.3.0 for 3.8.0: that adapter reports the unknown values this
|
|
66
66
|
# bootstrap's snapshot fields now accept, and stops synthesising unoccupied positions.
|
|
67
|
-
schema-0 = ["span-panel-api-schema-0>=1.3.
|
|
67
|
+
schema-0 = ["span-panel-api-schema-0>=1.3.0"]
|
|
68
68
|
# Raised to 1.2.1 for 3.6.1: that adapter builds the `pv` that `SpanPanelSnapshot.pv` now documents.
|
|
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
|
-
# Raised to 1.
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
schema-1 = ["span-panel-api-schema-1>=1.4.
|
|
71
|
+
# Raised to 1.4.0 for 3.8.0: for the same reason as schema-0's, and because that
|
|
72
|
+
# adapter fills the snapshot fields this bootstrap adds and exports the masked
|
|
73
|
+
# device tree.
|
|
74
|
+
schema-1 = ["span-panel-api-schema-1>=1.4.0"]
|
|
75
75
|
|
|
76
76
|
[project.urls]
|
|
77
77
|
Homepage = "https://github.com/SpanPanel/span-panel-api"
|
|
@@ -95,7 +95,7 @@ dev = [
|
|
|
95
95
|
# known. Dependabot raises the bump on its own; the bump PR re-runs
|
|
96
96
|
# `scripts/capture_parent_child_reference.py` and the suite then says whether
|
|
97
97
|
# the wire moved.
|
|
98
|
-
"ebus-panel-sim==0.
|
|
98
|
+
"ebus-panel-sim==0.9.0",
|
|
99
99
|
"pytest>=9.0.2",
|
|
100
100
|
"pytest-asyncio>=1.3.0",
|
|
101
101
|
"pytest-cov",
|
|
@@ -255,6 +255,12 @@ explicit_package_bases = true
|
|
|
255
255
|
# `tests/` on `sys.path`; the support packages the mypy hook checks resolve the same
|
|
256
256
|
# way here.
|
|
257
257
|
mypy_path = "src:tests"
|
|
258
|
+
# paho ships `py.typed` but leaves its topic matcher unannotated, so every call
|
|
259
|
+
# into it would be an untyped call. The schema-1 route table resolves what the
|
|
260
|
+
# matcher yields through a typed dict at once, which keeps its `Any` from going
|
|
261
|
+
# further; `warn_return_any` does not police that for you (a union or a
|
|
262
|
+
# `yield from` passes it), so keep it to that shape.
|
|
263
|
+
untyped_calls_exclude = ["paho.mqtt.matcher"]
|
|
258
264
|
# Excluded by name for the reason `[tool.ruff]` gives above: the capture script is
|
|
259
265
|
# checked like the library, and the hand-run tools written before that was expected
|
|
260
266
|
# are not.
|
|
@@ -53,6 +53,9 @@ from .models import (
|
|
|
53
53
|
FeedsRole,
|
|
54
54
|
FieldMetadata,
|
|
55
55
|
HomieSchemaTypes,
|
|
56
|
+
MaskedTree,
|
|
57
|
+
MaskedTreeDevice,
|
|
58
|
+
MaskedTreeMetadata,
|
|
56
59
|
PassphraseRotation,
|
|
57
60
|
ProtectionFunction,
|
|
58
61
|
SpanBatterySnapshot,
|
|
@@ -62,6 +65,7 @@ from .models import (
|
|
|
62
65
|
SpanPanelSnapshot,
|
|
63
66
|
SpanPcsSnapshot,
|
|
64
67
|
SpanPVSnapshot,
|
|
68
|
+
UndescribedDevice,
|
|
65
69
|
V2AuthResponse,
|
|
66
70
|
V2HomieSchema,
|
|
67
71
|
V2StatusInfo,
|
|
@@ -95,6 +99,7 @@ from .protocol import (
|
|
|
95
99
|
PanelControlProtocol,
|
|
96
100
|
SpanPanelClientProtocol,
|
|
97
101
|
StreamingCapableProtocol,
|
|
102
|
+
TreeExportingAdapter,
|
|
98
103
|
)
|
|
99
104
|
|
|
100
105
|
__version__ = _pkg_version("span-panel-api")
|
|
@@ -147,6 +152,15 @@ __all__ = [ # noqa: RUF022
|
|
|
147
152
|
"ControlTarget",
|
|
148
153
|
"ExtensionProperty",
|
|
149
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",
|
|
150
164
|
# Snapshots
|
|
151
165
|
"SpanBatterySnapshot",
|
|
152
166
|
"SpanCircuitSnapshot",
|
|
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
from collections.abc import Mapping
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
14
|
import re
|
|
15
|
-
from typing import Final, Literal
|
|
15
|
+
from typing import Final, Literal, NotRequired, TypedDict
|
|
16
16
|
|
|
17
17
|
# Homie schema type: {type_name: {property_name: {attribute: value}}}
|
|
18
18
|
# Values are heterogeneous JSON (str, int, bool, nested dicts).
|
|
@@ -64,6 +64,13 @@ class SpanCircuitSnapshot:
|
|
|
64
64
|
is_240v: bool | None = False
|
|
65
65
|
current_a: float | None = None
|
|
66
66
|
breaker_rating_a: float | None = None
|
|
67
|
+
# Whether the circuit's description declares a breaker rating, as distinct
|
|
68
|
+
# from whether its value has arrived. `True`: declared, so `breaker_rating_a`
|
|
69
|
+
# is `None` only until the retained value lands. `False`: the panel
|
|
70
|
+
# publishes no rating for this circuit. `None`: the adapter does not say
|
|
71
|
+
# (flat, REST, a hand-built snapshot) and the consumer must treat it as
|
|
72
|
+
# not known, never as `False`.
|
|
73
|
+
breaker_rating_declared: bool | None = None
|
|
67
74
|
always_on: bool = False # v2 new: circuit/always-on
|
|
68
75
|
relay_requester: str = "UNKNOWN" # v2 new: circuit/relay-requester
|
|
69
76
|
# Epoch seconds at which the reading last changed, as received over MQTT.
|
|
@@ -465,6 +472,36 @@ class SpanEvseSnapshot:
|
|
|
465
472
|
set topic when it is not.
|
|
466
473
|
"""
|
|
467
474
|
|
|
475
|
+
charge_current_limit_minimum_a: int | None = None
|
|
476
|
+
"""The lowest charge-current limit the charger declares a user may set, in amps. v1.0 only.
|
|
477
|
+
|
|
478
|
+
The minimum of the `<min>:<max>` range the limit's declaration states in
|
|
479
|
+
`$format`, from the same resolution as `charge_current_limit_a`; firmware
|
|
480
|
+
r202639 declares `6:<commissioned maximum>`. A write below it is refused
|
|
481
|
+
rather than published, so a control offers nothing under it.
|
|
482
|
+
|
|
483
|
+
A declaration, not a reading: it is reported as soon as the charger
|
|
484
|
+
describes itself, and whether or not the limit is settable, which
|
|
485
|
+
`charge_current_limit_settable` answers. `None` where the charger declares
|
|
486
|
+
no limit, no `$format`, or the format leaves this side open, and where the
|
|
487
|
+
format cannot be read as an integer range (not `integer`, not `<min>:<max>`,
|
|
488
|
+
a bound that is not a plain integer, or a minimum above the maximum). `None`
|
|
489
|
+
on the flat schema, which declares no range.
|
|
490
|
+
"""
|
|
491
|
+
|
|
492
|
+
charge_current_limit_maximum_a: int | None = None
|
|
493
|
+
"""The highest charge-current limit the charger declares a user may set, in amps. v1.0 only.
|
|
494
|
+
|
|
495
|
+
The maximum of the same declared range, `None` on the same terms. A write
|
|
496
|
+
above it is refused rather than published.
|
|
497
|
+
|
|
498
|
+
**Not `charge_current_ceiling_a`.** That is the commissioned ceiling, a
|
|
499
|
+
published value; this is the declared range, part of the limit's
|
|
500
|
+
description. Every r202639 capture declares a maximum equal to the ceiling
|
|
501
|
+
it publishes, but they are separate facts, and a write must satisfy both: a
|
|
502
|
+
control's upper bound is the lower of the two where both are present.
|
|
503
|
+
"""
|
|
504
|
+
|
|
468
505
|
lock_control: ControlTarget | None = None
|
|
469
506
|
"""Where the charger's connector lock is commanded, and which property reports it. v1.0 only.
|
|
470
507
|
|
|
@@ -1310,6 +1347,17 @@ class SpanPanelSnapshot:
|
|
|
1310
1347
|
which declares no roles.
|
|
1311
1348
|
"""
|
|
1312
1349
|
|
|
1350
|
+
publishes_shared_meters: bool = False
|
|
1351
|
+
"""Whether any circuit declares `meter/shared-with-device-ids`.
|
|
1352
|
+
|
|
1353
|
+
The declaration decides, not the value: a panel model that describes its
|
|
1354
|
+
multi-load breakers declares the list on every circuit, a standard breaker
|
|
1355
|
+
included, and publishes a value only where a meter is shared. Such a panel
|
|
1356
|
+
publishes each circuit of a multi-load breaker as its own device with its
|
|
1357
|
+
own rating. `False` where no circuit declares the list, as on MAIN 32, and
|
|
1358
|
+
on the flat schema, which declares none.
|
|
1359
|
+
"""
|
|
1360
|
+
|
|
1313
1361
|
# `shed/policy`, v1.0 only: how the panel decides what to shed, and the two
|
|
1314
1362
|
# SoC thresholds that make its behaviour predictable. The wire carries one
|
|
1315
1363
|
# `json` document; these are the parsed answer plus the document itself.
|
|
@@ -1506,3 +1554,65 @@ class SpanPanelSnapshot:
|
|
|
1506
1554
|
the feeding device is another panel with its own tree, not a child of this
|
|
1507
1555
|
one.
|
|
1508
1556
|
"""
|
|
1557
|
+
|
|
1558
|
+
|
|
1559
|
+
class MaskedTreeMetadata(TypedDict):
|
|
1560
|
+
"""What produced a `MaskedTree` and whether it can be trusted to be whole.
|
|
1561
|
+
|
|
1562
|
+
`masked` is always `True`: the tree never carries a serial, a credential or
|
|
1563
|
+
an address, and the marker lets whoever reads a forwarded diagnostic see that
|
|
1564
|
+
without inspecting the values. `tree_complete` is `False` when the panel had
|
|
1565
|
+
not finished publishing its tree, so what follows is a partial reading.
|
|
1566
|
+
"""
|
|
1567
|
+
|
|
1568
|
+
schema: Literal["tree-v1"]
|
|
1569
|
+
masked: Literal[True]
|
|
1570
|
+
producer: str
|
|
1571
|
+
tree_complete: bool
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
class MaskedTreeDevice(TypedDict):
|
|
1575
|
+
"""One described device in a `MaskedTree`, as a consumer forwards it in diagnostics.
|
|
1576
|
+
|
|
1577
|
+
Values are keyed `"<node>/<property>"` and kept as the wire strings the
|
|
1578
|
+
panel published, masked where they identify the device or the site.
|
|
1579
|
+
`numeric_properties` holds the values of declared `integer` and `float`
|
|
1580
|
+
properties, `properties` the values of every other declared property,
|
|
1581
|
+
`undeclared_properties` values the panel published that its own description
|
|
1582
|
+
does not declare, and `targets` its `$target` values. A declared property
|
|
1583
|
+
with no value has no key: never `None`, and never a `""` the panel did not
|
|
1584
|
+
send, because a placeholder would read as a reading. A `""` the panel did
|
|
1585
|
+
publish is kept as `""`.
|
|
1586
|
+
"""
|
|
1587
|
+
|
|
1588
|
+
description: dict[str, object]
|
|
1589
|
+
properties: dict[str, str]
|
|
1590
|
+
numeric_properties: dict[str, str]
|
|
1591
|
+
targets: dict[str, str]
|
|
1592
|
+
undeclared_properties: dict[str, str]
|
|
1593
|
+
state: NotRequired[str]
|
|
1594
|
+
|
|
1595
|
+
|
|
1596
|
+
class UndescribedDevice(TypedDict):
|
|
1597
|
+
"""A device the panel published values for but never described.
|
|
1598
|
+
|
|
1599
|
+
With no `$description` there is nothing to say which node a property belongs
|
|
1600
|
+
to, so `values` keeps each as `"<node>/<property>"` -> wire string, unsplit.
|
|
1601
|
+
"""
|
|
1602
|
+
|
|
1603
|
+
values: dict[str, str]
|
|
1604
|
+
targets: dict[str, str]
|
|
1605
|
+
state: NotRequired[str]
|
|
1606
|
+
|
|
1607
|
+
|
|
1608
|
+
class MaskedTree(TypedDict):
|
|
1609
|
+
"""The masked device tree a consumer forwards in diagnostics.
|
|
1610
|
+
|
|
1611
|
+
Produced by an adapter that offers `TreeExportingAdapter`. Described devices
|
|
1612
|
+
and undescribed ones are kept apart so a reader never mistakes one for the
|
|
1613
|
+
other.
|
|
1614
|
+
"""
|
|
1615
|
+
|
|
1616
|
+
metadata: MaskedTreeMetadata
|
|
1617
|
+
devices: dict[str, MaskedTreeDevice]
|
|
1618
|
+
undescribed_devices: dict[str, UndescribedDevice]
|