diff --git a/main.cpp b/main.cpp index 14f0b75..027d1aa 100644 --- a/main.cpp +++ b/main.cpp @@ -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/", 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"); diff --git a/main.cpp:Zone.Identifier b/main.cpp:Zone.Identifier index 4be6ccf..80edf1d 100644 Binary files a/main.cpp:Zone.Identifier and b/main.cpp:Zone.Identifier differ diff --git a/main_OLD.cpp b/main_OLD.cpp index 32378ca..14f0b75 100644 --- a/main_OLD.cpp +++ b/main_OLD.cpp @@ -9,6 +9,10 @@ #include #include #include +#include +#include +#include +#include #include // ============================================================================ @@ -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=&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 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 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 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=&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> 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.";