Files
Monitorink/backend/config.py
jerem ca4febbc44 Refresh e-ink: multi-régions + full toutes les 2h (basé temps)
Le full refresh apparaissait trop souvent: getbbox() renvoyait un seul
rectangle englobant tous les pixels modifiés, donc météo (haut) + heure de
MaJ (ailleurs) qui changeaient au même cycle produisaient un rectangle
quasi plein écran -> ratio > partial_max_ratio -> full forcé.

- frame.py: détection des bandes horizontales modifiées disjointes
  (_changed_regions), refresh partiel serré par zone. Full basé sur le
  temps écoulé (last_full_at + time.monotonic) au lieu d'un compteur de
  cycles. État pngs/regions en liste, get_png(client, region).
- config.py: full_refresh_interval_minutes (MONITORINK_FULL_INTERVAL_MIN,
  défaut 120). Suppression de partial_max_ratio.
- app.py: /frame.meta renvoie un bloc multi-ligne "MODE SEQ N" + N régions
  "i x y w h"; /frame.png?region=i.
- monitorinkloop.sh: display_meta parse le bloc et fait N fbink partiels.
2026-06-16 14:06:49 +02:00

115 lines
4.7 KiB
Python

"""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 (flash, efface le ghosting) est forcé au 1er lancement/reset puis toutes les
# N minutes — indépendamment du cycle Kobo. Entre deux, on ne fait que des partiels serrés sur
# les zones réellement modifiées. En dev on descend à 1-2 min pour tester rapidement.
full_refresh_interval_minutes: int = field(
default_factory=lambda: int(_get("MONITORINK_FULL_INTERVAL_MIN", "120"))
)
@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()