All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Reconnect loop log noise reduced —
SpanMqttClient._reconnect_loopnow splits the catch-all exception handler in two: expected transient failures (OSErrorfamily — refused connection, DNS miss, socket timeout,ssl.SSLError) log a one-line WARNING with the exception repr, while unexpected exceptions retain the full traceback viaexc_info=True. The common "panel offline" case no longer buries logs in paho/stdlib stack frames that add no diagnostic signal; genuinely unknown failures still surface full tracebacks for support-ticket triage.
get_fqdn()returnsstr | None—Nonenow distinguishes "no FQDN configured" (HTTP 404 or missing field) from an explicit empty string. Callers that treated""as "not registered" must update to check forNone.- Connection callback errors logged at WARNING —
SpanMqttClient._on_connection_changenow logs callback exceptions via_LOGGER.warning(..., exc_info=True)instead of_LOGGER.exception(...), consistent with_dispatch_snapshot. - Reconnect loop catches all exceptions —
AsyncMqttBridge._reconnect_loopno longer silently drops on non-OSErrorfailures (e.g.WebsocketConnectionError,ssl.SSLError). All exceptions are logged at WARNING and the loop keeps backing off. - Abnormal MQTT disconnects logged at WARNING — disconnects where
reason_code.is_failureis true now log at WARNING; clean disconnects continue to log at DEBUG.
- CA certificate no longer written to disk —
AsyncMqttBridge.connect()builds thessl.SSLContextfrom the fetched PEM viacadata, eliminating the temp-file lifecycle (and the small leak window on unexpected process exit) that the priortls_set(ca_certs=path)path required. - Deprecated
asyncio.get_event_loop()removed —_wait_for_circuit_namesnow usestime.monotonic(). The previous code emitted aDeprecationWarningon Python 3.12+. - Negative-zero on circuit
instant_power_w— explicit guard replaces a cryptic-raw or 0.0idiom inHomieDeviceConsumer._build_circuit. - DSM grid-exchanging heuristic uses epsilon — replaces
!= 0.0float comparison withabs(x) > 1.0 W, so theDSM_OFF_GRIDbranch is actually reachable when no BESS is commissioned and lugs readings hover near zero. SpanPanelAPIError.__str__override removed — the override silently hid exception args beyond the first; defaultException.__str__is now used.- Paho lock-layout check at import —
span_panel_api.mqtt.async_clientverifies on import that the_PAHO_LOCK_ATTRSlist exactly matches paho's*_mutexattributes. RaisesRuntimeError(notassert, sopython -Odoes not bypass it) on drift.
register_v2()— docstring now warns that each call creates a new client entry on the panel; callers should persist and reuse the returnedV2AuthResponserather than re-registering on every restart.- Stale simulation transport references removed from
protocol.pyandmodels.pymodule docstrings.
SpanMqttClient.register_connection_callback(cb)— subscribe to broker connection state transitions. Callback fires withFalseon broker disconnect andTrueon reconnect; returns an idempotent unregister function. Added toSpanPanelClientProtocolso any transport that claims the protocol must implement it.SpanPanelStaleDataErrorexception — raised byget_snapshot()when the client is not fully live. Derives fromSpanPanelError(not fromSpanPanelConnectionError), because "never connected" and "running but data not currently live" are semantically distinct states.
get_snapshot()contract — now raisesSpanPanelStaleDataErrorwhen the bridge is not connected or the Homie device has not reached ready state. Previously, the method silently returned a snapshot built from whatever the in-memory accumulator happened to hold, which made offline panels indistinguishable from online ones. This is the primary reason the span integration could not detect panel-offline transitions.
- Stale snapshot dispatch after bridge disconnect — a pending snapshot-debounce timer scheduled just before a bridge disconnect could fire afterwards, delivering a snapshot built from the still-
ready()accumulator to subscribers._on_connection_change(False)now cancels the pending timer, and_dispatch_snapshotis now guarded by the same liveness predicate asget_snapshot(), so push consumers never receive a post-disconnect stale snapshot.
- Consumers of
get_snapshot()must now handleSpanPanelStaleDataError. Any consumer with a broadexcept Exception(orexcept SpanPanelError) branch already handles this correctly.
- Revert accumulator to 2.5.1 behavior — the 2.5.2 lifecycle changes (property clearing, unconditional lifecycle transition on
$state=init, generation counter) caused false energy dip spikes on panel reboots and network interruptions. The 2.5.3 partial fix (removing the clearing) was insufficient — the unconditional lifecycle disruption on transient$state=initevents still triggered snapshot pipeline resets that produced 0.0 energy readings. Revertedaccumulator.pyandhomie.pyto their stable 2.5.1 state. The existing dirty-node tracking handles reboot transitions correctly without special-case lifecycle management.
Retired: Partial fix for 2.5.2 — removed property clearing but kept the lifecycle disruption that still caused false dips. Superseded by 2.5.4.
- Preserve property values on lifecycle reset — removed the property/timestamp/target clearing from
_handle_description().
Retired: Lifecycle changes caused false energy dip spikes. Superseded by 2.5.4.
- Clear stale property values on panel reboot — after a panel reboot, snapshots could mix pre-reboot and post-reboot data. The accumulator now detects reboots (including fast reboots where the broker LWT is skipped) and clears stale state before building the next snapshot.
- Snapshot cache invalidated on reboot — the snapshot cache is now discarded when a reboot is detected, forcing a full rebuild from fresh data.
- Replaced
assertwithRuntimeErrorin production code —HomieDeviceConsumer._rebuild_dirty_circuits()used anassertto guard a cached-snapshot invariant, which would be silently stripped bypython -O. Replaced with an explicitRuntimeErrorraise. - Fixed broken bandit pre-commit hook — bandit was pinned to v1.8.3, which is incompatible with Python 3.14. It silently skipped all source files (20/20) and reported "Passed" with zero issues. Bumped to v1.9.4 which scans all files correctly.
HomiePropertyAccumulator— new layer that handles generic Homie v5 protocol parsing (message routing, property/target storage, dirty-node tracking) with an explicit lifecycle state machine (HomieLifecycle), cleanly separated from SPAN-specific snapshot construction.$targetproperty support —SpanCircuitSnapshotgainsrelay_state_targetandpriority_targetfields, surfacing the desired-vs-actual state for relay and shed-priority commands.- Dirty-node snapshot caching —
HomieDeviceConsumer.build_snapshot()tracks which nodes changed since the last build and returns a cached snapshot when nothing is dirty, reducing per-scan CPU cost on constrained hardware.
- Layered Homie consumer architecture —
HomieDeviceConsumerno longer handles protocol plumbing. It reads fromHomiePropertyAccumulatorvia a query API (get_prop,get_target,nodes_by_type, etc.) and focuses solely on SPAN domain interpretation: power sign normalization, DSM derivation, unmapped tab synthesis, and snapshot assembly. SpanMqttClientcomposes both layers —connect()creates an accumulator and wires it into the consumer. The public client API is unchanged.- Property callbacks fire only on value change — retained messages replaying already-known values no longer trigger callback storms on MQTT reconnect.
- Moved SSL context creation to executor —
httpx.AsyncClient()eagerly callsssl.SSLContext.load_verify_locations()with the system CA bundle, which is a blocking file I/O operation that triggers Home Assistant's event loop protection. The SSL context is now created in an executor thread and passed to httpx viaverify=ctx.
- Added
license = "MIT"to package metadata — thepyproject.tomlwas missing the license field, causing license audit failures in downstream projects (HA core hassfest). - Loosened httpx version constraint — changed from
>=0.28.1,<0.29.0to>=0.28.1to satisfy HA core hassfest version restriction checks.
proximity_provenonV2StatusInfo— parsed from the v2 status endpoint response (firmware 202609+). ReturnsNoneon older panels where the field is absent, allowing callers to distinguish "not proven" from "unknown."HomieSchemaTypestype alias — replaces rawdict[str, dict[str, object]]throughout the codebase for Homie schema type signatures.log_schema_drifttest coverage — raisedfield_metadata.pycoverage from 58% to 98%.
- Injected HTTP client for v2 auth —
detect_api_version,register_v2,download_ca_cert, and other bootstrap functions accept an optionalhttpx_clientparameter. Consumers (e.g. Home Assistant) can pass their managed client instead of the library creating ad-hoc ones. - Blocking file I/O moved to executor — temp CA cert file write and cleanup in
AsyncMqttBridge.connect()anddisconnect()now run in an executor thread instead of on the event loop. - Narrowed CA cert download exception handling —
connect()catches specificOSError,SpanPanelConnectionError, andSpanPanelTimeoutErrorinstead of bareExceptionwhen fetching the CA certificate. - Removed
verify=Falsefrom fallback HTTP client — the library's internal fallbackhttpx.AsyncClientno longer setsverify=False. All bootstrap URLs are plain HTTP so the flag was irrelevant; removing it avoids misleading security impressions.
- 59 low-value tests — stripped tests that exercised Python language mechanics (dataclass construction, frozen, slots, IntFlag), tautological assertions, fragile source-code string inspection, redundant export checks, and duplicates across files. Test count: 310 → 251, coverage maintained at 96%.
- FQDN management endpoints —
register_fqdn(),get_fqdn(),delete_fqdn()for managing the panel's TLS certificate SAN via/api/v2/dns/fqdn(spanio/SPAN-API-Client-Docs#10)
- MQTT connection errors now wrapped as
SpanPanelConnectionError—OSErrorsubclasses raised during MQTT broker connection (DNS resolution failure, connection refused, network unreachable, etc.) are now caught and wrapped asSpanPanelConnectionError. Previously these propagated as unhandled exceptions, preventing consumers from handling them gracefully.
- Simulation engine removed —
DynamicSimulationEngine,SimulationConfig, and all simulation-related modules have been removed from the library. Simulation is now handled by the standalone SPAN Panel Simulator add-on.
- Negative zero on idle circuits — Circuit power negation (
-raw_power_w) produced IEEE 754-0.0when the panel reported0.0for an idle circuit. The value is now normalized to positive zero after negation.
- Panel size sourced from Homie schema —
panel_sizeis now derived from the circuitspaceproperty format in the Homie schema (GET /api/v2/homie/schema), which declares the valid range as"1:N:1"where N is the panel size. This replaces a non-deterministic heuristic that inferred panel size from the highest occupied breaker tab, which would undercount when trailing positions were empty. SpanMqttClient.connect()fetches schema internally — the client automatically callsget_homie_schema()duringconnect()and passes the panel size toHomieDeviceConsumer. Callers no longer need to fetch or passpanel_size.SpanPanelSnapshot.panel_size— type changed fromint | Nonetoint; always populated from the schemaV2HomieSchema.panel_size— new property that parses the schema's circuit space format to extract the authoritative panel sizeV2HomieSchemaexported from package public APIHomieDeviceConsumerrequirespanel_size— new required constructor parameter; unmapped tabs now fill to the schema-defined panel size rather than deriving from circuit datacreate_span_client()simplified —panel_sizeparameter removed; schema is fetched internally bySpanMqttClient.connect()
- MQTT
core/panel-sizetopic parsing — removed fromHomieDeviceConsumer; panel size comes from the schema, not a runtime MQTT property
v2.0.0 is a ground-up rewrite. The REST/OpenAPI transport has been removed entirely in favor of MQTT/Homie — the SPAN Panel's native v2 protocol. This is a breaking change: all consumer code must be updated to use the new API surface.
Package versions prior to 2.0.0 depend on the SPAN v1 REST API. SPAN will sunset v1 firmware at the end of 2026, at which point v1.x releases of this package will cease to function. Users should upgrade to 2.0.0.
- REST transport removed —
SpanPanelClient,SpanRestClient, thegenerated_client/OpenAPI layer, and all REST-related modules have been deleted - No more polling —
get_status(),get_panel_state(),get_circuits(),get_storage_soe()replaced byget_snapshot()returning a singleSpanPanelSnapshot - Protocol-based API — consumers code against
SpanPanelClientProtocol,CircuitControlProtocol, andStreamingCapableProtocol(PEP 544), not concrete classes - Authentication changed — passphrase-based v2 registration via
register_v2()replaces v1 token-based auth; factory handles this automatically - paho-mqtt is now required — moved from optional
[mqtt]extra to a core dependency - Circuit IDs are UUIDs — dashless UUID strings replace integer circuit IDs
- Shed priority values changed — v2 uses
NEVER/SOC_THRESHOLD/OFF_GRIDinstead of v1'sMUST_HAVE/NICE_TO_HAVE/NON_ESSENTIAL SpanPanelRetriableErrorremoved — retry logic is no longer in the library (no REST polling)set_async_delay_func()removed — no retry delay hook needed for MQTT transportcache_windowparameter removed — no caching needed; MQTT delivers state changes in real timeattrs,python-dateutildependencies removed
- MQTT/Homie transport (
span_panel_api.mqtt):SpanMqttClient— implements all three protocols (panel, circuit control, streaming)AsyncMqttBridge— paho-mqtt v2 wrapper with TLS/WebSocket, event-loop-driven socket I/O (no threads)HomieDeviceConsumer— Homie v5 state machine parsing MQTT topics into snapshotsMqttClientConfig— frozen configuration with transport type and TLS settings
- Snapshot dataclasses — immutable
SpanPanelSnapshot,SpanCircuitSnapshot,SpanBatterySnapshot,SpanPVSnapshot,SpanEvseSnapshotwith v2-native fields - v2 auth functions —
register_v2(),download_ca_cert(),get_homie_schema(),regenerate_passphrase() - API version detection —
detect_api_version()probes/api/v2/statusand returnsDetectionResult - Factory function —
create_span_client()handles registration and returns a configuredSpanMqttClient - PV/BESS metadata — vendor name, product name, nameplate capacity parsed from Homie device tree
- Power flows —
power_flow_pv,power_flow_battery,power_flow_grid,power_flow_siteon panel snapshot - Lugs current — per-phase upstream/downstream current (A) on panel snapshot
- Per-leg voltages —
l1_voltage,l2_voltageon panel snapshot - Panel metadata —
dominant_power_source,vendor_cloud,wifi_ssid,panel_size,main_breaker_rating_a - Streaming callbacks —
register_snapshot_callback()+start_streaming()/stop_streaming()for real-time push - Snapshot debounce —
snapshot_intervalparameter onSpanMqttClient(default 1.0s) rate-limitsbuild_snapshot()+ callback dispatch; set to 0 for immediate (no debounce). Runtime adjustment viaset_snapshot_interval() PanelCapabilityflag enum — runtime feature advertisement (EBUS_MQTT,PUSH_STREAMING,CIRCUIT_CONTROL,BATTERY_SOE)
412 Precondition Failednow treated as auth error (AUTH_ERROR_CODESupdated)- Version bumped from 1.1.14 to 2.0.0
- Python requirement relaxed to
>=3.10(from3.12+)
src/span_panel_api/rest/— entire REST client directorysrc/span_panel_api/client.py— backward-compat shimsrc/span_panel_api/generated_client/— OpenAPI v1 generated modelsgenerate_client.py— OpenAPI client generator scriptexamples/directory (YAML configs moved totests/fixtures/configs/)DeprecationInfo,CircuitCorrelationProtocol,CorrelationUnavailableError,SpanPanelRetriableErrorPanelCapability.REST_V1,PanelCapability.SIMULATIONflags- HTTP/retry constants from
const.py openapi.jsonspecification file
PanelControlProtocol— new protocol interface for panel-level settable properties, separate fromCircuitControlProtocolset_dominant_power_source()— publishes a Dominant Power Source override to the panel's core node via MQTTfind_node_by_type()made public — renamed from_find_node_by_type()onHomieDeviceConsumerto support external callers resolving node IDs by type
- EVSE snapshot model — new
SpanEvseSnapshotdataclass with status, lock state, advertised current, and device metadata (vendor, product, part number, serial number, software version) - EVSE Homie parsing —
HomieDeviceConsumer._build_evse_devices()extracts all 9 EVSE properties fromenergy.ebus.device.evsenodes - Multiple EVSE support —
SpanPanelSnapshot.evsedict keyed by node ID supports multiple commissioned chargers - EVSE simulation —
DynamicSimulationEnginegenerates EVSE snapshots for circuits withdevice_type == "evse" SpanEvseSnapshotexported from package public API
- Full BESS metadata parsing — vendor name, product name, model, serial number, software version, nameplate capacity, and connected state from Homie BESS node
- README documentation — event-loop I/O architecture and circuit name synchronization sections
- Bumped nodeenv dev dependency from 1.9.1 to 1.10.0
- Recognize panel Keep-Alive at 5 sec, handle
httpx.RemoteProtocolErrordefensively
- Simulation mode sign correction for solar and battery power values
- Fixed battery State of Energy (SOE) calculation to use configured battery behavior instead of hardcoded time-of-day assumptions
- Updated GitHub Actions setup-python from v5 to v6
- Updated dev dependencies group
- Fixed sign on power values in simulation mode
- Updated virtualenv from 20.33.0 to 20.34.0
- Updated GitHub Actions checkout from v4 to v5
- Enhanced simulation API with YAML configuration and dynamic overrides
- Battery behavior simulation capabilities
- Phase validation functionality
- Support for host field as serial number in simulation mode
- Time-based energy accumulation in simulation
- Power fluctuation patterns for different appliance types
- Per-circuit and per-branch variation controls
- Fixed authentication in simulation mode
- Fixed locking issues in simulation mode
- Fixed energy accumulation in simulation
- Fixed cache for unmapped circuits
- Refactored simulation to reduce code complexity
- Removed unused client_utils.py
- Simulation mode enhancements
- Test coverage for simulation edge cases
- Fixed panel constants and simulation demo
- Fixed energy accumulation in simulation
- Formatting and linting scripts
- Removed unused client_utils.py
- Fixed tests and linting errors
- Excluded defensive code from coverage
- Simulation mode — complete simulation system for development and testing without physical SPAN panel
- Dead code checking
- Test coverage for simulation mode
- Updated ruff configuration
- Moved uncategorized tests to appropriate files
- Upgraded openapi-python-client to 0.24.0 and regenerated client
- Loosened ruff dependency constraints
- Fixed tests compatibility issues
- Initial release of SPAN Panel API client library
- REST/OpenAPI transport for SPAN Panel v1 firmware
- Context manager, long-lived, and manual connection patterns
- Authentication system with token-based API access
- Panel status and state retrieval
- Circuit control (relay and priority management)
- Battery storage information (SOE)
- Virtual circuits for unmapped panel tabs
- Timeout and retry configuration with exponential backoff
- Time-based caching system
- Error categorization with specific exception types
- Home Assistant integration compatibility layer
- Simulation mode for testing without physical hardware
- Development toolchain with Poetry, pytest, mypy, ruff
| Version | Date | Transport | Summary |
|---|---|---|---|
| 2.5.4 | 04/2026 | MQTT/Homie | Revert accumulator to stable 2.5.1 behavior; fixes false energy dip spikes |
| 2.5.3 | 04/2026 | MQTT/Homie | (retired) Partial fix — still caused false dips from lifecycle disruption |
| 2.5.2 | 04/2026 | MQTT/Homie | (retired) Lifecycle changes caused false energy dip spikes |
| 2.5.1 | 04/2026 | MQTT/Homie | Replace assert with RuntimeError; fix bandit pre-commit hook |
| 2.5.0 | 03/2026 | MQTT/Homie | Homie accumulator layer, $target support, dirty-node snapshot caching |
| 2.4.2 | 03/2026 | MQTT/Homie | SSL context creation moved to executor |
| 2.4.1 | 03/2026 | MQTT/Homie | License metadata, loosened httpx constraint |
| 2.4.0 | 03/2026 | MQTT/Homie | proximityProven, injected HTTP client, executor file I/O, type alias, test cleanup |
| 2.3.2 | 03/2026 | MQTT/Homie | FQDN management endpoints |
| 2.3.1 | 03/2026 | MQTT/Homie | MQTT connection errors wrapped as SpanPanelConnectionError |
| 2.3.0 | 03/2026 | MQTT/Homie | Simulation engine removed |
| 2.2.4 | 03/2026 | MQTT/Homie | Negative zero fix on idle circuits |
| 2.2.3 | 03/2026 | MQTT/Homie | Panel size from Homie schema; panel_size always populated on snapshot |
| 2.0.2 | 03/2026 | MQTT/Homie | EVSE (EV charger) snapshot model, Homie parsing, simulation support |
| 2.0.1 | 03/2026 | MQTT/Homie | Full BESS metadata parsing, README documentation |
| 2.0.0 | 02/2026 | MQTT/Homie | Ground-up rewrite: MQTT-only, protocol-based API, real-time push, PV/BESS metadata |
| 1.1.14 | 12/2025 | REST | Keep-Alive and RemoteProtocolError handling |
| 1.1.9 | 9/2025 | REST | Simulation sign corrections |
| 1.1.8 | 2024 | REST | Simulation power sign fix |
| 1.1.6 | 2024 | REST | YAML simulation API, battery simulation |
| 1.1.5 | 2024 | REST | Simulation edge cases |
| 1.1.4 | 2024 | REST | Formatting and linting |
| 1.1.3 | 2024 | REST | Test and lint fixes |
| 1.1.2 | 2024 | REST | Simulation mode added |
| 1.1.1 | 2024 | REST | Dependency updates |
| 1.1.0 | 2024 | REST | Initial release |