span-panel-api 2.5.2__tar.gz → 2.5.4__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.2 → span_panel_api-2.5.4}/CHANGELOG.md +47 -27
  2. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/PKG-INFO +1 -1
  3. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/pyproject.toml +1 -1
  4. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/mqtt/accumulator.py +4 -38
  5. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/mqtt/homie.py +0 -7
  6. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/test_accumulator.py +68 -80
  7. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/test_mqtt_homie.py +19 -14
  8. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/uv.lock +35 -35
  9. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.codefactor +0 -0
  10. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.codefactor.yml +0 -0
  11. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.deps-installed +0 -0
  12. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  13. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  14. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.github/dependabot.yml +0 -0
  15. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.github/workflows/ci.yml +0 -0
  16. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.github/workflows/dependabot-auto-approve.yml +0 -0
  17. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.github/workflows/dependabot-auto-merge.yml +0 -0
  18. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.github/workflows/release.yml +0 -0
  19. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.gitignore +0 -0
  20. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.markdownlint-cli2.jsonc +0 -0
  21. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.markdownlint.json +0 -0
  22. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.pre-commit-config.yaml +0 -0
  23. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.prettierrc.json +0 -0
  24. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.vscode/extensions.json +0 -0
  25. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/.vscode/tasks.json +0 -0
  26. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/DEVELOPMENT.md +0 -0
  27. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/LICENSE +0 -0
  28. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/README.md +0 -0
  29. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/SECURITY.md +0 -0
  30. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/conftest.py +0 -0
  31. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/developer_attribute_readme.md +0 -0
  32. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/openapi.json +0 -0
  33. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/pytest.ini +0 -0
  34. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/pytest_output.log +0 -0
  35. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/scripts/__init__.py +0 -0
  36. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/scripts/coverage.py +0 -0
  37. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/scripts/format.sh +0 -0
  38. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/scripts/format_markdown.py +0 -0
  39. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/scripts/test_live_auth.py +0 -0
  40. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/setup-hooks.sh +0 -0
  41. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/__init__.py +0 -0
  42. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/_http.py +0 -0
  43. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/auth.py +0 -0
  44. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/const.py +0 -0
  45. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/detection.py +0 -0
  46. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/exceptions.py +0 -0
  47. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/factory.py +0 -0
  48. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/models.py +0 -0
  49. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/mqtt/__init__.py +0 -0
  50. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/mqtt/async_client.py +0 -0
  51. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/mqtt/client.py +0 -0
  52. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/mqtt/connection.py +0 -0
  53. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/mqtt/const.py +0 -0
  54. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/mqtt/field_metadata.py +0 -0
  55. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/mqtt/models.py +0 -0
  56. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/phase_validation.py +0 -0
  57. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/protocol.py +0 -0
  58. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/src/span_panel_api/py.typed +0 -0
  59. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/conftest.py +0 -0
  60. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/fixtures/configs/simulation_config_32_circuit.yaml +0 -0
  61. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/fixtures/configs/simulation_config_40_circuit_with_battery.yaml +0 -0
  62. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/fixtures/configs/simulation_config_8_tab_workshop.yaml +0 -0
  63. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/fixtures/v2/README.md +0 -0
  64. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/fixtures/v2/homie_schema.json +0 -0
  65. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/fixtures/v2/status.json +0 -0
  66. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/simulation_fixtures/circuits.response.txt +0 -0
  67. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/simulation_fixtures/panel.response.txt +0 -0
  68. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/simulation_fixtures/soe.response.txt +0 -0
  69. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/simulation_fixtures/status.response.txt +0 -0
  70. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/test_async_mqtt_client.py +0 -0
  71. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/test_auth_and_homie_helpers.py +0 -0
  72. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/test_detection_auth.py +0 -0
  73. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/test_field_metadata.py +0 -0
  74. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/test_mqtt_bridge.py +0 -0
  75. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/test_mqtt_connect_flow.py +0 -0
  76. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/test_mqtt_debounce.py +0 -0
  77. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/test_phase_validation_configs.py +0 -0
  78. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/test_phase_validation_errors.py +0 -0
  79. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/test_protocol_conformance.py +0 -0
  80. {span_panel_api-2.5.2 → span_panel_api-2.5.4}/tests/test_protocol_models.py +0 -0
