L'inglese è la lingua sorgente delle stringhe; l'italiano sta nel catalogo translations/harbour-aira-it.ts (99 stringhe), che lupdate aggiorna e lrelease compila in build. Di serie la lingua del sistema, con ripiego sull'inglese. - nuova classe Translator: risolve la lingua prima di createView e allinea QLocale::setDefault, così anche i numeri seguono la lingua (23,0 °C in italiano, 23.0 °C in inglese) — le temperature passano da formatValue, non più dal toFixed() di JavaScript che metteva sempre il punto - Qt 5.6 non ha QQmlEngine::retranslate(): al cambio di lingua la radice QML viene ricaricata (setSource ricrea il componente anche a url invariata) - le etichette di stato in airajson.cpp usano QCoreApplication::translate per esteso: lupdate non riconosce una funzione wrapper e le avrebbe perse - selettore della lingua in Impostazioni (Sistema / English / Italiano) - test: 41/41, carica il .qm vero come fa main.cpp e verifica entrambe le lingue, il ripiego e i formati numerici - anteprime bilingui (docs/anteprima/ inglese, it/ italiano), README e PIANO aggiornati, spec RPM con i cataloghi in %files
358 lines
13 KiB
QML
358 lines
13 KiB
QML
import QtQuick 2.2
|
|
import Sailfish.Silica 1.0
|
|
import "../components"
|
|
|
|
// Dashboard dell'impianto. Legge api.summary (QVariantMap dal C++), quindi non
|
|
// dipende dallo scope delle property del main qml.
|
|
Page {
|
|
id: page
|
|
|
|
allowedOrientations: Orientation.All
|
|
|
|
// obiettivo ACS scelto dall'utente (non sovrascritto dal refresh finche'
|
|
// non lo tocca, altrimenti lo slider "salta" a ogni lettura)
|
|
property int targetAcs: 55
|
|
property bool targetAcsTouched: false
|
|
// obiettivo della casa (zona 1); parte dalla temperatura attuale e non
|
|
// viene sovrascritto dal refresh finche' l'utente non lo tocca
|
|
property real targetCasa: 20
|
|
property bool targetCasaTouched: false
|
|
property string notice: ""
|
|
property bool noticeIsError: false
|
|
|
|
function openSettings() {
|
|
pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
|
|
}
|
|
|
|
function openCommands() {
|
|
pageStack.push(Qt.resolvedUrl("CommandsPage.qml"))
|
|
}
|
|
|
|
function showNotice(msg, isError) {
|
|
page.notice = msg
|
|
page.noticeIsError = isError === true
|
|
noticeTimer.restart()
|
|
}
|
|
|
|
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 temp(key) {
|
|
return api.formatValue(page.raw(key))
|
|
}
|
|
|
|
function flag(key) {
|
|
return page.raw(key) ? qsTr("yes") : qsTr("no")
|
|
}
|
|
|
|
function count(key) {
|
|
var v = page.raw(key)
|
|
if (v === undefined || v === null)
|
|
return "—"
|
|
return String(Math.round(Number(v)))
|
|
}
|
|
|
|
// "2026-10-09 18:52:17.199340" -> "18:52"
|
|
function shortTime(s) {
|
|
if (!s)
|
|
return "—"
|
|
var parts = String(s).split(" ")
|
|
if (parts.length < 2)
|
|
return String(s)
|
|
return parts[1].substring(0, 5)
|
|
}
|
|
|
|
Timer {
|
|
id: noticeTimer
|
|
interval: 4000
|
|
onTriggered: page.notice = ""
|
|
}
|
|
|
|
RemorseItem { id: remorse }
|
|
|
|
Connections {
|
|
target: api
|
|
onErrorOccurred: page.showNotice(message, true)
|
|
onCommandFinished: page.showNotice(message, !ok)
|
|
onSummaryChanged: {
|
|
if (!page.targetAcsTouched) {
|
|
var v = page.raw("dhw_target")
|
|
if (v !== undefined && v !== null)
|
|
page.targetAcs = Math.round(Number(v))
|
|
}
|
|
if (!page.targetCasaTouched) {
|
|
var c = page.raw("zone1_setpoint")
|
|
if (c !== undefined && c !== null && Number(c) > 0)
|
|
page.targetCasa = Math.round(Number(c) * 2) / 2
|
|
}
|
|
}
|
|
}
|
|
|
|
SilicaFlickable {
|
|
id: flick
|
|
anchors.fill: parent
|
|
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("Settings")
|
|
onClicked: page.openSettings()
|
|
}
|
|
MenuItem {
|
|
text: qsTr("Other commands")
|
|
onClicked: page.openCommands()
|
|
}
|
|
}
|
|
|
|
Column {
|
|
id: col
|
|
width: parent.width
|
|
spacing: Theme.paddingSmall
|
|
|
|
PageHeader { title: "Aira" }
|
|
|
|
// ---- avvisi ---------------------------------------------------- //
|
|
Label {
|
|
visible: page.notice.length > 0
|
|
x: Theme.horizontalPageMargin
|
|
width: parent.width - 2 * Theme.horizontalPageMargin
|
|
text: page.notice
|
|
color: page.noticeIsError ? Theme.errorColor : Theme.highlightColor
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
wrapMode: Text.Wrap
|
|
}
|
|
|
|
// ---- 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()
|
|
}
|
|
|
|
// ---- casa: temperatura della casa (dato principale) ------------ //
|
|
Label {
|
|
visible: api.configured
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: qsTr("House temperature")
|
|
color: Theme.secondaryColor
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
}
|
|
|
|
Label {
|
|
visible: api.configured
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: page.temp("zone1_temperature") + " °C"
|
|
color: Theme.primaryColor
|
|
font.pixelSize: Theme.fontSizeHuge
|
|
}
|
|
|
|
ProgressBar {
|
|
visible: api.configured
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
width: parent.width - 4 * Theme.horizontalPageMargin
|
|
minimumValue: 5
|
|
maximumValue: 30
|
|
value: {
|
|
var v = page.raw("zone1_temperature")
|
|
return (v === undefined || v === null) ? 0 : Number(v)
|
|
}
|
|
}
|
|
|
|
Label {
|
|
visible: api.configured
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: qsTr("target %1 °C · %2").arg(page.temp("zone1_setpoint")).arg(api.modeLabel(page.raw("zone1_mode")))
|
|
color: Theme.secondaryColor
|
|
font.pixelSize: Theme.fontSizeExtraSmall
|
|
}
|
|
|
|
Slider {
|
|
id: casaSlider
|
|
visible: api.configured
|
|
width: parent.width
|
|
label: qsTr("New house target")
|
|
minimumValue: 5
|
|
maximumValue: 30
|
|
stepSize: 0.5
|
|
value: page.targetCasa
|
|
valueText: page.fmt(value) + " °C"
|
|
onValueChanged: {
|
|
page.targetCasaTouched = true
|
|
page.targetCasa = Math.round(value * 2) / 2
|
|
}
|
|
}
|
|
|
|
Button {
|
|
id: setCasaButton
|
|
visible: api.configured
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
leftMargin: Theme.horizontalPageMargin
|
|
rightMargin: Theme.horizontalPageMargin
|
|
}
|
|
text: qsTr("Set house to %1 °C").arg(page.fmt(page.targetCasa))
|
|
onClicked: remorse.execute(setCasaButton,
|
|
qsTr("House temperature to %1 °C").arg(page.fmt(page.targetCasa)),
|
|
function() { api.sendCommand("set_heating_setpoint", page.targetCasa) })
|
|
}
|
|
|
|
// ---- acqua calda sanitaria ------------------------------------- //
|
|
SectionHeader { visible: api.configured; text: qsTr("Domestic hot water") }
|
|
|
|
Label {
|
|
visible: api.configured
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: page.temp("dhw_current") + " °C"
|
|
color: Theme.primaryColor
|
|
font.pixelSize: Theme.fontSizeLarge
|
|
}
|
|
|
|
ProgressBar {
|
|
visible: api.configured
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
width: parent.width - 4 * Theme.horizontalPageMargin
|
|
minimumValue: 20
|
|
maximumValue: 65
|
|
value: {
|
|
var v = page.raw("dhw_current")
|
|
return (v === undefined || v === null) ? 0 : Number(v)
|
|
}
|
|
}
|
|
|
|
InfoRow {
|
|
visible: api.configured
|
|
label: qsTr("DHW target")
|
|
value: page.temp("dhw_target") + " °C"
|
|
}
|
|
|
|
Slider {
|
|
visible: api.configured
|
|
width: parent.width
|
|
label: qsTr("New target")
|
|
minimumValue: 45
|
|
maximumValue: 60
|
|
stepSize: 1
|
|
value: page.targetAcs
|
|
valueText: page.fmt(value) + " °C"
|
|
onValueChanged: {
|
|
page.targetAcsTouched = true
|
|
page.targetAcs = Math.round(value)
|
|
}
|
|
}
|
|
|
|
Button {
|
|
id: setAcsButton
|
|
visible: api.configured
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
leftMargin: Theme.horizontalPageMargin
|
|
rightMargin: Theme.horizontalPageMargin
|
|
}
|
|
text: qsTr("Set target %1 °C").arg(page.targetAcs)
|
|
onClicked: remorse.execute(setAcsButton,
|
|
qsTr("DHW target to %1 °C").arg(page.targetAcs),
|
|
function() { api.sendCommand("set_hot_water_target", page.targetAcs) })
|
|
}
|
|
|
|
Button {
|
|
id: boostOnButton
|
|
visible: api.configured
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
leftMargin: Theme.horizontalPageMargin
|
|
rightMargin: Theme.horizontalPageMargin
|
|
}
|
|
text: qsTr("Start DHW boost")
|
|
onClicked: remorse.execute(boostOnButton, qsTr("Starting DHW boost"),
|
|
function() { api.sendCommand("hot_water_boost_on") })
|
|
}
|
|
|
|
Button {
|
|
id: boostOffButton
|
|
visible: api.configured
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
leftMargin: Theme.horizontalPageMargin
|
|
rightMargin: Theme.horizontalPageMargin
|
|
}
|
|
text: qsTr("Stop DHW boost")
|
|
onClicked: remorse.execute(boostOffButton, qsTr("Stopping DHW boost"),
|
|
function() { api.sendCommand("hot_water_boost_off") })
|
|
}
|
|
|
|
// ---- stato impianto ------------------------------------------- //
|
|
SectionHeader { visible: api.configured; text: qsTr("Plant") }
|
|
|
|
InfoRow { visible: api.configured; label: qsTr("Outdoor temperature"); value: page.temp("outdoor_temperature") + " °C" }
|
|
InfoRow { visible: api.configured; label: qsTr("Indoor temperature (pump)"); value: page.temp("indoor_temperature") + " °C" }
|
|
InfoRow { visible: api.configured; label: qsTr("Mode"); value: api.modeLabel(page.raw("operating_status")) }
|
|
InfoRow { visible: api.configured; label: qsTr("Away mode"); value: page.flag("away_mode") }
|
|
InfoRow { visible: api.configured; label: qsTr("Night mode"); value: page.flag("night_mode") }
|
|
InfoRow { visible: api.configured; label: qsTr("Forced heating"); value: page.flag("force_heating") }
|
|
InfoRow { visible: api.configured; label: qsTr("Inline heater"); value: page.flag("inline_heater") }
|
|
InfoRow { visible: api.configured; label: qsTr("Zone"); value: page.count("zones") }
|
|
InfoRow { visible: api.configured; label: qsTr("Errors"); value: page.count("errors_count") }
|
|
InfoRow { visible: api.configured; label: qsTr("Last reading"); value: page.shortTime(page.raw("pump_updated")) }
|
|
|
|
Button {
|
|
visible: api.configured
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
leftMargin: Theme.horizontalPageMargin
|
|
rightMargin: Theme.horizontalPageMargin
|
|
}
|
|
text: qsTr("Other commands")
|
|
onClicked: page.openCommands()
|
|
}
|
|
|
|
BusyIndicator {
|
|
visible: api.busy
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
size: BusyIndicatorSize.Small
|
|
running: true
|
|
}
|
|
|
|
Label {
|
|
visible: api.configured
|
|
x: Theme.horizontalPageMargin
|
|
width: parent.width - 2 * Theme.horizontalPageMargin
|
|
text: qsTr("Commands are sent to the bridge and from there to the plant: they may take a few seconds.")
|
|
color: Theme.secondaryColor
|
|
font.pixelSize: Theme.fontSizeExtraSmall
|
|
wrapMode: Text.Wrap
|
|
}
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
if (api.configured)
|
|
api.refresh()
|
|
}
|
|
} |