Affichage paysage: canevas 1680x1264, rotation 90deg, layout 2 colonnes

This commit is contained in:
jerem
2026-06-15 14:33:05 +02:00
parent d75872c065
commit 56f71c0ea6
4 changed files with 112 additions and 83 deletions

View File

@@ -10,10 +10,12 @@ MONITORINK_CLAUDE_UA=claude-code/2.1.172
# Burn rate via ccusage (nécessite ccusage installé + ~/.claude/projects monté). 0/1
MONITORINK_CCUSAGE=0
# Affichage
# Affichage — canevas de rendu en PAYSAGE ; le PNG est pivoté de 90° pour la Kobo.
MONITORINK_TZ=Europe/Paris
MONITORINK_WIDTH=1264
MONITORINK_HEIGHT=1680
MONITORINK_WIDTH=1680
MONITORINK_HEIGHT=1264
# Sens de rotation : "cw" = Kobo posée bouton à droite, "ccw" = bouton à gauche.
MONITORINK_ROTATE=cw
MONITORINK_CACHE_TTL=120
# Météo (Open-Meteo, sans clé) — coordonnées

View File

@@ -44,8 +44,12 @@ class Config:
# --- Affichage ---
timezone: str = field(default_factory=lambda: _get("MONITORINK_TZ", "Europe/Paris"))
locale: str = field(default_factory=lambda: _get("MONITORINK_LOCALE", "fr_FR"))
width: int = field(default_factory=lambda: int(_get("MONITORINK_WIDTH", "1264")))
height: int = field(default_factory=lambda: int(_get("MONITORINK_HEIGHT", "1680")))
# Canevas de RENDU en paysage (1680x1264). Le PNG est ensuite pivoté de 90° dans
# render.py pour le panneau e-ink physiquement en portrait (1264x1680).
width: int = field(default_factory=lambda: int(_get("MONITORINK_WIDTH", "1680")))
height: int = field(default_factory=lambda: int(_get("MONITORINK_HEIGHT", "1264")))
# Sens de rotation pour l'affichage Kobo : "cw" (bouton à droite) ou "ccw".
rotate: str = field(default_factory=lambda: _get("MONITORINK_ROTATE", "cw").lower())
# --- Claude ---
# Chemin du fichier .credentials.json d'un login Claude ISOLÉ dédié à Monitorink

View File

