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
This commit is contained in:
+74
-7
@@ -5,6 +5,7 @@
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QLocale>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
|
||||
namespace AiraJson {
|
||||
@@ -47,6 +48,29 @@ QVariantMap parseSummary(const QByteArray &body, bool *ok)
|
||||
return summary.toVariantMap();
|
||||
}
|
||||
|
||||
QVariantMap parseStats(const QByteArray &body, bool *ok)
|
||||
{
|
||||
if (ok)
|
||||
*ok = false;
|
||||
|
||||
QJsonParseError err;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(body, &err);
|
||||
if (err.error != QJsonParseError::NoError || !doc.isObject())
|
||||
return QVariantMap();
|
||||
|
||||
const QJsonObject root = doc.object();
|
||||
if (root.value(QStringLiteral("ok")).toBool() == false)
|
||||
return QVariantMap();
|
||||
|
||||
const QJsonObject stats = root.value(QStringLiteral("stats")).toObject();
|
||||
if (stats.isEmpty())
|
||||
return QVariantMap();
|
||||
|
||||
if (ok)
|
||||
*ok = true;
|
||||
return stats.toVariantMap();
|
||||
}
|
||||
|
||||
QString modeLabel(const QString &operatingStatus)
|
||||
{
|
||||
const QString s = operatingStatus;
|
||||
@@ -92,20 +116,63 @@ QString modeLabel(const QString &operatingStatus)
|
||||
}
|
||||
|
||||
QString formatValue(const QVariant &value)
|
||||
{
|
||||
// Un decimale: e' il caso delle temperature ("51,0").
|
||||
return formatNumber(value, 1);
|
||||
}
|
||||
|
||||
QString formatNumber(const QVariant &value, int decimals)
|
||||
{
|
||||
if (!value.isValid() || value.isNull())
|
||||
return QStringLiteral("—");
|
||||
|
||||
bool isNumber = false;
|
||||
const double d = value.toDouble(&isNumber);
|
||||
if (isNumber)
|
||||
// QLocale() = locale predefinito, che translator.cpp allinea alla lingua
|
||||
// dell'app: separatore decimale col punto in inglese, con la virgola in
|
||||
// italiano (QString::number darebbe sempre il punto).
|
||||
return QLocale().toString(d, 'f', 1);
|
||||
if (!isNumber)
|
||||
return QStringLiteral("—");
|
||||
|
||||
const QString s = value.toString();
|
||||
return s.isEmpty() ? QStringLiteral("—") : s;
|
||||
// QLocale() = locale predefinito, che translator.cpp allinea alla lingua
|
||||
// dell'app: separatore decimale col punto in inglese, con la virgola in
|
||||
// italiano (QString::number darebbe sempre il punto).
|
||||
return QLocale().toString(d, 'f', decimals < 0 ? 0 : decimals);
|
||||
}
|
||||
|
||||
QString formatMoney(const QVariant &value, const QString ¤cyCode)
|
||||
{
|
||||
if (!value.isValid() || value.isNull())
|
||||
return QStringLiteral("—");
|
||||
|
||||
bool isNumber = false;
|
||||
const double d = value.toDouble(&isNumber);
|
||||
if (!isNumber)
|
||||
return QStringLiteral("—");
|
||||
|
||||
// Posizione del simbolo e separatori seguono la lingua dell'app, non la
|
||||
// valuta: "21,78 €" in italiano, "€21.78" in inglese.
|
||||
const QString code = currencyCode.trimmed().toUpper();
|
||||
QString symbol;
|
||||
if (code == QLatin1String("EUR"))
|
||||
symbol = QString::fromUtf8("€");
|
||||
else if (!code.isEmpty())
|
||||
symbol = code; // valuta senza simbolo noto: codice ISO
|
||||
return QLocale().toCurrencyString(d, symbol);
|
||||
}
|
||||
|
||||
QString monthLabel(const QString &yearMonth)
|
||||
{
|
||||
// "2026-10" -> "ottobre 2026": il nome del mese segue la lingua dell'app.
|
||||
const QStringList parti = yearMonth.split(QLatin1Char('-'));
|
||||
if (parti.size() != 2)
|
||||
return QString();
|
||||
|
||||
bool annoOk = false;
|
||||
bool meseOk = false;
|
||||
const int anno = parti.at(0).toInt(&annoOk);
|
||||
const int mese = parti.at(1).toInt(&meseOk);
|
||||
if (!annoOk || !meseOk || mese < 1 || mese > 12)
|
||||
return QString();
|
||||
|
||||
return QStringLiteral("%1 %2").arg(QLocale().monthName(mese, QLocale::LongFormat)).arg(anno);
|
||||
}
|
||||
|
||||
QByteArray commandPayload(const QString &id, const QVariant &value)
|
||||
|
||||
@@ -23,12 +23,27 @@ QString normalizeBaseUrl(const QString &raw);
|
||||
// vuoto e mette *ok a false.
|
||||
QVariantMap parseSummary(const QByteArray &body, bool *ok);
|
||||
|
||||
// Legge il corpo di GET /stats (pagina "Dati": calore, elettricita', COP,
|
||||
// risparmi) nello stesso formato. Vuoto e *ok a false se non interpretabile.
|
||||
QVariantMap parseStats(const QByteArray &body, bool *ok);
|
||||
|
||||
// Codice di stato Aira ("OPERATING_STATUS_AUTO") in etichetta italiana.
|
||||
QString modeLabel(const QString &operatingStatus);
|
||||
|
||||
// Temperatura leggibile: "51,0" oppure "—" se il valore manca.
|
||||
QString formatValue(const QVariant &value);
|
||||
|
||||
// Numero con il numero di decimali voluto, col separatore della lingua
|
||||
// ("4,42" in italiano, "4.42" in inglese); "—" se il valore manca.
|
||||
QString formatNumber(const QVariant &value, int decimals);
|
||||
|
||||
// Importo con il simbolo della valuta ("21,78 €" in italiano, "€21.78" in
|
||||
// inglese): posizione e separatori seguono la lingua, non la valuta.
|
||||
QString formatMoney(const QVariant &value, const QString ¤cyCode);
|
||||
|
||||
// "2026-10" -> "ottobre 2026" / "October 2026". Stringa vuota se illeggibile.
|
||||
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);
|
||||
|
||||
@@ -157,6 +157,64 @@ QString ApiClient::formatValue(const QVariant &value) const
|
||||
return AiraJson::formatValue(value);
|
||||
}
|
||||
|
||||
QString ApiClient::formatNumber(const QVariant &value, int decimals) const
|
||||
{
|
||||
return AiraJson::formatNumber(value, decimals);
|
||||
}
|
||||
|
||||
QString ApiClient::formatMoney(const QVariant &value, const QString ¤cyCode) const
|
||||
{
|
||||
return AiraJson::formatMoney(value, currencyCode);
|
||||
}
|
||||
|
||||
QString ApiClient::monthLabel(const QString &yearMonth) const
|
||||
{
|
||||
return AiraJson::monthLabel(yearMonth);
|
||||
}
|
||||
|
||||
void ApiClient::refreshStats()
|
||||
{
|
||||
if (!configured()) {
|
||||
emit statsRefreshed(false, tr("Set the bridge address and token"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Il bridge tiene le statistiche in cache (10 minuti): la prima lettura
|
||||
// interroga il cloud, quindi il tempo massimo e' piu' largo del solito.
|
||||
QNetworkReply *reply = send("GET", QStringLiteral("/stats"), QByteArray(), 60);
|
||||
if (!reply) {
|
||||
emit statsRefreshed(false, tr("Bridge not configured"));
|
||||
return;
|
||||
}
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
|
||||
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
const QByteArray body = reply->readAll();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError || status != 200) {
|
||||
const QString msg = (reply->error() == QNetworkReply::OperationCanceledError)
|
||||
? tr("Timeout: no statistics")
|
||||
: AiraJson::errorMessage(body, status);
|
||||
qWarning() << "[api] statistiche non riuscite:" << status << msg;
|
||||
emit statsRefreshed(false, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const QVariantMap stats = AiraJson::parseStats(body, &ok);
|
||||
if (!ok) {
|
||||
qWarning() << "[api] statistiche non interpretabili";
|
||||
emit statsRefreshed(false, tr("Unreadable response from the bridge"));
|
||||
return;
|
||||
}
|
||||
m_stats = stats;
|
||||
emit statsChanged(stats);
|
||||
emit statsRefreshed(true, QString());
|
||||
});
|
||||
}
|
||||
|
||||
void ApiClient::sendCommand(const QString &id, const QVariant &value)
|
||||
{
|
||||
if (!configured()) {
|
||||
|
||||
@@ -21,6 +21,9 @@ class ApiClient : public QObject
|
||||
// le property dell'ApplicationWindow NON sono visibili alle pagine dello
|
||||
// pageStack, quindi la dashboard e la copertina non potrebbero condividerle.
|
||||
Q_PROPERTY(QVariantMap summary READ summary NOTIFY summaryChanged)
|
||||
// Statistiche della pagina "Dati" (calore, elettricita', COP, risparmi).
|
||||
// Separata dal riassunto: e' una chiamata piu' lenta, con cache lato bridge.
|
||||
Q_PROPERTY(QVariantMap stats READ stats NOTIFY statsChanged)
|
||||
|
||||
public:
|
||||
explicit ApiClient(Settings *settings, QObject *parent = 0);
|
||||
@@ -28,10 +31,14 @@ public:
|
||||
bool busy() const { return m_busy; }
|
||||
bool configured() const;
|
||||
QVariantMap summary() const { return m_summary; }
|
||||
QVariantMap stats() const { return m_stats; }
|
||||
|
||||
// Scarica il riassunto e lo emette con summaryChanged().
|
||||
Q_INVOKABLE void refresh();
|
||||
|
||||
// Scarica le statistiche (GET /stats) e le emette con statsChanged().
|
||||
Q_INVOKABLE void refreshStats();
|
||||
|
||||
// Prova GET /health (non richiede token) e risponde con connectionTested().
|
||||
Q_INVOKABLE void testConnection();
|
||||
|
||||
@@ -42,11 +49,16 @@ public:
|
||||
// tests/core_test.cpp), niente duplicazione di logica nel QML.
|
||||
Q_INVOKABLE QString modeLabel(const QString &operatingStatus) const;
|
||||
Q_INVOKABLE QString formatValue(const QVariant &value) const;
|
||||
Q_INVOKABLE QString formatNumber(const QVariant &value, int decimals) const;
|
||||
Q_INVOKABLE QString formatMoney(const QVariant &value, const QString ¤cyCode) const;
|
||||
Q_INVOKABLE QString monthLabel(const QString &yearMonth) const;
|
||||
|
||||
signals:
|
||||
void busyChanged();
|
||||
void configuredChanged();
|
||||
void summaryChanged(const QVariantMap &summary);
|
||||
void statsChanged(const QVariantMap &stats);
|
||||
void statsRefreshed(bool ok, const QString &message);
|
||||
void commandFinished(bool ok, const QString &message);
|
||||
void connectionTested(bool ok, const QString &message);
|
||||
void errorOccurred(const QString &message);
|
||||
@@ -61,6 +73,7 @@ private:
|
||||
QNetworkAccessManager m_nam;
|
||||
bool m_busy;
|
||||
QVariantMap m_summary;
|
||||
QVariantMap m_stats;
|
||||
};
|
||||
|
||||
#endif // APICLIENT_H
|
||||
|
||||
+2
-2
@@ -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.4.0"));
|
||||
app->setApplicationVersion(QStringLiteral("0.5.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.4.0 build" << __DATE__ << __TIME__;
|
||||
qDebug() << "harbour-aira v0.5.0 build" << __DATE__ << __TIME__;
|
||||
|
||||
view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings);
|
||||
view->rootContext()->setContextProperty(QStringLiteral("api"), &api);
|
||||
|
||||
Reference in New Issue
Block a user