@@ -4,7 +4,25 @@ 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.2] - 04/2026
7
+ ## [2.5.4] - 04/2026
8
+
9
+ ### Reverted
10
+
11
+ - **Revert accumulator to 2.5.1 behavior** — the 2.5.2 lifecycle changes (property clearing, unconditional lifecycle transition on `$state=init`, generation counter) caused false energy dip spikes on panel reboots and network interruptions. The 2.5.3
12
+ partial fix (removing the clearing) was insufficient — the unconditional lifecycle disruption on transient `$state=init` events still triggered snapshot pipeline resets that produced 0.0 energy readings. Reverted `accumulator.py` and `homie.py` to their
13
+ stable 2.5.1 state. The existing dirty-node tracking handles reboot transitions correctly without special-case lifecycle management.
14
+
15
+ ## [2.5.3] - 04/2026 (retired)
16
+
17
+ > **Retired:** Partial fix for 2.5.2 — removed property clearing but kept the lifecycle disruption that still caused false dips. Superseded by 2.5.4.
18
+
19
+ ### Fixed
20
+
21
+ - **Preserve property values on lifecycle reset** — removed the property/timestamp/target clearing from `_handle_description()`.
22
+
23
+ ## [2.5.2] - 04/2026 (retired)
24
+
25
+ > **Retired:** Lifecycle changes caused false energy dip spikes. Superseded by 2.5.4.
8
26
 
9
27
  ### Fixed
10
28
 
@@ -334,29 +352,31 @@ Package versions prior to 2.0.0 depend on the SPAN v1 REST API. SPAN will sunset
334
352
 
335
353
  ## Version History Summary
336
354
 
