Widen async_rehydrate's guard to cover the identity and Subdevice rebuild,
not just the replay. A stored row missing a field the current dataclass
declares raised KeyError straight out of async_setup_entry, which only
handles ConfigEntryNotReady -- so the entry landed in SETUP_ERROR, which HA
never retries, with its DTLS session left open on the fixed source port the
next attempt binds. It now fails the same way an unreachable device does.
Write the snapshot immediately instead of through async_delay_save. A
deferred write outlives whatever queued it: removing an entry inside the
delay window deleted the file and then had it recreated, orphaned, when the
timer fired; and a reload scheduled by _reconcile_rehydrated read the
pre-reload snapshot back off disk, so a device going quiet again mid-reload
rehydrated the stale set and reconciled a second time. Banking it before the
reconcile fixes the ordering. Failures are logged rather than raised -- a
board reporting something the JSON encoder rejects must not break polling.
A coverage gap is a claim about what the device currently reports, so
replaying a discovery snapshot shouldn't make it. Offline it would restate
the last live poll's conclusion while pointing the user at a diagnostics
download that stays empty until the appliance answers, and any drift in the
resolved device name between snapshot and live would churn the issue.
Not deduplication: HA already keys issues on (domain, issue_id), preserves
dismissed_version across async_get_or_create, and reloads non-persistent
issues with their dismissal intact -- one row per entry, and an "Ignore"
survives restarts.
An appliance switched off at the wall used to take its whole config entry
down with it: async_setup_entry raised ConfigEntryNotReady, so the device
read as failed and its entities existed only as registry rows until the
appliance came back.
Loading the entry anyway isn't enough on its own. Entities here are the
output of discovery, discovery only runs inside a successful poll, and
platforms enumerate `bound` exactly once at forward time -- so an entry
that loads while offline loads empty, and with no listeners subscribed the
base coordinator stops rescheduling and never polls again.
Bank the resources dict each successful first cycle hands _run_discovery,
along with the subdevice candidate list and the /oic identity that route
the registry, and replay it through _run_discovery when the first refresh
fails. Storing the poll input rather than a rendered entity list keeps one
implementation of discovery instead of two: the offline entity set is
produced by the same code that produced the live one.
Three things fall out of that:
- Platforms judge entity existence against `discovery_resources`, not the
live cache. The live cache deliberately stays empty, which is what keeps
a restored entity `unavailable` rather than rendering a stale value for
an appliance nobody can currently reach.
- A live discovery that disagrees with the snapshot reloads the entry --
platforms can't adopt a changed set in place, so a firmware update or a
sibling subdevice that starts answering needs a fresh setup.
- The entry holds one coordinator listener for its lifetime, so polling is
scheduled regardless of how many entities are live.
An entry that has never reached the device has no snapshot, keeps raising
ConfigEntryNotReady, and closes its session on the way out as before -- no
metadata to build a device from, and it leaves room for setup flows that
need to interact with the appliance (#168).
Restores the two tests PR #303 rewrote, narrowed to that no-snapshot path.
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.