span-panel-api 3.8.0b2__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.0b2 → span_panel_api-3.9.0b1}/CHANGELOG.md +13 -6
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/PKG-INFO +35 -20
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/README.md +32 -17
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/pyproject.toml +7 -8
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/models.py +18 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/adapter_fidelity/rules.py +8 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/adapter_fidelity/rules_captured.py +4 -2
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_circuits.py +56 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_shared_meters.py +41 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_shared_meters_captures.py +10 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/.gitignore +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/LICENSE +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/__init__.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/_http.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/_ssl.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/adapters.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/auth.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/const.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/detection.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/dispatch.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/exceptions.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/factory.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/mqtt/__init__.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/mqtt/async_client.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/mqtt/client.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/mqtt/connection.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/mqtt/const.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/mqtt/control.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/mqtt/models.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/phase_validation.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/protocol.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/py.typed +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/src/span_panel_api/schema_drift.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/adapter_fidelity/__init__.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/adapter_fidelity/harness.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/adapter_fidelity/test_capture_fidelity.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/adapter_fidelity/test_harness.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/adapter_fidelity/test_main32_fidelity.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/conftest.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/expected_failures.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/captures/LICENSE +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/captures/README.md +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/captures/SHA256SUMS +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/captures/r202639-a-tree-v1.json +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/captures/r202639-b-tree-v1.json +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/captures/r202639-c-tree-v1.json +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/captures/r202639-d-tree-v1.json +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/captures/r202639-e-tree-v1.json +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/distribution-enclosure-simulator.LICENSE +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/flat_wire.json +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/main32-tree-v1.README.md +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/main32-tree-v1.json +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/main32_r202639-tree-v1.README.md +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/main32_r202639-tree-v1.json +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/other_model_tree.json +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/panelbench_unvalued_by_both.json +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/panelbench_wire.json +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/v2/README.md +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/fixtures/v2/status.json +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/reference_payloads/README.md +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/reference_payloads/__init__.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/reference_payloads/bootstrap.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/reference_payloads/captures.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/reference_payloads/reidentify.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/reference_payloads/schema_one.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/reference_payloads/synthetic_trees.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/simulation_fixtures/circuits.response.txt +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/simulation_fixtures/panel.response.txt +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/simulation_fixtures/soe.response.txt +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/simulation_fixtures/status.response.txt +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_absent_readings_are_not_zero.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_accumulator.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_adapters_discovery.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_adopted_control.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_adoption.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_async_mqtt_client.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_auth_and_homie_helpers.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_auth_redaction.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_ca_pinning.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_capture_replay.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_captures_unchanged.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_catalog_divergence.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_circuit_fed_battery_captures.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_control_interceptor.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_declared_unvalued_captures.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_detection_auth.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_evse_lock_captures.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_evse_lock_control.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_exceptions.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_expected_failures_registry.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_factory_dispatch.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_feeds_role_captures.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_field_metadata.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_homie_schema_hash.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_https_transport.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_leaf_name_mismatch.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_live_flat_differential.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_mqtt_bridge.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_mqtt_client_connection.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_mqtt_connect_flow.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_mqtt_debounce.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_mqtt_homie.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_packaging.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_panel_properties_captures.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_phase_validation_configs.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_phase_validation_errors.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_plaintext_warning.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_positions_captures.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_protocol_conformance.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_protocol_models.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_public_api_unchanged.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_publish_outcome.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_redispatch_on_reconnect.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_reference_tree_values.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_register_passphrase_unavailable.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_register_rate_limit.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_register_rate_limit_companions.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_rest_transport_contract.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_fetch_transport_split.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_generation_cross_check.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_migration_delta.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_acceptance_companions.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_adapter.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_charge_limit.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_circuit_fed_battery.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_cleared_retained.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_conformance.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_connection_health.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_control_refusal.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_declared_unvalued.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_device_display_name.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_devices.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_discovery.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_evse_lock.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_extension.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_feeds_role.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_firmware.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_other_models.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_panel.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_panel_properties.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_pcs.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_positions.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_service_entrance.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_shed_forecast.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_sheddable.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_snapshot.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_space_less_meters.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_transport.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_tree_export.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_one_undeclared_devices.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_provenance.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_schema_zero_adapter.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_shared_http_client.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_snapshot_readiness_gate.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_space_less_meters_captures.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_ssl_context.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_synthetic_trees.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_tree_export_protocol.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/test_v2_status_parser.py +0 -0
- {span_panel_api-3.8.0b2 → span_panel_api-3.9.0b1}/tests/tls_fixtures.py +0 -0
|
@@ -7,9 +7,16 @@ 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
|
|
|
@@ -20,8 +27,8 @@ Snapshot fields that held a fabricated constant can now be unknown, and with `sp
|
|
|
20
27
|
|
|
21
28
|
### Changed
|
|
22
29
|
|
|
23
|
-
- **BREAKING FOR CONSUMERS: `SpanPanelSnapshot.proximity_proven` becomes `bool | None`**, and with `span-panel-api-schema-0` and `span-panel-api-schema-1` 1.
|
|
24
|
-
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`.
|
|
25
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`.
|
|
26
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
|
|
27
34
|
panel, where they were `0`.
|
|
@@ -32,8 +39,8 @@ Snapshot fields that held a fabricated constant can now be unknown, and with `sp
|
|
|
32
39
|
|
|
33
40
|
### Removed
|
|
34
41
|
|
|
35
|
-
- **BREAKING: with `span-panel-api-schema-0` and `span-panel-api-schema-1` 1.
|
|
36
|
-
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.
|
|
37
44
|
|
|
38
45
|
### Fixed
|
|
39
46
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: span-panel-api
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.9.0b1
|
|
4
4
|
Summary: A client library for SPAN Panel API
|
|
5
5
|
Project-URL: Homepage, https://github.com/SpanPanel/span-panel-api
|
|
6
6
|
Project-URL: Issues, https://github.com/SpanPanel/span-panel-api/issues
|
|
@@ -20,9 +20,9 @@ Requires-Dist: httpx<1.0,>=0.28.1
|
|
|
20
20
|
Requires-Dist: paho-mqtt<3.0.0,>=2.0.0
|
|
21
21
|
Requires-Dist: pyyaml>=6.0.0
|
|
22
22
|
Provides-Extra: schema-0
|
|
23
|
-
Requires-Dist: span-panel-api-schema-0>=1.3.
|
|
23
|
+
Requires-Dist: span-panel-api-schema-0>=1.3.0; extra == 'schema-0'
|
|
24
24
|
Provides-Extra: schema-1
|
|
25
|
-
Requires-Dist: span-panel-api-schema-1>=1.4.
|
|
25
|
+
Requires-Dist: span-panel-api-schema-1>=1.4.0; extra == 'schema-1'
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
|
|
28
28
|
# SPAN Panel API
|
|
@@ -156,17 +156,19 @@ The library defines structural subtyping protocols (PEP 544). All are `runtime_c
|
|
|
156
156
|
| `CircuitControlProtocol` | Relay and shed-priority control: `set_circuit_relay`, `set_circuit_priority` |
|
|
157
157
|
| `PanelControlProtocol` | Panel-level control: `set_dominant_power_source` |
|
|
158
158
|
| `EvseControlProtocol` | Per-charger control: `set_evse_charge_limit(node_id, amps)` |
|
|
159
|
+
| `EvseLockControlProtocol` | Per-charger connector lock: `set_evse_lock(node_id, locked)` |
|
|
159
160
|
| `AdoptedControlProtocol` | Write to a settable property of a device this library models nothing for |
|
|
160
161
|
| `ControlInterceptionProtocol` | Install one veto-and-observe point for every control command: `set_control_interceptor` |
|
|
161
162
|
| `StreamingCapableProtocol` | Push-based updates: `register_snapshot_callback`, `start_streaming`, `stop_streaming` |
|
|
162
163
|
|
|
163
|
-
The first
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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.
|
|
167
169
|
|
|
168
|
-
`ControlInterceptionProtocol` is separate from the
|
|
169
|
-
|
|
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.
|
|
170
172
|
|
|
171
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
|
|
172
174
|
transport-specific classes.
|
|
@@ -179,15 +181,17 @@ site details masked, and `None` otherwise.
|
|
|
179
181
|
All panel state is represented as immutable, frozen dataclasses:
|
|
180
182
|
|
|
181
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,
|
|
182
|
-
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,
|
|
183
|
-
`battery`, `pv`, `pv_inverters`, `evse`, `mid`, `pcs`,
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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.
|
|
187
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`.
|
|
188
192
|
`SpanPanelSnapshot.pv` is the lone inverter, or describes several together.
|
|
189
|
-
- **`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,
|
|
190
|
-
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.
|
|
191
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.
|
|
192
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).
|
|
193
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.
|
|
@@ -201,6 +205,10 @@ see the changelog, because `battery.model` changes value for existing flat users
|
|
|
201
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
|
|
202
206
|
answer is `V2StatusInfo.proximity_proven`, from `GET /api/v2/status`.
|
|
203
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
|
+
|
|
204
212
|
## Usage
|
|
205
213
|
|
|
206
214
|
### Factory Pattern (Recommended)
|
|
@@ -225,8 +233,11 @@ async def main():
|
|
|
225
233
|
# The upstream lugs' own meter. That is grid flow only where the lugs are
|
|
226
234
|
# the utility connection point; a BESS wired ahead of them, or a panel fed
|
|
227
235
|
# by another panel, makes it this panel's feed instead. `power_flow_grid`
|
|
228
|
-
# is the site-level figure in every topology.
|
|
229
|
-
|
|
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:
|
|
230
241
|
print(f"Grid power: {snapshot.instant_grid_power_w}W")
|
|
231
242
|
else:
|
|
232
243
|
print(f"Panel feed: {snapshot.instant_grid_power_w}W")
|
|
@@ -388,8 +399,9 @@ deliver it minutes later. `CONFIRMED` is strong evidence rather than proof: the
|
|
|
388
399
|
idempotent in its physical effect.
|
|
389
400
|
|
|
390
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
|
|
391
|
-
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
|
|
392
|
-
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.
|
|
393
405
|
|
|
394
406
|
### Control Interception
|
|
395
407
|
|
|
@@ -535,6 +547,7 @@ All exceptions inherit from `SpanPanelError`:
|
|
|
535
547
|
| `SpanPanelValidationError` | Data validation failure |
|
|
536
548
|
| `SpanPanelAPIError` | Unexpected HTTP response from v2 endpoints |
|
|
537
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 |
|
|
538
551
|
|
|
539
552
|
Three more are specific to the hot-loading model, and they are separate because the remedy differs:
|
|
540
553
|
|
|
@@ -614,6 +627,7 @@ One repository, three distributions. The bootstrap is at the root; each parser i
|
|
|
614
627
|
src/span_panel_api/ # distribution: span-panel-api (no parser)
|
|
615
628
|
├── __init__.py # Public API exports
|
|
616
629
|
├── _http.py # Shared httpx plumbing / client ownership rules
|
|
630
|
+
├── _ssl.py # The panel's trust anchor: SSL context, CA fingerprint, leaf-name probe
|
|
617
631
|
├── adapters.py # installed_adapter_keys(), resolve_adapter() — metadata, then lazy import
|
|
618
632
|
├── auth.py # v2 HTTP provisioning (register, cert, schema, passphrase)
|
|
619
633
|
├── const.py # Panel state constants (DSM, relay)
|
|
@@ -631,6 +645,7 @@ src/span_panel_api/ # distribution: span-panel-api (no parser)
|
|
|
631
645
|
├── client.py # SpanMqttClient (transport + control protocols)
|
|
632
646
|
├── connection.py # AsyncMqttBridge (event-loop-driven, no threads)
|
|
633
647
|
├── const.py # MQTT/Homie constants + UUID helpers
|
|
648
|
+
├── control.py # PublishOutcome, ControlCommand, ControlInterceptor
|
|
634
649
|
└── models.py # MqttClientConfig, MqttTransport
|
|
635
650
|
|
|
636
651
|
packages/schema-0/ # distribution: span-panel-api-schema-0
|
|
@@ -129,17 +129,19 @@ The library defines structural subtyping protocols (PEP 544). All are `runtime_c
|
|
|
129
129
|
| `CircuitControlProtocol` | Relay and shed-priority control: `set_circuit_relay`, `set_circuit_priority` |
|
|
130
130
|
| `PanelControlProtocol` | Panel-level control: `set_dominant_power_source` |
|
|
131
131
|
| `EvseControlProtocol` | Per-charger control: `set_evse_charge_limit(node_id, amps)` |
|
|
132
|
+
| `EvseLockControlProtocol` | Per-charger connector lock: `set_evse_lock(node_id, locked)` |
|
|
132
133
|
| `AdoptedControlProtocol` | Write to a settable property of a device this library models nothing for |
|
|
133
134
|
| `ControlInterceptionProtocol` | Install one veto-and-observe point for every control command: `set_control_interceptor` |
|
|
134
135
|
| `StreamingCapableProtocol` | Push-based updates: `register_snapshot_callback`, `start_streaming`, `stop_streaming` |
|
|
135
136
|
|
|
136
|
-
The first
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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.
|
|
140
142
|
|
|
141
|
-
`ControlInterceptionProtocol` is separate from the
|
|
142
|
-
|
|
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.
|
|
143
145
|
|
|
144
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
|
|
145
147
|
transport-specific classes.
|
|
@@ -152,15 +154,17 @@ site details masked, and `None` otherwise.
|
|
|
152
154
|
All panel state is represented as immutable, frozen dataclasses:
|
|
153
155
|
|
|
154
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,
|
|
155
|
-
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,
|
|
156
|
-
`battery`, `pv`, `pv_inverters`, `evse`, `mid`, `pcs`,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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.
|
|
160
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`.
|
|
161
165
|
`SpanPanelSnapshot.pv` is the lone inverter, or describes several together.
|
|
162
|
-
- **`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,
|
|
163
|
-
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.
|
|
164
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.
|
|
165
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).
|
|
166
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.
|
|
@@ -174,6 +178,10 @@ see the changelog, because `battery.model` changes value for existing flat users
|
|
|
174
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
|
|
175
179
|
answer is `V2StatusInfo.proximity_proven`, from `GET /api/v2/status`.
|
|
176
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
|
+
|
|
177
185
|
## Usage
|
|
178
186
|
|
|
179
187
|
### Factory Pattern (Recommended)
|
|
@@ -198,8 +206,11 @@ async def main():
|
|
|
198
206
|
# The upstream lugs' own meter. That is grid flow only where the lugs are
|
|
199
207
|
# the utility connection point; a BESS wired ahead of them, or a panel fed
|
|
200
208
|
# by another panel, makes it this panel's feed instead. `power_flow_grid`
|
|
201
|
-
# is the site-level figure in every topology.
|
|
202
|
-
|
|
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:
|
|
203
214
|
print(f"Grid power: {snapshot.instant_grid_power_w}W")
|
|
204
215
|
else:
|
|
205
216
|
print(f"Panel feed: {snapshot.instant_grid_power_w}W")
|
|
@@ -361,8 +372,9 @@ deliver it minutes later. `CONFIRMED` is strong evidence rather than proof: the
|
|
|
361
372
|
idempotent in its physical effect.
|
|
362
373
|
|
|
363
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
|
|
364
|
-
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
|
|
365
|
-
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.
|
|
366
378
|
|
|
367
379
|
### Control Interception
|
|
368
380
|
|
|
@@ -508,6 +520,7 @@ All exceptions inherit from `SpanPanelError`:
|
|
|
508
520
|
| `SpanPanelValidationError` | Data validation failure |
|
|
509
521
|
| `SpanPanelAPIError` | Unexpected HTTP response from v2 endpoints |
|
|
510
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 |
|
|
511
524
|
|
|
512
525
|
Three more are specific to the hot-loading model, and they are separate because the remedy differs:
|
|
513
526
|
|
|
@@ -587,6 +600,7 @@ One repository, three distributions. The bootstrap is at the root; each parser i
|
|
|
587
600
|
src/span_panel_api/ # distribution: span-panel-api (no parser)
|
|
588
601
|
├── __init__.py # Public API exports
|
|
589
602
|
├── _http.py # Shared httpx plumbing / client ownership rules
|
|
603
|
+
├── _ssl.py # The panel's trust anchor: SSL context, CA fingerprint, leaf-name probe
|
|
590
604
|
├── adapters.py # installed_adapter_keys(), resolve_adapter() — metadata, then lazy import
|
|
591
605
|
├── auth.py # v2 HTTP provisioning (register, cert, schema, passphrase)
|
|
592
606
|
├── const.py # Panel state constants (DSM, relay)
|
|
@@ -604,6 +618,7 @@ src/span_panel_api/ # distribution: span-panel-api (no parser)
|
|
|
604
618
|
├── client.py # SpanMqttClient (transport + control protocols)
|
|
605
619
|
├── connection.py # AsyncMqttBridge (event-loop-driven, no threads)
|
|
606
620
|
├── const.py # MQTT/Homie constants + UUID helpers
|
|
621
|
+
├── control.py # PublishOutcome, ControlCommand, ControlInterceptor
|
|
607
622
|
└── models.py # MqttClientConfig, MqttTransport
|
|
608
623
|
|
|
609
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,17 +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
|
-
|
|
75
|
-
schema-1 = ["span-panel-api-schema-1>=1.4.0b4"]
|
|
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"]
|
|
76
75
|
|
|
77
76
|
[project.urls]
|
|
78
77
|
Homepage = "https://github.com/SpanPanel/span-panel-api"
|
|
@@ -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.
|
|
@@ -1340,6 +1347,17 @@ class SpanPanelSnapshot:
|
|
|
1340
1347
|
which declares no roles.
|
|
1341
1348
|
"""
|
|
1342
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
|
+
|
|
1343
1361
|
# `shed/policy`, v1.0 only: how the panel decides what to shed, and the two
|
|
1344
1362
|
# SoC thresholds that make its behaviour predictable. The wire carries one
|
|
1345
1363
|
# `json` document; these are the parsed answer plus the document itself.
|
|
@@ -500,10 +500,18 @@ _DERIVATIONS: Final = (
|
|
|
500
500
|
"circuit.measures_outside_panel",
|
|
501
501
|
"circuits.declares_spaces: whether the description declares info/spaces, not a value",
|
|
502
502
|
),
|
|
503
|
+
Derivation(
|
|
504
|
+
"circuit.breaker_rating_declared",
|
|
505
|
+
"circuits.build_circuit: whether the description declares breaker/rating, not a value",
|
|
506
|
+
),
|
|
503
507
|
Derivation(
|
|
504
508
|
"panel.publishes_solar_roles",
|
|
505
509
|
"snapshot.build_snapshot: whether any circuit's description declares connection/feeds-role, not a value",
|
|
506
510
|
),
|
|
511
|
+
Derivation(
|
|
512
|
+
"panel.publishes_shared_meters",
|
|
513
|
+
"snapshot.build_snapshot: whether any circuit's description declares meter/shared-with-device-ids, not a value",
|
|
514
|
+
),
|
|
507
515
|
Derivation("pv.device_id", "devices.build_pv: the inverter's device id"),
|
|
508
516
|
Derivation("pv.node_id", "devices.pv_inverter_key: the feeding circuit's id, else the inverter's device id"),
|
|
509
517
|
Derivation("pv_inverter.device_id", "devices.build_pv: the inverter's device id"),
|
|
@@ -9,7 +9,7 @@ here rather than inherited.
|
|
|
9
9
|
Four of them are exceptions rather than derivations, and say so: the snapshot
|
|
10
10
|
holds `"UNKNOWN"` in a `str` field where the panel publishes nothing for it.
|
|
11
11
|
That is the field's long-standing absence value, not a reading; whether those
|
|
12
|
-
fields become optional is a type question deferred past 3.8.
|
|
12
|
+
fields become optional is a type question deferred past 3.8.0.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
@@ -22,6 +22,7 @@ from .rules import DROPS, MAIN32_RULES, TRANSFORMS
|
|
|
22
22
|
_SHARED: Final = frozenset(
|
|
23
23
|
{
|
|
24
24
|
"battery.present",
|
|
25
|
+
"circuit.breaker_rating_declared",
|
|
25
26
|
"circuit.circuit_id",
|
|
26
27
|
"circuit.device_type",
|
|
27
28
|
"circuit.is_never_backup",
|
|
@@ -29,6 +30,7 @@ _SHARED: Final = frozenset(
|
|
|
29
30
|
"circuit.measures_outside_panel",
|
|
30
31
|
"ctl.dps_target",
|
|
31
32
|
"ctl.priority_target",
|
|
33
|
+
"panel.publishes_shared_meters",
|
|
32
34
|
"panel.publishes_solar_roles",
|
|
33
35
|
}
|
|
34
36
|
)
|
|
@@ -38,7 +40,7 @@ _NO_RELAY = (
|
|
|
38
40
|
"circuits.build_circuit: a device that declares no switch node has no relay, "
|
|
39
41
|
"so it is not controllable, not sheddable and not always on"
|
|
40
42
|
)
|
|
41
|
-
_STR_SENTINEL = "pre-existing str sentinel; type question deferred past 3.8.
|
|
43
|
+
_STR_SENTINEL = "pre-existing str sentinel; type question deferred past 3.8.0"
|
|
42
44
|
|
|
43
45
|
_DERIVATIONS: Final = (
|
|
44
46
|
*(derivation for derivation in MAIN32_RULES.derivations if derivation.family in _SHARED),
|
|
@@ -15,13 +15,18 @@ expecting the capture to carry every enum member.
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import json
|
|
18
|
+
from dataclasses import replace
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
|
|
20
21
|
import pytest
|
|
21
22
|
|
|
22
23
|
from ebus_sdk.homie import DiscoveredDevice
|
|
23
24
|
|
|
25
|
+
from conftest import flat_schema
|
|
24
26
|
from reference_payloads.schema_one import device_from_topics, parent_child_tree
|
|
27
|
+
from reference_payloads.synthetic_trees import SyntheticDevice, discovered, hosted_circuit, space_less_meter, without_values
|
|
28
|
+
from span_panel_api_schema_0 import SchemaZeroAdapter
|
|
29
|
+
from span_panel_api_schema_0.const import TYPE_CIRCUIT
|
|
25
30
|
from span_panel_api_schema_1.circuits import build_circuit
|
|
26
31
|
|
|
27
32
|
_TREE = parent_child_tree()
|
|
@@ -116,6 +121,57 @@ def test_breaker_rating_and_current(kitchen: DiscoveredDevice) -> None:
|
|
|
116
121
|
assert circuit.current_a == pytest.approx(1.00833, rel=1e-4)
|
|
117
122
|
|
|
118
123
|
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Whether the breaker rating is declared
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _undeclared_rating(device: SyntheticDevice) -> SyntheticDevice:
|
|
130
|
+
"""The same circuit, its `breaker` node kept but `rating` neither declared nor valued."""
|
|
131
|
+
breaker = {prop: declaration for prop, declaration in device.nodes["breaker"].items() if prop != "rating"}
|
|
132
|
+
values = {path: value for path, value in device.values.items() if path != "breaker/rating"}
|
|
133
|
+
return replace(device, nodes={**device.nodes, "breaker": breaker}, values=values)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_rating_declared_and_valued() -> None:
|
|
137
|
+
circuit = build_circuit(discovered(hosted_circuit("c-1", (1,))))
|
|
138
|
+
|
|
139
|
+
assert circuit.breaker_rating_a == 20.0
|
|
140
|
+
assert circuit.breaker_rating_declared is True
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_rating_declared_not_yet_valued() -> None:
|
|
144
|
+
"""Declared before it is valued: the rating is coming, which is not the same as never."""
|
|
145
|
+
circuit = build_circuit(discovered(without_values(hosted_circuit("c-1", (1,)), "breaker/rating")))
|
|
146
|
+
|
|
147
|
+
assert circuit.breaker_rating_a is None
|
|
148
|
+
assert circuit.breaker_rating_declared is True
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_rating_not_declared() -> None:
|
|
152
|
+
circuit = build_circuit(discovered(_undeclared_rating(hosted_circuit("c-1", (1,)))))
|
|
153
|
+
|
|
154
|
+
assert circuit.breaker_rating_a is None
|
|
155
|
+
assert circuit.breaker_rating_declared is False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_no_breaker_node() -> None:
|
|
159
|
+
circuit = build_circuit(discovered(space_less_meter("meter-a")))
|
|
160
|
+
|
|
161
|
+
assert circuit.breaker_rating_a is None
|
|
162
|
+
assert circuit.breaker_rating_declared is False
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_schema_zero_does_not_say() -> None:
|
|
166
|
+
serial = "sim-40t-001"
|
|
167
|
+
circuit_id = "ac3dccda46a94b98878a227df6fed588"
|
|
168
|
+
adapter = SchemaZeroAdapter(serial_number=serial, schema=flat_schema(40))
|
|
169
|
+
adapter.handle_message(f"ebus/5/{serial}/$description", json.dumps({"nodes": {circuit_id: {"type": TYPE_CIRCUIT}}}))
|
|
170
|
+
adapter.handle_message(f"ebus/5/{serial}/$state", "ready")
|
|
171
|
+
|
|
172
|
+
assert adapter.build_snapshot().circuits[circuit_id].breaker_rating_declared is None
|
|
173
|
+
|
|
174
|
+
|
|
119
175
|
# ---------------------------------------------------------------------------
|
|
120
176
|
# The three flat booleans v1.0 retired
|
|
121
177
|
# ---------------------------------------------------------------------------
|
|
@@ -113,3 +113,44 @@ def test_a_flat_panel_shares_nothing() -> None:
|
|
|
113
113
|
|
|
114
114
|
assert all(circuit.meter_shared_with is None for circuit in snapshot.circuits.values())
|
|
115
115
|
assert shared_meter_groups(snapshot.circuits) == {}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# --- whether a panel publishes shared meters structurally ---------------------
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_a_panel_whose_circuits_declare_a_shared_meter_publishes_shared_meters() -> None:
|
|
122
|
+
assert _snapshot(*shared_pair("c-1", "c-2")).publishes_shared_meters is True
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_a_declared_list_with_no_value_still_marks_the_panel() -> None:
|
|
126
|
+
"""The declaration decides: a standard breaker declares the list and publishes no value."""
|
|
127
|
+
first, second = shared_pair("c-1", "c-2")
|
|
128
|
+
snapshot = _snapshot(
|
|
129
|
+
without_values(first, "meter/shared-with-device-ids"),
|
|
130
|
+
without_values(second, "meter/shared-with-device-ids"),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
assert snapshot.publishes_shared_meters is True
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_a_list_naming_no_known_circuit_still_marks_the_panel() -> None:
|
|
137
|
+
assert _snapshot(hosted_circuit("c-1", (1,), meter_shared_with=("ghost",))).publishes_shared_meters is True
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_only_the_meter_list_counts() -> None:
|
|
141
|
+
"""A relay shared with nothing on the meter says nothing about how the meters are described."""
|
|
142
|
+
snapshot = _snapshot(hosted_circuit("c-1", (1,), relay_shared_with=("c-2",)), hosted_circuit("c-2", (3,)))
|
|
143
|
+
|
|
144
|
+
assert snapshot.publishes_shared_meters is False
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_a_panel_with_no_circuit_declaring_the_list_does_not() -> None:
|
|
148
|
+
assert _snapshot(hosted_circuit("c-1", (1,)), hosted_circuit("c-2", (3,))).publishes_shared_meters is False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_main_32_and_the_flat_schema_do_not() -> None:
|
|
152
|
+
reference = replay(parent_child_tree(), _REFERENCE_PANEL).build_snapshot()
|
|
153
|
+
flat = SchemaZeroAdapter(serial_number="sim-40t-001", schema=flat_schema(40)).build_snapshot()
|
|
154
|
+
|
|
155
|
+
assert reference.publishes_shared_meters is False
|
|
156
|
+
assert flat.publishes_shared_meters is False
|
|
@@ -50,6 +50,16 @@ def test_the_captures_hold_shared_meters() -> None:
|
|
|
50
50
|
assert any(shared_meter_groups(capture_snapshot(stem).circuits) for stem in CAPTURES)
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
@pytest.mark.parametrize("stem", CAPTURES)
|
|
54
|
+
def test_shared_meters_are_published_exactly_where_a_circuit_declares_the_list(stem: str) -> None:
|
|
55
|
+
"""Every one of these models declares the list, on standard breakers too, whether or not any group exists."""
|
|
56
|
+
tree = capture_tree(stem)
|
|
57
|
+
declared = any(tree.declares(circuit_id, _SHARED_WITH[0]) for circuit_id in tree.circuits())
|
|
58
|
+
|
|
59
|
+
assert declared
|
|
60
|
+
assert capture_snapshot(stem).publishes_shared_meters is declared
|
|
61
|
+
|
|
62
|
+
|
|
53
63
|
@pytest.mark.parametrize("stem", CAPTURES)
|
|
54
64
|
def test_shared_with_is_neither_discovered_nor_an_extension_reading(stem: str) -> None:
|
|
55
65
|
adapter = capture_adapter(stem)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|