337
- | Version | Date | Transport | Summary |
338
- | ---------- | ------- | ---------- | ------------------------------------------------------------------------------------------------- |
339
- | **2.5.2** | 04/2026 | MQTT/Homie | Clear stale property values on panel reboot; fast reboot detection; cache generation invalidation |
340
- | **2.5.1** | 04/2026 | MQTT/Homie | Replace assert with RuntimeError; fix bandit pre-commit hook |
341
- | **2.5.0** | 03/2026 | MQTT/Homie | Homie accumulator layer, $target support, dirty-node snapshot caching |
342
- | **2.4.2** | 03/2026 | MQTT/Homie | SSL context creation moved to executor |
343
- | **2.4.1** | 03/2026 | MQTT/Homie | License metadata, loosened httpx constraint |
344
- | **2.4.0** | 03/2026 | MQTT/Homie | proximityProven, injected HTTP client, executor file I/O, type alias, test cleanup |
345
- | **2.3.2** | 03/2026 | MQTT/Homie | FQDN management endpoints |
346
- | **2.3.1** | 03/2026 | MQTT/Homie | MQTT connection errors wrapped as SpanPanelConnectionError |
347
- | **2.3.0** | 03/2026 | MQTT/Homie | Simulation engine removed |
348
- | **2.2.4** | 03/2026 | MQTT/Homie | Negative zero fix on idle circuits |
349
- | **2.2.3** | 03/2026 | MQTT/Homie | Panel size from Homie schema; `panel_size` always populated on snapshot |
350
- | **2.0.2** | 03/2026 | MQTT/Homie | EVSE (EV charger) snapshot model, Homie parsing, simulation support |
351
- | **2.0.1** | 03/2026 | MQTT/Homie | Full BESS metadata parsing, README documentation |
352
- | **2.0.0** | 02/2026 | MQTT/Homie | Ground-up rewrite: MQTT-only, protocol-based API, real-time push, PV/BESS metadata |
353
- | **1.1.14** | 12/2025 | REST | Keep-Alive and RemoteProtocolError handling |
354
- | **1.1.9** | 9/2025 | REST | Simulation sign corrections |
355
- | **1.1.8** | 2024 | REST | Simulation power sign fix |
356
- | **1.1.6** | 2024 | REST | YAML simulation API, battery simulation |
357
- | **1.1.5** | 2024 | REST | Simulation edge cases |
358
- | **1.1.4** | 2024 | REST | Formatting and linting |
359
- | **1.1.3** | 2024 | REST | Test and lint fixes |
360
- | **1.1.2** | 2024 | REST | Simulation mode added |
361
- | **1.1.1** | 2024 | REST | Dependency updates |
362
- | **1.1.0** | 2024 | REST | Initial release |
355
+ | Version | Date | Transport | Summary |
356
+ | ---------- | ------- | ---------- | ---------------------------------------------------------------------------------- |
357
+ | **2.5.4** | 04/2026 | MQTT/Homie | Revert accumulator to stable 2.5.1 behavior; fixes false energy dip spikes |
358
+ | **2.5.3** | 04/2026 | MQTT/Homie | _(retired)_ Partial fix still caused false dips from lifecycle disruption |
359
+ | **2.5.2** | 04/2026 | MQTT/Homie | _(retired)_ Lifecycle changes caused false energy dip spikes |
360
+ | **2.5.1** | 04/2026 | MQTT/Homie | Replace assert with RuntimeError; fix bandit pre-commit hook |
361
+ | **2.5.0** | 03/2026 | MQTT/Homie | Homie accumulator layer, $target support, dirty-node snapshot caching |
362
+ | **2.4.2** | 03/2026 | MQTT/Homie | SSL context creation moved to executor |
363
+ | **2.4.1** | 03/2026 | MQTT/Homie | License metadata, loosened httpx constraint |
364
+ | **2.4.0** | 03/2026 | MQTT/Homie | proximityProven, injected HTTP client, executor file I/O, type alias, test cleanup |
365
+ | **2.3.2** | 03/2026 | MQTT/Homie | FQDN management endpoints |
366
+ | **2.3.1** | 03/2026 | MQTT/Homie | MQTT connection errors wrapped as SpanPanelConnectionError |
367
+ | **2.3.0** | 03/2026 | MQTT/Homie | Simulation engine removed |
368
+ | **2.2.4** | 03/2026 | MQTT/Homie | Negative zero fix on idle circuits |
369
+ | **2.2.3** | 03/2026 | MQTT/Homie | Panel size from Homie schema; `panel_size` always populated on snapshot |
370
+ | **2.0.2** | 03/2026 | MQTT/Homie | EVSE (EV charger) snapshot model, Homie parsing, simulation support |
371
+ | **2.0.1** | 03/2026 | MQTT/Homie | Full BESS metadata parsing, README documentation |
372
+ | **2.0.0** | 02/2026 | MQTT/Homie | Ground-up rewrite: MQTT-only, protocol-based API, real-time push, PV/BESS metadata |
373
+ | **1.1.14** | 12/2025 | REST | Keep-Alive and RemoteProtocolError handling |
374
+ | **1.1.9** | 9/2025 | REST | Simulation sign corrections |
375
+ | **1.1.8** | 2024 | REST | Simulation power sign fix |
376
+ | **1.1.6** | 2024 | REST | YAML simulation API, battery simulation |
377
+ | **1.1.5** | 2024 | REST | Simulation edge cases |
378
+ | **1.1.4** | 2024 | REST | Formatting and linting |
379
+ | **1.1.3** | 2024 | REST | Test and lint fixes |
380
+ | **1.1.2** | 2024 | REST | Simulation mode added |
381
+ | **1.1.1** | 2024 | REST | Dependency updates |
382
+ | **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.2
3
+ Version: 2.5.4
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.2"
3
+ version = "2.5.4"
4
4
  description = "A client library for SPAN Panel API"
