Add time-based screen scheduling (e.g. Journey every morning 6-8)

New "Time-based schedule" section on the Rotation settings page: up to
4 rules, each a screen + a daily HH:MM-HH:MM window (crossing midnight
is fine, e.g. 22:00-02:00). While a rule's window is active, that
screen overrides rotation and any manual pin -- without touching
either's underlying state, so things go back to exactly how they were
once the window ends. Navidrome-playing still wins on top of a
schedule rule, same as it already does over everything else.

config.py: new schedules list (get_schedules/set_schedules), each rule
just {screen_id, start, end} -- being in the list *is* being enabled,
no separate flag (a settings row is "disabled" by leaving its screen
dropdown on "-- none --").

scheduler.py: _time_in_window() handles same-day and midnight-wrapping
windows, treats a zero-width window as never-matching (more likely a
blank settings row than an intentional all-day rule).
_active_scheduled_screen() slots into _select_screen() between
Navidrome-priority and the pin/rotation fallback.

Verified: unit tests for _time_in_window's edge cases (wraparound,
zero-width, malformed input) and the priority override, plus an
end-to-end run against the real app -- pinned a screen, added a rule
active right now, confirmed the status page switched to the scheduled
screen within one tick.
This commit is contained in:
Honey committed 2026-07-26 21:27:59 +02:00
1 parent 33b964f1f6
commit 9aacd4936a
5 files changed
+103 -3

No files matched your search

+17
View File
@@ -43,6 +43,7 @@ class Config:
data["rotation"].setdefault("enabled", DEFAULT_ROTATION["enabled"])
data.setdefault("navidrome_priority", True)
data.setdefault("pinned_screen", None)
data.setdefault("schedules", [])
data.setdefault("settings", {})
for screen_id, screen in screens.items():
data["settings"].setdefault(screen_id, {})
@@ -108,6 +109,22 @@ class Config:
self._data["pinned_screen"] = None
self.save()
# ----------------------------------------------------------- schedules
def get_schedules(self) -> list[dict]:
"""Each rule: {"screen_id", "start", "end"} with start/end as
"HH:MM" local time. A rule being in this list *is* it being
enabled -- there's no separate flag, disabling one is just removing
it (the settings form does this by leaving its screen dropdown on
"-- none --")."""
with self._lock:
return list(self._data["schedules"])
def set_schedules(self, schedules: list[dict]) -> None:
with self._lock:
self._data["schedules"] = schedules
self.save()
def get_navidrome_priority(self) -> bool:
with self._lock:
return bool(self._data["navidrome_priority"])