span-panel-api 2.5.1__tar.gz → 2.5.3__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.
Files changed (80) hide show
  1. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/CHANGELOG.md +45 -21
  2. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/PKG-INFO +1 -1
  3. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/pyproject.toml +1 -1
  4. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/mqtt/accumulator.py +44 -4
  5. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/mqtt/homie.py +7 -0
  6. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/test_accumulator.py +131 -0
  7. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/test_mqtt_homie.py +28 -0
  8. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/uv.lock +41 -41
  9. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.codefactor +0 -0
  10. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.codefactor.yml +0 -0
  11. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.deps-installed +0 -0
  12. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  13. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  14. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.github/dependabot.yml +0 -0
  15. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.github/workflows/ci.yml +0 -0
  16. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.github/workflows/dependabot-auto-approve.yml +0 -0
  17. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.github/workflows/dependabot-auto-merge.yml +0 -0
  18. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.github/workflows/release.yml +0 -0
  19. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.gitignore +0 -0
  20. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.markdownlint-cli2.jsonc +0 -0
  21. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.markdownlint.json +0 -0
  22. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.pre-commit-config.yaml +0 -0
  23. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.prettierrc.json +0 -0
  24. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.vscode/extensions.json +0 -0
  25. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/.vscode/tasks.json +0 -0
  26. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/DEVELOPMENT.md +0 -0
  27. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/LICENSE +0 -0
  28. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/README.md +0 -0
  29. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/SECURITY.md +0 -0
  30. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/conftest.py +0 -0
  31. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/developer_attribute_readme.md +0 -0
  32. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/openapi.json +0 -0
  33. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/pytest.ini +0 -0
  34. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/pytest_output.log +0 -0
  35. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/scripts/__init__.py +0 -0
  36. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/scripts/coverage.py +0 -0
  37. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/scripts/format.sh +0 -0
  38. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/scripts/format_markdown.py +0 -0
  39. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/scripts/test_live_auth.py +0 -0
  40. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/setup-hooks.sh +0 -0
  41. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/__init__.py +0 -0
  42. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/_http.py +0 -0
  43. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/auth.py +0 -0
  44. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/const.py +0 -0
  45. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/detection.py +0 -0
  46. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/exceptions.py +0 -0
  47. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/factory.py +0 -0
  48. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/models.py +0 -0
  49. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/mqtt/__init__.py +0 -0
  50. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/mqtt/async_client.py +0 -0
  51. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/mqtt/client.py +0 -0
  52. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/mqtt/connection.py +0 -0
  53. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/mqtt/const.py +0 -0
  54. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/mqtt/field_metadata.py +0 -0
  55. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/mqtt/models.py +0 -0
  56. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/phase_validation.py +0 -0
  57. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/protocol.py +0 -0
  58. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/src/span_panel_api/py.typed +0 -0
  59. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/conftest.py +0 -0
  60. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
  61. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
  62. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
  63. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/fixtures/v2/README.md +0 -0
  64. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/fixtures/v2/homie_schema.json +0 -0
  65. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/fixtures/v2/status.json +0 -0
  66. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/simulation_fixtures/circuits.response.txt +0 -0
  67. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/simulation_fixtures/panel.response.txt +0 -0
  68. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/simulation_fixtures/soe.response.txt +0 -0
  69. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/simulation_fixtures/status.response.txt +0 -0
  70. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/test_async_mqtt_client.py +0 -0
  71. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/test_auth_and_homie_helpers.py +0 -0
  72. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/test_detection_auth.py +0 -0
  73. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/test_field_metadata.py +0 -0
  74. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/test_mqtt_bridge.py +0 -0
  75. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/test_mqtt_connect_flow.py +0 -0
  76. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/test_mqtt_debounce.py +0 -0
  77. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/test_phase_validation_configs.py +0 -0
  78. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/test_phase_validation_errors.py +0 -0
  79. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/test_protocol_conformance.py +0 -0
  80. {span_panel_api-2.5.1 → span_panel_api-2.5.3}/tests/test_protocol_models.py +0 -0
