80bef6f524
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
79 lines
2.6 KiB
Python
79 lines
2.6 KiB
Python
"""Quiet hours — when the pet is allowed to make noise on its own.
|
|
|
|
Napping only ever suppresses *proactive* noise (heartbeat announcements,
|
|
forwarded notifications) and wandering. The wake word, click-to-talk and
|
|
push-to-talk still work: telling it to be quiet shouldn't mean it stops
|
|
answering when spoken to.
|
|
|
|
Pure logic (parsing + a time comparison) so it's testable without waiting
|
|
for 11pm.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import time as dtime
|
|
from typing import Iterable, Optional
|
|
|
|
|
|
class QuietHoursError(ValueError):
|
|
pass
|
|
|
|
|
|
def _parse_clock(value: str) -> dtime:
|
|
parts = value.strip().split(":")
|
|
if len(parts) != 2:
|
|
raise QuietHoursError(f"expected HH:MM, got {value!r}")
|
|
try:
|
|
hour, minute = int(parts[0]), int(parts[1])
|
|
except ValueError as exc:
|
|
raise QuietHoursError(f"expected HH:MM, got {value!r}") from exc
|
|
if not (0 <= hour <= 23 and 0 <= minute <= 59):
|
|
raise QuietHoursError(f"{value!r} is not a real time of day")
|
|
return dtime(hour, minute)
|
|
|
|
|
|
def parse_ranges(spec: str) -> list[tuple[dtime, dtime]]:
|
|
""""23:00-08:00, 13:00-14:00" -> [(23:00, 08:00), (13:00, 14:00)].
|
|
Empty/blank spec means "no quiet hours"."""
|
|
ranges: list[tuple[dtime, dtime]] = []
|
|
for chunk in (spec or "").split(","):
|
|
chunk = chunk.strip()
|
|
if not chunk:
|
|
continue
|
|
start, sep, end = chunk.partition("-")
|
|
if not sep:
|
|
raise QuietHoursError(f"expected HH:MM-HH:MM, got {chunk!r}")
|
|
ranges.append((_parse_clock(start), _parse_clock(end)))
|
|
return ranges
|
|
|
|
|
|
def in_ranges(now: dtime, ranges: Iterable[tuple[dtime, dtime]]) -> bool:
|
|
for start, end in ranges:
|
|
if start == end:
|
|
continue # a zero-length range is a typo, not "all day"
|
|
if start < end:
|
|
if start <= now < end:
|
|
return True
|
|
elif now >= start or now < end: # wraps past midnight
|
|
return True
|
|
return False
|
|
|
|
|
|
def is_quiet(spec: str, now: Optional[dtime] = None, on_error=None) -> bool:
|
|
"""True if *now* (defaults to the local wall clock) falls inside *spec*.
|
|
A malformed spec is reported via *on_error* and treated as "not quiet" —
|
|
a config typo shouldn't silently mute the pet forever."""
|
|
if not (spec or "").strip():
|
|
return False
|
|
try:
|
|
ranges = parse_ranges(spec)
|
|
except QuietHoursError as exc:
|
|
if on_error is not None:
|
|
on_error(exc)
|
|
return False
|
|
if now is None:
|
|
from datetime import datetime
|
|
|
|
now = datetime.now().time()
|
|
return in_ranges(now, ranges)
|