Nouveau module integrations/trackers.py : pour chaque tracker configuré (env MONITORINK_TRACKERS + bloc par clé), récupère ratio/uploaded/downloaded. Type unit3d_nuxt (c411) : login session (CSRF meta + /api/auth/login) car le ratio n'est pas lisible au token API ; session réutilisée, résultat caché (TTL 30 min). Section dashboard sous le NAS, style instrument 1-bit. Architecture par type pour ajouter d'autres trackers ensuite.
128 lines
4.5 KiB
Python
128 lines
4.5 KiB
Python
"""Construit le contexte, rend le HTML en paysage (1680x1264) puis le capture en PNG
|
|
niveaux de gris, pivoté de 90° pour le panneau e-ink portrait (1264x1680)."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import io
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from zoneinfo import ZoneInfo
|
|
|
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
from PIL import Image
|
|
from playwright.async_api import async_playwright
|
|
|
|
from config import config
|
|
from fonts import font_face_css
|
|
from integrations import claude_usage, codex, homeassistant, kobo, nas, trackers, weather
|
|
|
|
TEMPLATES = Path(__file__).parent / "templates"
|
|
|
|
JOURS = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"]
|
|
MOIS = [
|
|
"janvier", "février", "mars", "avril", "mai", "juin",
|
|
"juillet", "août", "septembre", "octobre", "novembre", "décembre",
|
|
]
|
|
|
|
_env = Environment(
|
|
loader=FileSystemLoader(str(TEMPLATES)),
|
|
autoescape=select_autoescape(["html"]),
|
|
)
|
|
|
|
|
|
def _gauges(usage: claude_usage.ClaudeUsage) -> list[dict]:
|
|
"""Transforme l'usage Claude en jauges affichables (la fonctionnalité phare)."""
|
|
out: list[dict] = []
|
|
if usage.five_hour:
|
|
out.append({
|
|
"name": "Session (5 h)",
|
|
"remaining": usage.five_hour.remaining_pct,
|
|
"resets_in": usage.five_hour.resets_in_human,
|
|
"extra": "",
|
|
})
|
|
if usage.seven_day:
|
|
extra = ""
|
|
if usage.seven_day_opus and usage.seven_day_opus.utilization:
|
|
extra = f"Opus {usage.seven_day_opus.remaining_pct:.0f}% rest."
|
|
out.append({
|
|
"name": "Hebdo (7 j)",
|
|
"remaining": usage.seven_day.remaining_pct,
|
|
"resets_in": usage.seven_day.resets_in_human,
|
|
"extra": extra,
|
|
})
|
|
return out
|
|
|
|
|
|
async def build_context() -> dict:
|
|
"""Récupère toutes les sources en parallèle et assemble le contexte du template."""
|
|
usage, wx, ha, nas_status, codex_status, tracker_stats = await asyncio.gather(
|
|
claude_usage.fetch_usage(),
|
|
weather.fetch_weather(),
|
|
homeassistant.fetch_states(),
|
|
nas.fetch_status(),
|
|
codex.fetch_status(),
|
|
trackers.fetch_all(),
|
|
)
|
|
|
|
now = datetime.now(ZoneInfo(config.timezone))
|
|
return {
|
|
"width": config.width,
|
|
"height": config.height,
|
|
"fonts": font_face_css(),
|
|
"time": now.strftime("%H:%M"),
|
|
"dow": JOURS[now.weekday()],
|
|
"date": f"{now.day} {MOIS[now.month - 1]}",
|
|
"weather": wx,
|
|
"claude": usage,
|
|
"gauges": _gauges(usage),
|
|
"ha_states": ha,
|
|
"nas": nas_status,
|
|
"codex": codex_status,
|
|
"trackers": tracker_stats,
|
|
"kobo": kobo.current(),
|
|
"updated": now.strftime("%H:%M"),
|
|
"stale": False,
|
|
}
|
|
|
|
|
|
def render_html(context: dict) -> str:
|
|
return _env.get_template("dashboard.html").render(**context)
|
|
|
|
|
|
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)
|
|
|
|
async with async_playwright() as p:
|
|
browser = await p.chromium.launch(args=["--no-sandbox", "--disable-dev-shm-usage"])
|
|
page = await browser.new_page(
|
|
viewport={"width": config.width, "height": config.height},
|
|
device_scale_factor=1,
|
|
)
|
|
await page.set_content(html, wait_until="networkidle")
|
|
png_bytes = await page.screenshot(type="png", clip={
|
|
"x": 0, "y": 0, "width": config.width, "height": config.height,
|
|
})
|
|
await browser.close()
|
|
|
|
# Conversion niveaux de gris (mode 'L') -> e-ink friendly, fichier plus léger.
|
|
img = Image.open(io.BytesIO(png_bytes)).convert("L")
|
|
# 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
|
|
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())
|