5
5
  authors = [
6
6
  {name = "SpanPanel"}
@@ -56,10 +56,6 @@ class HomiePropertyAccumulator:
56
56
  # Node type mapping from $description
57
57
  self._node_types: dict[str, str] = {}
58
58
 
59
- # Generation counter — incremented when $description clears property
60
- # values so consumers can invalidate caches built from stale data.
61
- self._generation: int = 0
62
-
63
59
  # Dirty tracking
64
60
  self._dirty_nodes: set[str] = set()
65
61
 
@@ -83,11 +79,6 @@ class HomiePropertyAccumulator:
83
79
  """Monotonic timestamp of the last READY transition, 0.0 if never ready."""
84
80
  return self._ready_since
85
81
 
86
- @property
87
- def generation(self) -> int:
88
- """Counter incremented on the initial $description and after lifecycle resets."""
89
- return self._generation
90
-
91
82
  def is_ready(self) -> bool:
92
83
  """True when lifecycle is READY."""
93
84
  return self._lifecycle == HomieLifecycle.READY
@@ -200,17 +191,10 @@ class HomiePropertyAccumulator:
200
191
  self._received_state_ready = False
201
192
  self._received_description = False
202
193
  else:
203
- # init, sleeping, alert, etc. — connected but not ready.
204
- # Always move out of READY/DESCRIPTION_RECEIVED into a
205
- # non-ready connected lifecycle state.
206
- #
207
- # Reset _received_description so that the upcoming $description
208
- # triggers a property clear. This covers fast reboots where
209
- # the broker's LWT ($state=disconnected) may not reach us
210
- # before the panel publishes $state=init.
211
- self._lifecycle = HomieLifecycle.CONNECTED
194
+ # init, sleeping, alert, etc. — connected but not ready
195
+ if self._lifecycle == HomieLifecycle.DISCONNECTED:
196
+ self._lifecycle = HomieLifecycle.CONNECTED
212
197
  self._received_state_ready = False
213
- self._received_description = False
214
198
 
215
199
  _LOGGER.debug("Homie $state: %s → lifecycle=%s", payload, self._lifecycle.value)
216
200
 
@@ -222,20 +206,6 @@ class HomiePropertyAccumulator:
222
206
  _LOGGER.warning("Invalid $description JSON")
223
207
  return
224
208
 
225
- # _handle_state() already reset _received_description to False due to
226
- # a state change that starts a new panel lifecycle, including
227
- # $state=disconnected/lost and other non-ready states such as init.
228
- # This means the panel rebooted while we were connected. On a pure
229
- # MQTT reconnect (no panel reboot), _received_description is still
230
- # True from the previous session so we skip the clear — the retained
231
- # property messages will carry the correct (unchanged) values.
232
- if not self._received_description:
233
- self._property_values.clear()
234
- self._property_timestamps.clear()
235
- self._target_values.clear()
236
- self._generation += 1
237
- _LOGGER.debug("Cleared stale property values (generation %d)", self._generation)
238
-
239
209
  self._received_description = True
240
210
  self._node_types.clear()
241
211
 
@@ -250,11 +220,7 @@ class HomiePropertyAccumulator:
250
220
  # Mark all known nodes dirty
251
221
  self._dirty_nodes.update(self._node_types.keys())
252
222
 
253
- _LOGGER.debug(
254
- "Parsed $description with %d nodes (generation %d)",
255
- len(self._node_types),
256
- self._generation,
257
- )
223
+ _LOGGER.debug("Parsed $description with %d nodes", len(self._node_types))
258
224
 
259
225
  # Lifecycle transition
260
226
  if self._received_state_ready:
@@ -64,7 +64,6 @@ 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
68
67
 
69
68
  # -- Delegation to accumulator -------------------------------------------
70
69
  # These thin wrappers allow SpanMqttClient (and legacy test code) to
@@ -119,12 +118,6 @@ class HomieDeviceConsumer:
119
118
 
120
119
  Must be called after accumulator is_ready() returns True.
121
120
  """
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
-
128
121
  dirty = self._acc.dirty_node_ids()
129
122
 
130
123
  if not dirty and self._cached_snapshot is not None:
@@ -164,128 +164,116 @@ class TestLifecycleDisconnection:
164
164
 
165
165
 
166
166
  # ---------------------------------------------------------------------------
167
- # Panel reboot: $description clears stale property values
167
+ # Lifecycle: READY resilience (core 2.5.4 revert invariant)
168
168
  # ---------------------------------------------------------------------------
169
169
 
170
170
 
171
- class TestDescriptionClearsProperties:
172
- """Verify that a panel reboot (disconnected -> $description) clears stale data.
171
+ class TestLifecycleReadyResilience:
172
+ """Transient $state values must NOT disrupt READY lifecycle.
173
173
 
174
- The clear only happens when _received_description is False, which requires
175
- a $state=disconnected (or lost) to have reset the lifecycle first. A
176
- re-delivered retained $description on a pure network reconnect does NOT
177
- clear, because _received_description is still True from the previous session.
174
+ This is the core behavioral invariant of the 2.5.1→2.5.4 revert:
175
+ only $state=disconnected/$state=lost should knock the device out
176
+ of READY. Transient states like init (e.g. from fast panel reboots
177
+ where the LWT may not arrive) must be ignored when already READY.
178
178
  """
179
179
 
180
- def _simulate_reboot(self, acc: HomiePropertyAccumulator) -> None:
181
- """Simulate the panel reboot lifecycle transition."""
182
- acc.handle_message(f"{PREFIX}/$state", "disconnected")
183
- acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC)
184
- acc.handle_message(f"{PREFIX}/$state", "ready")
185
-
186
- def test_description_clears_property_values_on_reboot(self):
187
- """A panel reboot must clear property values from the previous lifecycle."""
180
+ def test_init_state_when_ready_does_not_disrupt(self):
188
181
  acc = HomiePropertyAccumulator(SERIAL)
