fix: persist energy state on HA final write and unload #67

Merged
mat merged 1 commit from save-on-stop into main 2026-07-26 07:54:51 +00:00
Owner

Supprime le résidu du Problème 1 : jusqu'à 2 min d'énergie accumulée par le stream étaient perdues à chaque redémarrage de HA.

Problème (constaté en production)

PR #65 a ajouté la persistance de live_total_wh et supprimé les reculs de milliers de Wh au redémarrage. Mais async_save_energy_state() n'est appelé qu'à la fin de _async_update_data, soit une fois par cycle de poll (2 min). Tout ce que le stream WebSocket a intégré depuis la dernière sauvegarde est perdu au redémarrage.

Trace live au reboot (injection réseau) :

07:09:08.425   67.3568   dernière valeur live avant reboot
   --- reboot ---
07:10:00.745   63.2849   <-- valeur restaurée : 4.07 Wh perdus
07:10:22.817   63.6890   le stream repart

Pertes mesurées selon la puissance de l'appareil : 0.6 Wh (injection) à 15.25 Wh (piscine, 2 min de forte conso).

Correctif

  1. EVENT_HOMEASSISTANT_FINAL_WRITE : listener enregistré dans async_setup_entry (encapsulé dans entry.async_on_unload) qui await coordinator.async_save_energy_state(). C'est l'événement HA dédié à la dernière écriture disque.
  2. Sauvegarde au déchargement : async_unload_entry sauvegarde après l'arrêt du stream (accumulateur figé), donc un simple reload de l'intégration ne perd rien non plus.

coordinator.py n'est pas touché — async_save_energy_state / async_load_energy_state sont réutilisées telles quelles.

Pourquoi FINAL_WRITE et pas STOP

Vérifié dans la source HA (helpers/storage.py) :

if self.hass.state is CoreState.stopping:
    self._async_ensure_final_write_listener()
    return          # écriture DIFFÉRÉE
await self._async_handle_write_data()   # écriture immédiate

Pendant FINAL_WRITE, hass.state vaut final_write (pas stopping) → l'écriture est immédiate. Avec STOP elle aurait été différée.

Vérifié aussi dans core.py que async_stop fait STOPFINAL_WRITECLOSE sans décharger les config entries : le listener est donc bien encore enregistré quand l'événement part, et il n'y a pas de double-sauvegarde à l'extinction (le chemin unload ne sert qu'au reload).

Tests

2 nouveaux (tests/test_init.py, TDD RED→GREEN) : sauvegarde sur FINAL_WRITE et sauvegarde au déchargement, tous deux assertant que le store contient la valeur avancée (post-dernier-poll) et non l'ancienne. 91 tests au total, ruff + mypy clean.

Périmètre

custom_components/comwatt/__init__.py + tests/test_init.py uniquement. Base main. Indépendante de la PR sœur fresh-energy-publish (qui touche coordinator.py).

Supprime le résidu du Problème 1 : jusqu'à 2 min d'énergie accumulée par le stream étaient perdues à chaque redémarrage de HA. ## Problème (constaté en production) PR #65 a ajouté la persistance de `live_total_wh` et supprimé les reculs de **milliers** de Wh au redémarrage. Mais `async_save_energy_state()` n'est appelé qu'à la fin de `_async_update_data`, soit **une fois par cycle de poll (2 min)**. Tout ce que le stream WebSocket a intégré depuis la dernière sauvegarde est perdu au redémarrage. Trace live au reboot (injection réseau) : ``` 07:09:08.425 67.3568 dernière valeur live avant reboot --- reboot --- 07:10:00.745 63.2849 <-- valeur restaurée : 4.07 Wh perdus 07:10:22.817 63.6890 le stream repart ``` Pertes mesurées selon la puissance de l'appareil : 0.6 Wh (injection) à 15.25 Wh (piscine, 2 min de forte conso). ## Correctif 1. **`EVENT_HOMEASSISTANT_FINAL_WRITE`** : listener enregistré dans `async_setup_entry` (encapsulé dans `entry.async_on_unload`) qui `await coordinator.async_save_energy_state()`. C'est l'événement HA dédié à la dernière écriture disque. 2. **Sauvegarde au déchargement** : `async_unload_entry` sauvegarde après l'arrêt du stream (accumulateur figé), donc un simple *reload* de l'intégration ne perd rien non plus. `coordinator.py` n'est pas touché — `async_save_energy_state` / `async_load_energy_state` sont réutilisées telles quelles. ## Pourquoi FINAL_WRITE et pas STOP Vérifié dans la source HA (`helpers/storage.py`) : ```python if self.hass.state is CoreState.stopping: self._async_ensure_final_write_listener() return # écriture DIFFÉRÉE await self._async_handle_write_data() # écriture immédiate ``` Pendant `FINAL_WRITE`, `hass.state` vaut `final_write` (pas `stopping`) → l'écriture est **immédiate**. Avec `STOP` elle aurait été différée. Vérifié aussi dans `core.py` que `async_stop` fait `STOP` → `FINAL_WRITE` → `CLOSE` **sans décharger les config entries** : le listener est donc bien encore enregistré quand l'événement part, et il n'y a pas de double-sauvegarde à l'extinction (le chemin unload ne sert qu'au reload). ## Tests 2 nouveaux (`tests/test_init.py`, TDD RED→GREEN) : sauvegarde sur `FINAL_WRITE` et sauvegarde au déchargement, tous deux assertant que le store contient la valeur **avancée** (post-dernier-poll) et non l'ancienne. 91 tests au total, ruff + mypy clean. ## Périmètre `custom_components/comwatt/__init__.py` + `tests/test_init.py` uniquement. Base `main`. Indépendante de la PR sœur `fresh-energy-publish` (qui touche `coordinator.py`).
🐛 fix: persist energy state on HA final write and unload
All checks were successful
Validate / lint-ruff (pull_request) Successful in 8s
Validate / test-pytest (pull_request) Successful in 3m3s
Validate / type-check-mypy (pull_request) Successful in 3m7s
Validate / lint-ruff (push) Successful in 7s
Validate / test-pytest (push) Successful in 3m2s
Validate / type-check-mypy (push) Successful in 3m6s
5e3ccb9dea
mat changed title from WIP: fix: persist energy state on HA final write and unload to fix: persist energy state on HA final write and unload 2026-07-26 07:54:23 +00:00
mat merged commit 626799c0d1 into main 2026-07-26 07:54:51 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
mat/homeassistant-comwatt!67
No description provided.