Affichage paysage: canevas 1680x1264, rotation 90deg, layout 2 colonnes
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,84 +69,86 @@
|
||||
.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>
|
||||
|
||||
<header class="section">
|
||||
<div class="clock">{{ time }}</div>
|
||||
<div class="date">
|
||||
<div class="dow">{{ dow }}</div>
|
||||
<div>{{ date }}</div>
|
||||
</div>
|
||||
</header>
|
||||
<hr class="rule">
|
||||
<!-- Colonne gauche : heure, date, météo -->
|
||||
<div class="col col-left">
|
||||
<header class="section">
|
||||
<div class="clock">{{ time }}</div>
|
||||
<div class="date">
|
||||
<span class="dow">{{ dow }}</span> · {{ date }}
|
||||
</div>
|
||||
</header>
|
||||
<hr class="rule">
|
||||
|
||||
<div class="section weather">
|
||||
{% if weather.ok %}
|
||||
<div class="icon">{{ weather.icon }}</div>
|
||||
<div>
|
||||
<div class="temp">{{ weather.temp | round | int }}°</div>
|
||||
<div class="section weather">
|
||||
{% if weather.ok %}
|
||||
<div class="top">
|
||||
<div class="icon">{{ weather.icon }}</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>
|
||||
{% else %}
|
||||
<div class="meta">Météo indisponible</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr class="rule">
|
||||
|
||||
<div class="section">
|
||||
<div class="title">Abonnement Claude{% if claude.ok %} · Max 5x{% endif %}</div>
|
||||
{% if not claude.ok %}
|
||||
<div class="err">⚠ {{ claude.error }}</div>
|
||||
{% else %}
|
||||
{% for g in gauges %}
|
||||
<div class="gauge">
|
||||
<div class="row">
|
||||
<span class="name">{{ g.name }}</span>
|
||||
<span class="pct">{{ g.remaining | round | int }}<small>% restant</small></span>
|
||||
<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>
|
||||
<div class="bar {% if g.remaining < 20 %}low{% endif %}">
|
||||
<div class="fill" style="width: {{ (100 - g.remaining) | round(1) }}%;"></div>
|
||||
</div>
|
||||
<div class="sub">{{ (100 - g.remaining) | round | int }}% utilisé · reset dans {{ g.resets_in }}{% if g.extra %} · {{ g.extra }}{% endif %}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if claude.extra %}
|
||||
<div class="sub" style="font-size:32px;">{{ claude.extra.label }}</div>
|
||||
{% else %}
|
||||
<div class="meta">Météo indisponible</div>
|
||||
{% endif %}
|
||||
{% if claude.burn_rate %}
|
||||
<div class="sub" style="font-size:32px;">Burn rate : {{ claude.burn_rate | round | int }} tok/min</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if ha_states %}
|
||||
<hr class="rule">
|
||||
<div class="section">
|
||||
<div class="title">Maison</div>
|
||||
<div class="ha-grid">
|
||||
{% for s in ha_states %}
|
||||
<div class="ha-item"><span class="k">{{ s.label }}</span><span class="v">{{ s.display }}</span></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 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 %}
|
||||
<div class="err">⚠ {{ claude.error }}</div>
|
||||
{% else %}
|
||||
{% for g in gauges %}
|
||||
<div class="gauge">
|
||||
<div class="row">
|
||||
<span class="name">{{ g.name }}</span>
|
||||
<span class="pct">{{ g.remaining | round | int }}<small>% restant</small></span>
|
||||
</div>
|
||||
<div class="bar {% if g.remaining < 20 %}low{% endif %}">
|
||||
<div class="fill" style="width: {{ (100 - g.remaining) | round(1) }}%;"></div>
|
||||
</div>
|
||||
<div class="sub">{{ (100 - g.remaining) | round | int }}% utilisé · reset dans {{ g.resets_in }}{% if g.extra %} · {{ g.extra }}{% endif %}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if claude.extra %}
|
||||
<div class="sub" style="font-size:32px;">{{ claude.extra.label }}</div>
|
||||
{% endif %}
|
||||
{% if claude.burn_rate %}
|
||||
<div class="sub" style="font-size:32px;">Burn rate : {{ claude.burn_rate | round | int }} tok/min</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if ha_states %}
|
||||
<div class="section">
|
||||
<div class="title">Maison</div>
|
||||
<div class="ha-grid">
|
||||
{% for s in ha_states %}
|
||||
<div class="ha-item"><span class="k">{{ s.label }}</span><span class="v">{{ s.display }}</span></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<span>Monitorink · 3 taps = redémarrer</span>
|
||||
|
||||
Reference in New Issue
Block a user