Endpoint /mesures généralisé aux 4 domaines (capteurs, réservoirs, circuits, batteries), valeur actuelle réelle + points de bord synthétiques
This commit is contained in:
Executable
+106
@@ -0,0 +1,106 @@
|
|||||||
|
#include "dataprovider.h"
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QUrlQuery>
|
||||||
|
|
||||||
|
DataProvider::DataProvider(QObject *parent) : QObject(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void DataProvider::signalerSucces()
|
||||||
|
{
|
||||||
|
m_connecte = true;
|
||||||
|
m_derniereConnexionReussie = QDateTime::currentDateTime();
|
||||||
|
emit etatConnexionChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DataProvider::signalerEchec()
|
||||||
|
{
|
||||||
|
// Ne touche JAMAIS m_derniereConnexionReussie ici : on veut garder la
|
||||||
|
// trace du dernier succès réel, pas la remplacer par l'instant de l'échec.
|
||||||
|
if (m_connecte) {
|
||||||
|
m_connecte = false;
|
||||||
|
emit etatConnexionChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DataProvider::get_familles(const QString &type)
|
||||||
|
{
|
||||||
|
QUrl url(m_baseUrl + "/elements/familles");
|
||||||
|
QUrlQuery query;
|
||||||
|
query.addQueryItem("type", type);
|
||||||
|
url.setQuery(query);
|
||||||
|
|
||||||
|
auto reply = m_manager.get(QNetworkRequest(url));
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, reply, type]() {
|
||||||
|
reply->deleteLater();
|
||||||
|
if (reply->error() != QNetworkReply::NoError) {
|
||||||
|
signalerEchec();
|
||||||
|
emit erreur(reply->errorString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto doc = QJsonDocument::fromJson(reply->readAll());
|
||||||
|
const auto obj = doc.object();
|
||||||
|
if (obj["status"].toString() != "ok") {
|
||||||
|
signalerEchec();
|
||||||
|
emit erreur(obj["message"].toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
signalerSucces();
|
||||||
|
emit famillesRecues(type, obj["familles"].toArray());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void DataProvider::get_elements(const QString &type, int famille)
|
||||||
|
{
|
||||||
|
QUrl url(m_baseUrl + "/elements");
|
||||||
|
QUrlQuery query;
|
||||||
|
query.addQueryItem("type", type);
|
||||||
|
query.addQueryItem("famille", QString::number(famille));
|
||||||
|
url.setQuery(query);
|
||||||
|
|
||||||
|
auto reply = m_manager.get(QNetworkRequest(url));
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, reply, type, famille]() {
|
||||||
|
reply->deleteLater();
|
||||||
|
if (reply->error() != QNetworkReply::NoError) {
|
||||||
|
signalerEchec();
|
||||||
|
emit erreur(reply->errorString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto doc = QJsonDocument::fromJson(reply->readAll());
|
||||||
|
const auto obj = doc.object();
|
||||||
|
if (obj["status"].toString() != "ok") {
|
||||||
|
signalerEchec();
|
||||||
|
emit erreur(obj["message"].toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
signalerSucces();
|
||||||
|
emit elementsRecus(type, famille, obj["elements"].toArray());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void DataProvider::set_circuit(int id, int consigne)
|
||||||
|
{
|
||||||
|
QUrl url(m_baseUrl + "/elements/circuit/" + QString::number(id));
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
|
||||||
|
QJsonObject body;
|
||||||
|
body["consigne"] = consigne;
|
||||||
|
const QByteArray data = QJsonDocument(body).toJson();
|
||||||
|
|
||||||
|
auto reply = m_manager.sendCustomRequest(request, "PATCH", data);
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, reply, id]() {
|
||||||
|
reply->deleteLater();
|
||||||
|
const bool succes = (reply->error() == QNetworkReply::NoError);
|
||||||
|
if (succes) signalerSucces();
|
||||||
|
else {
|
||||||
|
signalerEchec();
|
||||||
|
emit erreur(reply->errorString());
|
||||||
|
}
|
||||||
|
emit circuitModifie(id, succes);
|
||||||
|
});
|
||||||
|
}
|
||||||
Executable
+65
@@ -0,0 +1,65 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <QObject>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QDateTime>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DataProvider - couche d'accès aux données du client WASM (cas 2/3).
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Remplace, pour les domaines déjà portés, les appels bdd.* directs du cas 1
|
||||||
|
// (qui reste inchangé, cf décision §21.1 de la synthèse). Toute la logique
|
||||||
|
// HTTP/JSON est encapsulée ici - le QML ne voit que des signaux avec des
|
||||||
|
// tableaux déjà prêts à l'emploi.
|
||||||
|
//
|
||||||
|
// Portée actuelle : les 4 domaines (circuit, batterie, capteur, reservoir)
|
||||||
|
// en lecture, écriture (consigne/mode auto) sur circuit uniquement.
|
||||||
|
//
|
||||||
|
// Suit également l'état de connexion (succès/échec de chaque requête) pour
|
||||||
|
// permettre à l'UI d'afficher un état "hors ligne" explicite plutôt que de
|
||||||
|
// laisser les listes silencieusement vides ou figées (cf synthèse, gestion
|
||||||
|
// du hors-ligne).
|
||||||
|
// ============================================================================
|
||||||
|
class DataProvider : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Q_PROPERTY(bool connecte READ connecte NOTIFY etatConnexionChange)
|
||||||
|
Q_PROPERTY(bool aDejaEteConnecte READ aDejaEteConnecte NOTIFY etatConnexionChange)
|
||||||
|
Q_PROPERTY(QDateTime derniereConnexionReussie READ derniereConnexionReussie NOTIFY etatConnexionChange)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DataProvider(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
// TODO : rendre l'adresse de base configurable (IP locale du CM5 en cas 2,
|
||||||
|
// domaine VPS en cas 3 - cf QR code d'accès, §"Architecture cible"/Cas 2-3
|
||||||
|
// de la synthèse) - en dur sur le relais VPS pour cette étape.
|
||||||
|
Q_INVOKABLE void get_familles(const QString &type);
|
||||||
|
Q_INVOKABLE void get_elements(const QString &type, int famille);
|
||||||
|
|
||||||
|
// Écriture rapide sur un circuit (bouton d'accueil) - PATCH /elements/circuit/<id>.
|
||||||
|
Q_INVOKABLE void set_circuit(int id, int consigne);
|
||||||
|
|
||||||
|
bool connecte() const { return m_connecte; }
|
||||||
|
bool aDejaEteConnecte() const { return m_derniereConnexionReussie.isValid(); }
|
||||||
|
QDateTime derniereConnexionReussie() const { return m_derniereConnexionReussie; }
|
||||||
|
|
||||||
|
signals:
|
||||||
|
// Le type (et, pour les éléments, la famille) sont désormais inclus dans
|
||||||
|
// chaque signal - indispensable avec plusieurs domaines actifs en
|
||||||
|
// parallèle, pour que le QML sache à quelle réponse il a affaire.
|
||||||
|
void famillesRecues(const QString &type, const QJsonArray &familles);
|
||||||
|
void elementsRecus(const QString &type, int famille, const QJsonArray &elements);
|
||||||
|
void circuitModifie(int id, bool succes);
|
||||||
|
void erreur(const QString &message);
|
||||||
|
void etatConnexionChange();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void signalerSucces();
|
||||||
|
void signalerEchec();
|
||||||
|
|
||||||
|
QNetworkAccessManager m_manager;
|
||||||
|
QString m_baseUrl = QStringLiteral("https://api.ship2net.fr");
|
||||||
|
bool m_connecte = false;
|
||||||
|
QDateTime m_derniereConnexionReussie; // invalide (isValid()==false) tant qu'aucun succès n'a jamais eu lieu
|
||||||
|
};
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QUrlQuery>
|
#include <QUrlQuery>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QMap>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -170,10 +172,10 @@ int main(int argc, char *argv[])
|
|||||||
const QString type = query.queryItemValue("type");
|
const QString type = query.queryItemValue("type");
|
||||||
|
|
||||||
int typeSource;
|
int typeSource;
|
||||||
if (type == "circuit") typeSource = TYPE_SOURCE_CIRCUIT;
|
if (type == "circuits") typeSource = TYPE_SOURCE_CIRCUIT;
|
||||||
else if (type == "batterie") typeSource = TYPE_SOURCE_BATTERIE;
|
else if (type == "batteries") typeSource = TYPE_SOURCE_BATTERIE;
|
||||||
else if (type == "capteur") typeSource = TYPE_SOURCE_CAPTEUR;
|
else if (type == "capteurs") typeSource = TYPE_SOURCE_CAPTEUR;
|
||||||
else if (type == "reservoir") typeSource = TYPE_SOURCE_RESERVOIR;
|
else if (type == "reservoirs") typeSource = TYPE_SOURCE_RESERVOIR;
|
||||||
else {
|
else {
|
||||||
QJsonObject err;
|
QJsonObject err;
|
||||||
err["status"] = "erreur";
|
err["status"] = "erreur";
|
||||||
@@ -189,7 +191,7 @@ int main(int argc, char *argv[])
|
|||||||
const QString nom = q1.value(1).toString();
|
const QString nom = q1.value(1).toString();
|
||||||
int etat = 0;
|
int etat = 0;
|
||||||
|
|
||||||
if (type == "circuit") {
|
if (type == "circuits") {
|
||||||
// Alarmes (Events) liées à cette famille :
|
// Alarmes (Events) liées à cette famille :
|
||||||
QSqlQuery q2;
|
QSqlQuery q2;
|
||||||
q2.prepare("SELECT COUNT(*) FROM TB_Events WHERE Type_source=:type AND Validite=1 AND Alarme_source>0 "
|
q2.prepare("SELECT COUNT(*) FROM TB_Events WHERE Type_source=:type AND Validite=1 AND Alarme_source>0 "
|
||||||
@@ -221,7 +223,7 @@ int main(int argc, char *argv[])
|
|||||||
etat = 1;
|
etat = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (type == "batterie") {
|
} else if (type == "batteries") {
|
||||||
// Règle plus simple que les circuits (cf §20.4/§21.8 synthèse) :
|
// Règle plus simple que les circuits (cf §20.4/§21.8 synthèse) :
|
||||||
// rouge si Etat_systeme en défaut sur une batterie de la famille,
|
// rouge si Etat_systeme en défaut sur une batterie de la famille,
|
||||||
// sinon orange si au moins une alarme (Events), sinon vert.
|
// sinon orange si au moins une alarme (Events), sinon vert.
|
||||||
@@ -243,7 +245,7 @@ int main(int argc, char *argv[])
|
|||||||
while (q3.next()) {
|
while (q3.next()) {
|
||||||
if (q3.value(0).toInt() != 0) etat = 2;
|
if (q3.value(0).toInt() != 0) etat = 2;
|
||||||
}
|
}
|
||||||
} else if (type == "capteur") {
|
} else if (type == "capteurs") {
|
||||||
// Période d'obsolescence NMEA (TB_Configuration) :
|
// Période d'obsolescence NMEA (TB_Configuration) :
|
||||||
int periodeObsolescenceNmea = 0;
|
int periodeObsolescenceNmea = 0;
|
||||||
{
|
{
|
||||||
@@ -278,7 +280,7 @@ int main(int argc, char *argv[])
|
|||||||
if (age > periodeObsolescenceNmea && etat < 1) etat = 1;
|
if (age > periodeObsolescenceNmea && etat < 1) etat = 1;
|
||||||
if ((etatCapteur > 0 || etatSysteme != 0) && etat < 2) etat = 2;
|
if ((etatCapteur > 0 || etatSysteme != 0) && etat < 2) etat = 2;
|
||||||
}
|
}
|
||||||
} else if (type == "reservoir") {
|
} else if (type == "reservoirs") {
|
||||||
// Alarmes (Events) liées à cette famille :
|
// Alarmes (Events) liées à cette famille :
|
||||||
QSqlQuery q2;
|
QSqlQuery q2;
|
||||||
q2.prepare("SELECT COUNT(*) FROM TB_Events WHERE Type_source=:type AND Validite=1 AND Alarme_source>0 "
|
q2.prepare("SELECT COUNT(*) FROM TB_Events WHERE Type_source=:type AND Validite=1 AND Alarme_source>0 "
|
||||||
@@ -326,7 +328,7 @@ int main(int argc, char *argv[])
|
|||||||
const QString type = query.queryItemValue("type");
|
const QString type = query.queryItemValue("type");
|
||||||
const QString familleStr = query.queryItemValue("famille");
|
const QString familleStr = query.queryItemValue("famille");
|
||||||
|
|
||||||
if (type != "circuit" && type != "batterie" && type != "capteur" && type != "reservoir") {
|
if (type != "circuits" && type != "batteries" && type != "capteurs" && type != "reservoirs") {
|
||||||
QJsonObject err;
|
QJsonObject err;
|
||||||
err["status"] = "erreur";
|
err["status"] = "erreur";
|
||||||
err["message"] = "type non supporté (circuit, batterie, capteur, reservoir)";
|
err["message"] = "type non supporté (circuit, batterie, capteur, reservoir)";
|
||||||
@@ -344,7 +346,7 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
QJsonArray elements;
|
QJsonArray elements;
|
||||||
|
|
||||||
if (type == "circuit") {
|
if (type == "circuits") {
|
||||||
QSqlQuery q;
|
QSqlQuery q;
|
||||||
q.prepare(
|
q.prepare(
|
||||||
"SELECT Cle_primaire, Etat_systeme, Nom, Etat_commande_carte, Mode_commande, Etat, Consigne, Old_consigne, "
|
"SELECT Cle_primaire, Etat_systeme, Nom, Etat_commande_carte, Mode_commande, Etat, Consigne, Old_consigne, "
|
||||||
@@ -383,7 +385,7 @@ int main(int argc, char *argv[])
|
|||||||
obj["etat_events"] = q.value(16).toInt();
|
obj["etat_events"] = q.value(16).toInt();
|
||||||
elements.append(obj);
|
elements.append(obj);
|
||||||
}
|
}
|
||||||
} else if (type == "batterie") {
|
} else if (type == "batteries") {
|
||||||
// Rappel : le champ "etat" retourné ici est le NOMBRE D'ALARMES,
|
// Rappel : le champ "etat" retourné ici est le NOMBRE D'ALARMES,
|
||||||
// pas un code d'état comme pour les circuits - fidèle au modèle
|
// pas un code d'état comme pour les circuits - fidèle au modèle
|
||||||
// natif existant (modele_batteries), pas une incohérence introduite ici.
|
// natif existant (modele_batteries), pas une incohérence introduite ici.
|
||||||
@@ -411,7 +413,7 @@ int main(int argc, char *argv[])
|
|||||||
obj["etat"] = q.value(7).toInt();
|
obj["etat"] = q.value(7).toInt();
|
||||||
elements.append(obj);
|
elements.append(obj);
|
||||||
}
|
}
|
||||||
} else if (type == "capteur") {
|
} else if (type == "capteurs") {
|
||||||
// Préférences d'unité du bateau (TB_Configuration) :
|
// Préférences d'unité du bateau (TB_Configuration) :
|
||||||
int uniteVolume = 0, uniteTemperature = 0, periodeObsolescenceNmea = 0;
|
int uniteVolume = 0, uniteTemperature = 0, periodeObsolescenceNmea = 0;
|
||||||
{
|
{
|
||||||
@@ -524,7 +526,7 @@ int main(int argc, char *argv[])
|
|||||||
obj["obsolete"] = (typeEntrees == TYPE_ENTREES_NMEA && age > periodeObsolescenceNmea);
|
obj["obsolete"] = (typeEntrees == TYPE_ENTREES_NMEA && age > periodeObsolescenceNmea);
|
||||||
elements.append(obj);
|
elements.append(obj);
|
||||||
}
|
}
|
||||||
} else if (type == "reservoir") {
|
} else if (type == "reservoirs") {
|
||||||
QSqlQuery q;
|
QSqlQuery q;
|
||||||
q.prepare(
|
q.prepare(
|
||||||
"SELECT Cle_primaire, Nom, Etat_systeme, Niveau, Capacite, Logique, "
|
"SELECT Cle_primaire, Nom, Etat_systeme, Niveau, Capacite, Logique, "
|
||||||
@@ -644,6 +646,358 @@ int main(int argc, char *argv[])
|
|||||||
return resp;
|
return resp;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// GET /mesures?type=<capteurs|reservoirs>&id=<id>&periode=<0-3>
|
||||||
|
// Historique pour le graphique de la page de détail (0=1h, 1=24h,
|
||||||
|
// 2=7j, 3=30j) - regroupement TOUJOURS calculé côté serveur (jamais de
|
||||||
|
// donnée brute non agrégée transmise, cf §20.6/§21.6 synthèse).
|
||||||
|
// Capteurs : id_variable résolu depuis la config de l'élément lui-même.
|
||||||
|
// Réservoirs : id_variable résolu par CODE FIXE ("Res_niveau" dans
|
||||||
|
// TB_Variables) - le niveau est un concept universel, pas une config
|
||||||
|
// propre à chaque réservoir comme pour un capteur.
|
||||||
|
// Circuits/batteries (sélection de variable côté client) à venir.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
server.route("/mesures", [](const QHttpServerRequest &request) {
|
||||||
|
QUrlQuery query(request.query());
|
||||||
|
const QString type = query.queryItemValue("type");
|
||||||
|
const QString idStr = query.queryItemValue("id");
|
||||||
|
const QString periodeStr = query.queryItemValue("periode");
|
||||||
|
|
||||||
|
if (type != "capteurs" && type != "reservoirs" && type != "circuits" && type != "batteries") {
|
||||||
|
QJsonObject err;
|
||||||
|
err["status"] = "erreur";
|
||||||
|
err["message"] = "type non supporté pour l'instant (capteurs, reservoirs, circuits, batteries)";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::NotImplemented));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool okId = false, okPeriode = false;
|
||||||
|
const int id = idStr.toInt(&okId);
|
||||||
|
const int periode = periodeStr.toInt(&okPeriode);
|
||||||
|
if (!okId || !okPeriode || periode < 0 || periode > 3) {
|
||||||
|
QJsonObject err;
|
||||||
|
err["status"] = "erreur";
|
||||||
|
err["message"] = "paramètres 'id'/'periode' manquants ou invalides (periode: 0=1h, 1=24h, 2=7j, 3=30j)";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::BadRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Durée de chaque période, en secondes :
|
||||||
|
static const qint64 DUREES_PERIODE[4] = { 3600, 86400, 604800, 2592000 }; // 1h, 24h, 7j, 30j
|
||||||
|
const qint64 dureeSecondes = DUREES_PERIODE[periode];
|
||||||
|
|
||||||
|
const QDateTime dateFin = QDateTime::currentDateTime();
|
||||||
|
const QDateTime dateDebut = dateFin.addSecs(-dureeSecondes);
|
||||||
|
|
||||||
|
// Regroupement calculé pour viser un nombre de points raisonnable à
|
||||||
|
// l'affichage, quelle que soit la période demandée - valeur à ajuster
|
||||||
|
// visuellement une fois le graphique réellement affiché (pas un
|
||||||
|
// chiffre figé arbitrairement bas, cf discussion §20.6).
|
||||||
|
static const int POINTS_CIBLE = 200;
|
||||||
|
const int regroupement = qMax(1, (int)(dureeSecondes / POINTS_CIBLE));
|
||||||
|
|
||||||
|
// Résolution de l'id_variable, de la précision d'affichage, et du nom
|
||||||
|
// de table de mesures - selon le domaine :
|
||||||
|
int idVariable = 0;
|
||||||
|
int precision = 1;
|
||||||
|
QString nomTable;
|
||||||
|
QString variableCircuit; // Utilisée aussi plus loin (valeur actuelle) si type == "circuits"
|
||||||
|
|
||||||
|
if (type == "capteurs") {
|
||||||
|
nomTable = "TB_Mesures_capteurs";
|
||||||
|
QSqlQuery qc;
|
||||||
|
qc.prepare("SELECT ID_Variable, Precision_affichage_mesures FROM TB_Capteurs WHERE Cle_primaire=:id");
|
||||||
|
qc.bindValue(":id", id);
|
||||||
|
qc.exec();
|
||||||
|
if (!qc.next()) {
|
||||||
|
QJsonObject err;
|
||||||
|
err["status"] = "erreur";
|
||||||
|
err["message"] = "capteur introuvable";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::NotFound));
|
||||||
|
}
|
||||||
|
idVariable = qc.value(0).toInt();
|
||||||
|
precision = qc.value(1).toInt();
|
||||||
|
} else if (type == "reservoirs") {
|
||||||
|
nomTable = "TB_Mesures_reservoirs";
|
||||||
|
|
||||||
|
QSqlQuery qExiste;
|
||||||
|
qExiste.prepare("SELECT Cle_primaire FROM TB_Reservoirs WHERE Cle_primaire=:id");
|
||||||
|
qExiste.bindValue(":id", id);
|
||||||
|
qExiste.exec();
|
||||||
|
if (!qExiste.next()) {
|
||||||
|
QJsonObject err;
|
||||||
|
err["status"] = "erreur";
|
||||||
|
err["message"] = "réservoir introuvable";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::NotFound));
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlQuery qv;
|
||||||
|
qv.prepare("SELECT Cle_primaire FROM TB_Variables WHERE Code='Res_niveau'");
|
||||||
|
qv.exec();
|
||||||
|
if (!qv.next()) {
|
||||||
|
QJsonObject err;
|
||||||
|
err["status"] = "erreur";
|
||||||
|
err["message"] = "configuration serveur incomplète (variable 'Res_niveau' introuvable)";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::InternalServerError));
|
||||||
|
}
|
||||||
|
idVariable = qv.value(0).toInt();
|
||||||
|
// Pas de colonne de précision dédiée pour les réservoirs (à la
|
||||||
|
// différence des capteurs) - un pourcentage à 1 décimale est une
|
||||||
|
// valeur par défaut raisonnable.
|
||||||
|
precision = 1;
|
||||||
|
} else if (type == "circuits") {
|
||||||
|
nomTable = "TB_Mesures_circuits";
|
||||||
|
|
||||||
|
// Circuits : le client choisit QUELLE variable tracer (contrairement
|
||||||
|
// à réservoirs/capteurs, à valeur fixe) :
|
||||||
|
variableCircuit = query.queryItemValue("variable");
|
||||||
|
if (variableCircuit != "consigne" && variableCircuit != "tension"
|
||||||
|
&& variableCircuit != "intensite" && variableCircuit != "frequence") {
|
||||||
|
QJsonObject err;
|
||||||
|
err["status"] = "erreur";
|
||||||
|
err["message"] = "paramètre 'variable' manquant ou invalide (consigne, tension, intensite, frequence)";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::BadRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Résolution du type de module du circuit - nécessaire car SP4AC
|
||||||
|
// utilise "tension_secteur" au lieu de "tension", et la précision
|
||||||
|
// d'affichage diffère selon le type de carte (cf circuit.qml natif) :
|
||||||
|
QSqlQuery qMod;
|
||||||
|
qMod.prepare(
|
||||||
|
"SELECT (SELECT Type FROM TB_Modules_config WHERE Adresse_k_bus=TB_Circuits.Adresse_module) "
|
||||||
|
"FROM TB_Circuits WHERE Cle_primaire=:id"
|
||||||
|
);
|
||||||
|
qMod.bindValue(":id", id);
|
||||||
|
qMod.exec();
|
||||||
|
if (!qMod.next()) {
|
||||||
|
QJsonObject err;
|
||||||
|
err["status"] = "erreur";
|
||||||
|
err["message"] = "circuit introuvable";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::NotFound));
|
||||||
|
}
|
||||||
|
const int typeModule = qMod.value(0).toInt();
|
||||||
|
static const int TYPE_MODULE_SP4AC = 3;
|
||||||
|
const bool estSP4AC = (typeModule == TYPE_MODULE_SP4AC);
|
||||||
|
|
||||||
|
QString codeVariable;
|
||||||
|
if (variableCircuit == "consigne") { codeVariable = "Circ_consigne"; precision = 0; }
|
||||||
|
else if (variableCircuit == "intensite") { codeVariable = "Circ_courant"; precision = estSP4AC ? 1 : 2; }
|
||||||
|
else if (variableCircuit == "frequence") { codeVariable = "Circ_frequence"; precision = 1; }
|
||||||
|
else { codeVariable = estSP4AC ? "Circ_tension_secteur" : "Circ_tension"; precision = estSP4AC ? 0 : 2; }
|
||||||
|
|
||||||
|
QSqlQuery qv2;
|
||||||
|
qv2.prepare("SELECT Cle_primaire FROM TB_Variables WHERE Code=:code");
|
||||||
|
qv2.bindValue(":code", codeVariable);
|
||||||
|
qv2.exec();
|
||||||
|
if (!qv2.next()) {
|
||||||
|
QJsonObject err;
|
||||||
|
err["status"] = "erreur";
|
||||||
|
err["message"] = "configuration serveur incomplète (variable '" + codeVariable + "' introuvable)";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::InternalServerError));
|
||||||
|
}
|
||||||
|
idVariable = qv2.value(0).toInt();
|
||||||
|
} else { // batteries
|
||||||
|
nomTable = "TB_Mesures_batteries";
|
||||||
|
|
||||||
|
// Batteries : le client choisit QUELLE variable tracer, comme pour
|
||||||
|
// les circuits. Valeur par défaut "soc" si non précisée (le vrai
|
||||||
|
// défaut natif est le comparatif d'énergie, hors périmètre ici).
|
||||||
|
variableCircuit = query.queryItemValue("variable");
|
||||||
|
if (variableCircuit.isEmpty()) variableCircuit = "soc";
|
||||||
|
|
||||||
|
static const QMap<QString, QString> CODES_BATTERIE = {
|
||||||
|
{"soc", "Batt_soc"}, {"tension", "Batt_tension"}, {"temperature", "Batt_temperature"},
|
||||||
|
{"courant_differentiel", "Batt_courant_differentiel"}, {"courant_charge", "Batt_courant_charge"},
|
||||||
|
{"courant_decharge", "Batt_courant_decharge"}, {"soh", "Batt_soh"}, {"autonomie", "Batt_autonomie"}
|
||||||
|
};
|
||||||
|
static const QMap<QString, int> PRECISIONS_BATTERIE = {
|
||||||
|
{"soc", 1}, {"tension", 2}, {"temperature", 1}, {"courant_differentiel", 2},
|
||||||
|
{"courant_charge", 2}, {"courant_decharge", 2}, {"soh", 1}, {"autonomie", 1}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!CODES_BATTERIE.contains(variableCircuit)) {
|
||||||
|
QJsonObject err;
|
||||||
|
err["status"] = "erreur";
|
||||||
|
err["message"] = "paramètre 'variable' invalide (soc, tension, temperature, courant_differentiel, courant_charge, courant_decharge, soh, autonomie)";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::BadRequest));
|
||||||
|
}
|
||||||
|
precision = PRECISIONS_BATTERIE.value(variableCircuit);
|
||||||
|
|
||||||
|
QSqlQuery qv3;
|
||||||
|
qv3.prepare("SELECT Cle_primaire FROM TB_Variables WHERE Code=:code");
|
||||||
|
qv3.bindValue(":code", CODES_BATTERIE.value(variableCircuit));
|
||||||
|
qv3.exec();
|
||||||
|
if (!qv3.next()) {
|
||||||
|
QJsonObject err;
|
||||||
|
err["status"] = "erreur";
|
||||||
|
err["message"] = "configuration serveur incomplète (variable '" + CODES_BATTERIE.value(variableCircuit) + "' introuvable)";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::InternalServerError));
|
||||||
|
}
|
||||||
|
idVariable = qv3.value(0).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString uniteBrute;
|
||||||
|
int typeUnite = 0;
|
||||||
|
{
|
||||||
|
QSqlQuery qv;
|
||||||
|
qv.prepare("SELECT Unite, Conversion_unite FROM TB_Variables WHERE Cle_primaire=:id");
|
||||||
|
qv.bindValue(":id", idVariable);
|
||||||
|
qv.exec();
|
||||||
|
if (qv.next()) {
|
||||||
|
uniteBrute = qv.value(0).toString();
|
||||||
|
typeUnite = qv.value(1).toInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int uniteVolume = 0, uniteTemperature = 0;
|
||||||
|
{
|
||||||
|
QSqlQuery q1("SELECT Valeur FROM TB_Configuration WHERE Nom='Unite_volume'");
|
||||||
|
if (q1.next()) uniteVolume = q1.value(0).toInt();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
QSqlQuery q2("SELECT Valeur FROM TB_Configuration WHERE Nom='Unite_temperature'");
|
||||||
|
if (q2.next()) uniteTemperature = q2.value(0).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString symboleUnite = uniteBrute;
|
||||||
|
if (typeUnite == TYPE_UNITE_VOLUME) symboleUnite = symbole_unite_volume(uniteVolume);
|
||||||
|
else if (typeUnite == TYPE_UNITE_TEMPERATURE) symboleUnite = symbole_unite_temperature(uniteTemperature);
|
||||||
|
|
||||||
|
// Fonction de conversion, réutilisée à plusieurs endroits ci-dessous :
|
||||||
|
auto convertir = [&](double brut) {
|
||||||
|
if (typeUnite == TYPE_UNITE_VOLUME) return conversion_volume(brut, uniteVolume);
|
||||||
|
if (typeUnite == TYPE_UNITE_TEMPERATURE) return conversion_temperature(brut, uniteTemperature);
|
||||||
|
return brut;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Valeur actuelle RÉELLE (TB_Capteurs.Valeur ou TB_Reservoirs.Niveau,
|
||||||
|
// toujours à jour) - PAS extrapolée depuis le dernier point de
|
||||||
|
// l'historique, qui peut être absent (aucune mesure récente) ou vieux
|
||||||
|
// de plusieurs minutes (valeur moyennée par bucket) :
|
||||||
|
QSqlQuery qActuel;
|
||||||
|
if (type == "capteurs") {
|
||||||
|
qActuel.prepare("SELECT Valeur FROM TB_Capteurs WHERE Cle_primaire=:id");
|
||||||
|
} else if (type == "reservoirs") {
|
||||||
|
qActuel.prepare("SELECT Niveau FROM TB_Reservoirs WHERE Cle_primaire=:id");
|
||||||
|
} else if (type == "circuits") {
|
||||||
|
QString colonne;
|
||||||
|
if (variableCircuit == "consigne") colonne = "Consigne";
|
||||||
|
else if (variableCircuit == "intensite") colonne = "Intensite";
|
||||||
|
else if (variableCircuit == "frequence") colonne = "Frequence";
|
||||||
|
else colonne = "Tension";
|
||||||
|
qActuel.prepare(QString("SELECT %1 FROM TB_Circuits WHERE Cle_primaire=:id").arg(colonne));
|
||||||
|
} else { // batteries
|
||||||
|
static const QMap<QString, QString> COLONNES_BATTERIE = {
|
||||||
|
{"soc", "Soc"}, {"tension", "Tension"}, {"temperature", "Temperature"},
|
||||||
|
{"courant_differentiel", "Courant_differentiel"}, {"courant_charge", "Courant_charge"},
|
||||||
|
{"courant_decharge", "Courant_decharge"}, {"soh", "Soh"}, {"autonomie", "Autonomie"}
|
||||||
|
};
|
||||||
|
qActuel.prepare(QString("SELECT %1 FROM TB_Batteries WHERE Cle_primaire=:id").arg(COLONNES_BATTERIE.value(variableCircuit)));
|
||||||
|
}
|
||||||
|
qActuel.bindValue(":id", id);
|
||||||
|
qActuel.exec();
|
||||||
|
double valeurActuelle = 0;
|
||||||
|
if (qActuel.next()) valeurActuelle = convertir(qActuel.value(0).toDouble());
|
||||||
|
|
||||||
|
// Point de départ synthétique : dernière valeur connue AVANT le début
|
||||||
|
// de la période, reportée exactement à la date de début - assure que
|
||||||
|
// la courbe reflète l'état réel dès le premier instant demandé, même
|
||||||
|
// si aucune mesure n'a été enregistrée DURANT la période elle-même
|
||||||
|
// (élément en mode "enregistrement sur changement" - une valeur
|
||||||
|
// stable ne crée aucune nouvelle ligne dans la table de mesures).
|
||||||
|
// Logique identique à celle déjà présente dans tb_mesures.cpp natif.
|
||||||
|
QJsonArray points;
|
||||||
|
double minVal = 0, maxVal = 0, sommeVal = 0;
|
||||||
|
qint64 minT = 0, maxT = 0;
|
||||||
|
int nb = 0;
|
||||||
|
double derniereValeurConnue = 0;
|
||||||
|
bool auMoinsUnePoint = false;
|
||||||
|
|
||||||
|
{
|
||||||
|
QSqlQuery qAvant;
|
||||||
|
qAvant.prepare(
|
||||||
|
QString("SELECT Valeur FROM %1 WHERE ID=:id AND ID_Variable=:idVariable "
|
||||||
|
"AND Date<=:debut AND Valeur IS NOT NULL ORDER BY Date DESC LIMIT 1").arg(nomTable)
|
||||||
|
);
|
||||||
|
qAvant.bindValue(":id", id);
|
||||||
|
qAvant.bindValue(":idVariable", idVariable);
|
||||||
|
qAvant.bindValue(":debut", dateDebut.toString("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
qAvant.exec();
|
||||||
|
if (qAvant.next()) {
|
||||||
|
const double v = convertir(qAvant.value(0).toDouble());
|
||||||
|
const qint64 t = dateDebut.toMSecsSinceEpoch();
|
||||||
|
QJsonObject pt; pt["t"] = t; pt["v"] = v;
|
||||||
|
points.append(pt);
|
||||||
|
minVal = maxVal = v;
|
||||||
|
minT = maxT = t;
|
||||||
|
sommeVal = v;
|
||||||
|
nb = 1;
|
||||||
|
derniereValeurConnue = v;
|
||||||
|
auMoinsUnePoint = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupération des points RÉELS de la période, regroupés côté SQL
|
||||||
|
// (moyenne par bucket) - même principe que la branche "barres
|
||||||
|
// d'énergie" déjà validée pour les circuits (§20.6), généralisé ici
|
||||||
|
// à toute courbe de mesure :
|
||||||
|
QSqlQuery qm;
|
||||||
|
qm.prepare(
|
||||||
|
QString("SELECT Date, AVG(Valeur) FROM %1 "
|
||||||
|
"WHERE ID=:id AND ID_Variable=:idVariable AND Date>:debut AND Date<=:fin AND Valeur IS NOT NULL "
|
||||||
|
"GROUP BY (UNIX_TIMESTAMP(Date) DIV :regroupement) ORDER BY Date").arg(nomTable)
|
||||||
|
);
|
||||||
|
qm.bindValue(":id", id);
|
||||||
|
qm.bindValue(":idVariable", idVariable);
|
||||||
|
qm.bindValue(":debut", dateDebut.toString("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
qm.bindValue(":fin", dateFin.toString("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
qm.bindValue(":regroupement", regroupement);
|
||||||
|
qm.exec();
|
||||||
|
|
||||||
|
while (qm.next()) {
|
||||||
|
const QDateTime dt = qm.value(0).toDateTime();
|
||||||
|
const double valeurConvertie = convertir(qm.value(1).toDouble());
|
||||||
|
const qint64 t = dt.toMSecsSinceEpoch();
|
||||||
|
|
||||||
|
QJsonObject pt;
|
||||||
|
pt["t"] = t;
|
||||||
|
pt["v"] = valeurConvertie;
|
||||||
|
points.append(pt);
|
||||||
|
|
||||||
|
if (nb == 0 || valeurConvertie < minVal) { minVal = valeurConvertie; minT = t; }
|
||||||
|
if (nb == 0 || valeurConvertie > maxVal) { maxVal = valeurConvertie; maxT = t; }
|
||||||
|
sommeVal += valeurConvertie;
|
||||||
|
nb++;
|
||||||
|
derniereValeurConnue = valeurConvertie;
|
||||||
|
auMoinsUnePoint = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point de fin synthétique : reporte la dernière valeur connue
|
||||||
|
// jusqu'à "maintenant", pour que la courbe occupe toute la période -
|
||||||
|
// purement visuel, ne modifie pas les statistiques min/max/moyenne
|
||||||
|
// (ce n'est pas une nouvelle mesure, juste la continuité visuelle de
|
||||||
|
// la précédente) :
|
||||||
|
if (auMoinsUnePoint) {
|
||||||
|
QJsonObject ptFin;
|
||||||
|
ptFin["t"] = dateFin.toMSecsSinceEpoch();
|
||||||
|
ptFin["v"] = derniereValeurConnue;
|
||||||
|
points.append(ptFin);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject reponse;
|
||||||
|
reponse["status"] = "ok";
|
||||||
|
reponse["points"] = points;
|
||||||
|
reponse["unite"] = symboleUnite;
|
||||||
|
reponse["precision"] = precision;
|
||||||
|
reponse["valeur_actuelle"] = valeurActuelle;
|
||||||
|
if (nb > 0) {
|
||||||
|
QJsonObject minObj; minObj["v"] = minVal; minObj["t"] = minT;
|
||||||
|
QJsonObject maxObj; maxObj["v"] = maxVal; maxObj["t"] = maxT;
|
||||||
|
reponse["min"] = minObj;
|
||||||
|
reponse["max"] = maxObj;
|
||||||
|
reponse["moyenne"] = sommeVal / nb;
|
||||||
|
}
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(reponse).toJson()));
|
||||||
|
});
|
||||||
|
|
||||||
auto tcpServer = new QTcpServer();
|
auto tcpServer = new QTcpServer();
|
||||||
if (!tcpServer->listen(QHostAddress::Any, 8080) || !server.bind(tcpServer)) {
|
if (!tcpServer->listen(QHostAddress::Any, 8080) || !server.bind(tcpServer)) {
|
||||||
qCritical() << "Impossible de démarrer le serveur HTTP sur le port 8080.";
|
qCritical() << "Impossible de démarrer le serveur HTTP sur le port 8080.";
|
||||||
|
|||||||
Binary file not shown.
+656
@@ -0,0 +1,656 @@
|
|||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QHttpServer>
|
||||||
|
#include <QHttpHeaders>
|
||||||
|
#include <QTcpServer>
|
||||||
|
#include <QSqlDatabase>
|
||||||
|
#include <QSqlQuery>
|
||||||
|
#include <QSqlError>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QUrlQuery>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// shipheart_api - Phase 4 : première tranche verticale, domaine "circuit"
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Deux endpoints réels, remplaçant le spike (/ping conservé comme sonde de vie
|
||||||
|
// générique, /test-db retiré car superflu désormais) :
|
||||||
|
// - GET /elements/familles?type=circuit
|
||||||
|
// - GET /elements?type=circuit&famille=<id>
|
||||||
|
// Logique et requêtes SQL reprises à l'identique de home.qml (init_circuits(),
|
||||||
|
// mise_a_jour_onglets_circuits(), mise_a_jour_circuits() - cf synthèse §20.4)
|
||||||
|
// et des constantes documentées dans le schéma (TB_Circuits.Etat, cf §20).
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Types de familles (cf TB_Familles.Type_source / TB_Events.Type_source) :
|
||||||
|
static const int TYPE_SOURCE_CIRCUIT = 1;
|
||||||
|
static const int TYPE_SOURCE_CAPTEUR = 2;
|
||||||
|
static const int TYPE_SOURCE_RESERVOIR = 3;
|
||||||
|
static const int TYPE_SOURCE_BATTERIE = 4;
|
||||||
|
|
||||||
|
// États circuit (cf commentaire colonne TB_Circuits.Etat dans le schéma) :
|
||||||
|
static const int ETAT_CIRCUIT_OK = 0;
|
||||||
|
static const int ETAT_CIRCUIT_LIMITATION = 1;
|
||||||
|
static const int ETAT_CIRCUIT_PROTECTION = 2;
|
||||||
|
static const int ETAT_CIRCUIT_DISJONCTE = 3;
|
||||||
|
static const int ETAT_CIRCUIT_ABSENCE_TERRE = 8;
|
||||||
|
static const int ETAT_CIRCUIT_ABSENCE_MASSE = 9;
|
||||||
|
static const int ETAT_COMMANDE_CARTE_OK = 0;
|
||||||
|
|
||||||
|
// Types d'entrées capteur (cf commentaire colonne TB_Capteurs.Type_entrees) :
|
||||||
|
static const int TYPE_ENTREES_AUX = 1;
|
||||||
|
static const int TYPE_ENTREES_NMEA = 7;
|
||||||
|
static const int TYPE_ENTREES_CALCULEE_BOOLEEN = 20;
|
||||||
|
|
||||||
|
// Types de conversion d'unité (cf commentaire colonne TB_Variables.Conversion_unite,
|
||||||
|
// confirmé identique à shipheart_type_unite_* dans main.qml) :
|
||||||
|
static const int TYPE_UNITE_VOLUME = 1;
|
||||||
|
static const int TYPE_UNITE_TEMPERATURE = 2;
|
||||||
|
static const int TYPE_UNITE_LATITUDE = 3;
|
||||||
|
static const int TYPE_UNITE_LONGITUDE = 4;
|
||||||
|
|
||||||
|
// Unités cibles (cf main.qml, shipheart_unite_volume_*/shipheart_unite_temperature_*) :
|
||||||
|
static const int UNITE_VOLUME_LITRES = 0;
|
||||||
|
static const int UNITE_VOLUME_US_GALLONS = 1;
|
||||||
|
static const int UNITE_VOLUME_IMP_GALLONS = 2;
|
||||||
|
static const int UNITE_TEMPERATURE_CELSIUS = 0;
|
||||||
|
static const int UNITE_TEMPERATURE_FAHRENHEIT = 1;
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Conversions - traduction fidèle de fonctions.js (conversion_volume(),
|
||||||
|
// conversion_temperature(), chaine_latitude(), chaine_longitude()) et de
|
||||||
|
// fonctions_localisation.js (symbole_unite_volume(), symbole_unite_temperature()).
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
static double conversion_volume(double val, int unite)
|
||||||
|
{
|
||||||
|
switch (unite) {
|
||||||
|
case UNITE_VOLUME_US_GALLONS: return val / 3.785411784;
|
||||||
|
case UNITE_VOLUME_IMP_GALLONS: return val / 4.546090;
|
||||||
|
case UNITE_VOLUME_LITRES:
|
||||||
|
default: return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static double conversion_temperature(double val, int unite)
|
||||||
|
{
|
||||||
|
switch (unite) {
|
||||||
|
case UNITE_TEMPERATURE_FAHRENHEIT: return (val * 1.8) + 32.0;
|
||||||
|
case UNITE_TEMPERATURE_CELSIUS:
|
||||||
|
default: return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString symbole_unite_volume(int unite)
|
||||||
|
{
|
||||||
|
switch (unite) {
|
||||||
|
case UNITE_VOLUME_US_GALLONS: return QStringLiteral("US Ga");
|
||||||
|
case UNITE_VOLUME_IMP_GALLONS: return QStringLiteral("Imp. Ga");
|
||||||
|
case UNITE_VOLUME_LITRES:
|
||||||
|
default: return QStringLiteral("lit.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString symbole_unite_temperature(int unite)
|
||||||
|
{
|
||||||
|
return (unite == UNITE_TEMPERATURE_FAHRENHEIT) ? QStringLiteral("°F") : QStringLiteral("°C");
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString chaine_latitude(double lat)
|
||||||
|
{
|
||||||
|
const int deg = static_cast<int>(lat);
|
||||||
|
const double minutes = (lat - deg) * 60.0;
|
||||||
|
const QString suffixe = (lat >= 0) ? QStringLiteral("N") : QStringLiteral("S");
|
||||||
|
return QString("%1°%2' %3").arg(deg).arg(minutes, 0, 'f', 3).arg(suffixe);
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString chaine_longitude(double lon)
|
||||||
|
{
|
||||||
|
const int deg = static_cast<int>(lon);
|
||||||
|
const double minutes = (lon - deg) * 60.0;
|
||||||
|
const QString suffixe = (lon >= 0) ? QStringLiteral("E") : QStringLiteral("W");
|
||||||
|
return QString("%1°%2' %3").arg(deg).arg(minutes, 0, 'f', 3).arg(suffixe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Ajoute l'en-tête CORS à une réponse - indispensable puisque le client WASM
|
||||||
|
// (servi par nginx sur un port distinct) et cette API (port 8080) constituent
|
||||||
|
// deux origines différentes du point de vue du navigateur. Sans cet en-tête,
|
||||||
|
// les requêtes aboutissent bien côté serveur (visibles dans les logs) mais
|
||||||
|
// leur réponse est bloquée silencieusement côté client.
|
||||||
|
// TODO : restreindre à l'origine réelle du client en production plutôt que
|
||||||
|
// '*', une fois l'authentification (cf "Architecture cible") en place.
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
static QHttpServerResponse withCors(QHttpServerResponse &&resp)
|
||||||
|
{
|
||||||
|
QHttpHeaders headers = resp.headers();
|
||||||
|
headers.append(QHttpHeaders::WellKnownHeader::AccessControlAllowOrigin, "*");
|
||||||
|
// Empêche le navigateur de mettre en cache les réponses - sans ça, des
|
||||||
|
// requêtes identiques répétées (rafraîchissement périodique) pourraient
|
||||||
|
// recevoir une réponse mise en cache plutôt que la valeur réellement à
|
||||||
|
// jour en base.
|
||||||
|
headers.append(QHttpHeaders::WellKnownHeader::CacheControl, "no-store");
|
||||||
|
resp.setHeaders(std::move(headers));
|
||||||
|
return std::move(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
QCoreApplication app(argc, argv);
|
||||||
|
|
||||||
|
// --- Connexion MariaDB locale ---
|
||||||
|
QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL");
|
||||||
|
db.setHostName("localhost");
|
||||||
|
db.setUserName("koriolan");
|
||||||
|
db.setPassword("Mp7Hgf12bV9zm");
|
||||||
|
db.setDatabaseName("shipheart");
|
||||||
|
|
||||||
|
if (!db.open()) {
|
||||||
|
qCritical() << "Connexion MariaDB échouée :" << db.lastError().text();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
qInfo() << "Connexion MariaDB réussie.";
|
||||||
|
|
||||||
|
QHttpServer server;
|
||||||
|
|
||||||
|
// Sonde de vie générique, sans BDD :
|
||||||
|
server.route("/ping", []() {
|
||||||
|
QJsonObject obj;
|
||||||
|
obj["status"] = "ok";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(obj).toJson()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// GET /elements/familles?type=circuit
|
||||||
|
// Liste des familles avec état d'alarme agrégé (fusionne les deux
|
||||||
|
// requêtes onglets de mise_a_jour_onglets_circuits()).
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
server.route("/elements/familles", [](const QHttpServerRequest &request) {
|
||||||
|
QUrlQuery query(request.query());
|
||||||
|
const QString type = query.queryItemValue("type");
|
||||||
|
|
||||||
|
int typeSource;
|
||||||
|
if (type == "circuits") typeSource = TYPE_SOURCE_CIRCUIT;
|
||||||
|
else if (type == "batteries") typeSource = TYPE_SOURCE_BATTERIE;
|
||||||
|
else if (type == "capteurs") typeSource = TYPE_SOURCE_CAPTEUR;
|
||||||
|
else if (type == "reservoirs") typeSource = TYPE_SOURCE_RESERVOIR;
|
||||||
|
else {
|
||||||
|
QJsonObject err;
|
||||||
|
err["status"] = "erreur";
|
||||||
|
err["message"] = "type non supporté (circuit, batterie, capteur, reservoir)";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::NotImplemented));
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray familles;
|
||||||
|
QSqlQuery q1(QString("SELECT Cle_primaire, Nom FROM TB_Familles WHERE Type_source=%1 ORDER BY Priorite_affichage").arg(typeSource));
|
||||||
|
|
||||||
|
while (q1.next()) {
|
||||||
|
const int idFamille = q1.value(0).toInt();
|
||||||
|
const QString nom = q1.value(1).toString();
|
||||||
|
int etat = 0;
|
||||||
|
|
||||||
|
if (type == "circuits") {
|
||||||
|
// Alarmes (Events) liées à cette famille :
|
||||||
|
QSqlQuery q2;
|
||||||
|
q2.prepare("SELECT COUNT(*) FROM TB_Events WHERE Type_source=:type AND Validite=1 AND Alarme_source>0 "
|
||||||
|
"AND Etat_test=1 AND ID_Source IN (SELECT Cle_primaire FROM TB_Circuits WHERE ID_Famille=:famille)");
|
||||||
|
q2.bindValue(":type", typeSource);
|
||||||
|
q2.bindValue(":famille", idFamille);
|
||||||
|
q2.exec();
|
||||||
|
if (q2.next() && q2.value(0).toInt() > 0) etat = 1;
|
||||||
|
|
||||||
|
// État agrégé des circuits de la famille :
|
||||||
|
QSqlQuery q3;
|
||||||
|
q3.prepare("SELECT Etat, Etat_commande_carte, Etat_systeme FROM TB_Circuits "
|
||||||
|
"WHERE ID_Famille=:famille AND Priorite_affichage_accueil>0");
|
||||||
|
q3.bindValue(":famille", idFamille);
|
||||||
|
q3.exec();
|
||||||
|
while (q3.next()) {
|
||||||
|
const int etatCircuit = q3.value(0).toInt();
|
||||||
|
const int etatCommandeCarte = q3.value(1).toInt();
|
||||||
|
const int etatSysteme = q3.value(2).toInt();
|
||||||
|
|
||||||
|
if (etatSysteme != 0
|
||||||
|
|| etatCircuit == ETAT_CIRCUIT_LIMITATION
|
||||||
|
|| etatCircuit == ETAT_CIRCUIT_PROTECTION
|
||||||
|
|| etatCircuit == ETAT_CIRCUIT_DISJONCTE
|
||||||
|
|| etatCircuit == ETAT_CIRCUIT_ABSENCE_TERRE
|
||||||
|
|| etatCircuit == ETAT_CIRCUIT_ABSENCE_MASSE) {
|
||||||
|
etat = 2;
|
||||||
|
} else if (etat < 2 && (etatCommandeCarte != ETAT_COMMANDE_CARTE_OK || etatCircuit != ETAT_CIRCUIT_OK)) {
|
||||||
|
etat = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (type == "batteries") {
|
||||||
|
// Règle plus simple que les circuits (cf §20.4/§21.8 synthèse) :
|
||||||
|
// rouge si Etat_systeme en défaut sur une batterie de la famille,
|
||||||
|
// sinon orange si au moins une alarme (Events), sinon vert.
|
||||||
|
|
||||||
|
// Alarmes (Events) liées à cette famille :
|
||||||
|
QSqlQuery q2;
|
||||||
|
q2.prepare("SELECT COUNT(*) FROM TB_Events WHERE Type_source=:type AND Validite=1 AND Alarme_source>0 "
|
||||||
|
"AND Etat_test=1 AND ID_Source IN (SELECT Cle_primaire FROM TB_Batteries WHERE ID_Famille=:famille)");
|
||||||
|
q2.bindValue(":type", typeSource);
|
||||||
|
q2.bindValue(":famille", idFamille);
|
||||||
|
q2.exec();
|
||||||
|
if (q2.next() && q2.value(0).toInt() > 0) etat = 1;
|
||||||
|
|
||||||
|
// Etat_systeme des batteries de la famille :
|
||||||
|
QSqlQuery q3;
|
||||||
|
q3.prepare("SELECT Etat_systeme FROM TB_Batteries WHERE ID_Famille=:famille AND Priorite_affichage_accueil>0");
|
||||||
|
q3.bindValue(":famille", idFamille);
|
||||||
|
q3.exec();
|
||||||
|
while (q3.next()) {
|
||||||
|
if (q3.value(0).toInt() != 0) etat = 2;
|
||||||
|
}
|
||||||
|
} else if (type == "capteurs") {
|
||||||
|
// Période d'obsolescence NMEA (TB_Configuration) :
|
||||||
|
int periodeObsolescenceNmea = 0;
|
||||||
|
{
|
||||||
|
QSqlQuery qc("SELECT Valeur FROM TB_Configuration WHERE Nom='Periode_obsolescence_capteur_nmea'");
|
||||||
|
if (qc.next()) periodeObsolescenceNmea = qc.value(0).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alarmes (Events) liées à cette famille :
|
||||||
|
QSqlQuery q2;
|
||||||
|
q2.prepare("SELECT COUNT(*) FROM TB_Events WHERE Type_source=:type AND Validite=1 AND Alarme_source>0 "
|
||||||
|
"AND Etat_test=1 AND ID_Source IN (SELECT Cle_primaire FROM TB_Capteurs WHERE ID_Famille=:famille)");
|
||||||
|
q2.bindValue(":type", typeSource);
|
||||||
|
q2.bindValue(":famille", idFamille);
|
||||||
|
q2.exec();
|
||||||
|
if (q2.next() && q2.value(0).toInt() > 0) etat = 1;
|
||||||
|
|
||||||
|
QSqlQuery q3;
|
||||||
|
q3.prepare(
|
||||||
|
"SELECT Etat, Etat_systeme, "
|
||||||
|
"IF(Type_entrees=:typeNmea, (SELECT (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(Date_maj)) FROM TB_Nmea2000 "
|
||||||
|
"WHERE Pgn=SUBSTRING_INDEX(Entrees, ':', 1) AND Field=SUBSTRING_INDEX(Entrees, ':', -1)), 0) AS Age "
|
||||||
|
"FROM TB_Capteurs WHERE ID_Famille=:famille AND Priorite_affichage_accueil>0"
|
||||||
|
);
|
||||||
|
q3.bindValue(":typeNmea", TYPE_ENTREES_NMEA);
|
||||||
|
q3.bindValue(":famille", idFamille);
|
||||||
|
q3.exec();
|
||||||
|
while (q3.next()) {
|
||||||
|
const int etatCapteur = q3.value(0).toInt();
|
||||||
|
const int etatSysteme = q3.value(1).toInt();
|
||||||
|
const int age = q3.value(2).toInt();
|
||||||
|
|
||||||
|
if (age > periodeObsolescenceNmea && etat < 1) etat = 1;
|
||||||
|
if ((etatCapteur > 0 || etatSysteme != 0) && etat < 2) etat = 2;
|
||||||
|
}
|
||||||
|
} else if (type == "reservoirs") {
|
||||||
|
// Alarmes (Events) liées à cette famille :
|
||||||
|
QSqlQuery q2;
|
||||||
|
q2.prepare("SELECT COUNT(*) FROM TB_Events WHERE Type_source=:type AND Validite=1 AND Alarme_source>0 "
|
||||||
|
"AND Etat_test=1 AND ID_Source IN (SELECT Cle_primaire FROM TB_Reservoirs WHERE ID_Famille=:famille)");
|
||||||
|
q2.bindValue(":type", typeSource);
|
||||||
|
q2.bindValue(":famille", idFamille);
|
||||||
|
q2.exec();
|
||||||
|
if (q2.next() && q2.value(0).toInt() > 0) etat = 1;
|
||||||
|
|
||||||
|
// Défaut ordre des sondes AUX (Etat_entrees) ou Etat_systeme en défaut :
|
||||||
|
QSqlQuery q3;
|
||||||
|
q3.prepare("SELECT Etat_entrees, Etat_systeme FROM TB_Reservoirs "
|
||||||
|
"WHERE ID_Famille=:famille AND Priorite_affichage_accueil>0");
|
||||||
|
q3.bindValue(":famille", idFamille);
|
||||||
|
q3.exec();
|
||||||
|
while (q3.next()) {
|
||||||
|
const int etatEntrees = q3.value(0).toInt();
|
||||||
|
const int etatSysteme = q3.value(1).toInt();
|
||||||
|
if ((etatEntrees > 0 || etatSysteme != 0) && etat < 2) etat = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject obj;
|
||||||
|
obj["id"] = idFamille;
|
||||||
|
obj["nom"] = nom;
|
||||||
|
obj["etat"] = etat;
|
||||||
|
familles.append(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject reponse;
|
||||||
|
reponse["status"] = "ok";
|
||||||
|
reponse["familles"] = familles;
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(reponse).toJson()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// GET /elements?type=circuit&famille=<id>
|
||||||
|
// Liste des circuits de la famille - sert chargement initial ET
|
||||||
|
// rafraîchissement périodique (même requête que init_circuits() et
|
||||||
|
// mise_a_jour_circuits(), cf §20.4 : ce n'est pas deux besoins
|
||||||
|
// différents, mais un seul, appelé deux fois).
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
server.route("/elements", [](const QHttpServerRequest &request) {
|
||||||
|
QUrlQuery query(request.query());
|
||||||
|
const QString type = query.queryItemValue("type");
|
||||||
|
const QString familleStr = query.queryItemValue("famille");
|
||||||
|
|
||||||
|
if (type != "circuits" && type != "batteries" && type != "capteurs" && type != "reservoirs") {
|
||||||
|
QJsonObject err;
|
||||||
|
err["status"] = "erreur";
|
||||||
|
err["message"] = "type non supporté (circuit, batterie, capteur, reservoir)";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::NotImplemented));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
|
const int idFamille = familleStr.toInt(&ok);
|
||||||
|
if (!ok) {
|
||||||
|
QJsonObject err;
|
||||||
|
err["status"] = "erreur";
|
||||||
|
err["message"] = "paramètre 'famille' manquant ou invalide";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::BadRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray elements;
|
||||||
|
|
||||||
|
if (type == "circuits") {
|
||||||
|
QSqlQuery q;
|
||||||
|
q.prepare(
|
||||||
|
"SELECT Cle_primaire, Etat_systeme, Nom, Etat_commande_carte, Mode_commande, Etat, Consigne, Old_consigne, "
|
||||||
|
"Mode_test, Tension, Intensite, Frequence, "
|
||||||
|
"(SELECT Type FROM TB_Modules_config WHERE Adresse_k_bus=TB_Circuits.Adresse_module) AS type_circuit, "
|
||||||
|
"No_circuit, "
|
||||||
|
"(SELECT COUNT(*) FROM TB_Events WHERE Validite=1 AND Visibilite=1 AND "
|
||||||
|
"(Action_circuits LIKE CONCAT(TB_Circuits.Cle_primaire,'/%') OR Action_circuits LIKE CONCAT('%;', TB_Circuits.Cle_primaire,'/%'))) AS nb_events, "
|
||||||
|
"Validation_cdes_events, "
|
||||||
|
"(SELECT COUNT(*) FROM TB_Events WHERE Validite=1 AND Type_source=:type AND Alarme_source>0 AND Etat_test=1 "
|
||||||
|
"AND ID_Source=TB_Circuits.Cle_primaire) AS Nb_alarmes "
|
||||||
|
"FROM TB_Circuits WHERE Priorite_affichage_accueil>0 AND ID_Famille=:famille "
|
||||||
|
"ORDER BY Priorite_affichage_accueil, Cle_primaire"
|
||||||
|
);
|
||||||
|
q.bindValue(":type", TYPE_SOURCE_CIRCUIT);
|
||||||
|
q.bindValue(":famille", idFamille);
|
||||||
|
q.exec();
|
||||||
|
|
||||||
|
while (q.next()) {
|
||||||
|
QJsonObject obj;
|
||||||
|
obj["id"] = q.value(0).toInt();
|
||||||
|
obj["etat_systeme"] = q.value(1).toInt();
|
||||||
|
obj["nom"] = q.value(2).toString();
|
||||||
|
obj["etat_commande_carte"] = q.value(3).toInt();
|
||||||
|
obj["mode_commande"] = q.value(4).toInt();
|
||||||
|
obj["etat"] = q.value(5).toInt();
|
||||||
|
obj["consigne"] = q.value(6).toInt();
|
||||||
|
obj["old_consigne"] = q.value(7).toInt();
|
||||||
|
obj["mode_test"] = q.value(8).toInt();
|
||||||
|
obj["tension"] = q.value(9).toDouble();
|
||||||
|
obj["intensite"] = q.value(10).toDouble();
|
||||||
|
obj["frequence"] = q.value(11).toDouble();
|
||||||
|
obj["type_circuit"] = q.value(12).toInt();
|
||||||
|
obj["no_circuit"] = q.value(13).toInt();
|
||||||
|
obj["validation_cdes_events"] = (q.value(14).toInt() == 0) ? -1 : q.value(15).toInt();
|
||||||
|
obj["etat_events"] = q.value(16).toInt();
|
||||||
|
elements.append(obj);
|
||||||
|
}
|
||||||
|
} else if (type == "batteries") {
|
||||||
|
// Rappel : le champ "etat" retourné ici est le NOMBRE D'ALARMES,
|
||||||
|
// pas un code d'état comme pour les circuits - fidèle au modèle
|
||||||
|
// natif existant (modele_batteries), pas une incohérence introduite ici.
|
||||||
|
QSqlQuery q;
|
||||||
|
q.prepare(
|
||||||
|
"SELECT Cle_primaire, Etat_systeme, Nom, Type, Soc, Tension, Tension_nominale, "
|
||||||
|
"(SELECT COUNT(*) FROM TB_Events WHERE Validite=1 AND Type_source=:type AND Alarme_source>0 AND Etat_test=1 "
|
||||||
|
"AND ID_Source=TB_Batteries.Cle_primaire) AS Nb_alarmes "
|
||||||
|
"FROM TB_Batteries WHERE ID_Famille=:famille "
|
||||||
|
"ORDER BY Priorite_affichage_accueil"
|
||||||
|
);
|
||||||
|
q.bindValue(":type", TYPE_SOURCE_BATTERIE);
|
||||||
|
q.bindValue(":famille", idFamille);
|
||||||
|
q.exec();
|
||||||
|
|
||||||
|
while (q.next()) {
|
||||||
|
QJsonObject obj;
|
||||||
|
obj["id"] = q.value(0).toInt();
|
||||||
|
obj["etat_systeme"] = q.value(1).toInt();
|
||||||
|
obj["nom"] = q.value(2).toString();
|
||||||
|
obj["type_batterie"] = q.value(3).toInt();
|
||||||
|
obj["niveau"] = q.value(4).toDouble();
|
||||||
|
obj["tension"] = q.value(5).toDouble();
|
||||||
|
obj["tension_nominale"] = q.value(6).toDouble();
|
||||||
|
obj["etat"] = q.value(7).toInt();
|
||||||
|
elements.append(obj);
|
||||||
|
}
|
||||||
|
} else if (type == "capteurs") {
|
||||||
|
// Préférences d'unité du bateau (TB_Configuration) :
|
||||||
|
int uniteVolume = 0, uniteTemperature = 0, periodeObsolescenceNmea = 0;
|
||||||
|
{
|
||||||
|
QSqlQuery qc("SELECT Valeur FROM TB_Configuration WHERE Nom='Unite_volume'");
|
||||||
|
if (qc.next()) uniteVolume = qc.value(0).toInt();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
QSqlQuery qc("SELECT Valeur FROM TB_Configuration WHERE Nom='Unite_temperature'");
|
||||||
|
if (qc.next()) uniteTemperature = qc.value(0).toInt();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
QSqlQuery qc("SELECT Valeur FROM TB_Configuration WHERE Nom='Periode_obsolescence_capteur_nmea'");
|
||||||
|
if (qc.next()) periodeObsolescenceNmea = qc.value(0).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlQuery q;
|
||||||
|
q.prepare(
|
||||||
|
"SELECT Cle_primaire, Etat_systeme, Nom, Type_entrees, Entrees, ID_Variable, Etat, "
|
||||||
|
"(SELECT COUNT(*) FROM TB_Events WHERE Validite=1 AND Type_source=:typeSource AND Alarme_source>0 AND Etat_test=1 "
|
||||||
|
"AND ID_Source=TB_Capteurs.Cle_primaire) AS Nb_alarmes, "
|
||||||
|
"Valeur, Precision_affichage_accueil, Liste_valeurs_textes, "
|
||||||
|
"IF(Type_entrees=:typeNmea, (SELECT (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(Date_maj)) FROM TB_Nmea2000 "
|
||||||
|
"WHERE Pgn=SUBSTRING_INDEX(Entrees, ':', 1) AND Field=SUBSTRING_INDEX(Entrees, ':', -1)), 0) AS Age "
|
||||||
|
"FROM TB_Capteurs WHERE ID_Famille=:famille AND Priorite_affichage_accueil>0 "
|
||||||
|
"ORDER BY Priorite_affichage_accueil, Nom"
|
||||||
|
);
|
||||||
|
q.bindValue(":typeSource", TYPE_SOURCE_CAPTEUR);
|
||||||
|
q.bindValue(":typeNmea", TYPE_ENTREES_NMEA);
|
||||||
|
q.bindValue(":famille", idFamille);
|
||||||
|
q.exec();
|
||||||
|
|
||||||
|
while (q.next()) {
|
||||||
|
const int id = q.value(0).toInt();
|
||||||
|
const int etatSysteme = q.value(1).toInt();
|
||||||
|
const QString nom = q.value(2).toString();
|
||||||
|
const int typeEntrees = q.value(3).toInt();
|
||||||
|
const int idVariable = q.value(5).toInt();
|
||||||
|
const int etatCapteur = q.value(6).toInt();
|
||||||
|
const int nbAlarmes = q.value(7).toInt();
|
||||||
|
const double valeurBrute = q.value(8).toDouble();
|
||||||
|
const int precision = q.value(9).toInt();
|
||||||
|
const QString listeValeursTextes = q.value(10).toString();
|
||||||
|
const int age = q.value(11).toInt();
|
||||||
|
|
||||||
|
// Résolution de l'unité/conversion via TB_Variables :
|
||||||
|
QSqlQuery qv;
|
||||||
|
qv.prepare("SELECT Unite, Conversion_unite FROM TB_Variables WHERE Cle_primaire=:id");
|
||||||
|
qv.bindValue(":id", idVariable);
|
||||||
|
qv.exec();
|
||||||
|
QString uniteBrute;
|
||||||
|
int typeUnite = 0;
|
||||||
|
if (qv.next()) {
|
||||||
|
uniteBrute = qv.value(0).toString();
|
||||||
|
typeUnite = qv.value(1).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject obj;
|
||||||
|
obj["id"] = id;
|
||||||
|
obj["etat_systeme"] = etatSysteme;
|
||||||
|
obj["nom"] = nom;
|
||||||
|
obj["etat"] = etatCapteur;
|
||||||
|
obj["etat_events"] = nbAlarmes;
|
||||||
|
|
||||||
|
if (typeUnite == TYPE_UNITE_LATITUDE) {
|
||||||
|
obj["valeur"] = chaine_latitude(valeurBrute);
|
||||||
|
obj["valeur_texte"] = true;
|
||||||
|
obj["unite"] = QString();
|
||||||
|
} else if (typeUnite == TYPE_UNITE_LONGITUDE) {
|
||||||
|
obj["valeur"] = chaine_longitude(valeurBrute);
|
||||||
|
obj["valeur_texte"] = true;
|
||||||
|
obj["unite"] = QString();
|
||||||
|
} else {
|
||||||
|
// Cas des listes de valeurs/textes (entrées AUX ou booléen calculé) :
|
||||||
|
QString texteListe;
|
||||||
|
if (typeEntrees == TYPE_ENTREES_AUX || typeEntrees == TYPE_ENTREES_CALCULEE_BOOLEEN) {
|
||||||
|
const auto paires = listeValeursTextes.split(';');
|
||||||
|
for (const auto &paire : paires) {
|
||||||
|
const auto champs = paire.split('/');
|
||||||
|
if (champs.size() == 2 && champs[0].toDouble() == valeurBrute) {
|
||||||
|
texteListe = champs[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!texteListe.isEmpty()) {
|
||||||
|
obj["valeur"] = texteListe;
|
||||||
|
obj["valeur_texte"] = true;
|
||||||
|
obj["unite"] = QString();
|
||||||
|
} else {
|
||||||
|
double valeurConvertie = valeurBrute;
|
||||||
|
QString symboleUnite = uniteBrute;
|
||||||
|
if (typeUnite == TYPE_UNITE_VOLUME) {
|
||||||
|
valeurConvertie = conversion_volume(valeurBrute, uniteVolume);
|
||||||
|
symboleUnite = symbole_unite_volume(uniteVolume);
|
||||||
|
} else if (typeUnite == TYPE_UNITE_TEMPERATURE) {
|
||||||
|
valeurConvertie = conversion_temperature(valeurBrute, uniteTemperature);
|
||||||
|
symboleUnite = symbole_unite_temperature(uniteTemperature);
|
||||||
|
}
|
||||||
|
// Toujours une chaîne (même pour les nombres) - évite que QML
|
||||||
|
// ne fige le rôle 'valeur' en type numérique lors d'une famille
|
||||||
|
// visitée avant "Position" (latitude/longitude), qui tronquerait
|
||||||
|
// alors ces dernières à leur seul chiffre de tête.
|
||||||
|
obj["valeur"] = QString::number(valeurConvertie, 'f', precision);
|
||||||
|
obj["valeur_texte"] = false;
|
||||||
|
obj["unite"] = symboleUnite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj["obsolete"] = (typeEntrees == TYPE_ENTREES_NMEA && age > periodeObsolescenceNmea);
|
||||||
|
elements.append(obj);
|
||||||
|
}
|
||||||
|
} else if (type == "reservoirs") {
|
||||||
|
QSqlQuery q;
|
||||||
|
q.prepare(
|
||||||
|
"SELECT Cle_primaire, Nom, Etat_systeme, Niveau, Capacite, Logique, "
|
||||||
|
"(SELECT COUNT(*) FROM TB_Events WHERE Validite=1 AND Type_source=:type AND Alarme_source>0 AND Etat_test=1 "
|
||||||
|
"AND ID_Source=TB_Reservoirs.Cle_primaire) AS Nb_alarmes, "
|
||||||
|
"Etat_entrees "
|
||||||
|
"FROM TB_Reservoirs WHERE ID_Famille=:famille "
|
||||||
|
"ORDER BY Priorite_affichage_accueil"
|
||||||
|
);
|
||||||
|
q.bindValue(":type", TYPE_SOURCE_RESERVOIR);
|
||||||
|
q.bindValue(":famille", idFamille);
|
||||||
|
q.exec();
|
||||||
|
|
||||||
|
while (q.next()) {
|
||||||
|
const int etatSysteme = q.value(2).toInt();
|
||||||
|
const int nbAlarmes = q.value(6).toInt();
|
||||||
|
const int etatEntrees = q.value(7).toInt();
|
||||||
|
|
||||||
|
// État agrégé, même règle que le natif (mise_a_jour_reservoirs()) :
|
||||||
|
// rouge si Etat_systeme en défaut ou défaut d'ordre des sondes AUX,
|
||||||
|
// sinon orange si au moins une alarme, sinon vert.
|
||||||
|
int etat = 0;
|
||||||
|
if (etatSysteme < 0 || etatEntrees > 0) etat = 2;
|
||||||
|
else if (nbAlarmes > 0) etat = 1;
|
||||||
|
|
||||||
|
QJsonObject obj;
|
||||||
|
obj["id"] = q.value(0).toInt();
|
||||||
|
obj["nom"] = q.value(1).toString();
|
||||||
|
obj["etat_systeme"] = etatSysteme;
|
||||||
|
obj["niveau"] = q.value(3).toDouble();
|
||||||
|
obj["capacite"] = q.value(4).toDouble();
|
||||||
|
obj["logique"] = q.value(5).toInt();
|
||||||
|
obj["etat_events"] = nbAlarmes;
|
||||||
|
obj["etat_entrees"] = etatEntrees;
|
||||||
|
obj["etat"] = etat;
|
||||||
|
elements.append(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject reponse;
|
||||||
|
reponse["status"] = "ok";
|
||||||
|
reponse["elements"] = elements;
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(reponse).toJson()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// PATCH /elements/circuit/<id>
|
||||||
|
// Écriture rapide (bouton d'accueil) - PAS le formulaire de config
|
||||||
|
// complet (config_circuit.qml, avec orchestration esclave/maître,
|
||||||
|
// §20.7) qui reste un futur endpoint séparé. Reproduit fidèlement
|
||||||
|
// changement_consigne()/toggle_cdes_auto() (identiques dans circuit.qml
|
||||||
|
// et Shipheart_bouton_circuit.qml).
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
server.route("/elements/circuit/<arg>", QHttpServerRequest::Method::Patch,
|
||||||
|
[](int idCircuit, const QHttpServerRequest &request) {
|
||||||
|
const QJsonDocument body = QJsonDocument::fromJson(request.body());
|
||||||
|
if (!body.isObject()) {
|
||||||
|
QJsonObject err;
|
||||||
|
err["status"] = "erreur";
|
||||||
|
err["message"] = "corps de requête JSON invalide";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::BadRequest));
|
||||||
|
}
|
||||||
|
const QJsonObject obj = body.object();
|
||||||
|
bool ecritureEffectuee = false;
|
||||||
|
|
||||||
|
// Changement de consigne :
|
||||||
|
if (obj.contains("consigne")) {
|
||||||
|
int consigne = obj["consigne"].toInt();
|
||||||
|
if (consigne < 0) consigne = 0;
|
||||||
|
if (consigne > 100) consigne = 100;
|
||||||
|
|
||||||
|
QString req = QString("UPDATE TB_Circuits SET New_consigne=%1 ").arg(consigne);
|
||||||
|
if (obj.contains("old_consigne")) {
|
||||||
|
req += QString(", Old_consigne=%1 ").arg(obj["old_consigne"].toInt());
|
||||||
|
}
|
||||||
|
req += QString("WHERE Cle_primaire='%1'").arg(idCircuit);
|
||||||
|
QSqlQuery q(req);
|
||||||
|
ecritureEffectuee = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bascule du mode "commandes automatiques" (Events) :
|
||||||
|
if (obj.contains("validation_cdes_events")) {
|
||||||
|
const int mode = obj["validation_cdes_events"].toInt() != 0 ? 1 : 0;
|
||||||
|
QSqlQuery q(QString("UPDATE TB_Circuits SET Validation_cdes_events=%1 WHERE Cle_primaire='%2'").arg(mode).arg(idCircuit));
|
||||||
|
ecritureEffectuee = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ecritureEffectuee) {
|
||||||
|
QJsonObject err;
|
||||||
|
err["status"] = "erreur";
|
||||||
|
err["message"] = "aucun champ reconnu (attendu : consigne, validation_cdes_events)";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::BadRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signale à l'Appli qu'un changement est en attente, pour un
|
||||||
|
// traitement rapide (100ms maxi) plutôt que son cycle normal
|
||||||
|
// (cf synchro_bdd(shipheart_type_synchro_data) du natif) :
|
||||||
|
QSqlQuery("UPDATE TB_Systeme SET Valeur=1 WHERE Nom=\"Synchro\"");
|
||||||
|
|
||||||
|
QJsonObject reponse;
|
||||||
|
reponse["status"] = "ok";
|
||||||
|
return withCors(QHttpServerResponse(QJsonDocument(reponse).toJson()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Réponse au "preflight" CORS (requête OPTIONS automatique du navigateur
|
||||||
|
// avant tout PATCH avec Content-Type: application/json) - sans elle, le
|
||||||
|
// navigateur abandonne silencieusement la vraie requête PATCH avant même
|
||||||
|
// de l'envoyer.
|
||||||
|
server.route("/elements/circuit/<arg>", QHttpServerRequest::Method::Options,
|
||||||
|
[](int) {
|
||||||
|
QHttpServerResponse resp(QHttpServerResponder::StatusCode::NoContent);
|
||||||
|
QHttpHeaders headers = resp.headers();
|
||||||
|
headers.append(QHttpHeaders::WellKnownHeader::AccessControlAllowOrigin, "*");
|
||||||
|
headers.append(QHttpHeaders::WellKnownHeader::AccessControlAllowMethods, "GET, PATCH, OPTIONS");
|
||||||
|
headers.append(QHttpHeaders::WellKnownHeader::AccessControlAllowHeaders, "Content-Type");
|
||||||
|
resp.setHeaders(std::move(headers));
|
||||||
|
return resp;
|
||||||
|
});
|
||||||
|
|
||||||
|
auto tcpServer = new QTcpServer();
|
||||||
|
if (!tcpServer->listen(QHostAddress::Any, 8080) || !server.bind(tcpServer)) {
|
||||||
|
qCritical() << "Impossible de démarrer le serveur HTTP sur le port 8080.";
|
||||||
|
delete tcpServer;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
qInfo() << "shipheart_api en écoute sur le port" << tcpServer->serverPort();
|
||||||
|
|
||||||
|
return app.exec();
|
||||||
|
}
|
||||||
Binary file not shown.
Reference in New Issue
Block a user