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:
@@ -11,6 +11,8 @@
|
|||||||
#include <QUrlQuery>
|
#include <QUrlQuery>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
|
#include <QVector>
|
||||||
|
#include <QPair>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -64,6 +66,32 @@ static const int UNITE_TEMPERATURE_FAHRENHEIT = 1;
|
|||||||
// conversion_temperature(), chaine_latitude(), chaine_longitude()) et de
|
// conversion_temperature(), chaine_latitude(), chaine_longitude()) et de
|
||||||
// fonctions_localisation.js (symbole_unite_volume(), symbole_unite_temperature()).
|
// 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)
|
static double conversion_volume(double val, int unite)
|
||||||
{
|
{
|
||||||
switch (unite) {
|
switch (unite) {
|
||||||
@@ -998,6 +1026,223 @@ int main(int argc, char *argv[])
|
|||||||
return withCors(QHttpServerResponse(QJsonDocument(reponse).toJson()));
|
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();
|
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.
Reference in New Issue
Block a user