Endpoint /energie (Phase 4, batteries) : total/barres/comparatif par circuit, troncature de période consciente du fuseau horaire du bateau, bornes de tranche exactes

This commit is contained in:
2026-08-27 10:11:27 +02:00
parent 2deaf8ad4d
commit d1514cae2d
2 changed files with 245 additions and 0 deletions
+245
View File
@@ -11,6 +11,8 @@
#include <QUrlQuery>
#include <QDateTime>
#include <QMap>
#include <QVector>
#include <QPair>
#include <QDebug>
// ============================================================================
@@ -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=<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.";
Binary file not shown.