Le miroir GitHub n'est pas encore synchronisé : les liens pointent sur git.etm-powersync.fr. INTERFACE.md n'est plus dans le dépôt public : la forme des méthodes NymeaEnergy.* se lit sur la box (JSONRPC.Introspect) ; GetOptimizerStatus n'est pas réservée à l'administrateur. Orthographe anglaise unifiée : optimizer, optimization. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ry3vBrJT1tHdxAaXdCWi6L
43 KiB
Protocole
Le protocole relie le moteur d'énergie de la box à un optimiseur, sur un canal dédié. Ce n'est pas l'API de l'application : celle-ci passe par l'API JSON-RPC.
!!! info "Ce qui fait foi"
- La forme des messages (champs, types, valeurs permises, ce que le moteur refuse)
est fixée par le contrat machine. Les tableaux de cette page en sont
générés à chaque publication : cadre en anglais, descriptions reprises telles
quelles du contrat, en français. Ils sont identiques dans les trois langues.
- Le sens est décrit en détail, en français, dans
OPTIMIZER_PROTOCOL.md,
dans le dépôt du moteur. Cette page en donne l'essentiel ; en cas d'écart, les
documents du moteur l'emportent.
Transport et rôles
- L'optimiseur est le serveur, le moteur est le client : le moteur se connecte et pose les questions. La box n'ouvre aucun port.
- Deux transports, mêmes messages :
unix://(socket local, par défaut) ettcp://(machine du réseau local). - JSON-RPC 2.0, un message par ligne (NDJSON), UTF-8.
- Si la connexion tombe, le moteur se reconnecte avec un délai croissant et pilote par ses règles en attendant.
| Property | Value (contract) |
|---|---|
| Protocol version | 1.1 |
| Application protocol | JSON-RPC 2.0 |
| Framing | ndjson (one message per line) |
| Encoding | utf-8 |
| Endpoint schemes | unix:// · tcp:// |
| Default endpoint | unix:///run/powersync/optimizer.sock |
| Client | engine |
| Server | optimizer |
| Max line size | 1048576 bytes |
| Handshake timeout | 5 s |
| Request timeout | 10 s |
| Reconnect backoff | 1 s → 60 s, ×2 |
| Requests in flight | 1 |
| Request ids | entiers croissants sur toute la vie du client, à travers les reconnexions |
| Line errors | ligne non JSON, sans id, id jamais émis, ligne > maxLineBytes : comptée, connexion CONSERVÉE |
| Token masking | toute occurrence du token dans un message reçu est remplacée par « [token masqué] » dès l'analyse |
Servers
| Name | URL | Summary (contract) | Example values |
|---|---|---|---|
unix |
unix:///run/powersync/optimizer.sock |
Défaut : optimiseur local, socket possédé par root:powersync, mode 0660. | |
tcp |
tcp://{host}:{port} |
Optimiseur sur le LAN. Token recommandé ; il circule en clair sans TLS (E16). | host = 192.168.1.10, port = 7400 |
Methods
| Method | Direction | Kind | Timeout | Summary (contract) |
|---|---|---|---|---|
Handshake |
engine→optimizer |
request | 5 s | Ouverture de session, à chaque (re)connexion (§3). |
GetPlan |
engine→optimizer |
request | 10 s | Demande d'un plan pour le contexte courant (§4-§6). |
UpdateLearned |
optimizer→engine |
notification (no id) | — | Estimations apprises de l'optimiseur (§8) — notification, sans id. |
Handshake
À chaque connexion, le moteur envoie Handshake avec sa version du protocole et, s'il est
configuré, le secret partagé. L'optimiseur répond sa version et, s'il le souhaite, son nom
et ses capacités. Une majeure différente, un secret refusé ou une réponse illisible :
la connexion est refusée, et le moteur pilote par ses règles. Si l'optimiseur annonce les
types d'action (kinds) qu'il émet, le moteur refuse ensuite toute action d'un autre type.
Handshake — params
Ouverture de session, à chaque (re)connexion (§3).
Envoyé par le moteur (client) dès la connexion. Délai : 5 s depuis le DÉBUT de la tentative ; au-delà, repli et nouvelle tentative au backoff. Le message porte le secret : il n'est jamais journalisé.
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
protocolVersion |
ProtocolVersion |
yes | Version du moteur ; seule la majeure est comparée. | |
engine |
string | yes | pattern ^etm-powersync-energy-plugin-etm/ |
Moteur et sa version. |
token |
HandshakeToken |
no |
result (handshake): HandshakeResult
Errors
| Code | Message | Note (contract) |
|---|---|---|
-32001 |
token refusé | Recommandé (optimiseur de référence). Toute réponse error est traitée pareil : handshake-refused, connexion fermée. |
HandshakeToken
Secret partagé (§3, E-T2-8) : 16 caractères au moins, 4 096 octets au plus (borne en octets non exprimable ici : contrôlée par le moteur), imprimable, sans blanc ni caractère de contrôle, UTF-8 admis. Envoyé seulement s'il est configuré ; jamais journalisé.
Type: string — ≥ 16 char. ; pattern ^[^\s\x00-\x1f\x7f]+$
HandshakeResult
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
protocolVersion |
string | yes | ≥ 1 char. ; pattern ^[0-9]+(\.[0-9]+)*$ |
Version semver de l'optimiseur ; même MAJEURE que le moteur exigée (règle major-mismatch). |
name |
string | no | Nom affiché (mode installateur), non contrôlé par le moteur (E-T7-3). | |
planning |
boolean | no | Vrai si l'optimiseur produit des plans multi-créneaux. Informatif. | |
capabilities |
Capabilities |
no |
Undeclared fields: ignored (optimizer-emitted form, open).
Capabilities
Capacités annoncées (§3, 1.1, optionnel). Affichées en mode installateur, réduites aux clés et valeurs connues (capacitesPubliees) ; un champ inconnu est ignoré (§9).
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
maxHorizonH |
number | no | ≥ 0 | Horizon maximal planifié, en heures. |
resolutionS |
number | no | ≥ 0 | Pas des créneaux, en secondes. |
kinds |
[ActionKind] |
no | PRÉSENT, il restreint les kinds acceptés (règle kind-not-announced). | |
usesForecast |
[string] | no | pv · baseload · price · temperature |
|
reasonCodes |
boolean | no | L'optimiseur émet des reasonCode du catalogue. |
Undeclared fields: ignored (optimizer-emitted form, open).
Le contexte : GetPlan
À chaque cycle, et sur événement (branchement d'un véhicule, nouvelle prévision,
modification de la configuration…), le moteur envoie GetPlan avec un contexte :
état du site et du compteur de tête, production, batterie, charges arbitrées avec leurs
bornes, verrous et besoins, prévisions sur une grille temporelle unique.
Quelques règles de lecture, qui changent les décisions :
- le compteur est transmis en mesure brute, jamais le bilan après pilotage ;
- une valeur inconnue vaut
null, jamais 0 ; - une charge indisponible (
available: false) ne doit recevoir aucune consigne, mais elle consomme : elle reste comptée ; - les verrous en cours (
lockMinPowerW,lockMaxPowerW) disent ce que la charge peut faire maintenant ; le moteur les applique quoi qu'il arrive ; - ce que mesure une charge se lit dans
measurement.source, jamais dans son mécanisme (adapter) ; une puissance déclarée n'est jamais une mesure.
La forme envoyée par le moteur est fermée : il n'émet aucun champ hors du contrat.
GetPlan — params
Demande d'un plan pour le contexte courant (§4-§6).
À chaque cycle (~1/min) et sur événement (trigger). Une requête en vol au plus : une plus récente remplace l'attente de la précédente (sa réponse est ignorée par id, ce n'est pas un échec). Délai : 10 s par requête. La réponse à la requête N sert au cycle N+1 (modèle cache). Le JSON Schema dit la FORME ; x-etm-semantic-rules dit ce que le validateur refuse en plus.
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
timestamp |
string | yes | instant with offset | Instant du cycle — le « maintenant » du validateur. |
trigger |
Trigger |
yes | ||
site |
Site |
yes | ||
meter |
Meter |
yes | ||
pv |
Pv |
yes | ||
battery |
Battery |
yes | ||
loads |
[Load] |
yes | Charges arbitrées, ⊆ GetLoadConfig. | |
forecast |
Forecast | null |
yes | null sans instantané de prévision. | |
tariff |
Tariff | null |
yes |
Errors
| Code | Message | Note (contract) |
|---|---|---|
-32603 |
erreur interne | Toute réponse error compte comme un échec de la requête (rpc-error, §7) ; le code n'est pas lu. |
Site
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
contractedPowerW |
number | null | yes | Puissance souscrite (W) ; null si inconnue (E1), jamais 0. | |
gridCapW |
null | yes | RÉSERVÉ (signaux réseau type §14a EnWG) : toujours null en 1.1. | |
phaseLimitA |
[number] | null | yes | Limite par phase en A ; null si inconnue. |
Undeclared fields: rejected (engine-emitted form, closed).
Meter
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
source |
string | null | yes | ThingId du compteur racine désigné ; null si inconnu. | |
importW |
number | yes | ≥ 0 | Puissance importée (W). |
exportW |
number | yes | ≥ 0 | Puissance exportée (W), mesure BRUTE (AGENTS.md règle 8) — jamais le net après pilotage. |
perPhaseA |
[number] | yes | Courant par phase (A). |
Undeclared fields: rejected (engine-emitted form, closed).
Pv
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
currentW |
number | null | yes | ≥ 0 | Production instantanée (W), positive ; null sans producteur (jamais 0). |
Undeclared fields: rejected (engine-emitted form, closed).
Battery
One of: BatteryAbsent · BatteryPresent.
Load
Une charge arbitrée (§5.3) — la forme dépend du mécanisme (adapter).
One of: LoadWatts · LoadEvCharger · LoadSgReady.
Forecast
Prévisions sur UNE grille (§5.2) ; tableaux alignés index par index.
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
start |
string | yes | instant with offset | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
resolutionS |
integer | yes | ≥ 1 | Pas de la grille (s) — fait foi, jamais supposé. |
steps |
integer | yes | ≥ 0 | Nombre de cases ; toutes les séries ont cette longueur. |
pv |
PvForecast | null |
yes | null sans courbe (site non désigné, charge utile refusée, canal muet). | |
baseload |
Baseload |
yes | ||
weather |
null | yes | Aucune source de température en 1.1 : toujours null. | |
price |
Price | null |
yes | null sans tarif saisi. |
Undeclared fields: rejected (engine-emitted form, closed).
Tariff
Tarif 1.0, conservé pour compatibilité ; un optimiseur 1.1 lit forecast.price. Aucun écrivain aujourd'hui (E3) : null en pratique.
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
provider |
string | null | yes | ||
current |
TariffEntry |
yes | ||
next |
[TariffNextEntry] |
yes |
Undeclared fields: rejected (engine-emitted form, closed).
BatteryAbsent
Aucun stockage déclaré : rien d'autre n'est émis.
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
present |
boolean | yes | false |
Undeclared fields: rejected (engine-emitted form, closed).
BatteryPresent
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
present |
boolean | yes | true |
|
socPercent |
number | yes | ||
powerW |
number | yes | Positif = charge, négatif = décharge. | |
capacityWh |
number | null | yes | null si inconnue. | |
maxChargeW |
number | null | yes | null si inconnue. | |
maxDischargeW |
number | null | yes | null si inconnue. | |
reserveSocPercent |
integer | null | yes | Réserve de secours (%) ; null si inconnue. |
Undeclared fields: rejected (engine-emitted form, closed).
LoadWatts
Charge pilotée en watts : combinaison de relais (relay-router) ou consigne continue (etmvariableload).
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
id |
string | yes | ≥ 1 char. | Identifiant de la charge — clé des actions. |
label |
string | yes | Nom lisible. | |
priority |
integer | yes | Rang effectif (1 = premier servi). | |
domain |
Domain |
no | ||
learned |
LoadLearned |
no | ||
needs |
LoadNeeds |
no | ||
adapter |
string | yes | relay-router · etmvariableload |
|
declared |
object | yes | ||
declared.minPowerW |
number | yes | 0 = aucun plancher. | |
declared.maxPowerW |
number | yes | ||
telemetry |
object | yes | ||
telemetry.currentPowerW |
number | yes | Puissance courante (W) telle que l'adaptateur la publie. | |
telemetry.available |
boolean | yes | Gouverne l'ALLOCATION, pas la COMPTABILITÉ (§5.3). | |
telemetry.measurement |
Measurement |
no | ||
telemetry.lockMinPowerW |
number | yes | Plancher imposé MAINTENANT par un verrou. | |
telemetry.lockMaxPowerW |
number | yes | Plafond imposé maintenant ; négatif = aucun plafond. | |
limits |
object | yes | ||
limits.minOnS |
integer | yes | ||
limits.minOffS |
integer | yes |
Undeclared fields: rejected (engine-emitted form, closed).
LoadEvCharger
Borne. Une borne CONFIGURÉE dont l'appareil manque n'a ni declared, ni limits, ni pluggedIn/charging : ses zéros diraient « inconnu ».
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
id |
string | yes | ≥ 1 char. | Identifiant de la charge — clé des actions. |
label |
string | yes | Nom lisible. | |
priority |
integer | yes | Rang effectif (1 = premier servi). | |
domain |
Domain |
no | ||
learned |
LoadLearned |
no | ||
needs |
LoadNeeds |
no | ||
adapter |
string | yes | "evcharger" |
|
declared |
object | no | ||
declared.minA |
number | yes | ||
declared.maxA |
number | yes | ||
declared.phases |
integer | yes | Phases VISÉES ce cycle, pas celles de la plaque (E26). | |
telemetry |
object | yes | ||
telemetry.currentPowerW |
number | yes | Puissance courante (W) telle que l'adaptateur la publie. | |
telemetry.available |
boolean | yes | Gouverne l'ALLOCATION, pas la COMPTABILITÉ (§5.3). | |
telemetry.measurement |
Measurement |
no | ||
telemetry.pluggedIn |
boolean | no | ||
telemetry.charging |
boolean | no | ||
telemetry.sessionWh |
number | no | Absent si la borne ne compte pas sa session (LM-1009 C1). | |
limits |
object | no | ||
limits.chargingEnabledLockS |
integer | yes | ||
limits.currentChangeLockS |
integer | yes |
Undeclared fields: rejected (engine-emitted form, closed).
LoadSgReady
PAC SG-Ready : 4 états normés, kind state.
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
id |
string | yes | ≥ 1 char. | Identifiant de la charge — clé des actions. |
label |
string | yes | Nom lisible. | |
priority |
integer | yes | Rang effectif (1 = premier servi). | |
domain |
Domain |
no | ||
learned |
LoadLearned |
no | ||
needs |
LoadNeeds |
no | ||
adapter |
string | yes | "sg-ready" |
|
declared |
object | yes | ||
declared.states |
[integer] | yes | ≥ 1 ; ≤ 4 | |
declared.estimatedPowerW |
map of number | yes | ||
telemetry |
object | yes | ||
telemetry.currentPowerW |
number | yes | Puissance courante (W) telle que l'adaptateur la publie. | |
telemetry.available |
boolean | yes | Gouverne l'ALLOCATION, pas la COMPTABILITÉ (§5.3). | |
telemetry.measurement |
Measurement |
no | ||
telemetry.state |
integer | yes | ≥ 0 ; ≤ 4 | État commandé (1-4) ; 0 = non applicable. |
telemetry.minState |
integer | yes | Fenêtre d'états autorisée MAINTENANT par minStateHold. | |
telemetry.maxState |
integer | yes | ||
limits |
object | yes | ||
limits.minStateHoldS |
integer | yes |
Undeclared fields: rejected (engine-emitted form, closed).
PvForecast
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
p50W |
[number | null] | yes | Puissance AC moyenne sur [t, t+pas), sommée sur les plans puis plafonnée à acLimitW ; null = case inconnue. Un plafond, pas une espérance (§5.2). | |
conversion |
string | yes | physics |
Seule valeur émise par le moteur GPL ; un consommateur ignore une valeur inconnue. |
corrections |
[Correction] |
yes | [] = conversion brute. | |
weatherModel |
string | null | yes | Modèle qui a RÉPONDU ; null si inconnu. | |
issuedAt |
string | null | yes | instant with offset | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
contentChangedAt |
string | null | yes | instant with offset | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
acLimitW |
integer | no | Plafond AC du site (W), s'il est configuré. | |
perField |
[PvField] |
no |
Undeclared fields: rejected (engine-emitted form, closed).
Baseload
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
model |
string | yes | profile · none |
Le moteur GPL émet profile ou none (learned n'est pas émis). |
p50W |
[number | null] | yes | ||
quality |
BaseloadQuality |
no |
Undeclared fields: rejected (engine-emitted form, closed).
Price
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
currency |
string | yes | "EUR" |
|
importCtkWh |
[number | null] | yes | ||
exportCtkWh |
[number | null] | yes | ||
includes |
[PriceInclude] |
yes | ||
source |
string | yes | fixed · tou |
Tarif SAISI : une zone (fixed) ou des zones horaires (tou). tempo et day-ahead ne sont pas émis en 1.1. |
knownUntil |
string | yes | instant with offset | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
Undeclared fields: rejected (engine-emitted form, closed).
TariffEntry
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
label |
string | yes | ||
priceCtkWh |
number | yes |
Undeclared fields: rejected (engine-emitted form, closed).
TariffNextEntry
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
label |
string | yes | ||
priceCtkWh |
number | yes | ||
from |
string | null | yes | instant with offset | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
Undeclared fields: rejected (engine-emitted form, closed).
LoadLearned
Absent tant qu'aucun profil n'a été reçu (E12).
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
dailyEnergyWh |
number | yes | ||
confidence |
number | yes | ≥ 0 ; ≤ 1 |
Undeclared fields: rejected (engine-emitted form, closed).
LoadNeeds
Besoins (§5.3). Champs 1.0 émis tels quels quand ils sont déclarés ; un optimiseur 1.1 préfère energyTargets.
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
dailyDeadline |
string | no | pattern ^[0-9]{2}:[0-9]{2}$ |
Heure murale locale HH:MM (1.0). |
minEnergyWhPerDay |
integer | no | > 0 | |
targetSocPercent |
integer | no | > 0 | |
deadline |
string | no | instant with offset | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
energyTargets |
[EnergyTarget] |
no | ≥ 1 items |
Undeclared fields: rejected (engine-emitted form, closed).
Measurement
Mesure par charge (§5.3, LM-1105/1106) : décider sur source, jamais sur adapter.
variant 1 — Rien ne mesure : état positif, pas une erreur.
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
source |
string | yes | "none" |
Undeclared fields: rejected (engine-emitted form, closed).
variant 2
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
source |
string | yes | meter · device |
|
powerW |
number | yes |
Undeclared fields: rejected (engine-emitted form, closed).
PvField
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
id |
string | yes | ThingId du plan. | |
corrections |
[Correction] |
yes |
Undeclared fields: rejected (engine-emitted form, closed).
BaseloadQuality
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
windowDays |
integer | yes | ||
minDays |
integer | yes | ||
resolutionS |
integer | yes | ||
daysCovered |
object | yes | ||
daysCovered.workday |
integer | yes | ||
daysCovered.weekend |
integer | yes | ||
minutes |
integer | yes | ||
commandedMinutes |
integer | yes | ||
negativeMinutes |
integer | yes | ||
incompleteMinutes |
integer | yes | ||
computedAt |
string | no | instant with offset | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
loads |
[BaseloadLoadQuality] |
yes |
Undeclared fields: rejected (engine-emitted form, closed).
EnergyTarget
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
by |
string | yes | instant with offset | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
minWh |
number | yes | > 0 | Énergie minimale à avoir délivrée à l'échéance. |
source |
string | yes | installer · user-intent |
learned n'est pas émis en 1.1. |
Undeclared fields: rejected (engine-emitted form, closed).
BaseloadLoadQuality
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
loadId |
string | yes | ||
source |
string | yes | meter · device · none |
|
minutesBySource |
map of integer | yes | keys meter · device · none |
|
constantMeasurement |
boolean | no |
Undeclared fields: rejected (engine-emitted form, closed).
La réponse : un plan ou une abstention
L'optimiseur répond vite — avec son dernier plan en cache s'il calcule longtemps — et recalcule en tâche de fond. Il répond soit :
- un plan par créneaux : chaque créneau porte une action par charge disponible
(consigne en watts ou en ampères, ou état SG-Ready), chacune avec son motif ; seul le
créneau en cours est exécuté, et
validUntilborne la durée de vie du plan ; - une abstention (
abstain: true) : « pas de plan maintenant ». Le moteur pilote par ses règles pour ce cycle, sans considérer l'optimiseur comme en panne.
Le motif d'une action a deux formes : un texte libre en français, obligatoire, pour
les traces ; et, facultatif, un code du catalogue (reasonCode) avec ses paramètres.
Vers l'application, seul le code est transmis, et l'application le traduit. Un code
inconnu part sous OPTIMIZER_DECISION.
La forme reçue par le moteur est ouverte : un champ inconnu est ignoré.
GetPlan — result
plan: GetPlanResult
Errors
| Code | Message | Note (contract) |
|---|---|---|
-32603 |
erreur interne | Toute réponse error compte comme un échec de la requête (rpc-error, §7) ; le code n'est pas lu. |
GetPlanResult
One of: Abstention · Plan.
Abstention
« Pas de plan maintenant » (§7) : repli pour ce cycle, pas un échec.
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
abstain |
boolean | yes | true |
|
reason |
string | no |
Undeclared fields: ignored (optimizer-emitted form, open).
Plan
Plan par créneaux (§6).
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
abstain |
boolean | no | false |
|
planId |
string | no | ||
strategy |
string | no | Chaîne libre, informative. | |
computedAt |
string | no | instant with offset | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
validUntil |
string | null | no | instant with offset | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
slots |
[Slot] |
yes | [] = abstention (empty-plan). Seul le créneau couvrant maintenant est exécuté. | |
diagnostics |
Diagnostics |
no |
Undeclared fields: ignored (optimizer-emitted form, open).
Slot
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
from |
string | yes | instant with offset | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
to |
string | yes | instant with offset | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
actions |
[Action] | null |
no | Absent ou null = aucune action. | |
expected |
Expected |
no |
Undeclared fields: ignored (optimizer-emitted form, open).
Diagnostics
Informatif : mode installateur et traces.
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
solver |
string | no | ||
status |
string | no | optimal · feasible · best-so-far · infeasible-relaxed |
|
gapPercent |
number | no | ||
computeMs |
number | no | ≥ 0 | |
inputsUsed |
object | no | ||
inputsUsed.pv |
string | no | physics · hybrid · none |
Undeclared fields: ignored (optimizer-emitted form, open).
Action
One of: SetpointAction · StateAction · ConstraintAction.
Expected
Trajectoire prévue : affichage et mesure d'écart, JAMAIS pour commander.
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
gridImportW |
number | no | ||
gridExportW |
number | no | ||
batterySocPercent |
any | no | RÉSERVÉ (annexe F) : ignoré. |
Undeclared fields: ignored (optimizer-emitted form, open).
SetpointAction
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
loadId |
string | yes | ≥ 1 char. | Charge visée : connue du contexte (règle unknown-load), une fois par créneau (duplicate-load). |
reason |
string | yes | ≥ 1 char. ; pattern \S |
Texte français, OBLIGATOIRE et non vide (règle missing-reason). Traces et journal seulement, jamais vers l'app. |
reasonCode |
ReasonCode |
no | ||
reasonParams |
ReasonParams |
no | ||
force |
any | no | INTERDIT sur le fil (§6) : ignoré, signalé une fois. | |
kind |
string | yes | "setpoint" |
|
powerW |
number | no | ≥ 0 | Consigne en watts (relay-router, etmvariableload ; ou borne). |
currentA |
number | no | ≥ 0 | Borne : courant (0 = suspendre). |
phases |
enum | no | 1 · 3 |
Borne : phases visées ; absent = inchangé. |
Undeclared fields: ignored (optimizer-emitted form, open).
At least one of: powerW, currentA.
StateAction
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
loadId |
string | yes | ≥ 1 char. | Charge visée : connue du contexte (règle unknown-load), une fois par créneau (duplicate-load). |
reason |
string | yes | ≥ 1 char. ; pattern \S |
Texte français, OBLIGATOIRE et non vide (règle missing-reason). Traces et journal seulement, jamais vers l'app. |
reasonCode |
ReasonCode |
no | ||
reasonParams |
ReasonParams |
no | ||
force |
any | no | INTERDIT sur le fil (§6) : ignoré, signalé une fois. | |
kind |
string | yes | "state" |
|
state |
integer | yes | ≥ 1 ; ≤ 4 | 1 = verrouillage, 2 = normal, 3 = recommandation, 4 = forcé. |
Undeclared fields: ignored (optimizer-emitted form, open).
ConstraintAction
RÉSERVÉ (annexe F) : forme décrite, toujours refusée (kind-mismatch).
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
loadId |
string | yes | ≥ 1 char. | Charge visée : connue du contexte (règle unknown-load), une fois par créneau (duplicate-load). |
reason |
string | yes | ≥ 1 char. ; pattern \S |
Texte français, OBLIGATOIRE et non vide (règle missing-reason). Traces et journal seulement, jamais vers l'app. |
reasonCode |
ReasonCode |
no | ||
reasonParams |
ReasonParams |
no | ||
force |
any | no | INTERDIT sur le fil (§6) : ignoré, signalé une fois. | |
kind |
string | yes | "constraint" |
|
charge |
enum | no | allow · forbid |
|
discharge |
enum | no | allow · forbid |
Undeclared fields: ignored (optimizer-emitted form, open).
ReasonParams
Paramètres du motif : valeurs nombre, booléen ou code [A-Za-z]+ ; clés admises par code : x-etm-reason-params. Toute autre clé est SUPPRIMÉE avant l'app (filtrée, pas refusée).
Type: map of number | boolean | string | null
Codes de motif émissibles
reasonCode |
Accepted reasonParams keys |
|---|---|
SURPLUS_SETPOINT |
budgetW, setpointW, stepped |
SURPLUS_INSUFFICIENT |
budgetW |
BELOW_MIN_POWER |
budgetW, minPowerW, state |
EV_DEADLINE |
— |
SG_FORCED |
budgetW, estimatedW |
SG_RECOMMENDED |
budgetW, estimatedW |
SG_NORMAL |
budgetW |
ECO_FLOOR_GRID |
gridW, atMaximum, remainingWh, targetWh, remainingS, regime, minAlwaysW, boundBy |
PRICE_SLOT |
priceCtkWh, setpointW |
OPTIMIZER_DECISION |
— |
UpdateLearned
Notification envoyée par l'optimiseur au moteur, sans réponse, pour transmettre ce qu'il a appris des charges. Dans la version 1.1 du moteur, elle est reçue et journalisée, mais pas encore exploitée.
UpdateLearned — params
Estimations apprises de l'optimiseur (§8) — notification, sans id.
Envoyée par l'OPTIMISEUR (sens inverse des deux autres), sans id, sans réponse. État 1.1 du moteur : reçue, journalisée une fois, IGNORÉE — rien n'est persisté, le contenu n'est pas lu. Forme non normée en 1.1.
| Field | Type | Required | Values | Description (contract, French) |
|---|---|---|---|---|
loads |
[object] | no | Non normé en 1.1 : intention §8 (learned + confidence par charge). |
Machine de repli
Il n'y a pas de battement de cœur séparé : une réponse valide à GetPlan signifie que
l'optimiseur est vivant. Les échecs se comptent par requête ; après plusieurs échecs
consécutifs, l'optimiseur est déclaré absent et le moteur pilote par ses règles jusqu'au
prochain plan valide. L'application voit cet état en continu (optimizerAlive,
activeStrategy).
Machine de repli §7 (OptimizerStatus) : heartbeat implicite — une réponse valide à GetPlan = optimiseur vivant. Les échecs se comptent PAR REQUÊTE.
Consecutive failed requests before optimizerAlive = false: 2.
| Situation (contract) | optimizerAlive |
activeStrategy |
|---|---|---|
| réponse plan valide | true |
optimizer |
| abstain, plan vide, aucun créneau courant | inchangé |
rules (ce cycle) |
| plan expiré (validUntil dépassé) | inchangé |
rules jusqu'au prochain plan valide |
| timeout / erreur / plan invalide sur deux requêtes consécutives | false |
rules |
| socket absent / fermé / handshake refusé | false |
rules |
| aucun endpoint configuré | false |
rules |
| mode dégradé L2 (compteur muet > 90 s) | inchangé |
none |
Règles de validation
Le JSON Schema du contrat dit la forme. Le moteur juge ensuite chaque réponse sur le
contexte de sa requête, règle par règle, dans l'ordre ci-dessous ; la première qui
échoue donne son code (fallbackCode, visible dans l'application).
Ce que le moteur refuse EN PLUS de la forme, dans l'ordre de ses contrôles (OptimizerPlan::valider, OptimizerClient::handleHandshakeReply). name = code du verdict. issue : abstention (repli ce cycle, pas un échec), expired, invalid (plan refusé en entier, compté comme échec), connection-refused (handshake). needs : ce qu'il faut connaître en plus de la réponse (la requête, le handshake).
| # | Rule | Method | Outcome | fallbackCode |
lastErrorCode |
Counts as failure | Also needs | Schema catches it too | What is rejected (contract) |
|---|---|---|---|---|---|---|---|---|---|
| 1 | abstain |
GetPlan |
abstention |
abstain |
— | no | — | no | result.abstain vaut true. |
| 2 | malformed-plan |
GetPlan |
invalid |
malformed-plan |
malformed-plan |
yes | — | yes | slots absent ou non liste ; un créneau qui n'est pas un objet ; actions non liste. |
| 3 | empty-plan |
GetPlan |
abstention |
empty-plan |
— | no | — | no | slots est une liste vide. |
| 4 | bad-time |
GetPlan |
invalid |
bad-time |
bad-time |
yes | — | yes | from/to illisibles ou sans décalage, ou from ≥ to ; validUntil illisible ou sans décalage. |
| 5 | slots-unordered |
GetPlan |
invalid |
slots-unordered |
slots-unordered |
yes | — | no | un créneau commence avant la fin du précédent. |
| 6 | malformed-action |
GetPlan |
invalid |
malformed-action |
malformed-action |
yes | — | yes | action qui n'est pas un objet, ou sans loadId (chaîne non vide). |
| 7 | unknown-load |
GetPlan |
invalid |
unknown-load |
unknown-load |
yes | request |
no | loadId absent des loads[] de la requête. |
| 8 | unknown-kind |
GetPlan |
invalid |
unknown-kind |
unknown-kind |
yes | — | yes | kind hors ActionKind. |
| 9 | kind-mismatch |
GetPlan |
invalid |
kind-mismatch |
kind-mismatch |
yes | request |
no | kind différent de celui du mécanisme de la charge (setpoint : evcharger, relay-router, etmvariableload ; state : sg-ready ; constraint : aucun). |
| 10 | kind-not-announced |
GetPlan |
invalid |
kind-not-announced |
kind-not-announced |
yes | handshake |
no | capabilities.kinds annoncé au handshake et kind absent de la liste. |
| 11 | missing-reason |
GetPlan |
invalid |
missing-reason |
missing-reason |
yes | — | yes | reason absent, non textuel, ou vide une fois les caractères de contrôle retirés. |
| 12 | bad-value |
GetPlan |
invalid |
bad-value |
bad-value |
yes | request |
yes | state absent ou hors 1-4 ; borne : currentA/powerW absents ou négatifs, phases hors {1, 3} ; charge en watts : powerW absent ou négatif. |
| 13 | duplicate-load |
GetPlan |
invalid |
duplicate-load |
duplicate-load |
yes | — | no | deux actions pour la même charge dans un créneau. |
| 14 | expired |
GetPlan |
expired |
expired |
— | no | request |
no | validUntil ≤ timestamp de la requête. |
| 15 | no-current-slot |
GetPlan |
abstention |
no-current-slot |
— | no | request |
no | aucun créneau ne couvre le timestamp de la requête. |
| 16 | incomplete-slot |
GetPlan |
invalid |
incomplete-slot |
incomplete-slot |
yes | request |
no | une charge available de la requête n'a pas d'action dans le créneau courant (décision Q1). |
| 17 | handshake-refused |
Handshake |
connection-refused |
no-plan |
handshake-refused |
no | — | no | réponse error (token absent ou différent : -32001 recommandé) → connexion fermée, nouvelle tentative au backoff. |
| 18 | invalid-handshake |
Handshake |
connection-refused |
no-plan |
invalid-handshake |
no | — | yes | result non objet, protocolVersion absent ou vide, capabilities présent et non objet. |
| 19 | major-mismatch |
Handshake |
connection-refused |
no-plan |
major-mismatch |
no | — | no | MAJEURE de protocolVersion (ce qui précède le premier point) différente de celle du moteur. |
Tolérances
Ce que le moteur accepte au-delà du contrat. Un optimiseur conforme ne s'en sert pas.
Ce que le moteur ACCEPTE au-delà du schéma — un optimiseur conforme ne s'en sert pas.
- reasonCode absent ou hors ReasonCode : action acceptée, motif transmis sous OPTIMIZER_DECISION sans paramètres.
- reasonParams : clé non admise pour le code (x-etm-reason-params) ou valeur refusée : clé supprimée, action acceptée.
- planId, strategy : lus comme chaînes quel que soit leur type ; computedAt illisible : ignoré.
- diagnostics, expected : non objets ignorés ; valeurs inconnues filtrées avant l'app.
- abstain : toute valeur « vraie » au sens de QVariant::toBool (true, chaîne non vide autre que « false »/« 0 », nombre non nul).
- champs inconnus (params et result) : ignorés (§9) ; force et champs réservés (annexe F) : ignorés et signalés une fois.
- state : 3.0 accepté comme 3 (entier au sens JSON Schema).
Listes fermées
Valeurs permises, régénérées depuis le code du moteur.
ProtocolVersion
Version du protocole parlée par le moteur (GEN : OptimizerProtocol::ProtocolVersion).
1.1
Regenerated from the engine code: OptimizerProtocol::ProtocolVersion.
Trigger
Cause de la requête (§5.1). GEN : SocketScheduler::knownTriggers() — du moins au plus significatif.
cycle · forecast · config · load-change · intent
Regenerated from the engine code: SocketScheduler::knownTriggers().
Domain
Intention déclarée par l'installateur, jamais déduite du mécanisme, non lue par l'arbitre (§5.3). GEN : LoadConfig::knownDomains().
ecs · ev · heating · hvac · battery
Regenerated from the engine code: LoadConfig::knownDomains().
ActionKind
Type d'action (§6). GEN : OptimizerPlan::kinds(). constraint est RÉSERVÉ (annexe F) : reconnu par le validateur, toujours refusé (règle kind-mismatch) faute de mécanisme porteur.
setpoint · state · constraint
Regenerated from the engine code: OptimizerPlan::kinds().
ReasonCode
Codes de motif qu'un optimiseur peut émettre (§6, liste blanche). GEN : DecisionCode::optimizerEmittableCodes(). Clés de paramètres admises par code : x-etm-reason-params.
SURPLUS_SETPOINT · SURPLUS_INSUFFICIENT · BELOW_MIN_POWER · EV_DEADLINE · SG_FORCED · SG_RECOMMENDED · SG_NORMAL · ECO_FLOOR_GRID · PRICE_SLOT · OPTIMIZER_DECISION
Regenerated from the engine code: DecisionCode::optimizerEmittableCodes().
PriceInclude
Composante incluse dans le prix (§5.2). GEN : TariffConfig::includes().
energy · grid · taxes
Regenerated from the engine code: TariffConfig::includes().
FallbackCode
Côté app (NymeaEnergy.GetPlan.fallbackCode, INTERFACE.md) : pourquoi l'optimiseur n'a pas commandé le cycle. Hors fil. GEN : SocketScheduler::knownFallbackCodes().
no-plan · abstain · empty-plan · no-current-slot · expired · malformed-plan · bad-time · slots-unordered · malformed-action · unknown-load · unknown-kind · kind-mismatch · kind-not-announced · missing-reason · bad-value · duplicate-load · incomplete-slot · no-shadow-slot
Regenerated from the engine code: SocketScheduler::knownFallbackCodes().
LastErrorCode
Côté app (NymeaEnergy.GetOptimizerStatus.lastErrorCode, INTERFACE.md) : dernier code d'erreur vu (connexion, requête, plan). Hors fil. GEN : SocketScheduler::knownLastErrorCodes().
invalid-endpoint · token-too-short · connect-failed · closed-during-handshake · connection-lost · handshake-timeout · handshake-refused · invalid-handshake · major-mismatch · line-too-long · invalid-json · missing-id · unknown-id · timeout · rpc-error · invalid-response · disconnected · malformed-plan · bad-time · slots-unordered · malformed-action · unknown-load · unknown-kind · kind-mismatch · kind-not-announced · missing-reason · bad-value · duplicate-load · incomplete-slot
Regenerated from the engine code: SocketScheduler::knownLastErrorCodes().
Correction
temperature · ac-limit