harbour-aira 0.1.0: client Sailfish per pompa di calore Aira (bridge HTTP + stato e comandi ACS)

This commit is contained in:
Carlo
2026-10-09 21:00:59 +02:00
commit 15963039ea
27 changed files with 1686 additions and 0 deletions
+196
View File
@@ -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
});
}