189
182
  _make_ready(acc)
190
- acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000")
191
- assert acc.get_prop("circuit-1", "exported-energy") == "1000"
192
-
193
- # Panel reboots — $state=disconnected resets lifecycle, then $description clears
194
- self._simulate_reboot(acc)
195
- assert acc.get_prop("circuit-1", "exported-energy") == ""
183
+ assert acc.lifecycle == HomieLifecycle.READY
184
+ acc.handle_message(f"{PREFIX}/$state", "init")
185
+ assert acc.lifecycle == HomieLifecycle.READY
186
+ assert acc.is_ready()
196
187
 
197
- def test_description_clears_timestamps_on_reboot(self):
198
- """Timestamps must also be cleared so stale timing data doesn't persist."""
188
+ def test_sleeping_state_when_ready_does_not_disrupt(self):
199
189
  acc = HomiePropertyAccumulator(SERIAL)
200
190
  _make_ready(acc)
201
- acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000")
202
- assert acc.get_timestamp("circuit-1", "exported-energy") > 0
203
-
204
- self._simulate_reboot(acc)
205
- assert acc.get_timestamp("circuit-1", "exported-energy") == 0
191
+ acc.handle_message(f"{PREFIX}/$state", "sleeping")
192
+ assert acc.lifecycle == HomieLifecycle.READY
206
193
 
207
- def test_description_clears_target_values_on_reboot(self):
208
- """Target values must also be cleared on panel reboot."""
194
+ def test_alert_state_when_ready_does_not_disrupt(self):
209
195
  acc = HomiePropertyAccumulator(SERIAL)
