#include "apiclient.h" #include "airajson.h" #include "settings.h" #include #include #include #include #include #include #include ApiClient::ApiClient(Settings *settings, QObject *parent) : QObject(parent) , m_settings(settings) , m_busy(false) , m_stale(false) , m_noticeIsError(false) , m_noticeTimer(0) , m_refreshTimer(0) { // Il messaggio dell'ultimo comando (o dell'ultimo errore) vale per ogni // pagina: lo stato sta qui, si spegne da solo e la UI lo mostra ancorato // in fondo alla schermata (NoticeBar), fuori dal contenuto che scorre. m_noticeTimer = new QTimer(this); m_noticeTimer->setSingleShot(true); m_noticeTimer->setInterval(7000); connect(m_noticeTimer, &QTimer::timeout, this, [this]() { if (m_notice.isEmpty()) return; m_notice.clear(); emit noticeChanged(); }); connect(this, &ApiClient::errorOccurred, this, [this](const QString &message) { setNotice(message, true); }); connect(this, &ApiClient::commandFinished, this, [this](bool ok, const QString &message) { setNotice(message, !ok); }); connect(this, &ApiClient::statsRefreshed, this, [this](bool ok, const QString &message) { setNotice(message, !ok); }); // Dopo un comando riuscito lo stato si rilegge da solo: l'impianto applica // in qualche istante, e senza questa rilettura gli interruttori della // pagina dei comandi resterebbero indietro rispetto alla pompa. m_refreshTimer = new QTimer(this); m_refreshTimer->setSingleShot(true); m_refreshTimer->setInterval(2000); connect(m_refreshTimer, &QTimer::timeout, this, &ApiClient::refresh); connect(this, &ApiClient::commandFinished, this, [this](bool ok, const QString &) { if (ok) 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. connect(m_settings, &Settings::baseUrlChanged, this, &ApiClient::configuredChanged); connect(m_settings, &Settings::tokenChanged, this, &ApiClient::configuredChanged); } } bool ApiClient::configured() const { if (!m_settings) return false; return !AiraJson::normalizeBaseUrl(m_settings->baseUrl()).isEmpty() && !m_settings->token().trimmed().isEmpty(); } QString ApiClient::stateShort() const { return AiraJson::stateShort(m_summary); } QString ApiClient::stateLong() const { return AiraJson::stateLong(m_summary); } int ApiClient::statusLevel() const { return AiraJson::statusLevel(m_summary); } void ApiClient::setNotice(const QString &message, bool isError) { m_notice = message; m_noticeIsError = isError; emit noticeChanged(); if (!message.isEmpty()) m_noticeTimer->start(); } void ApiClient::setBusy(bool value) { if (value == m_busy) return; m_busy = value; emit busyChanged(); } void ApiClient::armTimeout(QNetworkReply *reply, int seconds) { // QNetworkAccessManager non applica NESSUN timeout: senza watchdog una // richiesta verso un host irraggiungibile resta appesa e la UI sembra morta. QTimer *t = new QTimer(reply); // figlio della reply: si pulisce da solo t->setSingleShot(true); t->setInterval(seconds * 1000); connect(t, &QTimer::timeout, reply, [reply]() { if (reply->isRunning()) reply->abort(); }); connect(reply, &QNetworkReply::finished, t, &QTimer::stop); t->start(); } QNetworkReply *ApiClient::send(const QByteArray &verb, const QString &path, const QByteArray &body, int timeoutSec) { const QString base = AiraJson::normalizeBaseUrl(m_settings ? m_settings->baseUrl() : QString()); if (base.isEmpty()) { emit errorOccurred(tr("Bridge address not configured")); return 0; } const QUrl url(base + path); QNetworkRequest request(url); // variabile intermedia: evita il most-vexing-parse request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); request.setRawHeader("Authorization", "Bearer " + m_settings->token().toUtf8()); QNetworkReply *reply = 0; if (verb == "GET") reply = m_nam.get(request); else reply = m_nam.post(request, body); armTimeout(reply, timeoutSec); return reply; } void ApiClient::refresh() { if (!configured()) { emit errorOccurred(tr("Set the bridge address and token")); return; } setBusy(true); QNetworkReply *reply = send("GET", QStringLiteral("/summary"), QByteArray(), 15); if (!reply) { setBusy(false); return; } connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); setBusy(false); 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("Server unreachable (timeout)") : AiraJson::errorMessage(body, status); qWarning() << "[api] summary non riuscito:" << status << msg; if (!m_stale) { m_stale = true; emit staleChanged(); } emit errorOccurred(msg); return; } bool ok = false; const QVariantMap summary = AiraJson::parseSummary(body, &ok); if (!ok) { qWarning() << "[api] riassunto non interpretabile"; if (!m_stale) { m_stale = true; emit staleChanged(); } emit errorOccurred(tr("Unreadable response from the bridge")); return; } m_summary = summary; setBoostFromSummary(summary); if (m_stale) { m_stale = false; emit staleChanged(); } // Ora dell'ultimo aggiornamento riuscito: la Home la mostra, cosi' un // numero vecchio non passa per un numero fresco. m_lastUpdate = QLocale::system().toString(QTime::currentTime(), QLocale::ShortFormat); emit lastUpdateChanged(); emit summaryChanged(summary); }); } void ApiClient::testConnection() { const QString base = AiraJson::normalizeBaseUrl(m_settings ? m_settings->baseUrl() : QString()); if (base.isEmpty()) { emit connectionTested(false, tr("Enter the bridge address")); return; } // /health non richiede token: cosi' si distingue "server irraggiungibile" // da "token sbagliato". const QUrl url(base + QStringLiteral("/health")); QNetworkRequest request(url); // variabile intermedia (most vexing parse) QNetworkReply *reply = m_nam.get(request); armTimeout(reply, 10); connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (reply->error() != QNetworkReply::NoError || status != 200) { const QString msg = (reply->error() == QNetworkReply::OperationCanceledError) ? tr("Timeout: no response") : tr("Unreachable (HTTP %1)").arg(status); emit connectionTested(false, msg); return; } emit connectionTested(true, tr("Bridge reachable")); }); } QString ApiClient::modeLabel(const QString &operatingStatus) const { return AiraJson::modeLabel(operatingStatus); } 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()); }); } // Le misure della Home vanno al bridge, che le scrive nel suo registro: cosi' // per correggere la disposizione non servono ne' terminale sul telefono ne' // screenshot da interpretare a occhio. void ApiClient::reportLayout(const QVariantMap &info) { QNetworkReply *reply = send("POST", QStringLiteral("/layout"), AiraJson::layoutPayload(info), 5); if (!reply) return; connect(reply, &QNetworkReply::finished, reply, &QObject::deleteLater); } 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, extra); qDebug() << "[api] comando:" << id; // mai i valori: qui potrebbe passare altro QNetworkReply *reply = send("POST", QStringLiteral("/command"), payload, 30); if (!reply) { emit commandFinished(false, tr("Bridge not configured")); return; } setBusy(true); connect(reply, &QNetworkReply::finished, this, [this, reply, id]() { reply->deleteLater(); setBusy(false); 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: command not sent") : AiraJson::errorMessage(body, status); qWarning() << "[api] comando" << id << "fallito:" << status << msg; emit commandFinished(false, msg); return; } emit commandFinished(true, tr("Command sent")); 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(); }