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:
@@ -159,3 +159,41 @@ void DataProvider::get_mesures(const QString &type, int id, int periode, const Q
|
|||||||
emit mesuresRecues(type, id, periode, variable, obj);
|
emit mesuresRecues(type, id, periode, variable, obj);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DataProvider::get_energie(const QString &type, int id, int periode)
|
||||||
|
{
|
||||||
|
if (m_replyEnergieEnCours) m_replyEnergieEnCours->abort();
|
||||||
|
|
||||||
|
QUrl url(m_baseUrl + "/energie");
|
||||||
|
QUrlQuery query;
|
||||||
|
query.addQueryItem("type", type);
|
||||||
|
query.addQueryItem("id", QString::number(id));
|
||||||
|
query.addQueryItem("periode", QString::number(periode));
|
||||||
|
url.setQuery(query);
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setTransferTimeout(8000);
|
||||||
|
|
||||||
|
auto reply = m_manager.get(request);
|
||||||
|
m_replyEnergieEnCours = reply;
|
||||||
|
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, reply, type, id, periode]() {
|
||||||
|
reply->deleteLater();
|
||||||
|
if (m_replyEnergieEnCours == reply) m_replyEnergieEnCours = nullptr;
|
||||||
|
if (reply->error() == QNetworkReply::OperationCanceledError) return;
|
||||||
|
if (reply->error() != QNetworkReply::NoError) {
|
||||||
|
signalerEchec();
|
||||||
|
emit erreur(reply->errorString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto doc = QJsonDocument::fromJson(reply->readAll());
|
||||||
|
const auto obj = doc.object();
|
||||||
|
if (obj["status"].toString() != "ok") {
|
||||||
|
signalerEchec();
|
||||||
|
emit erreur(obj["message"].toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
signalerSucces();
|
||||||
|
emit energieRecue(type, id, periode, obj);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ public:
|
|||||||
// client), ignoré pour capteurs/reservoirs (variable fixe côté serveur).
|
// client), ignoré pour capteurs/reservoirs (variable fixe côté serveur).
|
||||||
Q_INVOKABLE void get_mesures(const QString &type, int id, int periode, const QString &variable = QString());
|
Q_INVOKABLE void get_mesures(const QString &type, int id, int periode, const QString &variable = QString());
|
||||||
|
|
||||||
|
// Consommation d'énergie (Phase 4, batteries uniquement pour l'instant) -
|
||||||
|
// GET /energie. Endpoint séparé de get_mesures() : réponse de forme très
|
||||||
|
// différente (barres + comparatif par circuit, pas une simple courbe).
|
||||||
|
Q_INVOKABLE void get_energie(const QString &type, int id, int periode);
|
||||||
|
|
||||||
bool connecte() const { return m_connecte; }
|
bool connecte() const { return m_connecte; }
|
||||||
bool aDejaEteConnecte() const { return m_derniereConnexionReussie.isValid(); }
|
bool aDejaEteConnecte() const { return m_derniereConnexionReussie.isValid(); }
|
||||||
QDateTime derniereConnexionReussie() const { return m_derniereConnexionReussie; }
|
QDateTime derniereConnexionReussie() const { return m_derniereConnexionReussie; }
|
||||||
@@ -59,6 +64,7 @@ signals:
|
|||||||
void elementsRecus(const QString &type, int famille, const QJsonArray &elements);
|
void elementsRecus(const QString &type, int famille, const QJsonArray &elements);
|
||||||
void circuitModifie(int id, bool succes);
|
void circuitModifie(int id, bool succes);
|
||||||
void mesuresRecues(const QString &type, int id, int periode, const QString &variable, const QJsonObject &donnees);
|
void mesuresRecues(const QString &type, int id, int periode, const QString &variable, const QJsonObject &donnees);
|
||||||
|
void energieRecue(const QString &type, int id, int periode, const QJsonObject &donnees);
|
||||||
void erreur(const QString &message);
|
void erreur(const QString &message);
|
||||||
void etatConnexionChange();
|
void etatConnexionChange();
|
||||||
|
|
||||||
@@ -74,4 +80,5 @@ private:
|
|||||||
QNetworkReply *m_replyFamillesEnCours = nullptr;
|
QNetworkReply *m_replyFamillesEnCours = nullptr;
|
||||||
QNetworkReply *m_replyElementsEnCours = nullptr;
|
QNetworkReply *m_replyElementsEnCours = nullptr;
|
||||||
QNetworkReply *m_replyMesuresEnCours = nullptr;
|
QNetworkReply *m_replyMesuresEnCours = nullptr;
|
||||||
|
QNetworkReply *m_replyEnergieEnCours = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
#include "dataprovider.h"
|
|
||||||
#include <QNetworkRequest>
|
|
||||||
#include <QNetworkReply>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QUrl>
|
|
||||||
#include <QUrlQuery>
|
|
||||||
|
|
||||||
DataProvider::DataProvider(QObject *parent) : QObject(parent)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void DataProvider::signalerSucces()
|
|
||||||
{
|
|
||||||
m_connecte = true;
|
|
||||||
m_derniereConnexionReussie = QDateTime::currentDateTime();
|
|
||||||
emit etatConnexionChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DataProvider::signalerEchec()
|
|
||||||
{
|
|
||||||
// Ne touche JAMAIS m_derniereConnexionReussie ici : on veut garder la
|
|
||||||
// trace du dernier succès réel, pas la remplacer par l'instant de l'échec.
|
|
||||||
if (m_connecte) {
|
|
||||||
m_connecte = false;
|
|
||||||
emit etatConnexionChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DataProvider::get_familles(const QString &type)
|
|
||||||
{
|
|
||||||
if (m_replyFamillesEnCours) {
|
|
||||||
m_replyFamillesEnCours->abort(); // Requête précédente encore en attente : remplacée par celle-ci
|
|
||||||
}
|
|
||||||
|
|
||||||
QUrl url(m_baseUrl + "/elements/familles");
|
|
||||||
QUrlQuery query;
|
|
||||||
query.addQueryItem("type", type);
|
|
||||||
url.setQuery(query);
|
|
||||||
|
|
||||||
QNetworkRequest request(url);
|
|
||||||
// Sans délai explicite, une requête vers un CM5 devenu injoignable peut
|
|
||||||
// rester en attente indéfiniment côté navigateur (ni succès ni échec) -
|
|
||||||
// 'finished' ne se déclencherait alors jamais, empêchant toute détection
|
|
||||||
// de perte de connexion.
|
|
||||||
request.setTransferTimeout(TIME_OUT_TRANSFER_DATA);
|
|
||||||
|
|
||||||
auto reply = m_manager.get(request);
|
|
||||||
m_replyFamillesEnCours = reply;
|
|
||||||
|
|
||||||
connect(reply, &QNetworkReply::finished, this, [this, reply, type]() {
|
|
||||||
reply->deleteLater();
|
|
||||||
if (m_replyFamillesEnCours == reply) m_replyFamillesEnCours = nullptr;
|
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::OperationCanceledError) {
|
|
||||||
return; // Annulée volontairement (remplacée par une requête plus récente) - pas un vrai échec réseau
|
|
||||||
}
|
|
||||||
if (reply->error() != QNetworkReply::NoError) {
|
|
||||||
signalerEchec();
|
|
||||||
emit erreur(reply->errorString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto doc = QJsonDocument::fromJson(reply->readAll());
|
|
||||||
const auto obj = doc.object();
|
|
||||||
if (obj["status"].toString() != "ok") {
|
|
||||||
signalerEchec();
|
|
||||||
emit erreur(obj["message"].toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
signalerSucces();
|
|
||||||
emit famillesRecues(type, obj["familles"].toArray());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void DataProvider::get_elements(const QString &type, int famille)
|
|
||||||
{
|
|
||||||
if (m_replyElementsEnCours) {
|
|
||||||
m_replyElementsEnCours->abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
QUrl url(m_baseUrl + "/elements");
|
|
||||||
QUrlQuery query;
|
|
||||||
query.addQueryItem("type", type);
|
|
||||||
query.addQueryItem("famille", QString::number(famille));
|
|
||||||
url.setQuery(query);
|
|
||||||
|
|
||||||
QNetworkRequest request(url);
|
|
||||||
request.setTransferTimeout(TIME_OUT_TRANSFER_DATA);
|
|
||||||
|
|
||||||
auto reply = m_manager.get(request);
|
|
||||||
m_replyElementsEnCours = reply;
|
|
||||||
|
|
||||||
connect(reply, &QNetworkReply::finished, this, [this, reply, type, famille]() {
|
|
||||||
reply->deleteLater();
|
|
||||||
if (m_replyElementsEnCours == reply) m_replyElementsEnCours = nullptr;
|
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::OperationCanceledError) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (reply->error() != QNetworkReply::NoError) {
|
|
||||||
signalerEchec();
|
|
||||||
emit erreur(reply->errorString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto doc = QJsonDocument::fromJson(reply->readAll());
|
|
||||||
const auto obj = doc.object();
|
|
||||||
if (obj["status"].toString() != "ok") {
|
|
||||||
signalerEchec();
|
|
||||||
emit erreur(obj["message"].toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
signalerSucces();
|
|
||||||
emit elementsRecus(type, famille, obj["elements"].toArray());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void DataProvider::set_circuit(int id, int consigne)
|
|
||||||
{
|
|
||||||
QUrl url(m_baseUrl + "/elements/circuit/" + QString::number(id));
|
|
||||||
QNetworkRequest request(url);
|
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
|
||||||
request.setTransferTimeout(TIME_OUT_TRANSFER_DATA);
|
|
||||||
|
|
||||||
QJsonObject body;
|
|
||||||
body["consigne"] = consigne;
|
|
||||||
const QByteArray data = QJsonDocument(body).toJson();
|
|
||||||
|
|
||||||
auto reply = m_manager.sendCustomRequest(request, "PATCH", data);
|
|
||||||
connect(reply, &QNetworkReply::finished, this, [this, reply, id]() {
|
|
||||||
reply->deleteLater();
|
|
||||||
const bool succes = (reply->error() == QNetworkReply::NoError);
|
|
||||||
if (succes) signalerSucces();
|
|
||||||
else {
|
|
||||||
signalerEchec();
|
|
||||||
emit erreur(reply->errorString());
|
|
||||||
}
|
|
||||||
emit circuitModifie(id, succes);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <QObject>
|
|
||||||
#include <QNetworkAccessManager>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QDateTime>
|
|
||||||
|
|
||||||
class QNetworkReply;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// DataProvider - couche d'accès aux données du client WASM (cas 2/3).
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Remplace, pour les domaines déjà portés, les appels bdd.* directs du cas 1
|
|
||||||
// (qui reste inchangé, cf décision §21.1 de la synthèse). Toute la logique
|
|
||||||
// HTTP/JSON est encapsulée ici - le QML ne voit que des signaux avec des
|
|
||||||
// tableaux déjà prêts à l'emploi.
|
|
||||||
//
|
|
||||||
// Portée actuelle : les 4 domaines (circuit, batterie, capteur, reservoir)
|
|
||||||
// en lecture, écriture (consigne/mode auto) sur circuit uniquement.
|
|
||||||
//
|
|
||||||
// Suit également l'état de connexion (succès/échec de chaque requête) pour
|
|
||||||
// permettre à l'UI d'afficher un état "hors ligne" explicite plutôt que de
|
|
||||||
// laisser les listes silencieusement vides ou figées (cf synthèse, gestion
|
|
||||||
// du hors-ligne).
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#define TIME_OUT_TRANSFER_DATA 5000 // En ms
|
|
||||||
|
|
||||||
|
|
||||||
class DataProvider : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
Q_PROPERTY(bool connecte READ connecte NOTIFY etatConnexionChange)
|
|
||||||
Q_PROPERTY(bool aDejaEteConnecte READ aDejaEteConnecte NOTIFY etatConnexionChange)
|
|
||||||
Q_PROPERTY(QDateTime derniereConnexionReussie READ derniereConnexionReussie NOTIFY etatConnexionChange)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit DataProvider(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
// TODO : rendre l'adresse de base configurable (IP locale du CM5 en cas 2,
|
|
||||||
// domaine VPS en cas 3 - cf QR code d'accès, §"Architecture cible"/Cas 2-3
|
|
||||||
// de la synthèse) - en dur sur le relais VPS pour cette étape.
|
|
||||||
Q_INVOKABLE void get_familles(const QString &type);
|
|
||||||
Q_INVOKABLE void get_elements(const QString &type, int famille);
|
|
||||||
|
|
||||||
// Écriture rapide sur un circuit (bouton d'accueil) - PATCH /elements/circuit/<id>.
|
|
||||||
Q_INVOKABLE void set_circuit(int id, int consigne);
|
|
||||||
|
|
||||||
bool connecte() const { return m_connecte; }
|
|
||||||
bool aDejaEteConnecte() const { return m_derniereConnexionReussie.isValid(); }
|
|
||||||
QDateTime derniereConnexionReussie() const { return m_derniereConnexionReussie; }
|
|
||||||
|
|
||||||
signals:
|
|
||||||
// Le type (et, pour les éléments, la famille) sont désormais inclus dans
|
|
||||||
// chaque signal - indispensable avec plusieurs domaines actifs en
|
|
||||||
// parallèle, pour que le QML sache à quelle réponse il a affaire.
|
|
||||||
void famillesRecues(const QString &type, const QJsonArray &familles);
|
|
||||||
void elementsRecus(const QString &type, int famille, const QJsonArray &elements);
|
|
||||||
void circuitModifie(int id, bool succes);
|
|
||||||
void erreur(const QString &message);
|
|
||||||
void etatConnexionChange();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void signalerSucces();
|
|
||||||
void signalerEchec();
|
|
||||||
|
|
||||||
QNetworkAccessManager m_manager;
|
|
||||||
QString m_baseUrl = QStringLiteral("https://api.ship2net.fr");
|
|
||||||
bool m_connecte = false;
|
|
||||||
QDateTime m_derniereConnexionReussie; // invalide (isValid()==false) tant qu'aucun succès n'a jamais eu lieu
|
|
||||||
|
|
||||||
// Requête en cours par type d'appel (familles/éléments) - permet d'annuler
|
|
||||||
// une requête encore en vol avant d'en émettre une nouvelle, pour éviter
|
|
||||||
// qu'une réponse "en retard" (envoyée pendant une coupure, résolue après
|
|
||||||
// qu'une requête plus récente ait déjà réussi) ne fasse clignoter l'état
|
|
||||||
// de connexion après une reconnexion.
|
|
||||||
QNetworkReply *m_replyFamillesEnCours = nullptr;
|
|
||||||
QNetworkReply *m_replyElementsEnCours = nullptr;
|
|
||||||
};
|
|
||||||
+196
-21
@@ -8,13 +8,16 @@ import qt.koriolan.shipheart 1.0
|
|||||||
// detail_batterie.qml - page de détail d'une batterie (valeur + historique).
|
// 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).
|
// Poussée sur le StackView depuis home.qml (clic court sur une batterie).
|
||||||
// Domaine batteries : sélecteur de variable (8 options), comme les circuits.
|
// Domaine batteries : sélecteur de variable (8 options simples + "Énergie").
|
||||||
// Le vrai défaut natif est le comparatif d'énergie (id_variable=0),
|
// "Énergie" (9e bouton) bascule vers une vue différente (barres + tableau
|
||||||
// volontairement laissé de côté (Phase 4, énergie, cf synthèse) - "SOC"
|
// comparatif par circuit) plutôt que le graphique de valeur habituel - cf
|
||||||
// choisi comme vue par défaut à la place, sur décision explicite.
|
// 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
|
// Mêmes simplifications volontaires que detail_capteur.qml pour le
|
||||||
// QtCharts, ligne continue plutôt qu'escalier, pas de zoom tactile).
|
// graphique de valeur (Canvas plutôt que QtCharts, ligne continue plutôt
|
||||||
|
// qu'escalier, pas de zoom tactile).
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
Shipheart_page {
|
Shipheart_page {
|
||||||
@@ -27,9 +30,11 @@ Shipheart_page {
|
|||||||
readonly property var periodesLibelles: [qsTr("1 heure"), qsTr("24 heures"), qsTr("7 jours"), qsTr("30 jours")]
|
readonly property var periodesLibelles: [qsTr("1 heure"), qsTr("24 heures"), qsTr("7 jours"), qsTr("30 jours")]
|
||||||
|
|
||||||
property int variableIndex: 0 // "soc" par défaut
|
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 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")]
|
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 var pointsCourbe: []
|
||||||
property real valeurMin: 0
|
property real valeurMin: 0
|
||||||
property real valeurMax: 0
|
property real valeurMax: 0
|
||||||
@@ -38,13 +43,18 @@ Shipheart_page {
|
|||||||
property double tempsMax: 0
|
property double tempsMax: 0
|
||||||
property string uniteAffichage: ""
|
property string uniteAffichage: ""
|
||||||
property int precisionAffichage: 1
|
property int precisionAffichage: 1
|
||||||
|
|
||||||
property real valeurActuelle: 0
|
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 {}
|
property DataProvider dataProvider: DataProvider {}
|
||||||
|
|
||||||
function charger_courbe() {
|
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) {
|
function formater_date(timestampMs) {
|
||||||
@@ -68,7 +78,15 @@ Shipheart_page {
|
|||||||
tempsMax = donnees.max.t;
|
tempsMax = donnees.max.t;
|
||||||
valeurMoyenne = donnees.moyenne;
|
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) {
|
function onErreur(message) {
|
||||||
@@ -97,8 +115,7 @@ Shipheart_page {
|
|||||||
color: couleur_gris_pale
|
color: couleur_gris_pale
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sélecteur de variable (8 options - grille 4x2 plutôt qu'une seule
|
// Sélecteur de variable (8 valeurs simples + énergie) :
|
||||||
// ligne, pour rester lisible) :
|
|
||||||
GridLayout {
|
GridLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
columns: 4
|
columns: 4
|
||||||
@@ -141,13 +158,15 @@ Shipheart_page {
|
|||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
spacing: 4
|
spacing: 4
|
||||||
Text {
|
Text {
|
||||||
text: valeurActuelle.toFixed(precisionAffichage)
|
text: modeEnergie
|
||||||
|
? (totalWh >= 1000 ? (totalWh / 1000).toFixed(2) : totalWh.toFixed(1))
|
||||||
|
: valeurActuelle.toFixed(precisionAffichage)
|
||||||
font.pixelSize: 64
|
font.pixelSize: 64
|
||||||
font.bold: true
|
font.bold: true
|
||||||
color: couleur_blanc_casse
|
color: couleur_blanc_casse
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
text: uniteAffichage
|
text: modeEnergie ? (totalWh >= 1000 ? "kWh" : "Wh") : uniteAffichage
|
||||||
font.pixelSize: 20
|
font.pixelSize: 20
|
||||||
color: couleur_gris_pale
|
color: couleur_gris_pale
|
||||||
Layout.alignment: Qt.AlignTop
|
Layout.alignment: Qt.AlignTop
|
||||||
@@ -157,6 +176,7 @@ Shipheart_page {
|
|||||||
Item { Layout.fillWidth: true }
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
visible: !modeEnergie
|
||||||
spacing: 10
|
spacing: 10
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Text { text: qsTr("Max"); color: couleur_gris_pale; Layout.preferredWidth: 90 }
|
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 { 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 {
|
RowLayout {
|
||||||
@@ -185,10 +212,12 @@ Shipheart_page {
|
|||||||
spacing: 15
|
spacing: 15
|
||||||
opacity: dataProvider.connecte ? 1.0 : 0.45
|
opacity: dataProvider.connecte ? 1.0 : 0.45
|
||||||
|
|
||||||
|
// --- Mode "variable simple" : graphique de valeur habituel ---
|
||||||
Canvas {
|
Canvas {
|
||||||
id: graphique
|
id: graphiqueVariable
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
visible: !modeEnergie
|
||||||
|
|
||||||
onPaint: {
|
onPaint: {
|
||||||
var ctx = getContext("2d");
|
var ctx = getContext("2d");
|
||||||
@@ -230,11 +259,43 @@ Shipheart_page {
|
|||||||
ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4);
|
ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
var nbLabelsX = 6;
|
// Quadrillage vertical ET étiquettes, ensemble, sur les mêmes
|
||||||
for (var j = 0; j <= nbLabelsX; j++) {
|
// paliers ronds - paliers de 5 min (1h), heure pleine (24h) ou
|
||||||
var t = tMin + (j / nbLabelsX) * (tMax - tMin);
|
// minuit (7j/30j). Remplace l'ancien double affichage (quadrillage
|
||||||
var x = px(t);
|
// sans étiquette + étiquettes à des positions non alignées).
|
||||||
ctx.fillText(Qt.formatDateTime(new Date(t), "HH:mm"), x - 18, height - 8);
|
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.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 {
|
ColumnLayout {
|
||||||
spacing: 8
|
spacing: 8
|
||||||
Repeater {
|
Repeater {
|
||||||
|
|||||||
+37
-6
@@ -203,12 +203,43 @@ Shipheart_page {
|
|||||||
ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4);
|
ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Étiquettes X :
|
// Quadrillage vertical ET étiquettes, ensemble, sur les mêmes
|
||||||
var nbLabelsX = 6;
|
// paliers ronds - paliers de 5 min (1h), heure pleine (24h) ou
|
||||||
for (var j = 0; j <= nbLabelsX; j++) {
|
// minuit (7j/30j). Remplace l'ancien double affichage (quadrillage
|
||||||
var t = tMin + (j / nbLabelsX) * (tMax - tMin);
|
// sans étiquette + étiquettes à des positions non alignées).
|
||||||
var x = px(t);
|
function prochain_palier_rond(instant, periodeIdx) {
|
||||||
ctx.fillText(Qt.formatDateTime(new Date(t), "HH:mm"), x - 18, height - 8);
|
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 :
|
// Ligne de la courbe :
|
||||||
|
|||||||
+37
-5
@@ -227,11 +227,43 @@ Shipheart_page {
|
|||||||
ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4);
|
ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
var nbLabelsX = 6;
|
// Quadrillage vertical ET étiquettes, ensemble, sur les mêmes
|
||||||
for (var j = 0; j <= nbLabelsX; j++) {
|
// paliers ronds - paliers de 5 min (1h), heure pleine (24h) ou
|
||||||
var t = tMin + (j / nbLabelsX) * (tMax - tMin);
|
// minuit (7j/30j). Remplace l'ancien double affichage (quadrillage
|
||||||
var x = px(t);
|
// sans étiquette + étiquettes à des positions non alignées).
|
||||||
ctx.fillText(Qt.formatDateTime(new Date(t), "HH:mm"), x - 18, height - 8);
|
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.strokeStyle = couleur_bleu_koriolan;
|
||||||
|
|||||||
@@ -194,12 +194,43 @@ Shipheart_page {
|
|||||||
ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4);
|
ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Étiquettes X :
|
// Quadrillage vertical ET étiquettes, ensemble, sur les mêmes
|
||||||
var nbLabelsX = 6;
|
// paliers ronds - paliers de 5 min (1h), heure pleine (24h) ou
|
||||||
for (var j = 0; j <= nbLabelsX; j++) {
|
// minuit (7j/30j). Remplace l'ancien double affichage (quadrillage
|
||||||
var t = tMin + (j / nbLabelsX) * (tMax - tMin);
|
// sans étiquette + étiquettes à des positions non alignées).
|
||||||
var x = px(t);
|
function prochain_palier_rond(instant, periodeIdx) {
|
||||||
ctx.fillText(Qt.formatDateTime(new Date(t), "HH:mm"), x - 18, height - 8);
|
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 :
|
// Ligne de la courbe :
|
||||||
|
|||||||
Reference in New Issue
Block a user