"""Configuration centralisée de Monitorink, chargée depuis l'environnement. Toutes les valeurs sensibles (token Claude, token Home Assistant) viennent de variables d'environnement / `.env` et ne sont jamais versionnées. """ from __future__ import annotations import os from dataclasses import dataclass, field from dotenv import load_dotenv load_dotenv() def _get(name: str, default: str = "") -> str: return os.environ.get(name, default).strip() def _get_list(name: str) -> list[str]: raw = _get(name) return [item.strip() for item in raw.split(",") if item.strip()] @dataclass(frozen=True) class HAEntity: """Une entité Home Assistant à afficher. Format env: `entity_id|Libellé|unité`.""" entity_id: str label: str unit: str = "" @classmethod def parse(cls, spec: str) -> "HAEntity": parts = [p.strip() for p in spec.split("|")] entity_id = parts[0] label = parts[1] if len(parts) > 1 and parts[1] else entity_id unit = parts[2] if len(parts) > 2 else "" return cls(entity_id=entity_id, label=label, unit=unit) @dataclass(frozen=True) class Config: # --- Affichage --- timezone: str = field(default_factory=lambda: _get("MONITORINK_TZ", "Europe/Paris")) locale: str = field(default_factory=lambda: _get("MONITORINK_LOCALE", "fr_FR")) # Canevas de RENDU en paysage (1680x1264). Le PNG est ensuite pivoté de 90° dans # render.py pour le panneau e-ink physiquement en portrait (1264x1680). width: int = field(default_factory=lambda: int(_get("MONITORINK_WIDTH", "1680"))) height: int = field(default_factory=lambda: int(_get("MONITORINK_HEIGHT", "1264"))) # Sens de rotation pour l'affichage Kobo : "cw" (bouton à droite) ou "ccw". rotate: str = field(default_factory=lambda: _get("MONITORINK_ROTATE", "cw").lower()) # --- Claude --- # Chemin du fichier .credentials.json d'un login Claude ISOLÉ dédié à Monitorink # (CLAUDE_CONFIG_DIR séparé). Le backend y lit/écrit (refresh) sans toucher le # ~/.claude partagé. L'endpoint /usage exige le scope user:profile -> login complet # requis (le token `claude setup-token` ne suffit pas, scope insuffisant). claude_creds_path: str = field( default_factory=lambda: _get("MONITORINK_CLAUDE_CREDS", "/creds/.credentials.json") ) claude_ua: str = field( default_factory=lambda: _get("MONITORINK_CLAUDE_UA", "claude-code/2.1.172") ) ccusage_enabled: bool = field( default_factory=lambda: _get("MONITORINK_CCUSAGE", "0") in ("1", "true", "yes") ) # --- Météo (Open-Meteo, sans clé) --- weather_lat: float = field(default_factory=lambda: float(_get("MONITORINK_LAT", "48.8566"))) weather_lon: float = field(default_factory=lambda: float(_get("MONITORINK_LON", "2.3522"))) # --- Home Assistant --- ha_base_url: str = field(default_factory=lambda: _get("MONITORINK_HA_URL").rstrip("/")) ha_token: str = field(default_factory=lambda: _get("MONITORINK_HA_TOKEN")) # --- NAS (moniteur maison nas_monitor, endpoint /api/status) --- nas_url: str = field(default_factory=lambda: _get("MONITORINK_NAS_URL")) # --- Codex (usage ChatGPT/Codex via wham/usage) --- # Fichier auth.json de Hermes monté en lecture seule : Hermes y maintient un token # openai-codex frais. Monitorink le relit à chaque rendu (aucun refresh côté Monitorink). codex_token_file: str = field( default_factory=lambda: _get("MONITORINK_CODEX_TOKEN_FILE", "/hermes-auth.json") ) # --- Cache / rafraîchissement serveur --- cache_ttl_seconds: int = field( default_factory=lambda: int(_get("MONITORINK_CACHE_TTL", "120")) ) # Intervalle mini entre deux appels réels à l'endpoint /usage de Claude (rate-limité). # Indépendant de la cadence de rendu : protège du 429 quand on rend souvent (dev 30 s). usage_ttl_seconds: int = field( default_factory=lambda: int(_get("MONITORINK_USAGE_TTL", "120")) ) # --- Refresh partiel e-ink (endpoints /frame.*) --- # Un full refresh est forcé tous les N cycles pour effacer le ghosting (1=toujours full). # En prod 12 (~1 h à 5 min/cycle) ; en dev on descend à 2 (~1 min à 30 s/cycle). full_refresh_every: int = field( default_factory=lambda: int(_get("MONITORINK_FULL_EVERY", "12")) ) # Si la zone modifiée dépasse cette fraction de l'écran, on bascule en full (partiel inutile). partial_max_ratio: float = field( default_factory=lambda: float(_get("MONITORINK_PARTIAL_MAX_RATIO", "0.6")) ) @property def ha_entities(self) -> list[HAEntity]: return [HAEntity.parse(s) for s in _get_list("MONITORINK_HA_ENTITIES")] @property def ha_enabled(self) -> bool: return bool(self.ha_base_url and self.ha_token) config = Config()