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

+1 -1
View File
@@ -99,7 +99,7 @@
label { display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.85rem; }
.hint { font-size: 0.78rem; color: var(--text-muted); margin: 0.35rem 0 0; }
input[type=text], input[type=password], input[type=number], select, input[type=file] {
input[type=text], input[type=password], input[type=number], input[type=time], select, input[type=file] {
width: 100%;
padding: 0.6rem 0.7rem;
font-size: 0.95rem;
+34
View File
@@ -35,6 +35,40 @@
</div>
</div>
<div class="card">
<div class="section-label">Time-based schedule</div>
<p class="hint" style="margin: -0.25rem 0 1rem;">
Show a specific screen automatically during a daily time window --
handy for e.g. checking bus departures every morning. Takes priority
over rotation and any manual pin while active (Navidrome priority
above still wins over this if enabled), and reverts back on its own
once the window ends. A window can cross midnight (e.g. 22:00-02:00).
Leave a row's screen on "-- none --" to disable it.
</p>
{% for i in range(1, max_schedule_rules + 1) %}
{% set rule = schedules[i - 1] if i - 1 < schedules|length else None %}
<div class="field" style="display: flex; gap: 0.6rem; align-items: flex-end; flex-wrap: wrap;">
<div style="flex: 1; min-width: 160px;">
<label for="schedule_screen_{{ i }}">Screen</label>
<select id="schedule_screen_{{ i }}" name="schedule_screen_{{ i }}">
<option value="">-- none --</option>
{% for screen_id, screen in screens.items() | sort(attribute="1.display_name") %}
<option value="{{ screen_id }}" {{ "selected" if rule and rule.screen_id == screen_id }}>{{ screen.display_name }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="schedule_start_{{ i }}">From</label>
<input type="time" id="schedule_start_{{ i }}" name="schedule_start_{{ i }}" value="{{ rule.start if rule else '' }}">
</div>
<div>
<label for="schedule_end_{{ i }}">To</label>
<input type="time" id="schedule_end_{{ i }}" name="schedule_end_{{ i }}" value="{{ rule.end if rule else '' }}">
</div>
</div>
{% endfor %}
</div>
<button type="submit">Save</button>
</form>
{% endblock %}