Mode Énergie dans detail_batterie.qml (barres + comparatif) ; axes X unifiés (quadrillage+étiquettes) sur les 4 pages de détail, espacement adapté par période

This commit is contained in:
2026-08-27 10:11:35 +02:00
parent 85ba4be46b
commit 81968d8900
8 changed files with 352 additions and 256 deletions
+196 -21
View File
@@ -8,13 +8,16 @@ import qt.koriolan.shipheart 1.0
// detail_batterie.qml - page de détail d'une batterie (valeur + historique).
// ----------------------------------------------------------------------------
// Poussée sur le StackView depuis home.qml (clic court sur une batterie).
// Domaine batteries : sélecteur de variable (8 options), comme les circuits.
// Le vrai défaut natif est le comparatif d'énergie (id_variable=0),
// volontairement laissé de côté (Phase 4, énergie, cf synthèse) - "SOC"
// choisi comme vue par défaut à la place, sur décision explicite.
// Domaine batteries : sélecteur de variable (8 options simples + "Énergie").
// "Énergie" (9e bouton) bascule vers une vue différente (barres + tableau
// comparatif par circuit) plutôt que le graphique de valeur habituel - cf
// synthèse §22, Phase 4. Version SIMPLIFIÉE par rapport au natif : pas
// d'intégrale de puissance pour rafraîchir la barre "en cours" (décision
// explicite, cf §22).
//
// Mêmes simplifications volontaires que detail_capteur.qml (Canvas plutôt que
// QtCharts, ligne continue plutôt qu'escalier, pas de zoom tactile).
// Mêmes simplifications volontaires que detail_capteur.qml pour le
// graphique de valeur (Canvas plutôt que QtCharts, ligne continue plutôt
// qu'escalier, pas de zoom tactile).
// ============================================================================
Shipheart_page {
@@ -27,9 +30,11 @@ Shipheart_page {
readonly property var periodesLibelles: [qsTr("1 heure"), qsTr("24 heures"), qsTr("7 jours"), qsTr("30 jours")]
property int variableIndex: 0 // "soc" par défaut
readonly property var variablesCodes: ["soc", "tension", "temperature", "courant_differentiel", "courant_charge", "courant_decharge", "soh", "autonomie"]
readonly property var variablesLibelles: [qsTr("SOC"), qsTr("Tension"), qsTr("Température"), qsTr("I. différentiel"), qsTr("I. charge"), qsTr("I. décharge"), qsTr("SOH"), qsTr("Autonomie")]
readonly property var variablesCodes: ["soc", "tension", "temperature", "courant_differentiel", "courant_charge", "courant_decharge", "soh", "autonomie", "energie"]
readonly property var variablesLibelles: [qsTr("SOC"), qsTr("Tension"), qsTr("Température"), qsTr("I. différentiel"), qsTr("I. charge"), qsTr("I. décharge"), qsTr("SOH"), qsTr("Autonomie"), qsTr("Énergie")]
readonly property bool modeEnergie: variablesCodes[variableIndex] === "energie"
// --- État pour le mode "variable simple" (graphique de valeur) ---
property var pointsCourbe: []
property real valeurMin: 0
property real valeurMax: 0
@@ -38,13 +43,18 @@ Shipheart_page {
property double tempsMax: 0
property string uniteAffichage: ""
property int precisionAffichage: 1
property real valeurActuelle: 0
// --- État pour le mode "énergie" (barres + comparatif) ---
property real totalWh: 0
property var barresEnergie: []
property var comparatifEnergie: []
property DataProvider dataProvider: DataProvider {}
function charger_courbe() {
dataProvider.get_mesures("batteries", idElement, periodeIndex, variablesCodes[variableIndex]);
if (modeEnergie) dataProvider.get_energie("batteries", idElement, periodeIndex);
else dataProvider.get_mesures("batteries", idElement, periodeIndex, variablesCodes[variableIndex]);
}
function formater_date(timestampMs) {
@@ -68,7 +78,15 @@ Shipheart_page {
tempsMax = donnees.max.t;
valeurMoyenne = donnees.moyenne;
}
graphique.requestPaint();
graphiqueVariable.requestPaint();
}
function onEnergieRecue(type, id, periode, donnees) {
if (id !== idElement || periode !== periodeIndex || !modeEnergie) return;
totalWh = donnees.total_wh;
barresEnergie = donnees.barres;
comparatifEnergie = donnees.comparatif;
graphiqueBarres.requestPaint();
}
function onErreur(message) {
@@ -97,8 +115,7 @@ Shipheart_page {
color: couleur_gris_pale
}
// Sélecteur de variable (8 options - grille 4x2 plutôt qu'une seule
// ligne, pour rester lisible) :
// Sélecteur de variable (8 valeurs simples + énergie) :
GridLayout {
Layout.fillWidth: true
columns: 4
@@ -141,13 +158,15 @@ Shipheart_page {
Layout.alignment: Qt.AlignVCenter
spacing: 4
Text {
text: valeurActuelle.toFixed(precisionAffichage)
text: modeEnergie
? (totalWh >= 1000 ? (totalWh / 1000).toFixed(2) : totalWh.toFixed(1))
: valeurActuelle.toFixed(precisionAffichage)
font.pixelSize: 64
font.bold: true
color: couleur_blanc_casse
}
Text {
text: uniteAffichage
text: modeEnergie ? (totalWh >= 1000 ? "kWh" : "Wh") : uniteAffichage
font.pixelSize: 20
color: couleur_gris_pale
Layout.alignment: Qt.AlignTop
@@ -157,6 +176,7 @@ Shipheart_page {
Item { Layout.fillWidth: true }
ColumnLayout {
visible: !modeEnergie
spacing: 10
RowLayout {
Text { text: qsTr("Max"); color: couleur_gris_pale; Layout.preferredWidth: 90 }
@@ -177,6 +197,13 @@ Shipheart_page {
Text { text: (valeurMax - valeurMin).toFixed(precisionAffichage + 1) + " " + uniteAffichage; color: couleur_blanc_casse }
}
}
Text {
visible: modeEnergie
text: qsTr("Consommation totale sur la période - détail par circuit ci-dessous")
color: couleur_gris_pale
font.pixelSize: 13
}
}
RowLayout {
@@ -185,10 +212,12 @@ Shipheart_page {
spacing: 15
opacity: dataProvider.connecte ? 1.0 : 0.45
// --- Mode "variable simple" : graphique de valeur habituel ---
Canvas {
id: graphique
id: graphiqueVariable
Layout.fillWidth: true
Layout.fillHeight: true
visible: !modeEnergie
onPaint: {
var ctx = getContext("2d");
@@ -230,11 +259,43 @@ Shipheart_page {
ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4);
}
var nbLabelsX = 6;
for (var j = 0; j <= nbLabelsX; j++) {
var t = tMin + (j / nbLabelsX) * (tMax - tMin);
var x = px(t);
ctx.fillText(Qt.formatDateTime(new Date(t), "HH:mm"), x - 18, height - 8);
// Quadrillage vertical ET étiquettes, ensemble, sur les mêmes
// paliers ronds - paliers de 5 min (1h), heure pleine (24h) ou
// minuit (7j/30j). Remplace l'ancien double affichage (quadrillage
// sans étiquette + étiquettes à des positions non alignées).
function prochain_palier_rond(instant, periodeIdx) {
var d = new Date(instant);
if (periodeIdx >= 2) {
d.setHours(0, 0, 0, 0);
if (d.getTime() < instant) d.setDate(d.getDate() + 1);
} else if (periodeIdx === 1) {
d.setMinutes(0, 0, 0);
if (d.getTime() < instant) d.setHours(d.getHours() + 1);
} else {
d.setSeconds(0, 0);
d.setMinutes(Math.ceil(d.getMinutes() / 5) * 5);
if (d.getTime() < instant) d.setMinutes(d.getMinutes() + 5);
}
return d.getTime();
}
var pasGrilleMs = (periodeIndex >= 2) ? 86400000 : (periodeIndex === 1) ? 3600000 : 5 * 60000;
var formatEtiquette = (periodeIndex >= 2) ? "dd/MM" : "HH:mm";
ctx.strokeStyle = "#2a2a2a";
ctx.fillStyle = couleur_gris_pale;
// Une étiquette sur 3 seulement pour 24h/30j (trop chargé sinon,
// 24-25 ou 30 barres) - à partir de la première, quitte à ce que les
// 1-2 dernières graduations restent sans étiquette :
var pasEtiquette = (periodeIndex === 1 || periodeIndex === 3) ? 3 : 1;
var indexGrille = 0;
for (var tGrille = prochain_palier_rond(tMin, periodeIndex); tGrille <= tMax; tGrille += pasGrilleMs, indexGrille++) {
var xGrille = px(tGrille);
ctx.beginPath();
ctx.moveTo(xGrille, marge);
ctx.lineTo(xGrille, marge + hauteurUtile);
ctx.stroke();
if (indexGrille % pasEtiquette === 0) {
ctx.fillText(Qt.formatDateTime(new Date(tGrille), formatEtiquette), xGrille - 18, height - 8);
}
}
ctx.strokeStyle = couleur_bleu_koriolan;
@@ -258,6 +319,120 @@ Shipheart_page {
}
}
// --- Mode "énergie" : barres + tableau comparatif ---
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
visible: modeEnergie
spacing: 10
Canvas {
id: graphiqueBarres
Layout.fillWidth: true
Layout.preferredHeight: parent.height * 0.45
onPaint: {
var ctx = getContext("2d");
ctx.reset();
ctx.clearRect(0, 0, width, height);
if (barresEnergie.length === 0) {
ctx.fillStyle = couleur_rouge_transparent;
ctx.font = "14px sans-serif";
ctx.fillText(qsTr("Pas assez de données sur cette période."), width / 2 - 110, height / 2);
return;
}
var marge = 45;
var largeurUtile = width - 2 * marge;
var hauteurUtile = height - 2 * marge;
var maxBarre = 0;
for (var i = 0; i < barresEnergie.length; i++) {
if (barresEnergie[i].valeur_wh > maxBarre) maxBarre = barresEnergie[i].valeur_wh;
}
if (maxBarre <= 0) maxBarre = 1;
var largeurBarre = (largeurUtile / barresEnergie.length) * 0.7;
var pasBarre = largeurUtile / barresEnergie.length;
ctx.strokeStyle = "#333333";
ctx.fillStyle = couleur_gris_pale;
ctx.font = "10px sans-serif";
var nbLignesY = 4;
for (var l = 0; l <= nbLignesY; l++) {
var v = (l / nbLignesY) * maxBarre;
var y = marge + hauteurUtile - (v / maxBarre) * hauteurUtile;
ctx.beginPath();
ctx.moveTo(marge, y);
ctx.lineTo(width - marge, y);
ctx.stroke();
ctx.fillText(v.toFixed(1), 2, y + 4);
}
ctx.fillStyle = couleur_bleu_koriolan;
for (var k = 0; k < barresEnergie.length; k++) {
var hBarre = (barresEnergie[k].valeur_wh / maxBarre) * hauteurUtile;
var xBarre = marge + k * pasBarre + (pasBarre - largeurBarre) / 2;
var yBarre = marge + hauteurUtile - hBarre;
ctx.fillRect(xBarre, yBarre, largeurBarre, hBarre);
if (k === 0 || k === barresEnergie.length - 1 || k === Math.floor(barresEnergie.length / 2)) {
ctx.fillStyle = couleur_gris_pale;
var dateDebutBarre = new Date(barresEnergie[k].debut);
var etiquette = (dateDebutBarre.getHours() === 0 && dateDebutBarre.getMinutes() === 0)
? Qt.formatDateTime(dateDebutBarre, "dd/MM")
: Qt.formatDateTime(dateDebutBarre, "HH:mm");
ctx.fillText(etiquette, xBarre - 15, height - 8);
ctx.fillStyle = couleur_bleu_koriolan;
}
}
}
}
// Tableau comparatif par circuit :
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "transparent"
border.color: "#333333"
border.width: 1
radius: 6
ListView {
anchors.fill: parent
anchors.margins: 8
clip: true
model: comparatifEnergie
spacing: 4
delegate: RowLayout {
width: ListView.view.width
Text {
text: modelData.nom
color: (modelData.consigne > 0) ? couleur_blanc_casse : couleur_gris_moyen
font.pixelSize: 13
Layout.preferredWidth: parent.width * 0.4
elide: Text.ElideRight
}
Text {
text: modelData.energie_wh.toFixed(2) + " Wh"
color: couleur_blanc_casse
font.pixelSize: 13
Layout.preferredWidth: parent.width * 0.3
}
Text {
text: modelData.pourcentage.toFixed(1) + " %"
color: couleur_gris_pale
font.pixelSize: 13
Layout.fillWidth: true
}
}
}
}
}
ColumnLayout {
spacing: 8
Repeater {
+37 -6
View File
@@ -203,12 +203,43 @@ Shipheart_page {
ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4);
}
// Étiquettes X :
var nbLabelsX = 6;
for (var j = 0; j <= nbLabelsX; j++) {
var t = tMin + (j / nbLabelsX) * (tMax - tMin);
var x = px(t);
ctx.fillText(Qt.formatDateTime(new Date(t), "HH:mm"), x - 18, height - 8);
// Quadrillage vertical ET étiquettes, ensemble, sur les mêmes
// paliers ronds - paliers de 5 min (1h), heure pleine (24h) ou
// minuit (7j/30j). Remplace l'ancien double affichage (quadrillage
// sans étiquette + étiquettes à des positions non alignées).
function prochain_palier_rond(instant, periodeIdx) {
var d = new Date(instant);
if (periodeIdx >= 2) {
d.setHours(0, 0, 0, 0);
if (d.getTime() < instant) d.setDate(d.getDate() + 1);
} else if (periodeIdx === 1) {
d.setMinutes(0, 0, 0);
if (d.getTime() < instant) d.setHours(d.getHours() + 1);
} else {
d.setSeconds(0, 0);
d.setMinutes(Math.ceil(d.getMinutes() / 5) * 5);
if (d.getTime() < instant) d.setMinutes(d.getMinutes() + 5);
}
return d.getTime();
}
var pasGrilleMs = (periodeIndex >= 2) ? 86400000 : (periodeIndex === 1) ? 3600000 : 5 * 60000;
var formatEtiquette = (periodeIndex >= 2) ? "dd/MM" : "HH:mm";
ctx.strokeStyle = "#2a2a2a";
ctx.fillStyle = couleur_gris_pale;
// Une étiquette sur 3 seulement pour 24h/30j (trop chargé sinon,
// 24-25 ou 30 barres) - à partir de la première, quitte à ce que les
// 1-2 dernières graduations restent sans étiquette :
var pasEtiquette = (periodeIndex === 1 || periodeIndex === 3) ? 3 : 1;
var indexGrille = 0;
for (var tGrille = prochain_palier_rond(tMin, periodeIndex); tGrille <= tMax; tGrille += pasGrilleMs, indexGrille++) {
var xGrille = px(tGrille);
ctx.beginPath();
ctx.moveTo(xGrille, marge);
ctx.lineTo(xGrille, marge + hauteurUtile);
ctx.stroke();
if (indexGrille % pasEtiquette === 0) {
ctx.fillText(Qt.formatDateTime(new Date(tGrille), formatEtiquette), xGrille - 18, height - 8);
}
}
// Ligne de la courbe :
+37 -5
View File
@@ -227,11 +227,43 @@ Shipheart_page {
ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4);
}
var nbLabelsX = 6;
for (var j = 0; j <= nbLabelsX; j++) {
var t = tMin + (j / nbLabelsX) * (tMax - tMin);
var x = px(t);
ctx.fillText(Qt.formatDateTime(new Date(t), "HH:mm"), x - 18, height - 8);
// Quadrillage vertical ET étiquettes, ensemble, sur les mêmes
// paliers ronds - paliers de 5 min (1h), heure pleine (24h) ou
// minuit (7j/30j). Remplace l'ancien double affichage (quadrillage
// sans étiquette + étiquettes à des positions non alignées).
function prochain_palier_rond(instant, periodeIdx) {
var d = new Date(instant);
if (periodeIdx >= 2) {
d.setHours(0, 0, 0, 0);
if (d.getTime() < instant) d.setDate(d.getDate() + 1);
} else if (periodeIdx === 1) {
d.setMinutes(0, 0, 0);
if (d.getTime() < instant) d.setHours(d.getHours() + 1);
} else {
d.setSeconds(0, 0);
d.setMinutes(Math.ceil(d.getMinutes() / 5) * 5);
if (d.getTime() < instant) d.setMinutes(d.getMinutes() + 5);
}
return d.getTime();
}
var pasGrilleMs = (periodeIndex >= 2) ? 86400000 : (periodeIndex === 1) ? 3600000 : 5 * 60000;
var formatEtiquette = (periodeIndex >= 2) ? "dd/MM" : "HH:mm";
ctx.strokeStyle = "#2a2a2a";
ctx.fillStyle = couleur_gris_pale;
// Une étiquette sur 3 seulement pour 24h/30j (trop chargé sinon,
// 24-25 ou 30 barres) - à partir de la première, quitte à ce que les
// 1-2 dernières graduations restent sans étiquette :
var pasEtiquette = (periodeIndex === 1 || periodeIndex === 3) ? 3 : 1;
var indexGrille = 0;
for (var tGrille = prochain_palier_rond(tMin, periodeIndex); tGrille <= tMax; tGrille += pasGrilleMs, indexGrille++) {
var xGrille = px(tGrille);
ctx.beginPath();
ctx.moveTo(xGrille, marge);
ctx.lineTo(xGrille, marge + hauteurUtile);
ctx.stroke();
if (indexGrille % pasEtiquette === 0) {
ctx.fillText(Qt.formatDateTime(new Date(tGrille), formatEtiquette), xGrille - 18, height - 8);
}
}
ctx.strokeStyle = couleur_bleu_koriolan;
+37 -6
View File
@@ -194,12 +194,43 @@ Shipheart_page {
ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4);
}
// Étiquettes X :
var nbLabelsX = 6;
for (var j = 0; j <= nbLabelsX; j++) {
var t = tMin + (j / nbLabelsX) * (tMax - tMin);
var x = px(t);
ctx.fillText(Qt.formatDateTime(new Date(t), "HH:mm"), x - 18, height - 8);
// Quadrillage vertical ET étiquettes, ensemble, sur les mêmes
// paliers ronds - paliers de 5 min (1h), heure pleine (24h) ou
// minuit (7j/30j). Remplace l'ancien double affichage (quadrillage
// sans étiquette + étiquettes à des positions non alignées).
function prochain_palier_rond(instant, periodeIdx) {
var d = new Date(instant);
if (periodeIdx >= 2) {
d.setHours(0, 0, 0, 0);
if (d.getTime() < instant) d.setDate(d.getDate() + 1);
} else if (periodeIdx === 1) {
d.setMinutes(0, 0, 0);
if (d.getTime() < instant) d.setHours(d.getHours() + 1);
} else {
d.setSeconds(0, 0);
d.setMinutes(Math.ceil(d.getMinutes() / 5) * 5);
if (d.getTime() < instant) d.setMinutes(d.getMinutes() + 5);
}
return d.getTime();
}
var pasGrilleMs = (periodeIndex >= 2) ? 86400000 : (periodeIndex === 1) ? 3600000 : 5 * 60000;
var formatEtiquette = (periodeIndex >= 2) ? "dd/MM" : "HH:mm";
ctx.strokeStyle = "#2a2a2a";
ctx.fillStyle = couleur_gris_pale;
// Une étiquette sur 3 seulement pour 24h/30j (trop chargé sinon,
// 24-25 ou 30 barres) - à partir de la première, quitte à ce que les
// 1-2 dernières graduations restent sans étiquette :
var pasEtiquette = (periodeIndex === 1 || periodeIndex === 3) ? 3 : 1;
var indexGrille = 0;
for (var tGrille = prochain_palier_rond(tMin, periodeIndex); tGrille <= tMax; tGrille += pasGrilleMs, indexGrille++) {
var xGrille = px(tGrille);
ctx.beginPath();
ctx.moveTo(xGrille, marge);
ctx.lineTo(xGrille, marge + hauteurUtile);
ctx.stroke();
if (indexGrille % pasEtiquette === 0) {
ctx.fillText(Qt.formatDateTime(new Date(tGrille), formatEtiquette), xGrille - 18, height - 8);
}
}
// Ligne de la courbe :