From d1514cae2d483cfcf7708946ed2362f451449d66 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 27 Aug 2026 10:11:27 +0200 Subject: [PATCH] =?UTF-8?q?Endpoint=20/energie=20(Phase=204,=20batteries)?= =?UTF-8?q?=20:=20total/barres/comparatif=20par=20circuit,=20troncature=20?= =?UTF-8?q?de=20p=C3=A9riode=20consciente=20du=20fuseau=20horaire=20du=20b?= =?UTF-8?q?ateau,=20bornes=20de=20tranche=20exactes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.cpp | 245 +++++++++++++++++++++++++++++++++++++++ main.cpp:Zone.Identifier | Bin 94 -> 91 bytes 2 files changed, 245 insertions(+) diff --git a/main.cpp b/main.cpp index c9d7468..14f0b75 100644 --- a/main.cpp +++ b/main.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include // ============================================================================ @@ -64,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) { @@ -998,6 +1026,223 @@ int main(int argc, char *argv[]) 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."; diff --git a/main.cpp:Zone.Identifier b/main.cpp:Zone.Identifier index ede892f68d6f9591e6f3fd81cc2cdf1999e800e1..4be6ccf63c3e3a48f2727cf84e0f995e8e64ecc0 100644 GIT binary patch delta 24 fcma!xp5P&znwMIXo|zh7kW*T0Sgco-S-=1QaP$bw delta 27 icma!!o8Tdqlvq-dT9lbu9G{z7Tw0V`tXGv;zyJV}R|-!6