diff --git a/frame/display.py b/frame/display.py index 4313144..4652e2a 100644 --- a/frame/display.py +++ b/frame/display.py @@ -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,9 +373,18 @@ 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: - species = fetch_species(cfg, _auth(cfg)) + 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: print(f"signature fetch failed: {e}", file=sys.stderr) # treat as no change @@ -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