Misurato in container: la ricarica della radice ri-traduce, il catalogo si trova anche con it_IT. La ricarica ora scarica la radice prima di ricaricarla, cosi' non dipende da un comportamento di setSource che cambia fra versioni di Qt.
413 lines
17 KiB
QML
413 lines
17 KiB
QML
import QtQuick 2.2
|
|
import Sailfish.Silica 1.0
|
|
import "../components"
|
|
|
|
// Home: una schermata da GUARDARE, non un modulo da compilare.
|
|
//
|
|
// Stato in una riga, le due letture che contano (casa e acqua calda), tre
|
|
// azioni rapide. Le regolazioni stanno in SetpointsPage, gli altri comandi in
|
|
// CommandsPage, i numeri in DataPage, i dettagli in PlantPage: qui solo quello
|
|
// che si guarda molte volte al giorno, senza scorrere (docs/UX-STUDIO.md).
|
|
Page {
|
|
id: page
|
|
|
|
allowedOrientations: Orientation.All
|
|
|
|
function raw(key) {
|
|
return api.summary[key]
|
|
}
|
|
|
|
// I numeri si formattano nel C++ (ApiClient::formatValue): cosi' il
|
|
// separatore decimale segue la lingua dell'app, non il toFixed di JS.
|
|
function fmt(value) { return api.formatValue(value) }
|
|
|
|
function count(key) {
|
|
var v = page.raw(key)
|
|
if (v === undefined || v === null)
|
|
return "—"
|
|
return String(Math.round(Number(v)))
|
|
}
|
|
|
|
// Colore del pallino di stato. Il livello (0 regolare, 1 modo attivo,
|
|
// 2 avviso, 3 allarme serio) lo calcola il C++; qui si sceglie il colore,
|
|
// e l'ambra quando i numeri sono vecchi: un dato vecchio non e' un dato
|
|
// buono. Rosso solo quando e' la macchina a essere ferma o guasta: un
|
|
// avviso (per esempio il compressore limitato dalla temperatura esterna)
|
|
// e' ambra, come nell'app ufficiale.
|
|
function statusColor() {
|
|
if (!api.hasData || api.stale)
|
|
return "orange"
|
|
var livello = api.statusLevel()
|
|
if (livello === 3)
|
|
return Theme.errorColor
|
|
if (livello === 2)
|
|
return "#ffb300"
|
|
if (livello === 1)
|
|
return Theme.highlightColor
|
|
return "#4caf50"
|
|
}
|
|
|
|
// Riga sotto il titolo: dice anche quando i numeri sono vecchi.
|
|
function updateText() {
|
|
if (!api.hasData)
|
|
return qsTr("No data yet")
|
|
if (api.stale)
|
|
return qsTr("Not updated · last %1").arg(api.lastUpdate)
|
|
if (api.lastUpdate.length === 0)
|
|
return ""
|
|
return qsTr("Updated at %1").arg(api.lastUpdate)
|
|
}
|
|
|
|
function openSettings() {
|
|
pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
|
|
}
|
|
|
|
function openCommands() {
|
|
pageStack.push(Qt.resolvedUrl("CommandsPage.qml"))
|
|
}
|
|
|
|
function openPlant() {
|
|
pageStack.push(Qt.resolvedUrl("PlantPage.qml"))
|
|
}
|
|
|
|
function openData() {
|
|
pageStack.push(Qt.resolvedUrl("DataPage.qml"))
|
|
}
|
|
|
|
// UNA sola assegnazione: in Qt 5.6 assegnare Component.onCompleted due volte
|
|
// e' un errore di compilazione del file, e la pagina non si crea piu'
|
|
// ("Type MainPage unavailable" e app senza Home).
|
|
Component.onCompleted: {
|
|
if (api.configured)
|
|
api.refresh()
|
|
misura.start()
|
|
}
|
|
|
|
// Niente RemorseItem sui tasti rapidi: Silica se lo ri-parenta dentro la
|
|
// fila dei tre tasti e ci disegna sopra il suo banner (il riquadro del
|
|
// remorse non ha la larghezza del tasto), coprendo i tasti vicini. Il
|
|
// messaggio lo dice la NoticeBar in fondo: la stessa cosa, senza rompere
|
|
// la fila. Segnalato da Carlo: "il problema c'e' quando premo un pulsante".
|
|
|
|
SilicaFlickable {
|
|
id: flick
|
|
anchors.fill: parent
|
|
// Banda riservata al messaggio in fondo: cosi' non copre il contenuto.
|
|
anchors.bottomMargin: notice.banda
|
|
contentHeight: col.height + Theme.paddingLarge
|
|
|
|
// I menu pulley stanno DENTRO la vista: come figli diretti della Page
|
|
// fanno deref su null (TypeError + crash).
|
|
PullDownMenu {
|
|
MenuItem {
|
|
text: qsTr("Refresh")
|
|
onClicked: api.refresh()
|
|
}
|
|
MenuItem {
|
|
text: qsTr("Data")
|
|
onClicked: page.openData()
|
|
}
|
|
MenuItem {
|
|
text: qsTr("Plant")
|
|
onClicked: page.openPlant()
|
|
}
|
|
MenuItem {
|
|
text: qsTr("Settings")
|
|
onClicked: page.openSettings()
|
|
}
|
|
}
|
|
|
|
Column {
|
|
id: col
|
|
width: parent.width
|
|
spacing: Theme.paddingSmall
|
|
|
|
PageHeader {
|
|
title: "Aira"
|
|
description: page.updateText()
|
|
}
|
|
|
|
// ---- bridge non configurato ------------------------------------ //
|
|
Label {
|
|
visible: !api.configured
|
|
x: Theme.horizontalPageMargin
|
|
width: parent.width - 2 * Theme.horizontalPageMargin
|
|
text: qsTr("Bridge not configured. Open the settings and enter the server address and the token.")
|
|
color: Theme.secondaryColor
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
wrapMode: Text.Wrap
|
|
}
|
|
|
|
Button {
|
|
visible: !api.configured
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: qsTr("Settings")
|
|
onClicked: page.openSettings()
|
|
}
|
|
|
|
// ---- stato in una riga ----------------------------------------- //
|
|
Item {
|
|
visible: api.configured
|
|
x: Theme.horizontalPageMargin
|
|
width: parent.width - 2 * Theme.horizontalPageMargin
|
|
height: Math.max(stato.height, pallino.height, attesa.height)
|
|
|
|
Rectangle {
|
|
id: pallino
|
|
width: Theme.fontSizeSmall * 0.6
|
|
height: width
|
|
radius: width / 2
|
|
color: page.statusColor()
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Label {
|
|
id: stato
|
|
anchors {
|
|
left: pallino.right
|
|
leftMargin: Theme.paddingMedium
|
|
right: attesa.left
|
|
rightMargin: Theme.paddingMedium
|
|
verticalCenter: parent.verticalCenter
|
|
}
|
|
text: api.stateShort()
|
|
color: Theme.primaryColor
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
BusyIndicator {
|
|
id: attesa
|
|
anchors {
|
|
right: parent.right
|
|
verticalCenter: parent.verticalCenter
|
|
}
|
|
size: BusyIndicatorSize.Small
|
|
running: api.busy
|
|
visible: api.busy
|
|
}
|
|
}
|
|
|
|
// ---- le due letture che contano -------------------------------- //
|
|
Row {
|
|
id: letture
|
|
visible: api.configured
|
|
x: Theme.horizontalPageMargin
|
|
width: parent.width - 2 * Theme.horizontalPageMargin
|
|
spacing: Theme.paddingSmall
|
|
|
|
ReadingCell {
|
|
width: (letture.width - letture.spacing) / 2
|
|
title: qsTr("House")
|
|
value: page.fmt(page.raw("zone1_temperature")) + " °C"
|
|
subtitle: qsTr("target %1 °C").arg(page.fmt(page.raw("zone1_setpoint")))
|
|
valueColor: api.stale ? Theme.secondaryColor : Theme.primaryColor
|
|
onTapped: pageStack.push(Qt.resolvedUrl("SetpointsPage.qml"),
|
|
{ "section": "house" })
|
|
}
|
|
|
|
ReadingCell {
|
|
width: (letture.width - letture.spacing) / 2
|
|
title: qsTr("Hot water")
|
|
value: page.fmt(page.raw("dhw_current")) + " °C"
|
|
subtitle: qsTr("target %1 °C").arg(page.fmt(page.raw("dhw_target")))
|
|
valueColor: api.stale ? Theme.secondaryColor : Theme.primaryColor
|
|
onTapped: pageStack.push(Qt.resolvedUrl("SetpointsPage.qml"),
|
|
{ "section": "water" })
|
|
}
|
|
}
|
|
|
|
// Una sola riga, sempre della stessa altezza: mentre il boost corre
|
|
// il conto alla rovescia prende il posto del suggerimento senza
|
|
// spostare niente sotto. Il carattere e' quello del suggerimento
|
|
// (fontSizeExtraSmall): se fosse piu' grande, la riga piu' alta
|
|
// spingerebbe giu' i pulsanti e la riga di contorno finirebbe loro
|
|
// addosso (segnalato da Carlo).
|
|
Item {
|
|
visible: api.configured
|
|
width: parent.width
|
|
height: Math.max(contoRovescia.implicitHeight,
|
|
suggerimento.implicitHeight)
|
|
|
|
Label {
|
|
id: contoRovescia
|
|
visible: api.boostActive
|
|
x: Theme.horizontalPageMargin
|
|
width: parent.width - 2 * Theme.horizontalPageMargin
|
|
text: qsTr("Boost · %1").arg(api.boostCountdown)
|
|
color: Theme.highlightColor
|
|
font.pixelSize: Theme.fontSizeExtraSmall
|
|
font.bold: true
|
|
}
|
|
|
|
Label {
|
|
id: suggerimento
|
|
visible: !api.boostActive
|
|
x: Theme.horizontalPageMargin
|
|
width: parent.width - 2 * Theme.horizontalPageMargin
|
|
text: qsTr("Tap a value to adjust it")
|
|
color: Theme.secondaryColor
|
|
font.pixelSize: Theme.fontSizeExtraSmall
|
|
}
|
|
}
|
|
|
|
// ---- azioni rapide --------------------------------------------- //
|
|
SectionHeader {
|
|
visible: api.configured
|
|
text: qsTr("Quick actions")
|
|
}
|
|
|
|
Row {
|
|
visible: api.configured
|
|
x: Theme.horizontalPageMargin
|
|
width: parent.width - 2 * Theme.horizontalPageMargin
|
|
spacing: Theme.paddingSmall
|
|
|
|
Button {
|
|
id: boostBtn
|
|
width: (parent.width - 2 * parent.spacing) / 3
|
|
// In Silica il tasto piglia l'altezza dal tema
|
|
// (itemSizeExtraSmall ~56 px), ma icona E etichetta ne
|
|
// occupano ~110: senza questa riga l'etichetta esce dalla
|
|
// scatola e la riga successiva della colonna le finisce
|
|
// sopra (segnalato da Carlo: "la riga sotto copre i
|
|
// pulsanti"). Vedi tests/label_fit_check.py.
|
|
height: Theme.itemSizeMedium
|
|
// Mentre il boost e' in corso il tasto ferma invece di
|
|
// avviare: dice cosa succede premendolo, come il tasto
|
|
// dell'assenza. "Stop boost" per intero non ci sta accanto
|
|
// all'icona (92 px contro 73 disponibili): resta "Stop",
|
|
// che il glifo di stop rende inequivocabile.
|
|
text: api.boostActive ? qsTr("Stop") : qsTr("Boost")
|
|
// La casella dell'icona la decide il tema: senza, l'Image
|
|
// prende la misura del file e si mangia il testo.
|
|
icon.source: api.boostActive ? "../icons/quick-boost-stop.png"
|
|
: "../icons/quick-boost.png"
|
|
icon.width: Theme.iconSizeExtraSmall
|
|
icon.height: Theme.iconSizeExtraSmall
|
|
// Comando diretto: il messaggio lo mostra la NoticeBar in
|
|
// fondo, senza il banner del remorse sulla fila dei tasti.
|
|
onClicked: api.sendCommand(api.boostActive ? "hot_water_boost_off"
|
|
: "hot_water_boost_on")
|
|
}
|
|
|
|
Button {
|
|
id: assenzaBtn
|
|
width: (parent.width - 2 * parent.spacing) / 3
|
|
height: Theme.itemSizeMedium
|
|
// Il pulsante dice cosa succede premendolo, non lo stato.
|
|
// Etichette corte: nel tasto, largo un terzo di schermo,
|
|
// icona e testo devono starci insieme.
|
|
text: page.raw("away_mode") ? qsTr("Home") : qsTr("Away")
|
|
icon.source: "../icons/quick-away.png"
|
|
icon.width: Theme.iconSizeExtraSmall
|
|
icon.height: Theme.iconSizeExtraSmall
|
|
onClicked: {
|
|
if (page.raw("away_mode")) {
|
|
api.sendCommand("away_mode_off")
|
|
} else {
|
|
// L'assenza si imposta da un popup con le date
|
|
// (richiesta dell'utente): si apre qui e il comando
|
|
// parte solo se si accetta.
|
|
var scelta = pageStack.push(Qt.resolvedUrl("AwayDialog.qml"))
|
|
scelta.accepted.connect(function() {
|
|
api.sendCommand("away_mode_on", null, {
|
|
"start": Qt.formatDate(scelta.inizio, "yyyy-MM-dd"),
|
|
"end": Qt.formatDate(scelta.fine, "yyyy-MM-dd")
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
Button {
|
|
width: (parent.width - 2 * parent.spacing) / 3
|
|
height: Theme.itemSizeMedium
|
|
// "Comandi" (8 lettere) non ci sta accanto a un'icona in un
|
|
// terzo di schermo: la pagina resta "Comandi impianto".
|
|
text: qsTr("More")
|
|
icon.source: "../icons/quick-commands.png"
|
|
icon.width: Theme.iconSizeExtraSmall
|
|
icon.height: Theme.iconSizeExtraSmall
|
|
onClicked: page.openCommands()
|
|
}
|
|
}
|
|
|
|
// ---- letture di contorno (si toccano: portano a Impianto) ------ //
|
|
BackgroundItem {
|
|
visible: api.configured
|
|
x: Theme.horizontalPageMargin
|
|
width: parent.width - 2 * Theme.horizontalPageMargin
|
|
height: contornoTxt.height + Theme.paddingMedium
|
|
onClicked: page.openPlant()
|
|
|
|
Label {
|
|
id: contornoTxt
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
verticalCenter: parent.verticalCenter
|
|
}
|
|
text: qsTr("Outside %1 °C · Thermostat %2 °C · Zones %3")
|
|
.arg(page.fmt(page.raw("outdoor_temperature")))
|
|
.arg(page.fmt(page.raw("thermostat_temperature")))
|
|
.arg(page.count("zones"))
|
|
color: Theme.secondaryColor
|
|
font.pixelSize: Theme.fontSizeExtraSmall
|
|
wrapMode: Text.Wrap
|
|
}
|
|
}
|
|
|
|
Item { width: 1; height: Theme.paddingMedium }
|
|
}
|
|
}
|
|
|
|
NoticeBar { id: notice }
|
|
|
|
// La disposizione non e' definitiva subito: si misura un secondo dopo.
|
|
// Questa e' la riga da leggere alla prima build sul telefono: la Home deve
|
|
// stare in una schermata sola, se "contenuto" supera "schermo" c'e' ancora
|
|
// qualcosa da togliere (o da accettare, ma sapendolo).
|
|
Timer {
|
|
id: misura
|
|
interval: 1000
|
|
onTriggered: {
|
|
console.log("harbour-aira home: contenuto", flick.contentHeight,
|
|
"schermo", height)
|
|
// Ogni riga della Home con la sua posizione: se una riga finisce
|
|
// addosso a un'altra, qui si legge quale e di quanto. E' la riga da
|
|
// mandare al primo avvio se la disposizione non torna.
|
|
var righe = []
|
|
for (var i = 0; i < col.children.length; ++i) {
|
|
var r = col.children[i]
|
|
if (r.height === undefined)
|
|
continue
|
|
console.log("harbour-aira riga", i, "y", Math.round(r.y),
|
|
"h", Math.round(r.height),
|
|
r.text !== undefined ? JSON.stringify(r.text) : "")
|
|
righe.push({
|
|
"i": i,
|
|
"y": Math.round(r.y),
|
|
"h": Math.round(r.height),
|
|
"t": r.text !== undefined ? String(r.text) : ""
|
|
})
|
|
}
|
|
// Le stesse misure vanno anche al bridge, che le scrive nel
|
|
// registro: cosi' l'agente le legge da li' invece di chiedere il
|
|
// terminale o di decifrare uno screenshot.
|
|
if (api.configured)
|
|
api.reportLayout({
|
|
"versione": appVersion,
|
|
"schermo": Math.round(height),
|
|
"contenuto": Math.round(flick.contentHeight),
|
|
"righe": righe,
|
|
// Diagnostica della lingua: con che scelta, che locale e che
|
|
// catalogo sta girando l'app in questo momento.
|
|
"lingua": translator.language === "" ? "sistema" : translator.language,
|
|
"locale": translator.localeName,
|
|
"catalogo": translator.catalogLoaded
|
|
})
|
|
}
|
|
}
|
|
}
|