Skip to content

fix: settings entities silently report 0/off when the snapshot is lost - #111

Merged
zakery292 merged 3 commits into
Monitor-My-Solar:developmentfrom
RAR:fix/settings-state-loss
Aug 2, 2026
Merged

fix: settings entities silently report 0/off when the snapshot is lost#111
zakery292 merged 3 commits into
Monitor-My-Solar:developmentfrom
RAR:fix/settings-state-loss

Conversation

@RAR

@RAR RAR commented Aug 2, 2026

Copy link
Copy Markdown

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 calling request_snapshot(), which swallowed publish failures and returned None — 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 = 0 and switches _state = False, so an entity that never received a value was indistinguishable from a genuine reading of 0 / off.

Observed on a live system: two dongles reported acchgsoclimit 0 and acchgstartsoc 0 while the inverters actually held 40/80 and 60/80, and an AC-charge switch read off while 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 seed None. This is what select.py already 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:

  • 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. There's a test pinning this.
  • 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 firing 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.

This is the same rule test_ota_snapshot_suppression.py already 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 select was 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.

RAR added 3 commits August 2, 2026 16:53
…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.
@zakery292
zakery292 merged commit aea0d13 into Monitor-My-Solar:development Aug 2, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants