Fix manual switch reverting instantly, add weather forecast

- scheduler: "Show X now" was a true one-shot -- reverted on the very next
  5s tick, causing the flash-to-X-then-flash-back-to-clock behavior. Now
  pins for 5 minutes (still preemptable by Navidrome priority, which stays
  authoritative). Added a "Resume automatic" action/button and status
  display showing what's pinned and until when.
- weather: daily forecast strip (3/5/7 days, selectable in settings) below
  current conditions, via Open-Meteo's daily= params in the same request.
  Fixed a bad symbol choice along the way -- DejaVu Sans has no glyph for
  the partly-cloudy emoji, was rendering as a tofu box; swapped to the
  plain cloud symbol, verified glyph coverage for the whole WMO code table.
This commit is contained in:
Honey committed 2026-07-21 23:06:16 +02:00
1 parent bfe4616f20
commit a19a7ea49e
7 files changed
+147 -40

No files matched your search

+35 -26
View File
@@ -22,6 +22,7 @@ from PIL import Image
logger = logging.getLogger(__name__)
TICK_SECONDS = 5
MANUAL_OVERRIDE_SECONDS = 300 # how long "show this now" sticks before falling back to automatic
PREVIEW_PATH = Path(__file__).resolve().parent / "web" / "static" / "preview.png"
@@ -44,12 +45,12 @@ class Scheduler:
self.display = display
self._caches = {screen_id: ScreenCache() for screen_id in screens}
self._current_screen_id: str | None = None
self._rotation_index = 0
self._rotation_switched_at = time.time()
self._last_image_hash: str | None = None
self._manual_screen: str | None = None
self._manual_until: float = 0.0
self._force_once = False
self._wake = threading.Event()
@@ -79,12 +80,24 @@ class Scheduler:
self._wake.set()
def request_switch(self, screen_id: str):
"""Pin a screen for MANUAL_OVERRIDE_SECONDS. Navidrome priority can
still preempt it if something starts playing -- that's the one thing
this app promises to always show reliably -- but rotation won't."""
self._manual_screen = screen_id
self._manual_until = time.time() + MANUAL_OVERRIDE_SECONDS
self._wake.set()
def request_resume_rotation(self):
self._manual_screen = None
self._wake.set()
def get_status(self) -> dict:
with self._status_lock:
return dict(self._status)
status = dict(self._status)
manual_active = bool(self._manual_screen) and time.time() < self._manual_until
status["manual_screen"] = self._manual_screen if manual_active else None
status["manual_until"] = time.strftime("%H:%M:%S", time.localtime(self._manual_until)) if manual_active else None
return status
# ------------------------------------------------------------- internal
@@ -98,18 +111,7 @@ class Scheduler:
self._wake.clear()
def _tick(self):
did_check = False
if self._manual_screen:
screen_id = self._manual_screen
self._manual_screen = None
state = self._check(screen_id)
did_check = True
else:
screen_id, state, checked = self._select_screen()
did_check = checked
self._current_screen_id = screen_id
screen_id, state, did_check = self._select_screen()
screen = self.screens[screen_id]
settings = self.config.get_screen_settings(screen_id)
resolution = self.display.resolution
@@ -138,17 +140,22 @@ class Scheduler:
self.config.save()
def _select_screen(self) -> tuple[str, object, bool]:
"""Returns (screen_id, state, did_check_happen)."""
"""Returns (screen_id, state, did_check_happen).
Priority order: Navidrome-when-playing (if enabled) always wins --
that's the one thing this app promises to show reliably. Then a
manual "show this now" pin, while it's still within its window. Then
normal rotation.
"""
if self.config.get_navidrome_priority() and "navidrome" in self.screens:
cache = self._caches["navidrome"]
navidrome = self.screens["navidrome"]
checked = False
if cache.is_stale(navidrome.poll_interval):
cache.state = self._check("navidrome")
cache.checked_at = time.time()
checked = True
if cache.state is not None:
return "navidrome", cache.state, checked
screen_id, state, checked = self._poll_screen("navidrome")
if state is not None:
return screen_id, state, checked
if self._manual_screen:
if time.time() < self._manual_until:
return self._poll_screen(self._manual_screen)
self._manual_screen = None # window expired
return self._rotation_screen()
@@ -162,8 +169,7 @@ class Scheduler:
now = time.time()
# With rotation disabled, just pin to whatever's currently selected --
# never auto-advance. (Manual "show this screen now" still works via
# request_switch(); it just won't stick past the next tick.)
# never auto-advance.
if rotation.get("enabled", True):
interval_seconds = rotation["interval_minutes"] * 60
if now - self._rotation_switched_at >= interval_seconds:
@@ -171,6 +177,9 @@ class Scheduler:
self._rotation_switched_at = now
screen_id = screen_ids[self._rotation_index % len(screen_ids)]
return self._poll_screen(screen_id)
def _poll_screen(self, screen_id: str) -> tuple[str, object, bool]:
cache = self._caches[screen_id]
screen = self.screens[screen_id]
checked = False