Files
Monitorink/backend/app.py
jerem c7395d1c37 Refresh partiel e-ink : ne redessine que la zone changée, full refresh ~1h
Backend : endpoints /frame.meta (ligne 'MODE X Y W H SEQ') + /frame.png qui
servent un crop de la zone modifiée (diff PIL par client) ou l'image pleine.
Full refresh forcé tous les N cycles (MONITORINK_FULL_EVERY=12, ~1h) ou si la
zone change sur plus de 60% de l'écran. Mode 'noop' quand rien ne change.

Anti-429 : l'usage Claude est mis en cache (MONITORINK_USAGE_TTL=120s) avec
repli sur la dernière valeur connue en cas d'erreur transitoire.

Kobo : monitorinkloop.sh récupère meta puis png et fait un fbink partiel
(-g file=,x=,y=) sans flash, full refresh (-c -f) en mode full. Refresh 5 min.
2026-06-15 18:42:32 +02:00

74 lines
2.5 KiB
Python

"""Serveur Monitorink : expose le dashboard en PNG pour la Kobo.
Endpoints :
GET /image.png -> dashboard 1264x1680 niveaux de gris (avec cache TTL)
GET /debug.html -> HTML brut (itération design, pas de screenshot)
GET /health -> sonde de vie
"""
from __future__ import annotations
import time
from fastapi import FastAPI, Response
from fastapi.responses import HTMLResponse, PlainTextResponse
import frame
import render
from config import config
from integrations import kobo
app = FastAPI(title="Monitorink", docs_url=None, redoc_url=None)
_cache: dict[str, object] = {"png": None, "ts": 0.0}
@app.get("/health")
async def health() -> dict:
return {"status": "ok"}
@app.get("/image.png")
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"])
if cached and not fresh and age < config.cache_ttl_seconds:
png = cached # type: ignore[assignment]
else:
png = await render.render_png()
_cache["png"] = png
_cache["ts"] = now
return Response(
content=png, # type: ignore[arg-type]
media_type="image/png",
headers={"Cache-Control": "no-store"},
)
@app.get("/frame.meta", response_class=PlainTextResponse)
async def frame_meta(client: str = "kobo", bat: int | None = None, chg: int = 0) -> Response:
# Refresh partiel : rend l'image, calcule la zone modifiée vs le dernier frame de ce client,
# et renvoie une ligne "MODE X Y W H SEQ" triviale à parser en shell busybox.
# MODE ∈ {full, partial, noop}. Le PNG correspondant est récupéré via /frame.png.
kobo.record(bat, bool(chg))
info = await frame.compute_frame(client)
line = f"{info['mode']} {info['x']} {info['y']} {info['w']} {info['h']} {info['seq']}"
return PlainTextResponse(line, headers={"Cache-Control": "no-store"})
@app.get("/frame.png")
async def frame_png(client: str = "kobo") -> Response:
# PNG décidé lors du dernier /frame.meta (crop en partial, image pleine en full).
png = frame.get_png(client)
if png is None:
return Response(status_code=503)
return Response(content=png, media_type="image/png", headers={"Cache-Control": "no-store"})
@app.get("/debug.html", response_class=HTMLResponse)
async def debug_html() -> str:
context = await render.build_context()
return render.render_html(context)