From ac1be48fabcb8bb23b27a11efd085dc1cc5ffe9d Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:21:42 -0700 Subject: [PATCH 1/3] fix: clear stale property values on panel reboot to prevent mixed-generation snapshots When the SPAN panel reboots, the MQTT accumulator previously retained pre-reboot property values. This could produce snapshots mixing stale pre-reboot data with fresh post-reboot data during the brief window before all circuits re-publish. The accumulator now conditionally clears property/target/timestamp storage when $description arrives after a lifecycle reset, with a generation counter that invalidates the consumer's snapshot cache. Covers both normal reboots (via LWT $state=disconnected) and fast reboots (via $state=init without LWT). Bumps version to 2.5.2. --- pyproject.toml | 2 +- src/span_panel_api/mqtt/accumulator.py | 36 ++++++- src/span_panel_api/mqtt/homie.py | 7 ++ tests/test_accumulator.py | 125 +++++++++++++++++++++++++ tests/test_mqtt_homie.py | 23 +++++ uv.lock | 20 +--- 6 files changed, 191 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f5be90b..385aaf1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "span-panel-api" -version = "2.5.1" +version = "2.5.2" description = "A client library for SPAN Panel API" authors = [ {name = "SpanPanel"} diff --git a/src/span_panel_api/mqtt/accumulator.py b/src/span_panel_api/mqtt/accumulator.py index a102ac8..ea45a95 100644 --- a/src/span_panel_api/mqtt/accumulator.py +++ b/src/span_panel_api/mqtt/accumulator.py @@ -56,6 +56,10 @@ def __init__(self, serial_number: str) -> None: # Node type mapping from $description self._node_types: dict[str, str] = {} + # Generation counter — incremented when $description clears property + # values so consumers can invalidate caches built from stale data. + self._generation: int = 0 + # Dirty tracking self._dirty_nodes: set[str] = set() @@ -79,6 +83,11 @@ 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 $description (panel reboot).""" + return self._generation + def is_ready(self) -> bool: """True when lifecycle is READY.""" return self._lifecycle == HomieLifecycle.READY @@ -191,10 +200,15 @@ 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 + # init, sleeping, alert, etc. — connected but not ready. + # 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. if self._lifecycle == HomieLifecycle.DISCONNECTED: self._lifecycle = HomieLifecycle.CONNECTED self._received_state_ready = False + self._received_description = False _LOGGER.debug("Homie $state: %s → lifecycle=%s", payload, self._lifecycle.value) @@ -206,6 +220,20 @@ def _handle_description(self, payload: str) -> None: _LOGGER.warning("Invalid $description JSON") return + # Clear stale property values when this is a fresh lifecycle — i.e., + # $state=disconnected/lost already reset _received_description to False. + # This means the panel rebooted while we were connected. On a pure + # MQTT reconnect (no panel reboot) the broker re-delivers the retained + # $description, but _received_description is still True from the + # previous session so we skip the clear — the retained property + # messages will carry the correct (unchanged) values. + if not self._received_description: + self._property_values.clear() + self._property_timestamps.clear() + self._target_values.clear() + self._generation += 1 + _LOGGER.debug("Cleared stale property values (generation %d)", self._generation) + self._received_description = True self._node_types.clear() @@ -220,7 +248,11 @@ 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", len(self._node_types)) + _LOGGER.debug( + "Parsed $description with %d nodes (generation %d)", + len(self._node_types), + self._generation, + ) # Lifecycle transition if self._received_state_ready: diff --git a/src/span_panel_api/mqtt/homie.py b/src/span_panel_api/mqtt/homie.py index ccd0776..fbff334 100644 --- a/src/span_panel_api/mqtt/homie.py +++ b/src/span_panel_api/mqtt/homie.py @@ -64,6 +64,7 @@ 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 @@ -118,6 +119,12 @@ 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: diff --git a/tests/test_accumulator.py b/tests/test_accumulator.py index 8a08d03..21bc76c 100644 --- a/tests/test_accumulator.py +++ b/tests/test_accumulator.py @@ -163,6 +163,131 @@ def test_init_state_moves_to_connected(self): assert acc.lifecycle == HomieLifecycle.CONNECTED +# --------------------------------------------------------------------------- +# Panel reboot: $description clears stale property values +# --------------------------------------------------------------------------- + + +class TestDescriptionClearsProperties: + """Verify that a panel reboot (disconnected -> $description) clears stale data. + + The clear only happens when _received_description is False, which requires + a $state=disconnected (or lost) to have reset the lifecycle first. A + re-delivered retained $description on a pure network reconnect does NOT + clear, because _received_description is still True from the previous session. + """ + + def _simulate_reboot(self, acc: HomiePropertyAccumulator) -> None: + """Simulate the panel reboot lifecycle transition.""" + acc.handle_message(f"{PREFIX}/$state", "disconnected") + acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC) + acc.handle_message(f"{PREFIX}/$state", "ready") + + def test_description_clears_property_values_on_reboot(self): + """A panel reboot must clear property values from the previous lifecycle.""" + acc = HomiePropertyAccumulator(SERIAL) + _make_ready(acc) + acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000") + assert acc.get_prop("circuit-1", "exported-energy") == "1000" + + # Panel reboots — $state=disconnected resets lifecycle, then $description clears + self._simulate_reboot(acc) + assert acc.get_prop("circuit-1", "exported-energy") == "" + + def test_description_clears_timestamps_on_reboot(self): + """Timestamps must also be cleared so stale timing data doesn't persist.""" + acc = HomiePropertyAccumulator(SERIAL) + _make_ready(acc) + acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000") + assert acc.get_timestamp("circuit-1", "exported-energy") > 0 + + self._simulate_reboot(acc) + assert acc.get_timestamp("circuit-1", "exported-energy") == 0 + + def test_description_clears_target_values_on_reboot(self): + """Target values must also be cleared on panel reboot.""" + acc = HomiePropertyAccumulator(SERIAL) + _make_ready(acc) + acc.handle_message(f"{PREFIX}/circuit-1/relay/$target", "OPEN") + assert acc.get_target("circuit-1", "relay") == "OPEN" + + self._simulate_reboot(acc) + assert acc.get_target("circuit-1", "relay") is None + + def test_reboot_increments_generation(self): + """Each panel reboot must advance the generation counter.""" + acc = HomiePropertyAccumulator(SERIAL) + assert acc.generation == 0 + + # First boot + acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC) + assert acc.generation == 1 + + # Reboot + acc.handle_message(f"{PREFIX}/$state", "disconnected") + acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC) + assert acc.generation == 2 + + def test_retained_redescription_does_not_clear(self): + """A re-delivered retained $description without a disconnect must NOT clear.""" + acc = HomiePropertyAccumulator(SERIAL) + _make_ready(acc) + acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000") + + # Simulate network reconnect — $description re-delivered without $state=disconnected + acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC) + + # Property values should be preserved + assert acc.get_prop("circuit-1", "exported-energy") == "1000" + assert acc.generation == 1 # still 1 from initial boot, no increment on re-delivery + + def test_fresh_properties_available_after_reboot(self): + """Post-reboot properties should be stored normally after clear.""" + acc = HomiePropertyAccumulator(SERIAL) + _make_ready(acc) + acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000") + + self._simulate_reboot(acc) + + # Fresh post-reboot value + acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "50") + assert acc.get_prop("circuit-1", "exported-energy") == "50" + + def test_fresh_target_values_available_after_reboot(self): + """Post-reboot target values should be stored normally after clear.""" + acc = HomiePropertyAccumulator(SERIAL) + _make_ready(acc) + acc.handle_message(f"{PREFIX}/circuit-1/relay/$target", "OPEN") + assert acc.get_target("circuit-1", "relay") == "OPEN" + + self._simulate_reboot(acc) + + # Fresh post-reboot target value + acc.handle_message(f"{PREFIX}/circuit-1/relay/$target", "CLOSED") + assert acc.get_target("circuit-1", "relay") == "CLOSED" + + def test_fast_reboot_without_lwt_still_clears(self): + """Panel reboots so fast that $state=disconnected (LWT) is skipped. + + The panel goes directly from ready -> init -> description -> ready. + $state=init must reset _received_description so the subsequent + $description triggers the property clear. + """ + acc = HomiePropertyAccumulator(SERIAL) + _make_ready(acc) + acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000") + gen_before = acc.generation + + # Fast reboot: no $state=disconnected, straight to init + acc.handle_message(f"{PREFIX}/$state", "init") + acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC) + acc.handle_message(f"{PREFIX}/$state", "ready") + + # Property values should be cleared + assert acc.get_prop("circuit-1", "exported-energy") == "" + assert acc.generation == gen_before + 1 + + # --------------------------------------------------------------------------- # Lifecycle: invalid JSON description # --------------------------------------------------------------------------- diff --git a/tests/test_mqtt_homie.py b/tests/test_mqtt_homie.py index 473a6d7..79a2878 100644 --- a/tests/test_mqtt_homie.py +++ b/tests/test_mqtt_homie.py @@ -905,6 +905,29 @@ def test_description_change_triggers_full_rebuild(self): snap2 = consumer.build_snapshot() assert snap2 is not snap1 + def test_description_invalidates_cache_via_generation(self): + """Panel reboot ($description) must clear cached snapshot so stale data is not reused.""" + acc, consumer = _build_ready_consumer() + node = "aabbccdd-1122-3344-5566-778899001122" + + # Pre-reboot: circuit has energy = 1000 + acc.handle_message(f"{PREFIX}/{node}/exported-energy", "1000") + snap_pre = consumer.build_snapshot() + circuit_id = "aabbccdd112233445566778899001122" + assert snap_pre.circuits[circuit_id].consumed_energy_wh == 1000.0 + + # Panel reboots — $state=disconnected resets lifecycle, $description clears values + acc.handle_message(f"{PREFIX}/$state", "disconnected") + acc.handle_message(f"{PREFIX}/$description", _make_description(_full_description())) + acc.handle_message(f"{PREFIX}/$state", "ready") + + # Post-reboot: circuit publishes reset energy = 50 + acc.handle_message(f"{PREFIX}/{node}/exported-energy", "50") + snap_post = consumer.build_snapshot() + + # Must reflect post-reboot value, not stale pre-reboot cache + assert snap_post.circuits[circuit_id].consumed_energy_wh == 50.0 + # --------------------------------------------------------------------------- # HomieDeviceConsumer — property callbacks diff --git a/uv.lock b/uv.lock index 3e40b34..c8d8396 100644 --- a/uv.lock +++ b/uv.lock @@ -130,43 +130,31 @@ sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8 wheels = [ { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, @@ -438,33 +426,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, - { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" }, { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" }, { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" }, { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" }, { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" }, { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" }, - { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" }, { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" }, { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" }, { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" }, { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, - { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, - { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, @@ -1310,7 +1292,7 @@ wheels = [ [[package]] name = "span-panel-api" -version = "2.5.1" +version = "2.5.2" source = { editable = "." } dependencies = [ { name = "httpx" }, From 574d2ee2aabc5b7c33ebe2bf30a0e68dca9a1a3d Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:30:22 -0700 Subject: [PATCH 2/3] add changelog entry for v2.5.2 --- CHANGELOG.md | 55 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66f77ac..d7e124d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ 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.2] - 04/2026 + +### Fixed + +- **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. + ## [2.5.1] - 04/2026 ### Fixed @@ -326,24 +334,29 @@ 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.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.2** | 04/2026 | MQTT/Homie | Clear stale property values on panel reboot; fast reboot detection; cache generation invalidation | +| **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 | From 9db07ee84ef7769cd948e32b40b4ff33f01d5bd0 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:35:57 -0700 Subject: [PATCH 3/3] fix: always transition lifecycle out of READY on non-ready $state Address PR review feedback: - _handle_state else branch now unconditionally sets lifecycle to CONNECTED, fixing a bug where is_ready() could return True when the device was in init/sleeping/alert state - Updated property-clear comment to reflect all reset triggers - Fixed generation docstring to match actual increment semantics --- src/span_panel_api/mqtt/accumulator.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/span_panel_api/mqtt/accumulator.py b/src/span_panel_api/mqtt/accumulator.py index ea45a95..76d38f0 100644 --- a/src/span_panel_api/mqtt/accumulator.py +++ b/src/span_panel_api/mqtt/accumulator.py @@ -85,7 +85,7 @@ def ready_since(self) -> float: @property def generation(self) -> int: - """Counter incremented on each $description (panel reboot).""" + """Counter incremented on the initial $description and after lifecycle resets.""" return self._generation def is_ready(self) -> bool: @@ -201,12 +201,14 @@ def _handle_state(self, payload: str) -> None: 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. - if self._lifecycle == HomieLifecycle.DISCONNECTED: - self._lifecycle = HomieLifecycle.CONNECTED + self._lifecycle = HomieLifecycle.CONNECTED self._received_state_ready = False self._received_description = False @@ -220,13 +222,13 @@ def _handle_description(self, payload: str) -> None: _LOGGER.warning("Invalid $description JSON") return - # Clear stale property values when this is a fresh lifecycle — i.e., - # $state=disconnected/lost already reset _received_description to False. + # _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. # This means the panel rebooted while we were connected. On a pure - # MQTT reconnect (no panel reboot) the broker re-delivers the retained - # $description, but _received_description is still True from the - # previous session so we skip the clear — the retained property - # messages will carry the correct (unchanged) values. + # MQTT reconnect (no panel reboot), _received_description is still + # True from the previous session so we skip the clear — the retained + # property messages will carry the correct (unchanged) values. if not self._received_description: self._property_values.clear() self._property_timestamps.clear()