Files
musicmouse/python-backend/musicmouse/services/web/state.py
Martin Bauer a55cc4f67f Clock: a "Delphin Uhr" overlay with the school bus and a parents' timer
Einfg on the kiosk keyboard, or Guide (KEY_EPG) on the IR remote, shows a big
analog watch over whatever page is open, built from the Delphin Uhr mockup
without its swimming dolphin. The overlay stays mounted and is only hidden, so it
appears at once. Beside the watch, only while they run:

- The bus: on school days, a silent countdown to `general.clock.bus_time` (07:25)
  that starts `bus_lead_minutes` (60) before. School days skip weekends and the
  school and public holidays of `general.clock.subdivision` (DE-BY), fetched daily
  from openholidaysapi.org and cached under the library cache. A badge on the
  clock says whether there is school today, and why not.
- The dolphin timer, set in parent mode (5/10/15/30 minutes, or until a time).
  It lives in the backend, so a parent's phone can start it for the kiosk. When it
  runs out the kiosk opens the clock at 00:00 and a soft bell chimes on the
  device; it clears after a minute, or at the next key, tap or remote press.

Parent mode is now a way in rather than a page: `?parentMode=1` adds a 🔧 button
next to the ⌨ one, or an "Eltern" tab in the phone's dock, so the URL can stay
bookmarked. The timer comes first there, the device settings below it.

Unmapped IR buttons are logged at debug level, to find out what irserver calls a
new one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:45:45 +02:00

80 lines
2.8 KiB
Python

"""One place that answers "what is the mouse doing right now?".
Assembled on demand rather than accumulated from events. The join between what is
playing and which figure is on the reader is the same one the MQTT player entity makes:
read ``mouse.active_figure``, do not keep a second copy of it that can drift.
"""
from __future__ import annotations
from datetime import date
from musicmouse.app import App
from musicmouse.services.web.schemas import (
BusOut,
ClockStateOut,
ConnectionOut,
PlayerStateOut,
TimerOut,
)
from musicmouse.services.web.settings import to_percent
__all__ = ["snapshot"]
def snapshot(app: App) -> PlayerStateOut:
player = app.player
playlist = player.playlist
album = app.album_for(playlist)
index = player.track_index
# The player only knows a file path, and a tagged file's name is rarely its title
# ("01 - So ein schoener Tag.mp3"). The library read the real one at scan time.
indexed = (
album.tracks[index] if album and 0 <= index < len(album.tracks) else None
)
track = player.current_track
return PlayerStateOut(
playing=player.is_playing,
album_id=album.id if album else None,
album_title=album.title if album else (playlist.name if playlist else None),
artist=album.artist if album else None,
kind=album.kind if album else None,
track_index=index,
track_title=indexed.title if indexed else (track.title if track else None),
track_count=len(playlist) if playlist else 0,
position=player.position,
# Likewise the duration: the player only has one once libVLC has opened the file.
duration=(indexed.duration if indexed else 0.0) or player.duration,
volume=to_percent(player.volume, app.config.general),
active_figure=app.mouse.active_figure,
connected=ConnectionOut(
firmware=app.mouse.connected,
mqtt=app.state.mqtt_connected,
lirc=app.state.lirc_connected,
),
clock=clock_state(app),
)
def clock_state(app: App, today: date | None = None) -> ClockStateOut:
state = app.state
config = app.config.general.clock
# Without a holiday calendar a weekday counts as a school day: a bus countdown on
# a holiday is a smaller surprise than none on the first day back.
today = today or date.today()
school = state.school_day if state.school_day is not None else today.weekday() < 5
timer = state.parent_timer
return ClockStateOut(
school_day=state.school_day,
school_day_reason=state.school_day_reason,
bus=BusOut(time=config.bus_time, lead_minutes=config.bus_lead_minutes)
if school
else None,
parent_timer=TimerOut(
started_at=timer.started_at, ends_at=timer.ends_at, ringing=timer.ringing
)
if timer
else None,
)