363 Commits

Author SHA1 Message Date
Patrick Schurig
fae7101182 debian: 1.15.2+etm72 — protocole optimiseur 1.2, émission bornée (Q7), instants avec décalage
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HE6X3TcKdiuCucBHA2XX4n
2026-10-22 19:31:14 +02:00
Patrick Schurig
3a4398f0b1 Fusion fix/instants-decalage : tout instant émis porte son décalage
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HE6X3TcKdiuCucBHA2XX4n
2026-10-22 19:30:58 +02:00
Patrick Schurig
052d728137 Fusion fix/q7-request-too-long : le moteur n'émet jamais de ligne de plus de 1 Mio, libellé de charge borné à 256 caractères
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HE6X3TcKdiuCucBHA2XX4n
2026-10-22 19:30:58 +02:00
Patrick Schurig
c831fb5c42 Fusion feature/protocole-1.2 : protocole optimiseur 1.2, règles Q2 à Q9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HE6X3TcKdiuCucBHA2XX4n
2026-10-22 19:30:58 +02:00
Patrick Schurig
f29d83e2f9 test(conformance) : la session jugée sous un fuseau non UTC
testTheSessionMatchesTheContractOffUtc : le moteur sous Europe/Berlin,
cadencé en heure locale comme en production, du 2026-10-25 02:58 CEST au
retour à 02:00 CET. Aucun échec de requête, et la session passe
tools/etm-powersync-schema-check --semantic contre le contrat embarqué.

Contre-épreuve, correctif retiré : 8 rpc-error (le constat VM), et le
validateur seul refuse chaque params.timestamp sur son motif `pattern`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:10:15 +02:00
Patrick Schurig
9d8b60fdd4 protocole optimiseur : tout instant émis porte son décalage
Constat VM du 2026-10-21 (Europe/Berlin) : l'optimiseur conforme refusait
chaque GetPlan, -32602 sur params.timestamp. `instant()` écrivait
toString(Qt::ISODate), qui n'émet aucun décalage pour une heure locale —
celle de currentDateTime(), donc de ctx.timestamp en production. Les
goldens, construits en UTC ou à décalage fixe, ne pouvaient pas le voir.

Le décalage de l'instant reçu est désormais rendu explicite : une heure
locale garde l'heure du site (+02:00), une heure UTC reste en Z. Il est lu
sur l'instant, donc 02:30 la nuit du retour à l'heure d'hiver sort +02:00
puis +01:00. Goldens inchangés.

Tests (loadmodel) : cinq fuseaux dont un négatif et un à la demi-heure,
été et hiver, tous les instants du contexte ; la nuit des deux changements
d'heure, minute par minute. Vérifiés échouant sans le correctif.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:10:15 +02:00
Patrick Schurig
6b1dd95e2e SetLoadConfig : libellé de charge borné à 256 caractères, à l'écriture seulement (Q7)
Le libellé part tel quel dans chaque GetPlan. Rien ne le bornait : une saisie de plus de
1 Mio suffisait à faire dépasser la limite de ligne du protocole. La borne arrête la
saisie en amont ; le refus d'émission du client (commit précédent) reste le filet.

- LoadConfigStore::validateLabels(), appelée en tête de setConfigs() : un libellé de plus
  de 256 points de code Unicode → refus en bloc, rien n'est modifié ; SetLoadConfig rend
  EnergyErrorInvalidParameter et journalise le motif en français, qui nomme la charge par
  son id et donne la longueur, jamais le libellé. Placée AVANT isValid() parce que les
  motifs suivants citent le libellé.
- Même partage que les rangs (LM-1209-a-bis) : load() ne l'applique pas. Un libellé plus
  long déjà persisté est chargé tel quel, ni tronqué ni écarté, et signalé ; la première
  écriture devra le raccourcir.
- Test : testLabelIsBoundedOnWriteNeverOnLoad (256 accepté, 257 refusé sans modification
  en mémoire ni sur disque, configuration héritée conservée entière).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:07:32 +02:00
Patrick Schurig
cd91baf98f optimiseur : le moteur n'émet jamais de ligne de plus de 1 Mio (Q7, protocole 1.2)
Ferme l'écart Q7 trouvé au lot protocole 1.2. Une requête GetPlan dont la ligne
dépasserait maxLineBytes n'est plus écrite sur le socket : OptimizerClient::requestPlan()
sérialise d'abord, rend l'identifiant, puis émet planFailed(id, "request-too-long") au
tour de boucle suivant. L'échec compte (règle des échecs consécutifs, §7), la connexion
est conservée, et le journal le dit une fois par connexion, avec la taille, jamais le
contenu. Avant : la ligne partait, l'optimiseur la jetait, et la requête finissait en
délai dépassé sans que la cause se voie.

Le handshake n'est pas contrôlé : sa ligne est bornée par construction (token de 4 096
octets au plus, 24 Kio une fois échappé).

