// 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 #include #include #include #include #include #include #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; }