Merge pull request #391 from JayChickenK/observe-silent-href-subpolls

Keep asking the appliance for readings that never send live updates
This commit is contained in:
Marc Billow authored and GitHub committed 2026-08-19 21:59:34 -05:00
commit b0caebe14d
4 files changed
+180 -10

No files matched your search

+18 -9
View File
@@ -991,22 +991,31 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# ------------------------------------------------------------------
async def _run_subpolls(self, force: bool = False) -> None:
"""Poll hot/warm hrefs in the gaps between summary polls. No-op in
observe-primary mode (those hrefs are already covered by push)
unless `force` is set -- set when this cycle's sweep found the
cache disagreeing with a still-live observe session (see
log_sweep_discrepancies): a bounded fallback for a channel gone
silent without a reconnect."""
"""Poll hot/warm hrefs in the gaps between summary polls.
In observe-primary mode this is a no-op for hrefs the device is
actually pushing, unless `force` is set (sweep disagreed with the
cache -- see log_sweep_discrepancies). Hrefs that were subscribed
but stayed silent through the grace period (issue #92) stay on
the hot/warm cadence via `fallback_hrefs`.
"""
if self._observe.mode == MODE_OBSERVE and not force:
return
hot = self._hot_hrefs
warm = self._warm_hrefs
silent = self._observe.fallback_hrefs
if not silent:
return
hot = [h for h in self._hot_hrefs if h in silent]
warm = [h for h in self._warm_hrefs if h in silent]
else:
hot = self._hot_hrefs
warm = self._warm_hrefs
if not hot and not warm:
return
step = self._SUBPOLL_STEP_S
for i in range(1, 10): # slots 1..9 (T+3 s … T+27 s)
await asyncio.sleep(step)
hrefs = list(hot) + (list(warm) if i % 2 == 0 else [])
if not hrefs:
continue
async with self._session_lock:
try:
await self.hass.async_add_executor_job(self._poll_hrefs_blocking, hrefs)
+19 -1
View File
@@ -95,8 +95,15 @@ class ObserveManager:
self._notified: set[str] = set()
self._last_notify_ts: float | None = None
# Wakes try_enter_observe_mode's grace wait early once enough hrefs
# have notified. Guards only `_notified` mutations + the `wait_for`.
# have notified. Guards `_notified` mutations, the `wait_for`, and
# fallback_hrefs (enter_observe_mode assignment, on_notification
# discard).
self._notify_cond = threading.Condition()
# Idle while polling, except after downgrade_to_poll (every href
# that was subscribed). While in observe mode this is the set of
# subscribed hrefs that have not yet notified (issue #92) -- they
# stay on the hot/warm sub-poll cadence. on_notification discards
# an href once it pushes, so a late first notify self-corrects.
self.fallback_hrefs: set[str] = set()
self._on_applied: Callable[[str, dict, str], None] | None = None
self._refresh_task: ObserveRefreshTask | None = None
@@ -205,6 +212,10 @@ class ObserveManager:
return
with self._notify_cond:
self._notified.add(href)
# Snapshot in enter_observe_mode is at the 80% quorum, not the
# full grace period -- a late first push (blockwise refetch)
# must drop the href so we don't keep GET-polling a live one.
self.fallback_hrefs.discard(href)
self._last_notify_ts = time.monotonic()
self._notify_cond.notify_all()
self.log.debug("observe notify: %s", href)
@@ -270,6 +281,13 @@ class ObserveManager:
#294) -- committing against a session a reconnect already replaced
would claim observe mode with nothing left to notice it's dead."""
self.subscribed_hrefs = set(subscribed)
# Issue #92: subscribed-but-silent hrefs are counted as covered by
# push if we drop this, but they never emit a notify. Keep them on
# the poll cadence via fallback_hrefs (otherwise idle in observe).
# Same lock as on_notification's discard so a notify in this window
# cannot land on a set object that is about to be replaced.
with self._notify_cond:
self.fallback_hrefs = set(subscribed) - self._notified
self._set_mode(MODE_OBSERVE)
self.start_refresh_task(session)