fix: settings entities silently report 0/off when the snapshot is lost - #111
Merged
zakery292 merged 3 commits intoAug 2, 2026
Merged
Conversation
…te() asyncio.get_event_loop() no longer creates a loop implicitly (deprecated in 3.12, removed in 3.14), so every test using this helper raised 'RuntimeError: There is no current event loop in thread' and the suite could not be collected on a current interpreter. 175 passed on Python 3.14 after this change.
Dongles on FW >= 4.3.0 stream change-data only, so a hold register (a setting) reaches HA exactly once: in the connect-time snapshot. Settings don't change on their own, so if that snapshot is lost nothing re-sends them and the entity never receives a value. Two problems made that silent and permanent: 1. Numbers were seeded `_attr_native_value = 0` and switches `_state = False`, so an entity that never received data was indistinguishable from a real reading of 0/off -- it showed a confident wrong value rather than 'unknown'. Observed live: two dongles reported acchgsoclimit 0 / acchgstartsoc 0 while the inverters actually held 40/80 and 60/80. select.py already seeded None; number and switch now match it. 2. request_recovery_snapshot() stamped its 30s debounce window before publishing, and request_snapshot() swallowed publish failures. The recovery triggers (boot-count change, availability online) fire while the dongle is still rebooting, so the request can be published before it has resubscribed and is then lost -- with the window already spent, the follow-up triggers were swallowed and the settings stayed empty for the rest of the session. request_snapshot() now reports whether it actually published, and the window is only recorded when it did. This is the same rule the OTA-suppression path already follows (see test_ota_snapshot_suppression.py); it just also has to apply when the publish itself fails. Tests: 3 new tests, each verified to fail before this change and pass after. 179 passed.
Publishing a snapshot request is not the same as delivering one. The
recovery triggers (boot-count change, availability online) fire while the
dongle is still coming back, so a request can go out before it has
resubscribed and is then dropped. Because FW >= 4.3.0 streams change-data
only and settings never change on their own, nothing re-sends them: one
lost request leaves every setting entity empty until the next reboot.
Arm a timer on each successful publish and disarm it when the reply
arrives on <dongle>/snap/hold; if it doesn't, re-request, bounded to
_snapshot_max_retries (3) at _snapshot_retry_delay (20s).
Only /snap/hold counts as delivery. The hold half is what carries the
settings, so an /snap/input-only reply must not disarm the retry.
Retries respect the existing OTA suppression: a dongle mid-OTA has no
snapshot queue and a {"what":"all"} request reboot-loops it, so a timer
that fires during an OTA is dropped rather than re-requesting. Armed
timers are also cancelled in stop_mqtt_subscription(), so a reload
doesn't orphan them.
Tests: 6 new tests, all verified to fail before this change and pass
after. 185 passed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
Dongles on FW >= 4.3.0 stream change-data only, so a hold register (a setting) reaches HA exactly once: in the connect-time snapshot. Settings don't change on their own, so if that snapshot is lost, nothing ever re-sends them.
Three things combined to make that silent and permanent.
1. A lost request is never retried.
request_snapshot()publishes and assumes delivery. The recovery triggers (boot-count change, availability online) fire while the dongle is still coming back, so the request can go out before it has resubscribed.2. The debounce window was spent before the request left.
request_recovery_snapshot()stamped_last_recovery_snapshot[dongle_id]before callingrequest_snapshot(), which swallowed publish failures and returnedNone— so the caller couldn't tell a sent request from a lost one. A burst of triggers around a reboot collapsed into one attempt, and if that attempt was lost the follow-ups were swallowed too.3. The result looked like real data. Numbers were seeded
_attr_native_value = 0and switches_state = False, so an entity that never received a value was indistinguishable from a genuine reading of0/off.Observed on a live system: two dongles reported
acchgsoclimit0 andacchgstartsoc0 while the inverters actually held 40/80 and 60/80, and an AC-charge switch readoffwhile the inverter had it enabled. Cross-checked against the same registers read over a second, independent path. It also reproduces on a plain HA restart, not just on dongle reboots — five dongles requested, no replies, all settings stuck at defaults until the integration was reloaded.The changes
Report
unknown, not a fabricated value. Numbers and switches now seedNone. This is whatselect.pyalready does — the other two platforms just weren't consistent with it.Only record the debounce window once the request has actually gone out.
request_snapshot()now returns whether it published.Verify and retry. Arm a timer on each successful publish; disarm it when the reply arrives on
<dongle>/snap/hold; otherwise re-request, bounded to 3 retries at 20s.Two details worth calling out:
/snap/holdcounts as delivery. The hold half is what carries the settings, so an/snap/input-only reply must not disarm the retry. There's a test pinning this.{"what":"all"}request reboot-loops it, so a timer firing during an OTA is dropped rather than re-requesting. Armed timers are also cancelled instop_mqtt_subscription()so a reload doesn't orphan them.This is the same rule
test_ota_snapshot_suppression.pyalready asserts — "must not burn the recovery debounce window while suppressed so the post-OTA refresh still goes through". It just also has to apply when the publish itself fails, and the reply needs checking rather than assuming.Tests
9 new tests. Each was verified to fail against unmodified source and pass with the change; one deliberately asserts
selectwas already correct, as a baseline, and passes either way.185 passed.
Note on the base
Includes the one-commit test fix from #110, without which the suite can't be collected on Python 3.12+. If #110 merges first this shrinks to the two fix commits.
Running on my own system since it was written; the state loss it fixes has not recurred.