Pygame framebuffer clock for Le Potato (ARM Debian) with aiohttp webhook server. Renders 12-hour clock directly to /dev/fb0 (no X11/Wayland). Supports full-screen message overlays pushed via a browser dashboard or Bearer-token API. Includes first-run setup wizard, session-based dashboard auth, bcrypt password storage, per-IP rate limiting, and systemd service unit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
45 lines
1.6 KiB
Python
45 lines
1.6 KiB
Python
import threading
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
@dataclass
|
|
class MessageState:
|
|
_text: str | None = field(default=None)
|
|
_expires_at: float | None = field(default=None) # monotonic timestamp
|
|
_lock: threading.Lock = field(default_factory=threading.Lock)
|
|
|
|
def set(self, text: str, duration: float | None) -> None:
|
|
"""Set message. duration=None means persistent (no expiry)."""
|
|
with self._lock:
|
|
self._text = text
|
|
self._expires_at = time.monotonic() + duration if duration else None
|
|
|
|
def clear(self) -> None:
|
|
with self._lock:
|
|
self._text = None
|
|
self._expires_at = None
|
|
|
|
def get(self) -> tuple[str | None, float | None]:
|
|
"""Return (text, remaining_seconds). Both None if no active message."""
|
|
with self._lock:
|
|
if self._text is None:
|
|
return None, None
|
|
if self._expires_at is not None and time.monotonic() >= self._expires_at:
|
|
self._text = None
|
|
self._expires_at = None
|
|
return None, None
|
|
remaining: float | None = None
|
|
if self._expires_at is not None:
|
|
remaining = max(0.0, self._expires_at - time.monotonic())
|
|
return self._text, remaining
|
|
|
|
def to_dict(self) -> dict:
|
|
text, remaining = self.get()
|
|
return {
|
|
"active": text is not None,
|
|
"text": text,
|
|
"remaining_seconds": round(remaining, 1) if remaining is not None else None,
|
|
"persistent": text is not None and remaining is None,
|
|
}
|