490 lines
22 KiB
QML
490 lines
22 KiB
QML
import QtQuick 2.12
|
|
import QtQuick.Controls 2.12
|
|
import QtQuick.Layouts 1.12
|
|
|
|
import qt.koriolan.shipheart 1.0
|
|
|
|
import "responsive.js" as Responsive
|
|
|
|
// ============================================================================
|
|
// 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 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 pour le
|
|
// graphique de valeur (Canvas plutôt que QtCharts, ligne continue plutôt
|
|
// qu'escalier, pas de zoom tactile).
|
|
// ============================================================================
|
|
|
|
Shipheart_page {
|
|
id: page_detail_batterie
|
|
|
|
property int idElement: 0
|
|
property string nomElement: ""
|
|
|
|
property int periodeIndex: 1 // 24 heures par défaut
|
|
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", "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
|
|
property real valeurMoyenne: 0
|
|
property double tempsMin: 0
|
|
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() {
|
|
if (modeEnergie) dataProvider.get_energie("batteries", idElement, periodeIndex);
|
|
else dataProvider.get_mesures("batteries", idElement, periodeIndex, variablesCodes[variableIndex]);
|
|
}
|
|
|
|
function formater_date(timestampMs) {
|
|
return Qt.formatDateTime(new Date(timestampMs), "dd/MM HH:mm");
|
|
}
|
|
|
|
Connections {
|
|
target: dataProvider
|
|
|
|
function onMesuresRecues(type, id, periode, variable, donnees) {
|
|
if (id !== idElement || periode !== periodeIndex || variable !== variablesCodes[variableIndex]) return;
|
|
|
|
pointsCourbe = donnees.points;
|
|
uniteAffichage = donnees.unite;
|
|
precisionAffichage = donnees.precision;
|
|
valeurActuelle = donnees.valeur_actuelle;
|
|
if (donnees.points.length > 0) {
|
|
valeurMin = donnees.min.v;
|
|
tempsMin = donnees.min.t;
|
|
valeurMax = donnees.max.v;
|
|
tempsMax = donnees.max.t;
|
|
valeurMoyenne = donnees.moyenne;
|
|
}
|
|
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) {
|
|
console.log("DataProvider erreur (détail batterie) : " + message);
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: charger_courbe()
|
|
|
|
Timer {
|
|
interval: 5000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: charger_courbe()
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 20
|
|
spacing: 15
|
|
|
|
Text {
|
|
text: nomElement
|
|
font.pixelSize: Responsive.police(22, page_detail_batterie.width)
|
|
font.bold: true
|
|
color: couleur_gris_pale
|
|
}
|
|
|
|
// Sélecteur de variable (8 valeurs simples + énergie) - 4 colonnes
|
|
// normalement, 2 si écran étroit :
|
|
GridLayout {
|
|
Layout.fillWidth: true
|
|
columns: Responsive.colonnesSelonLargeur(page_detail_batterie.width, variablesLibelles.length)
|
|
columnSpacing: 8
|
|
rowSpacing: 8
|
|
|
|
Repeater {
|
|
model: variablesLibelles
|
|
delegate: Rectangle {
|
|
Layout.fillWidth: true
|
|
height: Math.max(34, Responsive.HAUTEUR_MIN_TACTILE)
|
|
radius: 8
|
|
color: (index === variableIndex) ? shipheart_couleur_fond_bouton_actif : shipheart_couleur_fond_bouton
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: modelData
|
|
color: couleur_blanc_casse
|
|
font.pixelSize: Responsive.police(12, page_detail_batterie.width)
|
|
font.bold: (index === variableIndex)
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: {
|
|
variableIndex = index;
|
|
charger_courbe();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 20
|
|
opacity: dataProvider.connecte ? 1.0 : 0.45
|
|
|
|
RowLayout {
|
|
Layout.alignment: Qt.AlignVCenter
|
|
spacing: 4
|
|
Text {
|
|
text: modeEnergie
|
|
? (totalWh >= 1000 ? (totalWh / 1000).toFixed(2) : totalWh.toFixed(1))
|
|
: valeurActuelle.toFixed(precisionAffichage)
|
|
font.pixelSize: Responsive.police(64, page_detail_batterie.width)
|
|
font.bold: true
|
|
color: couleur_blanc_casse
|
|
}
|
|
Text {
|
|
text: modeEnergie ? (totalWh >= 1000 ? "kWh" : "Wh") : uniteAffichage
|
|
font.pixelSize: Responsive.police(20, page_detail_batterie.width)
|
|
color: couleur_gris_pale
|
|
Layout.alignment: Qt.AlignTop
|
|
}
|
|
}
|
|
|
|
Item { Layout.fillWidth: true }
|
|
|
|
ColumnLayout {
|
|
visible: !modeEnergie
|
|
spacing: 10
|
|
RowLayout {
|
|
Text { text: qsTr("Max"); color: couleur_gris_pale; font.pixelSize: Responsive.police(13, page_detail_batterie.width); Layout.preferredWidth: 90 }
|
|
Text { text: valeurMax.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; font.pixelSize: Responsive.police(13, page_detail_batterie.width); Layout.preferredWidth: 100 }
|
|
Text { text: "[" + formater_date(tempsMax) + "]"; color: couleur_gris_moyen; font.pixelSize: Responsive.police(13, page_detail_batterie.width) }
|
|
}
|
|
RowLayout {
|
|
Text { text: qsTr("Moyen"); color: couleur_gris_pale; font.pixelSize: Responsive.police(13, page_detail_batterie.width); Layout.preferredWidth: 90 }
|
|
Text { text: valeurMoyenne.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; font.pixelSize: Responsive.police(13, page_detail_batterie.width); Layout.preferredWidth: 100 }
|
|
}
|
|
RowLayout {
|
|
Text { text: qsTr("Min"); color: couleur_gris_pale; font.pixelSize: Responsive.police(13, page_detail_batterie.width); Layout.preferredWidth: 90 }
|
|
Text { text: valeurMin.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; font.pixelSize: Responsive.police(13, page_detail_batterie.width); Layout.preferredWidth: 100 }
|
|
Text { text: "[" + formater_date(tempsMin) + "]"; color: couleur_gris_moyen; font.pixelSize: Responsive.police(13, page_detail_batterie.width) }
|
|
}
|
|
RowLayout {
|
|
Text { text: qsTr("Delta (Max - Min)"); color: couleur_gris_pale; font.pixelSize: Responsive.police(13, page_detail_batterie.width); Layout.preferredWidth: 160 }
|
|
Text { text: (valeurMax - valeurMin).toFixed(precisionAffichage + 1) + " " + uniteAffichage; color: couleur_blanc_casse; font.pixelSize: Responsive.police(13, page_detail_batterie.width) }
|
|
}
|
|
}
|
|
|
|
Text {
|
|
visible: modeEnergie
|
|
text: qsTr("Consommation totale sur la période - détail par circuit ci-dessous")
|
|
color: couleur_gris_pale
|
|
font.pixelSize: Responsive.police(13, page_detail_batterie.width)
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
spacing: 15
|
|
opacity: dataProvider.connecte ? 1.0 : 0.45
|
|
|
|
// --- Mode "variable simple" : graphique de valeur habituel ---
|
|
Canvas {
|
|
id: graphiqueVariable
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
visible: !modeEnergie
|
|
|
|
onPaint: {
|
|
var ctx = getContext("2d");
|
|
ctx.reset();
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
if (pointsCourbe.length < 2) {
|
|
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 tMin = pointsCourbe[0].t;
|
|
var tMax = pointsCourbe[pointsCourbe.length - 1].t;
|
|
|
|
var padding = (valeurMax - valeurMin) * 0.1 || 1;
|
|
var yMin = valeurMin - padding;
|
|
var yMax = valeurMax + padding;
|
|
|
|
function px(t) { return marge + ((t - tMin) / (tMax - tMin)) * largeurUtile; }
|
|
function py(v) { return marge + hauteurUtile - ((v - yMin) / (yMax - yMin)) * hauteurUtile; }
|
|
|
|
ctx.strokeStyle = "#333333";
|
|
ctx.fillStyle = couleur_gris_pale;
|
|
ctx.font = "11px sans-serif";
|
|
var nbLignesY = 5;
|
|
for (var i = 0; i <= nbLignesY; i++) {
|
|
var v = yMin + (i / nbLignesY) * (yMax - yMin);
|
|
var y = py(v);
|
|
ctx.beginPath();
|
|
ctx.moveTo(marge, y);
|
|
ctx.lineTo(width - marge, y);
|
|
ctx.stroke();
|
|
ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4);
|
|
}
|
|
|
|
// 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;
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
for (var k = 0; k < pointsCourbe.length; k++) {
|
|
var xx = px(pointsCourbe[k].t);
|
|
var yy = py(pointsCourbe[k].v);
|
|
if (k === 0) ctx.moveTo(xx, yy);
|
|
else ctx.lineTo(xx, yy);
|
|
}
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = "lightgreen";
|
|
ctx.beginPath();
|
|
ctx.arc(px(tempsMin), py(valeurMin), 4, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
ctx.beginPath();
|
|
ctx.arc(px(tempsMax), py(valeurMax), 4, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
// --- 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: Responsive.police(13, page_detail_batterie.width)
|
|
Layout.preferredWidth: parent.width * 0.4
|
|
elide: Text.ElideRight
|
|
}
|
|
Text {
|
|
text: modelData.energie_wh.toFixed(2) + " Wh"
|
|
color: couleur_blanc_casse
|
|
font.pixelSize: Responsive.police(13, page_detail_batterie.width)
|
|
Layout.preferredWidth: parent.width * 0.3
|
|
}
|
|
Text {
|
|
text: modelData.pourcentage.toFixed(1) + " %"
|
|
color: couleur_gris_pale
|
|
font.pixelSize: Responsive.police(13, page_detail_batterie.width)
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
spacing: 8
|
|
Repeater {
|
|
model: periodesLibelles
|
|
delegate: Rectangle {
|
|
Layout.preferredWidth: 140
|
|
Layout.preferredHeight: Math.max(40, Responsive.HAUTEUR_MIN_TACTILE)
|
|
radius: 8
|
|
color: (index === periodeIndex) ? shipheart_couleur_fond_bouton_actif : shipheart_couleur_fond_bouton
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: modelData
|
|
color: couleur_blanc_casse
|
|
font.pixelSize: Responsive.police(14, page_detail_batterie.width)
|
|
font.bold: (index === periodeIndex)
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: {
|
|
periodeIndex = index;
|
|
charger_courbe();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bouton retour :
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: Math.max(44, Responsive.HAUTEUR_MIN_TACTILE)
|
|
radius: 8
|
|
color: shipheart_couleur_fond_bouton
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: "↩"
|
|
font.pixelSize: Responsive.police(20, page_detail_batterie.width)
|
|
color: couleur_blanc_casse
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: stackView.pop()
|
|
}
|
|
}
|
|
}
|
|
}
|