210
196
  _make_ready(acc)
211
- acc.handle_message(f"{PREFIX}/circuit-1/relay/$target", "OPEN")
212
- assert acc.get_target("circuit-1", "relay") == "OPEN"
213
-
214
- self._simulate_reboot(acc)
215
- assert acc.get_target("circuit-1", "relay") is None
197
+ acc.handle_message(f"{PREFIX}/$state", "alert")
198
+ assert acc.lifecycle == HomieLifecycle.READY
216
199
 
217
- def test_reboot_increments_generation(self):
218
- """Each panel reboot must advance the generation counter."""
200
+ def test_init_from_connected_stays_connected(self):
201
+ """$state=init while CONNECTED should not regress lifecycle."""
219
202
  acc = HomiePropertyAccumulator(SERIAL)
220
- assert acc.generation == 0
203
+ acc.handle_message(f"{PREFIX}/$state", "ready") # → CONNECTED (no desc yet)
204
+ assert acc.lifecycle == HomieLifecycle.CONNECTED
205
+ acc.handle_message(f"{PREFIX}/$state", "init")
206
+ assert acc.lifecycle == HomieLifecycle.CONNECTED
221
207
 
222
- # First boot
223
- acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC)
224
- assert acc.generation == 1
225
208
 
226
- # Reboot
227
- acc.handle_message(f"{PREFIX}/$state", "disconnected")
228
- acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC)
229
- assert acc.generation == 2
209
+ # ---------------------------------------------------------------------------
210
+ # Lifecycle: reboot sequence (value preservation + dirty tracking)
211
+ # ---------------------------------------------------------------------------
212
+
230
213
 
231
- def test_retained_redescription_does_not_clear(self):
232
- """A re-delivered retained $description without a disconnect must NOT clear."""
214
+ class TestLifecycleReboot:
215
+ """Reboot handling relies on dirty-node tracking, not property clearing."""
216
+
217
+ def test_reboot_preserves_property_values(self):
218
+ """Properties stored before disconnect survive the full reboot cycle."""
233
219
  acc = HomiePropertyAccumulator(SERIAL)
234
220
  _make_ready(acc)
235
- acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000")
221
+ acc.handle_message(f"{PREFIX}/core/power", "100")
222
+ acc.mark_clean()
236
223
 
237
- # Simulate network reconnect $description re-delivered without $state=disconnected
224
+ # Full reboot sequence: disconnect → init → description ready
225
+ acc.handle_message(f"{PREFIX}/$state", "disconnected")
226
+ acc.handle_message(f"{PREFIX}/$state", "init")
238
227
  acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC)
228
+ acc.handle_message(f"{PREFIX}/$state", "ready")
239
229
 
240
- # Property values should be preserved
241
- assert acc.get_prop("circuit-1", "exported-energy") == "1000"
242
- assert acc.generation == 1 # still 1 from initial boot, no increment on re-delivery
230
+ assert acc.get_prop("core", "power") == "100"
231
+ assert acc.lifecycle == HomieLifecycle.READY
243
232
 
244
- def test_fresh_properties_available_after_reboot(self):
245
- """Post-reboot properties should be stored normally after clear."""
233
+ def test_reboot_description_marks_all_nodes_dirty(self):
234
+ """$description after reboot marks all nodes dirty for cache invalidation."""
246
235
  acc = HomiePropertyAccumulator(SERIAL)
247
236
  _make_ready(acc)
248
- acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000")
237
+ acc.mark_clean()
238
+ assert len(acc.dirty_node_ids()) == 0
249
239
 
250
- self._simulate_reboot(acc)
240
+ acc.handle_message(f"{PREFIX}/$state", "disconnected")
241
+ acc.handle_message(f"{PREFIX}/$state", "init")
242
+ acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC)
251
243
 