- knownErrorCodes() : + request-too-long ; LastErrorCode du contrat régénéré
  (--enums-from), blocs générés de la prose régénérés.
- OPTIMIZER_PROTOCOL.md §0/§2 et x-etm-transport : l'écart connu devient la règle tenue.
  Pas de changement de version.
- Tests : testARequestOverTheLineLimitIsNeverSent remplace le XFAIL (aucun octet émis,
  limite exacte éprouvée des deux côtés), testARequestTooLongIsLoggedOncePerConnection,
  testARequestTooLongCountsAsAFailure (socketscheduler).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:07:11 +02:00
Patrick Schurig
8f24709874 protocole optimiseur 1.2 : les règles Q2 à Q9, et le moteur annonce 1.2
docs/OPTIMIZER_PROTOCOL.md passe en v1.2 (mineure) et écrit les décisions du
2026-10-21 à leur place :

- §2 : maxLineBytes (1 Mio, '\n' non compté) dans les deux sens ; lots
  interdits dans les deux sens ; /run/powersync appartient au paquet de
  l'optimiseur, le moteur ne le crée ni ne le suppose. Écart connu noté :
  le moteur ne borne pas encore ce qu'il émet.
- §3 : la majeure se compare des deux côtés (-32602 puis fermeture) ; -32001
  normé ; un token non exigé est accepté ; un handshake refusé est fermé des
  deux côtés ; GetPlan avant handshake → -32600, connexion gardée.
- §7 : contexte GetPlan invalide → -32602 avec chemins et natures, jamais les
  valeurs, compté comme un échec ; codes JSON-RPC 2.0 standard sinon ; table
  des erreurs GÉNÉRÉE depuis le contrat.
- §8 : UpdateLearned à sens unique, -32601 avec id ; forme non normée en 1.2.
- §9, §0 (changelog, compatibilité 1.1), annexes A, E, G.

Contrat OpenRPC : methods[].errors complétés (x-etm-connection),
x-etm-errors-engine, x-etm-message-errors (nouveau), x-etm-transport
(maxLineBytesScope, batches, runtimeDirectory), règle handshake-refused.
La version (info.version, ProtocolVersion) régénérée depuis le code par
--enums-from.

Moteur : OptimizerProtocol::ProtocolVersion et OptimizerClient::kProtocolVersion
passent à "1.2" ; testHandshakeAndPlan vérifie aussi que les deux constantes
sont égales. Aucun autre comportement ne change.

Tests suivis : version attendue dans testoptimizerclient et
testsocketscheduler, handshake de la session de schema-check-test, et un
test du bloc « errors » dans gen-protocol-doc-test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 14:01:46 +02:00
Patrick Schurig
7bb2b4a194 test(conformance) : sonde des règles 1.2 que le moteur ne provoque pas
Quatre fonctions, une connexion de SONDE sur l'optimiseur (sans le moteur),
pour ce que le client du moteur n'envoie jamais :

- testGetPlanBeforeTheHandshakeIsRefused (Q3) : -32600 à son id, puis le
  handshake sur la même connexion est accepté (connexion gardée) ;
- testARefusedHandshakeIsClosedByTheOptimizer (Q3, Q4) : autre majeure →
  -32602 puis l'optimiseur ferme ; avec un token attendu, token faux →
  -32001, fermé pareil ;
- testABatchIsRefused (Q8) : un lot → une erreur -32600 à id null, son
  contenu non traité, connexion gardée ;
- testALineOverTheLimitIsRefused (Q7) : maxLineBytes + 1 → -32600 à id null,
  une ligne d'exactement maxLineBytes est lue.

Le traitement d'un message se constate par une réponse normée sans contexte :
UpdateLearned avec id → -32601 (Q6). Réponses lues brutes : un lot renvoyé
se verrait. README : tableau, boucle de lancement, ce qui n'est pas vérifié.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 14:01:35 +02:00
Patrick Schurig
4cbbb81802 optimiseur de référence : protocole 1.2 — codes d'erreur normés, lots, lignes trop longues
Il s'annonce 1.2 et s'aligne sur les règles écrites par ce lot :

- Q3 : GetPlan avant un handshake réussi → -32600 (était -32002, hors norme),
  connexion gardée ; Handshake d'une autre majeure → -32602 puis fermeture
  (il ne comparait pas la majeure du moteur) ; token refusé → -32001, inchangé.
- Q2 : un timestamp illisible ou sans décalage n'est plus une abstention mais
  -32602, error.data.problems = [{path, problem}], jamais la valeur reçue.
