fix(etm): ECS-410 — échec d'écriture relais, échelle bornée et défaut collant
writeRelay() jetait le ThingActionInfo* : aucun acquittement, aucun retour arrière, available codé en dur à true, et m_currentStage mis à jour comme si tout avait réussi — on annonçait une puissance non appliquée. MODÈLE ASYNCHRONE. executeAction est asynchrone et update() ne doit jamais attendre (AGENTS règle 5). « Attendre le résultat » ne veut donc pas dire bloquer le cycle : l'écriture est émise, l'adaptateur retient combien d'acquittements il attend (m_pending), le verdict tombe quand le compteur retombe à zéro, et la conséquence est traitée au cycle suivant. Motif repris tel quel d'EvCharger (evcharger.cpp:293-299), y compris `this` en contexte de connexion — si l'adaptateur meurt, les callbacks sont coupés proprement. INDÉTERMINATION. Pendant une transition, telemetry() annonce max(m_stagePrev, m_stageTarget) : on ne sait pas ce qui est fermé, on annonce donc la plus haute des deux puissances possibles. Même direction qu'ECS-411 (relais injoignable supposé fermé) — ne jamais annoncer moins que ce qui peut être appliqué. Sous-estimer fait sur-allouer les charges suivantes ; surestimer ne fait que retarder une montée. ÉCHELLE BORNÉE à trois barreaux, une tentative chacun, aucune boucle : cible → retour arrière → arrêt total → défaut. Le retour arrière est asynchrone au même titre et passe par le même compteur. DÉFAUT COLLANT, pas clignotant. m_faulted est un verrou posé une seule fois, sans délai ni expiration : available ne peut pas osciller d'un cycle à l'autre. Seul NymeaEnergy.ClearLoadFault le lève — acte délibéré et journalisé de l'opérateur. La reconstruction le lève aussi, mais par construction : un adaptateur neuf n'a pas d'historique. À la levée, l'état matériel est RELU (ECS-411), pas supposé. CANAL OUVERT. LoadContext n'avait AUCUN champ available : le publier aurait été décoratif. Ajouté à LoadContextTelemetry, avec sa sémantique écrite noir sur blanc — il gouverne l'allocation, PAS la comptabilité. Une charge en défaut ne reçoit rien mais reste comptée : une puissance qu'on ne sait plus couper est de la conso fixe, au même titre que la base de la maison. Le figeage est porté par lockMin == lockMax == puissance crue engagée, jamais un plafond nul sous un plancher non nul. Le même canal servira ECS-601. OPTIMIZER_PROTOCOL.md mis à jour dans le MÊME lot, comme l'exige le §11 de la spec : available, lockMinPowerW et lockMaxPowerW documentés avec leur sémantique. clearFault() est PURE VIRTUELLE sur ILoadAdapter : les trois autres adaptateurs la déclarent sans effet plutôt que d'hériter d'un défaut vide. Leur généralisation est portée par ECS-414. Test testEcsPartialFailure : cas nominal sans défaut, puis échelle complète via un relais introuvable — défaut atteint, relais valide bien ramené à l'ouverture par la tentative d'arrêt total, available faux, plancher == plafond == puissance comptée, plus aucune commande une heure plus tard, puis levée délibérée. Build amd64 0 erreur. Simulation : 16/16. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6ff8c7d67a
commit
5ec101fbe4
@ -150,6 +150,25 @@ Notes de schéma :
|
|||||||
- `forecast` : contenu défini par `CONTRACT_A_openmeteo.md`. Peut être `null`
|
- `forecast` : contenu défini par `CONTRACT_A_openmeteo.md`. Peut être `null`
|
||||||
(l'optimiseur peut sourcer ses propres prévisions — il est libre).
|
(l'optimiseur peut sourcer ses propres prévisions — il est libre).
|
||||||
|
|
||||||
|
### `loads[].telemetry` — champs ETM non encore normalisés
|
||||||
|
|
||||||
|
Trois champs ajoutés par l'implémentation ETM et **absents des exemples ci-dessus**.
|
||||||
|
Ils sont documentés ici parce qu'ils changent la lecture du contexte, et qu'une
|
||||||
|
implémentation d'optimiseur qui les ignore prendra de mauvaises décisions.
|
||||||
|
|
||||||
|
| Champ | Type | Sens |
|
||||||
|
|---|---|---|
|
||||||
|
| `available` | bool | **Gouverne l'ALLOCATION, pas la COMPTABILITÉ.** Faux ⇒ la charge ne doit recevoir **aucune** consigne neuve — mais elle **continue d'être comptée** pour ce qu'elle consomme via `currentPowerW`. Un optimiseur qui se contenterait de « sauter » les charges indisponibles sauterait aussi leur comptabilité, et offrirait aux charges suivantes un budget qui n'existe pas. |
|
||||||
|
| `lockMinPowerW` | W | Plancher imposé **maintenant** par un verrou anti-rebond : puissance déjà engagée que l'adaptateur ne peut pas couper. Le budget DOIT être décrémenté de cette valeur **même si elle dépasse le budget disponible** — sinon les charges de rang suivant reçoivent un résidu surestimé et l'installation soutire au réseau. |
|
||||||
|
| `lockMaxPowerW` | W | Plafond imposé **maintenant** par un verrou : au-delà, l'adaptateur refusera de démarrer. **Négatif ⇒ aucun plafond** (et non « plafond nul »). |
|
||||||
|
|
||||||
|
Une charge en défaut d'écriture est **figée** : `available = false` et
|
||||||
|
`lockMinPowerW == lockMaxPowerW ==` la puissance crue engagée. Le plancher n'est
|
||||||
|
jamais supérieur au plafond — un tel couple ferait dérailler tout écrêtage.
|
||||||
|
|
||||||
|
Ces champs sont exprimés **en watts**, jamais en index de palier : aucun
|
||||||
|
identifiant de relais ni index de combinaison ne franchit cette frontière.
|
||||||
|
|
||||||
## 6. Réponse : plan par créneaux
|
## 6. Réponse : plan par créneaux
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@ -111,6 +111,11 @@ public:
|
|||||||
void updateSoftConfig(int priority, const LoadNeeds &needs) override
|
void updateSoftConfig(int priority, const LoadNeeds &needs) override
|
||||||
{ m_priority = priority; m_needs = needs; }
|
{ m_priority = priority; m_needs = needs; }
|
||||||
|
|
||||||
|
//! \brief Sans effet : cet adaptateur n'implémente pas encore l'échelle de défaut ECS-410.
|
||||||
|
//! La généralisation est portée par **ECS-414** (lot de mise en configuration
|
||||||
|
//! du SgReadyAdapter). Déclaré explicitement plutôt qu'hérité d'un défaut vide.
|
||||||
|
void clearFault() override {}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Dernière consigne (W) effectivement écrite (avant écrêtage thing).
|
* \brief Dernière consigne (W) effectivement écrite (avant écrêtage thing).
|
||||||
* \return Consigne commandée en W, bornée à \c maxPowerW ; 0 tant qu'aucune action
|
* \return Consigne commandée en W, bornée à \c maxPowerW ; 0 tant qu'aucune action
|
||||||
|
|||||||
@ -76,6 +76,11 @@ public:
|
|||||||
void updateSoftConfig(int priority, const LoadNeeds &needs) override
|
void updateSoftConfig(int priority, const LoadNeeds &needs) override
|
||||||
{ Q_UNUSED(priority) Q_UNUSED(needs) }
|
{ Q_UNUSED(priority) Q_UNUSED(needs) }
|
||||||
|
|
||||||
|
//! \brief Sans effet : l'EV n'implémente pas encore l'échelle de défaut ECS-410.
|
||||||
|
//! La généralisation est portée par **ECS-414** (lot de mise en configuration
|
||||||
|
//! du SgReadyAdapter). Déclaré explicitement plutôt qu'hérité d'un défaut vide.
|
||||||
|
void clearFault() override {}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Borne VE sous-jacente (lecture).
|
* \brief Borne VE sous-jacente (lecture).
|
||||||
* \return Pointeur non-propriétaire vers l'EvCharger ; jamais nul (fixé à la construction).
|
* \return Pointeur non-propriétaire vers l'EvCharger ; jamais nul (fixé à la construction).
|
||||||
|
|||||||
@ -92,4 +92,18 @@ public:
|
|||||||
* la config ignorerait sans bruit les changements de rang.
|
* la config ignorerait sans bruit les changements de rang.
|
||||||
*/
|
*/
|
||||||
virtual void updateSoftConfig(int priority, const LoadNeeds &needs) = 0;
|
virtual void updateSoftConfig(int priority, const LoadNeeds &needs) = 0;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Lève le verrou de défaut de la charge (ECS-410).
|
||||||
|
*
|
||||||
|
* Le défaut est COLLANT par conception — « cesser toute commande jusqu'à intervention ».
|
||||||
|
* Sa levée est un acte délibéré de l'opérateur, jamais une expiration : sans quoi
|
||||||
|
* \c available oscillerait d'un cycle à l'autre et le waterfall réagirait à chaque
|
||||||
|
* battement.
|
||||||
|
*
|
||||||
|
* \note **Pure virtuelle à dessein.** Tous les adaptateurs ne savent pas encore tomber en
|
||||||
|
* défaut — seul \c RelayRouter implémente l'échelle ECS-410 — mais chacun doit le
|
||||||
|
* DÉCLARER. La généralisation est portée par **ECS-414**.
|
||||||
|
*/
|
||||||
|
virtual void clearFault() = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
#include <integrations/thing.h>
|
#include <integrations/thing.h>
|
||||||
#include <types/action.h>
|
#include <types/action.h>
|
||||||
#include <types/param.h>
|
#include <types/param.h>
|
||||||
|
#include <integrations/thingactioninfo.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
//! Au-delà, 2^N combinaisons explose. 16 relais = 65536 combos — bien au-delà de tout ECS réel.
|
//! Au-delà, 2^N combinaisons explose. 16 relais = 65536 combos — bien au-delà de tout ECS réel.
|
||||||
@ -93,7 +94,7 @@ LoadDescriptor RelayRouter::descriptor() const
|
|||||||
LoadTelemetry RelayRouter::telemetry() const
|
LoadTelemetry RelayRouter::telemetry() const
|
||||||
{
|
{
|
||||||
LoadTelemetry t;
|
LoadTelemetry t;
|
||||||
t.available = true;
|
t.available = !m_faulted; // ECS-410 : en défaut, la charge sort de l'ARBITRAGE…
|
||||||
t.lastActionAt = m_lastActionAt;
|
t.lastActionAt = m_lastActionAt;
|
||||||
|
|
||||||
// currentPowerW :
|
// currentPowerW :
|
||||||
@ -118,6 +119,19 @@ LoadTelemetry RelayRouter::telemetry() const
|
|||||||
if (!metered && m_currentStage > 0 && m_currentStage < m_levels.size())
|
if (!metered && m_currentStage > 0 && m_currentStage < m_levels.size())
|
||||||
power = m_levels.at(m_currentStage); // nominal commandé
|
power = m_levels.at(m_currentStage); // nominal commandé
|
||||||
|
|
||||||
|
// ECS-410 — INDÉTERMINATION. Tant que des écritures sont en vol, on ne sait pas ce qui est
|
||||||
|
// réellement fermé : on annonce la PLUS HAUTE des deux puissances possibles. Même direction
|
||||||
|
// qu'ECS-411 (relais injoignable supposé fermé) : ne JAMAIS annoncer moins que ce qui peut
|
||||||
|
// être appliqué. Sous-estimer fait sur-allouer les charges suivantes et soutirer au réseau ;
|
||||||
|
// surestimer ne fait que retarder une montée.
|
||||||
|
if (!metered && m_pending > 0) {
|
||||||
|
const int hi = qMax(m_stagePrev, m_stageTarget);
|
||||||
|
if (hi >= 0 && hi < m_levels.size())
|
||||||
|
power = qMax(power, static_cast<double>(m_levels.at(hi)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// …mais PAS de la COMPTABILITÉ : une puissance qu'on ne sait plus couper reste consommée.
|
||||||
|
// La retirer du bilan offrirait aux charges suivantes un budget qui n'existe pas.
|
||||||
t.currentPowerW = power;
|
t.currentPowerW = power;
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
@ -139,6 +153,17 @@ LoadContext RelayRouter::toLoadContext(const QDateTime &now) const
|
|||||||
// internes en puissances via la table de paliers. Sans ce canal, le scheduler décrémente
|
// internes en puissances via la table de paliers. Sans ce canal, le scheduler décrémente
|
||||||
// le budget d'une valeur que l'adaptateur va écrêter, et les charges suivantes reçoivent
|
// le budget d'une valeur que l'adaptateur va écrêter, et les charges suivantes reçoivent
|
||||||
// un résidu surestimé pendant toute la fenêtre minOn (AGENTS règle 4).
|
// un résidu surestimé pendant toute la fenêtre minOn (AGENTS règle 4).
|
||||||
|
ctx.telemetry.available = !m_faulted;
|
||||||
|
|
||||||
|
if (m_faulted) {
|
||||||
|
// ECS-410 — charge FIGÉE, ni montée ni descente : plancher ET plafond à la puissance
|
||||||
|
// crue engagée. Un plafond nul avec un plancher non nul serait contradictoire et
|
||||||
|
// ferait dérailler le qBound du scheduler.
|
||||||
|
ctx.telemetry.lockMinPowerW = ctx.telemetry.currentPowerW;
|
||||||
|
ctx.telemetry.lockMaxPowerW = ctx.telemetry.currentPowerW;
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
int minStage, maxStage;
|
int minStage, maxStage;
|
||||||
lockWindow(now, minStage, maxStage);
|
lockWindow(now, minStage, maxStage);
|
||||||
ctx.telemetry.lockMinPowerW = m_levels.at(minStage);
|
ctx.telemetry.lockMinPowerW = m_levels.at(minStage);
|
||||||
@ -157,6 +182,14 @@ LoadAction RelayRouter::applyAction(const LoadAction &action, const QDateTime &n
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ECS-410 — en défaut, on CESSE TOUTE COMMANDE, y compris les replis forcés : le matériel
|
||||||
|
// n'a pas répondu à trois tentatives, en réémettre n'apporterait rien et masquerait l'état.
|
||||||
|
if (m_faulted) {
|
||||||
|
LoadAction refused = action;
|
||||||
|
refused.powerW = refused.estimatedPowerW = currentSetpointW();
|
||||||
|
return refused;
|
||||||
|
}
|
||||||
|
|
||||||
// ECS-412 — ARMEMENT PARESSEUX du verrou (démarrage à froid). Au premier \c now reçu,
|
// ECS-412 — ARMEMENT PARESSEUX du verrou (démarrage à froid). Au premier \c now reçu,
|
||||||
// on estampille la « dernière commutation » : le verrou est donc armé pour sa durée
|
// on estampille la « dernière commutation » : le verrou est donc armé pour sa durée
|
||||||
// configurée, puis EXPIRE naturellement.
|
// configurée, puis EXPIRE naturellement.
|
||||||
@ -324,13 +357,110 @@ int RelayRouter::deduceStageFromThings() const
|
|||||||
return m_levels.size() - 1;
|
return m_levels.size() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RelayRouter::applyRelayStage(int stage)
|
void RelayRouter::writeRelay(const QString &thingId, bool on)
|
||||||
|
{
|
||||||
|
Thing *relay = m_thingManager ? m_thingManager->findConfiguredThing(ThingId(thingId)) : nullptr;
|
||||||
|
if (!relay) {
|
||||||
|
qCWarning(dcNymeaEnergy()) << "[RelayRouter]" << m_label << "— relais non trouvé:" << thingId;
|
||||||
|
m_writeFailed = true; // compte comme un échec : ECS-410 §1
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StateType powerStateType = relay->thingClass().stateTypes().findByName("power");
|
||||||
|
if (powerStateType.id().isNull()) {
|
||||||
|
relay->setStateValue("power", on); // repli mock : synchrone, toujours réussi
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Action powerAction(powerStateType.id(), relay->id(), Action::TriggeredByRule);
|
||||||
|
powerAction.setParams(ParamList() << Param(powerStateType.id(), on));
|
||||||
|
ThingActionInfo *info = m_thingManager->executeAction(powerAction);
|
||||||
|
if (!info) {
|
||||||
|
m_writeFailed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECS-410 — on n'attend PAS : le cycle rend la main immédiatement (AGENTS règle 5).
|
||||||
|
// Le verdict arrive par signal, et la conséquence est traitée au cycle suivant.
|
||||||
|
// `this` en contexte de connexion : si l'adaptateur meurt, le callback est coupé
|
||||||
|
// proprement — motif repris d'EvCharger::setChargingEnabled (evcharger.cpp:293-299).
|
||||||
|
++m_pending;
|
||||||
|
connect(info, &ThingActionInfo::finished, this, [this, info]() {
|
||||||
|
if (info->status() != Thing::ThingErrorNoError) {
|
||||||
|
m_writeFailed = true;
|
||||||
|
qCWarning(dcNymeaEnergy()) << "[RelayRouter]" << m_label
|
||||||
|
<< "— écriture relais en échec, status" << info->status();
|
||||||
|
}
|
||||||
|
if (--m_pending == 0)
|
||||||
|
settleTransition();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void RelayRouter::settleTransition()
|
||||||
|
{
|
||||||
|
// Appelé quand TOUTES les écritures d'une étape sont acquittées. Fait avancer l'échelle
|
||||||
|
// d'ECS-410 d'un cran, une tentative par cran, sans jamais boucler.
|
||||||
|
if (!m_writeFailed) {
|
||||||
|
m_stagePrev = m_currentStage; // transition confirmée : l'indétermination cesse
|
||||||
|
m_stageTarget = m_currentStage;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_writeFailed = false;
|
||||||
|
|
||||||
|
switch (m_phase) {
|
||||||
|
case PhaseNominale:
|
||||||
|
// Barreau 2 — retour à l'état précédent.
|
||||||
|
qCWarning(dcNymeaEnergy()) << "[RelayRouter]" << m_label
|
||||||
|
<< "— échec d'écriture : retour au palier" << m_levels.at(m_stagePrev) << "W.";
|
||||||
|
m_phase = PhaseRepli;
|
||||||
|
m_currentStage = m_stagePrev;
|
||||||
|
emitRelayWrites(m_stagePrev);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PhaseRepli:
|
||||||
|
// Barreau 3 — arrêt total. Dernière tentative.
|
||||||
|
qCWarning(dcNymeaEnergy()) << "[RelayRouter]" << m_label
|
||||||
|
<< "— retour arrière en échec : arrêt total.";
|
||||||
|
m_phase = PhaseArretTotal;
|
||||||
|
m_currentStage = 0;
|
||||||
|
m_stageTarget = 0;
|
||||||
|
emitRelayWrites(0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PhaseArretTotal:
|
||||||
|
case PhaseDefaut:
|
||||||
|
// Fin de l'échelle : verdict. Verrou COLLANT — aucune commande jusqu'à
|
||||||
|
// ClearLoadFault (ECS-410 §4). Il ne se lève pas tout seul : pas de clignotement.
|
||||||
|
qCCritical(dcNymeaEnergy()) << "[RelayRouter]" << m_label
|
||||||
|
<< "— arrêt total en échec : charge EN DÉFAUT, plus aucune"
|
||||||
|
<< "commande émise. Levée par NymeaEnergy.ClearLoadFault.";
|
||||||
|
m_phase = PhaseDefaut;
|
||||||
|
m_faulted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RelayRouter::clearFault()
|
||||||
|
{
|
||||||
|
if (!m_faulted)
|
||||||
|
return;
|
||||||
|
qCInfo(dcNymeaEnergy()) << "[RelayRouter]" << m_label
|
||||||
|
<< "— défaut levé par l'opérateur (ClearLoadFault).";
|
||||||
|
m_faulted = false;
|
||||||
|
m_phase = PhaseNominale;
|
||||||
|
m_writeFailed = false;
|
||||||
|
// L'état matériel réel est inconnu après un défaut : on le relit plutôt que de le supposer
|
||||||
|
// (même principe qu'ECS-411, et jamais moins que ce qui peut être appliqué).
|
||||||
|
m_currentStage = deduceStageFromThings();
|
||||||
|
m_stagePrev = m_stageTarget = m_currentStage;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RelayRouter::emitRelayWrites(int stage)
|
||||||
{
|
{
|
||||||
// Set CIBLE du palier (delta complet : chaque relais connu amené à son état on/off cible).
|
// Set CIBLE du palier (delta complet : chaque relais connu amené à son état on/off cible).
|
||||||
// Gère les mappings NON-CASCADÉS (monter d'un palier peut éteindre des relais).
|
// Gère les mappings NON-CASCADÉS (monter d'un palier peut éteindre des relais).
|
||||||
const QSet<QString> wantOn = [&]() {
|
const QSet<QString> wantOn = [&]() {
|
||||||
QSet<QString> s;
|
QSet<QString> s;
|
||||||
if (stage < m_relayMapping.size())
|
if (stage >= 0 && stage < m_relayMapping.size())
|
||||||
for (const QString &id : m_relayMapping.at(stage))
|
for (const QString &id : m_relayMapping.at(stage))
|
||||||
s.insert(id);
|
s.insert(id);
|
||||||
return s;
|
return s;
|
||||||
@ -341,22 +471,6 @@ void RelayRouter::applyRelayStage(int stage)
|
|||||||
for (const QString &id : list)
|
for (const QString &id : list)
|
||||||
allRelays.insert(id);
|
allRelays.insert(id);
|
||||||
|
|
||||||
auto writeRelay = [&](const QString &thingId, bool on) {
|
|
||||||
Thing *relay = m_thingManager->findConfiguredThing(ThingId(thingId));
|
|
||||||
if (!relay) {
|
|
||||||
qCWarning(dcNymeaEnergy()) << "[RelayRouter]" << m_label << "— relais non trouvé:" << thingId;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
StateType powerStateType = relay->thingClass().stateTypes().findByName("power");
|
|
||||||
if (!powerStateType.id().isNull()) {
|
|
||||||
Action powerAction(powerStateType.id(), relay->id(), Action::TriggeredByRule);
|
|
||||||
powerAction.setParams(ParamList() << Param(powerStateType.id(), on));
|
|
||||||
m_thingManager->executeAction(powerAction);
|
|
||||||
} else {
|
|
||||||
relay->setStateValue("power", on); // repli mock
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// off-before-on : couper d'abord les relais hors-cible, puis enclencher la cible → pas de
|
// off-before-on : couper d'abord les relais hors-cible, puis enclencher la cible → pas de
|
||||||
// sur-puissance transitoire (somme des deux paliers) sur une transition non-cascadée.
|
// sur-puissance transitoire (somme des deux paliers) sur une transition non-cascadée.
|
||||||
for (const QString &id : allRelays)
|
for (const QString &id : allRelays)
|
||||||
@ -364,4 +478,17 @@ void RelayRouter::applyRelayStage(int stage)
|
|||||||
writeRelay(id, false);
|
writeRelay(id, false);
|
||||||
for (const QString &id : wantOn)
|
for (const QString &id : wantOn)
|
||||||
writeRelay(id, true);
|
writeRelay(id, true);
|
||||||
|
|
||||||
|
// Chemin entièrement synchrone (mock, ou aucun relais) : aucun acquittement n'arrivera,
|
||||||
|
// le verdict se prend donc tout de suite.
|
||||||
|
if (m_pending == 0)
|
||||||
|
settleTransition();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RelayRouter::applyRelayStage(int stage)
|
||||||
|
{
|
||||||
|
m_stagePrev = m_currentStage;
|
||||||
|
m_stageTarget = stage;
|
||||||
|
m_writeFailed = false;
|
||||||
|
emitRelayWrites(stage);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include "iloadadapter.h"
|
#include "iloadadapter.h"
|
||||||
|
|
||||||
|
class ThingActionInfo;
|
||||||
#include "../types/loadconfig.h" // LoadConfigRelay
|
#include "../types/loadconfig.h" // LoadConfigRelay
|
||||||
|
|
||||||
class Thing;
|
class Thing;
|
||||||
@ -93,7 +95,27 @@ public:
|
|||||||
//! l'adaptateur — donc sans réarmer les verrous ni recommuter les relais.
|
//! l'adaptateur — donc sans réarmer les verrous ni recommuter les relais.
|
||||||
void updateSoftConfig(int priority, const LoadNeeds &needs) override;
|
void updateSoftConfig(int priority, const LoadNeeds &needs) override;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Lève le verrou de défaut (ECS-410, RPC \c NymeaEnergy.ClearLoadFault).
|
||||||
|
* \note Le défaut est COLLANT par conception — « cesser toute commande jusqu'à
|
||||||
|
* intervention ». Sa levée est donc un acte délibéré et tracé de l'opérateur, jamais
|
||||||
|
* une expiration. L'état matériel réel étant inconnu après un défaut, il est RELU
|
||||||
|
* (même principe qu'ECS-411) et non supposé.
|
||||||
|
*/
|
||||||
|
void clearFault() override;
|
||||||
|
|
||||||
|
//! \return Vrai si la charge est en défaut (plus aucune commande émise).
|
||||||
|
bool faulted() const { return m_faulted; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
//! Barreaux de l'échelle ECS-410. Une tentative par barreau, jamais de boucle.
|
||||||
|
enum Phase { PhaseNominale, PhaseRepli, PhaseArretTotal, PhaseDefaut };
|
||||||
|
|
||||||
|
void writeRelay(const QString &thingId, bool on);
|
||||||
|
void emitRelayWrites(int stage);
|
||||||
|
//! Verdict pris quand toutes les écritures d'une étape sont acquittées.
|
||||||
|
void settleTransition();
|
||||||
|
|
||||||
//! Palier le plus haut dont la puissance ≤ \p powerW (≥ 0).
|
//! Palier le plus haut dont la puissance ≤ \p powerW (≥ 0).
|
||||||
int stageForPower(double powerW) const;
|
int stageForPower(double powerW) const;
|
||||||
/*!
|
/*!
|
||||||
@ -124,4 +146,12 @@ private:
|
|||||||
int m_currentStage = 0;
|
int m_currentStage = 0;
|
||||||
QDateTime m_lastSwitch; //!< Dernier changement (null = jamais).
|
QDateTime m_lastSwitch; //!< Dernier changement (null = jamais).
|
||||||
QDateTime m_lastActionAt;
|
QDateTime m_lastActionAt;
|
||||||
|
|
||||||
|
// --- ECS-410 : suivi asynchrone des écritures ---------------------------------------
|
||||||
|
int m_pending = 0; //!< Écritures émises non encore acquittées.
|
||||||
|
bool m_writeFailed = false; //!< Au moins un acquittement en erreur sur l'étape courante.
|
||||||
|
int m_stagePrev = 0; //!< Palier avant la transition en cours.
|
||||||
|
int m_stageTarget = 0; //!< Palier visé par la transition en cours.
|
||||||
|
Phase m_phase = PhaseNominale;
|
||||||
|
bool m_faulted = false; //!< Verrou de défaut. COLLANT : seul clearFault() le lève.
|
||||||
};
|
};
|
||||||
|
|||||||
@ -95,6 +95,11 @@ public:
|
|||||||
void updateSoftConfig(int priority, const LoadNeeds &needs) override
|
void updateSoftConfig(int priority, const LoadNeeds &needs) override
|
||||||
{ Q_UNUSED(priority) Q_UNUSED(needs) }
|
{ Q_UNUSED(priority) Q_UNUSED(needs) }
|
||||||
|
|
||||||
|
//! \brief Sans effet : la PAC SG-Ready n'implémente pas encore l'échelle de défaut ECS-410.
|
||||||
|
//! La généralisation est portée par **ECS-414** (lot de mise en configuration
|
||||||
|
//! du SgReadyAdapter). Déclaré explicitement plutôt qu'hérité d'un défaut vide.
|
||||||
|
void clearFault() override {}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief État SG-Ready courant (1-4).
|
* \brief État SG-Ready courant (1-4).
|
||||||
* \return Dernier état réellement commuté ; valeur initiale tant qu'aucune transition
|
* \return Dernier état réellement commuté ; valeur initiale tant qu'aucune transition
|
||||||
|
|||||||
@ -156,6 +156,18 @@ bool EnergyArbitrator::sameHardware(const LoadConfig &a, const LoadConfig &b)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool EnergyArbitrator::clearLoadFault(const QString &loadId)
|
||||||
|
{
|
||||||
|
ILoadAdapter *adapter = m_loadAdapters.value(loadId, nullptr);
|
||||||
|
if (!adapter) {
|
||||||
|
qCWarning(dcNymeaEnergy()) << "[Arbitre] ClearLoadFault : charge inconnue" << loadId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
qCInfo(dcNymeaEnergy()) << "[Arbitre] ClearLoadFault demandé par l'opérateur pour" << loadId;
|
||||||
|
adapter->clearFault();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void EnergyArbitrator::rebuildLoadAdapters()
|
void EnergyArbitrator::rebuildLoadAdapters()
|
||||||
{
|
{
|
||||||
if (!m_loadConfigStore) {
|
if (!m_loadConfigStore) {
|
||||||
|
|||||||
@ -117,6 +117,15 @@ public:
|
|||||||
*/
|
*/
|
||||||
void setLoadConfigStore(LoadConfigStore *store);
|
void setLoadConfigStore(LoadConfigStore *store);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Lève le verrou de défaut d'une charge pilotée (ECS-410).
|
||||||
|
* \param loadId Identifiant logique de la charge.
|
||||||
|
* \return Vrai si la charge existe ; faux si l'identifiant est inconnu.
|
||||||
|
* \note Acte délibéré de l'opérateur, exposé par \c NymeaEnergy.ClearLoadFault. Le défaut
|
||||||
|
* ne se lève jamais seul : c'est ce qui empêche \c available de clignoter.
|
||||||
|
*/
|
||||||
|
bool clearLoadFault(const QString &loadId);
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Mode dégradé L2 actif (compteur muet > 90 s) — override de SmartChargingManager.
|
* \brief Mode dégradé L2 actif (compteur muet > 90 s) — override de SmartChargingManager.
|
||||||
* \return \c true tant que les consignes de repli L2 tiennent ; \c false en régime normal.
|
* \return \c true tant que les consignes de repli L2 tiennent ; \c false en régime normal.
|
||||||
|
|||||||
@ -204,6 +204,29 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc,
|
|||||||
setpointW = chosen;
|
setpointW = chosen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ECS-410 — charge INDISPONIBLE : elle sort de l'ARBITRAGE, pas de la COMPTABILITÉ.
|
||||||
|
// Aucune allocation neuve, mais sa consommation reste retranchée du budget : une
|
||||||
|
// puissance qu'on ne sait plus couper est de la conso fixe, au même titre que la base de
|
||||||
|
// la maison. La retirer du bilan offrirait aux charges suivantes un budget qui n'existe
|
||||||
|
// pas — c'est le trou d'ECS-306 rouvert par une autre porte.
|
||||||
|
//
|
||||||
|
// Le figeage est porté par l'adaptateur : lockMin == lockMax == puissance crue engagée.
|
||||||
|
// Le clamp ci-dessous suffit donc à produire la bonne comptabilité, sans branche spéciale.
|
||||||
|
if (!lc.telemetry.available) {
|
||||||
|
const double gele = lc.telemetry.lockMinPowerW;
|
||||||
|
LoadAction la;
|
||||||
|
la.loadId = lc.id;
|
||||||
|
la.kind = LoadAction::Setpoint;
|
||||||
|
la.funding = LoadAction::Surplus;
|
||||||
|
la.powerW = gele;
|
||||||
|
la.estimatedPowerW = gele;
|
||||||
|
la.reason = QStringLiteral("%1 INDISPONIBLE — aucune commande émise ; %2 W toujours "
|
||||||
|
"comptés au budget (levée par ClearLoadFault)")
|
||||||
|
.arg(lc.label).arg(qRound(gele));
|
||||||
|
remainingSurplusW = budgetW - gele;
|
||||||
|
return la;
|
||||||
|
}
|
||||||
|
|
||||||
// ECS-306 — clamp lock-aware AVANT de décrémenter le budget (AGENTS règle 4 : les
|
// ECS-306 — clamp lock-aware AVANT de décrémenter le budget (AGENTS règle 4 : les
|
||||||
// bornes de l'adaptateur écrêtent TOUTE sortie de stratégie, et leur résultat doit
|
// bornes de l'adaptateur écrêtent TOUTE sortie de stratégie, et leur résultat doit
|
||||||
// revenir à l'arbitre). Sans ceci, l'adaptateur remonte le palier de son côté et le
|
// revenir à l'arbitre). Sans ceci, l'adaptateur remonte le palier de son côté et le
|
||||||
|
|||||||
@ -55,6 +55,22 @@ struct SurplusTariff {
|
|||||||
/*! \brief Télémétrie d'une charge dans le contexte §5 loads[].telemetry. */
|
/*! \brief Télémétrie d'une charge dans le contexte §5 loads[].telemetry. */
|
||||||
struct LoadContextTelemetry {
|
struct LoadContextTelemetry {
|
||||||
double currentPowerW = 0; //!< Puissance mesurée (W).
|
double currentPowerW = 0; //!< Puissance mesurée (W).
|
||||||
|
/*!
|
||||||
|
* \brief Disponibilité de la charge pour l'ARBITRAGE (ECS-410, et plus tard ECS-601).
|
||||||
|
*
|
||||||
|
* **Sémantique à ne pas confondre — elle gouverne l'ALLOCATION, pas la COMPTABILITÉ.**
|
||||||
|
* Une charge indisponible ne reçoit **rien** de neuf, mais elle continue d'être **comptée
|
||||||
|
* pour ce qu'elle consomme réellement** via \c currentPowerW. Un scheduler qui se
|
||||||
|
* contenterait de « sauter » les charges indisponibles sauterait aussi leur comptabilité
|
||||||
|
* et rouvrirait le trou d'ECS-306 par une autre porte : les charges suivantes se verraient
|
||||||
|
* offrir un budget qui n'existe pas.
|
||||||
|
*
|
||||||
|
* Faux quand : la charge est en défaut d'écriture (ECS-410, verrou collant levé seulement
|
||||||
|
* par \c ClearLoadFault), ou — à venir — quand une sonde thermique déclare la consigne
|
||||||
|
* atteinte (ECS-601). Les deux cas convergent : un ballon en température a une conso réelle
|
||||||
|
* nulle, donc la distinction ne mord que sur le défaut.
|
||||||
|
*/
|
||||||
|
bool available = true;
|
||||||
// --- evcharger ---
|
// --- evcharger ---
|
||||||
bool pluggedIn = false; //!< Véhicule branché. Une borne non branchée est ignorée du plan.
|
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).
|
bool charging = false; //!< Charge effectivement en cours (courant non nul côté borne).
|
||||||
|
|||||||
@ -23,6 +23,9 @@
|
|||||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
#include "nymeaenergyjsonhandler.h"
|
#include "nymeaenergyjsonhandler.h"
|
||||||
|
#ifdef ETM_ARBITRATOR
|
||||||
|
#include "etm/energyarbitrator.h"
|
||||||
|
#endif
|
||||||
#include "types/charginginfo.h"
|
#include "types/charginginfo.h"
|
||||||
#include "smartchargingmanager.h"
|
#include "smartchargingmanager.h"
|
||||||
#include "spotmarket/spotmarketmanager.h"
|
#include "spotmarket/spotmarketmanager.h"
|
||||||
@ -194,6 +197,16 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
|
|||||||
returns.insert("o:autonomyRate", enumValueName(Double));
|
returns.insert("o:autonomyRate", enumValueName(Double));
|
||||||
registerMethod("GetEnergyRatios", description, params, returns, Types::PermissionScopeControlThings);
|
registerMethod("GetEnergyRatios", description, params, returns, Types::PermissionScopeControlThings);
|
||||||
|
|
||||||
|
// [ETM] ECS-410 — levée du verrou de défaut d'une charge. Délibérée et journalisée : le
|
||||||
|
// défaut est collant par conception (« cesser toute commande jusqu'à intervention »), sa
|
||||||
|
// levée doit donc être un acte tracé, pas un effet de bord d'un autre appel.
|
||||||
|
params.clear(); returns.clear();
|
||||||
|
description = "Clear the fault latch of a controllable load (ECS-410). Deliberate operator "
|
||||||
|
"action: the fault never clears by itself.";
|
||||||
|
params.insert("loadId", enumValueName(String));
|
||||||
|
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
||||||
|
registerMethod("ClearLoadFault", description, params, returns, Types::PermissionScopeControlThings);
|
||||||
|
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
params.clear();
|
params.clear();
|
||||||
@ -577,6 +590,23 @@ QVariantMap NymeaEnergyJsonHandler::currentEnergyRatios()
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JsonReply *NymeaEnergyJsonHandler::ClearLoadFault(const QVariantMap ¶ms)
|
||||||
|
{
|
||||||
|
#ifdef ETM_ARBITRATOR
|
||||||
|
EnergyArbitrator *arb = dynamic_cast<EnergyArbitrator *>(m_smartChargingManager);
|
||||||
|
if (arb && arb->clearLoadFault(params.value("loadId").toString())) {
|
||||||
|
QVariantMap ret;
|
||||||
|
ret.insert("energyError", enumValueName(EnergyManager::EnergyErrorNoError));
|
||||||
|
return createReply(ret);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
Q_UNUSED(params)
|
||||||
|
#endif
|
||||||
|
QVariantMap ret;
|
||||||
|
ret.insert("energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter));
|
||||||
|
return createReply(ret);
|
||||||
|
}
|
||||||
|
|
||||||
JsonReply *NymeaEnergyJsonHandler::GetEnergyRatios(const QVariantMap ¶ms)
|
JsonReply *NymeaEnergyJsonHandler::GetEnergyRatios(const QVariantMap ¶ms)
|
||||||
{
|
{
|
||||||
Q_UNUSED(params)
|
Q_UNUSED(params)
|
||||||
|
|||||||
@ -73,6 +73,17 @@ public:
|
|||||||
// [Phase 2] Ratios canoniques (autoconsommation / autonomie), % [0,100].
|
// [Phase 2] Ratios canoniques (autoconsommation / autonomie), % [0,100].
|
||||||
Q_INVOKABLE JsonReply *GetEnergyRatios(const QVariantMap ¶ms);
|
Q_INVOKABLE JsonReply *GetEnergyRatios(const QVariantMap ¶ms);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief [ETM] Lève le verrou de défaut d'une charge pilotée (ECS-410).
|
||||||
|
* \param params \c {"loadId": string}.
|
||||||
|
* \return \c energyError — \c EnergyErrorNoError si la charge existe et son défaut est
|
||||||
|
* levé, \c EnergyErrorInvalidParameter si l'identifiant est inconnu.
|
||||||
|
* \note Acte DÉLIBÉRÉ de l'opérateur : le défaut d'ECS-410 est collant par conception, et
|
||||||
|
* cet appel est le seul moyen de le lever hors reconstruction de l'adaptateur. Il est
|
||||||
|
* journalisé côté arbitre — c'est ce qui rend le diagnostic possible après coup.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE JsonReply *ClearLoadFault(const QVariantMap ¶ms);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void PhasePowerLimitChanged(const QVariantMap ¶ms);
|
void PhasePowerLimitChanged(const QVariantMap ¶ms);
|
||||||
void AcquisitionToleranceChanged(const QVariantMap ¶ms);
|
void AcquisitionToleranceChanged(const QVariantMap ¶ms);
|
||||||
|
|||||||
@ -1143,6 +1143,89 @@ void Simulation::testEcsColdStartLockExpires()
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Simulation::testEcsPartialFailure()
|
||||||
|
{
|
||||||
|
#ifndef ETM_ARBITRATOR
|
||||||
|
QSKIP("testEcsPartialFailure nécessite ETM_ARBITRATOR.");
|
||||||
|
#else
|
||||||
|
// [ECS-410] Un relais introuvable fait échouer toute écriture le concernant. On exerce
|
||||||
|
// ainsi l'échelle complète : cible → retour arrière → arrêt total → défaut.
|
||||||
|
cleanupTestCase();
|
||||||
|
m_energyLogDbFilePath = ":/databases/2022-06-22-energylogs.sqlite";
|
||||||
|
initTestCase();
|
||||||
|
EnergyArbitrator *arb = dynamic_cast<EnergyArbitrator *>(m_experiencePlugin->smartChargingManager());
|
||||||
|
QVERIFY(arb);
|
||||||
|
ThingManager *tm = NymeaCore::instance()->thingManager();
|
||||||
|
|
||||||
|
QUuid rOk = addPowerSwitch(1000, 26661);
|
||||||
|
Thing *tOk = tm->findConfiguredThing(rOk);
|
||||||
|
QVERIFY(tOk);
|
||||||
|
tOk->setStateValue("power", false);
|
||||||
|
const QString rKo = QStringLiteral("{deadbeef-0000-0000-0000-000000000000}"); // jamais configuré
|
||||||
|
|
||||||
|
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
|
||||||
|
auto sp = [&](double w) {
|
||||||
|
LoadAction a; a.kind = LoadAction::Setpoint; a.funding = LoadAction::Surplus;
|
||||||
|
a.powerW = w; a.reason = QStringLiteral("test échec d'écriture"); return a;
|
||||||
|
};
|
||||||
|
// ECS-410 est ASYNCHRONE par construction (AGENTS règle 5 : jamais d'attente dans
|
||||||
|
// update()). Les acquittements arrivent par signal : sans faire tourner la boucle
|
||||||
|
// d'événements, l'échelle ne progresse pas d'un seul barreau.
|
||||||
|
auto laisserRetomber = [&]() { QTest::qWait(300); };
|
||||||
|
|
||||||
|
// --- Cas nominal : un seul relais VALIDE, aucune échelle déclenchée -------------------
|
||||||
|
{
|
||||||
|
RelayRouter *sain = new RelayRouter(tm, "sain", "Charge saine",
|
||||||
|
QList<LoadConfigRelay>({ {rOk.toString(), 1000} }), 0, 0, 1, LoadNeeds(), arb);
|
||||||
|
sain->applyAction(sp(1000), t0);
|
||||||
|
laisserRetomber();
|
||||||
|
QCOMPARE(qRound(sain->currentSetpointW()), 1000);
|
||||||
|
QCOMPARE(tOk->stateValue("power").toBool(), true);
|
||||||
|
QVERIFY2(!sain->faulted(), "une écriture réussie ne doit pas lever de défaut");
|
||||||
|
QCOMPARE(sain->telemetry().available, true);
|
||||||
|
tOk->setStateValue("power", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Échelle complète : relais introuvable → défaut ----------------------------------
|
||||||
|
RelayRouter *r = new RelayRouter(tm, "ko", "Charge en panne",
|
||||||
|
QList<LoadConfigRelay>({ {rOk.toString(), 1000}, {rKo, 2000} }), 0, 0, 1, LoadNeeds(), arb);
|
||||||
|
QVERIFY(!r->faulted());
|
||||||
|
|
||||||
|
// Cible 3000 W = les DEUX relais. L'écriture sur rKo échoue → barreau 2 (retour arrière),
|
||||||
|
// qui échoue aussi → barreau 3 (arrêt total), qui échoue → défaut.
|
||||||
|
r->applyAction(sp(3000), t0);
|
||||||
|
laisserRetomber();
|
||||||
|
QVERIFY2(r->faulted(), "l'échelle ECS-410 doit aboutir au défaut");
|
||||||
|
|
||||||
|
// Le relais VALIDE a bien été ramené à l'ouverture par la tentative d'arrêt total :
|
||||||
|
// le barreau 3 n'est pas décoratif.
|
||||||
|
QCOMPARE(tOk->stateValue("power").toBool(), false);
|
||||||
|
|
||||||
|
// available bascule à faux — et ne clignote pas : il est COLLANT.
|
||||||
|
QCOMPARE(r->telemetry().available, false);
|
||||||
|
LoadContext c = r->toLoadContext(t0.addSecs(600));
|
||||||
|
QCOMPARE(c.telemetry.available, false);
|
||||||
|
// Charge FIGÉE : plancher == plafond. Un plafond nul sous un plancher non nul ferait
|
||||||
|
// dérailler le qBound du scheduler.
|
||||||
|
QCOMPARE(qRound(c.telemetry.lockMinPowerW), qRound(c.telemetry.lockMaxPowerW));
|
||||||
|
|
||||||
|
// …et la charge continue d'être COMPTÉE : elle sort de l'arbitrage, pas de la comptabilité.
|
||||||
|
QCOMPARE(qRound(c.telemetry.lockMinPowerW), qRound(c.telemetry.currentPowerW));
|
||||||
|
|
||||||
|
// Plus aucune commande n'est émise, même longtemps après : le défaut ne s'évapore pas.
|
||||||
|
tOk->setStateValue("power", false);
|
||||||
|
r->applyAction(sp(1000), t0.addSecs(3600));
|
||||||
|
laisserRetomber();
|
||||||
|
QCOMPARE(tOk->stateValue("power").toBool(), false);
|
||||||
|
QVERIFY(r->faulted());
|
||||||
|
|
||||||
|
// Levée DÉLIBÉRÉE, par l'opérateur — le seul chemin hors reconstruction.
|
||||||
|
r->clearFault();
|
||||||
|
QVERIFY(!r->faulted());
|
||||||
|
QCOMPARE(r->telemetry().available, true);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void Simulation::run_data()
|
void Simulation::run_data()
|
||||||
{
|
{
|
||||||
// Simulation infos
|
// Simulation infos
|
||||||
|
|||||||
@ -99,6 +99,9 @@ private slots:
|
|||||||
// suffisant : la charge reste éteinte PENDANT minOffS puis s'enclenche. L'armement à
|
// suffisant : la charge reste éteinte PENDANT minOffS puis s'enclenche. L'armement à
|
||||||
// froid doit être transitoire, jamais permanent (défaut constaté au banc 2026-08-09).
|
// froid doit être transitoire, jamais permanent (défaut constaté au banc 2026-08-09).
|
||||||
void testEcsColdStartLockExpires();
|
void testEcsColdStartLockExpires();
|
||||||
|
// [ECS-410] Échec d'écriture en cours de transition : retour arrière, arrêt total, puis
|
||||||
|
// état de défaut collant. Chaque marche de l'échelle a son cas, et la levée est délibérée.
|
||||||
|
void testEcsPartialFailure();
|
||||||
|
|
||||||
void printStates(Thing *thing);
|
void printStates(Thing *thing);
|
||||||
void updateChargerMeter(Thing *thing);
|
void updateChargerMeter(Thing *thing);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user