"""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)