0.6.0 blocco 3: copertina, versione, documenti e anteprime nelle due lingue

This commit is contained in:
2026-10-10 09:52:32 +02:00
parent 85702880d5
commit 36033daf3f
18 changed files with 385 additions and 123 deletions
+225 -72
View File
@@ -327,6 +327,92 @@ TEXTS = {
LANG = "en"
# --------------------------------------------------------------------------- #
# Testi aggiunti nella 0.6.0 (Home di consultazione, pagina Regolazioni,
# Comandi con interruttori). Le due lingue in un colpo solo: le anteprime
# restano bilingui come l'app.
TEXTS["en"].update({
"outside": "Outside {v}",
"house_cell": "House",
"hot_water_cell": "Hot water",
"cell_target": "target {v}",
"status_fine": "Everything is fine",
"status_water": "Heating the hot water",
"updated_at": "Updated at 21:18",
"pull_hint": "Pull down: Aggiorna · Dati · Impianto · Impostazioni",
"tap_adjust": "Tap a value to adjust it",
"quick_actions": "Quick actions",
"boost": "Boost",
"away_btn": "Away mode",
"back_home": "Back home",
"commands": "Commands",
"contorno": "Outside {a} · Thermostat {b} · Zones 1",
"setpoints": "Setpoints",
"current_target": "Current target",
"keep_now": "What the plant is keeping now",
"water_now": "Water now {v}",
"hot_water_boost": "Hot water boost",
"set_house_v": "Set house to {v} °C",
"set_water_v": "Set target {v} °C",
"plant_commands": "Plant commands",
"modes": "Modes",
"sw_away_desc": "Lowers the house and stops the hot water",
"sw_forced_desc": "Heats at full power until you stop it",
"sw_heating_desc": "The heating can turn on",
"sw_water_desc": "The heat pump keeps the water in temperature",
"legionella": "Legionella cycle",
"switches_note": "The switches show the current state of the plant.",
"commands_note": "Commands go through the bridge and may take a few seconds.",
"diagnostics": "Diagnostics",
"plant_check": "Plant check",
"manual_mode": "Manual mode",
"defrost": "Defrost",
"signature_lights": "Signature lights",
"save_and_test": "Save and test",
"cover_outdoor": "outside",
})
TEXTS["it"].update({
"house_cell": "Casa",
"hot_water_cell": "Acqua calda",
"cell_target": "obiettivo {v}",
"status_fine": "Tutto regolare",
"status_water": "Sta scaldando l'acqua",
"updated_at": "Aggiornato alle 21:18",
"pull_hint": "Trascina in giù: Aggiorna · Dati · Impianto · Impostazioni",
"tap_adjust": "Tocca un valore per regolarlo",
"quick_actions": "Azioni rapide",
"boost": "Boost",
"away_btn": "Assenza",
"back_home": "Torna a casa",
"commands": "Comandi",
"contorno": "Esterno {a} · Termostato {b} · Zone 1",
"setpoints": "Regolazioni",
"current_target": "Obiettivo attuale",
"keep_now": "Quello che l'impianto mantiene adesso",
"water_now": "Acqua ora {v}",
"hot_water_boost": "Boost acqua calda",
"set_house_v": "Imposta casa a {v} °C",
"set_water_v": "Imposta obiettivo {v} °C",
"plant_commands": "Comandi impianto",
"modes": "Modalità",
"sw_away_desc": "Abbassa la casa e spegne l'acqua calda",
"sw_forced_desc": "Scalda a pieno regime finché non lo fermi",
"sw_heating_desc": "Il riscaldamento può accendersi",
"sw_water_desc": "Mantiene l'acqua in temperatura",
"legionella": "Ciclo legionella",
"switches_note": "Gli interruttori mostrano lo stato attuale dell'impianto.",
"commands_note": "I comandi passano dal bridge e possono richiedere qualche secondo.",
"diagnostics": "Diagnostica",
"plant_check": "Verifica impianto",
"manual_mode": "Modalità manuale",
"defrost": "Sbrinamento",
"signature_lights": "Luci di firma",
"save_and_test": "Salva e prova",
"cover_outdoor": "esterna",
"night_one_hour": "Modalità notte per un'ora",
})
def T(key: str) -> str:
return TEXTS[LANG][key]
@@ -363,65 +449,67 @@ def value_row(d: ImageDraw.ImageDraw, y: int, title: str, subtitle: str,
# --------------------------------------------------------------------------- #
def reading_cell(d: ImageDraw.ImageDraw, x: int, y: int, w: int, h: int,
title: str, value: str, subtitle: str,
frac: float = None) -> None:
"""Riquadro di una lettura grande (componente ReadingCell dell'app): titolo,
valore, riga sotto e barra di avanzamento facoltativa."""
d.rounded_rectangle([x, y, x + w, y + h], radius=18, fill=(26, 32, 41),
outline=BTN_EDGE, width=2)
d.text((x + 26, y + 22), title, font=font(24), fill=SECONDARY)
d.text((x + 26, y + 56), value, font=font(56, bold=True), fill=PRIMARY)
d.text((x + 26, y + 132), subtitle, font=font(20), fill=SECONDARY)
if frac is not None:
bx0, bx1 = x + 26, x + w - 26
by = y + h - 30
d.rounded_rectangle([bx0, by, bx1, by + 10], radius=5, fill=(46, 54, 66))
d.rounded_rectangle([bx0, by, bx0 + (bx1 - bx0) * max(0.0, min(1.0, frac)), by + 10],
radius=5, fill=ACCENT)
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."""
"""Home della 0.6.0: una schermata da guardare, non un modulo. Stato in una
riga, le due letture che contano, tre azioni rapide. Niente scorrimento."""
img = background()
d = ImageDraw.Draw(img)
status_bar(d)
page_header(d, T("app"))
d.text((M, 128), T("updated_at"), font=font(22), fill=SECONDARY)
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,
y = 176
# ---- stato in una riga ------------------------------------------------ #
d.ellipse([M, y + 7, M + 18, y + 25], fill=OK)
d.text((M + 34, y), T("status_water"), font=font(26), fill=PRIMARY)
y += 54
# ---- le due letture che contano --------------------------------------- #
gap = 20
cw = (W - 2 * M - gap) // 2
ch = 200
reading_cell(d, M, y, cw, ch, T("house_cell"), dec(23.0) + " °C",
T("cell_target").format(v=dec(19.0) + " °C"))
reading_cell(d, M + cw + gap, y, cw, ch, T("hot_water_cell"), dec(50.6) + " °C",
T("cell_target").format(v=dec(55.0) + " °C"),
(50.6 - 10) / (55.0 - 10))
y += ch + 18
d.text((M, y), T("tap_adjust"), font=font(22), fill=SECONDARY)
y += 54
# ---- azioni rapide ----------------------------------------------------- #
y = section_header(d, y, T("quick_actions"))
bw = (W - 2 * M - 2 * gap) // 3
for i, testo in enumerate((T("boost"), T("away_btn"), T("commands"))):
x = M + i * (bw + gap)
d.rounded_rectangle([x, y, x + bw, y + 74], radius=18, fill=BTN_BG,
outline=BTN_EDGE, width=2)
tw = d.textlength(testo, font=font(24))
d.text((x + (bw - tw) / 2, y + 23), testo, font=font(24), fill=PRIMARY)
y += 104
# ---- letture di contorno ----------------------------------------------- #
d.text((M, y), T("contorno").format(a=dec(17.8) + " °C", b=dec(22.8) + " °C"),
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
@@ -449,10 +537,9 @@ def settings_screen() -> Image.Image:
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)
# Un pulsante solo: salvare e provare erano due gesti per la stessa cosa.
y = button(d, y, T("save_and_test"), primary=True)
d.text((M, y), T("connection_ok"), font=font(24), fill=OK)
y += 58
y = button(d, y, T("save"))
return img
@@ -488,32 +575,87 @@ def cover_screen() -> Image.Image:
return img
def switch_row(d: ImageDraw.ImageDraw, y: int, text: str, description: str,
checked: bool) -> int:
"""Riga con interruttore (componente TextSwitch dell'app): etichetta e
spiegazione a sinistra, interruttore a destra con lo stato dell'impianto."""
d.text((M, y), text, font=font(28), fill=PRIMARY)
d.text((M, y + 38), description, font=font(22), fill=SECONDARY)
pw, ph = 96, 44
x0 = W - M - pw
y0 = y + 8
d.rounded_rectangle([x0, y0, x0 + pw, y0 + ph], radius=ph / 2,
fill=ACCENT if checked else (48, 56, 68))
cx = x0 + pw - ph / 2 if checked else x0 + ph / 2
d.ellipse([cx - ph / 2 + 5, y0 + 5, cx + ph / 2 - 5, y0 + ph - 5],
fill=(238, 242, 246))
return y + 98
def commands_screen() -> Image.Image:
"""Comandi impianto della 0.6.0: gli interruttori dicono lo stato e si
cambiano in un tocco; i pulsanti restano per le azioni senza stato."""
img = background()
d = ImageDraw.Draw(img)
status_bar(d)
page_header(d, T("other_commands"))
page_header(d, T("plant_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, 150, T("modes"))
y = switch_row(d, y, T("away"), T("sw_away_desc"), False)
y = switch_row(d, y, T("forced_heating"), T("sw_forced_desc"), False)
y = switch_row(d, y, T("heating"), T("sw_heating_desc"), True)
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 + 10, T("dhw_section"))
y = switch_row(d, y, T("hot_water_active"), T("sw_water_desc"), True)
y = button(d, y, T("legionella"))
y = section_header(d, y + 24, T("noise"))
y = section_header(d, y + 10, 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)
d.text((M, y + 12), T("switches_note"), font=font(22), fill=SECONDARY)
return img
def setpoints_screen() -> Image.Image:
"""Regolazioni: un blocco per grandezza, con il valore attuale sempre
visibile, le preimpostazioni e la conferma."""
img = background()
d = ImageDraw.Draw(img)
status_bar(d)
page_header(d, T("setpoints"))
y = section_header(d, 150, T("heating_house"))
y = value_row(d, y, T("current_target"), T("keep_now"), dec(19.0) + " °C")
y = slider(d, y, (19.0 - 5) / (30 - 5), dec(19.0) + " °C", T("new_house_target"))
y = preset_row(d, y, ["16°", "17°", "18°", "19°", "20°", "21°"])
y = button(d, y, T("set_house_v").format(v=dec(19.0)), primary=True)
y = section_header(d, y + 6, T("dhw_section"))
y = value_row(d, y, T("dhw_target"), T("water_now").format(v=dec(50.6) + " °C"),
dec(55.0) + " °C")
y = slider(d, y, (55.0 - 45) / (60 - 45), dec(55.0) + " °C", T("target_label"))
y = preset_row(d, y, ["45°", "47°", "50°", "55°", "60°"])
y = button(d, y, T("set_water_v").format(v=dec(55.0)), primary=True)
y = section_header(d, y + 6, T("hot_water_boost"))
y = button(d, y, T("start_boost"))
y = button(d, y, T("stop_boost"))
return img
def preset_row(d: ImageDraw.ImageDraw, y: int, valori: list) -> int:
"""Fila di valori rapidi: un tocco invece del cursore, per le dita fredde."""
gap = 12
bw = (W - 2 * M - (len(valori) - 1) * gap) // len(valori)
for i, testo in enumerate(valori):
x = M + i * (bw + gap)
d.rounded_rectangle([x, y, x + bw, y + 62], radius=16, fill=BTN_BG,
outline=BTN_EDGE, width=2)
tw = d.textlength(testo, font=font(24))
d.text((x + (bw - tw) / 2, y + 17), testo, font=font(24), fill=PRIMARY)
return y + 86
def data_screen() -> Image.Image:
"""Pagina "Dati": gli stessi numeri della scheda Dati dell'app ufficiale."""
img = background()
@@ -566,11 +708,18 @@ def plant_screen() -> Image.Image:
status_bar(d)
page_header(d, T("plant"))
y = 150
# La frase estesa dell'app ufficiale: sta qui, non in Home.
righe = wrap(d, T("status_relax"), font(24), W - 2 * M)
for i, riga in enumerate(righe):
d.text((M, 132 + i * 32), riga, font=font(24), fill=PRIMARY)
y = 150 + 32 * len(righe)
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"))):
(T("forced_heating"), T("off")), (T("manual_mode"), T("off")),
(T("defrost"), T("off")), (T("signature_lights"), T("on")),
(T("pump"), T("stopped")), (T("allowed_modes"), T("heating"))):
y = label_pair(d, y, lab, val)
y = section_header(d, y + 20, T("accessories"))
@@ -587,6 +736,9 @@ def plant_screen() -> Image.Image:
y = section_header(d, y + 20, T("errors"))
d.text((M, y), T("no_errors"), font=font(24), fill=OK)
y = section_header(d, y + 40, T("diagnostics"))
y = button(d, y, T("plant_check"))
return img
@@ -602,7 +754,8 @@ if __name__ == "__main__":
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),
for name, fn in (("dashboard", main_screen), ("regolazioni", setpoints_screen),
("dati", data_screen),
("impianto", plant_screen), ("comandi", commands_screen),
("impostazioni", settings_screen), ("copertina", cover_screen)):
path = out / f"{name}.png"