feat(§11): la mesure d'une charge se lit sur l'appareil, et publie sa source
LM-1105/LM-1106 implantés. measuredW disparaît, sans alias. ILoadAdapter::deviceMeasurement() — PURE VIRTUELLE, comme runtimeView() et pour la même raison : un mécanisme ajouté qui hériterait d'un « non mesurable » par défaut sortirait de la mesure sans bruit, et son écran afficherait « pas mesurable » avec l'autorité d'un constat. Le compilateur pose la question. Le type porte une CAPACITÉ, pas une valeur nulle : `available` répond à « cette charge est-elle mesurable », question posée à la ThingClass, jamais à la valeur du moment. Un routeur au palier 0 mesure 0 W et reste mesurable ; sans ce booléen les deux seraient indistinguables. attachMeasurements() résout meter → device → none. Le compteur explicite garde la priorité, et pas d'abord parce qu'il mesure mieux : un réglage sans effet est pire que pas de réglage. La priorité porte sur la SOURCE, pas sur la valeur — compteur déclaré injoignable, on retombe sur l'appareil, parce que se taire perdrait une mesure qu'on a et que le silence se lirait « non mesurable ». Sous `none`, powerW est OMIS. La puissance nominale reste lisible dans mechanism.*, qui dit d'où elle vient ; la faire passer pour une mesure produirait un écart commandé↔mesuré identiquement nul à tous les cycles — un « tout concorde » permanent, corollaire LM-104. DEUX AFFINEMENTS IMPOSÉS PAR L'IMPLANTATION, tous deux de modèle : LM-1105-a — une mesure PARTIELLE n'est pas une mesure. Là où plusieurs Things portent la charge, il faut qu'ils la portent TOUS : trois relais dont un seul mesure annonceraient 500 W là où 1 500 W coulent, et l'écart enverrait chercher une panne inexistante. À ne pas confondre avec LoadTelemetry::currentPowerW, qui accepte « au moins un » et retombe sur le nominal — celui-là est le juge runtime du budget et doit rendre un nombre à chaque cycle. Les fusionner rouvrirait la correction B. LM-1105-b — la règle présuppose que le Thing piloté est TRAVERSÉ par la puissance. Un relais de relay-router commute la charge, donc sa puissance EST celle de la charge ; un contact SG-Ready est un contact de SIGNAL — la PAC est alimentée ailleurs et un contact qui mesurerait quelque chose mesurerait sa propre bobine. sg-ready est donc `none` par CONSTRUCTION, même sur des relais qui mesurent — c'est le cas du banc — et c'est le seul mécanisme pour lequel meterThingId reste la seule voie. RelayRouter : la capacité se juge sur la ThingClass de TOUS les relais déclarés, la valeur se lit sur les relais FERMÉS. Jugée sur les relais actifs, la source vaudrait `device` au palier 1 et `none` au palier 0 — un bloc de mesure qui apparaît et disparaît au rythme du thermostat. VÉRIFIÉ ÉCHOUANT ainsi. testMeasurementSourceIsPublished : les trois sources sans une ligne de config de compteur, le palier 0 qui ne fait pas basculer la source, la PAC en `none` alors que ses contacts mesurent, la disparition de measuredW, puis les deux règles de priorité — le compteur explicite l'emporte (1 234 contre 2 900), un compteur injoignable fait retomber sur l'appareil. Ne touche PAS ce que l'arbitre lit : LM-1101 tient, LoadTelemetry::currentPowerW est inchangé, et le recrédit anti-clignotement n'a jamais transité par meterThingId. amd64 0 erreur / 0 avertissement. Suites, un processus par test : simulation 30/30, charging 16/16, loadmodel 18/18, spotmarket 7/7. 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
fcba98159f
commit
e392c1c7b8
13
AGENTS.md
13
AGENTS.md
@ -209,7 +209,7 @@ la somme des charges mesurées** (3 250 W contre 3 980 W pour la seule V2C, plus
|
||||
Même famille que l'aveuglement documenté aux résistances ECS. Ce n'est pas un problème
|
||||
d'arbitrage — câblage ou vue SunSpec — mais un budget juste suppose un compteur qui voit tout.
|
||||
|
||||
**DÉCIDÉ, PAS ENCORE ÉCRIT (2026-08-28)** — **la mesure par charge se lit sur l'appareil
|
||||
**FAIT (2026-08-28, `1.15.2+etm26`)** — **la mesure par charge se lit sur l'appareil
|
||||
piloté**, `specs/spec_loadmodel.md` **LM-1105/LM-1106**, sur un constat d'usage de l'agent app.
|
||||
`meterThingId` était la SEULE source : il fallait désigner une borne comme son propre compteur
|
||||
pour qu'elle publie ce qu'elle mesure déjà. Il reste **prioritaire** — un réglage sans effet est
|
||||
@ -220,6 +220,17 @@ Ce que la vérification a trouvé, et qui décide de la forme : `telemetry().cur
|
||||
(`relay-router`, dont le booléen `metered` est calculé puis jeté), déclaré (`sg-ready`,
|
||||
délibérément, invariant 8). D'où `measurement {source, o:powerW}` en remplacement de
|
||||
`measuredW`, avec `source: "none"` comme état positif. **Contrat déplacé** : `INTERFACE.md`.
|
||||
`ILoadAdapter::deviceMeasurement()` est **pure virtuelle**, comme `runtimeView()` : un mécanisme
|
||||
ajouté qui hériterait d'un « non mesurable » par défaut sortirait de la mesure sans bruit, et son
|
||||
écran afficherait « pas mesurable » avec l'autorité d'un constat.
|
||||
|
||||
Deux affinements imposés par l'implantation, tous deux de modèle : **LM-1105-a** — une mesure
|
||||
partielle n'est pas une mesure (relais mixtes → `none`) ; **LM-1105-b** — la règle présuppose que
|
||||
le Thing piloté est TRAVERSÉ par la puissance, ce qu'un contact SG-Ready n'est pas : `sg-ready`
|
||||
est `none` par construction, et c'est le seul mécanisme où `meterThingId` reste la seule voie.
|
||||
Éprouvé par `testMeasurementSourceIsPublished` (trois sources + les deux règles de priorité),
|
||||
la règle « capacité jugée sur la classe » **vérifiée échouant** quand on la juge sur les relais
|
||||
actifs.
|
||||
|
||||
> Ce lot ne touche PAS ce que l'arbitre lit : LM-1101 tient, et le recrédit anti-clignotement
|
||||
> continue de lire `LoadContext::telemetry.currentPowerW`, qui vient de l'adaptateur et n'a
|
||||
|
||||
24
INTERFACE.md
24
INTERFACE.md
@ -676,6 +676,22 @@ le **libellé de la charge n'y figure jamais** (le client le résout via `GetLoa
|
||||
| `device` | L'appareil piloté publie lui-même sa puissance — borne, `etmvariableload`, relais mesurant |
|
||||
| `none` | **Rien ne mesure cette charge.** `powerW` est alors ABSENT, jamais nul |
|
||||
|
||||
**Quand vaut-il `none` ?** Trois cas, et le premier surprend :
|
||||
|
||||
- **Toujours pour `sg-ready`**, même montée sur des relais qui mesurent. Un contact SG-Ready est
|
||||
un contact de **signal** : la PAC est alimentée ailleurs, son courant ne traverse jamais K1/K2,
|
||||
et un contact qui mesurerait quelque chose mesurerait **sa propre bobine**. C'est le seul
|
||||
mécanisme pour lequel `meterThingId` reste la seule voie vers une mesure.
|
||||
- **Topologie de relais MIXTE** — un relais mesurant, deux nus. Une somme partielle présentée
|
||||
comme la mesure de la charge annoncerait 500 W là où 1 500 W coulent, et l'écart enverrait
|
||||
chercher une panne inexistante. Une mesure partielle n'est pas une mesure.
|
||||
- **Thing sans l'état de puissance**, tout simplement.
|
||||
|
||||
> **`source` ne dépend ni de la valeur ni de l'instant.** Un `relay-router` au palier 0 n'a aucun
|
||||
> relais fermé et publie `{source: "device", powerW: 0}` : **0 W est une mesure**, pas une
|
||||
> absence. La capacité se juge sur la ThingClass de **tous** les Things déclarés — sinon le bloc
|
||||
> de mesure apparaîtrait et disparaîtrait au rythme du thermostat.
|
||||
|
||||
Publiée **à côté** de `allocatedW`, jamais fondue dedans : `allocatedW` est ce que le waterfall
|
||||
a **alloué**, `measurement.powerW` ce qui a été **vu**. Les confondre rendrait la somme des
|
||||
allocations irréconciliable avec `budget.allocatedW`.
|
||||
@ -692,6 +708,14 @@ allocations irréconciliable avec `budget.allocatedW`.
|
||||
> bloc de mesure au lieu d'afficher un vide. Sans lui, « pas mesurable » et « compteur en
|
||||
> panne » se ressemblent.
|
||||
|
||||
> **L'asymétrie `device` / `meter`, et pourquoi elle vaut d'être dite à l'écran.** Sous
|
||||
> `device`, une **concordance** ne prouve pas grand-chose : plusieurs wallbox renvoient leur
|
||||
> consigne en guise de puissance, et « commandé == mesuré » n'y est alors qu'une tautologie.
|
||||
> Sous `meter`, une concordance est une **preuve** — commande et mesure viennent de deux boîtes
|
||||
> différentes. Autrement dit : **`device` détecte la désobéissance, `meter` est le seul à
|
||||
> pouvoir détecter un mensonge.** C'est ce qui justifie de laisser `meterThingId` **disponible**
|
||||
> sur une charge qui publie déjà sa puissance : masqué par défaut, jamais interdit.
|
||||
|
||||
> **Migration depuis `measuredW`.** L'ancienne clé disparaît ; elle ne pouvait pas exprimer
|
||||
> trois états, et la doubler d'un second champ aurait obligé le client à joindre deux clés pour
|
||||
> lire une seule chose. `measuredW: X` devient `measurement: {source: …, powerW: X}` ; son
|
||||
|
||||
@ -270,8 +270,8 @@ Et `targetPercentage = 80` observé sur la V2C **confirme sur machine** le défa
|
||||
|
||||
## 7. Le compteur par charge : vous avez raison, et le modèle bouge
|
||||
|
||||
*(Votre constat d'usage du 2026-08-28. Tranché : `spec_loadmodel.md` **LM-1105** et
|
||||
**LM-1106**. Contrat : `INTERFACE.md`. **Pas encore implémenté** — rien à consommer aujourd'hui.)*
|
||||
*(Votre constat d'usage du 2026-08-28. Tranché : `spec_loadmodel.md` **LM-1105/1106**.
|
||||
Contrat : `INTERFACE.md`. **LIVRÉ** en `1.15.2+etm26` — `measuredW` n'existe plus.)*
|
||||
|
||||
### Le constat est retenu tel quel
|
||||
|
||||
@ -326,6 +326,23 @@ en panne », que rien ne séparait.
|
||||
> `meterThingId` reste **autorisé** sur une borne qui publie déjà : à masquer par défaut, pas à
|
||||
> interdire.
|
||||
|
||||
### Deux choses trouvées en implantant, qui changent ce que vous afficherez
|
||||
|
||||
**`sg-ready` est `none` par CONSTRUCTION, pas par accident de matériel.** Un contact SG-Ready est
|
||||
un contact de **signal** : la PAC est alimentée ailleurs, son courant ne traverse jamais K1/K2.
|
||||
Même montée sur des relais qui mesurent — c'est le cas de notre banc — la charge reste non
|
||||
mesurable par son organe de commande. Pour une PAC, `meterThingId` n'est pas un réglage avancé :
|
||||
c'est la **seule** voie vers une mesure, et l'écran devrait le proposer, pas le masquer.
|
||||
|
||||
**Une topologie de relais MIXTE est `none` aussi.** Un relais mesurant et deux nus donneraient
|
||||
une somme partielle — 500 W annoncés là où 1 500 W coulent — et l'écart vous ferait afficher une
|
||||
panne qui n'existe pas. Une mesure partielle n'est pas une mesure.
|
||||
|
||||
> Et un point d'affichage qui vous évitera un faux bug : **`{source: "device", powerW: 0}` est
|
||||
> une mesure**, pas une absence. Un routeur de relais au palier 0 mesure réellement 0 W et reste
|
||||
> mesurable — la source ne bascule pas vers `none` quand la charge s'éteint. Si elle basculait,
|
||||
> votre bloc de mesure apparaîtrait et disparaîtrait au rythme du thermostat.
|
||||
|
||||
### Votre coût, et il est d'un écran
|
||||
|
||||
`measuredW: X` → `measurement: {source, powerW: X}` ; son **absence** → `source: "none"`.
|
||||
|
||||
@ -173,6 +173,22 @@ void EtmVariableLoadAdapter::applySafeState(const QDateTime &now)
|
||||
|
||||
// ---- privé ---------------------------------------------------------------
|
||||
|
||||
LoadMeasurement EtmVariableLoadAdapter::deviceMeasurement() const
|
||||
{
|
||||
LoadMeasurement m;
|
||||
Thing *thing = m_thingManager->findConfiguredThing(ThingId(m_id));
|
||||
if (!thing)
|
||||
return m; // Thing absent : rien ne mesure. THING_MISSING le dit déjà par ailleurs.
|
||||
|
||||
// Question posée à la CLASSE : une charge à consigne 0 mesure 0 W et reste mesurable.
|
||||
if (thing->thingClass().stateTypes().findByName("currentPowerW").id().isNull())
|
||||
return m;
|
||||
|
||||
m.available = true;
|
||||
m.powerW = thing->stateValue("currentPowerW").toDouble();
|
||||
return m;
|
||||
}
|
||||
|
||||
double EtmVariableLoadAdapter::readCurrentPowerW() const
|
||||
{
|
||||
Thing *thing = m_thingManager->findConfiguredThing(ThingId(m_id));
|
||||
|
||||
@ -137,6 +137,18 @@ public:
|
||||
*/
|
||||
LoadRuntimeView runtimeView(const QDateTime &now) const override;
|
||||
|
||||
/*!
|
||||
* \brief Mesure propre de la charge pilotée (§11 / LM-1105).
|
||||
* \return La puissance que le Thing rapporte lui-même, quand sa ThingClass porte
|
||||
* \c currentPowerW ; « non mesurable » sinon.
|
||||
* \note **Ne retombe PAS sur la consigne en vol**, contrairement à \c telemetry(). Celle-ci
|
||||
* doit rendre un nombre à chaque cycle — le recrédit anti-clignotement en dépend — et
|
||||
* annonce donc la plus haute consigne possible quand rien ne mesure (ECS-414). Ici, une
|
||||
* consigne n'est pas une mesure : la publier comme telle produirait un écart
|
||||
* commandé↔mesuré identiquement nul (LM-1106).
|
||||
*/
|
||||
LoadMeasurement deviceMeasurement() const override;
|
||||
|
||||
//! \brief État sûr : consigne **0 W**, en \c force = true (ECS-413). \param now Temps de cycle.
|
||||
void applySafeState(const QDateTime &now) override;
|
||||
|
||||
|
||||
@ -129,6 +129,18 @@ LoadRuntimeView EvAdapter::runtimeView(const QDateTime &now) const
|
||||
return v;
|
||||
}
|
||||
|
||||
LoadMeasurement EvAdapter::deviceMeasurement() const
|
||||
{
|
||||
LoadMeasurement m;
|
||||
// Question posée à la CLASSE, pas à la valeur : une borne à l'arrêt mesure 0 W et reste
|
||||
// mesurable. Confondre les deux ferait basculer la source publiée à chaque démarrage.
|
||||
if (!m_charger->thing()->thingClass().stateTypes().findByName("currentPower").id().isNull()) {
|
||||
m.available = true;
|
||||
m.powerW = m_charger->currentPower();
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
LoadAction EvAdapter::applyAction(const LoadAction &action, const QDateTime &now)
|
||||
{
|
||||
if (action.kind != LoadAction::Setpoint)
|
||||
|
||||
@ -100,6 +100,16 @@ public:
|
||||
*/
|
||||
LoadRuntimeView runtimeView(const QDateTime &now) const override;
|
||||
|
||||
/*!
|
||||
* \brief Mesure propre de la borne (§11 / LM-1105).
|
||||
* \return La puissance que la borne rapporte elle-même, quand sa ThingClass porte l'état.
|
||||
* \note Une borne est **traversée** par la puissance qu'elle délivre : sa mesure est bien
|
||||
* celle de la charge, et non celle d'un organe de commande. C'est le cas qui a motivé
|
||||
* LM-1105 — il fallait jusqu'ici désigner la borne comme son propre \c meterThingId
|
||||
* pour publier une valeur qu'elle donne d'elle-même.
|
||||
*/
|
||||
LoadMeasurement deviceMeasurement() const override;
|
||||
|
||||
//! \brief Sans effet : l'EV n'est pas construit depuis \c LoadConfig et ne peut donc pas
|
||||
//! être « désactivé » par ce chemin. \param now Ignoré.
|
||||
void applySafeState(const QDateTime &now) override { Q_UNUSED(now) }
|
||||
|
||||
@ -17,6 +17,28 @@ struct LoadTelemetry {
|
||||
QDateTime lastActionAt; //!< Dernier instant où applyAction() a produit un effet.
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Mesure PROPRE de l'appareil piloté — §11, LM-1105/LM-1106.
|
||||
*
|
||||
* Distincte de \c LoadTelemetry::currentPowerW, et la distinction est tout l'objet du type.
|
||||
* Ce champ-là est le **juge runtime du budget** : il vaut une mesure quand il en existe une et
|
||||
* la puissance **commandée ou déclarée** sinon, parce que le recrédit anti-clignotement a
|
||||
* besoin d'un nombre à chaque cycle. Celui-ci ne vaut **que** ce qu'un appareil a réellement
|
||||
* mesuré, ou rien.
|
||||
*
|
||||
* \par Pourquoi une CAPACITÉ et non une valeur nulle
|
||||
* \c available répond à « cette charge est-elle mesurable ? », question posée à la
|
||||
* \c ThingClass, jamais à la valeur du moment. Un routeur au palier 0 mesure 0 W et reste
|
||||
* mesurable ; sans ce booléen, les deux seraient indistinguables et la source publiée
|
||||
* basculerait d'un cycle à l'autre sur une installation dont rien n'a changé.
|
||||
*/
|
||||
struct LoadMeasurement {
|
||||
//! Vrai si les Things pilotés portent l'état de puissance — CAPACITÉ, pas valeur.
|
||||
bool available = false;
|
||||
//! Puissance réellement mesurée (W) ; sans objet quand \c available est faux.
|
||||
double powerW = 0;
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Vue runtime d'une charge destinée à la FRONTIÈRE DE TÉLÉMÉTRIE (RPC), et à elle seule.
|
||||
*
|
||||
@ -173,6 +195,39 @@ public:
|
||||
*/
|
||||
virtual LoadRuntimeView runtimeView(const QDateTime &now) const = 0;
|
||||
|
||||
/*!
|
||||
* \brief Cette charge est-elle mesurée par les appareils qu'elle pilote, et combien ?
|
||||
*
|
||||
* §11 — **LM-1105** : le moteur lit l'appareil qu'il pilote plutôt que d'exiger qu'on le
|
||||
* désigne comme son propre compteur dans \c meterThingId. **LM-1106** : ce qui est publié
|
||||
* est un couple {valeur, source}, et c'est cette méthode qui répond pour la source
|
||||
* \c "device".
|
||||
*
|
||||
* \return \c available faux = **rien ne mesure cette charge** (\c source: "none" à la
|
||||
* publication) ; vrai = \c powerW est une mesure réelle.
|
||||
*
|
||||
* \note **La question porte sur la ThingClass, jamais sur la valeur ni sur l'instant.** Un
|
||||
* \c RelayRouter au palier 0 n'a aucun relais fermé : interroger les relais ACTIFS
|
||||
* ferait osciller la source publiée d'un cycle à l'autre. Elle se pose sur TOUS les
|
||||
* Things déclarés de la charge.
|
||||
* \note **Et elle présuppose que le Thing piloté est TRAVERSÉ par la puissance.** Un relais
|
||||
* de \c RelayRouter commute la charge, donc sa puissance EST celle de la charge ; un
|
||||
* contact SG-Ready est un contact de SIGNAL — la PAC est alimentée ailleurs, et son
|
||||
* courant ne passe jamais par lui. Un contact qui mesurerait quelque chose mesurerait sa
|
||||
* propre bobine. \c SgReadyAdapter répond donc toujours « non mesurable », et c'est le
|
||||
* seul mécanisme pour lequel \c meterThingId reste la seule voie.
|
||||
* \note **Une mesure PARTIELLE n'est pas une mesure.** Là où plusieurs Things portent la
|
||||
* charge, il faut qu'ils la portent TOUS : sommer les seuls relais mesurants
|
||||
* publierait une puissance sous-évaluée, et l'écart commandé↔mesuré enverrait chercher
|
||||
* une panne qui n'existe pas (LM-1102).
|
||||
* \note **Pure virtuelle à dessein**, comme \c runtimeView() et pour la même raison : un
|
||||
* mécanisme ajouté qui hériterait d'un défaut « non mesurable » sortirait de la mesure
|
||||
* SANS BRUIT, et son écran afficherait « pas mesurable » avec l'autorité d'un constat.
|
||||
* Le compilateur pose la question à chaque nouvel adaptateur.
|
||||
* \note N'entre JAMAIS dans une décision : l'arbitre ne la lit que pour publier (règle 2).
|
||||
*/
|
||||
virtual LoadMeasurement deviceMeasurement() const = 0;
|
||||
|
||||
/*!
|
||||
* \brief Amène le matériel dans l'ÉTAT SÛR de cet adaptateur (ECS-413).
|
||||
*
|
||||
|
||||
@ -515,6 +515,37 @@ void RelayRouter::settleTransition()
|
||||
}
|
||||
}
|
||||
|
||||
LoadMeasurement RelayRouter::deviceMeasurement() const
|
||||
{
|
||||
LoadMeasurement m;
|
||||
if (m_relayNominalW.isEmpty())
|
||||
return m;
|
||||
|
||||
// CAPACITÉ — la question porte sur la ThingClass de TOUS les relais déclarés, jamais sur
|
||||
// ceux qui sont fermés maintenant : au palier 0 il n'y en a aucun, et juger là-dessus
|
||||
// ferait basculer la source publiée à chaque extinction (LM-1106).
|
||||
//
|
||||
// TOUS, et pas « au moins un » : une topologie mixte donnerait une somme sous-évaluée
|
||||
// présentée comme la mesure de la charge — un écart commandé↔mesuré qui envoie chercher
|
||||
// une panne inexistante (LM-1102). Une mesure partielle n'est pas une mesure.
|
||||
for (auto it = m_relayNominalW.constBegin(); it != m_relayNominalW.constEnd(); ++it) {
|
||||
Thing *relay = m_thingManager->findConfiguredThing(ThingId(it.key()));
|
||||
if (!relay || relay->thingClass().stateTypes().findByName("currentPower").id().isNull())
|
||||
return m; // un seul relais nu, et la charge n'est pas mesurable
|
||||
}
|
||||
|
||||
// VALEUR — sur les relais FERMÉS. Un thermostat qui a ouvert donne 0 W relais fermé, et
|
||||
// c'est une mesure juste : la charge ne consomme réellement rien.
|
||||
m.available = true;
|
||||
const QList<QString> fermes = m_currentStage < m_relayMapping.size()
|
||||
? m_relayMapping.at(m_currentStage)
|
||||
: QList<QString>();
|
||||
for (const QString &thingId : fermes)
|
||||
if (Thing *relay = m_thingManager->findConfiguredThing(ThingId(thingId)))
|
||||
m.powerW += relay->stateValue("currentPower").toDouble();
|
||||
return m;
|
||||
}
|
||||
|
||||
LoadRuntimeView RelayRouter::runtimeView(const QDateTime &now) const
|
||||
{
|
||||
LoadRuntimeView v;
|
||||
|
||||
@ -175,6 +175,26 @@ public:
|
||||
*/
|
||||
LoadRuntimeView runtimeView(const QDateTime &now) const override;
|
||||
|
||||
/*!
|
||||
* \brief Mesure propre des relais qui portent la charge (§11 / LM-1105).
|
||||
*
|
||||
* \return Somme des \c currentPower des relais FERMÉS au palier courant, à condition que
|
||||
* **tous** les relais déclarés portent l'état ; « non mesurable » sinon.
|
||||
*
|
||||
* \note **La capacité se juge sur TOUS les relais déclarés, la valeur se lit sur les relais
|
||||
* FERMÉS.** Juger la capacité sur les relais fermés ferait répondre « non mesurable » au
|
||||
* palier 0, où aucun ne l'est — la source publiée basculerait à chaque extinction, sur
|
||||
* une installation dont rien n'a changé (LM-1106).
|
||||
* \note **Tous, et pas au moins un.** Une topologie mixte — un relais mesurant, deux nus —
|
||||
* produirait une somme SOUS-ÉVALUÉE présentée comme la mesure de la charge, et l'écart
|
||||
* commandé↔mesuré enverrait chercher une panne qui n'existe pas (LM-1102). Une mesure
|
||||
* partielle n'est pas une mesure ; c'est le cas où \c meterThingId garde tout son sens.
|
||||
* \note Distincte de \c telemetry(), qui accepte « au moins un » et retombe sur le palier
|
||||
* NOMINAL sinon : elle doit rendre un nombre à chaque cycle pour le recrédit. Ici, un
|
||||
* nominal n'est pas une mesure.
|
||||
*/
|
||||
LoadMeasurement deviceMeasurement() const override;
|
||||
|
||||
//! \brief État sûr du routeur : TOUS relais ouverts (ECS-413).
|
||||
//! \param now Temps de cycle — l'action passe en \c force = true, verrous bypassés.
|
||||
void applySafeState(const QDateTime &now) override;
|
||||
|
||||
@ -120,6 +120,27 @@ public:
|
||||
*/
|
||||
LoadRuntimeView runtimeView(const QDateTime &now) const override;
|
||||
|
||||
/*!
|
||||
* \brief Toujours « non mesurable » — et c'est une propriété du MÉCANISME (§11 / LM-1105).
|
||||
*
|
||||
* \return \c available toujours faux.
|
||||
*
|
||||
* \note Ce n'est pas un « pas encore » : un contact SG-Ready est un contact de **SIGNAL**.
|
||||
* La PAC est alimentée ailleurs et son courant ne traverse jamais K1/K2 — un contact qui
|
||||
* mesurerait quelque chose mesurerait **sa propre bobine**. La règle générale
|
||||
* « le moteur lit l'appareil qu'il pilote » présuppose que l'appareil piloté est
|
||||
* TRAVERSÉ par la puissance de la charge ; ici il ne l'est pas.
|
||||
* \note C'est donc le seul mécanisme pour lequel \c meterThingId reste la **seule** voie
|
||||
* vers une mesure, et cela doit se lire à l'écran plutôt que se deviner : d'où
|
||||
* \c source: "none" publié, plutôt qu'une clé absente (LM-1106).
|
||||
* \warning Ne PAS publier \c m_estimatedPowerW ici. C'est une puissance **déclarée** —
|
||||
* le juge du recrédit budget, invariant 8 — et la faire passer pour une mesure
|
||||
* produirait un écart commandé↔mesuré identiquement nul à tous les cycles : un « tout
|
||||
* concorde » permanent, corollaire LM-104.
|
||||
*/
|
||||
LoadMeasurement deviceMeasurement() const override
|
||||
{ return LoadMeasurement{}; }
|
||||
|
||||
/*!
|
||||
* \brief État sûr de la PAC : **état 2** (normal, mains off) — JAMAIS l'état 1 (blocage).
|
||||
* \param now Temps de cycle ; l'action passe en \c force = true (bypass minStateHold).
|
||||
|
||||
@ -1072,26 +1072,68 @@ void EnergyArbitrator::buildTelemetry(const Plan &plan, const Slot &slot, const
|
||||
|
||||
void EnergyArbitrator::attachMeasurements(QVariantMap &entry, const QString &loadId) const
|
||||
{
|
||||
if (!m_tm || !m_loadConfigStore)
|
||||
if (!m_tm)
|
||||
return;
|
||||
|
||||
LoadConfig cfg;
|
||||
bool trouvee = false;
|
||||
for (const LoadConfig &c : m_loadConfigStore->configs()) {
|
||||
if (c.id() == loadId) { cfg = c; trouvee = true; break; }
|
||||
for (const LoadConfig &c : m_loadConfigStore ? m_loadConfigStore->configs() : LoadConfigs()) {
|
||||
if (c.id() == loadId) { cfg = c; break; }
|
||||
}
|
||||
if (!trouvee)
|
||||
return;
|
||||
|
||||
// Compteur — VÉRIFICATION seulement (LM-1101). Publiée à côté de allocatedW, jamais
|
||||
// fondue dedans : la somme des allocations doit rester réconciliable avec
|
||||
// budget.allocatedW, et une mesure en retard sur la commande la rendrait fausse.
|
||||
// --- MESURE : {valeur, source}, jamais un nombre seul (§11 / LM-1106) -----------------
|
||||
//
|
||||
// Trois régimes, et deux d'entre eux étaient publiés sous le même nom jusqu'ici. Le
|
||||
// client ne pouvait pas distinguer « un compteur a été posé exprès » de « l'appareil se
|
||||
// déclare », alors que l'écart commandé↔mesuré ne conclut pas la même chose dans les deux
|
||||
// cas : sous "device" il dit que la charge n'obéit pas ; sous "meter" il ouvre trois
|
||||
// hypothèses — la charge, le compteur, ou le câblage — dont il ne tranche aucune.
|
||||
QString source = QStringLiteral("none");
|
||||
double powerW = 0;
|
||||
|
||||
// 1. Le compteur EXPLICITE d'abord (LM-1105). Non pas d'abord parce qu'il mesure mieux —
|
||||
// c'est vrai, et ce n'est pas la raison — mais parce qu'un réglage sans effet est pire
|
||||
// que pas de réglage : préférer l'appareil au compteur que l'installateur a pris la
|
||||
// peine de désigner rendrait le champ décoratif.
|
||||
if (!cfg.meterThingId().isEmpty()) {
|
||||
Thing *t = m_tm->findConfiguredThing(ThingId(cfg.meterThingId()));
|
||||
if (t && t->hasState("currentPower"))
|
||||
entry.insert(QStringLiteral("measuredW"), t->stateValue("currentPower").toDouble());
|
||||
if (t && t->hasState("currentPower")) {
|
||||
source = QStringLiteral("meter");
|
||||
powerW = t->stateValue("currentPower").toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. À défaut, l'APPAREIL PILOTÉ — LM-1105, et c'est le lot. La priorité porte sur la
|
||||
// SOURCE, pas sur la valeur : un compteur déclaré mais injoignable fait retomber ici
|
||||
// plutôt que taire une mesure qu'on a. Le silence se lirait « charge non mesurable ».
|
||||
if (source == QLatin1String("none")) {
|
||||
ILoadAdapter *adapter = m_loadAdapters.value(loadId, nullptr);
|
||||
if (!adapter)
|
||||
adapter = m_adapters.value(loadId, nullptr);
|
||||
if (adapter) {
|
||||
const LoadMeasurement m = adapter->deviceMeasurement();
|
||||
if (m.available) {
|
||||
source = QStringLiteral("device");
|
||||
powerW = m.powerW;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. "none" est un état POSITIF, pas une clé qui manque : « cette charge n'est pas
|
||||
// mesurable ». C1 de LM-1009 appliqué ici — une capacité manquante SE DIT. Sans lui,
|
||||
// « pas mesurable » et « compteur en panne » se ressemblent, et l'app continue
|
||||
// d'afficher un bloc de mesure vide sur un sg-ready qui n'en aura jamais.
|
||||
//
|
||||
// Et powerW reste OMIS : la puissance nominale existe, elle est déjà lisible dans
|
||||
// mechanism.* qui dit d'où elle vient. La faire passer pour une mesure produirait un
|
||||
// écart commandé↔mesuré identiquement nul à tous les cycles — un « tout concorde »
|
||||
// permanent, c'est-à-dire le corollaire LM-104 : l'hypothèse de prudence devient un
|
||||
// masquage de panne.
|
||||
QVariantMap mesure;
|
||||
mesure.insert(QStringLiteral("source"), source);
|
||||
if (source != QLatin1String("none"))
|
||||
mesure.insert(QStringLiteral("powerW"), powerW);
|
||||
entry.insert(QStringLiteral("measurement"), mesure);
|
||||
|
||||
// Sonde — lecture seule (LM-1103). Sa raison d'être immédiate est le diagnostic ; sa
|
||||
// raison d'être durable est l'HISTORIQUE, sans lequel les seuils éco/confort du §10 ne
|
||||
// se règleront jamais autrement qu'au doigt mouillé.
|
||||
|
||||
@ -732,6 +732,12 @@ QVariantMap NymeaEnergyJsonHandler::loadTelemetrySchema()
|
||||
decisionItem.insert("code", enumValueName(String));
|
||||
decisionItem.insert("params", enumValueName(Variant));
|
||||
|
||||
// §11 / LM-1106 — la mesure et SA SOURCE. « source » discrimine, « powerW » n'existe que
|
||||
// quand quelque chose mesure réellement (jamais sous « none »).
|
||||
QVariantMap measurementItem;
|
||||
measurementItem.insert("source", enumValueName(String)); // meter | device | none
|
||||
measurementItem.insert("o:powerW", enumValueName(Double));
|
||||
|
||||
QVariantMap loadItem;
|
||||
loadItem.insert("loadId", enumValueName(String));
|
||||
// ALLOCATION du waterfall (ce qui a décrémenté le budget), pas la mesure : la mesure vit
|
||||
@ -750,8 +756,14 @@ QVariantMap NymeaEnergyJsonHandler::loadTelemetrySchema()
|
||||
// §11 (LM-1101/1103) — mesures de la charge, publiées à côté de l'allocation et JAMAIS
|
||||
// fondues dedans : la somme des allocations doit rester réconciliable avec
|
||||
// budget.allocatedW, et une mesure en retard sur la commande la rendrait fausse.
|
||||
// Omises quand la charge ne déclare pas de Thing, ou quand le Thing ne porte pas l'état.
|
||||
loadItem.insert("o:measuredW", enumValueName(Double));
|
||||
//
|
||||
// LM-1106 — « measurement » est un COUPLE {valeur, source}, et non le « measuredW » nu
|
||||
// qu'il remplace. Le nombre seul ne pouvait pas dire d'où il venait, alors que l'écart
|
||||
// commandé↔mesuré ne conclut pas la même chose selon la source ; et « source: none » est
|
||||
// un état POSITIF — cette charge n'est pas mesurable — que l'absence d'une clé ne savait
|
||||
// pas distinguer d'un compteur en panne. « powerW » est omis sous « none » : la puissance
|
||||
// nominale reste lisible dans mechanism.*, elle ne se fait pas passer pour une mesure.
|
||||
loadItem.insert("o:measurement", measurementItem);
|
||||
loadItem.insert("o:temperatureC", enumValueName(Double));
|
||||
loadItem.insert("o:lock", lockItem); // absent = aucun verrou actif
|
||||
loadItem.insert("o:mechanism", mechItem);
|
||||
|
||||
@ -909,6 +909,40 @@ sans configuration. `meterThingId` ne reste nécessaire que là où rien ne mesu
|
||||
> palier 0 n'a aucun relais actif : interroger les relais *actifs* ferait osciller la réponse
|
||||
> d'un cycle à l'autre, sur une installation dont rien n'a changé. La question est « ces Things
|
||||
> portent-ils l'état ? », posée sur **tous** les Things déclarés de la charge.
|
||||
>
|
||||
> *Vérifié : jugée sur les relais actifs, la source publiée vaut `device` au palier 1 et `none`
|
||||
> au palier 0 — un écran de mesure qui apparaît et disparaît au rythme du thermostat
|
||||
> (`testMeasurementSourceIsPublished`, contre-épreuve).*
|
||||
|
||||
**LM-1105-a — Une mesure PARTIELLE n'est pas une mesure.** *(Affinement imposé par
|
||||
l'implantation, 2026-08-28.)*
|
||||
|
||||
Là où plusieurs Things portent la charge — les relais d'un `relay-router` —, il faut qu'ils la
|
||||
portent **tous**. « Au moins un » produirait une somme **sous-évaluée** présentée comme la mesure
|
||||
de la charge : une topologie de trois relais dont un seul mesure annoncerait 500 W là où
|
||||
1 500 W coulent, et l'écart commandé↔mesuré enverrait chercher une panne qui n'existe pas
|
||||
(LM-1102). Une topologie mixte est donc `none` — et c'est un des cas où `meterThingId` garde
|
||||
tout son sens.
|
||||
|
||||
> ⚠️ **À ne pas confondre avec `LoadTelemetry::currentPowerW`**, qui accepte « au moins un » et
|
||||
> retombe sur le palier **nominal** sinon. Les deux ne répondent pas à la même question :
|
||||
> celui-là est le **juge runtime du budget**, il doit rendre un nombre à chaque cycle parce que
|
||||
> le recrédit anti-clignotement en dépend. Celui-ci ne vaut que ce qu'un appareil a réellement
|
||||
> mesuré, ou rien. **Les fusionner rouvrirait la correction B.**
|
||||
|
||||
**LM-1105-b — La règle présuppose que le Thing piloté est TRAVERSÉ par la puissance.**
|
||||
*(Trouvé en implantant, et c'est la limite exacte de LM-1105.)*
|
||||
|
||||
« Le moteur lit l'appareil qu'il pilote » n'est vrai que si l'appareil piloté est **dans le
|
||||
chemin de puissance** de la charge. Un relais de `relay-router` commute la charge : sa puissance
|
||||
**est** celle de la charge. Un contact **SG-Ready** est un contact de **SIGNAL** — la PAC est
|
||||
alimentée ailleurs, son courant ne traverse jamais K1/K2, et un contact qui mesurerait quelque
|
||||
chose mesurerait **sa propre bobine**.
|
||||
|
||||
**`sg-ready` est donc `none` par CONSTRUCTION**, et non par accident de matériel : même montée
|
||||
sur des relais qui mesurent, la charge reste non mesurable par son organe de commande. C'est le
|
||||
seul mécanisme pour lequel `meterThingId` demeure la **seule** voie vers une mesure — et cela
|
||||
doit se lire à l'écran plutôt que se deviner.
|
||||
|
||||
**Le compteur explicite reste PRIORITAIRE, et ce n'est pas d'abord une question de précision.**
|
||||
Un compteur divisionnaire posé exprès mesure mieux qu'un appareil qui se déclare — c'est vrai,
|
||||
|
||||
@ -5626,3 +5626,159 @@ void Simulation::testChargerWithoutCarIsArbitrated()
|
||||
QCOMPARE(qRound(tele.value(routeurId.toString()).value("allocatedW").toDouble()), 3000);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Simulation::testMeasurementSourceIsPublished()
|
||||
{
|
||||
#ifndef ETM_ARBITRATOR
|
||||
QSKIP("testMeasurementSourceIsPublished nécessite ETM_ARBITRATOR.");
|
||||
#else
|
||||
// [§11 · LM-1105/LM-1106] Il fallait désigner une charge comme SON PROPRE compteur dans
|
||||
// meterThingId pour qu'elle publie ce qu'elle mesure déjà. Le moteur lit désormais
|
||||
// l'appareil qu'il pilote — et publie D'OÙ vient le nombre, parce que l'écart
|
||||
// commandé↔mesuré ne conclut pas la même chose selon la source.
|
||||
//
|
||||
// Ce test porte les trois sources ET les deux règles de priorité. Sans la seconde moitié
|
||||
// il passerait aussi si le compteur explicite avait été purement et simplement remplacé.
|
||||
const QString cfgPath = QDir::tempPath() + "/etm-loadcfg-measure.json";
|
||||
QFile::remove(cfgPath);
|
||||
qputenv("NYMEA_ENERGY_LOAD_CONFIG", cfgPath.toUtf8());
|
||||
|
||||
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 meterId = addMeter();
|
||||
m_experiencePlugin->energyManager()->setRootMeter(meterId);
|
||||
Thing *meter = tm->findConfiguredThing(meterId);
|
||||
QVERIFY(meter);
|
||||
meter->setStateValue("connected", true);
|
||||
|
||||
// Trois mécanismes. Les relais du mock portent `currentPower` — c'est délibéré : le
|
||||
// sg-ready doit répondre « non mesurable » MÊME LÀ, parce que ses contacts sont des
|
||||
// contacts de SIGNAL et que le courant de la PAC ne les traverse pas.
|
||||
QUuid rA = addPowerSwitch(1000, 26661); // relay-router, 1 relais
|
||||
QUuid k1 = addPowerSwitch(0, 26662); // sg-ready K1
|
||||
QUuid k2 = addPowerSwitch(0, 26663); // sg-ready K2
|
||||
QUuid varId = addEtmVariableLoad(27041); // etmvariableload dynamic
|
||||
Thing *varThing = tm->findConfiguredThing(varId);
|
||||
QVERIFY(varThing && tm->findConfiguredThing(rA));
|
||||
QVERIFY(tm->findConfiguredThing(k1) && tm->findConfiguredThing(k2));
|
||||
varThing->setStateValue("currentPowerW", 0);
|
||||
|
||||
// Le compteur divisionnaire, posé EXPRÈS sur le routeur — il n'entrera en jeu qu'au 3.
|
||||
QUuid divId = addMeter(26670);
|
||||
Thing *div = tm->findConfiguredThing(divId);
|
||||
QVERIFY(div);
|
||||
div->setStateValue("connected", true);
|
||||
div->setStateValue("currentPower", 1234);
|
||||
|
||||
LoadConfigStore *store = new LoadConfigStore(arb);
|
||||
arb->setLoadConfigStore(store);
|
||||
|
||||
auto poserConfig = [&](const QString &meterThingId) {
|
||||
LoadConfigs cfgs;
|
||||
QVariantMap routeur{
|
||||
{"id", varId.toString()}, {"label", "Routeur PV"}, {"adapter", "etmvariableload"},
|
||||
{"mode", "dynamic"}, {"maxPowerW", 3000}, {"priority", 1}, {"enabled", true}};
|
||||
if (!meterThingId.isEmpty())
|
||||
routeur.insert("meterThingId", meterThingId);
|
||||
cfgs.append(LoadConfig::fromMap(routeur));
|
||||
cfgs.append(LoadConfig::fromMap(sgReadyConfigMap("pac-mesure", "PAC", k1, k2,
|
||||
800.0, 1600.0, 300, 2)));
|
||||
cfgs.append(LoadConfig::fromMap(QVariantMap{
|
||||
{"id", "ecs-mesure"}, {"label", "ECS relais"}, {"adapter", "relay-router"},
|
||||
{"mode", "fixed"}, {"priority", 3}, {"enabled", true},
|
||||
{"relays", QVariantList() << QVariantMap{{"thingId", rA.toString()},
|
||||
{"powerW", 1000}}},
|
||||
{"minOnS", 0}, {"minOffS", 0}}));
|
||||
QString err;
|
||||
QVERIFY2(store->setConfigs(cfgs, &err), err.toUtf8());
|
||||
};
|
||||
|
||||
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
|
||||
|
||||
// ---- 1. Les TROIS sources, sans une ligne de configuration de compteur ----------------
|
||||
poserConfig(QString());
|
||||
meter->setStateValue("currentPower", -6000);
|
||||
arb->simulationCallUpdate(t0);
|
||||
QCoreApplication::processEvents();
|
||||
QHash<QString, QVariantMap> tele = telemetrieParId(arb->loadTelemetry());
|
||||
|
||||
// L'appareil piloté publie sa puissance : le moteur la lit, personne n'a rien désigné.
|
||||
// 0 W est une MESURE, pas une absence — et la source ne doit pas basculer pour autant.
|
||||
const QVariantMap mRouteur = tele.value(varId.toString()).value("measurement").toMap();
|
||||
QCOMPARE(mRouteur.value("source").toString(), QString("device"));
|
||||
QVERIFY2(mRouteur.contains("powerW"), "une charge mesurable doit publier sa valeur");
|
||||
QCOMPARE(qRound(mRouteur.value("powerW").toDouble()), 0);
|
||||
|
||||
// Le relay-router est servi : son relais est fermé et le mock rapporte 1 000 W. La mesure
|
||||
// traverse bien le relais — c'est ce qui distingue un organe de COMMUTATION d'un contact
|
||||
// de signal (voir la PAC juste dessous).
|
||||
const QVariantMap mEcs = tele.value("ecs-mesure").value("measurement").toMap();
|
||||
QCOMPARE(mEcs.value("source").toString(), QString("device"));
|
||||
QCOMPARE(qRound(mEcs.value("powerW").toDouble()), 1000);
|
||||
|
||||
// La PAC : « none », ALORS QUE ses contacts portent currentPower. Un contact SG-Ready est
|
||||
// un contact de signal — mesurer sa bobine ne mesure pas la PAC. C'est un état POSITIF,
|
||||
// et powerW est OMIS : une nominale qui se ferait passer pour une mesure produirait un
|
||||
// écart identiquement nul à tous les cycles.
|
||||
const QVariantMap mPac = tele.value("pac-mesure").value("measurement").toMap();
|
||||
QCOMPARE(mPac.value("source").toString(), QString("none"));
|
||||
QVERIFY2(!mPac.contains("powerW"),
|
||||
"une charge non mesurable a publié une puissance sous le nom d'une mesure");
|
||||
|
||||
// Et l'ancienne clé a bien DISPARU — deux noms pour une chose divergent.
|
||||
QVERIFY2(!tele.value(varId.toString()).contains("measuredW"), "measuredW survit");
|
||||
|
||||
// ---- 1-bis. Palier 0 : la SOURCE ne bascule pas, la valeur seule tombe -----------------
|
||||
// C'est la règle « la capacité se juge sur la CLASSE de tous les relais déclarés, jamais
|
||||
// sur ceux qui sont fermés ». Jugée sur les relais fermés, elle répondrait « none » ici —
|
||||
// et la source publiée basculerait à chaque extinction, sur une installation dont rien
|
||||
// n'a changé. Il faut un budget NÉGATIF : à budget nul le recrédit anti-clignotement
|
||||
// rendrait à l'ECS les 1 000 W qu'il consomme, et le relais resterait fermé.
|
||||
meter->setStateValue("currentPower", 2000); // 2 000 W IMPORTÉS
|
||||
arb->simulationCallUpdate(t0.addSecs(30));
|
||||
QCoreApplication::processEvents();
|
||||
tele = telemetrieParId(arb->loadTelemetry());
|
||||
const QVariantMap mEcsEteint = tele.value("ecs-mesure").value("measurement").toMap();
|
||||
QCOMPARE(mEcsEteint.value("source").toString(), QString("device"));
|
||||
QCOMPARE(qRound(mEcsEteint.value("powerW").toDouble()), 0);
|
||||
|
||||
// ---- 2. La valeur SUIT l'appareil ------------------------------------------------------
|
||||
meter->setStateValue("currentPower", -6000);
|
||||
varThing->setStateValue("currentPowerW", 2900);
|
||||
arb->simulationCallUpdate(t0.addSecs(60));
|
||||
QCoreApplication::processEvents();
|
||||
tele = telemetrieParId(arb->loadTelemetry());
|
||||
QCOMPARE(tele.value(varId.toString()).value("measurement").toMap()
|
||||
.value("source").toString(), QString("device"));
|
||||
QCOMPARE(qRound(tele.value(varId.toString()).value("measurement").toMap()
|
||||
.value("powerW").toDouble()), 2900);
|
||||
|
||||
// ---- 3. Le compteur EXPLICITE reste prioritaire ----------------------------------------
|
||||
// Pas d'abord parce qu'il mesure mieux : parce qu'un réglage sans effet est pire que pas
|
||||
// de réglage. 1234 W du compteur doivent l'emporter sur 2900 W déclarés par l'appareil.
|
||||
poserConfig(divId.toString());
|
||||
arb->simulationCallUpdate(t0.addSecs(120));
|
||||
QCoreApplication::processEvents();
|
||||
tele = telemetrieParId(arb->loadTelemetry());
|
||||
const QVariantMap mAvecCompteur = tele.value(varId.toString()).value("measurement").toMap();
|
||||
QCOMPARE(mAvecCompteur.value("source").toString(), QString("meter"));
|
||||
QCOMPARE(qRound(mAvecCompteur.value("powerW").toDouble()), 1234);
|
||||
|
||||
// ---- 4. …mais la priorité porte sur la SOURCE, pas sur la valeur -----------------------
|
||||
// Compteur déclaré et injoignable : on retombe sur l'appareil. Se taire perdrait une
|
||||
// mesure qu'on a, et le silence se lirait « charge non mesurable » — ce qui serait faux.
|
||||
poserConfig(QUuid::createUuid().toString());
|
||||
arb->simulationCallUpdate(t0.addSecs(180));
|
||||
QCoreApplication::processEvents();
|
||||
tele = telemetrieParId(arb->loadTelemetry());
|
||||
const QVariantMap mRepli = tele.value(varId.toString()).value("measurement").toMap();
|
||||
QCOMPARE(mRepli.value("source").toString(), QString("device"));
|
||||
QCOMPARE(qRound(mRepli.value("powerW").toDouble()), 2900);
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -172,6 +172,13 @@ private slots:
|
||||
// Le mode manuel, lui, reste une exclusion : la seconde moitié du test le prouve.
|
||||
void testChargerWithoutCarIsArbitrated();
|
||||
|
||||
// ── §11 / LM-1105 + LM-1106 ────────────────────────────────────────────
|
||||
// La mesure d'une charge se lit sur l'APPAREIL PILOTÉ, sans qu'on ait à le désigner comme
|
||||
// son propre compteur — et ce qui est publié est un COUPLE {valeur, source}. Les trois
|
||||
// sources sont éprouvées, plus les deux règles de priorité : le compteur explicite gagne,
|
||||
// mais un compteur injoignable fait retomber sur l'appareil au lieu de taire la mesure.
|
||||
void testMeasurementSourceIsPublished();
|
||||
|
||||
void printStates(Thing *thing);
|
||||
void updateChargerMeter(Thing *thing);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user