@@ -1,4 +1,5 @@
"""Construit le contexte, rend le HTML puis le capture en PNG niveaux de gris (1264x1680)."""
"""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
@@ -98,6 +99,10 @@ async def render_png() -> bytes:
# 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
img = img.transpose(rota)
out = io.BytesIO()
img.save(out, format="PNG", optimize=True)
return out.getvalue()

View File

@@ -3,7 +3,8 @@
<head>
<meta charset="utf-8">
<style>
/* Dashboard e-ink 1264x1680 — noir & blanc, fort contraste, pas de dépendance couleur. */
/* Dashboard e-ink PAYSAGE 1680x1264 — noir & blanc, fort contraste, sans couleur.
Le PNG est pivoté de 90° côté backend pour le panneau portrait de la Kobo. */
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--ink: #000;
@@ -18,34 +19,49 @@
font-family: "DejaVu Sans", "Noto Sans", Arial, sans-serif;
-webkit-font-smoothing: none;
}
body { padding: 48px 56px; display: flex; flex-direction: column; }
/* Deux colonnes + pied de page pleine largeur. */
body {
padding: 44px 52px;
display: grid;
grid-template-columns: 600px 1fr;
grid-template-rows: 1fr auto;
grid-template-areas: "left right" "footer footer";
column-gap: 56px;
row-gap: 28px;
}
.col {
display: flex; flex-direction: column; min-width: 0;
}
.col-left { grid-area: left; }
.col-right { grid-area: right; padding-left: 56px; border-left: 5px solid var(--line); }
.section { margin-bottom: 40px; }
.rule { border: 0; border-top: 4px solid var(--line); margin: 0 0 28px; }
.section:last-child { margin-bottom: 0; }
.rule { border: 0; border-top: 4px solid var(--line); margin: 0 0 32px; }
/* En-tête : heure + date */
header { display: flex; justify-content: space-between; align-items: flex-end; }
.clock { font-size: 150px; font-weight: 800; line-height: 0.9; letter-spacing: -4px; }
.date { font-size: 40px; font-weight: 600; text-align: right; }
.date .dow { font-size: 52px; font-weight: 800; text-transform: capitalize; }
/* En-tête : heure au-dessus, date dessous (alignées à gauche). */
header { display: flex; flex-direction: column; gap: 10px; }
.clock { font-size: 210px; font-weight: 800; line-height: 0.82; letter-spacing: -6px; }
.date { font-size: 46px; font-weight: 600; }
.date .dow { font-size: 68px; font-weight: 800; text-transform: capitalize; }
/* Météo */
.weather { display: flex; align-items: center; gap: 36px; }
.weather .icon { font-size: 110px; line-height: 1; }
.weather .temp { font-size: 96px; font-weight: 800; }
.weather .meta { font-size: 36px; color: var(--muted); }
.weather .top { display: flex; align-items: center; gap: 32px; }
.weather .icon { font-size: 140px; line-height: 1; }
.weather .temp { font-size: 130px; font-weight: 800; line-height: 1; }
.weather .meta { font-size: 40px; color: var(--muted); margin-top: 18px; }
.weather .meta b { color: var(--ink); }
/* Titre de section */
.title { font-size: 34px; font-weight: 800; text-transform: uppercase;
letter-spacing: 3px; margin-bottom: 22px; }
.title { font-size: 36px; font-weight: 800; text-transform: uppercase;
letter-spacing: 3px; margin-bottom: 24px; }
/* Jauges Claude */
.gauge { margin-bottom: 34px; }
.gauge { margin-bottom: 36px; }
.gauge .row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 12px; }
.gauge .name { font-size: 40px; font-weight: 700; }
.gauge .pct { font-size: 64px; font-weight: 800; }
.gauge .name { font-size: 42px; font-weight: 700; }
.gauge .pct { font-size: 66px; font-weight: 800; }
.gauge .pct small { font-size: 32px; font-weight: 600; }
.bar { position: relative; height: 46px; background: var(--gauge-bg);
.bar { position: relative; height: 50px; background: var(--gauge-bg);
border: 4px solid var(--ink); border-radius: 6px; overflow: hidden; }
.bar .fill { position: absolute; top: 0; left: 0; bottom: 0; background: var(--ink); }
.bar.low .fill { background: repeating-linear-gradient(45deg, #000 0 14px, #fff 14px 22px); }
@@ -53,47 +69,49 @@
.err { font-size: 40px; font-weight: 700; padding: 24px; border: 4px dashed var(--ink); }
/* Grille Home Assistant */
.ha-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px 40px; }
.ha-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px 44px; }
.ha-item { display: flex; justify-content: space-between; align-items: baseline;
border-bottom: 3px solid var(--ink); padding-bottom: 12px; }
.ha-item .k { font-size: 38px; font-weight: 600; }
.ha-item .v { font-size: 44px; font-weight: 800; }
footer { margin-top: auto; display: flex; justify-content: space-between;
footer { grid-area: footer; display: flex; justify-content: space-between;
font-size: 28px; color: var(--muted); padding-top: 20px; border-top: 3px solid var(--ink); }
.stale { font-weight: 800; color: var(--ink); }
</style>
</head>
<body>
<!-- Colonne gauche : heure, date, météo -->
<div class="col col-left">
<header class="section">
<div class="clock">{{ time }}</div>
<div class="date">
<div class="dow">{{ dow }}</div>
<div>{{ date }}</div>
<span class="dow">{{ dow }}</span> · {{ date }}
</div>
</header>
<hr class="rule">
<div class="section weather">
{% if weather.ok %}
<div class="top">
<div class="icon">{{ weather.icon }}</div>
<div>
<div class="temp">{{ weather.temp | round | int }}°</div>
</div>
<div class="meta">
{{ weather.label }} · ressenti <b>{{ weather.feels_like | round | int }}°</b>
</div>
</div>
<div class="meta" style="margin-left:auto; text-align:right;">
<div>min <b>{{ weather.temp_min | round | int }}°</b> / max <b>{{ weather.temp_max | round | int }}°</b></div>
{% if weather.precip_prob is not none %}<div>pluie <b>{{ weather.precip_prob }}%</b></div>{% endif %}
<div class="meta">
min <b>{{ weather.temp_min | round | int }}°</b> / max <b>{{ weather.temp_max | round | int }}°</b>{% if weather.precip_prob is not none %} · pluie <b>{{ weather.precip_prob }}%</b>{% endif %}
</div>
{% else %}
<div class="meta">Météo indisponible</div>
{% endif %}
</div>
<hr class="rule">
</div>
<!-- Colonne droite : abonnement Claude + maison -->
<div class="col col-right">
<div class="section">
<div class="title">Abonnement Claude{% if claude.ok %} · Max 5x{% endif %}</div>
{% if not claude.ok %}
@@ -121,7 +139,6 @@
</div>
{% if ha_states %}
<hr class="rule">
<div class="section">
<div class="title">Maison</div>
<div class="ha-grid">
@@ -131,6 +148,7 @@
</div>
</div>
{% endif %}
</div>
<footer>
<span>Monitorink · 3 taps = redémarrer</span>