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>
Expose autoconsommation/autonomie comme source canonique de l'energymanager,
pour remplacer le seam interim app-side (EnergyRatiosInterim.compute).
Voie (c) : méthode + notification sur le handler NymeaEnergy de NOTRE plugin
(déjà forké/buildé), calculées depuis EnergyManager::totalX() — interface
abstraite stable. PAS de champs sur Energy.PowerBalance → aucun fork du coeur
nymea (nymea-experience-plugin-energy) à rebaser perpétuellement.
- EnergyRatiosCalculator (GPL pur mesure) : Δ de cumuls, baseline minuit local,
reseed (1er appel / nouveau jour / non-monotone Δ<0), den<=0 -> n/a, clamp
[0,100]. Porté 1:1 du seam interim app.
- NymeaEnergy.GetEnergyRatios -> {o:selfConsumptionRate, o:autonomyRate} Double,
champ OMIS si n/a (jamais null).
- NymeaEnergy.EnergyRatiosChanged : meme forme, branchee sur powerBalanceChanged(),
emise uniquement quand une valeur (ou sa disponibilite) change.
- testEnergyRatiosAlignment : vecteurs joues 1:1 contre l'interim (seed, normal,
clamp-bas, den<=0->n/a, non-monotone, nouveau jour local).
- docs/INTERFACE_energyratios.md : contrat pour l'agent app (RPC a consommer).
Build prod 0/0. Suite simulation 25/0 (testLoadConfigRpc traverse le handler
modifie -> pas de regression).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pont moteur↔app : déclaration des charges etmvariableload persistée et éditable,
construction des adaptateurs à chaud depuis la config (remplace le registre en dur).
- LoadConfig / LoadConfigNeeds (Q_GADGET typés, introspectables) — forme mot pour mot
du LoadDescriptor §4 (jonction inter-repos avec l'app).
- LoadConfigStore : persistance atomique /var/lib/nymea/energy-load-configuration.json,
validation en bloc, signal changed().
- Handler NymeaEnergy.GetLoadConfig / SetLoadConfig (+ notif LoadConfigChanged). GET typé
objectRef<LoadConfig> ; SET schéma inline o: (powerLevels/needs conditionnels §4).
- EnergyArbitrator : setLoadConfigStore + rebuildEtmVariableLoadAdapters (enabled==true
seulement, §9) ; repli L2 = setPowerSetpoint(0) force=true sur tout etmvariableload
(ferme le trou sécurité ouvert en T2).
- Tests simulation : migration ECS→etmvariableload (arrondi fixed/dynamic, recrédit,
délestage, round-trip powerSetpoint), budget partagé etmvariableload↔PAC, watchdog L2
réactivé, persistance + construction depuis config + injection RPC end-to-end.
PAC SG-Ready reste hors config (§8, gelée). Les 5 tâches du brief sont couvertes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>