feat: deep-sleep awareness and reliable delivery to sleeping devices#60
Closed
davelee98 wants to merge 12 commits into
Closed
feat: deep-sleep awareness and reliable delivery to sleeping devices#60davelee98 wants to merge 12 commits into
davelee98 wants to merge 12 commits into
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce the deep-sleep model that lets a battery-powered display's entry survive while the device is dark: - const.py: sleep_mode/missed_cycles/queue_timeout options, cached-state key, content delivered/expired event names, and the pending-state dispatcher signal. - sleep.py: SleepProfile resolves the options override plus the device power config into is_sleepy, availability_interval and probably_asleep(); pure and unit-tested. deep_sleep_enabled is computed locally (power_mode == BATTERY and deep_sleep_time_seconds > 0) with no dependency on unreleased library API. - __init__.py: cache every successful interrogation into entry.data; on a dark, sleepy device set up entirely from cache without connecting; apply async_set_fallback_availability_interval so entities stay available across sleep cycles. All non-cache paths keep the original ConfigEntryNotReady/ConfigEntryAuthFailed behavior. - config_flow.py: OptionsFlowWithReload options flow (sleep_mode, missed_cycles, queue_timeout) that reloads the entry on change. - strings/translations/icons: options form, pending sensor, and new error keys (grouped here for all phases to avoid churn). - tests: pure unit tests for SleepProfile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deliver content to a sleeping display at its next wake instead of dropping it:
- coordinator.py: async_subscribe_device_seen fires on every parsed
advertisement (the wake rendezvous), mirroring async_subscribe_reboot.
- delivery.py: DeliveryManager owns pending slots (latest-wins upload +
config-resync), drains them over a single BLE session on device-seen bounded
by a ~30 s per-wake deadline, arms per-upload deadline timers that fire
opendisplay_content_expired, fires opendisplay_content_delivered on success,
and refreshes the entry cache after a resync.
- services.py: upload_image/drawcustom become SupportsResponse.OPTIONAL
returning {status, expires_at}. A freshness gate queues directly when the tag
is provably asleep; a live BLE connect/timeout failure on a sleepy device
queues instead of raising. LED/buzzer fail fast with a device_sleeping error
when provably asleep. Non-sleepy devices follow the original code paths.
- __init__.py: instantiate the manager, keep entities available across sleep,
route the reboot edge to a config resync for sleepy devices (D7), and tear
the manager down on unload. Add the binary_sensor platform.
- image.py: show the queued frame immediately with pending/queued_at attributes.
- binary_sensor.py: "Update pending" entity backed by the manager state.
- tests: DeliveryManager unit tests with mocked hass/coordinator/device.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- update.py: an install requested while the tag is provably asleep fails fast with a translated device_sleeping_ota error telling the user to wake it and retry. The multi-connection AppLoader flash cannot be driven reliably inside a wake window, so this is the minimal-correct gate rather than queuing the OTA into the delivery drain (documented limitation). - diagnostics.py: report the resolved sleep profile, availability interval, config-resync flag, and delivery slot state (timestamps/attempts only, never queued image bytes). Reboot-edge rework (D7) and encrypted-device reauth-on-delivery (Phase 3) are implemented in __init__.py and delivery.py respectively, committed with Phase 2 to keep those modules import-consistent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Temporarily install py-opendisplay from the davelee98 fork's feat/ble-speed branch so the BLE write-without-response change for 0x71 image data can be tested before a PyPI release. Revert to a versioned pin (==7.12.0) once that release is published. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wake-drain deadline of 30s was too short for a large image over a slow BLE link (the device stays awake while connected, so it was HA's own timeout aborting mid-transfer). Raise DELIVERY_DEADLINE_S to 600s. On deadline timeout, split TimeoutError out of the quiet BLE-retry path: record the attempt, log at ERROR, and raise HomeAssistantError so it is not silently retried. py-opendisplay wraps its own read/connect timeouts as BLETimeoutError, so a bare TimeoutError here can only be the drain deadline firing. Add MAX_DELIVERY_ATTEMPTS=5: after five failed wakes, _give_up_upload drops the slot, cancels the expiry timer, sets last_error="failed", and fires content_expired (attempts==5 distinguishes it from time expiry) so an undeliverable frame stops retrying every wake until queue_timeout. Tests cover the deadline raise-and-count path and the max-attempts giveup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… callback last_seen read coordinator.data.last_seen, a time.time() snapshot written only inside the de-dup/connectable-gated advertisement callback, so it froze while the device was still being seen. Read async_last_service_info(..., connectable=False).time instead -- the same _all_history source the Bluetooth advertisement monitor uses -- converted from the monotonic clock to wall time. Sensor-only; the coordinator OpenDisplayUpdate.last_seen field is unchanged (still used by services/update). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The freshness gate treated probably_asleep as authoritative and queued
image sends without any connect attempt. That over-triggers: BLE adverts
are lossy (a busy scanner/proxy can miss an entire 10 s wake window), and
Silabs tags advertise continuously in EM2 while their power config reads
as sleepy. Meanwhile HA retains a connectable BLEDevice ~3-5 min after
the last advert, so a connect attempt stays possible long after the gate
flips at ~15 s.
Image sends now spend one short connect attempt (max_attempts=1,
timeout=5 s) before queuing. A dark ESP32 (radio fully off in deep
sleep) costs at most ~5 s vs the old doomed ~40 s budget the gate was
built to avoid, and pays zero device battery; a probe that lands holds
the tag awake and delivers live. A never-seen/long-gone tag has no
connectable BLEDevice and short-circuits to the queue at near-zero cost.
- New probe_before_queue option (default on) in the options flow,
resolved via SleepProfile
- _async_connect_and_run gains optional connect_timeout/max_attempts,
threaded to OpenDisplayDevice; all other call sites keep defaults
- Post-probe freshness re-check kicks notify_device_seen("post-probe")
to close the advert-during-probe drain race
- LED/buzzer and OTA gates unchanged (still fail fast)
- New tests/test_services.py covering the send gate; D5 revision note
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat: probe before queue for sleeping devices
fix(sensor): source last_seen from the bluetooth stack, not the gated callback
These were internal investigation/planning notes, not user-facing docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
|
can resolve conflict in manifest.json after py-opendisplay PR is merged and released. |
Author
|
superseded. closing. |
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.
Summary
Adds deep-sleep awareness and reliable image delivery to sleeping devices, on top of
feat/clean-port.sleep.py,binary_sensor.py): track whether a device is asleep, expose availability and a sleep binary sensor, and set up from cache so entities survive restarts without a live connection.delivery.py): queue image uploads for probably-asleep devices and deliver on wake, with a 600s delivery deadline, loud failure, and a retry cap of 5.last_seenfrom the Bluetooth stack rather than the gated callback.Tests
New coverage under
tests/:test_delivery.py,test_sleep.py,test_services.py,test_last_seen.py.Notes
Internal investigation/planning notes were intentionally left out of this PR.
🤖 Generated with Claude Code