Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 41 additions & 32 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.5.3] - 04/2026
## [2.5.4] - 04/2026

### Reverted

- **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=init` events still triggered snapshot pipeline resets that produced 0.0 energy readings. Reverted `accumulator.py` and `homie.py` to their
stable 2.5.1 state. The existing dirty-node tracking handles reboot transitions correctly without special-case lifecycle management.

## [2.5.3] - 04/2026 (retired)

> **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.

### Fixed

- **Preserve property values on lifecycle reset** — the 2.5.2 property clear caused `_parse_float('')` to return `0.0` for energy counters during panel reboots or network interruptions, triggering false dip-compensation offsets in the integration that
permanently inflated energy sensor values. Removed the property/timestamp/target clearing from `_handle_description()`. Pre-reboot values now serve as safe placeholders until the panel re-publishes fresh data.
- **Snapshot cache invalidated on reboot** — the generation counter still increments on lifecycle resets, forcing consumers to discard cached snapshots and rebuild from current accumulator state.
- **Preserve property values on lifecycle reset** — removed the property/timestamp/target clearing from `_handle_description()`.

## [2.5.2] - 04/2026 (retired)

> **Retired:** The property-clearing behavior introduced in this release caused false energy dip spikes. Superseded by 2.5.3.
> **Retired:** Lifecycle changes caused false energy dip spikes. Superseded by 2.5.4.

### Fixed

Expand Down Expand Up @@ -344,30 +352,31 @@ Package versions prior to 2.0.0 depend on the SPAN v1 REST API. SPAN will sunset

## Version History Summary

| Version | Date | Transport | Summary |
| ---------- | ------- | ---------- | ----------------------------------------------------------------------------------------- |
| **2.5.3** | 04/2026 | MQTT/Homie | Preserve property values on lifecycle reset; fix false energy dip spikes from 2.5.2 clear |
| **2.5.2** | 04/2026 | MQTT/Homie | _(retired)_ Clear stale property values on panel reboot; 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 |
| 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 |
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "span-panel-api"
version = "2.5.3"
version = "2.5.4"
description = "A client library for SPAN Panel API"
authors = [
{name = "SpanPanel"}
Expand Down
48 changes: 4 additions & 44 deletions src/span_panel_api/mqtt/accumulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,6 @@ def __init__(self, serial_number: str) -> None:
# Node type mapping from $description
self._node_types: dict[str, str] = {}

# Generation counter — incremented whenever a new lifecycle's
# $description is accepted (including initial boot) so consumers
# can invalidate snapshot caches built from prior-lifecycle data.
self._generation: int = 0

# Dirty tracking
self._dirty_nodes: set[str] = set()

Expand All @@ -84,11 +79,6 @@ def ready_since(self) -> float:
"""Monotonic timestamp of the last READY transition, 0.0 if never ready."""
return self._ready_since

@property
def generation(self) -> int:
"""Counter incremented on each new lifecycle's ``$description`` to invalidate consumer caches."""
return self._generation

def is_ready(self) -> bool:
"""True when lifecycle is READY."""
return self._lifecycle == HomieLifecycle.READY
Expand Down Expand Up @@ -201,17 +191,10 @@ def _handle_state(self, payload: str) -> None:
self._received_state_ready = False
self._received_description = False
else:
# init, sleeping, alert, etc. — connected but not ready.
# Always move out of READY/DESCRIPTION_RECEIVED into a
# non-ready connected lifecycle state.
#
# Reset _received_description so that the upcoming $description
# triggers a property clear. This covers fast reboots where
# the broker's LWT ($state=disconnected) may not reach us
# before the panel publishes $state=init.
self._lifecycle = HomieLifecycle.CONNECTED
# init, sleeping, alert, etc. — connected but not ready
if self._lifecycle == HomieLifecycle.DISCONNECTED:
self._lifecycle = HomieLifecycle.CONNECTED
self._received_state_ready = False
self._received_description = False

Comment thread
cayossarian marked this conversation as resolved.
_LOGGER.debug("Homie $state: %s → lifecycle=%s", payload, self._lifecycle.value)

Expand All @@ -223,25 +206,6 @@ def _handle_description(self, payload: str) -> None:
_LOGGER.warning("Invalid $description JSON")
return

# _handle_state() already reset _received_description to False due to
# a state change that starts a new panel lifecycle, including
# $state=disconnected/lost and other non-ready states such as init.
# Increment the generation counter to invalidate consumer snapshot
# caches, but preserve property values — pre-reboot readings serve
# as safe placeholders until the panel re-publishes. Clearing values
# would emit 0.0 for energy counters via _parse_float(""), triggering
# false dip-compensation offsets in the integration.
#
# On a pure MQTT reconnect (no panel reboot), _received_description
# is still True from the previous session so we skip this block —
# the retained property messages carry the correct (unchanged) values.
if not self._received_description:
self._generation += 1
_LOGGER.debug(
"Panel reboot detected (generation %d); preserving property values as placeholders",
self._generation,
)

self._received_description = True
self._node_types.clear()

Expand All @@ -256,11 +220,7 @@ def _handle_description(self, payload: str) -> None:
# Mark all known nodes dirty
self._dirty_nodes.update(self._node_types.keys())

_LOGGER.debug(
"Parsed $description with %d nodes (generation %d)",
len(self._node_types),
self._generation,
)
_LOGGER.debug("Parsed $description with %d nodes", len(self._node_types))

# Lifecycle transition
if self._received_state_ready:
Expand Down
7 changes: 0 additions & 7 deletions src/span_panel_api/mqtt/homie.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ def __init__(self, accumulator: HomiePropertyAccumulator, panel_size: int) -> No
self._acc = accumulator
self._panel_size = panel_size
self._cached_snapshot: SpanPanelSnapshot | None = None
self._cache_generation: int = 0

# -- Delegation to accumulator -------------------------------------------
# These thin wrappers allow SpanMqttClient (and legacy test code) to
Expand Down Expand Up @@ -119,12 +118,6 @@ def build_snapshot(self) -> SpanPanelSnapshot:

Must be called after accumulator is_ready() returns True.
"""
# Invalidate cache when the accumulator generation advances
# (panel reboot cleared all property values).
if self._acc.generation != self._cache_generation:
self._cached_snapshot = None
self._cache_generation = self._acc.generation

dirty = self._acc.dirty_node_ids()

if not dirty and self._cached_snapshot is not None:
Expand Down
Loading