Correction robustesse : vérification et rétablissement automatique de la connexion MariaDB en tout début de chaque route (assurer_connexion_bdd) - corrige une perte silencieuse de données après une longue période d'exécution continue (connexion tombée sans jamais être rétablie), validé par test direct (KILL de la connexion côté MariaDB)

This commit is contained in:
2026-09-23 19:27:52 +02:00
parent d1514cae2d
commit 88591790d0
3 changed files with 630 additions and 0 deletions
+31
View File
@@ -164,6 +164,32 @@ static QHttpServerResponse withCors(QHttpServerResponse &&resp)
return std::move(resp);
}
// ----------------------------------------------------------------------------
// Vérifie que la connexion MariaDB (ouverte une seule fois au démarrage) est
// toujours active, et la rétablit sinon. isOpen() peut rester vrai même après
// une coupure réseau ou un dépassement de wait_timeout côté serveur - un test
// léger (SELECT 1) confirme que la connexion répond réellement. Sans ce
// contrôle, l'API reste "vivante" indéfiniment mais chaque requête SQL échoue
// silencieusement après une longue période d'inactivité (constaté après
// plusieurs semaines d'exécution continue, cf synthèse).
// À appeler en tout premier dans chaque route qui touche la base.
// ----------------------------------------------------------------------------
static void assurer_connexion_bdd()
{
QSqlDatabase db = QSqlDatabase::database();
if (db.isOpen()) {
QSqlQuery test(db);
if (test.exec("SELECT 1")) return;
}
qWarning() << "Connexion MariaDB perdue, tentative de reconnexion...";
db.close();
if (db.open()) {
qInfo() << "Reconnexion MariaDB réussie.";
} else {
qCritical() << "Reconnexion MariaDB échouée :" << db.lastError().text();
}
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
@@ -196,6 +222,7 @@ int main(int argc, char *argv[])
// requêtes onglets de mise_a_jour_onglets_circuits()).
// ------------------------------------------------------------------
server.route("/elements/familles", [](const QHttpServerRequest &request) {
assurer_connexion_bdd();
QUrlQuery query(request.query());
const QString type = query.queryItemValue("type");
@@ -352,6 +379,7 @@ int main(int argc, char *argv[])
// différents, mais un seul, appelé deux fois).
// ------------------------------------------------------------------
server.route("/elements", [](const QHttpServerRequest &request) {
assurer_connexion_bdd();
QUrlQuery query(request.query());
const QString type = query.queryItemValue("type");
const QString familleStr = query.queryItemValue("famille");
@@ -610,6 +638,7 @@ int main(int argc, char *argv[])
// ------------------------------------------------------------------
server.route("/elements/circuit/<arg>", QHttpServerRequest::Method::Patch,
[](int idCircuit, const QHttpServerRequest &request) {
assurer_connexion_bdd();
const QJsonDocument body = QJsonDocument::fromJson(request.body());
if (!body.isObject()) {
QJsonObject err;
@@ -686,6 +715,7 @@ int main(int argc, char *argv[])
// Circuits/batteries (sélection de variable côté client) à venir.
// ------------------------------------------------------------------
server.route("/mesures", [](const QHttpServerRequest &request) {
assurer_connexion_bdd();
QUrlQuery query(request.query());
const QString type = query.queryItemValue("type");
const QString idStr = query.queryItemValue("id");
@@ -1037,6 +1067,7 @@ int main(int argc, char *argv[])
// 90% du temps de traitement natif pour un gain marginal).
// ------------------------------------------------------------------
server.route("/energie", [](const QHttpServerRequest &request) {
assurer_connexion_bdd();
QUrlQuery query(request.query());
const QString type = query.queryItemValue("type");
const QString idStr = query.queryItemValue("id");
Binary file not shown.
+599
View File
@@ -9,6 +9,10 @@
#include <QJsonArray>
#include <QJsonDocument>
#include <QUrlQuery>
#include <QDateTime>
#include <QMap>
#include <QVector>
#include <QPair>
#include <QDebug>
// ============================================================================
@@ -62,6 +66,32 @@ static const int UNITE_TEMPERATURE_FAHRENHEIT = 1;
// conversion_temperature(), chaine_latitude(), chaine_longitude()) et de
// fonctions_localisation.js (symbole_unite_volume(), symbole_unite_temperature()).
// ----------------------------------------------------------------------------
// Troncature d'une date à un palier propre selon la période d'analyse -
// traduction fidèle de ajustement_periode_analyse() (fonctions_date_heure.js
// natif). Appliquée à la fois à la date de début et à la date de fin :
// tronquer la fin vers le bas exclut naturellement l'intervalle "en cours"
// (pas encore terminé), sans étape séparée.
static QDateTime tronquer_periode(const QDateTime &dt, int periode)
{
QDateTime r = dt;
const QTime t = r.time();
switch (periode) {
case 0: // 1 heure -> palier de 5 minutes
r.setTime(QTime(t.hour(), (t.minute() / 5) * 5, 0));
break;
case 1: // 24 heures -> heure pleine
r.setTime(QTime(t.hour(), 0, 0));
break;
case 2: // 7 jours -> tranche de 6h
r.setTime(QTime((t.hour() / 6) * 6, 0, 0));
break;
default: // 30 jours -> minuit
r.setTime(QTime(0, 0, 0));
break;
}
return r;
}
static double conversion_volume(double val, int unite)
{
switch (unite) {
@@ -644,6 +674,575 @@ int main(int argc, char *argv[])
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()));
});
// ------------------------------------------------------------------
// GET /energie?type=batteries&id=<id>&periode=<0-3>
// Consommation d'énergie des circuits DC rattachés à une batterie
// (Phase 4, cf synthèse §22). Version SIMPLIFIÉE par rapport au natif :
// pas d'intégrale de puissance (LEAD/LAG) pour rafraîchir l'intervalle
// "en cours" - la dernière barre reflète simplement le dernier
// regroupement déjà finalisé par l'Appli C, avec un léger retard
// (décision explicite, cf §22 - le calcul par intégrale représentait
// 90% du temps de traitement natif pour un gain marginal).
// ------------------------------------------------------------------
server.route("/energie", [](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 != "batteries") {
QJsonObject err;
err["status"] = "erreur";
err["message"] = "type non supporté (batteries uniquement)";
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));
}
QSqlQuery qExiste;
qExiste.prepare("SELECT Cle_primaire FROM TB_Batteries WHERE Cle_primaire=:id");
qExiste.bindValue(":id", id);
qExiste.exec();
if (!qExiste.next()) {
QJsonObject err;
err["status"] = "erreur";
err["message"] = "batterie introuvable";
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::NotFound));
}
// Variable "énergie" - globale, comme "Res_niveau" pour les réservoirs
// (une seule définition TB_Variables, pas une par circuit) :
QSqlQuery qv;
qv.prepare("SELECT Cle_primaire FROM TB_Variables WHERE Code='Circ_energie'");
qv.exec();
if (!qv.next()) {
QJsonObject err;
err["status"] = "erreur";
err["message"] = "configuration serveur incomplète (variable 'Circ_energie' introuvable)";
return withCors(QHttpServerResponse(QJsonDocument(err).toJson(), QHttpServerResponder::StatusCode::InternalServerError));
}
const int idVariableEnergie = qv.value(0).toInt();
static const qint64 DUREES_PERIODE[4] = { 3600, 86400, 604800, 2592000 };
const qint64 dureeSecondes = DUREES_PERIODE[periode];
// Le CM5 tourne en UTC, la BDD stocke les dates en UTC - mais les
// paliers de troncature (minuit, 6h...) doivent être calculés en
// heure LOCALE DU BATEAU (TB_Configuration.Fuseau_horaire +
// Heure_ete), pas en UTC brut, sous peine de désaligner les tranches
// de plusieurs heures par rapport à ce qu'affiche le natif :
double fuseauHoraire = 0;
int heureEte = 0;
{
QSqlQuery qf("SELECT Valeur FROM TB_Configuration WHERE Nom='Fuseau_horaire'");
if (qf.next()) fuseauHoraire = qf.value(0).toDouble();
}
{
QSqlQuery qe("SELECT Valeur FROM TB_Configuration WHERE Nom='Heure_ete'");
if (qe.next()) heureEte = qe.value(0).toInt();
}
const qint64 offsetSecondes = (qint64)((fuseauHoraire + heureEte) * 3600);
// Troncature à un palier propre (cf ajustement_periode_analyse()
// natif) - exclut naturellement l'intervalle "en cours" en arrondissant
// la fin vers le bas, et garantit des barres alignées sur des paliers
// réguliers plutôt que sur l'instant exact de la requête :
const QDateTime maintenantUTC = QDateTime::currentDateTime();
const QDateTime maintenantLocal = maintenantUTC.addSecs(offsetSecondes);
const QDateTime finLocale = tronquer_periode(maintenantLocal, periode);
const QDateTime debutLocal = tronquer_periode(maintenantLocal.addSecs(-dureeSecondes), periode);
// Reconversion en UTC - les dates stockées en base et utilisées dans
// les requêtes SQL sont toujours en UTC :
const QDateTime dateFin = finLocale.addSecs(-offsetSecondes);
const QDateTime dateDebut = debutLocal.addSecs(-offsetSecondes);
const QString chaineDebut = dateDebut.toString("yyyy-MM-dd HH:mm:ss");
const QString chaineFin = dateFin.toString("yyyy-MM-dd HH:mm:ss");
// "Type" de mesure et regroupement des barres - mêmes seuils que le
// natif (batterie.qml/mise_a_jour_courbes(), branche énergie) :
const int typeMesure = (periode == 0) ? 3 : 2;
qint64 regroupement = 1;
if (periode == 2) regroupement = 6 * 3600; // 7 jours : barres de 6h
else if (periode == 3) regroupement = 24 * 3600; // 30 jours : barres de 24h
// Largeur RÉELLE de chaque tranche, pour le calcul du "début" affiché -
// distincte de 'regroupement' (simple diviseur SQL, qui vaut 1 pour 1h/24h
// puisque l'Appli C pré-agrège déjà à la bonne granularité, sans qu'on
// ait besoin de regrouper davantage côté serveur). Correspond au nombre
// de graduations natif (13 pour 1h, 25 pour 24h) :
qint64 largeurTranche = regroupement;
if (periode == 0) largeurTranche = 5 * 60; // 1 heure : tranches de 5 min
else if (periode == 1) largeurTranche = 3600; // 24 heures : tranches de 1h
// Circuits DC rattachés à cette batterie (shipbase/SP812/SP8DC -
// pas les modules AC, qui n'ont pas de suivi d'énergie de ce type) :
const QString sousRequeteCircuits =
"SELECT Cle_primaire FROM TB_Circuits WHERE Adresse_module IN "
"(SELECT Adresse_k_bus FROM TB_Modules_config WHERE ID_Batterie=:idBatterie AND (Type=0 OR Type=1 OR Type=4))";
// 1) Total sur toute la période :
double totalWh = 0;
{
QSqlQuery qTotal;
qTotal.prepare(
"SELECT SUM(Valeur) FROM TB_Mesures_circuits WHERE Type=:typeMesure AND ID_Variable=:idVar "
"AND ID IN (" + sousRequeteCircuits + ") AND Date>=:debut AND Date<=:fin"
);
qTotal.bindValue(":typeMesure", typeMesure);
qTotal.bindValue(":idVar", idVariableEnergie);
qTotal.bindValue(":idBatterie", id);
qTotal.bindValue(":debut", chaineDebut);
qTotal.bindValue(":fin", chaineFin);
qTotal.exec();
if (qTotal.next()) totalWh = qTotal.value(0).toDouble();
}
// 2) Barres par intervalle (une par regroupement) :
QJsonArray barres;
{
QSqlQuery qBarres;
qBarres.prepare(
"SELECT SUM(Valeur), MAX(Date) FROM TB_Mesures_circuits WHERE Type=:typeMesure AND ID_Variable=:idVar "
"AND ID IN (" + sousRequeteCircuits + ") AND Date>=:debut AND Date<=:fin "
"GROUP BY (UNIX_TIMESTAMP(Date) - UNIX_TIMESTAMP(:debut2)) DIV :regroupement ORDER BY MAX(Date)"
);
qBarres.bindValue(":typeMesure", typeMesure);
qBarres.bindValue(":idVar", idVariableEnergie);
qBarres.bindValue(":idBatterie", id);
qBarres.bindValue(":debut", chaineDebut);
qBarres.bindValue(":fin", chaineFin);
qBarres.bindValue(":debut2", chaineDebut);
qBarres.bindValue(":regroupement", regroupement);
qBarres.exec();
while (qBarres.next()) {
// Les mesures d'énergie sont PRÉCISÉMENT calées sur xx:59:59
// par construction (intégrales dont la somme doit couvrir
// exactement la réalité, sans trou ni recouvrement) - +1s sur
// la dernière mesure réelle donne donc TOUJOURS la borne
// exacte de la tranche, sans calcul plus complexe nécessaire :
const QDateTime frontiere = qBarres.value(1).toDateTime().addSecs(1);
QJsonObject barre;
barre["fin"] = frontiere.toMSecsSinceEpoch();
barre["debut"] = frontiere.addSecs(-largeurTranche).toMSecsSinceEpoch();
barre["valeur_wh"] = qBarres.value(0).toDouble();
barres.append(barre);
}
}
// 3) Comparatif par circuit, trié par énergie décroissante :
QJsonArray comparatif;
{
QSqlQuery qComp;
qComp.prepare(
"SELECT ID, SUM(Valeur) AS energie FROM TB_Mesures_circuits WHERE Type=:typeMesure AND ID_Variable=:idVar "
"AND ID IN (" + sousRequeteCircuits + ") AND Date>=:debut AND Date<=:fin "
"GROUP BY ID ORDER BY energie DESC"
);
qComp.bindValue(":typeMesure", typeMesure);
qComp.bindValue(":idVar", idVariableEnergie);
qComp.bindValue(":idBatterie", id);
qComp.bindValue(":debut", chaineDebut);
qComp.bindValue(":fin", chaineFin);
qComp.exec();
QVector<QPair<int, double>> lignes;
double totalComparatif = 0;
while (qComp.next()) {
const int idCircuit = qComp.value(0).toInt();
const double energieCircuit = qComp.value(1).toDouble();
lignes.append({idCircuit, energieCircuit});
totalComparatif += energieCircuit;
}
for (const auto &ligne : lignes) {
QSqlQuery qInfoCircuit;
qInfoCircuit.prepare("SELECT Nom, Consigne FROM TB_Circuits WHERE Cle_primaire=:id");
qInfoCircuit.bindValue(":id", ligne.first);
qInfoCircuit.exec();
QJsonObject c;
c["id"] = ligne.first;
c["energie_wh"] = ligne.second;
c["pourcentage"] = (totalComparatif > 0) ? (ligne.second * 100.0 / totalComparatif) : 0;
if (qInfoCircuit.next()) {
c["nom"] = qInfoCircuit.value(0).toString();
c["consigne"] = qInfoCircuit.value(1).toInt();
}
comparatif.append(c);
}
}
QJsonObject reponse;
reponse["status"] = "ok";
reponse["total_wh"] = totalWh;
reponse["barres"] = barres;
reponse["comparatif"] = comparatif;
reponse["debug_debut"] = chaineDebut;
reponse["debug_fin"] = chaineFin;
reponse["debug_regroupement"] = (double)regroupement;
return withCors(QHttpServerResponse(QJsonDocument(reponse).toJson()));
});
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.";