"Show X now" now persists indefinitely (survives restarts too, it's in config.json) instead of auto-expiring back to rotation after 5 minutes. Implemented by reusing the existing rotation.enabled flag rather than a separate scheduler-side timer: pinning a screen sets rotation.enabled=False and records which screen in a new pinned_screen config field. Re-enabling rotation -- either the "Resume automatic" button or just checking "Enabled" again on the Rotation settings page -- clears the pin and resumes normal cycling either way. Navidrome priority is unchanged: still checked first, still preempts a pin the instant something's playing, same as it already preempted rotation. Verified end-to-end: pin persists across many scheduler ticks (previously would have reverted after 5 min), both resume paths (button and settings checkbox) clear it correctly.
119 lines
4.5 KiB
Python
119 lines
4.5 KiB
Python
"""Single JSON config file, loaded once and kept in memory.
|
|
|
|
Unlike InkyPi's Config (which we spent a long time debugging), writes happen
|
|
immediately on every change from the web UI, and the scheduler re-reads the
|
|
in-memory structure fresh on every tick -- there's no "did the setting
|
|
actually take effect" gap, because there's no long sleep to get stuck in.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import threading
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_CONFIG_PATH = Path(__file__).resolve().parent / "config.json"
|
|
|
|
DEFAULT_ROTATION = {
|
|
"screens": ["clock", "weather"],
|
|
"interval_minutes": 5,
|
|
"enabled": True,
|
|
}
|
|
|
|
|
|
class Config:
|
|
def __init__(self, screens: dict, path: Path = DEFAULT_CONFIG_PATH):
|
|
self.path = path
|
|
self._lock = threading.Lock()
|
|
self._data = self._load(screens)
|
|
|
|
def _load(self, screens: dict) -> dict:
|
|
if self.path.exists():
|
|
with open(self.path) as f:
|
|
data = json.load(f)
|
|
else:
|
|
data = {}
|
|
|
|
data.setdefault("rotation", dict(DEFAULT_ROTATION))
|
|
# Migrate configs written before rotation had minutes/enabled.
|
|
if "interval_seconds" in data["rotation"] and "interval_minutes" not in data["rotation"]:
|
|
data["rotation"]["interval_minutes"] = max(1, round(data["rotation"].pop("interval_seconds") / 60))
|
|
data["rotation"].setdefault("interval_minutes", DEFAULT_ROTATION["interval_minutes"])
|
|
data["rotation"].setdefault("enabled", DEFAULT_ROTATION["enabled"])
|
|
data.setdefault("navidrome_priority", True)
|
|
data.setdefault("pinned_screen", None)
|
|
data.setdefault("settings", {})
|
|
for screen_id, screen in screens.items():
|
|
data["settings"].setdefault(screen_id, {})
|
|
for key, value in screen.default_settings().items():
|
|
data["settings"][screen_id].setdefault(key, value)
|
|
|
|
return data
|
|
|
|
def save(self) -> None:
|
|
with self._lock:
|
|
tmp_path = self.path.with_suffix(".json.tmp")
|
|
with open(tmp_path, "w") as f:
|
|
json.dump(self._data, f, indent=2)
|
|
tmp_path.replace(self.path)
|
|
logger.debug("Config written to %s", self.path)
|
|
|
|
# ------------------------------------------------------------ screens
|
|
|
|
def get_screen_settings(self, screen_id: str) -> dict:
|
|
"""Returns the live, mutable settings dict for a screen -- screens are
|
|
allowed to write ephemeral state into it (e.g. Navidrome's last-known
|
|
track for the idle fallback)."""
|
|
with self._lock:
|
|
return self._data["settings"].setdefault(screen_id, {})
|
|
|
|
def update_screen_settings(self, screen_id: str, updates: dict) -> None:
|
|
with self._lock:
|
|
self._data["settings"].setdefault(screen_id, {}).update(updates)
|
|
self.save()
|
|
|
|
# ----------------------------------------------------------- rotation
|
|
|
|
def get_rotation(self) -> dict:
|
|
with self._lock:
|
|
return dict(self._data["rotation"])
|
|
|
|
def set_rotation(self, screens: list[str], interval_minutes: int, enabled: bool) -> None:
|
|
with self._lock:
|
|
self._data["rotation"] = {"screens": screens, "interval_minutes": interval_minutes, "enabled": enabled}
|
|
if enabled:
|
|
# Re-enabling rotation (e.g. via the settings checkbox)
|
|
# supersedes any manual pin -- that's the intended way to
|
|
# get back to automatic switching.
|
|
self._data["pinned_screen"] = None
|
|
self.save()
|
|
|
|
def get_pinned_screen(self) -> str | None:
|
|
with self._lock:
|
|
return self._data.get("pinned_screen")
|
|
|
|
def pin_screen(self, screen_id: str) -> None:
|
|
"""Pin a screen indefinitely -- turns rotation off so it doesn't
|
|
fight with the pin. Stays until the pin is explicitly cleared or
|
|
rotation is re-enabled (settings checkbox, or resume_rotation())."""
|
|
with self._lock:
|
|
self._data["pinned_screen"] = screen_id
|
|
self._data["rotation"]["enabled"] = False
|
|
self.save()
|
|
|
|
def resume_rotation(self) -> None:
|
|
with self._lock:
|
|
self._data["rotation"]["enabled"] = True
|
|
self._data["pinned_screen"] = None
|
|
self.save()
|
|
|
|
def get_navidrome_priority(self) -> bool:
|
|
with self._lock:
|
|
return bool(self._data["navidrome_priority"])
|
|
|
|
def set_navidrome_priority(self, enabled: bool) -> None:
|
|
with self._lock:
|
|
self._data["navidrome_priority"] = enabled
|
|
self.save()
|