Server: stop the photo from advancing during quiet hours too
Build and push server image / build-and-push (push) Successful in 35s
Build and push server image / build-and-push (push) Successful in 35s
Quiet hours only clamped what refresh_interval_s the *device* is told to sleep for -- the separate elapsed-time check in photo_queue.get_current() (used by /frame/image, /frame/photo-info, /frame/face-labels, and /api/queue) had no quiet-hours awareness at all, since it runs independent of the device. An open web UI tab polling /api/queue overnight, or just loading the page during a quiet window, would silently advance which photo is "current" on raw elapsed time alone -- nothing reaches the panel until the device wakes after quiet hours end, but the pointer moving mid-window still isn't what "don't do anything overnight" implies. get_current() now takes an in_quiet_hours flag that suppresses only the elapsed-time trigger; an unset/invalid current photo is still picked regardless (showing nothing is worse than showing something even at 3am). New _in_quiet_hours() helper in main.py, passed at all four call sites.
This commit is contained in:
+19
-4
@@ -129,6 +129,21 @@ def _effective_refresh_interval_s(cfg: config.FrameConfig) -> int:
|
|||||||
return min(cfg.refresh_interval_s, seconds_to_boundary)
|
return min(cfg.refresh_interval_s, seconds_to_boundary)
|
||||||
|
|
||||||
|
|
||||||
|
def _in_quiet_hours(cfg: config.FrameConfig) -> bool:
|
||||||
|
"""Whether quiet hours are in effect right now -- separate from
|
||||||
|
_effective_refresh_interval_s, which only shapes what the *device* is
|
||||||
|
told to sleep for. This instead gates photo_queue.get_current()'s
|
||||||
|
time-based advance, since that check runs independent of the device
|
||||||
|
(also triggered by the web UI's /api/queue, e.g. an open browser tab
|
||||||
|
polling overnight) and would otherwise happily advance the current
|
||||||
|
photo mid-quiet-hours on raw elapsed time alone."""
|
||||||
|
if not cfg.quiet_hours_enabled:
|
||||||
|
return False
|
||||||
|
now = datetime.now(_zoneinfo(cfg.timezone))
|
||||||
|
in_quiet, _ = _quiet_hours_state(now, cfg.quiet_hours_start, cfg.quiet_hours_end)
|
||||||
|
return in_quiet
|
||||||
|
|
||||||
|
|
||||||
def _max_expected_gap_s(cfg: config.FrameConfig) -> int:
|
def _max_expected_gap_s(cfg: config.FrameConfig) -> int:
|
||||||
"""Longest gap between wakes the device might legitimately have --
|
"""Longest gap between wakes the device might legitimately have --
|
||||||
normally just refresh_interval_s, but quiet hours can make the real
|
normally just refresh_interval_s, but quiet hours can make the real
|
||||||
@@ -350,7 +365,7 @@ def frame_image():
|
|||||||
|
|
||||||
with config.locked():
|
with config.locked():
|
||||||
cfg = config.load() # re-read: state may have changed since the unlocked read above
|
cfg = config.load() # re-read: state may have changed since the unlocked read above
|
||||||
if photo_queue.get_current(cfg, assets):
|
if photo_queue.get_current(cfg, assets, in_quiet_hours=_in_quiet_hours(cfg)):
|
||||||
config.save(cfg)
|
config.save(cfg)
|
||||||
|
|
||||||
return Response(content=_render_asset(client, cfg, cfg.current_asset_id), media_type="application/octet-stream")
|
return Response(content=_render_asset(client, cfg, cfg.current_asset_id), media_type="application/octet-stream")
|
||||||
@@ -593,7 +608,7 @@ def frame_photo_info():
|
|||||||
|
|
||||||
with config.locked():
|
with config.locked():
|
||||||
cfg = config.load() # re-read: state may have changed since the unlocked read above
|
cfg = config.load() # re-read: state may have changed since the unlocked read above
|
||||||
if photo_queue.get_current(cfg, assets):
|
if photo_queue.get_current(cfg, assets, in_quiet_hours=_in_quiet_hours(cfg)):
|
||||||
config.save(cfg)
|
config.save(cfg)
|
||||||
|
|
||||||
if not cfg.current_asset_id:
|
if not cfg.current_asset_id:
|
||||||
@@ -662,7 +677,7 @@ def frame_face_labels():
|
|||||||
|
|
||||||
with config.locked():
|
with config.locked():
|
||||||
cfg = config.load() # re-read: state may have changed since the unlocked read above
|
cfg = config.load() # re-read: state may have changed since the unlocked read above
|
||||||
if photo_queue.get_current(cfg, assets):
|
if photo_queue.get_current(cfg, assets, in_quiet_hours=_in_quiet_hours(cfg)):
|
||||||
config.save(cfg)
|
config.save(cfg)
|
||||||
|
|
||||||
if not cfg.current_asset_id:
|
if not cfg.current_asset_id:
|
||||||
@@ -703,7 +718,7 @@ def api_queue():
|
|||||||
|
|
||||||
with config.locked():
|
with config.locked():
|
||||||
cfg = config.load() # re-read: state may have changed since the unlocked read above
|
cfg = config.load() # re-read: state may have changed since the unlocked read above
|
||||||
current_changed = photo_queue.get_current(cfg, assets)
|
current_changed = photo_queue.get_current(cfg, assets, in_quiet_hours=_in_quiet_hours(cfg))
|
||||||
queue_before = list(cfg.queue)
|
queue_before = list(cfg.queue)
|
||||||
photo_queue.sync_queue_length(cfg, assets)
|
photo_queue.sync_queue_length(cfg, assets)
|
||||||
if current_changed or cfg.queue != queue_before:
|
if current_changed or cfg.queue != queue_before:
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ def sync_queue_length(cfg: FrameConfig, assets: list[dict]) -> None:
|
|||||||
_top_up(cfg, assets)
|
_top_up(cfg, assets)
|
||||||
|
|
||||||
|
|
||||||
def get_current(cfg: FrameConfig, assets: list[dict]) -> bool:
|
def get_current(cfg: FrameConfig, assets: list[dict], in_quiet_hours: bool = False) -> bool:
|
||||||
"""Time-based, idempotent path used by GET /frame/image. Advances only
|
"""Time-based, idempotent path used by GET /frame/image. Advances only
|
||||||
if the current photo is unset/invalid or refresh_interval_s has
|
if the current photo is unset/invalid or refresh_interval_s has
|
||||||
elapsed since it was set. Returns whether it changed anything, so the
|
elapsed since it was set. Returns whether it changed anything, so the
|
||||||
@@ -188,13 +188,20 @@ def get_current(cfg: FrameConfig, assets: list[dict]) -> bool:
|
|||||||
the interval is a no-op both times -- what makes an unplanned device
|
the interval is a no-op both times -- what makes an unplanned device
|
||||||
reboot safe: it just re-reads the current photo instead of skipping
|
reboot safe: it just re-reads the current photo instead of skipping
|
||||||
ahead, while a wake that lands after the interval has elapsed still
|
ahead, while a wake that lands after the interval has elapsed still
|
||||||
advances exactly once, even after a long time offline."""
|
advances exactly once, even after a long time offline.
|
||||||
|
|
||||||
|
in_quiet_hours suppresses *only* the elapsed-time trigger -- an
|
||||||
|
unset/invalid current photo still gets picked regardless, since
|
||||||
|
showing nothing is worse than showing something even at 3am. This
|
||||||
|
check runs independent of the device (also triggered by the web UI's
|
||||||
|
/api/queue), so without this an open browser tab polling overnight
|
||||||
|
would silently advance the current photo on raw elapsed time alone,
|
||||||
|
even though the device itself is correctly asleep through the
|
||||||
|
window (see main.py's _effective_refresh_interval_s)."""
|
||||||
valid_ids = {a["id"] for a in assets}
|
valid_ids = {a["id"] for a in assets}
|
||||||
stale = (
|
needs_pick = not cfg.current_asset_id or cfg.current_asset_id not in valid_ids
|
||||||
not cfg.current_asset_id
|
time_elapsed = (time.time() - cfg.current_asset_set_at) >= cfg.refresh_interval_s
|
||||||
or cfg.current_asset_id not in valid_ids
|
stale = needs_pick or (time_elapsed and not in_quiet_hours)
|
||||||
or (time.time() - cfg.current_asset_set_at) >= cfg.refresh_interval_s
|
|
||||||
)
|
|
||||||
if not stale:
|
if not stale:
|
||||||
return False
|
return False
|
||||||
advance_forced(cfg, assets)
|
advance_forced(cfg, assets)
|
||||||
|
|||||||
Reference in New Issue
Block a user