- Q7 : une ligne de plus de 1 Mio ('\n' non compté) reçoit -32600 à id null.
  Correctif au passage : readline(LIGNE_MAX + 2) rendait le reste d'une
  ligne trop longue comme des lignes suivantes, et comptait le '\n' (une
  ligne d'exactement 1 Mio était refusée). La ligne est désormais lue jusqu'à
  sa fin par morceaux, sans être gardée.
- Q8 : un lot reçoit -32600 à id null, aucun de ses messages n'est traité
  (il était ignoré en silence).
- -32700 à id null pour une ligne non JSON (ignorée en silence avant).
- Q5 et Q6 étaient déjà tenus : token accepté sans --token-file,
  UpdateLearned en notification ignorée, avec id → -32601.

README : tableau des erreurs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 14:01:25 +02:00
Patrick Schurig
952f64d24b test(optimizerclient) : le moteur face aux règles 1.2 — fermeture, lots, longueur de ligne
Preuves, côté moteur, des points Q4, Q7 et Q8 du protocole 1.2. Aucun code du
moteur n'est modifié.

- testARefusedHandshakeIsClosedByTheEngine (Q4) : -32001 ou -32602 au
  handshake, faux optimiseur qui garde son socket ouvert → c'est le moteur qui
  ferme (handshake-refused), nouvelle tentative au backoff seulement.
- testAReceivedBatchIsNeverUnpacked (Q8, reçu) : un lot portant la réponse
  attendue est une ligne écartée (invalid-json), connexion conservée, réponse
  non lue ; la requête finit en délai dépassé.
- testEachLineSentIsOneObject (Q8, émis) : lu sur les octets, chaque ligne
  émise est UN objet JSON, une requête remplacée donne une ligne de plus.
- testAContextOverTheLineLimitIsStillSent (Q7, émis) : ÉCART constaté, non
  corrigé dans ce lot — requestPlan() ne borne pas ce qu'il émet ; un GetPlan
  de plus de 1 Mio part tel quel. QEXPECT_FAIL : le test passera en XPASS le
  jour où le moteur bornera l'émission.

Q7 en réception était déjà éprouvé par testLineLengthLimit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 14:01:16 +02:00
Patrick Schurig
e56485d014 outil(protocole) : bloc généré « errors », les erreurs que renvoie l'optimiseur
etm-powersync-gen-protocol-doc sait rendre un bloc `errors` : une table des
methods[].errors puis de x-etm-message-errors (code, méthode, message,
connexion après l'erreur, quand), suivie de ce que le moteur fait d'une
réponse error (x-etm-errors-engine). Aucune prose n'utilise encore le bloc :
--check reste vert sur l'état commité.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 14:00:57 +02:00
Patrick Schurig
73b956d1fc debian: 1.15.2+etm71 — éco sous le premier palier, droits NymeaEnergy.* séparés
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 07:41:24 +02:00
Patrick Schurig
0346775bb9 Fusion feature/droits-lecture-ecriture : droits de NymeaEnergy.* séparés
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 07:40:55 +02:00
Patrick Schurig
341f4ddbae Fusion fix/eco-sous-palier : ECO_FLOOR_BELOW_STAGE, sous le premier palier rien n'est acheté
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 07:40:55 +02:00
Patrick Schurig
3ddf88efac test(decisions): référence régénérée après ECO_FLOOR_BELOW_STAGE
Moteur de cd7cd1a contre l'ancienne référence (61db6cf), comparateur
--sous-palier, lignes du nouveau test retirées : IDENTIQUE HORS MOTIFS ET HORS
MOTIF ÉCO SOUS PALIER — 23 792 lignes, 23 784 identiques à l'octet, 8 qui ne
diffèrent que par ECO_FLOOR_GRID → ECO_FLOOR_BELOW_STAGE (niveau éco, et
decision quand l'éco est en tête). purchasedWh identique, sans --achat.
Aucune consigne ne change. S'y ajoutent les 4 lignes de
testEcoBelowTheFirstStageBuysNothing : 69 fonctions, 23 796 lignes.
check : IDENTIQUE, 0 rejeu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 08:57:52 +02:00
Patrick Schurig
35808f17bd test(decisions): comparateur --sous-palier, seul ECO_FLOOR_GRID → ECO_FLOOR_BELOW_STAGE est admis
Retire le motif des niveaux « eco » portant l'un des deux codes et les
confronte deux à deux : tout autre passage, ou un ECO_FLOOR_GRID aux
paramètres modifiés, est un ÉCART. targetW, counts, progress du niveau restent
comparés. Les changements sont listés par scénario et par charge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 08:43:30 +02:00
Patrick Schurig
30527b9373 docs(protocole): ECO_FLOOR_BELOW_STAGE n'est pas émissible par un optimiseur, et pourquoi
Prose §6 seulement : la liste blanche et le contrat OpenRPC ne changent pas.
L'optimiseur ne reçoit pas les paliers (powerLevels non émis, §5.3) ; seul le
moteur sait qu'une consigne tombe sous le premier palier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 08:43:30 +02:00
Patrick Schurig
cd7cd1a37a fix(eco): ECO_FLOOR_BELOW_STAGE — sous le premier palier, le niveau éco dit « rien n'est acheté »
Sur une charge à paliers (powerLevels déclarés : relay-router, etmvariableload
fixed), une cadence éco sous le premier palier laisse le mécanisme au palier 0.
Le niveau éco publiait pourtant ECO_FLOOR_GRID « 83 W achetés au réseau ».
Il porte désormais ECO_FLOOR_BELOW_STAGE {targetW, stageW, gridW: 0,
o:remainingWh, o:remainingS}. Le premier palier est declared.powerLevels[1],
la source de BELOW_MIN_POWER en passe confort. Une charge continue garde
ECO_FLOOR_GRID.

Aucune consigne ne change : même targetW, mêmes counts, même draw, même
palier, même purchasedWh. Seul le motif du niveau change, et avec lui (R7,
inchangée) decision, decision.level et les lignes [Arbitre]/[RelayRouter].
Hors liste blanche optimiseur : l'optimiseur ne reçoit pas les paliers (E9).

Tests : testEcoBelowTheFirstStageBuysNothing (neuf, échoue sur 61db6cf),
testTheDecisionDescribesTheMergedAction mis à jour (il exigeait l'achat sur
un relais de 1000 W à 83 W), rendu FR et refus optimiseur en loadmodel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 08:43:24 +02:00
Patrick Schurig
6c68d17edc test(charging): table des droits NymeaEnergy.* figée, et droits mordant sur transport authentifié
testNymeaEnergyPermissionScopes compare le permissionScope PUBLIÉ par JSONRPC.Introspect (la
table que nymea consulte) à une table écrite dans le test : méthode ajoutée, retirée ou droit
changé → échec, avec tous les écarts listés.

testNymeaEnergyPermissionsEnforced crée de vrais comptes sur le MockTcpServer du harnais, dont
l'authentification est active (Hello : authenticationRequired vérifié) :
- sans droit : GetLoadTelemetry, GetTariff, GetBatteryLevelConsideration répondent ;
  SetTariff {zones: []} et SetLoadDerogation → « Permission denied. », tarif inchangé ;
- ControlThings : SetLoadDerogation posée puis levée ; SetLoadConfig refusé, config inchangée ;
- ConfigureThings : SetTariff passe ; SetPhasePowerLimit et GetOptimizerTrace refusés.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 08:26:24 +02:00
Patrick Schurig
8a055eee9c feat(rpc): droits de NymeaEnergy.* séparés — lire, commander, configurer, administrer
Chaque méthode déclare son droit au registerMethod ; plus aucune ne tombe en Admin par la
valeur par défaut de libnymea.

- None (tout compte authentifié) : tous les Get* sauf GetOptimizerTrace, dont
  GetBatteryLevelConsideration (Admin par oubli jusqu'ici). GetOptimizerStatus ne porte ni
  l'endpoint ni le token.
- ControlThings : SetChargingInfo, SetLoadDerogation, ClearLoadDerogation, ClearLoadFault,
  SetLockOnUnplug.
- ConfigureThings : SetLoadConfig, SetTariff, SetForecastSite, SetAcquisitionTolerance,
  SetSpotMarketConfiguration.
- Admin, désormais écrit : SetPhasePowerLimit, SetBatteryLevelConsideration,
  SetOptimizerEndpoint, SetOptimizerToken, GetOptimizerTrace, SetOptimizerTrace.

Un registerMethod privé SANS valeur par défaut masque celui de JsonHandler : oublier le droit
ne compile plus. C'est la seule garde possible pour une méthode Admin, le défaut étant Admin.

Changement de contrat RPC (JSONRPC.Introspect publie permissionScope). Les droits ne mordent
que sur un transport avec authentification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 08:26:24 +02:00
Patrick Schurig
61db6cfd66 test(conformance): mode attendu plan | abstain, un optimiseur qui s'abstient est jugé conforme
S'abstenir est une réponse permise (§7). Avec ETM_POWERSYNC_CONFORMANCE_EXPECT=abstain,
testEveryPlanIsAccepted exige une abstention à chaque cycle (rules, fallbackCode abstain,
verdict abstention/abstain, aucun échec, optimizerAlive inchangé) ; testTheSafetyLoopStaysOnTop
vérifie L4 sous le rule-based (shed exigé) puis se termine en QSKIP, rien à écrêter. Mode `plan`
(défaut) inchangé. Le mode retenu est affiché en tête de chaque fonction.

Pas de déduction depuis le handshake : `planning` est informatif (multi-créneaux ou non) et
`kinds` absent veut dire « tout admis » ; la référence en mode abstain annonce des kinds, le mode
protocol-1.0 (planning false, sans capabilities) planifie. README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:46:53 +02:00
Patrick Schurig
308444f39d test: traces réelles du moteur jugées contre le contrat (refoptimizer)
Les 16 lignes de testTracesReplayWithZeroDivergence : chaque requête, et
chaque réponse de l'optimiseur rejugée sur sa requête (--trace --semantic).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:48:30 +02:00
Patrick Schurig
fc41c4ff40 test: suite etm-powersync-optimizer-conformance pour un optimiseur quelconque
Lance l'optimiseur de $ETM_POWERSYNC_OPTIMIZER_CMD (défaut : la référence,
mode normal) et y branche le vrai moteur : handshake direct accepté, GetPlan
répondus sous 10 s, session entière conforme au contrat embarqué (règles
sémantiques comprises), chaque plan appliqué sans fallbackCode, consigne hors
bornes écrêtée et surcharge délestée par L4. Relais transparent pour capturer
la session. README : la lancer contre un autre optimiseur.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:46:53 +02:00
Patrick Schurig
2f191f4bec docs(protocole): messages générés depuis le contrat, prose réalignée sur le code
32 blocs générés (§3, §5.1-§5.3, §6, §7, §8) ; annexe G (contrat, validateur,
conformité). Écarts prose/code corrigés, le contrat suivant le code : refus de
token par réponse error, optimizerAlive inchangé sur abstention, exemples du
§5 sans les champs jamais émis (provider, perField détaillé, weather, comfort,
defaults, group, nominalPowerW, volumeL) et avec ceux qui le sont (available,
currentPowerW), instants avec décalage, engine réel en annexe A ; §5.2 aligné
sur la décision du 2026-10-16 (l'optimiseur ne consomme que le contexte).
Contrat : listes régénérées depuis le code (ordre des clés de x-etm-reason-params).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:34:17 +02:00
Patrick Schurig
ead662e044 feat(outils): etm-powersync-gen-protocol-doc, prose générée depuis le contrat
Tableaux de messages (champs, types, obligatoire, valeurs, sens), règles
sémantiques et codes de motif, entre balises BEGIN/END GENERATED de
OPTIMIZER_PROTOCOL.md. --check (code 1 si la prose ou la forme canonique du
contrat n'est pas à jour) branché en 5/5 de tools/ci-quality.sh. --enums-from
pose les listes fermées du contrat depuis le dump du code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:34:17 +02:00
Patrick Schurig
07cb44d35f test: les requêtes réelles du moteur jugées contre le contrat
optimizerclient : chaque Handshake envoyé ; socketscheduler : tout ce que le
moteur envoie au faux optimiseur, plus testTheEngineSpeaksTheContractItServes
(GetOptimizerProtocol == embarqué == docs, requêtes jugées avec le contrat
servi) ; refoptimizer : session capturée par la référence, requêtes toujours,
session entière et règles sémantiques en normal et en 1.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:30:44 +02:00
Patrick Schurig
d95a65cb71 test: suite etm-powersync-protocolschema (contrat == docs == code)
Contrat embarqué identique à docs/ ; listes x-etm-generated égales au code
(ancre côté test, dump pour régénérer) ; chaque code de verdict, d'erreur et
de repli lu dans les SOURCES figure dans les listes ; règles sémantiques ==
verdicts du validateur, même ordre ; sérialiseur GetPlan jugé conforme sur
toutes ses branches par le validateur publié.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:30:44 +02:00
Patrick Schurig
053263bc38 feat(rpc): NymeaEnergy.GetOptimizerProtocol sert le contrat embarqué
Le contrat OpenRPC est une ressource Qt du plugin (jamais lu sur disque à
l'exécution) ; GetOptimizerProtocol rend { protocolVersion, schema }, lecture,
PermissionScopeControlThings, aucun secret, sans *Changed (statique pour un
binaire). Documenté dans INTERFACE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:30:44 +02:00
Patrick Schurig
b45d0496c6 feat(moteur): listes fermées du protocole exposées par le code
OptimizerPlan::kinds() (désormais appliquée par convertirAction) et
verdictCodes(), dans l'ordre des contrôles ; SocketScheduler::knownTriggers()
(appliquée par rangDeclencheur), knownFallbackCodes(), knownLastErrorCodes() ;
OptimizerClient::knownErrorCodes(). Le contrat OpenRPC en régénère ses
énumérations. Aucun comportement changé : mêmes listes, mêmes ordres.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:30:44 +02:00
Patrick Schurig
fb581218c8 feat(outils): etm-powersync-schema-check, validateur du contrat OpenRPC
Python, bibliothèque standard seule. Sous-ensemble de JSON Schema déclaré ;
un mot-clé non géré est une erreur de schéma, jamais ignoré. Valide un
message, une session NDJSON ou des traces ; --semantic applique
x-etm-semantic-rules dans l'ordre du validateur du moteur (une règle sans
implantation, ou l'inverse, est une erreur).

Test : références du sérialiseur et traces réelles du moteur conformes ;
optimiseur de référence mode par mode, et qui rejette chaque mode mauvais.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:15:51 +02:00
Patrick Schurig
fc8bc1acd3 feat(ref-optimizer): --capture de la session et mode wrong-type
--capture écrit chaque message reçu et envoyé (NDJSON), token remplacé par
autant d'astérisques. wrong-type rend un planId numérique, que le moteur
accepte et que le contrat refuse : contre-épreuve de la conformité.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:15:51 +02:00
Patrick Schurig
9322bc8a2f docs(protocole): contrat OpenRPC 1.3 du protocole optimiseur 1.1
docs/etm-powersync-optimizer-protocol.openrpc.json décrit Handshake, GetPlan
(SurplusContext complet : en-tête, état, forecast, loads[] par mécanisme) et
UpdateLearned, relus champ par champ sur le sérialiseur, le client et le
validateur du moteur. Requêtes du moteur : additionalProperties false.
Réponses d'optimiseur : ouvertes (§9). Transport, sens, heartbeat, machine §7,
règles sémantiques du validateur et tolérances : extensions x-etm-…

Les listes fermées marquées x-etm-generated seront régénérées depuis le code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:15:51 +02:00
Patrick Schurig
11bed27572 release: 1.15.2+etm70 — correctifs a, b, c ; socket optimiseur 1.1, traces, outils de test (brief 4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ry3vBrJT1tHdxAaXdCWi6L
2026-10-17 14:56:55 +02:00
Patrick Schurig
31269b531f docs(optimiseur): paramètre de OptimizerClient::tokenLength documenté (correctifs brief 4, point 4)
Doxygen : 1 avertissement introduit par 5768c36 (paramètre non documenté) ;
0 après. Commentaire seul.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:55:44 +02:00
Patrick Schurig
5768c36953 fix(optimiseur): token d'au moins 16 caractères (correctifs brief 4, point 4)
Question 4 de T7 : le masquage à la réception remplace TOUTE occurrence du
token dans ce que l'optimiseur renvoie ; un token court, présent par hasard
dans un identifiant de charge, corromprait le plan reçu (unknown-load).

- SetOptimizerToken (writeToken) refuse un token non vide de moins de
  16 caractères (points de code UTF-8) : EnergyErrorInvalidParameter, fault
  `token-refused` comme les autres refus ; rien n'est modifié ; le motif est
  journalisé, jamais la valeur. Vide = retrait, inchangé.
- Le client relit le fichier au début de chaque tentative ; plus court que
  16 caractères, il ne se connecte PAS (ni avec ce token, ni sans token : les
  mesures du site ne partent pas vers un optimiseur que rien n'authentifie).
  lastErrorCode `token-too-short`, dit une fois par série, nouvelle tentative
  au backoff jusqu'au remplacement du fichier.

INTERFACE.md (SetOptimizerToken, lastErrorCode) et protocole §3 (E-T2-8,
forme du token). Tests : 15 refusé, 16 accepté, 15 « é » refusés (caractères,
pas octets), vide = retrait, fichier court sans connexion puis token conforme
envoyé ; tokens des suites portés à 16 au moins. Contre-épreuves M4a (refus
retiré) et M4b (fichier court envoyé) : mordent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:44:19 +02:00
Patrick Schurig
e3b916e249 fix(optimiseur): fallbackCode dit pourquoi on replie (correctifs brief 4, point 3)
Constat `fallbackCode` de T7 (E-T7-6) : une abstention, un plan expiré ou
refusé à la réception vidaient le cache, et le cycle suivant repliait avec
`no-plan` — l'app ne distinguait pas « l'optimiseur s'abstient » de « rien
reçu ».

Le code du verdict de réception d'une réponse non appliquée est gardé comme
code de repli jusqu'à l'issue de la requête suivante ; un plan valide
l'efface, un échec de requête ou une connexion perdue le ramènent à
`no-plan`, qui ne dit plus que « rien reçu ». Publication seule : aucune
décision ne change.

INTERFACE.md : liste fermée des valeurs de fallbackCode et leur lecture
(« pas d'optimiseur », « pas encore de plan », « l'optimiseur s'abstient »,
« plan refusé »). Tests : refoptimizer lit GetPlan.fallbackCode après chaque
cycle de chaque mode (abstain, expired, invalid, unknown-load, missing-load…,
puis retour normal sans fallbackCode) ; socketscheduler vérifie le code publié
pour chaque refus (les tests qui épinglaient `no-plan` sont mis à jour).
Contre-épreuve M3 (toujours `no-plan`) : mord.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:39:12 +02:00
Patrick Schurig
ce323a6e1f fix(optimiseur): une tentative de reconnexion n'est pas une nouvelle perte (correctifs brief 4, point 2)
Constat `drop` de T7 : SocketScheduler notait ConnexionPerdue à CHAQUE état
non connecté du client (Connecting, Waiting d'une tentative), réécrivait
lastErrorCode avec celui du client et émettait etatChange() à chaque
tentative au backoff (une par minute au plafond).

La perte se note désormais à la transition : sortie de Ready ou de
Handshaking (fermeture, handshake refusé), ou premier échec d'un client qui
n'a jamais été connecté (sinon un optimiseur absent dès le départ
n'aurait jamais de lastErrorCode). Les tentatives suivantes d'une panne déjà
notée ne touchent ni au dernier code, ni aux échecs, ni à l'état publié.
Publication seule : aucune décision ne change.

Test socketscheduler::testAReconnectionAttemptIsNotANewLoss : optimiseur
disparu, 3 tentatives au backoff → 1 seul etatChange, 1 seule notification,
lastErrorCode `connection-lost` (le client en est à `connect-failed`) ; au
retour, les transitions du §7 et rien d'autre. Ligne `drop` de refoptimizer :
le dernier code reste `disconnected`. Contre-épreuve M2 (ancien comportement) :
mord sur les deux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:34:38 +02:00
Patrick Schurig
8c0645d371 feat(tools): l'optimiseur de référence s'annonce powersync-optimizer (correctifs brief 4, point 1)
Décision du 2026-10-17 : l'exemple GPL annonce `powersync-optimizer` dans le
`name` du handshake ; `etm-optimizer` reste le nom de l'optimiseur ETM,
propriétaire, hors de ce dépôt. Le fichier et son répertoire gardent le
préfixe des artefacts (`tools/etm-powersync-ref-optimizer/`).

README de l'outil, annexe E et tools/README.md le disent en une phrase ;
refoptimizer vérifie le `name` publié par GetOptimizerStatus (1.1 et 1.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:32:51 +02:00
Patrick Schurig
fb017f87a7 test(intégration): L2 éprouvé sous un plan d'optimiseur qui commande tout à fond (brief 4 T7)
La contre-épreuve E4 (L2 ne suspend plus la planification) ne mordait que par la ligne de
trace : le plan en cache valait déjà 0 avant L2. Le test ramène l'optimiseur à 2000 / 3000 W
et PAC forcée juste avant la coupure du compteur ; E4 mord désormais sur les consignes de repli.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:45:44 +02:00
Patrick Schurig
f8333f42f6 docs(optimiseur): annexe E — chemin, périmètre et codes de l'optimiseur de référence (brief 4 T7)
tools/ref-optimizer → tools/etm-powersync-ref-optimizer ; ce qu'il reproduit (passe confort)
et ce que le contexte 1.1 ne lui permet pas de reproduire ; codes émissibles seulement.
tools/README.md : renvoi vers son README, règle de comparaison d'une action state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:28:34 +02:00
Patrick Schurig
644741b0ef test(intégration): suite etm-powersync-refoptimizer, moteur réel + optimiseur de référence réel (brief 4 T7, §3)
Chaque test lance tools/etm-powersync-ref-optimizer en sous-processus (démarrage borné,
échec en clair, jamais bloqué).
- identité Q6 (banc restreint, passe confort seule) sur unix:// et tcp:// : ce que les mocks
  reçoivent au cycle i sous l'optimiseur == au cycle i-1 sous le rule-based seul, 36 cycles ;
- chaque mode de défaut → la ligne du tableau §7, sur le banc complet, et le retour ;
  notifications une par transition ; slow : update() ≤ 1 ms ;
- optimiseur 1.0 accepté ; traces rejouées sur un nouvel optimiseur : 0 divergence, puis
  mode deviate : divergence, code 1 ; L4 et L2 gagnent ; token recopié : nulle part ;
- catalogue des codes émissibles identique à celui du moteur.
8 fonctions, 33 / 0, 54 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:16:43 +02:00
Patrick Schurig
f42dc709e3 fix(tools): trace-replay compare une action state sur son état seul (brief 4 T7)
Le plan du rule-based tracé porte la puissance ESTIMÉE d'une action state ; un optimiseur ne
la donne pas (§6 ne la définit pas). Le rejeu d'une trace rule-based contre l'optimiseur de
référence voyait donc une divergence sur chaque PAC. Seuls l'état (state) et la puissance
d'une consigne (setpoint) sont comparés. Test unitaire ajouté (7 / 0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:16:43 +02:00
Patrick Schurig
6fcce1a470 feat(tools): etm-powersync-ref-optimizer, optimiseur de référence 1.1 (brief 4 T7, annexe E)
Python 3, bibliothèque standard seule, GPL-3.0. Socket unix par défaut
(unix:///run/powersync/optimizer.sock, 0660), tcp:// accepté. Handshake (majeure, capacités,
token lu dans un fichier et relu à chaque handshake, -32001 puis fermeture s'il diffère),
GetPlan, abstention, plusieurs clients à la fois (un fil par connexion, plan fonction pure de
la requête).

Plan à un créneau qui reproduit la passe confort du waterfall rule-based depuis le seul
contexte GetPlan : tri (rang, id), surplus net signé, recrédit, réserve de stockage, plancher,
plafond, verrous, mapping SG-Ready avec hystérésis. Codes du catalogue émissible seulement,
clés documentées seulement (--list-codes, confronté au moteur par le test).

Modes d'injection : normal, abstain, slow, invalid, expired, wrong-major, drop, unknown-load,
forbidden-kind, missing-load, echo-token, protocol-1.0, deviate ; --mode-file pour changer de
mode sans reconnexion. README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:16:43 +02:00
Patrick Schurig
693e01579f fix(optimiseur): un token recopié par l'optimiseur ne sort par aucun journal (brief 4 T7, correctif A)
Constat de T6 (question 2) : la trace masquait un token recopié dans reason ou planId, mais
les lignes [Optimiseur] (planId, texte, détail des refus, nom du handshake) et
GetOptimizerStatus.name le portaient en clair.

- OptimizerSecret : UNE fonction de masquage (texte brut et échappé JSON, objet JSON à toute
  profondeur, clés comprises ; ligne de trace sérialisée), partagée par le client et les traces.
- OptimizerClient masque chaque message reçu dès son analyse, avec le token envoyé sur cette
  connexion : rien en aval (journal, traces, app) ne voit le secret ; dit une fois.
- Test testAnEchoedTokenLeaksNowhere : secret avec " et \ recopié partout ; aucune ligne de
  journal (tous niveaux), aucune trace, aucune réponse RPC. 4 contre-épreuves mordent.

decisions sans endpoint : IDENTIQUE (23 792 lignes), 0 rejeu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:10:15 +02:00
Patrick Schurig
10683744d2 test(traces): le traceur n'apparie une réponse qu'à la ligne de sa requête (brief 4 T6)
Contre-épreuve T4 : sans le contrôle d'identifiant du traceur, aucun test ne cassait — le
SocketScheduler écarte déjà les réponses obsolètes en amont. Ce test unitaire éprouve la
seconde garde seule (autre requête, aucune requête, réponse en double). socketscheduler :
34 fonctions, 117 / 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:24:02 +02:00
Patrick Schurig
6c44a4e091 docs(traces): INTERFACE.md (réglage, RPC, format, fichiers, plafond), protocole annexe D (brief 4 T6)
Annexe D : défaut désactivé, nom des fichiers, rotation locale, plafond, appariement
requête → réponse (la ligne N attend la réponse à la requête N), no-response, jamais le token ;
le rejeu est fait par tools/etm-powersync-trace-replay (et non plus « hems-sim »).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:12:25 +02:00
Patrick Schurig
64a864bfaa feat(tools): etm-powersync-trace-replay, rejeu des traces vers un optimiseur (brief 4 T6)
Python 3, bibliothèque standard seule. unix:// par défaut (unix:///run/powersync/optimizer.sock),
tcp:// accepté ; token lu dans un fichier (--token-file), jamais en argument ni affiché.
Réponses écrites à côté de la trace, jamais d'écrasement ; résumé : requêtes, taux
d'abstention, computeMs médian et max, cycles non comparables, cycles dont les actions du
créneau courant diffèrent du plan enregistré. --fail-on-divergence : code 1.

Test (unittest) contre un serveur Unix minimal qui renvoie les réponses enregistrées (ce n'est
pas l'optimiseur de référence) : trace synthétique et trois traces réelles du moteur → zéro
divergence ; réponse modifiée → divergence vue, code 1 ; tcp://, refus d'écraser, endpoint
absent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:12:25 +02:00
Patrick Schurig
483bbcb027 feat(traces): traces de décision NDJSON, une ligne par cycle (brief 4 T6, annexe D)
Réglage optimizerTrace (energy.conf Optimizer/trace, …RetentionDays, …MaxBytesPerDay), défaut
DÉSACTIVÉ : ni objet, ni fil, ni fichier. NymeaEnergy.GetOptimizerTrace / SetOptimizerTrace,
PermissionScopeAdmin comme SetOptimizerEndpoint, type contrôlé par le moteur, appliqué à chaud.

Une ligne par cycle — rule-based, optimiseur, et L2 (strategy none, sans requête) : t (heure
locale), request (params GetPlan envoyés, ou qui l'auraient été), response (ce qui a répondu
à CETTE requête : result brut, error.code, no-response ; sinon le plan du rule-based en codes),
executed (appliqué, clamped, shed), strategy, latencyMs. La ligne N attend la réponse à la
requête N (elle sert au cycle N+1).

Toutes les E/S sur un fil dédié (OptimizerTraceWriter) : rotation à minuit heure locale, purge
au-delà de la rétention (14 j), plafond par fichier journalier (64 Mio, mesuré : 5 Ko/ligne
rule-based, 39 Ko sous un optimiseur à 48 créneaux, 1 cycle/min), file bornée à 256 lignes.
Échec d'écriture et plafond dits une fois. update() ne fait que poster. Le token n'est jamais
dans une ligne ; recopié par une réponse d'optimiseur, il est masqué.

Preuves : decisions IDENTIQUE traces désactivées ET activées (21 132 lignes de trace écrites,
ETM_DECISIONS_TRACES=1). socketscheduler 33 fonctions, 114/0 (+10 fonctions T6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:12:25 +02:00