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.
This commit is contained in:
jerem
2026-06-15 18:42:32 +02:00
parent ce20d3675d
commit c7395d1c37
8 changed files with 257 additions and 27 deletions

View File

@@ -85,8 +85,9 @@ def render_html(context: dict) -> str:
return _env.get_template("dashboard.html").render(**context)
async def render_png() -> bytes:
"""Rend le dashboard en PNG niveaux de gris prêt pour l'e-ink de la Kobo."""
async def render_image() -> Image.Image:
"""Rend le dashboard en image PIL niveaux de gris (mode 'L'), déjà pivotée pour le
panneau e-ink portrait (1264x1680). Base commune au PNG et au diff de refresh partiel."""
context = await build_context()
html = render_html(context)
@@ -107,7 +108,16 @@ async def render_png() -> bytes:
# Le canevas est rendu en paysage (1680x1264) ; on pivote de 90° pour le panneau
# e-ink physiquement en portrait. "cw" = bouton à droite (rotation horaire).
rota = Image.ROTATE_270 if config.rotate == "cw" else Image.ROTATE_90
img = img.transpose(rota)
return img.transpose(rota)
def encode_png(img: Image.Image) -> bytes:
"""Encode une image PIL en PNG optimisé (helper partagé render_png / frame.py)."""
out = io.BytesIO()
img.save(out, format="PNG", optimize=True)
return out.getvalue()
async def render_png() -> bytes:
"""Rend le dashboard en PNG niveaux de gris prêt pour l'e-ink de la Kobo."""
return encode_png(await render_image())