From ba7ea0af3fd6899845d2a3e439c208b5246e6c10 Mon Sep 17 00:00:00 2001 From: jerem Date: Mon, 15 Jun 2026 16:01:09 +0200 Subject: [PATCH] Affiche la batterie de la Kobo (push via params /image.png) en pied de page --- backend/app.py | 5 ++- backend/integrations/kobo.py | 53 ++++++++++++++++++++++++++++++++ backend/render.py | 3 +- backend/templates/dashboard.html | 1 + kobo/monitorinkloop.sh | 30 ++++++++++++++++-- 5 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 backend/integrations/kobo.py diff --git a/backend/app.py b/backend/app.py index 0044ecd..fface94 100644 --- a/backend/app.py +++ b/backend/app.py @@ -14,6 +14,7 @@ from fastapi.responses import HTMLResponse import render from config import config +from integrations import kobo app = FastAPI(title="Monitorink", docs_url=None, redoc_url=None) @@ -26,7 +27,9 @@ async def health() -> dict: @app.get("/image.png") -async def image(fresh: int = 0) -> Response: +async def image(fresh: int = 0, bat: int | None = None, chg: int = 0) -> Response: + # La Kobo pousse sa batterie ici (bat=0-100, chg=1 si en charge) à chaque fetch. + kobo.record(bat, bool(chg)) now = time.time() cached = _cache["png"] age = now - float(_cache["ts"]) diff --git a/backend/integrations/kobo.py b/backend/integrations/kobo.py new file mode 100644 index 0000000..90a8efb --- /dev/null +++ b/backend/integrations/kobo.py @@ -0,0 +1,53 @@ +"""État de la liseuse Kobo (batterie), POUSSÉ par le script monitorinkloop.sh. + +Contrairement aux autres intégrations qui *tirent* leurs données depuis une API, ici la +Kobo *pousse* son niveau de batterie en paramètres de l'URL /image.png à chaque fetch +(elle seule connaît sa charge). On conserve la dernière valeur en mémoire, horodatée, +pour la rendre au prochain dessin du dashboard. +""" +from __future__ import annotations + +import time +from dataclasses import dataclass + +# Au-delà de ce délai sans nouvelle de la Kobo, la valeur est jugée périmée. +STALE_AFTER_SECONDS = 1800 + + +@dataclass +class KoboState: + percent: int | None = None + charging: bool = False + updated_ts: float = 0.0 + + @property + def ok(self) -> bool: + return self.percent is not None + + @property + def age_seconds(self) -> float: + return time.time() - self.updated_ts if self.updated_ts else float("inf") + + @property + def stale(self) -> bool: + return self.age_seconds > STALE_AFTER_SECONDS + + @property + def low(self) -> bool: + return self.percent is not None and self.percent <= 15 and not self.charging + + +_state = KoboState() + + +def record(percent: int | None, charging: bool) -> None: + """Enregistre le dernier niveau rapporté par la Kobo (borné à 0-100).""" + if percent is None: + return + _state.percent = max(0, min(100, percent)) + _state.charging = charging + _state.updated_ts = time.time() + + +def current() -> KoboState: + return _state diff --git a/backend/render.py b/backend/render.py index b1b909f..3d7c762 100644 --- a/backend/render.py +++ b/backend/render.py @@ -13,7 +13,7 @@ from PIL import Image from playwright.async_api import async_playwright from config import config -from integrations import claude_usage, codex, homeassistant, nas, weather +from integrations import claude_usage, codex, homeassistant, kobo, nas, weather TEMPLATES = Path(__file__).parent / "templates" @@ -75,6 +75,7 @@ async def build_context() -> dict: "ha_states": ha, "nas": nas_status, "codex": codex_status, + "kobo": kobo.current(), "updated": now.strftime("%H:%M"), "stale": False, } diff --git a/backend/templates/dashboard.html b/backend/templates/dashboard.html index 7d88ea6..17417b1 100644 --- a/backend/templates/dashboard.html +++ b/backend/templates/dashboard.html @@ -189,6 +189,7 @@ diff --git a/kobo/monitorinkloop.sh b/kobo/monitorinkloop.sh index ecf8e79..c6a46a1 100755 --- a/kobo/monitorinkloop.sh +++ b/kobo/monitorinkloop.sh @@ -18,10 +18,36 @@ BUSYBOX="./bin/busybox_kobo" log() { echo "[$(date '+%H:%M:%S')] $*"; sync; } +read_battery() { + # Renvoie "CAP|CHG" (ex. "85|0"), CHG=1 si en charge. Vide si introuvable. + # On lit capacity + status dans le même dossier /sys/class/power_supply/*. + for d in /sys/class/power_supply/*/; do + [ -r "${d}capacity" ] || continue + cap=$(cat "${d}capacity" 2>/dev/null) + chg=0 + if [ -r "${d}status" ]; then + case "$(cat "${d}status" 2>/dev/null)" in + Charging|Full) chg=1 ;; + esac + fi + echo "${cap}|${chg}" + return 0 + done + echo "" +} + fetch() { + # On pousse la batterie de la Kobo en paramètres d'URL (le backend la mémorise). + url="$IMAGE_URL" + bat="$(read_battery)" + if [ -n "$bat" ]; then + cap="${bat%%|*}"; chg="${bat##*|}" + case "$url" in *\?*) sep="&" ;; *) sep="?" ;; esac + url="${url}${sep}bat=${cap}&chg=${chg}" + fi # busybox wget (toujours présent), fallback curl si dispo dans le PATH. - "$BUSYBOX" wget -q -T 30 -O "$TMP" "$IMAGE_URL" 2>/dev/null && return 0 - command -v curl >/dev/null 2>&1 && curl -fsSL -m 30 -o "$TMP" "$IMAGE_URL" 2>/dev/null && return 0 + "$BUSYBOX" wget -q -T 30 -O "$TMP" "$url" 2>/dev/null && return 0 + command -v curl >/dev/null 2>&1 && curl -fsSL -m 30 -o "$TMP" "$url" 2>/dev/null && return 0 return 1 }