396 lines
13 KiB
C++
396 lines
13 KiB
C++
#include "apiclient.h"
|
|
#include "airajson.h"
|
|
#include "settings.h"
|
|
|
|
#include <QDebug>
|
|
#include <QLocale>
|
|
#include <QNetworkRequest>
|
|
#include <QNetworkReply>
|
|
#include <QTime>
|
|
#include <QTimer>
|
|
#include <QUrl>
|
|
|
|
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();
|
|
}
|