from __future__ import annotations import json from datetime import datetime, timezone from pathlib import Path from typing import Any from pydantic import BaseModel, Field from .automation import AutomationDocument from .models import ActionEvent LOCAL_DIR = Path(".local") CONFIG_PATH = LOCAL_DIR / "config.yaml" AUTOMATIONS_PATH = LOCAL_DIR / "automations.json" STATE_PATH = LOCAL_DIR / "state.json" EVENTS_PATH = LOCAL_DIR / "events.jsonl" class LocalConfig(BaseModel): camera_url: str | None = None webhook_url: str | None = None default_classes: str | None = None default_detector_model: str | None = None default_device: str | None = None default_image_size: int | None = None default_max_detections: int | None = None llm_provider: str | None = None replicate_api_token: str | None = None replicate_model: str | None = None replicate_reasoning_effort: str | None = None openai_api_key: str | None = None openai_model: str | None = None anthropic_api_key: str | None = None anthropic_model: str | None = None class RuntimeState(BaseModel): last_fired: dict[str, float] = Field(default_factory=dict) snoozed_until: dict[str, float] = Field(default_factory=dict) def load_local_config(path: Path = CONFIG_PATH) -> LocalConfig: if not path.exists(): return LocalConfig() try: import yaml except ImportError as exc: # pragma: no cover - dependency guard raise RuntimeError("Install PyYAML to load local config.") from exc data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} return LocalConfig.model_validate(data) def load_saved_automations(path: Path = AUTOMATIONS_PATH) -> AutomationDocument | None: if not path.exists(): return None return AutomationDocument.model_validate(json.loads(path.read_text(encoding="utf-8"))) def save_automations(document: AutomationDocument, path: Path = AUTOMATIONS_PATH) -> None: _ensure_parent(path) path.write_text(document.model_dump_json(by_alias=True, indent=2), encoding="utf-8") def load_runtime_state(path: Path = STATE_PATH) -> RuntimeState: if not path.exists(): return RuntimeState() return RuntimeState.model_validate(json.loads(path.read_text(encoding="utf-8"))) def save_runtime_state(state: RuntimeState, path: Path = STATE_PATH) -> None: _ensure_parent(path) path.write_text(state.model_dump_json(indent=2), encoding="utf-8") def append_events(events: list[ActionEvent], path: Path = EVENTS_PATH) -> None: if not events: return _ensure_parent(path) with path.open("a", encoding="utf-8") as file: for event in events: payload: dict[str, Any] = event.model_dump(mode="json") payload["recorded_at"] = datetime.now(timezone.utc).isoformat() file.write(json.dumps(payload, ensure_ascii=False) + "\n") def _ensure_parent(path: Path) -> None: path.parent.mkdir(parents=True, exist_ok=True)