252
- # Fresh post-reboot value
253
- acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "50")
254
- assert acc.get_prop("circuit-1", "exported-energy") == "50"
244
+ dirty = acc.dirty_node_ids()
245
+ assert "core" in dirty
246
+ assert "circuit-1" in dirty
247
+ assert "circuit-2" in dirty
248
+ assert "bess-0" in dirty
255
249
 
256
- def test_fresh_target_values_available_after_reboot(self):
257
- """Post-reboot target values should be stored normally after clear."""
250
+ def test_reboot_timestamps_preserved(self):
251
+ """Timestamps are not cleared during reboot only updated on new values."""
258
252
  acc = HomiePropertyAccumulator(SERIAL)
259
253
  _make_ready(acc)
260
- acc.handle_message(f"{PREFIX}/circuit-1/relay/$target", "OPEN")
261
- assert acc.get_target("circuit-1", "relay") == "OPEN"
262
-
263
- self._simulate_reboot(acc)
254
+ acc.handle_message(f"{PREFIX}/core/power", "100")
255
+ ts_before = acc.get_timestamp("core", "power")
256
+ assert ts_before > 0
264
257
 
265
- # Fresh post-reboot target value
266
- acc.handle_message(f"{PREFIX}/circuit-1/relay/$target", "CLOSED")
267
- assert acc.get_target("circuit-1", "relay") == "CLOSED"
258
+ acc.handle_message(f"{PREFIX}/$state", "disconnected")
259
+ acc.handle_message(f"{PREFIX}/$state", "init")
260
+ acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC)
261
+ acc.handle_message(f"{PREFIX}/$state", "ready")
268
262
 
269
- def test_fast_reboot_without_lwt_still_clears(self):
270
- """Panel reboots so fast that $state=disconnected (LWT) is skipped.
263
+ assert acc.get_timestamp("core", "power") == ts_before
271
264
 
272
- The panel goes directly from ready -> init -> description -> ready.
273
- $state=init must reset _received_description so the subsequent
274
- $description triggers the property clear.
275
- """
265
+ def test_reboot_target_values_preserved(self):
266
+ """Target values survive the reboot cycle."""
276
267
  acc = HomiePropertyAccumulator(SERIAL)
277
268
  _make_ready(acc)
278
- acc.handle_message(f"{PREFIX}/circuit-1/exported-energy", "1000")
279
- gen_before = acc.generation
269
+ acc.handle_message(f"{PREFIX}/core/relay/$target", "OPEN")
280
270
 
281
- # Fast reboot: no $state=disconnected, straight to init
271
+ acc.handle_message(f"{PREFIX}/$state", "disconnected")
282
272
  acc.handle_message(f"{PREFIX}/$state", "init")
283
273
  acc.handle_message(f"{PREFIX}/$description", SIMPLE_DESC)
284
274
  acc.handle_message(f"{PREFIX}/$state", "ready")
285
275
 
286
- # Property values should be cleared
287
- assert acc.get_prop("circuit-1", "exported-energy") == ""
288
- assert acc.generation == gen_before + 1
276
+ assert acc.get_target("core", "relay") == "OPEN"
289
277
 
290
278
 
291
279
  # ---------------------------------------------------------------------------
