4 Commits

Author SHA1 Message Date
Patrick Schurig
e37f204047 fix(config): ECS-110-b — un Thing n'appartient qu'à UNE charge active
Le cas général n'était pas couvert, et le trou était complet. isValid() s'arrête au bord
d'une charge : deux charges de configuration pouvaient revendiquer le même Thing — un
relais déclaré à la fois dans la charge ECS et dans un état SG-Ready — avec exactement
les conséquences que la garde de l'arbitre évite pour la PAC codée en dur : deux
commandes contradictoires sur un organe, sa puissance comptée deux fois dans le budget
(règle absolue 1). Rien ne l'empêchait, rien ne l'aurait signalé.

L'exclusivité est une propriété de l'ENSEMBLE, qu'aucune charge ne peut vérifier seule :
LoadConfigStore::validateSet(), appelée par setConfigs() (rejet total, rien persisté) et
au chargement (l'entrée en conflit est écartée, le reste survit — écarter une charge ne
commande rien, la charger commanderait faux).

Deux doublons intra-charge manquaient aussi : deux étages relay-router sur le même Thing,
que le routeur fusionnait en silence en annonçant le double de la puissance réelle, et un
relais répété dans un même état SG-Ready. Un relais présent dans PLUSIEURS états reste
légitime : l'état 4 est l'union des états 1 et 3.

Portée tranchée : seules les charges enabled sont confrontées — une charge désactivée ne
construit aucun adaptateur, et l'interdire empêcherait de préparer un remplacement ;
activer repasse par setConfigs(). ThingIds normalisés via QUuid, la configuration
mélangeant « {uuid} » et « uuid ».

LM-302-b : tout mécanisme futur doit déclarer ses Things via claimedThingIds(), seul
point à étendre — l'oublier le rendrait invisible à la vérification.

Suite complète : 112 tests, 0 échec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 16:29:28 +02:00
Patrick Schurig
41d3a329e4 docs(etm): contrats Doxygen — 25 derniers éléments hors fichiers différés
Complète la couverture : 31 → 6 avertissements. Les 6 restants sont exactement
les deux fichiers différés jusqu'à l'étape 3 (relayrouter.h 3,
energyarbitrator.h 3, ce dernier étant la cible d'ECS-412).

energyratioscalculator.h traité EN PREMIER, pour la raison qui le distingue :
c'était le seul endroit où l'absence de documentation cachait une règle métier.
La règle était en fait écrite — mais en commentaires « // », invisibles à
doxygen, donc invérifiables. Conversion en blocs Doxygen SANS réécriture du
fond : les trois cas de reseed de la baseline (premier appel, changement de jour
local, compteur non monotone) et la garantie « dénominateur ≤ 0 → n/a, jamais de
NaN » deviennent des invariants opposables. Précisé au passage que le paramètre
`now` doit être en heure LOCALE — passer de l'UTC déplacerait la frontière de
journée.

Reste du lot : loadaction.h (quels champs sont significatifs selon le kind),
plan.h ([from, to[ et l'interdiction de retourner un plan invalide),
loaddescriptor.h, loadconfigstore.h (tolérance de chargement : un fichier absent
n'est pas une erreur, une entrée invalide est ignorée seule).

Build amd64 0 erreur. Simulation : 7/7, dont testEnergyRatiosAlignment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 11:47:19 +02:00
Patrick Schurig
0fefbc509e fix(doc): deux défauts Doxygen réels dans etm/
Corrections, pas ajouts. Les deux n'apparaissent que sous une configuration
générant réellement une sortie (cf. Doxyfile) — ils étaient invisibles jusqu'ici.

- loadconfigstore.h : « <LoadConfig §4> » dans le bloc de tête était interprété
  comme une balise xml/html non supportée, ce qui tronquait la description du
  format fichier. Reformulé sans chevrons.

- sgreadyadapter.h : lockWindow() documentait « \param[out] minState / maxState »
  en une seule directive ; doxygen n'en reconnaissait aucune des deux et
  considérait « now » et « maxState » comme non documentés. Scindé en trois
  directives, une par paramètre.

Aucun changement de comportement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 11:46:42 +02:00
Patrick Schurig
7184fe4e1d feat(etm): config charge pilotée persistée (Get/SetLoadConfig) + repli L2
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>
2026-06-28 09:51:38 +02:00