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>
etm-powersync-ref-optimizer — optimiseur de référence
Python 3, bibliothèque standard seule, GPL-3.0 comme le dépôt. Protocole : docs/OPTIMIZER_PROTOCOL.md
1.2 (annexe E). Il répond un plan à un créneau qui reproduit le waterfall du rule-based (passe
confort : même ordre, même budget, mêmes écrêtages) en ne lisant que le contexte GetPlan — aucun
accès réseau sortant, aucun fichier hors le token. Usages : test de bout en bout du moteur, point de
comparaison d'un banc, exemple pour qui écrit son propre optimiseur.
Nom annoncé au handshake : powersync-optimizer (c'est le name que GetOptimizerStatus
publie). powersync-optimizer = exemple de référence GPL (ce dépôt) ; etm-optimizer = optimiseur ETM (propriétaire, hors de ce dépôt). Le fichier et son
répertoire gardent le préfixe des artefacts du dépôt, etm-powersync-ref-optimizer.
Ce qu'il ne reproduit pas (le contexte 1.1 ne le porte pas) : passe éco, dérogations, plafond de
soutirage, paliers d'un relay-router (il envoie une consigne continue que le routeur arrondit),
refus de commande, proxy VE. L'en-tête du script le détaille.
Lancer
# socket local (défaut : unix:///run/powersync/optimizer.sock, mode 0660)
./tools/etm-powersync-ref-optimizer/etm-powersync-ref-optimizer
# sur le LAN, avec un token (lu dans un FICHIER, jamais en argument)
./tools/etm-powersync-ref-optimizer/etm-powersync-ref-optimizer --listen tcp://0.0.0.0:7600 \
--token-file /etc/powersync/optimizer.token
Il écrit ÉCOUTE <endpoint> sur la sortie standard quand il écoute (le port réel de tcp://…:0),
et son journal sur la sortie d'erreur. Plusieurs clients à la fois (un fil par connexion, aucun état
partagé) : le moteur et un rejeu peuvent lui parler ensemble. SIGTERM l'arrête proprement.
Erreurs (protocole 1.2)
Codes JSON-RPC 2.0, plus -32001 (protocole §7) :
| Reçu | Réponse | Connexion |
|---|---|---|
Handshake sans le token attendu, ou avec un autre |
-32001 |
fermée |
Handshake d'une autre majeure |
-32602 |
fermée |
GetPlan avant un handshake réussi |
-32600 |
gardée |
GetPlan dont le timestamp est absent, illisible ou sans décalage |
-32602, error.data.problems = [{path, problem}], jamais la valeur |
gardée |
| ligne non JSON | -32700, id: null |
gardée |
lot […] |
-32600, id: null, aucun message du lot traité |
gardée |
ligne de plus de 1 Mio (\n non compté) |
-32600, id: null, jetée sans être gardée en mémoire |
gardée |
requête d'une autre méthode, dont UpdateLearned avec id |
-32601 |
gardée |
Sans --token-file, un token reçu au handshake est accepté sans être lu. Une notification (sans
id) est ignorée.
Pointer le moteur dessus
# méthodes NymeaEnergy (permission Admin) — INTERFACE.md, « Optimiseur »
NymeaEnergy.SetOptimizerEndpoint {"endpoint": "unix:///run/powersync/optimizer.sock"}
NymeaEnergy.SetOptimizerToken {"token": "<le même secret que dans --token-file>"} # clé seule
NymeaEnergy.GetOptimizerStatus # optimizerAlive, activeStrategy, name, lastErrorCode…
Sur tcp:// sans TLS, le token circule en clair sur le LAN (protocole §3).
Traces, puis rejeu
NymeaEnergy.SetOptimizerTrace {"enabled": true} # défaut : désactivé
NymeaEnergy.GetOptimizerTrace # → directory
# une ligne par cycle : <directory>/etm-powersync-optimizer-trace-AAAA-MM-JJ.ndjson
# rejouer une journée sur un optimiseur (celui-ci, ou un candidat)
./tools/etm-powersync-trace-replay <directory>/etm-powersync-optimizer-trace-2026-10-17.ndjson \
--endpoint unix:///run/powersync/optimizer.sock --fail-on-divergence
Le rejeu compare, cycle par cycle, les actions du créneau courant au plan enregistré (tools/README.md).
Modes d'injection de défauts (--mode, tests)
normal · abstain · slow (répond après --slow-s, défaut 12 s, au-delà des 10 s du moteur) ·
invalid (slots n'est pas une liste) · expired (validUntil passé) · wrong-major (handshake
2.0) · drop (ferme la connexion à chaque GetPlan) · unknown-load · forbidden-kind (kind
constraint) · missing-load (une charge disponible absente du créneau courant) · echo-token
(recopie le token dans reason et planId) · protocol-1.0 (optimiseur 1.0 : sans champs 1.1) ·
deviate (cascade en ordre inverse : doit diverger) · wrong-type (planId numérique : le moteur
l'accepte, le contrat OpenRPC le refuse — contre-épreuve de la suite de conformité).
--mode-file FICHIER : le mode est relu dans ce fichier à chaque message — un test change de mode
sans reconnexion. --capture FICHIER : chaque message reçu et envoyé, une ligne JSON-RPC par
message — la session que tools/etm-powersync-schema-check --ndjson juge contre le contrat ; le
token y devient autant d'astérisques (forme gardée, valeur jamais écrite). --list-codes : le catalogue des codes émissibles qu'il embarque (le test
d'intégration le confronte au moteur).
Tests : tests/auto/refoptimizer (suite etm-powersync-refoptimizer, moteur réel + ce script en
sous-processus).