span-panel-api 3.6.0__tar.gz → 3.6.1b1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/CHANGELOG.md +9 -1
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/PKG-INFO +22 -18
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/README.md +20 -16
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/pyproject.toml +3 -2
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/models.py +14 -8
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_one_snapshot.py +74 -21
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/.gitignore +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/LICENSE +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/__init__.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/_http.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/_ssl.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/adapters.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/auth.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/const.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/detection.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/dispatch.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/exceptions.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/factory.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/mqtt/__init__.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/mqtt/async_client.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/mqtt/client.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/mqtt/connection.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/mqtt/const.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/mqtt/control.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/mqtt/models.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/phase_validation.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/protocol.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/py.typed +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/src/span_panel_api/schema_drift.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/conftest.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/fixtures/flat_wire.json +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/fixtures/panelbench_unvalued_by_both.json +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/fixtures/panelbench_wire.json +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/fixtures/v2/README.md +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/fixtures/v2/status.json +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/reference_payloads/README.md +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/reference_payloads/__init__.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/reference_payloads/bootstrap.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/reference_payloads/schema_one.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/simulation_fixtures/circuits.response.txt +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/simulation_fixtures/panel.response.txt +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/simulation_fixtures/soe.response.txt +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/simulation_fixtures/status.response.txt +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_absent_readings_are_not_zero.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_accumulator.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_adapters_discovery.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_adopted_control.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_adoption.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_async_mqtt_client.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_auth_and_homie_helpers.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_auth_redaction.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_ca_pinning.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_catalog_divergence.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_control_interceptor.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_detection_auth.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_exceptions.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_factory_dispatch.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_field_metadata.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_https_transport.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_leaf_name_mismatch.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_live_flat_differential.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_mqtt_bridge.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_mqtt_client_connection.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_mqtt_connect_flow.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_mqtt_debounce.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_mqtt_homie.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_packaging.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_phase_validation_configs.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_phase_validation_errors.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_plaintext_warning.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_protocol_conformance.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_protocol_models.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_public_api_unchanged.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_publish_outcome.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_redispatch_on_reconnect.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_reference_tree_values.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_register_passphrase_unavailable.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_rest_transport_contract.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_fetch_transport_split.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_generation_cross_check.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_migration_delta.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_one_adapter.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_one_charge_limit.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_one_circuits.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_one_conformance.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_one_connection_health.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_one_control_refusal.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_one_devices.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_one_discovery.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_one_extension.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_one_firmware.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_one_panel.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_one_pcs.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_one_service_entrance.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_one_shed_forecast.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_one_transport.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_provenance.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_schema_zero_adapter.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_shared_http_client.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_ssl_context.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_v2_status_parser.py +0 -0
- {span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/tls_fixtures.py +0 -0
|
@@ -7,7 +7,15 @@ 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
|
-
## [3.6.
|
|
10
|
+
## [3.6.1b1]
|
|
11
|
+
|
|
12
|
+
3.6.0 was withdrawn from PyPI; this release carries its changes, listed under 3.6.0 below, except how `pv` is chosen, which this release replaces.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **`SpanPanelSnapshot.pv` is documented as the lone inverter or the inverters together**, and the `schema-1` extra requires `span-panel-api-schema-1` 1.2.1 or newer.
|
|
17
|
+
|
|
18
|
+
## [3.6.0] [YANKED]
|
|
11
19
|
|
|
12
20
|
The snapshot carries every PV inverter a panel commissions, and registration copes with a panel that cannot read its own passphrase.
|
|
13
21
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: span-panel-api
|
|
3
|
-
Version: 3.6.
|
|
3
|
+
Version: 3.6.1b1
|
|
4
4
|
Summary: A client library for SPAN Panel API
|
|
5
5
|
Project-URL: Homepage, https://github.com/SpanPanel/span-panel-api
|
|
6
6
|
Project-URL: Issues, https://github.com/SpanPanel/span-panel-api/issues
|
|
@@ -22,7 +22,7 @@ Requires-Dist: pyyaml>=6.0.0
|
|
|
22
22
|
Provides-Extra: schema-0
|
|
23
23
|
Requires-Dist: span-panel-api-schema-0>=1.2.0; extra == 'schema-0'
|
|
24
24
|
Provides-Extra: schema-1
|
|
25
|
-
Requires-Dist: span-panel-api-schema-1>=1.2.
|
|
25
|
+
Requires-Dist: span-panel-api-schema-1>=1.2.1b1; extra == 'schema-1'
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
|
|
28
28
|
# SPAN Panel API
|
|
@@ -175,21 +175,25 @@ transport-specific classes.
|
|
|
175
175
|
|
|
176
176
|
All panel state is represented as immutable, frozen dataclasses:
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
178
|
+
- **`SpanPanelSnapshot`**: complete panel state. Identity (serial, firmware, vendor, model, hardware version), panel size, main breaker rating, main relay, door and proximity state, uptime, network links with the Wi-Fi SSID and vendor cloud, grid and
|
|
179
|
+
feedthrough power and energy, grid/DSM state, run configuration, dominant power source, grid islandability, per-leg voltages, power flows, lugs current and whether the lugs are the service entrance, shed policy and forecast. It holds `circuits`,
|
|
180
|
+
`battery`, `pv`, `pv_inverters`, `evse`, `mid`, `pcs`, `adopted_devices` and `extension_properties`.
|
|
181
|
+
- **`SpanCircuitSnapshot`**: one circuit. Id, name, relay state and requester, power, energy and their update times, tabs, 240 V, breaker rating, current, priority, user controllability, sheddable / never-backup / always-on flags, device type, relative
|
|
182
|
+
position, PCS management and priority, and the `$target` pending state for relay and priority.
|
|
183
|
+
- **`SpanBatterySnapshot`**: the BESS. SoE percentage and kWh, its own meter reading, communication state, link health, vendor / `model` / `part_number` / serial / version, nameplate capacity.
|
|
184
|
+
- **`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
|
+
`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, and the effective
|
|
187
|
+
limit it applies.
|
|
188
|
+
- **`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
|
+
- **`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
|
+
- **`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.
|
|
191
|
+
- **`ExtensionProperty`**: a vendor property on a device this library _does_ model. The subject it hangs off, its node and property ids and its `path`, datatype / unit / format, settability, value, and whether its node has curated siblings.
|
|
192
|
+
|
|
193
|
+
Identity is normalised across every DER class: **`model` is the human designation**, on `battery`, `evse` and `pv` alike, and **`part_number` is the SKU**, on `battery` and `evse`; `SpanPVSnapshot` has no SKU field. `product_name` was retired in 3.0.0 —
|
|
194
|
+
see the changelog, because `battery.model` changes value for existing flat users at that upgrade.
|
|
195
|
+
|
|
196
|
+
`mid`, `pcs`, `adopted_devices`, `extension_properties` and the per-DER link-health fields exist only under the parent/child schema. They are `None` or empty on a flat panel rather than absent, so a consumer reads the same snapshot type either way.
|
|
193
197
|
|
|
194
198
|
## Usage
|
|
195
199
|
|
|
@@ -611,7 +615,7 @@ src/span_panel_api/ # distribution: span-panel-api (no parser)
|
|
|
611
615
|
├── dispatch.py # select_adapter_key() — what does this panel need?
|
|
612
616
|
├── exceptions.py # Exception hierarchy
|
|
613
617
|
├── factory.py # create_span_client() → SpanMqttClient
|
|
614
|
-
├── models.py # Snapshot dataclasses (panel, circuit, battery, PV, EVSE, MID, adopted)
|
|
618
|
+
├── models.py # Snapshot dataclasses (panel, circuit, battery, PV, EVSE, MID, PCS, adopted, extension)
|
|
615
619
|
├── phase_validation.py # Electrical phase utilities
|
|
616
620
|
├── protocol.py # PEP 544 protocols, SchemaAdapter, PanelCapability flags
|
|
617
621
|
├── schema_drift.py # Reporting a panel that outruns what we can read
|
|
@@ -148,21 +148,25 @@ transport-specific classes.
|
|
|
148
148
|
|
|
149
149
|
All panel state is represented as immutable, frozen dataclasses:
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
151
|
+
- **`SpanPanelSnapshot`**: complete panel state. Identity (serial, firmware, vendor, model, hardware version), panel size, main breaker rating, main relay, door and proximity state, uptime, network links with the Wi-Fi SSID and vendor cloud, grid and
|
|
152
|
+
feedthrough power and energy, grid/DSM state, run configuration, dominant power source, grid islandability, per-leg voltages, power flows, lugs current and whether the lugs are the service entrance, shed policy and forecast. It holds `circuits`,
|
|
153
|
+
`battery`, `pv`, `pv_inverters`, `evse`, `mid`, `pcs`, `adopted_devices` and `extension_properties`.
|
|
154
|
+
- **`SpanCircuitSnapshot`**: one circuit. Id, name, relay state and requester, power, energy and their update times, tabs, 240 V, breaker rating, current, priority, user controllability, sheddable / never-backup / always-on flags, device type, relative
|
|
155
|
+
position, PCS management and priority, and the `$target` pending state for relay and priority.
|
|
156
|
+
- **`SpanBatterySnapshot`**: the BESS. SoE percentage and kWh, its own meter reading, communication state, link health, vendor / `model` / `part_number` / serial / version, nameplate capacity.
|
|
157
|
+
- **`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
|
+
`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, and the effective
|
|
160
|
+
limit it applies.
|
|
161
|
+
- **`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
|
+
- **`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
|
+
- **`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.
|
|
164
|
+
- **`ExtensionProperty`**: a vendor property on a device this library _does_ model. The subject it hangs off, its node and property ids and its `path`, datatype / unit / format, settability, value, and whether its node has curated siblings.
|
|
165
|
+
|
|
166
|
+
Identity is normalised across every DER class: **`model` is the human designation**, on `battery`, `evse` and `pv` alike, and **`part_number` is the SKU**, on `battery` and `evse`; `SpanPVSnapshot` has no SKU field. `product_name` was retired in 3.0.0 —
|
|
167
|
+
see the changelog, because `battery.model` changes value for existing flat users at that upgrade.
|
|
168
|
+
|
|
169
|
+
`mid`, `pcs`, `adopted_devices`, `extension_properties` and the per-DER link-health fields exist only under the parent/child schema. They are `None` or empty on a flat panel rather than absent, so a consumer reads the same snapshot type either way.
|
|
166
170
|
|
|
167
171
|
## Usage
|
|
168
172
|
|
|
@@ -584,7 +588,7 @@ src/span_panel_api/ # distribution: span-panel-api (no parser)
|
|
|
584
588
|
├── dispatch.py # select_adapter_key() — what does this panel need?
|
|
585
589
|
├── exceptions.py # Exception hierarchy
|
|
586
590
|
├── factory.py # create_span_client() → SpanMqttClient
|
|
587
|
-
├── models.py # Snapshot dataclasses (panel, circuit, battery, PV, EVSE, MID, adopted)
|
|
591
|
+
├── models.py # Snapshot dataclasses (panel, circuit, battery, PV, EVSE, MID, PCS, adopted, extension)
|
|
588
592
|
├── phase_validation.py # Electrical phase utilities
|
|
589
593
|
├── protocol.py # PEP 544 protocols, SchemaAdapter, PanelCapability flags
|
|
590
594
|
├── schema_drift.py # Reporting a panel that outruns what we can read
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "span-panel-api"
|
|
3
|
-
version = "3.6.
|
|
3
|
+
version = "3.6.1b1"
|
|
4
4
|
description = "A client library for SPAN Panel API"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "SpanPanel"}
|
|
@@ -63,7 +63,8 @@ dependencies = [
|
|
|
63
63
|
# the inverter identity fields 3.6.0 adds. Upgrading through the extra is what
|
|
64
64
|
# brings the adapters that do.
|
|
65
65
|
schema-0 = ["span-panel-api-schema-0>=1.2.0"]
|
|
66
|
-
|
|
66
|
+
# Raised to 1.2.1b1 for 3.6.1b1: that adapter stops ranking one inverter as pv.
|
|
67
|
+
schema-1 = ["span-panel-api-schema-1>=1.2.1b1"]
|
|
67
68
|
|
|
68
69
|
[project.urls]
|
|
69
70
|
Homepage = "https://github.com/SpanPanel/span-panel-api"
|
|
@@ -78,7 +78,8 @@ class SpanPVSnapshot:
|
|
|
78
78
|
|
|
79
79
|
A panel may commission more than one inverter, and from firmware r202639 each
|
|
80
80
|
is published as its own device. `SpanPanelSnapshot.pv_inverters` carries all
|
|
81
|
-
of them; `SpanPanelSnapshot.pv`
|
|
81
|
+
of them; `SpanPanelSnapshot.pv` is the lone inverter, or describes several
|
|
82
|
+
together, as that field documents.
|
|
82
83
|
"""
|
|
83
84
|
|
|
84
85
|
vendor_name: str | None = None # pv/vendor-name
|
|
@@ -117,6 +118,10 @@ class SpanPVSnapshot:
|
|
|
117
118
|
it does not know) and is deliberately distinct from `False`. The enum has
|
|
118
119
|
three members, `OK,LOST,DEGRADED`, and no UNKNOWN, so absence is the only
|
|
119
120
|
way to say it.
|
|
121
|
+
|
|
122
|
+
On a `pv` that describes several inverters together, `None` also means no
|
|
123
|
+
link is reported down and not every one is reported up, so it is never
|
|
124
|
+
`True` while any inverter's link is unreported; see `SpanPanelSnapshot.pv`.
|
|
120
125
|
"""
|
|
121
126
|
|
|
122
127
|
serial_number: str | None = None
|
|
@@ -141,7 +146,8 @@ class SpanPVSnapshot:
|
|
|
141
146
|
Named after `SpanEvseSnapshot.node_id`, which plays the same role for
|
|
142
147
|
chargers. It is the feeding circuit's id when a circuit feeds the inverter,
|
|
143
148
|
because that id stays put when the inverter's own device id changes, and
|
|
144
|
-
`device_id` otherwise.
|
|
149
|
+
`device_id` otherwise. `None` also on a `pv` that describes several
|
|
150
|
+
inverters together.
|
|
145
151
|
"""
|
|
146
152
|
|
|
147
153
|
|
|
@@ -1182,13 +1188,13 @@ class SpanPanelSnapshot:
|
|
|
1182
1188
|
circuits: dict[str, SpanCircuitSnapshot] = field(default_factory=dict)
|
|
1183
1189
|
battery: SpanBatterySnapshot = field(default_factory=SpanBatterySnapshot)
|
|
1184
1190
|
pv: SpanPVSnapshot = field(default_factory=SpanPVSnapshot)
|
|
1185
|
-
"""
|
|
1191
|
+
"""The lone PV inverter, or the inverters together; the empty snapshot when none is commissioned.
|
|
1186
1192
|
|
|
1187
|
-
With
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1193
|
+
With one inverter it is that inverter, identical to its `pv_inverters` entry. With several it describes them together
|
|
1194
|
+
and identifies none of them: vendor and model where every inverter shares one, the sum of their installed DC sizes where
|
|
1195
|
+
every inverter publishes one, and their link down if any is reported down, up if every one is reported up, unknown
|
|
1196
|
+
otherwise. It never carries an inverter's device id, key, serial, firmware, feeding circuit or position. A consumer that
|
|
1197
|
+
needs one inverter reads `pv_inverters`.
|
|
1192
1198
|
"""
|
|
1193
1199
|
pv_inverters: dict[str, SpanPVSnapshot] = field(default_factory=dict)
|
|
1194
1200
|
"""Every commissioned PV inverter, keyed by `SpanPVSnapshot.node_id`.
|
|
@@ -40,7 +40,6 @@ def test_roles_are_sorted_by_declared_type_not_device_id() -> None:
|
|
|
40
40
|
assert len(roles.lugs) == 2
|
|
41
41
|
assert len(roles.evse) == 2
|
|
42
42
|
assert roles.bess is not None and roles.bess.device_id == "bess"
|
|
43
|
-
assert roles.pv is not None and roles.pv.device_id == "pv"
|
|
44
43
|
assert [device.device_id for device in roles.pvs] == ["pv"]
|
|
45
44
|
assert roles.mid is not None and roles.mid.device_id == "bess-mid"
|
|
46
45
|
|
|
@@ -161,9 +160,9 @@ def _multi_inverter_children() -> list[DiscoveredDevice]:
|
|
|
161
160
|
"""The capture with three inverters: two fed by a circuit each, one fed by none.
|
|
162
161
|
|
|
163
162
|
The second inverter's circuit sits on lower breaker spaces than the captured
|
|
164
|
-
solar circuit,
|
|
165
|
-
Only the second inverter publishes a serial; the others leave
|
|
166
|
-
which is the common case.
|
|
163
|
+
solar circuit, which once made the library rank it first; nothing ranks
|
|
164
|
+
inverters now. Only the second inverter publishes a serial; the others leave
|
|
165
|
+
it unpublished, which is the common case.
|
|
167
166
|
"""
|
|
168
167
|
tree = {device_id: dict(topics) for device_id, topics in _TREE.items()}
|
|
169
168
|
pv_topics = tree.pop("pv")
|
|
@@ -195,7 +194,7 @@ def test_every_inverter_is_a_snapshot_keyed_by_its_feeding_circuit_or_its_device
|
|
|
195
194
|
|
|
196
195
|
|
|
197
196
|
def test_every_inverters_feeding_circuit_is_labeled_pv() -> None:
|
|
198
|
-
"""
|
|
197
|
+
"""Every inverter's circuit, however many are commissioned: an unlabeled PV circuit reads as a load."""
|
|
199
198
|
snapshot = build_snapshot(_device(PANEL), _multi_inverter_children())
|
|
200
199
|
|
|
201
200
|
assert {cid for cid, c in snapshot.circuits.items() if c.device_type == "pv"} == {SOLAR_CIRCUIT, SECOND_SOLAR_CIRCUIT}
|
|
@@ -235,34 +234,88 @@ def test_every_circuit_feeding_an_inverter_is_labeled_pv_whatever_the_order() ->
|
|
|
235
234
|
assert snapshot.pv_inverters[SOLAR_CIRCUIT].device_id == FIRST_PV
|
|
236
235
|
|
|
237
236
|
|
|
238
|
-
def
|
|
237
|
+
def test_with_several_inverters_pv_describes_them_together() -> None:
|
|
238
|
+
"""No inverter is primary: `pv` identifies none of them, whatever the tree order."""
|
|
239
239
|
children = _multi_inverter_children()
|
|
240
240
|
forward = build_snapshot(_device(PANEL), children)
|
|
241
241
|
backward = build_snapshot(_device(PANEL), list(reversed(children)))
|
|
242
242
|
|
|
243
|
-
assert forward.pv
|
|
244
|
-
assert
|
|
245
|
-
assert forward.pv
|
|
243
|
+
assert forward.pv == backward.pv
|
|
244
|
+
assert forward.pv.device_id is None
|
|
245
|
+
assert forward.pv.node_id is None
|
|
246
|
+
assert forward.pv.feed_circuit_id is None
|
|
247
|
+
assert forward.pv.serial_number is None
|
|
248
|
+
assert forward.pv.software_version is None
|
|
249
|
+
assert forward.pv.relative_position is None
|
|
250
|
+
assert forward.pv.vendor_name == forward.pv_inverters[SOLAR_CIRCUIT].vendor_name
|
|
251
|
+
assert forward.pv.model == forward.pv_inverters[SOLAR_CIRCUIT].model
|
|
252
|
+
assert forward.pv.nameplate_capacity_w == 24000.0
|
|
253
|
+
# Two links reported up and one unreported: not known to be all up, not known down.
|
|
254
|
+
assert forward.pv.connected is None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_one_link_down_is_the_link_down() -> None:
|
|
258
|
+
"""Three-valued: a link known down decides, whatever the others say.
|
|
259
|
+
|
|
260
|
+
The lost link is the first inverter's, on the higher breaker spaces, so the
|
|
261
|
+
removed lowest-space rule, which described the second inverter, read it up.
|
|
262
|
+
"""
|
|
263
|
+
lost = device_from_topics(
|
|
264
|
+
SOLAR_CIRCUIT,
|
|
265
|
+
{**_TREE[SOLAR_CIRCUIT], "connection/feeds-device-id": FIRST_PV, "connection/feeds-device-status": "LOST"},
|
|
266
|
+
)
|
|
267
|
+
children = [lost if device.device_id == SOLAR_CIRCUIT else device for device in _multi_inverter_children()]
|
|
246
268
|
|
|
269
|
+
assert build_snapshot(_device(PANEL), children).pv.connected is False
|
|
247
270
|
|
|
248
|
-
def test_an_inverter_without_a_feeding_circuit_is_primary_only_when_alone() -> None:
|
|
249
|
-
lone = [device for device in _multi_inverter_children() if device.device_id not in (FIRST_PV, SECOND_PV)]
|
|
250
271
|
|
|
251
|
-
|
|
272
|
+
def test_every_link_up_is_the_link_up() -> None:
|
|
273
|
+
fed = [device for device in _multi_inverter_children() if device.device_id != UNFED_PV]
|
|
274
|
+
|
|
275
|
+
assert build_snapshot(_device(PANEL), fed).pv.connected is True
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_one_unpublished_nameplate_leaves_the_sum_unknown() -> None:
|
|
279
|
+
unsized = {topic: value for topic, value in _TREE["pv"].items() if topic != "info/nominal-power"}
|
|
280
|
+
children = [
|
|
281
|
+
device_from_topics(SECOND_PV, unsized) if device.device_id == SECOND_PV else device
|
|
282
|
+
for device in _multi_inverter_children()
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
assert build_snapshot(_device(PANEL), children).pv.nameplate_capacity_w is None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_a_vendor_not_shared_is_unknown() -> None:
|
|
289
|
+
children = [
|
|
290
|
+
(
|
|
291
|
+
device_from_topics(SECOND_PV, {**_TREE["pv"], "info/vendor-name": "SolarEdge"})
|
|
292
|
+
if device.device_id == SECOND_PV
|
|
293
|
+
else device
|
|
294
|
+
)
|
|
295
|
+
for device in _multi_inverter_children()
|
|
296
|
+
]
|
|
252
297
|
|
|
253
|
-
assert
|
|
254
|
-
assert set(snapshot.pv_inverters) == {UNFED_PV}
|
|
298
|
+
assert build_snapshot(_device(PANEL), children).pv.vendor_name is None
|
|
255
299
|
|
|
256
300
|
|
|
257
|
-
def
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
301
|
+
def test_what_several_inverters_do_not_share_is_unknown() -> None:
|
|
302
|
+
children = [
|
|
303
|
+
device_from_topics(SECOND_PV, {**_TREE["pv"], "info/model": "SE7600H"}) if device.device_id == SECOND_PV else device
|
|
304
|
+
for device in _multi_inverter_children()
|
|
305
|
+
]
|
|
261
306
|
|
|
262
|
-
|
|
307
|
+
snapshot = build_snapshot(_device(PANEL), children)
|
|
308
|
+
|
|
309
|
+
assert snapshot.pv.model is None
|
|
310
|
+
assert snapshot.pv.vendor_name == snapshot.pv_inverters[SOLAR_CIRCUIT].vendor_name
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_a_lone_inverter_is_pv_whatever_feeds_it() -> None:
|
|
314
|
+
lone = [device for device in _multi_inverter_children() if device.device_id not in (FIRST_PV, SECOND_PV)]
|
|
315
|
+
|
|
316
|
+
snapshot = build_snapshot(_device(PANEL), lone)
|
|
263
317
|
|
|
264
|
-
assert
|
|
265
|
-
assert primary.device_id == "pv-a"
|
|
318
|
+
assert snapshot.pv == snapshot.pv_inverters[UNFED_PV]
|
|
266
319
|
|
|
267
320
|
|
|
268
321
|
def test_the_serial_is_carried_where_published_and_absent_otherwise() -> None:
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/fixtures/panelbench_unvalued_by_both.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/simulation_fixtures/circuits.response.txt
RENAMED
|
File without changes
|
{span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/simulation_fixtures/panel.response.txt
RENAMED
|
File without changes
|
|
File without changes
|
{span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/simulation_fixtures/status.response.txt
RENAMED
|
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
|
|
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
|
{span_panel_api-3.6.0 → span_panel_api-3.6.1b1}/tests/test_register_passphrase_unavailable.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|