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

+13
View File
@@ -12,6 +12,7 @@ from screens.picture import UPLOAD_DIR
ALLOWED_IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "bmp", "webp"}
PIL_FORMAT_TO_EXT = {"JPEG": "jpg", "PNG": "png", "GIF": "gif", "BMP": "bmp", "WEBP": "webp"}
PICTURE_URL_TIMEOUT = 15
MAX_SCHEDULE_RULES = 4
PICTURE_URL_HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
}
@@ -289,12 +290,24 @@ def create_app(config, screens: dict, scheduler) -> Flask:
enabled = "rotation_enabled" in request.form
config.set_rotation(selected, interval_minutes, enabled)
config.set_navidrome_priority("navidrome_priority" in request.form)
schedule_rules = []
for i in range(1, MAX_SCHEDULE_RULES + 1):
rule_screen = request.form.get(f"schedule_screen_{i}", "").strip()
start = request.form.get(f"schedule_start_{i}", "").strip()
end = request.form.get(f"schedule_end_{i}", "").strip()
if rule_screen in screens and start and end:
schedule_rules.append({"screen_id": rule_screen, "start": start, "end": end})
config.set_schedules(schedule_rules)
return redirect(url_for("settings_rotation", saved=1))
return render_template(
"settings_rotation.html",
screens=screens,
rotation=config.get_rotation(),
navidrome_priority=config.get_navidrome_priority(),
schedules=config.get_schedules(),
max_schedule_rules=MAX_SCHEDULE_RULES,
)
return app