feat(§10 étape C): l'avancement est une différence, et le régime se publie
`sessionEnergy` est l'ACCUMULATEUR du plugin V2C, pas un compteur remis à zéro pour le moteur : l'écart avec `chargeEnergy` est constant au dix-millième (7,2044 kWh sur deux échantillons à deux heures d'écart), parce qu'il porte les incréments d'avant la dernière remise à zéro de la borne. Le lire directement ferait croire toute obligation éco tenue de 7,2 kWh dès le premier cycle — un nombre crédible et faux que rien ne signalerait. Contre-épreuve : lecture directe, le test annonce 7204. La base se prend à l'ouverture de l'OBLIGATION, jamais au démarrage de nymead : sinon un redémarrage en pleine session réinitialise l'avancement sur une valeur déjà accumulée, et la recharge de nuit relue au matin repart de zéro. Elle est RECAPTURÉE sur un recul franc de l'accumulateur — une nouvelle session — mais pas sur un recul infime, qui est du bruit de lecture et coûterait l'avancement acquis à chaque frémissement du compteur. Ce que le moteur ne refait PAS : la détection de remise à zéro de la BORNE vit dans le plugin, qui interroge à 30 s. Le moteur travaille à la minute et raterait des cycles entiers. Lire comment le plugin l'avait écrite a évité d'ajouter une seconde accumulation, moins fine, en concurrence de la première. Trois maillons manquants raccordés, tous déclarés et alimentés par personne : EvCharger n'exposait pas sessionEnergy ; LoadContextTelemetry::sessionWh n'était rempli nulle part ; et toLoadContext() ABANDONNAIT les besoins déclarés, si bien que LoadContext::needs n'était rempli par aucun adaptateur. L'obligation éco n'existait qu'en configuration. Mock : sessionEnergy ajouté à chargerPhaseSwitching UNIQUEMENT — `charger` reste sans, ce qui donne le cas unmeasurable nativement. La suite porte les deux régimes, comme le banc. Le régime est publié alors qu'aucune décision ne s'y appuie encore, et c'est délibéré : cela sépare publier le régime de décider avec, comme measurement l'a été de l'arbitrage, et rend R6 testable le jour où le motif naîtra au lieu d'un invariant posé sur du code que rien n'émet. Suite : simulation 56/56, charging 48/48 identique à la référence ligne à ligne, loadmodel 21/21, spotmarket 32/32, doxygen 0 avertissement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
This commit is contained in:
parent
f2ba9f4e30
commit
a8f8734fc2
@ -1,3 +1,39 @@
|
||||
powersync-energy-plugin-nymea (1.15.2+etm40) trixie; urgency=medium
|
||||
|
||||
* §10 étape C — L'AVANCEMENT BRANCHÉ EN LECTURE SEULE, et le RÉGIME publié (R5).
|
||||
`progress.regime` ∈ measured | estimated | unmeasurable, porté par le NIVEAU. Sous
|
||||
`unmeasurable`, `deliveredWh` est ABSENT et jamais 0 : une dégradation ne doit pas se lire
|
||||
comme un avancement nul (LM-1009 C1).
|
||||
* LE PIÈGE, mesuré et évité (LM-1012-b). `sessionEnergy` est l'ACCUMULATEUR du plugin V2C,
|
||||
pas un compteur remis à zéro pour nous : l'écart avec `chargeEnergy` est constant au
|
||||
dix-millième (7,2044 kWh sur deux échantillons à deux heures d'écart). Le lire directement
|
||||
ferait croire toute obligation tenue de 7,2 kWh dès le premier cycle. L'avancement est donc
|
||||
une DIFFÉRENCE depuis une base — contre-épreuve : lecture directe, le test annonce 7204.
|
||||
* Base prise à l'ouverture de l'OBLIGATION, jamais au démarrage de nymead : sinon une recharge
|
||||
de nuit relue au matin repart de zéro. RECAPTURÉE sur un recul franc de l'accumulateur
|
||||
(nouvelle session), pas sur un recul infime, qui est du bruit de lecture.
|
||||
* CE QUE LE MOTEUR NE REFAIT PAS : la détection de remise à zéro de la BORNE vit dans le
|
||||
plugin, qui interroge à 30 s ; le moteur travaille à la minute et raterait des cycles
|
||||
entiers. Regarder comment le plugin l'avait écrite a évité une seconde accumulation, moins
|
||||
fine, en concurrence de la première.
|
||||
* Trois maillons manquants raccordés au passage, tous déclarés et alimentés par personne :
|
||||
`EvCharger` n'exposait pas `sessionEnergy` ; `LoadContextTelemetry::sessionWh` n'était rempli
|
||||
nulle part ; et `toLoadContext()` ABANDONNAIT les besoins déclarés, si bien que
|
||||
`LoadContext::needs` n'était rempli par aucun adaptateur — l'obligation éco n'existait qu'en
|
||||
configuration.
|
||||
* Mock : `sessionEnergy` ajouté à `chargerPhaseSwitching` UNIQUEMENT. `charger` reste sans, ce
|
||||
qui donne le cas `unmeasurable` nativement — la suite porte les deux régimes, comme le banc.
|
||||
* Le régime est publié ALORS QU'AUCUNE DÉCISION NE S'Y APPUIE, délibérément : cela sépare
|
||||
publier le régime de décider avec, comme `measurement` l'a été de l'arbitrage, et rend R6
|
||||
testable le jour où le motif naîtra plutôt qu'un invariant sur du code que rien n'émet.
|
||||
* Rapport amont rédigé pour le SIGSEGV de `Logger::log()` (docs/upstream/) : « hors de ce
|
||||
lot » ne veut pas dire sans conséquence — un plantage du moteur de journalisation, c'est
|
||||
nymead qui tombe chez un client et le HEMS qui cesse de piloter.
|
||||
* Suite : simulation 56/56, charging 48/48 identique à la référence, loadmodel 21/21,
|
||||
spotmarket 32/32, doxygen 0 avertissement.
|
||||
|
||||
-- Patrick Schurig <etm.schurig@gmail.com> Mon, 31 Aug 2026 08:15:00 +0200
|
||||
|
||||
powersync-energy-plugin-nymea (1.15.2+etm39) trixie; urgency=medium
|
||||
|
||||
* §10 étape B — `draw` publie l'AUTORISATION, et elle se lit au lieu d'être forgée.
|
||||
|
||||
70
docs/upstream/RAPPORT_nymea_logger_sigsegv.md
Normal file
70
docs/upstream/RAPPORT_nymea_logger_sigsegv.md
Normal file
@ -0,0 +1,70 @@
|
||||
# Rapport amont — SIGSEGV dans `Logger::log()` (libnymea 1.15.2)
|
||||
|
||||
**À remonter à nymea.** Rédigé le 2026-08-31 ; le vidage est conservé (voir « Pièces »).
|
||||
|
||||
> **Pourquoi ce rapport plutôt qu'une ligne de changelog.** Le plantage n'a aucune trame ETM dans
|
||||
> sa pile — il est donc « hors de notre lot ». Mais *hors de notre lot* ne veut pas dire *sans
|
||||
> conséquence* : un plantage dans le moteur de journalisation, c'est **`nymead` qui tombe chez un
|
||||
> client**, et un HEMS qui cesse de piloter. Une box qui ne pilote plus ne se signale pas d'elle-même.
|
||||
|
||||
## Symptôme
|
||||
|
||||
`SIGSEGV` dans `Logger::log(const QList<QString>&, const QMap<QString, QVariant>&)`, atteint par
|
||||
un **événement posté** (`sendPostedEvents` → `QObject::event`) depuis `libnymea-core`.
|
||||
|
||||
```
|
||||
#0 Logger::log(QList<QString> const&, QMap<QString,QVariant> const&) libnymea.so.1 + 0x14ea24
|
||||
#1 ?? libnymea-core.so.1 + 0x1edae6
|
||||
#2 ?? libQt6Core.so.6 + 0x1e4403
|
||||
#3 QObject::event(QEvent*) libQt6Core.so.6
|
||||
#4 QCoreApplication::notifyInternal2(QObject*, QEvent*) libQt6Core.so.6
|
||||
#5 QCoreApplicationPrivate::sendPostedEvents(...) libQt6Core.so.6
|
||||
#6 ?? libQt6Core.so.6 + 0x3a8c57
|
||||
#7 g_main_context_iteration libglib-2.0.so.0
|
||||
```
|
||||
|
||||
Un seul fil actif ; les cinq autres sont en attente. Le processus tenait ouverte la base
|
||||
`/tmp/nymea-test/energylogs.sqlite` (moteur de journalisation SQLite).
|
||||
|
||||
## Reproductibilité — FAIBLE, et c'est le point
|
||||
|
||||
Observé **une fois** sur une exécution de suite de tests en tâche de fond. **Non reproduit** en
|
||||
trois séquences identiques ensuite, ni en exécution isolée (48/48, code de retour 0).
|
||||
|
||||
C'est précisément ce qui rend le rapport utile : à cette fréquence, le réflexe est de relancer
|
||||
jusqu'à ce que ça passe et de conclure à une « fragilité de test ». Le vidage a été récupéré au
|
||||
lieu d'être ignoré, et il montre que le plantage n'est **pas** dans le code de test.
|
||||
|
||||
## Hypothèse — à confirmer par quelqu'un qui a les symboles
|
||||
|
||||
L'enchaînement — événement **posté**, délivré depuis la boucle glib, sur un objet du moteur de
|
||||
journalisation, en fin de vie de processus — a la forme d'un **usage après libération** : une
|
||||
invocation en file d'attente délivrée à un `Logger` déjà détruit pendant l'arrêt.
|
||||
|
||||
**C'est une hypothèse, pas une conclusion.** La trame `libnymea-core.so.1 + 0x1edae6` n'a pas pu
|
||||
être symbolisée ici (paquet sans symboles de débogage). Avec `libnymea-dbgsym`, elle nommerait
|
||||
l'appelant en une commande.
|
||||
|
||||
Si l'hypothèse tient, elle a une conséquence de production : l'arrêt d'un `nymead` chargé peut
|
||||
laisser des écritures de journal en file, et le risque n'est pas limité aux tests.
|
||||
|
||||
## Environnement
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `libnymea1` | `1.15.2+202606191336~trixie1` |
|
||||
| Qt | `6.8.2+dfsg-9+deb13u2` |
|
||||
| Système | Debian 13 (trixie), x86_64 |
|
||||
| Binaire | suite de tests `charging` du plugin énergie ETM |
|
||||
|
||||
## Pièces
|
||||
|
||||
- Vidage conservé : `coredumpctl` du 2026-08-31 07:06:01 CEST, PID 115071, 2,7 Mio compressés.
|
||||
- Commande de symbolisation utile côté nymea :
|
||||
`gdb -batch -ex 'bt full' <binaire> <core>` avec `libnymea-dbgsym` installé.
|
||||
|
||||
## Ce qui est demandé
|
||||
|
||||
1. Symboliser la trame `libnymea-core.so.1 + 0x1edae6` — elle nomme l'appelant, et l'hypothèse
|
||||
tombe ou se confirme immédiatement.
|
||||
2. Vérifier la durée de vie du `Logger` vis-à-vis des événements postés à l'arrêt.
|
||||
@ -80,6 +80,11 @@ LoadContext EtmVariableLoadAdapter::toLoadContext(const QDateTime &now) const
|
||||
ctx.label = m_label;
|
||||
ctx.priority = m_priority;
|
||||
ctx.declared = descriptor().declared;
|
||||
// §10-C — les BESOINS déclarés atteignent enfin le cycle. Ils étaient posés dans le
|
||||
// descripteur et abandonnés ici : `LoadContext::needs` n'était rempli par personne, donc
|
||||
// l'obligation éco n'existait qu'en configuration. Transmis tels quels, sans être lus par
|
||||
// aucune décision à ce stade — la passe éco est l'étape suivante.
|
||||
ctx.needs = descriptor().needs;
|
||||
|
||||
ctx.telemetry.currentPowerW = telemetry().currentPowerW;
|
||||
ctx.telemetry.available = telemetry().available;
|
||||
|
||||
@ -107,6 +107,11 @@ LoadContext EvAdapter::toLoadContext(const QDateTime &now) const
|
||||
ctx.telemetry.available = m_charger->available();
|
||||
ctx.telemetry.pluggedIn = m_charger->pluggedIn();
|
||||
ctx.telemetry.charging = m_charger->charging();
|
||||
// §10-C — l'avancement MESURÉ, enfin alimenté. Le champ était déclaré depuis LM-1009 et
|
||||
// rempli nulle part : une échéance dont l'avancement ne se mesure pas est pire qu'une
|
||||
// échéance absente, parce qu'elle continue de s'afficher comme si elle tenait.
|
||||
ctx.telemetry.sessionMeasurable = m_charger->hasSessionEnergy();
|
||||
ctx.telemetry.sessionWh = m_charger->sessionEnergyWh();
|
||||
|
||||
// Pas de fenêtre de verrou de palier côté EV : lockMaxPowerW négatif = aucun plafond
|
||||
// (convention du contexte), et lockMinPowerW nul = rien d'engagé de non-coupable.
|
||||
|
||||
@ -196,7 +196,12 @@ LoadContext RelayRouter::toLoadContext(const QDateTime &now) const
|
||||
ctx.adapter = QStringLiteral("relay-router");
|
||||
ctx.label = m_label;
|
||||
ctx.priority = m_priority;
|
||||
ctx.declared = descriptor().declared; // powerLevels/maxPowerW dérivés (watts)
|
||||
ctx.declared = descriptor().declared;
|
||||
// §10-C — les BESOINS déclarés atteignent enfin le cycle. Ils étaient posés dans le
|
||||
// descripteur et abandonnés ici : `LoadContext::needs` n'était rempli par personne, donc
|
||||
// l'obligation éco n'existait qu'en configuration. Transmis tels quels, sans être lus par
|
||||
// aucune décision à ce stade — la passe éco est l'étape suivante.
|
||||
ctx.needs = descriptor().needs; // powerLevels/maxPowerW dérivés (watts)
|
||||
ctx.needs = m_needs;
|
||||
|
||||
ctx.telemetry.currentPowerW = telemetry().currentPowerW;
|
||||
|
||||
@ -446,6 +446,60 @@ void EnergyArbitrator::update(const QDateTime ¤tDateTime)
|
||||
publishTelemetry(false);
|
||||
}
|
||||
|
||||
namespace {
|
||||
//! Recul en deçà duquel on ne conclut PAS à une nouvelle session : bruit de lecture. Même
|
||||
//! principe que le seuil du plugin V2C, transposé en Wh.
|
||||
constexpr double BruitAvancementWh = 50.0;
|
||||
}
|
||||
|
||||
EcoProgress EnergyArbitrator::ecoProgress(const LoadContext &lc) const
|
||||
{
|
||||
EcoProgress p;
|
||||
p.targetWh = lc.needs.minEnergyWhPerDay;
|
||||
|
||||
// Pas d'obligation déclarée : rien à mesurer, et surtout rien à publier. Une charge sans
|
||||
// obligation éco n'aura jamais d'entrée « eco » — l'absence EST l'information (R1).
|
||||
if (p.targetWh <= 0) {
|
||||
m_ecoBaseWh.remove(lc.id);
|
||||
return p; // regime vide : l'appelant n'émet pas de niveau éco
|
||||
}
|
||||
|
||||
// C1 de LM-1009 — la borne ne compte pas : l'obligation n'est PAS mesurable, et ça se dit.
|
||||
// `deliveredWh` reste absent, jamais zéro : une dégradation ne doit pas se lire comme un
|
||||
// avancement nul, sans quoi l'échéance continue de s'afficher comme si elle tenait.
|
||||
if (!lc.telemetry.sessionMeasurable) {
|
||||
p.regime = QString::fromLatin1(ProgressRegime::Unmeasurable);
|
||||
m_ecoBaseWh.remove(lc.id);
|
||||
return p;
|
||||
}
|
||||
|
||||
const double sessionWh = lc.telemetry.sessionWh;
|
||||
|
||||
if (!m_ecoBaseWh.contains(lc.id)) {
|
||||
// OUVERTURE de l'obligation : la base se prend ICI. Pas au démarrage de nymead — la
|
||||
// reprendre là réinitialiserait l'avancement d'une session en cours sur une valeur
|
||||
// déjà accumulée, et une recharge de nuit relue au matin repartirait de zéro.
|
||||
m_ecoBaseWh.insert(lc.id, sessionWh);
|
||||
} else if (sessionWh < m_ecoBaseWh.value(lc.id) - BruitAvancementWh) {
|
||||
// RECUL FRANC de l'accumulateur : le plugin l'a remis à la valeur brute, donc une
|
||||
// nouvelle session s'est ouverte. On recapture, sinon la différence devient négative.
|
||||
// Ce n'est PAS la détection de remise à zéro de la borne — celle-là vit dans le plugin,
|
||||
// à 30 s, là où le moteur travaille à la minute et raterait des cycles entiers.
|
||||
qCInfo(dcNymeaEnergy()) << "[Arbitre] Avancement éco" << lc.id
|
||||
<< "— l'accumulateur a reculé de"
|
||||
<< (m_ecoBaseWh.value(lc.id) - sessionWh)
|
||||
<< "Wh : nouvelle session, base reprise à" << sessionWh << "Wh.";
|
||||
m_ecoBaseWh.insert(lc.id, sessionWh);
|
||||
}
|
||||
|
||||
p.regime = QString::fromLatin1(ProgressRegime::Measured);
|
||||
p.delivered = true;
|
||||
// Un recul INFIME laisse la base au-dessus de la valeur lue ; l'écrêtage évite un
|
||||
// avancement négatif sans masquer un vrai recul, qui a déjà provoqué la recapture.
|
||||
p.deliveredWh = qMax(0.0, sessionWh - m_ecoBaseWh.value(lc.id));
|
||||
return p;
|
||||
}
|
||||
|
||||
void EnergyArbitrator::shedWaterfallForOverload(const QDateTime &now)
|
||||
{
|
||||
m_shedActions.clear();
|
||||
@ -1424,6 +1478,26 @@ void EnergyArbitrator::buildTelemetry(const Plan &plan, const Slot &slot, const
|
||||
for (auto it = action.counts.constBegin(); it != action.counts.constEnd(); ++it)
|
||||
counts.insert(it.key(), it.value());
|
||||
niveau.insert(QStringLiteral("counts"), counts);
|
||||
|
||||
// R5 — LE RÉGIME D'AVANCEMENT, porté par le NIVEAU et non par la charge. Publié dès
|
||||
// maintenant, alors qu'aucune décision ne s'y appuie encore : c'est délibéré, et c'est
|
||||
// ce qui rendra R6 (`ECO_FLOOR_MET` interdit sous `unmeasurable`) testable le jour où le
|
||||
// motif naîtra. On sépare PUBLIER LE RÉGIME de DÉCIDER AVEC, exactement comme
|
||||
// `measurement` a été séparé de l'arbitrage.
|
||||
//
|
||||
// Absent quand aucune obligation n'est déclarée : une charge sans obligation éco ne
|
||||
// porte pas un avancement à zéro, elle n'en porte pas (R1).
|
||||
const EcoProgress avancement = ecoProgress(adapter->toLoadContext(now));
|
||||
if (!avancement.regime.isEmpty()) {
|
||||
QVariantMap progress;
|
||||
progress.insert(QStringLiteral("regime"), avancement.regime);
|
||||
progress.insert(QStringLiteral("targetWh"), avancement.targetWh);
|
||||
// `deliveredWh` ABSENT sous `unmeasurable`, jamais 0 : une dégradation ne doit pas
|
||||
// se lire comme un avancement nul (LM-1009 C1). C'est la moitié qui compte.
|
||||
if (avancement.delivered)
|
||||
progress.insert(QStringLiteral("deliveredWh"), avancement.deliveredWh);
|
||||
niveau.insert(QStringLiteral("progress"), progress);
|
||||
}
|
||||
entry.insert(QStringLiteral("levels"), QVariantList() << niveau);
|
||||
entry.insert(QStringLiteral("available"), adapter->telemetry().available);
|
||||
// R7 — LE MOTIF HÉRITÉ DIT DE QUEL NIVEAU IL PARLE. `decision` reste au niveau de la
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
// qui permet au rebuild incrémental (ECS-412) de comparer l'ancienne et la nouvelle config.
|
||||
#include "types/loadconfig.h"
|
||||
#include "types/drawcap.h"
|
||||
#include "types/ecoprogress.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QVariantMap>
|
||||
@ -344,6 +345,34 @@ public:
|
||||
*/
|
||||
QList<double> phaseDrawW(Electricity::Phases masque, uint limiteA) const;
|
||||
|
||||
/*!
|
||||
* \brief [§10-C] Avancement d'une obligation éco, et le régime qui dit ce qu'il vaut.
|
||||
*
|
||||
* \param lc Charge du cycle. \return L'avancement — \c regime toujours renseigné.
|
||||
*
|
||||
* \par La BASE, et les deux questions qu'elle pose
|
||||
* `sessionWh` est un ACCUMULATEUR de plugin, pas un compteur remis à zéro pour nous : côté
|
||||
* V2C, l'écart entre `sessionEnergy` et `chargeEnergy` est constant (7,2044 kWh mesurés sur
|
||||
* deux échantillons à deux heures d'écart), parce qu'il porte les incréments d'avant la
|
||||
* dernière remise à zéro de la borne. **Le lire directement ferait croire toute obligation
|
||||
* tenue de 7,2 kWh dès le premier cycle** — un nombre crédible et faux que rien ne
|
||||
* signalerait. L'avancement est donc une DIFFÉRENCE depuis une base.
|
||||
*
|
||||
* **Quand la base est-elle prise ?** À l'ouverture de l'OBLIGATION, jamais au démarrage de
|
||||
* nymead : la reprendre au démarrage réinitialiserait l'avancement d'une session en cours
|
||||
* sur une valeur déjà accumulée, et une recharge de nuit relue au matin repartirait de zéro.
|
||||
*
|
||||
* **Et si `sessionWh` RECULE ?** C'est une nouvelle session : le plugin remet son
|
||||
* accumulateur à la valeur brute à l'ouverture. La base est alors RECAPTURÉE, sinon la
|
||||
* différence devient négative. On ne refait pas la détection de remise à zéro de la borne —
|
||||
* le plugin la fait déjà, à 30 s, là où le moteur travaille à la minute et raterait des
|
||||
* cycles entiers. On ne détecte ici que le recul de l'accumulateur LUI-MÊME.
|
||||
*
|
||||
* \note Un recul infime est du bruit de lecture, pas une nouvelle session — même seuil de
|
||||
* principe que le plugin. En dessous, la base suit la valeur sans rien conclure.
|
||||
*/
|
||||
EcoProgress ecoProgress(const LoadContext &lc) const;
|
||||
|
||||
/*!
|
||||
* \brief [L4] Déleste les charges du waterfall tant que le plafond de tirage est dépassé.
|
||||
*
|
||||
@ -690,6 +719,10 @@ private:
|
||||
//! loadId → action de DÉLESTAGE L4 du cycle. Revendication : le dispatch saute ces charges,
|
||||
//! un seul commandeur par organe (règle absolue 1). Vidée à chaque évaluation de L4.
|
||||
QHash<QString, LoadAction> m_shedActions;
|
||||
//! loadId → énergie de session (Wh) au moment où l'obligation s'est OUVERTE. Base des
|
||||
//! différences d'avancement. Recapturée quand l'accumulateur de la borne recule (nouvelle
|
||||
//! session), jamais au démarrage de nymead — voir \c ecoProgress().
|
||||
mutable QHash<QString, double> m_ecoBaseWh;
|
||||
ThingManager *m_tm = nullptr; //!< ThingManager (pour construire les adaptateurs config).
|
||||
LoadConfigStore *m_loadConfigStore = nullptr; //!< Store config charge pilotée (non adopté).
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ HEADERS += \
|
||||
$$PWD/types/surpluscontext.h \
|
||||
$$PWD/types/plan.h \
|
||||
$$PWD/types/drawcap.h \
|
||||
$$PWD/types/ecoprogress.h \
|
||||
$$PWD/types/loadconfig.h \
|
||||
$$PWD/config/loadconfigstore.h \
|
||||
$$PWD/adapters/iloadadapter.h \
|
||||
|
||||
45
energyplugin/etm/types/ecoprogress.h
Normal file
45
energyplugin/etm/types/ecoprogress.h
Normal file
@ -0,0 +1,45 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
/*!
|
||||
* \file ecoprogress.h
|
||||
* \brief Avancement d'une obligation éco — et le RÉGIME qui dit ce que vaut cet avancement.
|
||||
*/
|
||||
|
||||
//! \brief Codes de régime publiés (R5). Opaques, comparés jamais traduits.
|
||||
namespace ProgressRegime {
|
||||
//! L'avancement est LU sur un compteur d'appareil. Le seul régime sur lequel on peut conclure.
|
||||
constexpr const char *Measured = "measured";
|
||||
//! L'avancement est une INTÉGRATION du moteur (LM-1009 D3) : une estimation, jamais une lecture.
|
||||
constexpr const char *Estimated = "estimated";
|
||||
//! Rien ne mesure l'avancement. État à part entière, et POSITIF : c'est C1 de LM-1009.
|
||||
constexpr const char *Unmeasurable = "unmeasurable";
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Avancement d'une obligation, avec son régime.
|
||||
*
|
||||
* \par Pourquoi le régime accompagne TOUJOURS la valeur
|
||||
* Un avancement mesuré et un avancement estimé ne se distinguent par aucune de leurs valeurs :
|
||||
* les deux sont un nombre qui monte. Si le régime ne se lit pas, le second se fait passer pour
|
||||
* le premier — C2 de \rule{LM-1009}, et la même doctrine que \c measurement.source pour la
|
||||
* puissance : **publier le régime, ne jamais le laisser déduire.**
|
||||
*/
|
||||
struct EcoProgress {
|
||||
//! Un des codes de \c ProgressRegime. Toujours renseigné quand l'obligation existe.
|
||||
QString regime;
|
||||
|
||||
//! \brief Vrai si \c deliveredWh a un sens. Faux sous \c unmeasurable — et le champ est
|
||||
//! alors ABSENT de la charge utile, jamais à 0 : une dégradation ne doit pas se lire
|
||||
//! comme un avancement nul (\rule{LM-1009} C1).
|
||||
bool delivered = false;
|
||||
|
||||
//! Énergie livrée depuis l'ouverture de l'obligation (Wh). Sans objet si \c !delivered.
|
||||
double deliveredWh = 0;
|
||||
|
||||
//! Énergie à livrer (Wh), déclarée par \c needs. 0 = aucune obligation en cours.
|
||||
double targetWh = 0;
|
||||
};
|
||||
@ -75,7 +75,24 @@ struct LoadContextTelemetry {
|
||||
// --- evcharger ---
|
||||
bool pluggedIn = false; //!< Véhicule branché. Une borne non branchée est ignorée du plan.
|
||||
bool charging = false; //!< Charge effectivement en cours (courant non nul côté borne).
|
||||
double sessionWh = 0; //!< Énergie chargée dans la session courante (Wh).
|
||||
/*!
|
||||
* \brief La borne COMPTE-T-ELLE l'énergie de sa session ? CAPACITÉ, pas valeur.
|
||||
*
|
||||
* Faux ⇒ \c sessionWh est sans objet, et l'avancement est \c unmeasurable. La question est
|
||||
* posée à la ThingClass : une session qui démarre mesure 0 Wh et reste mesurable. Sans ce
|
||||
* booléen, les deux seraient indistinguables et le régime basculerait d'un cycle à l'autre
|
||||
* sur une installation dont rien n'a changé (\rule{LM-1009} C1).
|
||||
*/
|
||||
bool sessionMeasurable = false;
|
||||
|
||||
/*!
|
||||
* \brief Énergie chargée dans la session courante (Wh) — ACCUMULATEUR, pas compteur brut.
|
||||
*
|
||||
* Côté V2C, le plugin l'accumule déjà à travers les remises à zéro de la borne, et le
|
||||
* **remet à la valeur brute à chaque nouvelle session**. Il peut donc RECULER. Le moteur ne
|
||||
* refait pas cette accumulation : il en prend une BASE et détecte le recul.
|
||||
*/
|
||||
double sessionWh = 0;
|
||||
// --- charges pilotées en watts : fenêtre de verrou (ECS-306) ---
|
||||
//! Plancher (W) imposé MAINTENANT par le verrou \c minOn : puissance déjà engagée que
|
||||
//! l'adaptateur ne peut pas couper. Le scheduler DOIT décrémenter le budget de cette
|
||||
|
||||
@ -299,6 +299,19 @@ ThingActionInfo* EvCharger::setChargingEnabled(bool power, const QDateTime &curr
|
||||
return info;
|
||||
}
|
||||
|
||||
bool EvCharger::hasSessionEnergy() const
|
||||
{
|
||||
return thing()->thingClass().hasStateType("sessionEnergy");
|
||||
}
|
||||
|
||||
double EvCharger::sessionEnergyWh() const
|
||||
{
|
||||
if (!hasSessionEnergy())
|
||||
return 0;
|
||||
// La borne publie des kWh (unit KiloWattHour) ; le modèle raisonne en Wh.
|
||||
return thing()->stateValue("sessionEnergy").toDouble() * 1000.0;
|
||||
}
|
||||
|
||||
bool EvCharger::pluggedIn() const
|
||||
{
|
||||
if (thing()->hasState("pluggedIn"))
|
||||
|
||||
@ -59,6 +59,26 @@ public:
|
||||
ThingActionInfo *setChargingEnabled(bool power, const QDateTime ¤tDateTime, bool force = false);
|
||||
|
||||
bool pluggedIn() const;
|
||||
|
||||
/*!
|
||||
* \brief [ETM §10-C] La borne COMPTE-T-ELLE l'énergie de sa session ?
|
||||
* \return Vrai si la ThingClass porte \c sessionEnergy — une CAPACITÉ, pas une valeur.
|
||||
* \note Question posée à la ThingClass, jamais à la valeur du moment : une session qui
|
||||
* démarre mesure 0 kWh et reste parfaitement mesurable. Même distinction que
|
||||
* \c LoadMeasurement::available (\rule{LM-1106}), et pour la même raison — sans elle, le
|
||||
* régime d'avancement basculerait d'un cycle à l'autre sur une installation inchangée.
|
||||
*/
|
||||
bool hasSessionEnergy() const;
|
||||
|
||||
/*!
|
||||
* \brief [ETM §10-C] Énergie livrée dans la session courante (Wh).
|
||||
* \return La valeur publiée par la borne, convertie en Wh ; 0 si l'état n'existe pas.
|
||||
* \note C'est un ACCUMULATEUR de plugin, pas un compteur brut : côté V2C il est monotone à
|
||||
* travers les remises à zéro de la borne, et **remis à la valeur brute à chaque nouvelle
|
||||
* session**. Il peut donc RECULER d'une session à l'autre. Le moteur ne refait pas cette
|
||||
* accumulation — il détecte le recul (\rule{LM-1012-b}).
|
||||
*/
|
||||
double sessionEnergyWh() const;
|
||||
bool charging() const;
|
||||
|
||||
uint maxChargingCurrentMaxValue() const;
|
||||
|
||||
@ -923,6 +923,40 @@ Comportement observé, non postulé : forçage interdit, la PAC de test **reste
|
||||
fait redescendre d'**état**, vers 2 et jamais 1 (`ECS-413`). C'est le cas que le bornage de la
|
||||
cascade (`LM-1210`) ne pouvait pas traiter — il empêche d'allouer, il ne retire rien à personne.
|
||||
|
||||
**LM-1012-b — l'avancement est une DIFFÉRENCE depuis une base, jamais une lecture directe.**
|
||||
*(§10 étape C, 2026-08-31. Mesuré sur le banc, et ça ne se déduisait pas du code.)*
|
||||
|
||||
`sessionEnergy` est l'**accumulateur du plugin**, pas un compteur remis à zéro pour le moteur.
|
||||
Côté V2C, l'écart avec `chargeEnergy` est constant au dix-millième — 7,2044 kWh sur deux
|
||||
échantillons à deux heures d'écart — parce qu'il porte les incréments d'avant la dernière remise
|
||||
à zéro de la borne. **Le lire directement ferait croire toute obligation éco tenue de 7,2 kWh dès
|
||||
le premier cycle** : un nombre crédible et faux, que rien ne signalerait.
|
||||
|
||||
**LM-1012-b-i — la base se prend à l'ouverture de l'OBLIGATION, jamais au démarrage de nymead.**
|
||||
La prendre au démarrage réinitialiserait l'avancement d'une session en cours sur une valeur déjà
|
||||
accumulée : une recharge de nuit relue au matin repartirait de zéro.
|
||||
|
||||
**LM-1012-b-ii — un recul FRANC de l'accumulateur recapture la base.** Le plugin le remet à la
|
||||
valeur brute à chaque nouvelle session ; sans recapture, la différence deviendrait négative. Un
|
||||
recul **infime** est du bruit de lecture et ne conclut à rien — la base reste, sinon l'avancement
|
||||
acquis se perdrait à chaque frémissement du compteur.
|
||||
|
||||
> **Ce que le moteur ne refait PAS.** La détection de remise à zéro de la BORNE vit dans le
|
||||
> plugin, qui interroge à 30 s. Le moteur travaille à la minute et raterait des cycles entiers ;
|
||||
> il ne détecte donc que le recul de l'**accumulateur lui-même**. Regarder comment le plugin l'a
|
||||
> fait avant de le refaire est ce qui a évité d'écrire une seconde accumulation, moins fine, en
|
||||
> concurrence de la première.
|
||||
|
||||
**LM-1012-c — le RÉGIME d'avancement se publie, porté par le niveau (R5).**
|
||||
`progress.regime` ∈ `measured` \| `estimated` \| `unmeasurable`. Sous `unmeasurable`,
|
||||
`deliveredWh` est **absent, jamais 0** : une dégradation ne doit pas se lire comme un avancement
|
||||
nul, sans quoi l'échéance continue de s'afficher comme si elle tenait (`LM-1009` C1).
|
||||
|
||||
Le régime est publié **alors qu'aucune décision ne s'y appuie encore**, et c'est délibéré : cela
|
||||
sépare **publier le régime** de **décider avec**, exactement comme `measurement` a été séparé de
|
||||
l'arbitrage. C'est ce qui rendra **R6** — `ECO_FLOOR_MET` interdit sous `unmeasurable` — testable
|
||||
le jour où le motif naîtra, au lieu d'un invariant posé sur un code que rien n'émet.
|
||||
|
||||
**LM-1011 — `BELOW_MIN_POWER` prime sur `DRAW_CAP` dès qu'il reste de l'autorisation.**
|
||||
*(Précédence tranchée par Patrick le 2026-08-29, sur une question de l'agent app. Écrite ICI,
|
||||
une fois, pour qu'aucun côté de la frontière ne l'établisse pour son compte.)*
|
||||
|
||||
@ -6683,6 +6683,103 @@ void Simulation::testDrawAuthorisationSurvivesTheBatteryReserve()
|
||||
#endif
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief [§10 étape C] L'avancement est une DIFFÉRENCE depuis une base, et le régime se publie.
|
||||
*
|
||||
* \par Le piège, mesuré sur le banc le 2026-08-31
|
||||
* `sessionEnergy` est l'ACCUMULATEUR du plugin V2C, pas un compteur remis à zéro pour nous :
|
||||
* l'écart avec `chargeEnergy` est constant au dix-millième (7,2044 kWh sur deux échantillons à
|
||||
* deux heures d'écart), parce qu'il porte les incréments d'avant la dernière remise à zéro de la
|
||||
* borne. **Le lire directement ferait croire toute obligation tenue de 7,2 kWh dès le premier
|
||||
* cycle** — un nombre crédible et faux que rien ne signalerait.
|
||||
*
|
||||
* \par Les deux questions que la base pose, et leurs réponses
|
||||
* 1. **Quand est-elle prise ?** À l'ouverture de l'OBLIGATION, jamais au démarrage de nymead :
|
||||
* sinon un redémarrage en pleine session réinitialise l'avancement sur une valeur déjà
|
||||
* accumulée, et la recharge de nuit relue au matin repart de zéro.
|
||||
* 2. **Que devient-elle si `sessionWh` recule ?** Le plugin remet son accumulateur à la valeur
|
||||
* brute à chaque nouvelle session : la base est RECAPTURÉE, sinon la différence devient
|
||||
* négative. On ne refait pas la détection de remise à zéro de la BORNE — elle vit dans le
|
||||
* plugin, à 30 s, là où le moteur travaille à la minute et raterait des cycles entiers.
|
||||
*
|
||||
* \warning **Ce test devient faux** si `sessionEnergy` cesse d'être un accumulateur de plugin
|
||||
* pour devenir un compteur remis à zéro par session : la recapture serait alors sans objet, et
|
||||
* c'est l'assertion 4 qu'il faudrait retirer, pas le moteur.
|
||||
*/
|
||||
void Simulation::testEcoProgressBaseAndRegime()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
QSKIP("nécessite ETM_ARBITRATOR.");
|
||||
#else
|
||||
cleanupTestCase();
|
||||
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
|
||||
initTestCase();
|
||||
|
||||
EnergyArbitrator *arb = dynamic_cast<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
|
||||
QVERIFY(arb);
|
||||
|
||||
LoadContext lc;
|
||||
lc.id = "borne-eco";
|
||||
lc.needs.minEnergyWhPerDay = 10000; // obligation : 10 kWh par jour
|
||||
|
||||
// 1. SANS obligation déclarée, il n'y a rien à publier — pas un avancement à zéro. Une
|
||||
// charge sans obligation éco ne porte pas d'entrée « eco » : l'absence EST l'information.
|
||||
LoadContext sansObligation = lc;
|
||||
sansObligation.needs.minEnergyWhPerDay = 0;
|
||||
QVERIFY2(arb->ecoProgress(sansObligation).regime.isEmpty(),
|
||||
"sans obligation, aucun régime : le niveau éco n'existe pas");
|
||||
|
||||
// 2. Borne qui ne COMPTE PAS : régime `unmeasurable`, et `deliveredWh` SANS OBJET.
|
||||
// C1 de LM-1009 — une dégradation ne doit jamais se lire comme un avancement nul, sinon
|
||||
// l'échéance continue de s'afficher comme si elle tenait.
|
||||
LoadContext nonMesurable = lc;
|
||||
nonMesurable.telemetry.sessionMeasurable = false;
|
||||
nonMesurable.telemetry.sessionWh = 7204; // valeur présente mais SANS SIGNIFICATION
|
||||
const EcoProgress sansMesure = arb->ecoProgress(nonMesurable);
|
||||
QCOMPARE(sansMesure.regime, QString(ProgressRegime::Unmeasurable));
|
||||
QCOMPARE(sansMesure.delivered, false);
|
||||
QVERIFY2(qFuzzyIsNull(sansMesure.deliveredWh),
|
||||
"sous unmeasurable, deliveredWh n'est pas publié — et vaut donc 0 en interne");
|
||||
|
||||
// 3. LE PIÈGE LUI-MÊME. L'accumulateur vaut déjà 7204 Wh à l'ouverture de l'obligation.
|
||||
// L'avancement doit être NUL, pas 7204 : la base est prise ICI.
|
||||
LoadContext mesuree = lc;
|
||||
mesuree.telemetry.sessionMeasurable = true;
|
||||
mesuree.telemetry.sessionWh = 7204;
|
||||
const EcoProgress ouverture = arb->ecoProgress(mesuree);
|
||||
QCOMPARE(ouverture.regime, QString(ProgressRegime::Measured));
|
||||
QCOMPARE(ouverture.delivered, true);
|
||||
QCOMPARE(qRound(ouverture.deliveredWh), 0);
|
||||
QCOMPARE(qRound(ouverture.targetWh), 10000);
|
||||
|
||||
// …et l'avancement suit la DIFFÉRENCE, pas la valeur lue.
|
||||
mesuree.telemetry.sessionWh = 9204;
|
||||
QCOMPARE(qRound(arb->ecoProgress(mesuree).deliveredWh), 2000);
|
||||
|
||||
// 4. RECUL FRANC : le plugin a remis son accumulateur à la valeur brute d'une nouvelle
|
||||
// session. La base est recapturée — sans quoi la différence serait NÉGATIVE.
|
||||
mesuree.telemetry.sessionWh = 120;
|
||||
QCOMPARE(qRound(arb->ecoProgress(mesuree).deliveredWh), 0);
|
||||
mesuree.telemetry.sessionWh = 620;
|
||||
QCOMPARE(qRound(arb->ecoProgress(mesuree).deliveredWh), 500);
|
||||
|
||||
// 5. Recul INFIME : c'est du bruit de lecture, pas une nouvelle session. La base ne bouge
|
||||
// pas — la recapturer ici perdrait l'avancement acquis à chaque frémissement du compteur.
|
||||
mesuree.telemetry.sessionWh = 610;
|
||||
const EcoProgress bruit = arb->ecoProgress(mesuree);
|
||||
QCOMPARE(qRound(bruit.deliveredWh), 490);
|
||||
QVERIFY2(bruit.deliveredWh >= 0, "un avancement ne peut pas être négatif");
|
||||
|
||||
// 6. L'obligation retirée LIBÈRE la base : une nouvelle obligation repart d'un repère neuf,
|
||||
// et n'hérite pas de l'avancement d'une obligation qui n'existe plus.
|
||||
LoadContext retiree = mesuree;
|
||||
retiree.needs.minEnergyWhPerDay = 0;
|
||||
arb->ecoProgress(retiree);
|
||||
mesuree.telemetry.sessionWh = 5000;
|
||||
QCOMPARE(qRound(arb->ecoProgress(mesuree).deliveredWh), 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::testCountsSumToTarget()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
|
||||
@ -194,6 +194,7 @@ private slots:
|
||||
void testPhaseAllowanceReadsTheStateItTests();
|
||||
void testDrawCapBoundsTheCascade();
|
||||
void testDrawAuthorisationSurvivesTheBatteryReserve();
|
||||
void testEcoProgressBaseAndRegime();
|
||||
void testARefusedStateIsDistinguishableFromAnApplied();
|
||||
void testL4ShedsAPacAndSaysItForcedTheLock();
|
||||
|
||||
|
||||
@ -12,8 +12,13 @@
|
||||
"name": "meter",
|
||||
"displayName": "Mocked Meter",
|
||||
"id": "2721a051-6e12-471a-baba-21d87c4cebc9",
|
||||
"createMethods": ["user"],
|
||||
"interfaces": ["energymeter", "connectable"],
|
||||
"createMethods": [
|
||||
"user"
|
||||
],
|
||||
"interfaces": [
|
||||
"energymeter",
|
||||
"connectable"
|
||||
],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "7abcc8a1-08b1-45bc-9116-10f9848359f9",
|
||||
@ -112,7 +117,7 @@
|
||||
"displayName": "Current power phase B",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "cfdc65bf-8e5a-40dd-acf3-79f3b86fd808",
|
||||
@ -128,7 +133,7 @@
|
||||
"displayName": "Total energy consumed",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0.00
|
||||
"defaultValue": 0.0
|
||||
},
|
||||
{
|
||||
"id": "0420b758-e77f-4cf5-a30b-a6e1235b1efd",
|
||||
@ -136,7 +141,7 @@
|
||||
"displayName": "Total energy produced",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0.00
|
||||
"defaultValue": 0.0
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -144,8 +149,14 @@
|
||||
"name": "charger",
|
||||
"displayName": "Mocked charger",
|
||||
"id": "5a3ae99f-c7da-46df-9104-f477be4606b7",
|
||||
"createMethods": ["user"],
|
||||
"interfaces": ["evcharger", "smartmeterconsumer", "connectable"],
|
||||
"createMethods": [
|
||||
"user"
|
||||
],
|
||||
"interfaces": [
|
||||
"evcharger",
|
||||
"smartmeterconsumer",
|
||||
"connectable"
|
||||
],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "652624a2-8f9a-4bc3-b34f-5e3492af4d30",
|
||||
@ -159,7 +170,15 @@
|
||||
"name": "phases",
|
||||
"displayName": "Phases connected",
|
||||
"type": "QString",
|
||||
"allowedValues": ["A", "B", "C", "AB", "BC", "AC", "ABC"],
|
||||
"allowedValues": [
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"AB",
|
||||
"BC",
|
||||
"AC",
|
||||
"ABC"
|
||||
],
|
||||
"defaultValue": "A"
|
||||
},
|
||||
{
|
||||
@ -195,7 +214,7 @@
|
||||
"displayName": "Maximum charging current",
|
||||
"displayNameAction": "Set maximum charging current",
|
||||
"type": "double",
|
||||
"defaultValue":6,
|
||||
"defaultValue": 6,
|
||||
"minValue": 6,
|
||||
"maxValue": 32,
|
||||
"unit": "Ampere",
|
||||
@ -239,7 +258,15 @@
|
||||
"name": "usedPhases",
|
||||
"displayName": "Used phases",
|
||||
"type": "QString",
|
||||
"possibleValues": ["A", "B", "C", "AB", "BC", "AC", "ABC"],
|
||||
"possibleValues": [
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"AB",
|
||||
"BC",
|
||||
"AC",
|
||||
"ABC"
|
||||
],
|
||||
"defaultValue": "A"
|
||||
},
|
||||
{
|
||||
@ -304,7 +331,7 @@
|
||||
"displayName": "Current power phase B",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "859d3a00-5896-44a9-b1f4-e44757079c44",
|
||||
@ -335,8 +362,14 @@
|
||||
"name": "chargerPhaseSwitching",
|
||||
"displayName": "Mocked charger with phase count switching",
|
||||
"id": "9208d9f0-280c-469d-a145-106f3277470c",
|
||||
"createMethods": ["user"],
|
||||
"interfaces": ["evcharger", "smartmeterconsumer", "connectable"],
|
||||
"createMethods": [
|
||||
"user"
|
||||
],
|
||||
"interfaces": [
|
||||
"evcharger",
|
||||
"smartmeterconsumer",
|
||||
"connectable"
|
||||
],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "d4492038-51bf-4f3c-8b93-89af4d8edd6b",
|
||||
@ -350,7 +383,15 @@
|
||||
"name": "phases",
|
||||
"displayName": "Phases connected",
|
||||
"type": "QString",
|
||||
"allowedValues": ["A", "B", "C", "AB", "BC", "AC", "ABC"],
|
||||
"allowedValues": [
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"AB",
|
||||
"BC",
|
||||
"AC",
|
||||
"ABC"
|
||||
],
|
||||
"defaultValue": "ABC"
|
||||
},
|
||||
{
|
||||
@ -388,7 +429,7 @@
|
||||
"displayName": "Maximum charging current",
|
||||
"displayNameAction": "Set maximum charging current",
|
||||
"type": "double",
|
||||
"defaultValue":6,
|
||||
"defaultValue": 6,
|
||||
"minValue": 6,
|
||||
"maxValue": 32,
|
||||
"unit": "Ampere",
|
||||
@ -402,6 +443,14 @@
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "7c1e4a92-3f6b-4d18-9a54-2b8e0d5f1c73",
|
||||
"name": "sessionEnergy",
|
||||
"displayName": "Session energy",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0.0
|
||||
},
|
||||
{
|
||||
"id": "fdd5c075-72f8-48e1-bc07-5ec69bae6179",
|
||||
"name": "pluggedIn",
|
||||
@ -434,7 +483,15 @@
|
||||
"name": "usedPhases",
|
||||
"displayName": "Used phases",
|
||||
"type": "QString",
|
||||
"possibleValues": ["A", "B", "C", "AB", "BC", "AC", "ABC"],
|
||||
"possibleValues": [
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"AB",
|
||||
"BC",
|
||||
"AC",
|
||||
"ABC"
|
||||
],
|
||||
"defaultValue": "A"
|
||||
},
|
||||
{
|
||||
@ -443,7 +500,10 @@
|
||||
"displayName": "Desired number of charging phases",
|
||||
"displayNameAction": "Set desired number of charging phases",
|
||||
"type": "uint",
|
||||
"possibleValues": [1, 3],
|
||||
"possibleValues": [
|
||||
1,
|
||||
3
|
||||
],
|
||||
"defaultValue": 1,
|
||||
"minValue": 1,
|
||||
"maxValue": 3,
|
||||
@ -511,7 +571,7 @@
|
||||
"displayName": "Current power phase B",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "acc9e040-5ed9-4813-843b-89fa974c9ed5",
|
||||
@ -542,8 +602,13 @@
|
||||
"name": "simpleCharger",
|
||||
"displayName": "Mocked charger simple",
|
||||
"id": "29bcf255-b654-4764-be92-399bc26fe7c3",
|
||||
"createMethods": ["user"],
|
||||
"interfaces": ["evcharger", "connectable"],
|
||||
"createMethods": [
|
||||
"user"
|
||||
],
|
||||
"interfaces": [
|
||||
"evcharger",
|
||||
"connectable"
|
||||
],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "e94f863b-47a2-44e4-8103-bb4b8a817f47",
|
||||
@ -585,7 +650,7 @@
|
||||
"displayName": "Maximum charging current",
|
||||
"displayNameAction": "Set maximum charging current",
|
||||
"type": "double",
|
||||
"defaultValue":6,
|
||||
"defaultValue": 6,
|
||||
"minValue": 6,
|
||||
"maxValue": 32,
|
||||
"unit": "Ampere",
|
||||
@ -597,8 +662,12 @@
|
||||
"name": "car",
|
||||
"displayName": "Mocked car",
|
||||
"id": "4513f801-836e-40a7-8784-c02650a9bdc6",
|
||||
"createMethods": ["user"],
|
||||
"interfaces": ["electricvehicle"],
|
||||
"createMethods": [
|
||||
"user"
|
||||
],
|
||||
"interfaces": [
|
||||
"electricvehicle"
|
||||
],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "29533bc8-d71e-4ce5-8ce3-bf87f0370391",
|
||||
@ -702,8 +771,12 @@
|
||||
"name": "notification",
|
||||
"displayName": "Mocked notification",
|
||||
"id": "ee1871b8-46f9-4784-bbe1-e33db16b8753",
|
||||
"createMethods": ["user"],
|
||||
"interfaces": ["notifications"],
|
||||
"createMethods": [
|
||||
"user"
|
||||
],
|
||||
"interfaces": [
|
||||
"notifications"
|
||||
],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "b3d60a6b-05f5-43f9-9ba0-04fb185c7825",
|
||||
@ -713,7 +786,7 @@
|
||||
"defaultValue": 6659
|
||||
}
|
||||
],
|
||||
"stateTypes": [ ],
|
||||
"stateTypes": [],
|
||||
"actionTypes": [
|
||||
{
|
||||
"id": "28a02de9-0e93-42d7-88bf-539e024f8945",
|
||||
@ -744,8 +817,12 @@
|
||||
"name": "energyStorage",
|
||||
"displayName": "Mocked energy storage",
|
||||
"id": "d0d5bbf0-249c-46ed-ac6a-5f271b2b0b0f",
|
||||
"createMethods": ["user"],
|
||||
"interfaces": ["energystorage"],
|
||||
"createMethods": [
|
||||
"user"
|
||||
],
|
||||
"interfaces": [
|
||||
"energystorage"
|
||||
],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "b074a76f-6382-48b9-b101-1d13904f30c8",
|
||||
@ -821,8 +898,12 @@
|
||||
"name": "powerSwitch",
|
||||
"displayName": "Mocked Power Switch (relais ECS)",
|
||||
"id": "841f8905-d1d7-4053-909f-01123b497747",
|
||||
"createMethods": ["user"],
|
||||
"interfaces": ["power"],
|
||||
"createMethods": [
|
||||
"user"
|
||||
],
|
||||
"interfaces": [
|
||||
"power"
|
||||
],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "e3398429-45fd-4add-a789-4d11bfd9560f",
|
||||
@ -864,7 +945,9 @@
|
||||
"name": "etmVariableLoad",
|
||||
"displayName": "Mocked etmvariableload (ECS/routeur à puissance pilotable)",
|
||||
"id": "b7c41f0a-3e2d-4a91-9f6b-2c8e1d5a7b40",
|
||||
"createMethods": ["user"],
|
||||
"createMethods": [
|
||||
"user"
|
||||
],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "c8d52a1b-4f3e-4b02-a07c-3d9f2e6b8c51",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user