docs(offline-setup): what PR #303 measures, and what a working version needs
PR #303 loads the entry when the first poll fails. Measured on its branch, that produces an entry with zero bound entities and zero coordinator listeners, so DataUpdateCoordinator never reschedules and the device never recovers without a manual reload. Record why entities can't be created offline here (discovery is the only source of `bound`, and platforms enumerate it once), what a working version would need (persisted discovery snapshot, reconcile-on-reconnect, a listener that keeps polling alive), and the cheaper retry-and-reload option that solves the filed issue on its own.
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
# Loading a config entry while the appliance is offline
|
||||
|
||||
Issue #295 asks for sub-30s recovery when a powered-off appliance comes back,
|
||||
instead of HA's `ConfigEntryNotReady` backoff (30s → … → 15 min). PR #303
|
||||
tries to get there by catching the first-refresh failure in
|
||||
`async_setup_entry` and loading the entry anyway.
|
||||
|
||||
That doesn't work here, and the reason is worth writing down: this
|
||||
integration has no static entity list. Every entity comes from discovery,
|
||||
and discovery only happens inside a successful poll.
|
||||
|
||||
## What PR #303 produces today
|
||||
|
||||
Measured on the PR's branch — set up with `_poll_once` raising, then advance
|
||||
the clock four summary intervals:
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| entry state | `LOADED` |
|
||||
| `coordinator.bound` | 0 |
|
||||
| entities in the state machine | 0 |
|
||||
| registry entries | 1 (the disabled connection-mode sensor) |
|
||||
| coordinator listeners | 0 |
|
||||
| `_unsub_refresh` | `None` |
|
||||
| poll attempts over the next 4 intervals | **0** |
|
||||
|
||||
The entry loads and then never polls again. `DataUpdateCoordinator._async_refresh`
|
||||
reschedules only `if not auth_failed and self._listeners and not
|
||||
self.hass.is_stopping`; with no bound entities the only unconditional entity
|
||||
is `LocalThingsConnectionModeSensor`, which is
|
||||
`entity_registry_enabled_default = False` and so never added and never
|
||||
subscribes. Nothing reloads the entry either. The device comes back online to
|
||||
an entry that is permanently empty until a manual reload — strictly worse
|
||||
than the backoff it replaces, which did recover on its own within 15 minutes.
|
||||
|
||||
## Why entities can't just be created offline
|
||||
|
||||
Four independent gates, all of which need live device data:
|
||||
|
||||
1. `bound` is only ever assigned in `_run_discovery` (`coordinator.py:1081`),
|
||||
which runs on a poll's `resources` dict.
|
||||
2. All ten platforms enumerate `coordinator.bound` exactly once, at forward
|
||||
time (`sensor.py:34` and siblings). Nothing adds entities later — the
|
||||
invariant is already documented at `coordinator.py:1283-1286`.
|
||||
3. `_is_included` (`entity.py:39`) returns False whenever `last_resources`
|
||||
has no rep for the href. Even a fully reconstructed `bound` filters to
|
||||
nothing while `StateCache` is empty.
|
||||
4. `LocalThingsEntity` is a bare `CoordinatorEntity` with no `available`
|
||||
override, no `RestoreEntity`, and no `Store` anywhere in the component. An
|
||||
entity that did exist offline would be `unavailable` with no state.
|
||||
|
||||
The issue cites ESPHome, Shelly, LIFX and WLED as precedent for setup that
|
||||
never fails. Those integrations can do it because each one has a *persisted
|
||||
device description* to build entities from — ESPHome keeps its entity list in
|
||||
`.storage`, Shelly caches device info. The pattern is portable; the mechanism
|
||||
underneath it is the part PR #303 is missing.
|
||||
|
||||
## What a working version needs
|
||||
|
||||
Three separable pieces, plus a gating rule.
|
||||
|
||||
### 1. A persisted discovery snapshot
|
||||
|
||||
Write the discovery result back onto the config entry after each successful
|
||||
`_run_discovery`. There is already precedent for exactly this shape —
|
||||
`_persist_identity` (`coordinator.py:973`), `CONF_LEARNED_MODES`,
|
||||
`CONF_CLOUD_COURSES`.
|
||||
|
||||
`BoundEntity` holds live `Capability`/`SamsungEntityDescription` objects, so
|
||||
it isn't directly serializable, but it is re-derivable: persist
|
||||
`device_type_name`, the materialized `Subdevice` list (plain str/tuple
|
||||
fields), and per entity the `(subdevice key, capability, desc.key, instance,
|
||||
key_override, instance_name)` tuple. Rehydrate by re-resolving through
|
||||
`resolve_registry` + `CAPABILITIES`.
|
||||
|
||||
Persist the *post-`_is_included`* set, so rehydration doesn't need reps, and
|
||||
flag the coordinator as rehydrated so gate 3 above is skipped for that pass.
|
||||
|
||||
### 2. Reconcile on reconnect
|
||||
|
||||
The snapshot is a guess about a device we haven't talked to yet. When the
|
||||
first successful poll lands, `_run_discovery` computes the real set; if it
|
||||
differs from what was rehydrated, the entry has to reload, because gate 2
|
||||
means platforms can't add the difference in place. `async_schedule_reload`
|
||||
covers it (available well below the 2025.1.0 floor in `hacs.json`).
|
||||
|
||||
This is the piece that makes the whole thing safe against a firmware update,
|
||||
a newly-appearing subdevice, or a different appliance at the same IP. PR #303
|
||||
has no equivalent.
|
||||
|
||||
### 3. Keep polling with no listeners
|
||||
|
||||
Independent of the above, and worth doing on its own merits: hold one
|
||||
refresh alive for the entry's lifetime so scheduling never depends on entity
|
||||
count.
|
||||
|
||||
```python
|
||||
entry.async_on_unload(coordinator.async_add_listener(lambda: None))
|
||||
```
|
||||
|
||||
That alone fixes the measured "never polls again" bug, and covers the case
|
||||
where every rehydrated entity happens to be registry-disabled.
|
||||
|
||||
### Gating rule: only load offline when there's a snapshot
|
||||
|
||||
An entry that has never successfully polled has nothing to restore and keeps
|
||||
raising `ConfigEntryNotReady`. This is what answers the objection in the PR
|
||||
thread — with a snapshot we *do* have metadata to build a device from, and
|
||||
without one HA's backoff is still the right behavior. It also leaves room for
|
||||
the #168-style flows that need to interact with the device during setup: a
|
||||
device that never completed setup still blocks.
|
||||
|
||||
## What this still won't do
|
||||
|
||||
Entities will be present and `unavailable` — not showing their last values.
|
||||
Gate 4 means last-known values require either `RestoreEntity` per platform or
|
||||
persisting `StateCache`, and both mean asserting state the integration cannot
|
||||
verify: a washer unplugged for a week would read "Running". HA's convention
|
||||
is that unreachable means unavailable, and the recorder keeps the history
|
||||
either way, so long-term statistics and history graphs are unaffected by this
|
||||
choice.
|
||||
|
||||
Worth being explicit about, because it is the gap between what PR #303
|
||||
promises in the thread ("load their previously recorded states") and what any
|
||||
correct version can deliver.
|
||||
|
||||
## The cheaper alternative
|
||||
|
||||
If the goal is only issue #295's — fast recovery — pieces 1 and 2 are
|
||||
unnecessary. Keep `ConfigEntryNotReady`, and add a retry that probes on a
|
||||
fixed short interval and calls `async_schedule_reload` on the first success.
|
||||
No persistence, no reconcile, no new failure modes. The device tile still
|
||||
reads "Retrying setup" while the appliance is off, which is true, and
|
||||
recovery drops from up-to-15-min to one probe interval.
|
||||
|
||||
The issue's other suggestion — wire zeroconf so the device's own boot
|
||||
announcement triggers the retry — is a non-starter as things stand: there is
|
||||
no `zeroconf` or `dhcp` key in `manifest.json` and the config flow is
|
||||
user-driven only, so HA has no discovery signal for this integration to hang
|
||||
a retry on. It would first need a confirmed mDNS service on the appliance.
|
||||
|
||||
## Recommendation
|
||||
|
||||
The cheap alternative solves the filed issue. The three-piece version solves
|
||||
the goal stated later in the PR thread, at the cost of a new persisted
|
||||
schema, a reconcile path, and a reload-on-mismatch — and it still leaves
|
||||
entities unavailable, which is the part that was actually being asked for.
|
||||
Do the cheap one first; treat offline entity materialization as a separate
|
||||
change with its own issue.
|
||||
Reference in New Issue
Block a user