- Home: stato in una riga col pallino, due letture grandi toccabili, tre azioni rapide; via i due slider, i sette pulsanti e la scheda separata. - Nuova pagina Regolazioni: un blocco per grandezza, valore attuale sempre visibile, preimpostazioni per le dita fredde, conferma esplicita; il pulsante e' attivo solo se c'e' davvero qualcosa da cambiare. - Messaggi dei comandi spostati in ApiClient (api.notice) con NoticeBar ancorata in fondo: prima nascevano fuori dallo schermo. - Stato breve/esteso/livello in C++ (testabile senza SDK), 18 verifiche nuove: 79/79. - stale + lastUpdate: la Home dice 'aggiornato alle HH:MM' e spegne i numeri vecchi invece di farli passare per freschi. - 192/192 stringhe tradotte (54 nuove).
258 lines
14 KiB
C++
258 lines
14 KiB
C++
// Verifica locale del nucleo di harbour-aira, senza SDK Sailfish.
|
|
// Copre la parte pura (src/airajson.cpp): normalizzazione dell'indirizzo,
|
|
// lettura del riassunto, etichette di stato, corpo dei comandi, messaggi
|
|
// d'errore. Il networking e la UI si verificano sul device.
|
|
//
|
|
// Build: ~/Qt/5.15.2/gcc_64/bin/qmake tests/core_test.pro && make && ./core_test
|
|
#include <QCoreApplication>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QDebug>
|
|
#include <QLocale>
|
|
#include <QStringList>
|
|
#include <QTranslator>
|
|
|
|
#include "../src/airajson.h"
|
|
|
|
static int failures = 0;
|
|
static int checks = 0;
|
|
|
|
static void check(bool cond, const QString &what)
|
|
{
|
|
++checks;
|
|
if (!cond) {
|
|
++failures;
|
|
qWarning() << "FALLITO:" << what;
|
|
} else {
|
|
qInfo() << "ok:" << what;
|
|
}
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
QCoreApplication app(argc, argv);
|
|
|
|
// ---- normalizeBaseUrl ------------------------------------------------- //
|
|
check(AiraJson::normalizeBaseUrl("192.168.2.50:8790") == "http://192.168.2.50:8790",
|
|
"host:porta riceve lo schema http");
|
|
check(AiraJson::normalizeBaseUrl("http://192.168.2.50:8790") == "http://192.168.2.50:8790",
|
|
"schema http preservato");
|
|
check(AiraJson::normalizeBaseUrl("https://aira.example.tld/bridge/") == "https://aira.example.tld/bridge",
|
|
"https preservato, slash finale tolto");
|
|
check(AiraJson::normalizeBaseUrl(" 192.168.2.50:8790// ") == "http://192.168.2.50:8790",
|
|
"spazi e slash ripetuti normalizzati");
|
|
check(AiraJson::normalizeBaseUrl("").isEmpty(), "stringa vuota resta vuota (nessuna richiesta)");
|
|
check(AiraJson::normalizeBaseUrl(" ").isEmpty(), "solo spazi = vuoto");
|
|
|
|
// ---- parseSummary ----------------------------------------------------- //
|
|
const QByteArray good = R"({"ok":true,"summary":{"outdoor_temperature":18.1,"dhw_current":51.0,"dhw_target":55.0,"operating_status":"OPERATING_STATUS_AUTO","away_mode":false,"errors_count":0}})";
|
|
bool ok = false;
|
|
QVariantMap m = AiraJson::parseSummary(good, &ok);
|
|
check(ok, "riassunto valido riconosciuto");
|
|
check(qFuzzyCompare(m.value("dhw_current").toDouble(), 51.0), "dhw_current letto");
|
|
check(m.value("operating_status").toString() == "OPERATING_STATUS_AUTO", "operating_status letto");
|
|
|
|
QVariantMap bad = AiraJson::parseSummary("non sono json", &ok);
|
|
check(!ok && bad.isEmpty(), "JSON non valido -> vuoto con ok=false");
|
|
|
|
QVariantMap err = AiraJson::parseSummary(R"({"ok":false,"error":"token errato"})", &ok);
|
|
check(!ok && err.isEmpty(), "risposta di errore -> vuoto");
|
|
check(AiraJson::parseSummary(R"({"ok":true})", &ok).isEmpty() && !ok,
|
|
"ok=true senza summary -> vuoto");
|
|
|
|
// ---- modeLabel -------------------------------------------------------- //
|
|
// Attese in inglese: e' la lingua SORGENTE delle stringhe (l'italiano arriva
|
|
// dal catalogo caricato piu' avanti).
|
|
check(AiraJson::modeLabel("OPERATING_STATUS_AUTO") == "Automatic", "AUTO -> Automatic");
|
|
check(AiraJson::modeLabel("OPERATING_STATUS_HOT_WATER") == "Hot water", "hot water");
|
|
check(AiraJson::modeLabel("") == "—", "stato vuoto -> trattino");
|
|
check(AiraJson::modeLabel("PUMP_MODE_STATE_HEATING") == "Heating",
|
|
"zone state: heating");
|
|
check(AiraJson::modeLabel("PUMP_MODE_STATE_IDLE") == "Idle",
|
|
"zone state: idle");
|
|
check(AiraJson::modeLabel("PUMP_MODE_STATE_IGNOTO") == "ignoto",
|
|
"unknown zone state -> prefix stripped");
|
|
check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_IDLE") == "Stopped",
|
|
"pump stopped");
|
|
check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_RUNNING") == "Running",
|
|
"pump running");
|
|
check(AiraJson::modeLabel("OPERATING_STATUS_DEFROST") == "defrost",
|
|
"unknown state: prefix stripped, lowercase");
|
|
|
|
// ---- formatValue ------------------------------------------------------ //
|
|
// Il separatore decimale segue il locale predefinito (che translator.cpp
|
|
// allinea alla lingua scelta): qui si fissa per rendere l'esito deterministico
|
|
// e si verifica che cambi davvero con la lingua.
|
|
QLocale::setDefault(QLocale::c());
|
|
check(AiraJson::formatValue(QVariant(51.0)) == "51.0", "numero a un decimale (locale C)");
|
|
check(AiraJson::formatValue(QVariant(18.14)) == "18.1", "arrotondamento a un decimale");
|
|
QLocale::setDefault(QLocale(QLocale::Italian, QLocale::Italy));
|
|
check(AiraJson::formatValue(QVariant(23.0)) == "23,0", "in italiano separatore virgola");
|
|
check(AiraJson::formatValue(QVariant(18.14)) == "18,1", "in italiano arrotonda con la virgola");
|
|
QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedKingdom));
|
|
check(AiraJson::formatValue(QVariant(23.0)) == "23.0", "in inglese separatore punto");
|
|
QLocale::setDefault(QLocale::c());
|
|
|
|
// Invariante su cui poggia Translator::apply(""): QLocale::system() non risente
|
|
// di setDefault(), quindi "lingua vuota" torna sempre alla lingua del telefono
|
|
// anche dopo che l'utente ha scelto l'italiano. (QLocale() invece cambierebbe.)
|
|
const QLocale systemBefore = QLocale::system();
|
|
QLocale::setDefault(QLocale(QLocale::Italian, QLocale::Italy));
|
|
check(QLocale::system() == systemBefore, "setDefault non tocca QLocale::system()");
|
|
check(QLocale() == QLocale(QLocale::Italian, QLocale::Italy), "QLocale() segue setDefault");
|
|
QLocale::setDefault(QLocale::c());
|
|
check(AiraJson::formatValue(QVariant()) == "—", "valore assente -> trattino");
|
|
check(AiraJson::formatValue(QVariant(QString())) == "—", "stringa vuota -> trattino");
|
|
|
|
// ---- commandPayload --------------------------------------------------- //
|
|
const QJsonObject c1 = QJsonDocument::fromJson(
|
|
AiraJson::commandPayload("set_hot_water_target", 55)).object();
|
|
check(c1.value("id").toString() == "set_hot_water_target", "id nel corpo del comando");
|
|
check(c1.value("value").toInt() == 55, "valore numerico nel corpo");
|
|
|
|
const QJsonObject c2 = QJsonDocument::fromJson(
|
|
AiraJson::commandPayload("hot_water_boost_on", QVariant())).object();
|
|
check(c2.contains("value") == false, "comando senza parametri: nessun campo value");
|
|
|
|
// ---- errorMessage ----------------------------------------------------- //
|
|
check(AiraJson::errorMessage(R"({"ok":false,"error":"token mancante o errato"})", 401)
|
|
== "token mancante o errato", "messaggio d'errore dal corpo");
|
|
check(AiraJson::errorMessage("", 502) == "HTTP error 502", "http status fallback");
|
|
check(AiraJson::errorMessage("", 0) == "server unreachable", "status 0 = unreachable");
|
|
|
|
// ---- parseStats (pagina "Dati") --------------------------------------- //
|
|
const QByteArray statsBody = R"({"ok":true,"stats":{"currency":"EUR","period_days":30,"month":{"label":"2026-10","heat_kwh":70.8,"energy_kwh":29.3,"cop_heating":null,"cop_dhw":4.42}}})";
|
|
bool okStats = false;
|
|
const QVariantMap st = AiraJson::parseStats(statsBody, &okStats);
|
|
check(okStats, "statistiche valide riconosciute");
|
|
check(st.value("currency").toString() == "EUR", "valuta letta");
|
|
check(st.value("period_days").toInt() == 30, "periodo letto");
|
|
check(qFuzzyCompare(st.value("month").toMap().value("heat_kwh").toDouble(), 70.8),
|
|
"calore prodotto letto");
|
|
check(st.value("month").toMap().value("cop_dhw").toDouble() == 4.42,
|
|
"COP acqua calda letto");
|
|
check(st.value("month").toMap().value("cop_heating").isNull(),
|
|
"efficienza riscaldamento assente resta nulla (nessun dato)");
|
|
check(AiraJson::parseStats(R"({"ok":false,"error":"token errato"})", &okStats).isEmpty() && !okStats,
|
|
"statistiche in errore -> vuoto");
|
|
check(AiraJson::parseStats("non sono json", &okStats).isEmpty() && !okStats,
|
|
"statistiche non JSON -> vuoto");
|
|
|
|
// ---- formatNumber / formatMoney / monthLabel -------------------------- //
|
|
QLocale::setDefault(QLocale(QLocale::Italian, QLocale::Italy));
|
|
check(AiraJson::formatNumber(QVariant(4.4213), 2) == "4,42", "due decimali (italiano)");
|
|
check(AiraJson::formatNumber(QVariant(29.3), 1) == "29,3", "un decimale (italiano)");
|
|
check(AiraJson::formatNumber(QVariant(), 2) == "—", "numero assente -> trattino");
|
|
// toCurrencyString usa lo spazio unificatore del locale: si controllano
|
|
// separatore e simbolo, non la stringa esatta.
|
|
const QString importoIt = AiraJson::formatMoney(QVariant(21.78), "EUR");
|
|
check(importoIt.contains("21,78") && importoIt.contains(QString::fromUtf8("€")),
|
|
"importo in italiano: virgola e simbolo");
|
|
check(AiraJson::formatMoney(QVariant(21.78), "eur") == importoIt,
|
|
"codice valuta minuscolo accettato");
|
|
check(AiraJson::formatMoney(QVariant(), "EUR") == "—", "importo assente -> trattino");
|
|
check(AiraJson::monthLabel("2026-10") == "ottobre 2026", "mese in italiano");
|
|
check(AiraJson::monthLabel("non-mese").isEmpty(), "chiave mese illeggibile -> vuoto");
|
|
check(AiraJson::monthLabel("2026-13").isEmpty(), "mese 13 -> vuoto");
|
|
QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedKingdom));
|
|
check(AiraJson::formatNumber(QVariant(4.4213), 2) == "4.42", "due decimali (inglese)");
|
|
const QString importoEn = AiraJson::formatMoney(QVariant(21.78), "EUR");
|
|
check(importoEn.contains("21.78") && importoEn.contains(QString::fromUtf8("€")),
|
|
"importo in inglese: punto e simbolo");
|
|
check(AiraJson::monthLabel("2026-10") == "October 2026", "mese in inglese");
|
|
QLocale::setDefault(QLocale::c());
|
|
|
|
// ---- catalogo italiano: il .qm caricato davvero ------------------------ //
|
|
// Non basta che il .ts esista: qui si installa il .qm come fa main.cpp e si
|
|
// controlla che le stringhe sorgente inglesi diventino italiane. Il percorso
|
|
// si puo' passare come argv[1]; il default e' quello relativo alla cartella
|
|
// di build del test (tests/build).
|
|
{
|
|
const QString qm = (argc > 1) ? QString::fromLocal8Bit(argv[1])
|
|
: QStringLiteral("../../translations/harbour-aira-it.qm");
|
|
QTranslator translator;
|
|
if (translator.load(qm)) {
|
|
QCoreApplication::installTranslator(&translator);
|
|
check(AiraJson::modeLabel("PUMP_MODE_STATE_HEATING") == "Riscaldamento",
|
|
"catalogo it: Heating -> Riscaldamento");
|
|
check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_IDLE") == "Ferma",
|
|
"catalogo it: Stopped -> Ferma");
|
|
check(AiraJson::modeLabel("PUMP_MODE_STATE_IDLE") == "In attesa",
|
|
"catalogo it: Idle -> In attesa");
|
|
check(AiraJson::errorMessage("", 0) == "server non raggiungibile",
|
|
"catalogo it: messaggio d'errore tradotto");
|
|
QCoreApplication::removeTranslator(&translator);
|
|
check(AiraJson::modeLabel("PUMP_MODE_STATE_HEATING") == "Heating",
|
|
"catalogo rimosso: si torna all'inglese (ripiego)");
|
|
} else {
|
|
qWarning() << "SALTO il controllo del catalogo: .qm non trovato in" << qm
|
|
<< "-> lancialo con lrelease, oppure passa il percorso come argomento";
|
|
}
|
|
}
|
|
|
|
// --- stato in una riga e per esteso (Home e pagina Impianto) ----------- //
|
|
{
|
|
QVariantMap calmo;
|
|
calmo.insert("errors_count", 0);
|
|
calmo.insert("away_mode", false);
|
|
calmo.insert("night_mode", false);
|
|
calmo.insert("force_heating", false);
|
|
calmo.insert("hot_water_heating", false);
|
|
calmo.insert("pump_active_state", "PUMP_ACTIVE_STATE_IDLE");
|
|
check(AiraJson::stateShort(calmo) == "Everything is fine",
|
|
"stato breve: nessuna attivita'");
|
|
check(AiraJson::stateLong(calmo) == "Relax: the heat pump is running smoothly.",
|
|
"stato esteso: frase dell'app ufficiale");
|
|
check(AiraJson::statusLevel(calmo) == 0, "livello: regolare");
|
|
|
|
QVariantMap lavora = calmo;
|
|
lavora.insert("pump_active_state", "PUMP_ACTIVE_STATE_RUNNING");
|
|
check(AiraJson::stateShort(lavora) == "Working", "stato breve: pompa in funzione");
|
|
check(AiraJson::statusLevel(lavora) == 0,
|
|
"livello: pompa in funzione e' comunque regolare");
|
|
|
|
QVariantMap acqua = calmo;
|
|
acqua.insert("hot_water_heating", true);
|
|
check(AiraJson::stateShort(acqua) == "Heating the hot water",
|
|
"stato breve: sta scaldando l'acqua");
|
|
check(AiraJson::stateLong(acqua) == "The heat pump is heating the hot water.",
|
|
"stato esteso: acqua calda in carica");
|
|
|
|
QVariantMap assente = calmo;
|
|
assente.insert("away_mode", true);
|
|
check(AiraJson::stateShort(assente) == "Away mode", "stato breve: assenza");
|
|
check(AiraJson::statusLevel(assente) == 1, "livello: modo attivo, non regolare");
|
|
|
|
QVariantMap uno = calmo;
|
|
uno.insert("errors_count", 1);
|
|
check(AiraJson::stateShort(uno) == "Attention: one error",
|
|
"stato breve: un errore (singolare)");
|
|
check(AiraJson::stateLong(uno) == "Attention: one error on the plant",
|
|
"stato esteso: un errore");
|
|
check(AiraJson::statusLevel(uno) == 2, "livello: errore");
|
|
|
|
QVariantMap due = calmo;
|
|
due.insert("errors_count", 2);
|
|
check(AiraJson::stateShort(due) == "Attention: 2 errors",
|
|
"stato breve: due errori (plurale)");
|
|
check(AiraJson::stateLong(due) == "Attention: 2 errors on the plant",
|
|
"stato esteso: due errori");
|
|
|
|
// L'errore ha la precedenza su tutto: e' quello che va visto per primo.
|
|
QVariantMap misto = assente;
|
|
misto.insert("errors_count", 3);
|
|
check(AiraJson::stateShort(misto) == "Attention: 3 errors",
|
|
"stato breve: l'errore vince sul modo attivo");
|
|
check(AiraJson::statusLevel(misto) == 2, "livello: errore prima di tutto");
|
|
|
|
check(AiraJson::stateShort(QVariantMap()) == "No data",
|
|
"stato breve: nessun dato");
|
|
check(AiraJson::stateLong(QVariantMap()).isEmpty(),
|
|
"stato esteso: nessun dato = nessuna frase");
|
|
}
|
|
|
|
qInfo() << "----";
|
|
qInfo() << checks - failures << "/" << checks << "verifiche superate";
|
|
return failures == 0 ? 0 : 1;
|
|
}
|