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.
75 lines
3.3 KiB
HTML
75 lines
3.3 KiB
HTML
{% extends "base.html" %}
|
|
{% block content %}
|
|
<h1>Rotation</h1>
|
|
|
|
<form method="post">
|
|
<div class="card">
|
|
<div class="section-label">Priority</div>
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="navidrome_priority" {{ "checked" if navidrome_priority }}>
|
|
Navidrome takes over immediately whenever something is playing, regardless of rotation
|
|
</label>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="section-label">Rotation</div>
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="rotation_enabled" {{ "checked" if rotation.enabled }}>
|
|
Enabled -- cycle through the screens below on a timer
|
|
</label>
|
|
<p class="hint" style="margin: 0.6rem 0 1.1rem;">
|
|
Clicking "Show X now" on the Status page turns this off automatically
|
|
and fixes the display to that screen until you turn rotation back on
|
|
here (Navidrome priority above still applies on top of a pin, if
|
|
enabled). With no pin and rotation off, it just sits on the first
|
|
screen in the order below.
|
|
</p>
|
|
<div class="field">
|
|
<label for="screens">Order</label>
|
|
<input type="text" id="screens" name="screens" value="{{ rotation.screens | join(', ') }}" placeholder="clock, weather">
|
|
<p class="hint">Comma-separated, in order. Available: {{ screens.keys() | join(', ') }}.</p>
|
|
</div>
|
|
<div class="field">
|
|
<label for="interval_minutes">Switch every (minutes)</label>
|
|
<input type="number" id="interval_minutes" name="interval_minutes" value="{{ rotation.interval_minutes }}" min="1">
|
|
</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 %}
|