@@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  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).
6
6
 
7
+ ## [2.5.3] - 04/2026
8
+
9
+ ### Fixed
10
+
11
+ - **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
12
+ 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.
13
+ - **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.
14
+
15
+ ## [2.5.2] - 04/2026 (retired)
16
+
17
+ > **Retired:** The property-clearing behavior introduced in this release caused false energy dip spikes. Superseded by 2.5.3.
18
+
19
+ ### Fixed
20
+
21
+ - **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
22
+ building the next snapshot.
23
+ - **Snapshot cache invalidated on reboot** — the snapshot cache is now discarded when a reboot is detected, forcing a full rebuild from fresh data.
24
+
7
25
  ## [2.5.1] - 04/2026
8
26
 
9
27
  ### Fixed
@@ -326,24 +344,30 @@ Package versions prior to 2.0.0 depend on the SPAN v1 REST API. SPAN will sunset
326
344
 
327
345
  ## Version History Summary
328
346
 
329
- | Version | Date | Transport | Summary |
330
- | ---------- | ------- | ---------- | ---------------------------------------------------------------------------------- |
331
- | **2.4.0** | 03/2026 | MQTT/Homie | proximityProven, injected HTTP client, executor file I/O, type alias, test cleanup |
332
- | **2.3.2** | 03/2026 | MQTT/Homie | FQDN management endpoints |
333
- | **2.3.1** | 03/2026 | MQTT/Homie | MQTT connection errors wrapped as SpanPanelConnectionError |
334
- | **2.3.0** | 03/2026 | MQTT/Homie | Simulation engine removed |
335
- | **2.2.4** | 03/2026 | MQTT/Homie | Negative zero fix on idle circuits |
336
- | **2.2.3** | 03/2026 | MQTT/Homie | Panel size from Homie schema; `panel_size` always populated on snapshot |
337
- | **2.0.2** | 03/2026 | MQTT/Homie | EVSE (EV charger) snapshot model, Homie parsing, simulation support |
338
- | **2.0.1** | 03/2026 | MQTT/Homie | Full BESS metadata parsing, README documentation |
339
- | **2.0.0** | 02/2026 | MQTT/Homie | Ground-up rewrite: MQTT-only, protocol-based API, real-time push, PV/BESS metadata |
340
- | **1.1.14** | 12/2025 | REST | Keep-Alive and RemoteProtocolError handling |
341
- | **1.1.9** | 9/2025 | REST | Simulation sign corrections |
342
- | **1.1.8** | 2024 | REST | Simulation power sign fix |
343
- | **1.1.6** | 2024 | REST | YAML simulation API, battery simulation |
344
- | **1.1.5** | 2024 | REST | Simulation edge cases |
345
- | **1.1.4** | 2024 | REST | Formatting and linting |
346
- | **1.1.3** | 2024 | REST | Test and lint fixes |
347
- | **1.1.2** | 2024 | REST | Simulation mode added |
348
- | **1.1.1** | 2024 | REST | Dependency updates |
349
- | **1.1.0** | 2024 | REST | Initial release |
347
+ | Version | Date | Transport | Summary |
348
+ | ---------- | ------- | ---------- | ----------------------------------------------------------------------------------------- |
349
+ | **2.5.3** | 04/2026 | MQTT/Homie | Preserve property values on lifecycle reset; fix false energy dip spikes from 2.5.2 clear |
350
+ | **2.5.2** | 04/2026 | MQTT/Homie | _(retired)_ Clear stale property values on panel reboot; caused false energy dip spikes |
351
+ | **2.5.1** | 04/2026 | MQTT/Homie | Replace assert with RuntimeError; fix bandit pre-commit hook |
352
+ | **2.5.0** | 03/2026 | MQTT/Homie | Homie accumulator layer, $target support, dirty-node snapshot caching |
353
+ | **2.4.2** | 03/2026 | MQTT/Homie | SSL context creation moved to executor |
354
+ | **2.4.1** | 03/2026 | MQTT/Homie | License metadata, loosened httpx constraint |
355
+ | **2.4.0** | 03/2026 | MQTT/Homie | proximityProven, injected HTTP client, executor file I/O, type alias, test cleanup |
356
+ | **2.3.2** | 03/2026 | MQTT/Homie | FQDN management endpoints |
357
+ | **2.3.1** | 03/2026 | MQTT/Homie | MQTT connection errors wrapped as SpanPanelConnectionError |
358
+ | **2.3.0** | 03/2026 | MQTT/Homie | Simulation engine removed |
359
+ | **2.2.4** | 03/2026 | MQTT/Homie | Negative zero fix on idle circuits |
360
+ | **2.2.3** | 03/2026 | MQTT/Homie | Panel size from Homie schema; `panel_size` always populated on snapshot |
361
+ | **2.0.2** | 03/2026 | MQTT/Homie | EVSE (EV charger) snapshot model, Homie parsing, simulation support |
362
+ | **2.0.1** | 03/2026 | MQTT/Homie | Full BESS metadata parsing, README documentation |
363
+ | **2.0.0** | 02/2026 | MQTT/Homie | Ground-up rewrite: MQTT-only, protocol-based API, real-time push, PV/BESS metadata |
364
+ | **1.1.14** | 12/2025 | REST | Keep-Alive and RemoteProtocolError handling |
365
+ | **1.1.9** | 9/2025 | REST | Simulation sign corrections |
366
+ | **1.1.8** | 2024 | REST | Simulation power sign fix |
367
+ | **1.1.6** | 2024 | REST | YAML simulation API, battery simulation |
368
+ | **1.1.5** | 2024 | REST | Simulation edge cases |
369
+ | **1.1.4** | 2024 | REST | Formatting and linting |
370
+ | **1.1.3** | 2024 | REST | Test and lint fixes |
371
+ | **1.1.2** | 2024 | REST | Simulation mode added |
372
+ | **1.1.1** | 2024 | REST | Dependency updates |
373
+ | **1.1.0** | 2024 | REST | Initial release |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: span-panel-api
3
- Version: 2.5.1
3
+ Version: 2.5.3
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "span-panel-api"
3
- version = "2.5.1"
3
+ version = "2.5.3"
4
4
  description = "A client library for SPAN Panel API"
