With `general.screen.off_command` set, the monitor switches off once nothing has played and nobody has done anything for `auto_off_minutes` (15 by default, 0 for never, editable in parent mode). "Doing anything" is any intent a person could have caused: a remote key, a web command, or a key press or tap in the kiosk, which the page reports at most every 30s via POST /api/screen/activity. The first such intent after an auto-off runs `on_command`, so the remote wakes the screen too. The auto-off itself is silent; switches a person asks for still blubb. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
3.4 KiB
Python
106 lines
3.4 KiB
Python
"""Volume as a percentage, and the handful of settings parent mode may change.
|
|
|
|
Two jobs that both come down to "keep the config file's numbers out of the child's UI".
|
|
|
|
The volume mapping lives here rather than in the frontend so that MQTT, the rotary
|
|
encoder and the firmware carry on speaking device units, unaware that a browser is
|
|
using a different scale.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from ruamel.yaml import YAML
|
|
|
|
from musicmouse.config import GeneralConfig
|
|
from musicmouse.services.web.schemas import SettingsIn, SettingsOut
|
|
|
|
_log = logging.getLogger(__name__)
|
|
|
|
__all__ = [
|
|
"atomic_write",
|
|
"load_document",
|
|
"read_settings",
|
|
"to_device_volume",
|
|
"to_percent",
|
|
"write_settings",
|
|
]
|
|
|
|
|
|
def to_device_volume(percent: int, general: GeneralConfig) -> int:
|
|
"""Map 0..100 onto the configured range, so 100 % is exactly ``max_volume``."""
|
|
span = general.max_volume - general.min_volume
|
|
if span <= 0:
|
|
return general.min_volume
|
|
return general.min_volume + round(span * max(0, min(100, percent)) / 100)
|
|
|
|
|
|
def to_percent(volume: int, general: GeneralConfig) -> int:
|
|
"""The inverse, for the state snapshot."""
|
|
span = general.max_volume - general.min_volume
|
|
if span <= 0:
|
|
return 100
|
|
return max(0, min(100, round((volume - general.min_volume) / span * 100)))
|
|
|
|
|
|
def read_settings(general: GeneralConfig) -> SettingsOut:
|
|
screen = general.screen
|
|
return SettingsOut(
|
|
min_volume=general.min_volume,
|
|
max_volume=general.max_volume,
|
|
initial_volume=general.initial_volume,
|
|
volume_increment=general.volume_increment,
|
|
button_leds_brightness=general.button_leds_brightness,
|
|
screen_auto_off_minutes=(
|
|
screen.auto_off_minutes if screen is not None and screen.off_command else None
|
|
),
|
|
)
|
|
|
|
|
|
def _yaml() -> YAML:
|
|
yaml = YAML(typ="rt")
|
|
yaml.preserve_quotes = True
|
|
return yaml
|
|
|
|
|
|
def load_document(path: Path) -> Any:
|
|
"""The config file, round-trip parsed so it keeps its comments and formatting.
|
|
|
|
Shared by every writer that patches ``config.yml`` in place - a config that
|
|
explains itself is worth more than one a save could rewrite from scratch.
|
|
"""
|
|
with path.open(encoding="utf-8") as handle:
|
|
return _yaml().load(handle)
|
|
|
|
|
|
def atomic_write(path: Path, document: Any) -> None:
|
|
"""Write a round-trip-loaded document back, through a sibling temp file so an
|
|
interrupted save cannot truncate the real one."""
|
|
temp = path.with_name(f"{path.name}.tmp{os.getpid()}")
|
|
try:
|
|
with temp.open("w", encoding="utf-8") as handle:
|
|
_yaml().dump(document, handle)
|
|
temp.replace(path)
|
|
except BaseException:
|
|
temp.unlink(missing_ok=True)
|
|
raise
|
|
_log.info("Wrote settings to %s", path)
|
|
|
|
|
|
def write_settings(path: Path, settings: SettingsIn) -> None:
|
|
"""Patch the settings into ``config.yml`` in place."""
|
|
document = load_document(path)
|
|
general = document["general"]
|
|
values = settings.model_dump()
|
|
# The one setting that lives in a subsection rather than flat under `general`.
|
|
auto_off = values.pop("screen_auto_off_minutes")
|
|
for key, value in values.items():
|
|
general[key] = value
|
|
if auto_off is not None and general.get("screen") is not None:
|
|
general["screen"]["auto_off_minutes"] = auto_off
|
|
atomic_write(path, document)
|