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