Add configurable display mode: rolling window or daily reset

New config options:
- mode = "window" (rolling hours, default) or "today" (daily reset)
- start_time = "06:00" (when today mode resets)
- hold_overnight = true (keep overnight collage until first bird)

In "today" mode the collage only shows birds since start_time,
growing through the day. With hold_overnight=true, the display
keeps the previous full collage until the first bird is detected
after start_time (no blank screen at dawn).

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
Chris Sader
2026-07-17 14:48:09 -05:00
parent 6884cce3c7
commit d586be0fac
+36 -2
View File
@@ -45,6 +45,9 @@ DEFAULTS = {
"bw_days": 7, # BirdWeather lookback window, in days
"bw_country": "us", # geocoder country for the ZIP
"hours": 24,
"mode": "window", # "window" = rolling hours; "today" = daily reset
"start_time": "06:00", # "today" mode: collage resets at this time
"hold_overnight": True, # "today" mode: keep overnight collage until first bird
"image": "", # local PNG written by the shooter
"image_url": "", # or a published screenshot URL
"shoot": False, # or capture inline (needs a browser; the 3 A+ and Zero 2 W both handle it)
@@ -109,6 +112,27 @@ def fetch_species(cfg, auth=None):
return fetch_recent(cfg["base_url"], cfg["hours"], cfg["timeout"], auth)
def _today_hours(cfg):
"""Compute the hours window for 'today' mode.
Returns hours since start_time today (or since yesterday's start_time
if we haven't reached it yet today).
"""
now = datetime.now()
parts = cfg["start_time"].split(":")
start_h, start_m = int(parts[0]), int(parts[1]) if len(parts) > 1 else 0
start_today = now.replace(hour=start_h, minute=start_m, second=0, microsecond=0)
if now < start_today:
from datetime import timedelta
yesterday_start = start_today - timedelta(days=1)
hours = (now - yesterday_start).total_seconds() / 3600
return max(1, hours)
hours = (now - start_today).total_seconds() / 3600
return max(1, hours)
# --- image ------------------------------------------------------------------
def get_image(src, timeout, auth=None):
if re.match(r"^https?://", src):
@@ -317,7 +341,7 @@ def in_quiet_hours(cfg, hour):
# --- run --------------------------------------------------------------------
def obtain_image(cfg, species=None):
def obtain_image(cfg, species=None, window_hours=None):
if cfg.get("species_source") == "birdweather":
from shoot import shoot_birdweather
if species is None: # gate skipped (--no-signature): fetch the list to render
@@ -335,6 +359,7 @@ def obtain_image(cfg, species=None):
headline_px=cfg["shoot_headline_px"], eyebrow_px=cfg["shoot_eyebrow_px"],
lowercase=cfg["shoot_lowercase"], mat=cfg["shoot_mat"],
small_floor=cfg["shoot_small_floor"], count_exp=cfg["shoot_count_exp"], timeout_ms=cfg["timeout"] * 1000,
window_hours=int(window_hours) if window_hours else None,
user=cfg["basic_user"], password=cfg["basic_pass"])
return Image.open(out).convert("RGB")
src = cfg["image_url"] or cfg["image"]
@@ -348,8 +373,17 @@ def run(cfg, preview=None, force=False, use_signature=True, mat_box=False):
state = load_state(cfg["state"])
sig = None
species = None
window_hours = None
if cfg["mode"] == "today":
window_hours = _today_hours(cfg)
if use_signature:
try:
if cfg["mode"] == "today":
species = fetch_recent(cfg["base_url"], max(1, int(window_hours + 0.5)), cfg["timeout"], _auth(cfg))
if not species and cfg["hold_overnight"]:
print("today mode: no birds yet, holding overnight collage")
return
else:
species = fetch_species(cfg, _auth(cfg))
sig = signature(species)
except Exception as e:
@@ -366,7 +400,7 @@ def run(cfg, preview=None, force=False, use_signature=True, mat_box=False):
print("refresh:", "changed" if changed else "heal")
try:
img = fit_panel(obtain_image(cfg, species))
img = fit_panel(obtain_image(cfg, species, window_hours))
except Exception as e:
print(f"could not get image: {e}", file=sys.stderr) # keep last panel image
return