forked from Rockachopa/Timmy-time-dashboard
Co-authored-by: Kimi Agent <kimi@timmy.local> Co-committed-by: Kimi Agent <kimi@timmy.local>
83 lines
2.6 KiB
Python
83 lines
2.6 KiB
Python
"""Time adapter — circadian awareness for Timmy.
|
|
|
|
Emits time-of-day events so Timmy knows the current period
|
|
and tracks how long since the last user interaction.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import UTC, datetime
|
|
|
|
from infrastructure.events.bus import emit
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Time-of-day periods: (event_name, start_hour, end_hour)
|
|
_PERIODS = [
|
|
("morning", 6, 9),
|
|
("afternoon", 12, 14),
|
|
("evening", 18, 20),
|
|
("late_night", 23, 24),
|
|
("late_night", 0, 3),
|
|
]
|
|
|
|
|
|
def classify_period(hour: int) -> str | None:
|
|
"""Return the circadian period name for a given hour, or None."""
|
|
for name, start, end in _PERIODS:
|
|
if start <= hour < end:
|
|
return name
|
|
return None
|
|
|
|
|
|
class TimeAdapter:
|
|
"""Emits circadian and interaction-tracking events."""
|
|
|
|
def __init__(self) -> None:
|
|
self._last_interaction: datetime | None = None
|
|
self._last_period: str | None = None
|
|
self._last_date: str | None = None
|
|
|
|
def record_interaction(self, now: datetime | None = None) -> None:
|
|
"""Record a user interaction timestamp."""
|
|
self._last_interaction = now or datetime.now(UTC)
|
|
|
|
def time_since_last_interaction(
|
|
self,
|
|
now: datetime | None = None,
|
|
) -> float | None:
|
|
"""Seconds since last user interaction, or None if no interaction."""
|
|
if self._last_interaction is None:
|
|
return None
|
|
current = now or datetime.now(UTC)
|
|
return (current - self._last_interaction).total_seconds()
|
|
|
|
async def tick(self, now: datetime | None = None) -> list[str]:
|
|
"""Check current time and emit relevant events.
|
|
|
|
Returns list of event types emitted (useful for testing).
|
|
"""
|
|
current = now or datetime.now(UTC)
|
|
emitted: list[str] = []
|
|
|
|
# --- new_day ---
|
|
date_str = current.strftime("%Y-%m-%d")
|
|
if self._last_date is not None and date_str != self._last_date:
|
|
event_type = "time.new_day"
|
|
await emit(event_type, source="time_adapter", data={"date": date_str})
|
|
emitted.append(event_type)
|
|
self._last_date = date_str
|
|
|
|
# --- circadian period ---
|
|
period = classify_period(current.hour)
|
|
if period is not None and period != self._last_period:
|
|
event_type = f"time.{period}"
|
|
await emit(
|
|
event_type,
|
|
source="time_adapter",
|
|
data={"hour": current.hour, "period": period},
|
|
)
|
|
emitted.append(event_type)
|
|
self._last_period = period
|
|
|
|
return emitted
|