@@ -905,28 +905,33 @@ 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 clear cached snapshot so stale data is not reused."""
908
+ def test_reboot_dirty_nodes_invalidate_cache(self):
909
+ """Post-reboot $description marks all nodes dirty full rebuild, not cached."""
910
910
  acc, consumer = _build_ready_consumer()
911
911
  node = "aabbccdd-1122-3344-5566-778899001122"
912
+ acc.handle_message(f"{PREFIX}/{node}/active-power", "-100.0")
913
+ snap1 = consumer.build_snapshot()
912
914
 
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 clears values
915
+ # Reboot sequence
920
916
  acc.handle_message(f"{PREFIX}/$state", "disconnected")
917
+ acc.handle_message(f"{PREFIX}/$state", "init")
921
918
  acc.handle_message(f"{PREFIX}/$description", _make_description(_full_description()))
922
919
  acc.handle_message(f"{PREFIX}/$state", "ready")
923
920
 
924
- # Post-reboot: circuit publishes reset energy = 50
925
- acc.handle_message(f"{PREFIX}/{node}/exported-energy", "50")
926
- snap_post = consumer.build_snapshot()
921
+ snap2 = consumer.build_snapshot()
922
+ assert snap2 is not snap1 # cache invalidated by dirty nodes
927
923
 
928
- # Must reflect post-reboot value, not stale pre-reboot cache
929
- assert snap_post.circuits[circuit_id].consumed_energy_wh == 50.0
924
+ def test_init_while_ready_does_not_invalidate_cache(self):
925
+ """$state=init while READY must not disrupt snapshot caching."""
926
+ acc, consumer = _build_ready_consumer()
927
+ node = "aabbccdd-1122-3344-5566-778899001122"
928
+ acc.handle_message(f"{PREFIX}/{node}/active-power", "-100.0")
929
+ snap1 = consumer.build_snapshot()
930
+
931
+ acc.handle_message(f"{PREFIX}/$state", "init")
932
+
933
+ snap2 = consumer.build_snapshot()
934
+ assert snap2 is snap1 # same cached object — no disruption
930
935
 
931
936
 
932
937
  # ---------------------------------------------------------------------------
@@ -415,45 +415,45 @@ toml = [
415
415
 
416
416
  [[package]]
417
417
  name = "cryptography"
418
- version = "46.0.6"
418
+ version = "46.0.7"
419
419
  source = { registry = "https://pypi.org/simple" }
420
420
  dependencies = [
421
421
  { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
422
422
  { name = "typing-extensions", marker = "python_full_version < '3.11'" },
423
423
  ]
424
- 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" }
425
- wheels = [
426
- { 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" },
427
- { 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" },
428
- { 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" },
429
- { 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" },
430
- { 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" },
431
- { 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" },
432
- { 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" },
433
- { 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" },
434
- { 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" },
435
- { 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" },
436
- { 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" },
437
- { 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" },
438
- { 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" },
439
- { 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" },
440
- { 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" },
441
- { 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" },
442
- { 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" },
443
- { 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" },
444
- { 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" },
445
- { 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" },
446
- { 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" },
447
- { 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" },
448
- { 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" },
449
- { 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" },
450
- { 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" },
451
- { 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" },
452
- { 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" },
453
- { 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" },
454
- { 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" },
455
- { 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" },
456
- { 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" },
424
+ 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" }
425
+ wheels = [
426
+ { 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" },
427
+ { 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" },
428
+ { 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" },
429
+ { 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" },
430
+ { 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" },
431
+ { 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" },
432
+ { 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" },
433
+ { 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" },
434
+ { 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" },
435
+ { 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" },
436
+ { 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" },
437
+ { 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" },
438
+ { 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" },
439
+ { 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" },
440
+ { 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" },
441
+ { 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" },
442
+ { 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" },
443
+ { 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" },
444
+ { 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" },
445
+ { 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" },
446
+ { 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" },
447
+ { 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" },
448
+ { 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" },
449
+ { 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" },
450
+ { 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" },
451
+ { 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" },
452
+ { 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" },
453
+ { 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" },
454
+ { 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" },
455
+ { 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" },
456
+ { 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" },
457
457
  ]
458
458
 
459
459
  [[package]]
@@ -1292,7 +1292,7 @@ wheels = [
1292
1292
 
1293
1293
  [[package]]
1294
1294
  name = "span-panel-api"
1295
- version = "2.5.2"
1295
+ version = "2.5.4"
1296
1296
  source = { editable = "." }
1297
1297
  dependencies = [
1298
1298
  { name = "httpx" },
File without changes
File without changes