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
Protocol
The protocol links the box's energy engine to an optimizer, over a dedicated channel. It is not the app's API: the app goes through the JSON-RPC API.
!!! info "What is authoritative"
- The shape of messages (fields, types, allowed values, what the engine rejects) is
set by the machine-readable contract. The tables on this page are
generated from it on every publication: framing in English, descriptions taken
as-is from the contract, in French. They are identical in all three languages.
- The meaning is described in detail, in French, in
OPTIMIZER_PROTOCOL.md,
in the engine repository. This page gives the essentials; where they differ, the
engine's documents prevail.
Transport and roles
- The optimizer is the server, the engine is the client: the engine connects and asks the questions. The box opens no port.
- Two transports, same messages:
unix://(local socket, default) andtcp://(machine on the local network). - JSON-RPC 2.0, one message per line (NDJSON), UTF-8.
- If the connection drops, the engine reconnects with a growing delay and drives the loads by its rules in the meantime.
| 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
On every connection, the engine sends Handshake with its protocol version and, if
configured, the shared secret. The optimizer answers with its version and, if it wishes,
its name and capabilities. A different major version, a refused secret or an
unreadable answer: the connection is refused, and the engine drives by its rules. If the
optimizer announces the action kinds (kinds) it emits, the engine then rejects any action
of another kind.
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).
The context: GetPlan
On every cycle, and on events (vehicle plugged in, new forecast, configuration
change…), the engine sends GetPlan with a context: state of the site and of the grid
meter, production, battery, the arbitrated loads with their bounds, locks and needs, and
forecasts on a single time grid.
A few reading rules that change decisions:
- the meter is sent as a raw measurement, never as the balance after control;
- an unknown value is
null, never 0; - an unavailable load (
available: false) must receive no setpoint, but it consumes: it is still counted; - current locks (
lockMinPowerW,lockMaxPowerW) say what the load can do now; the engine enforces them regardless; - what a load measures is read from
measurement.source, never from its mechanism (adapter); a declared power is never a measurement.
The shape sent by the engine is closed: it emits no field outside the contract.
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).
The answer: a plan or an abstention
The optimizer answers quickly — with its last cached plan if it computes for long — and recomputes in the background. It answers either:
- a plan in time slots: each slot carries one action per available load (setpoint in
watts or amps, or SG-Ready state), each with its reason; only the current slot is
executed, and
validUntilbounds the plan's lifetime; - an abstention (
abstain: true): "no plan right now". The engine drives by its rules for that cycle, without treating the optimizer as failed.
An action's reason has two forms: a free text in French, mandatory, for the traces;
and, optionally, a code from the catalogue (reasonCode) with its parameters. Only the
code is passed on to the app, which translates it. An unknown code goes out as
OPTIMIZER_DECISION.
The shape received by the engine is open: an unknown field is ignored.
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
Emittable reason codes
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 sent by the optimizer to the engine, with no answer, to pass on what it has learned about the loads. In version 1.1 of the engine, it is received and logged, but not yet used.
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). |
Fallback state machine
There is no separate heartbeat: a valid answer to GetPlan means the optimizer is alive.
Failures are counted per request; after several consecutive failures, the optimizer is
declared absent and the engine drives by its rules until the next valid plan. The app sees
this state continuously (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 |
Validation rules
The contract's JSON Schema states the shape. The engine then judges each answer against
the context of its request, rule by rule, in the order below; the first that fails
gives its code (fallbackCode, visible in the app).
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. |
Tolerances
What the engine accepts beyond the contract. A conformant optimizer does not rely on it.
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).
Closed lists
Allowed values, regenerated from the engine's code.
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