Files
harbour-aira/scripts/render_mockups.py
T
kaneda 4f279a18b0 0.5.0: riordino come l'app ufficiale (home, Dati, Impianto)
- home: temperatura esterna e in casa/assente in alto, messaggio di stato,
  scheda impianto con temperatura stanza e acqua calda; comandi sotto
- pagina Dati: calore prodotto, elettricita' usata, COP acqua calda e
  riscaldamento, risparmio, CO2 evitata, Smart Tariff Control (mese per mese)
- pagina Impianto: stato, termostato a parete, versioni software, errori
- bridge 0.4.0: rotta /stats (servizio statistiche del cloud) e blocco
  dispositivo in /summary (ID breve, versioni, termostato)
- C++: parseStats, formatNumber, formatMoney, monthLabel (separatore e
  valuta secondo la lingua); 61/61 verifiche del nucleo
- anteprime aggiornate (dati, impianto) in inglese e italiano
2026-10-10 08:59:03 +02:00

611 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Anteprima di layout delle schermate di harbour-aira.
NON sono screenshot: sono disegni generati con Pillow che riproducono
disposizione, gerarchia e misure delle tre schermate (dashboard, impostazioni,
copertina) usando i dati reali dell'impianto. Servono a validare il layout
prima di compilare: l'app vera usa i componenti Silica, non questi disegni.
python3 scripts/render_mockups.py -> docs/anteprima/*.png
"""
from __future__ import annotations
import pathlib
from PIL import Image, ImageDraw, ImageFont
W, H = 720, 1280
M = 72 # margine laterale (= Theme.horizontalPageMargin circa)
OUT = pathlib.Path("docs/anteprima")
# Palette Silica, ambience scura
BG_TOP = (10, 12, 16)
BG_BOT = (22, 28, 36)
PRIMARY = (240, 242, 245)
SECONDARY = (139, 147, 156)
ACCENT = (79, 195, 232)
TRACK = (56, 64, 74)
BTN_BG = (32, 39, 48)
BTN_EDGE = (98, 110, 124)
OK = (122, 214, 150)
FONTS = "/usr/share/fonts/truetype/dejavu/"
def font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont:
name = "DejaVuSans-Bold.ttf" if bold else "DejaVuSans.ttf"
return ImageFont.truetype(FONTS + name, size)
def background() -> Image.Image:
img = Image.new("RGB", (W, H), BG_TOP)
d = ImageDraw.Draw(img)
for y in range(H):
t = y / H
d.line([(0, y), (W, y)], fill=(
int(BG_TOP[0] + (BG_BOT[0] - BG_TOP[0]) * t),
int(BG_TOP[1] + (BG_BOT[1] - BG_TOP[1]) * t),
int(BG_TOP[2] + (BG_BOT[2] - BG_TOP[2]) * t),
))
return img
def status_bar(d: ImageDraw.ImageDraw) -> None:
d.text((M, 26), "21:20", font=font(22), fill=SECONDARY)
# indicazioni di sistema (segno grafico, non testo reale)
x = W - M - 96
d.rounded_rectangle([x, 30, x + 44, 48], radius=5, outline=SECONDARY, width=2)
d.rounded_rectangle([x + 4, 34, x + 30, 44], radius=2, fill=SECONDARY)
d.line([(x + 22, 16), (x + 30, 16)], fill=SECONDARY, width=2)
d.line([(x + 8, 14), (x + 20, 14)], fill=SECONDARY, width=2)
d.line([(x + 38, 16), (x + 48, 16)], fill=SECONDARY, width=2)
for i in range(4):
d.line([(x + 62 + i * 9, 42 - i * 5), (x + 66 + i * 9, 42 - i * 5)],
fill=SECONDARY, width=3)
def page_header(d: ImageDraw.ImageDraw, title: str) -> None:
d.text((M, 84), title, font=font(44, bold=True), fill=PRIMARY)
def section_header(d: ImageDraw.ImageDraw, y: int, title: str) -> int:
d.text((M, y), title, font=font(30), fill=ACCENT)
return y + 52
def label_pair(d: ImageDraw.ImageDraw, y: int, left: str, right: str,
right_color=PRIMARY) -> int:
d.text((M, y), left, font=font(26), fill=SECONDARY)
rw = d.textlength(right, font=font(26))
d.text((W - M - rw, y), right, font=font(26), fill=right_color)
return y + 44
def progress(d: ImageDraw.ImageDraw, y: int, frac: float) -> None:
d.rounded_rectangle([M, y, W - M, y + 3], radius=2, fill=TRACK)
end = M + int((W - 2 * M) * frac)
d.rounded_rectangle([M, y, end, y + 3], radius=2, fill=ACCENT)
def slider(d: ImageDraw.ImageDraw, y: int, frac: float, value_text: str,
label: str = "Obiettivo ACS") -> int:
d.text((M, y), label, font=font(24), fill=SECONDARY)
vw = d.textlength(value_text, font=font(24))
d.text((W - M - vw, y), value_text, font=font(24), fill=PRIMARY)
ty = y + 58
d.rounded_rectangle([M, ty, W - M, ty + 3], radius=2, fill=TRACK)
end = M + int((W - 2 * M) * frac)
d.rounded_rectangle([M, ty, end, ty + 3], radius=2, fill=ACCENT)
d.ellipse([end - 9, ty - 7, end + 9, ty + 11], fill=PRIMARY)
return ty + 44
def button(d: ImageDraw.ImageDraw, y: int, text: str, primary: bool = False) -> int:
h = 66
d.rounded_rectangle([M, y, W - M, y + h], radius=h // 2,
fill=BTN_BG, outline=ACCENT if primary else BTN_EDGE, width=2)
tw = d.textlength(text, font=font(26, bold=primary))
d.text(((W - tw) / 2, y + (h - 32) / 2), text, font=font(26, bold=primary),
fill=ACCENT if primary else PRIMARY)
return y + h + 14
def wrap(d: ImageDraw.ImageDraw, text: str, fnt, max_width: float) -> list:
"""Spezza il testo in righe che stanno nella larghezza data (come il QML,
che manda a capo le etichette lunghe invece di tagliarle)."""
lines, cur = [], ""
for word in text.split():
prova = (cur + " " + word).strip()
if not cur or d.textlength(prova, font=fnt) <= max_width:
cur = prova
else:
lines.append(cur)
cur = word
if cur:
lines.append(cur)
return lines
# --------------------------------------------------------------------------- #
# Testi delle anteprime. L'app e' in inglese (lingua sorgente) e italiano, quindi
# le anteprime esistono nelle due lingue: quelle di docs/anteprima/ sono in
# inglese, quelle di docs/anteprima/it/ in italiano.
TEXTS = {
"en": {
"app": "Aira",
"house_temperature": "House temperature",
"heating": "Heating",
"target": "target",
"new_house_target": "New house target",
"set_house": "Set house to {v} °C",
"dhw_section": "Domestic hot water",
"dhw_target": "DHW target",
"target_label": "Target",
"set_acs": "Set DHW target",
"start_boost": "Start DHW boost",
"stop_boost": "Stop DHW boost",
"plant": "Plant",
"outdoor": "Outdoor",
"indoor_pump": "Indoor (pump)",
"pull_to_refresh": "Pull down to refresh",
"settings": "Settings",
"language": "Language",
"system": "System",
"bridge": "Bridge",
"address_label": "Bridge address",
"token_label": "Bridge token",
"settings_note": "The address and the token stay in the local\n"
"configuration of the app, read-only for others.",
"test_connection": "Test connection",
"connection_ok": "Bridge reachable",
"save": "Save",
"cover_target": "target",
"cover_acs": "DHW",
"cover_outdoor": "outdoor",
"cover_footer": "house temperature",
"other_commands": "Other commands",
"current_state": "Current state",
"away": "Away mode",
"night_mode": "Night mode",
"forced_heating": "Forced heating",
"hot_water_active": "Hot water active",
"pump": "Pump",
"stopped": "Stopped",
"allowed_modes": "Allowed modes",
"on": "yes",
"off": "no",
"turn_away_on": "Turn away mode on",
"turn_away_off": "Turn away mode off",
"noise": "Noise",
"night_one_hour": "Night mode for one hour",
"force_heating": "Force heating",
"confirm_hint": "(asks for confirmation first)",
# --- riordino come l'app ufficiale (v0.5.0) ------------------------- #
"outside": "Outside {v}",
"at_home": "At home",
"status_relax": "Relax: the heat pump is running smoothly.",
"status_detail": "Automatic · last reading 21:18",
"device_name": "Aira heat pump",
"device_link": "Connected through the cloud",
"room_temperature": "Room temperature",
"target_value": "Target {v}",
"dhw_row": "Domestic hot water",
"heating_house": "Heating the house",
"setpoint_line": "Heating setpoint 19.0 °C · Heating",
"details": "Details",
"plant_accessories": "Plant and accessories",
"consumption": "Consumption and savings",
"data_title": "Data",
"data_note": "Numbers calculated by Aira in the cloud:\nthe app only reads them from your own bridge.",
"month": "Month of October 2026",
"heat_produced": "Heat produced",
"heat_sub": "What the heat pump gave the house",
"electricity": "Electricity used",
"electricity_sub": "What the heat pump took from the grid",
"eff_dhw": "Efficiency, domestic hot water",
"eff_heat": "Efficiency, heating",
"savings_title": "Your savings, last 30 days",
"saved_bill": "Saved on the bill",
"saved_sub": "Compared with a gas boiler",
"co2": "CO₂ avoided",
"smart": "Smart Tariff Control",
"no_data": "no data",
"status": "Status",
"connection": "Connection",
"cloud": "Cloud",
"mode": "Mode",
"automatic": "Automatic",
"accessories": "Accessories",
"thermostat": "Aira wall thermostat",
"sensors": "Temperature 22.8 °C · Humidity 73.3 % · Battery ok",
"versions": "Versions",
"device_id": "Device ID",
"platform": "Platform",
"connectivity": "Connectivity",
"climate_control": "Climate control",
"outdoor_unit": "Outdoor unit",
"errors": "Errors",
"no_errors": "No errors reported by the plant.",
},
"it": {
"app": "Aira",
"house_temperature": "Temperatura in casa",
"heating": "Riscaldamento",
"target": "obiettivo",
"new_house_target": "Nuovo obiettivo casa",
"set_house": "Imposta casa a {v} °C",
"dhw_section": "Acqua calda sanitaria",
"dhw_target": "Obiettivo acqua calda",
"target_label": "Obiettivo ACS",
"set_acs": "Imposta obiettivo ACS",
"start_boost": "Avvia boost ACS",
"stop_boost": "Ferma boost ACS",
"plant": "Impianto",
"outdoor": "Esterna",
"indoor_pump": "Interna (pompa)",
"pull_to_refresh": "Trascina verso il basso per aggiornare",
"settings": "Impostazioni",
"language": "Lingua",
"system": "Sistema",
"bridge": "Bridge",
"address_label": "Indirizzo del bridge",
"token_label": "Token del bridge",
"settings_note": "Indirizzo e token restano nella configurazione\n"
"locale dell'app, in sola lettura per gli altri.",
"test_connection": "Prova connessione",
"connection_ok": "Bridge raggiungibile",
"save": "Salva",
"cover_target": "obiettivo",
"cover_acs": "ACS",
"cover_outdoor": "esterna",
"cover_footer": "temperatura in casa",
"other_commands": "Altri comandi",
"current_state": "Stato attuale",
"away": "Assenza",
"night_mode": "Night mode",
"forced_heating": "Riscaldamento forzato",
"hot_water_active": "Acqua calda attiva",
"pump": "Pompa",
"stopped": "Ferma",
"allowed_modes": "Modalità consentite",
"on": "sì",
"off": "no",
"turn_away_on": "Attiva assenza",
"turn_away_off": "Disattiva assenza",
"noise": "Rumore",
"night_one_hour": "Night mode per un'ora",
"force_heating": "Forza riscaldamento",
"confirm_hint": "(chiede conferma prima di partire)",
# --- riordino come l'app ufficiale (v0.5.0) ------------------------- #
"outside": "Esterno {v}",
"at_home": "In casa",
"status_relax": "Relax: la pompa di calore funziona regolarmente.",
"status_detail": "Automatico · ultima lettura 21:18",
"device_name": "Aira Pompa di calore",
"device_link": "Connessa tramite Cloud",
"room_temperature": "Temperatura stanza",
"target_value": "Obiettivo {v}",
"dhw_row": "Acqua calda",
"heating_house": "Riscaldamento della casa",
"setpoint_line": "Obiettivo riscaldamento 19,0 °C · Riscaldamento",
"details": "Dettagli",
"plant_accessories": "Impianto e accessori",
"consumption": "Consumi e risparmi",
"data_title": "Dati",
"data_note": "Numeri calcolati da Aira nel cloud:\nl'app li legge soltanto dal tuo bridge.",
"month": "Mese di ottobre 2026",
"heat_produced": "Calore prodotto",
"heat_sub": "Quanto la pompa ha dato alla casa",
"electricity": "Elettricità usata",
"electricity_sub": "Quanta ne ha presa dalla rete",
"eff_dhw": "Efficienza, acqua calda",
"eff_heat": "Efficienza, riscaldamento",
"savings_title": "I tuoi risparmi, ultimi 30 giorni",
"saved_bill": "Risparmiati in bolletta",
"saved_sub": "Rispetto a una caldaia a gas",
"co2": "CO₂ evitata",
"smart": "Smart Tariff Control",
"no_data": "nessun dato",
"status": "Stato",
"connection": "Connessione",
"cloud": "Cloud",
"mode": "Modalità",
"automatic": "Automatico",
"accessories": "Accessori",
"thermostat": "Termostato a parete Aira",
"sensors": "Temperatura 22,8 °C · Umidità 73,3 % · Batteria ok",
"versions": "Versioni",
"device_id": "ID dispositivo",
"platform": "Piattaforma",
"connectivity": "Connettività",
"climate_control": "Controllo climatico",
"outdoor_unit": "Unità esterna",
"errors": "Errori",
"no_errors": "Nessun errore segnalato dall'impianto.",
},
}
LANG = "en"
def T(key: str) -> str:
return TEXTS[LANG][key]
def dec(value: float) -> str:
"""Numero a un decimale con il separatore della lingua, come fa l'app."""
out = f"{value:.1f}"
return out.replace(".", ",") if LANG == "it" else out
# --------------------------------------------------------------------------- #
def num(value: float, n: int = 1) -> str:
"""Numero con n decimali e il separatore della lingua, come fa l'app."""
out = f"{value:.{n}f}"
return out.replace(".", ",") if LANG == "it" else out
def card(d: ImageDraw.ImageDraw, y: int, h: int) -> None:
"""Scheda dello sfondo (BackgroundItem dell'app): angoli tondi, bordo tenue."""
d.rounded_rectangle([M, y, W - M, y + h], radius=18, fill=(26, 32, 41),
outline=BTN_EDGE, width=2)
def value_row(d: ImageDraw.ImageDraw, y: int, title: str, subtitle: str,
value: str) -> int:
"""Titolo e sottotitolo a sinistra, valore grande a destra (componente
ValueRow dell'app: e' la forma delle righe della scheda dell'impianto)."""
d.text((M + 28, y), title, font=font(26), fill=PRIMARY)
d.text((M + 28, y + 36), subtitle, font=font(22), fill=SECONDARY)
vw = d.textlength(value, font=font(42, bold=True))
d.text((W - M - 28 - vw, y + 6), value, font=font(42, bold=True), fill=PRIMARY)
return y + 96
# --------------------------------------------------------------------------- #
def main_screen() -> Image.Image:
"""Schermata iniziale, ordinata come quella dell'app ufficiale: temperatura
esterna in alto, messaggio di stato, scheda dell'impianto, poi i comandi."""
img = background()
d = ImageDraw.Draw(img)
status_bar(d)
page_header(d, T("app"))
y = 150
# ---- barra in alto: fuori casa / a casa ------------------------------ #
d.text((M, y), T("outside").format(v=dec(18.0) + " °C"), font=font(24), fill=SECONDARY)
casa = T("at_home")
d.text((W - M - d.textlength(casa, font=font(22)), y + 2), casa,
font=font(22), fill=SECONDARY)
y += 58
# ---- messaggio di stato ---------------------------------------------- #
fnt = font(30)
righe = wrap(d, T("status_relax"), fnt, W - 2 * M - 20)
for i, riga in enumerate(righe):
d.text(((W - d.textlength(riga, font=fnt)) / 2, y + i * 40), riga,
font=fnt, fill=PRIMARY)
y += 40 * len(righe) + 6
det = T("status_detail")
d.text(((W - d.textlength(det, font=font(22))) / 2, y), det, font=font(22), fill=SECONDARY)
y += 46
# ---- scheda dell'impianto -------------------------------------------- #
card(d, y, 282)
d.text((M + 28, y + 22), T("device_name"), font=font(26), fill=PRIMARY)
d.text((M + 28, y + 58), T("device_link"), font=font(22), fill=SECONDARY)
d.text((W - M - 46, y + 24), "›", font=font(42), fill=ACCENT)
value_row(d, y + 100, T("room_temperature"),
T("target_value").format(v=dec(19.0) + " °C"), dec(22.8) + " °C")
value_row(d, y + 186, T("dhw_row"),
T("target_value").format(v=dec(55.0) + " °C"), dec(51.0) + " °C")
y += 282 + 26
# ---- riscaldamento della casa ---------------------------------------- #
y = section_header(d, y, T("heating_house"))
line = T("setpoint_line")
d.text(((W - d.textlength(line, font=font(22))) / 2, y - 8), line,
font=font(22), fill=SECONDARY)
y = slider(d, y + 28, (19.0 - 5) / (30 - 5), dec(19.0) + " °C", T("new_house_target"))
y = button(d, y, T("set_house").format(v=dec(19.0)), primary=True)
# ---- acqua calda sanitaria ------------------------------------------- #
y = section_header(d, y + 6, T("dhw_section"))
progress(d, y, (51.0 - 20) / (65 - 20))
y += 22
y = slider(d, y + 4, (55.0 - 45) / (60 - 45), "55 °C", T("target_label"))
y = button(d, y, T("set_acs"), primary=True)
y = button(d, y, T("start_boost"))
y = button(d, y, T("stop_boost"))
# ---- pagine collegate (scorrono sotto) ------------------------------- #
y = section_header(d, y + 6, T("details"))
y = button(d, y, T("plant_accessories"))
y = button(d, y, T("consumption"))
return img
def settings_screen() -> Image.Image:
img = background()
d = ImageDraw.Draw(img)
status_bar(d)
page_header(d, T("settings"))
# ---- lingua ----------------------------------------------------------- #
y = section_header(d, 156, T("language"))
y = label_pair(d, y, T("language"), T("system"))
y += 16
# ---- bridge ----------------------------------------------------------- #
y = section_header(d, y, T("bridge"))
for lab, val in ((T("address_label"), "192.168.3.21:8790"),
(T("token_label"), "••••••••••••••••••••••••••••")):
d.text((M, y), lab, font=font(24), fill=SECONDARY)
y += 36
d.text((M, y), val, font=font(30), fill=PRIMARY)
y += 52
d.line([(M, y), (W - M, y)], fill=BTN_EDGE, width=2)
y += 44
d.multiline_text((M, y), T("settings_note"), font=font(22), fill=SECONDARY, spacing=10)
y += 104
y = button(d, y, T("test_connection"), primary=True)
d.text((M, y), T("connection_ok"), font=font(24), fill=OK)
y += 58
y = button(d, y, T("save"))
return img
def cover_screen() -> Image.Image:
img = Image.new("RGB", (W, H), (16, 20, 26))
d = ImageDraw.Draw(img)
# griglia di app sullo sfondo, per collocare la copertina
for row in range(5):
for col in range(4):
cx = 108 + col * 168
cy = 150 + row * 180
d.rounded_rectangle([cx - 52, cy - 52, cx + 52, cy + 52], radius=22,
fill=(30, 36, 45))
# copertina
card = [96, 430, W - 96, 430 + 470]
d.rounded_rectangle(card, radius=34, fill=(14, 18, 24), outline=ACCENT, width=2)
d.text((card[0] + 34, card[1] + 30), T("app"), font=font(40, bold=True), fill=PRIMARY)
d.line([(card[0] + 34, card[1] + 100), (card[2] - 34, card[1] + 100)],
fill=BTN_EDGE, width=2)
y = card[1] + 132
big = dec(23.0) + " °C"
d.text(((W - d.textlength(big, font=font(76, bold=True))) / 2, y),
big, font=font(76, bold=True), fill=PRIMARY)
y += 108
for lab, val in ((T("cover_target"), dec(19.0) + " °C"),
(T("cover_acs"), dec(51.0) + " °C"),
(T("cover_outdoor"), dec(18.0) + " °C")):
d.text((card[0] + 34, y), lab, font=font(24), fill=SECONDARY)
vw = d.textlength(val, font=font(24))
d.text((card[2] - 34 - vw, y), val, font=font(24), fill=PRIMARY)
y += 52
d.text((card[0] + 34, card[3] - 60), T("cover_footer"), font=font(22), fill=SECONDARY)
return img
def commands_screen() -> Image.Image:
img = background()
d = ImageDraw.Draw(img)
status_bar(d)
page_header(d, T("other_commands"))
y = section_header(d, 156, T("current_state"))
for lab, val in ((T("away"), T("off")), (T("night_mode"), T("off")),
(T("forced_heating"), T("off")), (T("hot_water_active"), T("on")),
(T("pump"), T("stopped")), (T("allowed_modes"), T("heating"))):
y = label_pair(d, y, lab, val)
y = section_header(d, y + 24, T("away"))
y = button(d, y, T("turn_away_on"))
y = button(d, y, T("turn_away_off"))
y = section_header(d, y + 24, T("noise"))
y = button(d, y, T("night_one_hour"))
y = section_header(d, y + 24, T("heating"))
y = button(d, y, T("force_heating"), primary=True)
hint = T("confirm_hint")
d.text(((W - d.textlength(hint, font=font(22))) / 2, y - 4), hint, font=font(22), fill=SECONDARY)
return img
def data_screen() -> Image.Image:
"""Pagina "Dati": gli stessi numeri della scheda Dati dell'app ufficiale."""
img = background()
d = ImageDraw.Draw(img)
status_bar(d)
page_header(d, T("data_title"))
y = 150
d.multiline_text((M, y), T("data_note"), font=font(22), fill=SECONDARY, spacing=8)
y += 96
# ---- energia e efficienza del mese ------------------------------------ #
y = section_header(d, y, T("month"))
d.text((M, y), T("heat_produced"), font=font(26), fill=PRIMARY)
d.text((M, y + 34), T("heat_sub"), font=font(22), fill=SECONDARY)
v = dec(70.8) + " kWh"
d.text((W - M - d.textlength(v, font=font(40, bold=True)), y + 4), v,
font=font(40, bold=True), fill=PRIMARY)
y += 96
d.text((M, y), T("electricity"), font=font(26), fill=PRIMARY)
d.text((M, y + 34), T("electricity_sub"), font=font(22), fill=SECONDARY)
v = dec(29.3) + " kWh"
d.text((W - M - d.textlength(v, font=font(40, bold=True)), y + 4), v,
font=font(40, bold=True), fill=PRIMARY)
y += 96
y = label_pair(d, y, T("eff_dhw"), num(4.42, 2))
y = label_pair(d, y, T("eff_heat"), T("no_data"))
# ---- risparmi ---------------------------------------------------------- #
y = section_header(d, y + 20, T("savings_title"))
d.text((M, y), T("saved_bill"), font=font(26), fill=PRIMARY)
d.text((M, y + 34), T("saved_sub"), font=font(22), fill=SECONDARY)
v = num(21.78, 2) + " €"
d.text((W - M - d.textlength(v, font=font(40, bold=True)), y + 4), v,
font=font(40, bold=True), fill=ACCENT)
y += 96
y = label_pair(d, y, T("co2"), dec(38.1) + " kg")
y = label_pair(d, y, T("smart"), num(2.71, 2) + " €", right_color=ACCENT)
hint = T("pull_to_refresh")
d.text(((W - d.textlength(hint, font=font(22))) / 2, H - 44),
hint, font=font(22), fill=SECONDARY)
return img
def plant_screen() -> Image.Image:
"""Pagina "Impianto": stato, accessori, versioni, errori."""
img = background()
d = ImageDraw.Draw(img)
status_bar(d)
page_header(d, T("plant"))
y = 150
y = section_header(d, y, T("status"))
for lab, val in ((T("connection"), T("cloud")), (T("mode"), T("automatic")),
(T("away"), T("off")), (T("night_mode"), T("off")),
(T("forced_heating"), T("off"))):
y = label_pair(d, y, lab, val)
y = section_header(d, y + 20, T("accessories"))
d.text((M, y), T("thermostat"), font=font(26), fill=PRIMARY)
y += 36
d.text((M, y), T("sensors"), font=font(22), fill=SECONDARY)
y += 56
y = section_header(d, y, T("versions"))
for lab, val in ((T("device_id"), "1f4b26"), (T("platform"), "6.10.0"),
(T("connectivity"), "3.30.1"), (T("climate_control"), "3.9.12"),
(T("outdoor_unit"), "1.30.0")):
y = label_pair(d, y, lab, val)
y = section_header(d, y + 20, T("errors"))
d.text((M, y), T("no_errors"), font=font(24), fill=OK)
return img
if __name__ == "__main__":
import sys
lang = "en"
if "--lang" in sys.argv:
lang = sys.argv[sys.argv.index("--lang") + 1]
if lang not in TEXTS:
raise SystemExit(f"lingua non disponibile: {lang}")
LANG = lang
out = pathlib.Path("docs/anteprima") if lang == "en" else pathlib.Path("docs/anteprima") / lang
out.mkdir(parents=True, exist_ok=True)
for name, fn in (("dashboard", main_screen), ("dati", data_screen),
("impianto", plant_screen), ("comandi", commands_screen),
("impostazioni", settings_screen), ("copertina", cover_screen)):
path = out / f"{name}.png"
fn().save(path)
print(f"scritto {path} ({path.stat().st_size} byte)")