0.7.0: assenza con date, conto alla rovescia del boost, catalogo rifatto

- AwayDialog.qml: popup con inizio e fine (Dialog, ValueButton,
  DatePickerDialog: componenti e proprieta' verificati nella guida Silica
  dell'SDK). Il tasto Assenza apre il popup invece di mandare il comando;
- conto alla rovescia 30:00 -> 00:00 in Home e in copertina, con i secondi
  dell'impianto (AiraJson::countdown + timer che si riallinea a ogni lettura);
- commandPayload accetta campi extra (start/end);
- catalogo: lupdate leggeva solo i C++ e marcava 151 voci QML come obsolete.
  Ora src e qml espliciti: 197 stringhe, 0 non tradotte;
- versione 0.7.0 (spec, main.cpp), PROTOCOL, README/PIANO, post per il forum.
This commit is contained in:
Carlo
2026-10-11 12:05:59 +02:00
parent 6ef6f5bfd8
commit d2cb9d692f
17 changed files with 481 additions and 80 deletions
+17 -1
View File
@@ -175,15 +175,31 @@ QString monthLabel(const QString &yearMonth)
return QStringLiteral("%1 %2").arg(QLocale().monthName(mese, QLocale::LongFormat)).arg(anno);
}
QByteArray commandPayload(const QString &id, const QVariant &value)
QByteArray commandPayload(const QString &id, const QVariant &value, const QVariantMap &extra)
{
QJsonObject o;
o.insert(QStringLiteral("id"), id);
if (value.isValid() && !value.isNull())
o.insert(QStringLiteral("value"), QJsonValue::fromVariant(value));
// Campi in piu' (per esempio start ed end dell'assenza): li passa al bridge
// cosi' come sono, saltando quelli vuoti.
for (QVariantMap::const_iterator it = extra.constBegin(); it != extra.constEnd(); ++it) {
const QVariant v = it.value();
if (v.isValid() && !v.isNull() && !v.toString().isEmpty())
o.insert(it.key(), QJsonValue::fromVariant(v));
}
return QJsonDocument(o).toJson(QJsonDocument::Compact);
}
QString countdown(int seconds)
{
if (seconds < 0)
seconds = 0;
return QStringLiteral("%1:%2")
.arg(seconds / 60, 2, 10, QLatin1Char('0'))
.arg(seconds % 60, 2, 10, QLatin1Char('0'));
}
QString errorMessage(const QByteArray &body, int httpStatus)
{
QJsonParseError err;
+8 -2
View File
@@ -59,8 +59,14 @@ QString formatMoney(const QVariant &value, const QString &currencyCode);
QString monthLabel(const QString &yearMonth);
// Corpo JSON per POST /command. value viene omesso se nullo (i comandi
// semplici non hanno parametri).
QByteArray commandPayload(const QString &id, const QVariant &value);
// semplici non hanno parametri). `extra` aggiunge campi in piu' (per esempio
// start/end dell'assenza): passano al bridge saltando i vuoti.
QByteArray commandPayload(const QString &id, const QVariant &value,
const QVariantMap &extra = QVariantMap());
// Conto alla rovescia leggibile da secondi: 1800 -> "30:00", 65 -> "01:05",
// 0 -> "00:00". Oltre l'ora i minuti continuano (240 min -> "240:00").
QString countdown(int seconds);
// Messaggio d'errore leggibile da una risposta del bridge
// ({"ok":false,"error":"..."}), con fallback sullo stato HTTP.
+51 -2
View File
@@ -53,6 +53,14 @@ ApiClient::ApiClient(Settings *settings, QObject *parent)
m_refreshTimer->start();
});
// Un secondo alla volta mentre il boost e' in corso: i secondi veri li da'
// il bridge a ogni rilettura (ogni 30 s), qui si scorre in mezzo.
m_boostActive = false;
m_boostRemaining = 0;
m_boostTimer = new QTimer(this);
m_boostTimer->setInterval(1000);
connect(m_boostTimer, &QTimer::timeout, this, &ApiClient::tickBoost);
if (m_settings) {
// La UI deve riflettere subito il passaggio da "non configurato" a
// "configurato" quando si salvano indirizzo o token.
@@ -186,6 +194,7 @@ void ApiClient::refresh()
return;
}
m_summary = summary;
setBoostFromSummary(summary);
if (m_stale) {
m_stale = false;
emit staleChanged();
@@ -295,14 +304,14 @@ void ApiClient::refreshStats()
});
}
void ApiClient::sendCommand(const QString &id, const QVariant &value)
void ApiClient::sendCommand(const QString &id, const QVariant &value, const QVariantMap &extra)
{
if (!configured()) {
emit commandFinished(false, tr("Set the bridge address and token"));
return;
}
const QByteArray payload = AiraJson::commandPayload(id, value);
const QByteArray payload = AiraJson::commandPayload(id, value, extra);
qDebug() << "[api] comando:" << id; // mai i valori: qui potrebbe passare altro
QNetworkReply *reply = send("POST", QStringLiteral("/command"), payload, 30);
@@ -332,3 +341,43 @@ void ApiClient::sendCommand(const QString &id, const QVariant &value)
refresh(); // i valori letti cambiano: rileggo
});
}
QString ApiClient::boostCountdown() const
{
return AiraJson::countdown(m_boostRemaining);
}
// I secondi veri arrivano dal riassunto: il bridge li ricava dalla FINE del
// boost letta dall'impianto. Il timer li fa solo scorrere fra una lettura e
// l'altra, cosi' il conto non riparte da capo quando si riapre la app (e non
// sbaglia se il boost e' stato avviato da un'altra parte).
void ApiClient::setBoostFromSummary(const QVariantMap &summary)
{
const int restanti = summary.value(QStringLiteral("boost_remaining_seconds")).toInt();
const bool attivo = restanti > 0;
if (!attivo && !m_boostActive && m_boostRemaining == 0) {
m_boostTimer->stop(); // niente da mostrare: non disturbo
return;
}
m_boostRemaining = restanti;
m_boostActive = attivo;
if (attivo)
m_boostTimer->start();
else
m_boostTimer->stop();
emit boostChanged();
}
void ApiClient::tickBoost()
{
if (m_boostRemaining > 0)
--m_boostRemaining;
if (m_boostRemaining <= 0) {
m_boostRemaining = 0;
m_boostActive = false;
m_boostTimer->stop(); // arrivato a 00:00
}
emit boostChanged();
}
+19 -1
View File
@@ -35,6 +35,13 @@ class ApiClient : public QObject
// messaggio e' lo stesso ovunque si stia.
Q_PROPERTY(QString notice READ notice NOTIFY noticeChanged)
Q_PROPERTY(bool noticeIsError READ noticeIsError NOTIFY noticeChanged)
// Conto alla rovescia del boost ACS (richiesta dell'utente: da 30:00 a
// 00:00). I secondi arrivano dal bridge, che li calcola dalla FINE del
// boost letta dall'impianto: cosi' il numero non dipende dal momento in cui
// l'app ha visto partire il comando. Qui scorrono di secondo in secondo.
Q_PROPERTY(bool boostActive READ boostActive NOTIFY boostChanged)
Q_PROPERTY(int boostRemaining READ boostRemaining NOTIFY boostChanged)
Q_PROPERTY(QString boostCountdown READ boostCountdown NOTIFY boostChanged)
public:
explicit ApiClient(Settings *settings, QObject *parent = 0);
@@ -48,6 +55,9 @@ public:
bool hasData() const { return !m_summary.isEmpty(); }
QString notice() const { return m_notice; }
bool noticeIsError() const { return m_noticeIsError; }
bool boostActive() const { return m_boostActive; }
int boostRemaining() const { return m_boostRemaining; }
QString boostCountdown() const;
// Stato leggibile in una riga (stateShort) e per esteso (stateLong), piu'
// il livello da cui la UI prende il colore. Stanno in C++ perche' sono
@@ -66,7 +76,9 @@ public:
Q_INVOKABLE void testConnection();
// Invia un comando: sendCommand("set_hot_water_target", 55).
Q_INVOKABLE void sendCommand(const QString &id, const QVariant &value = QVariant());
// `extra` aggiunge campi al corpo JSON (start/end dell'assenza).
Q_INVOKABLE void sendCommand(const QString &id, const QVariant &value = QVariant(),
const QVariantMap &extra = QVariantMap());
// Formattazione per la UI: unica fonte di verita' in C++ (testata in
// tests/core_test.cpp), niente duplicazione di logica nel QML.
@@ -82,6 +94,7 @@ signals:
void staleChanged();
void lastUpdateChanged();
void noticeChanged();
void boostChanged();
void summaryChanged(const QVariantMap &summary);
void statsChanged(const QVariantMap &stats);
void statsRefreshed(bool ok, const QString &message);
@@ -96,6 +109,8 @@ private:
void setBusy(bool value);
// Mostra un messaggio in fondo alla pagina per qualche secondo.
void setNotice(const QString &message, bool isError);
void setBoostFromSummary(const QVariantMap &summary);
void tickBoost();
Settings *m_settings;
QNetworkAccessManager m_nam;
@@ -108,6 +123,9 @@ private:
QTimer *m_refreshTimer;
QVariantMap m_summary;
QVariantMap m_stats;
bool m_boostActive;
int m_boostRemaining;
QTimer *m_boostTimer;
};
#endif // APICLIENT_H
+2 -2
View File
@@ -21,7 +21,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
app->setOrganizationName(QStringLiteral("harbour"));
app->setApplicationName(QStringLiteral("aira"));
app->setApplicationVersion(QStringLiteral("0.6.9"));
app->setApplicationVersion(QStringLiteral("0.7.0"));
Settings settings;
ApiClient api(&settings);
@@ -36,7 +36,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
// Prima riga del log: dice subito se il device sta girando la build nuova
// o una stale (trappola nota del build engine).
qDebug() << "harbour-aira v0.6.9 build" << __DATE__ << __TIME__;
qDebug() << "harbour-aira v0.7.0 build" << __DATE__ << __TIME__;
view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings);
view->rootContext()->setContextProperty(QStringLiteral("api"), &api);