Backend Monitorink: serveur PNG (Claude usage + météo + HA)
This commit is contained in:
103
backend/render.py
Normal file
103
backend/render.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Construit le contexte, rend le HTML puis le capture en PNG niveaux de gris (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 integrations import claude_usage, homeassistant, 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 = await asyncio.gather(
|
||||
claude_usage.fetch_usage(),
|
||||
weather.fetch_weather(),
|
||||
homeassistant.fetch_states(),
|
||||
)
|
||||
|
||||
now = datetime.now(ZoneInfo(config.timezone))
|
||||
return {
|
||||
"width": config.width,
|
||||
"height": config.height,
|
||||
"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,
|
||||
"updated": now.strftime("%H:%M"),
|
||||
"stale": False,
|
||||
}
|
||||
|
||||
|
||||
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."""
|
||||
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")
|
||||
out = io.BytesIO()
|
||||
img.save(out, format="PNG", optimize=True)
|
||||
return out.getvalue()
|
||||
Reference in New Issue
Block a user