Server: stop the photo from advancing during quiet hours too
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:
2026-07-21 00:05:46 -04:00
parent 475888306e
commit 759c042f92
2 changed files with 33 additions and 11 deletions
+19 -4
View File
@@ -129,6 +129,21 @@ def _effective_refresh_interval_s(cfg: config.FrameConfig) -> int:
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:
"""Longest gap between wakes the device might legitimately have --
normally just refresh_interval_s, but quiet hours can make the real
@@ -350,7 +365,7 @@ def frame_image():
with config.locked():
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)
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():
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)
if not cfg.current_asset_id:
@@ -662,7 +677,7 @@ def frame_face_labels():
with config.locked():
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)
if not cfg.current_asset_id:
@@ -703,7 +718,7 @@ def api_queue():
with config.locked():
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)
photo_queue.sync_queue_length(cfg, assets)
if current_changed or cfg.queue != queue_before:
+14 -7
View File
@@ -180,7 +180,7 @@ def sync_queue_length(cfg: FrameConfig, assets: list[dict]) -> None:
_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
if the current photo is unset/invalid or refresh_interval_s has
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
reboot safe: it just re-reads the current photo instead of skipping
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}
stale = (
not cfg.current_asset_id
or cfg.current_asset_id not in valid_ids
or (time.time() - cfg.current_asset_set_at) >= cfg.refresh_interval_s
)
needs_pick = not cfg.current_asset_id or cfg.current_asset_id not in valid_ids
time_elapsed = (time.time() - cfg.current_asset_set_at) >= cfg.refresh_interval_s
stale = needs_pick or (time_elapsed and not in_quiet_hours)
if not stale:
return False
advance_forced(cfg, assets)