harbour-aira 0.1.0: client Sailfish per pompa di calore Aira (bridge HTTP + stato e comandi ACS)
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
#include "apiclient.h"
|
||||
#include "airajson.h"
|
||||
#include "settings.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
ApiClient::ApiClient(Settings *settings, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_settings(settings)
|
||||
, m_busy(false)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
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(QStringLiteral("Indirizzo del bridge non configurato"));
|
||||
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(QStringLiteral("Configura indirizzo e token del bridge"));
|
||||
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)
|
||||
? QStringLiteral("Server non raggiungibile (timeout)")
|
||||
: AiraJson::errorMessage(body, status);
|
||||
qWarning() << "[api] summary non riuscito:" << status << msg;
|
||||
emit errorOccurred(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const QVariantMap summary = AiraJson::parseSummary(body, &ok);
|
||||
if (!ok) {
|
||||
qWarning() << "[api] riassunto non interpretabile";
|
||||
emit errorOccurred(QStringLiteral("Risposta del bridge non interpretabile"));
|
||||
return;
|
||||
}
|
||||
m_summary = summary;
|
||||
emit summaryChanged(summary);
|
||||
});
|
||||
}
|
||||
|
||||
void ApiClient::testConnection()
|
||||
{
|
||||
const QString base = AiraJson::normalizeBaseUrl(m_settings ? m_settings->baseUrl() : QString());
|
||||
if (base.isEmpty()) {
|
||||
emit connectionTested(false, QStringLiteral("Inserisci l'indirizzo del bridge"));
|
||||
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)
|
||||
? QStringLiteral("Timeout: nessuna risposta")
|
||||
: QStringLiteral("Irraggiungibile (HTTP %1)").arg(status);
|
||||
emit connectionTested(false, msg);
|
||||
return;
|
||||
}
|
||||
emit connectionTested(true, QStringLiteral("Bridge raggiungibile"));
|
||||
});
|
||||
}
|
||||
|
||||
QString ApiClient::modeLabel(const QString &operatingStatus) const
|
||||
{
|
||||
return AiraJson::modeLabel(operatingStatus);
|
||||
}
|
||||
|
||||
QString ApiClient::formatValue(const QVariant &value) const
|
||||
{
|
||||
return AiraJson::formatValue(value);
|
||||
}
|
||||
|
||||
void ApiClient::sendCommand(const QString &id, const QVariant &value)
|
||||
{
|
||||
if (!configured()) {
|
||||
emit commandFinished(false, QStringLiteral("Configura indirizzo e token del bridge"));
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray payload = AiraJson::commandPayload(id, value);
|
||||
qDebug() << "[api] comando:" << id; // mai i valori: qui potrebbe passare altro
|
||||
|
||||
QNetworkReply *reply = send("POST", QStringLiteral("/command"), payload, 30);
|
||||
if (!reply) {
|
||||
emit commandFinished(false, QStringLiteral("Bridge non configurato"));
|
||||
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)
|
||||
? QStringLiteral("Timeout: comando non inviato")
|
||||
: AiraJson::errorMessage(body, status);
|
||||
qWarning() << "[api] comando" << id << "fallito:" << status << msg;
|
||||
emit commandFinished(false, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
emit commandFinished(true, QStringLiteral("Comando inviato"));
|
||||
refresh(); // i valori letti cambiano: rileggo
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user