5
5
  authors = [
6
6
  {name = "SpanPanel"}
@@ -56,6 +56,11 @@ class HomiePropertyAccumulator:
56
56
  # Node type mapping from $description
57
57
  self._node_types: dict[str, str] = {}
58
58
 
59
+ # Generation counter — incremented whenever a new lifecycle's
60
+ # $description is accepted (including initial boot) so consumers
61
+ # can invalidate snapshot caches built from prior-lifecycle data.
62
+ self._generation: int = 0
63
+
59
64
  # Dirty tracking
60
65
  self._dirty_nodes: set[str] = set()
61
66
 
@@ -79,6 +84,11 @@ class HomiePropertyAccumulator:
79
84
  """Monotonic timestamp of the last READY transition, 0.0 if never ready."""
80
85
  return self._ready_since
81
86
 
87
+ @property
88
+ def generation(self) -> int:
89
+ """Counter incremented on each new lifecycle's ``$description`` to invalidate consumer caches."""
90
+ return self._generation
91
+
82
92
  def is_ready(self) -> bool:
83
93
  """True when lifecycle is READY."""
84
94
  return self._lifecycle == HomieLifecycle.READY
@@ -191,10 +201,17 @@ class HomiePropertyAccumulator:
191
201
  self._received_state_ready = False
192
202
  self._received_description = False
193
203
  else:
194
- # init, sleeping, alert, etc. — connected but not ready
195
- if self._lifecycle == HomieLifecycle.DISCONNECTED:
196
- self._lifecycle = HomieLifecycle.CONNECTED
204
+ # init, sleeping, alert, etc. — connected but not ready.
205
+ # Always move out of READY/DESCRIPTION_RECEIVED into a
206
+ # non-ready connected lifecycle state.
207
+ #
208
+ # Reset _received_description so that the upcoming $description
209
+ # triggers a property clear. This covers fast reboots where
210
+ # the broker's LWT ($state=disconnected) may not reach us
211
+ # before the panel publishes $state=init.
212
+ self._lifecycle = HomieLifecycle.CONNECTED
197
213
  self._received_state_ready = False
214
+ self._received_description = False
198
215
 
199
216
  _LOGGER.debug("Homie $state: %s → lifecycle=%s", payload, self._lifecycle.value)
200
217
 
@@ -206,6 +223,25 @@ class HomiePropertyAccumulator:
206
223
  _LOGGER.warning("Invalid $description JSON")
207
224
  return
208
225
 
226
+ # _handle_state() already reset _received_description to False due to
227
+ # a state change that starts a new panel lifecycle, including
228
+ # $state=disconnected/lost and other non-ready states such as init.
229
+ # Increment the generation counter to invalidate consumer snapshot
230
+ # caches, but preserve property values — pre-reboot readings serve
231
+ # as safe placeholders until the panel re-publishes. Clearing values
232
+ # would emit 0.0 for energy counters via _parse_float(""), triggering
233
+ # false dip-compensation offsets in the integration.
234
+ #
235
+ # On a pure MQTT reconnect (no panel reboot), _received_description
236
+ # is still True from the previous session so we skip this block —
237
+ # the retained property messages carry the correct (unchanged) values.
238
+ if not self._received_description:
239
+ self._generation += 1
240
+ _LOGGER.debug(
241
+ "Panel reboot detected (generation %d); preserving property values as placeholders",
242
+ self._generation,
243
+ )
244
+
209
245
  self._received_description = True
210
246
  self._node_types.clear()
211
247
 
@@ -220,7 +256,11 @@ class HomiePropertyAccumulator:
220
256
  # Mark all known nodes dirty
221
257
  self._dirty_nodes.update(self._node_types.keys())
222
258
 
223
- _LOGGER.debug("Parsed $description with %d nodes", len(self._node_types))
259
+ _LOGGER.debug(
260
+ "Parsed $description with %d nodes (generation %d)",
261
+ len(self._node_types),
262
+ self._generation,
263
+ )
224
264
 
225
265
  # Lifecycle transition
226
266
  if self._received_state_ready:
@@ -64,6 +64,7 @@ class HomieDeviceConsumer:
64
64
  self._acc = accumulator
65
65
  self._panel_size = panel_size
66
66
  self._cached_snapshot: SpanPanelSnapshot | None = None
67
+ self._cache_generation: int = 0
67
68
 
68
69
  # -- Delegation to accumulator -------------------------------------------
69
70
  # These thin wrappers allow SpanMqttClient (and legacy test code) to
@@ -118,6 +119,12 @@ class HomieDeviceConsumer:
118
119
 
119
120
  Must be called after accumulator is_ready() returns True.
120
121
  """
122
+ # Invalidate cache when the accumulator generation advances
123
+ # (panel reboot cleared all property values).
124
+ if self._acc.generation != self._cache_generation:
125
+ self._cached_snapshot = None
126
+ self._cache_generation = self._acc.generation
127
+
121
128
  dirty = self._acc.dirty_node_ids()
122
129
 
123
130
  if not dirty and self._cached_snapshot is not None:
@@ -163,6 +163,137 @@ class TestLifecycleDisconnection:
163
163
  assert acc.lifecycle == HomieLifecycle.CONNECTED
164
164
 
165
165
 
166
+ # ---------------------------------------------------------------------------
167
+ # Panel reboot: $description preserves property values as placeholders
168
+ # ---------------------------------------------------------------------------
169
+
170
+
171
+ class TestDescriptionOnReboot:
172
+ """Verify that a panel reboot preserves property values as placeholders.
173
+
174
+ On lifecycle reset ($state=disconnected/lost or non-ready states like init),
175
+ _received_description is set to False. The subsequent $description increments
176
+ the generation counter (invalidating consumer snapshot caches) but does NOT
177
+ clear property values — pre-reboot values serve as safe placeholders until
178
+ the panel re-publishes fresh data.
179
+
180
+ A re-delivered retained $description on a pure network reconnect does NOT
181
+ increment generation, because _received_description is still True.
182
+ """
183
+
184
+ def _simulate_reboot(self, acc: HomiePropertyAccumulator) -> None:
185
+ """Simulate the panel reboot lifecycle transition."""
186
+ acc.handle_message(f"{PREFIX}/$state", "disconnected")
187
+ acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC)
188
+ acc.handle_message(f"{PREFIX}/$state", "ready")
189
+
190
+ def test_reboot_preserves_property_values(self):
191
+ """A panel reboot must preserve property values as placeholders."""
192
+ acc = HomiePropertyAccumulator(SERIAL)
193
+ _make_ready(acc)
194
+ acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000")
195
+ assert acc.get_prop("circuit-1", "exported-energy") == "1000"
196
+
197
+ # Panel reboots — $state=disconnected resets lifecycle, then $description preserves
198
+ self._simulate_reboot(acc)
199
+ assert acc.get_prop("circuit-1", "exported-energy") == "1000"
200
+
201
+ def test_reboot_preserves_timestamps(self):
202
+ """Timestamps must be preserved on reboot so callers can detect stale data if needed."""
203
+ acc = HomiePropertyAccumulator(SERIAL)
204
+ _make_ready(acc)
205
+ acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000")
206
+ ts_before = acc.get_timestamp("circuit-1", "exported-energy")
207
+ assert ts_before > 0
208
+
209
+ self._simulate_reboot(acc)
210
+ assert acc.get_timestamp("circuit-1", "exported-energy") == ts_before
211
+
212
+ def test_reboot_preserves_target_values(self):
213
+ """Target values must also be preserved on panel reboot."""
214
+ acc = HomiePropertyAccumulator(SERIAL)
215
+ _make_ready(acc)
216
+ acc.handle_message(f"{PREFIX}/circuit-1/relay/$target", "OPEN")
217
+ assert acc.get_target("circuit-1", "relay") == "OPEN"
218
+
219
+ self._simulate_reboot(acc)
220
+ assert acc.get_target("circuit-1", "relay") == "OPEN"
221
+
222
+ def test_reboot_increments_generation(self):
223
+ """Each panel reboot must advance the generation counter."""
224
+ acc = HomiePropertyAccumulator(SERIAL)
225
+ assert acc.generation == 0
226
+
227
+ # First boot
228
+ acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC)
229
+ assert acc.generation == 1
230
+
231
+ # Reboot
232
+ acc.handle_message(f"{PREFIX}/$state", "disconnected")
233
+ acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC)
234
+ assert acc.generation == 2
235
+
236
+ def test_retained_redescription_preserves_values_without_generation_bump(self):
237
+ """A re-delivered retained $description without a disconnect must NOT bump generation."""
238
+ acc = HomiePropertyAccumulator(SERIAL)
239
+ _make_ready(acc)
240
+ acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000")
241
+
242
+ # Simulate network reconnect — $description re-delivered without $state=disconnected
243
+ acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC)
244
+
245
+ # Property values should be preserved and generation must not increment
246
+ assert acc.get_prop("circuit-1", "exported-energy") == "1000"
247
+ assert acc.generation == 1 # still 1 from initial boot, no increment on re-delivery
248
+
249
+ def test_fresh_properties_overwrite_preserved_after_reboot(self):
250
+ """Post-reboot properties should overwrite the preserved placeholder values."""
251
+ acc = HomiePropertyAccumulator(SERIAL)
252
+ _make_ready(acc)
253
+ acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000")
254
+
255
+ self._simulate_reboot(acc)
256
+
257
+ # Fresh post-reboot value overwrites preserved placeholder
258
+ acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "50")
259
+ assert acc.get_prop("circuit-1", "exported-energy") == "50"
260
+
261
+ def test_fresh_target_values_overwrite_preserved_after_reboot(self):
262
+ """Post-reboot target values should overwrite the preserved placeholder values."""
263
+ acc = HomiePropertyAccumulator(SERIAL)
264
+ _make_ready(acc)
265
+ acc.handle_message(f"{PREFIX}/circuit-1/relay/$target", "OPEN")
266
+ assert acc.get_target("circuit-1", "relay") == "OPEN"
267
+
268
+ self._simulate_reboot(acc)
269
+
270
+ # Fresh post-reboot target value overwrites preserved placeholder
271
+ acc.handle_message(f"{PREFIX}/circuit-1/relay/$target", "CLOSED")
272
+ assert acc.get_target("circuit-1", "relay") == "CLOSED"
273
+
274
+ def test_fast_reboot_without_lwt_preserves_values(self):
275
+ """Panel reboots so fast that $state=disconnected (LWT) is skipped.
276
+
277
+ The panel goes directly from ready -> init -> description -> ready.
278
+ $state=init must reset _received_description so the subsequent
279
+ $description increments the generation counter but does NOT clear
280
+ property values — pre-reboot values serve as safe placeholders.
281
+ """
282
+ acc = HomiePropertyAccumulator(SERIAL)
283
+ _make_ready(acc)
284
+ acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000")
285
+ gen_before = acc.generation
286
+
287
+ # Fast reboot: no $state=disconnected, straight to init
288
+ acc.handle_message(f"{PREFIX}/$state", "init")
289
+ acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC)
290
+ acc.handle_message(f"{PREFIX}/$state", "ready")
291
+
292
+ # Property values should be preserved
293
+ assert acc.get_prop("circuit-1", "exported-energy") == "1000"
294
+ assert acc.generation == gen_before + 1
295
+
296
+
166
297
  # ---------------------------------------------------------------------------
167
298
  # Lifecycle: invalid JSON description
168
299
  # ---------------------------------------------------------------------------
@@ -905,6 +905,34 @@ class TestSnapshotCaching:
905
905
  snap2 = consumer.build_snapshot()
906
906
  assert snap2 is not snap1
907
907
 
908
+ def test_description_invalidates_cache_via_generation(self):
909
+ """Panel reboot ($description) must invalidate cached snapshot but preserve values."""
910
+ acc, consumer = _build_ready_consumer()
911
+ node = "aabbccdd-1122-3344-5566-778899001122"
912
+
913
+ # Pre-reboot: circuit has energy = 1000
914
+ acc.handle_message(f"{PREFIX}/{node}/exported-energy", "1000")
915
+ snap_pre = consumer.build_snapshot()
916
+ circuit_id = "aabbccdd112233445566778899001122"
917
+ assert snap_pre.circuits[circuit_id].consumed_energy_wh == 1000.0
918
+
919
+ # Panel reboots — $state=disconnected resets lifecycle, $description preserves values
920
+ acc.handle_message(f"{PREFIX}/$state", "disconnected")
921
+ acc.handle_message(f"{PREFIX}/$description", _make_description(_full_description()))
922
+ acc.handle_message(f"{PREFIX}/$state", "ready")
923
+
924
+ # Before new property messages: snapshot uses preserved pre-reboot value
925
+ snap_preserved = consumer.build_snapshot()
926
+ assert snap_preserved.circuits[circuit_id].consumed_energy_wh == 1000.0
927
+ assert snap_preserved is not snap_pre # cache was invalidated
928
+
929
+ # Post-reboot: circuit publishes new value
930
+ acc.handle_message(f"{PREFIX}/{node}/exported-energy", "50")
931
+ snap_post = consumer.build_snapshot()
932
+
933
+ # Must reflect post-reboot value
934
+ assert snap_post.circuits[circuit_id].consumed_energy_wh == 50.0
935
+
908
936
 
909
937
  # ---------------------------------------------------------------------------
910
938
  # HomieDeviceConsumer — property callbacks
@@ -427,51 +427,51 @@ toml = [
427
427
 
428
428
  [[package]]
429
429
  name = "cryptography"
430
- version = "46.0.6"
430
+ version = "46.0.7"
431
431
  source = { registry = "https://pypi.org/simple" }
432
432
  dependencies = [
433
433
  { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
434
434
  { name = "typing-extensions", marker = "python_full_version < '3.11'" },
435
435
  ]
436
- sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" }
437
- wheels = [
438
- { 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" },
439
- { 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" },
440
- { 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" },
441
- { 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" },
442
- { 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" },
443
- { 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" },
444
- { 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" },
445
- { 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" },
446
- { 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" },
447
- { 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" },
448
- { 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" },
449
- { 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" },
450
- { 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" },
451
- { 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" },
452
- { 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" },
453
- { 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" },
454
- { 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" },
455
- { 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" },
456
- { 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" },
457
- { 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" },
458
- { 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" },
459
- { 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" },
460
- { 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" },
461
- { 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" },
462
- { 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" },
463
- { 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" },
464
- { 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" },
465
- { 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" },
466
- { 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" },
467
- { 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" },
468
- { 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" },
469
- { 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" },
470
- { 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" },
471
- { url = "https://files.pythonhosted.org/packages/bc/1f/4c926f50df7749f000f20eede0c896769509895e2648db5da0ed55db711d/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8", size = 4218227, upload-time = "2026-03-25T23:34:40.871Z" },
472
- { url = "https://files.pythonhosted.org/packages/c6/65/707be3ffbd5f786028665c3223e86e11c4cda86023adbc56bd72b1b6bab5/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0", size = 4381399, upload-time = "2026-03-25T23:34:42.609Z" },
473
- { url = "https://files.pythonhosted.org/packages/f3/6d/73557ed0ef7d73d04d9aba745d2c8e95218213687ee5e76b7d236a5030fc/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b", size = 4217595, upload-time = "2026-03-25T23:34:44.205Z" },
474
- { url = "https://files.pythonhosted.org/packages/9e/c5/e1594c4eec66a567c3ac4400008108a415808be2ce13dcb9a9045c92f1a0/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a", size = 4380912, upload-time = "2026-03-25T23:34:46.328Z" },
436
+ sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
437
+ wheels = [
438
+ { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
439
+ { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
440
+ { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
441
+ { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
442
+ { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
443
+ { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
444
+ { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
445
+ { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
446
+ { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
447
+ { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
448
+ { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
449
+ { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
450
+ { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
451
+ { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
452
+ { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" },
453
+ { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" },
454
+ { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" },
455
+ { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" },
456
+ { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" },
457
+ { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
458
+ { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
459
+ { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
460
+ { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
461
+ { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
462
+ { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
463
+ { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
464
+ { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
465
+ { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
466
+ { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
467
+ { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
468
+ { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
469
+ { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
470
+ { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
471
+ { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" },
472
+ { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" },
473
+ { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" },
474
+ { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" },
475
475
  ]
476
476
 
477
477
  [[package]]
@@ -1310,7 +1310,7 @@ wheels = [
1310
1310
 
1311
1311
  [[package]]
1312
1312
  name = "span-panel-api"
1313
- version = "2.5.1"
1313
+ version = "2.5.3"
1314
1314
  source = { editable = "." }
1315
1315
  dependencies = [
1316
1316
  { name = "httpx" },
File without changes
File without changes