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>
This commit is contained in:
parent
ead662e044
commit
2f191f4bec
@ -10,6 +10,17 @@ Ce document fait autorité. Il sert trois usages :
|
||||
|
||||
Statut : **v1.1** — figé à la fin de la beta. Versionné en semver (`protocolVersion`).
|
||||
|
||||
> ### La forme est un contrat MACHINE
|
||||
>
|
||||
> La **forme** des messages — champs, types, obligatoire, valeurs permises, et ce que le
|
||||
> validateur du moteur refuse en plus — est écrite dans
|
||||
> **`docs/etm-powersync-optimizer-protocol.openrpc.json`** (OpenRPC 1.3). Il fait foi sur la forme ;
|
||||
> ce document fait foi sur le **sens**. Le moteur l'embarque et le sert
|
||||
> (`NymeaEnergy.GetOptimizerProtocol`, `INTERFACE.md`) ; les deux côtés sont testés contre lui
|
||||
> (annexe G). Les tableaux entre balises `BEGIN GENERATED` / `END GENERATED` ci-dessous sont
|
||||
> **produits depuis le contrat** par `tools/etm-powersync-gen-protocol-doc` (`--check` en CI) :
|
||||
> ne pas les éditer à la main. Les exemples JSON, eux, restent illustratifs.
|
||||
|
||||
> ### Ce document ne décrit PAS la frontière JSON-RPC de l'app
|
||||
>
|
||||
> Le protocole ci-dessous relie le **moteur** à un **optimiseur** (couche A ↔ couche B), sur
|
||||
@ -61,6 +72,10 @@ valeur avait été écrite sans définition ; **aucun émetteur ne l'a jamais pr
|
||||
interdit sur le fil.
|
||||
- Annexe D **Traces et rejeu** ; Annexe E **Optimiseur de référence** ;
|
||||
Annexe F **Champs réservés** (batterie).
|
||||
- **Contrat machine (2026-10-17, sans changement de fil)** : la forme des messages devient le
|
||||
document OpenRPC `docs/etm-powersync-optimizer-protocol.openrpc.json`, servi par le moteur,
|
||||
tableaux de ce document générés depuis lui ; annexe G. Les exemples du §5 ont été réalignés sur
|
||||
ce que le moteur émet réellement (champs jamais émis signalés comme tels).
|
||||
|
||||
### 1.0 (2026-06) — version initiale
|
||||
- 2026-08-09 (`92d0bef`) : `available`, `lockMinPowerW`, `lockMaxPowerW` documentés (ECS-410).
|
||||
@ -135,8 +150,9 @@ Réponse attendue :
|
||||
log explicite, repli rule-based.
|
||||
- `planning` : `true` si l'optimiseur produit des plans multi-créneaux ; `false` s'il
|
||||
ne répond qu'au présent (plan à un créneau). Informationnel.
|
||||
- `token` : si configuré côté plugin et absent/incorrect côté serveur → connexion fermée.
|
||||
Recommandé pour tout endpoint `tcp://`. **Forme** (E-T2-8) : chaîne de **16 caractères au
|
||||
- `token` : si configuré côté plugin et absent/incorrect côté serveur → réponse `error`
|
||||
(code `-32001` recommandé ; le moteur traite **toute** réponse `error` au handshake comme un
|
||||
refus, `handshake-refused`) puis connexion fermée. Recommandé pour tout endpoint `tcp://`. **Forme** (E-T2-8) : chaîne de **16 caractères au
|
||||
moins** et de 4 096 octets au plus, imprimable, sans blanc ni caractère de contrôle (UTF-8
|
||||
admis). Le plugin refuse d'en poser un plus court, et ne se connecte pas avec un fichier de
|
||||
token plus court (ni sans token) : il masque toute occurrence du token dans ce que l'optimiseur
|
||||
@ -145,7 +161,58 @@ Réponse attendue :
|
||||
plugin rejette toute action dont le `kind` n'y est pas annoncé.
|
||||
- Pas de `locale` : la locale nymea est par connexion app (`AGENTS.md` invariant 12) ; la
|
||||
traduction vers l'app passe par `reasonCode` (§6).
|
||||
- Handshake invalide ou timeout (5 s) → repli, nouvelle tentative au backoff.
|
||||
- Handshake invalide ou timeout (5 s) → repli, nouvelle tentative au backoff. Invalide :
|
||||
`result` non objet, `protocolVersion` absent ou vide, `capabilities` présent mais non objet
|
||||
(`invalid-handshake`) ; majeure différente (`major-mismatch`).
|
||||
- `name` : optionnel, informatif, affiché en mode installateur ; jamais contrôlé.
|
||||
|
||||
<!-- BEGIN GENERATED table:Handshake.params -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`Handshake` — params**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `protocolVersion` | `ProtocolVersion` | oui | `"1.1"` | Version du moteur ; seule la majeure est comparée. |
|
||||
| `engine` | string | oui | motif `^etm-powersync-energy-plugin-etm/` | Moteur et sa version. |
|
||||
| `token` | string | non | ≥ 16 car. | 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é. |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:HandshakeResult -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`HandshakeResult`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `protocolVersion` | string | oui | ≥ 1 car. ; motif `^[0-9]+(\.[0-9]+)*$` | Version semver de l'optimiseur ; même MAJEURE que le moteur exigée (règle major-mismatch). |
|
||||
| `name` | string | non | | Nom affiché (mode installateur), non contrôlé par le moteur (E-T7-3). |
|
||||
| `planning` | boolean | non | | Vrai si l'optimiseur produit des plans multi-créneaux. Informatif. |
|
||||
| `capabilities` | `Capabilities` | non | | |
|
||||
|
||||
Champs non déclarés : ignorés (§9).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:Capabilities -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`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).
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `maxHorizonH` | number | non | ≥ 0 | Horizon maximal planifié, en heures. |
|
||||
| `resolutionS` | number | non | ≥ 0 | Pas des créneaux, en secondes. |
|
||||
| `kinds` | [string] | non | `setpoint` · `state` · `constraint` | PRÉSENT, il restreint les kinds acceptés (règle kind-not-announced). |
|
||||
| `usesForecast` | [string] | non | `pv` · `baseload` · `price` · `temperature` | |
|
||||
| `reasonCodes` | boolean | non | | L'optimiseur émet des reasonCode du catalogue. |
|
||||
|
||||
Champs non déclarés : ignorés (§9).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
## 4. Cycle et heartbeat
|
||||
|
||||
@ -185,7 +252,7 @@ Réponse attendue :
|
||||
"forecast":{ "... §5.2 ... ou null" },
|
||||
"tariff":{
|
||||
"provider":"tempo","current":{"label":"HP_BLEU","priceCtkWh":16.1},
|
||||
"next":[{"from":"2026-06-08T06:00","label":"ROUGE","priceCtkWh":75.6}]
|
||||
"next":[{"from":"2026-06-08T06:00:00+02:00","label":"ROUGE","priceCtkWh":75.6}]
|
||||
}
|
||||
}}
|
||||
```
|
||||
@ -204,7 +271,144 @@ Réponse attendue :
|
||||
source PV** : nymea additionne tous les producteurs, et un onduleur communicant doublé d'un
|
||||
compteur de production sur la même chaîne compterait la même énergie deux fois.
|
||||
- `site.gridCapW` : réservé (signaux réseau type §14a EnWG). `null` en France.
|
||||
- `tariff` (1.0) reste émis pour compatibilité ; un optimiseur 1.1 lit `forecast.price`.
|
||||
- `tariff` (1.0) reste émis pour compatibilité ; un optimiseur 1.1 lit `forecast.price`. Aucun
|
||||
écrivain aujourd'hui (E3) : le moteur 1.1 émet `tariff: null`.
|
||||
|
||||
<!-- BEGIN GENERATED table:GetPlan.params -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`GetPlan` — params**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `timestamp` | string | oui | instant avec décalage | Instant du cycle — le « maintenant » du validateur. |
|
||||
| `trigger` | string | oui | `cycle` · `forecast` · `config` · `load-change` · `intent` | Cause de la requête (§5.1). GEN : SocketScheduler::knownTriggers() — du moins au plus significatif. |
|
||||
| `site` | `Site` | oui | | |
|
||||
| `meter` | `Meter` | oui | | |
|
||||
| `pv` | `Pv` | oui | | |
|
||||
| `battery` | `Battery` | oui | | |
|
||||
| `loads` | [`Load`] | oui | | Charges arbitrées, ⊆ GetLoadConfig. |
|
||||
| `forecast` | `Forecast` \| null | oui | | null sans instantané de prévision. |
|
||||
| `tariff` | `Tariff` \| null | oui | | |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:Site -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`Site`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `contractedPowerW` | number \| null | oui | | Puissance souscrite (W) ; null si inconnue (E1), jamais 0. |
|
||||
| `gridCapW` | null | oui | | RÉSERVÉ (signaux réseau type §14a EnWG) : toujours null en 1.1. |
|
||||
| `phaseLimitA` | [number] \| null | oui | | Limite par phase en A ; null si inconnue. |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:Meter -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`Meter`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `source` | string \| null | oui | | ThingId du compteur racine désigné ; null si inconnu. |
|
||||
| `importW` | number | oui | ≥ 0 | Puissance importée (W). |
|
||||
| `exportW` | number | oui | ≥ 0 | Puissance exportée (W), mesure BRUTE (AGENTS.md règle 8) — jamais le net après pilotage. |
|
||||
| `perPhaseA` | [number] | oui | | Courant par phase (A). |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:Pv -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`Pv`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `currentW` | number \| null | oui | ≥ 0 | Production instantanée (W), positive ; null sans producteur (jamais 0). |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:Battery -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`BatteryAbsent`** — Aucun stockage déclaré : rien d'autre n'est émis.
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `present` | boolean | oui | `false` | |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
**`BatteryPresent`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `present` | boolean | oui | `true` | |
|
||||
| `socPercent` | number | oui | | |
|
||||
| `powerW` | number | oui | | Positif = charge, négatif = décharge. |
|
||||
| `capacityWh` | number \| null | oui | | null si inconnue. |
|
||||
| `maxChargeW` | number \| null | oui | | null si inconnue. |
|
||||
| `maxDischargeW` | number \| null | oui | | null si inconnue. |
|
||||
| `reserveSocPercent` | integer \| null | oui | | Réserve de secours (%) ; null si inconnue. |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:Tariff -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`Tariff`** — Tarif 1.0, conservé pour compatibilité ; un optimiseur 1.1 lit forecast.price. Aucun écrivain aujourd'hui (E3) : null en pratique.
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `provider` | string \| null | oui | | |
|
||||
| `current` | `TariffEntry` | oui | | |
|
||||
| `next` | [`TariffNextEntry`] | oui | | |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:TariffEntry -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`TariffEntry`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `label` | string | oui | | |
|
||||
| `priceCtkWh` | number | oui | | |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:TariffNextEntry -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`TariffNextEntry`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `label` | string | oui | | |
|
||||
| `priceCtkWh` | number | oui | | |
|
||||
| `from` | string \| null | oui | instant avec décalage | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
### 5.2 Prévisions (`forecast`)
|
||||
|
||||
@ -218,7 +422,6 @@ index par index ; `null` dans une case = valeur inconnue — **jamais 0, jamais
|
||||
"resolutionS":3600,
|
||||
"steps":34,
|
||||
"pv":{
|
||||
"provider":"open-meteo",
|
||||
"weatherModel":"meteofrance_seamless",
|
||||
"conversion":"physics",
|
||||
"corrections":["temperature","ac-limit"],
|
||||
@ -227,16 +430,15 @@ index par index ; `null` dans une case = valeur inconnue — **jamais 0, jamais
|
||||
"acLimitW":8000,
|
||||
"p50W":[ ... ],
|
||||
"perField":[
|
||||
{"id":"<thingId plan>","label":"Toit sud","peakW":6000,"tiltDeg":35,"azimuthDeg":0,
|
||||
"corrections":["temperature"],"p50W":[ ... ]}
|
||||
{"id":"<thingId plan>","corrections":["temperature"]}
|
||||
]
|
||||
},
|
||||
"baseload":{"model":"none","p50W":[null, ...]},
|
||||
"weather":{"temperatureC":[ ... ]},
|
||||
"weather":null,
|
||||
"price":{
|
||||
"currency":"EUR","importCtkWh":[ ... ],"exportCtkWh":[ ... ],
|
||||
"includes":["energy","grid","taxes"],"source":"tempo",
|
||||
"knownUntil":"2026-10-13T06:00:00+02:00"
|
||||
"includes":["energy","grid","taxes"],"source":"tou",
|
||||
"knownUntil":"2026-10-13T22:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -294,8 +496,140 @@ Règles :
|
||||
connu sur tout l'horizon, `knownUntil` = fin de la grille. Un créneau à cheval sur deux
|
||||
zones reçoit la **moyenne pondérée par la durée** — le coût exact d'une puissance
|
||||
constante sur le créneau, seule valeur cohérente avec les autres séries de la grille.
|
||||
- `forecast` peut être `null`. L'optimiseur reste libre de sourcer ses propres prévisions
|
||||
(pont LAN, côté optimiseur), mais **le plugin, lui, ne sort pas** pour les obtenir.
|
||||
- `forecast` peut être `null`. L'optimiseur ne consomme **que** ce que le moteur lui transmet
|
||||
(décision du 2026-10-16) : il n'appelle ni service météo ni fournisseur de tarif, et le plugin,
|
||||
lui, ne sort pas pour les obtenir.
|
||||
- **Ce que le moteur 1.1 n'émet pas** (décrit ici pour la suite, absent du contrat, donc refusé
|
||||
s'il apparaissait sans changement de contrat) : `pv.provider` (E25), `pv.p10W`/`p90W`,
|
||||
`perField[].label`/`peakW`/`tiltDeg`/`azimuthDeg`/`p50W` (seuls `id` et `corrections` sont
|
||||
émis), `weather.temperatureC` (`weather` vaut toujours `null`), `baseload.model: learned`,
|
||||
`price.source` `tempo`/`day-ahead` (seuls `fixed` et `tou`, tarif saisi).
|
||||
|
||||
<!-- BEGIN GENERATED table:Forecast -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`Forecast`** — Prévisions sur UNE grille (§5.2) ; tableaux alignés index par index.
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `start` | string | oui | instant avec décalage | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
|
||||
| `resolutionS` | integer | oui | ≥ 1 | Pas de la grille (s) — fait foi, jamais supposé. |
|
||||
| `steps` | integer | oui | ≥ 0 | Nombre de cases ; toutes les séries ont cette longueur. |
|
||||
| `pv` | `PvForecast` \| null | oui | | null sans courbe (site non désigné, charge utile refusée, canal muet). |
|
||||
| `baseload` | `Baseload` | oui | | |
|
||||
| `weather` | null | oui | | Aucune source de température en 1.1 : toujours null. |
|
||||
| `price` | `Price` \| null | oui | | null sans tarif saisi. |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:PvForecast -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`PvForecast`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `p50W` | [number \| null] | oui | | 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 | oui | `physics` | Seule valeur émise par le moteur GPL ; un consommateur ignore une valeur inconnue. |
|
||||
| `corrections` | [string] | oui | `temperature` · `ac-limit` | [] = conversion brute. |
|
||||
| `weatherModel` | string \| null | oui | | Modèle qui a RÉPONDU ; null si inconnu. |
|
||||
| `issuedAt` | string \| null | oui | instant avec décalage | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
|
||||
| `contentChangedAt` | string \| null | oui | instant avec décalage | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
|
||||
| `acLimitW` | integer | non | | Plafond AC du site (W), s'il est configuré. |
|
||||
| `perField` | [`PvField`] | non | | |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:PvField -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`PvField`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `id` | string | oui | | ThingId du plan. |
|
||||
| `corrections` | [string] | oui | `temperature` · `ac-limit` | |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:Baseload -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`Baseload`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `model` | string | oui | `profile` · `none` | Le moteur GPL émet profile ou none (learned n'est pas émis). |
|
||||
| `p50W` | [number \| null] | oui | | |
|
||||
| `quality` | `BaseloadQuality` | non | | |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:BaseloadQuality -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`BaseloadQuality`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `windowDays` | integer | oui | | |
|
||||
| `minDays` | integer | oui | | |
|
||||
| `resolutionS` | integer | oui | | |
|
||||
| `daysCovered` | object | oui | | |
|
||||
| `daysCovered.workday` | integer | oui | | |
|
||||
| `daysCovered.weekend` | integer | oui | | |
|
||||
| `minutes` | integer | oui | | |
|
||||
| `commandedMinutes` | integer | oui | | |
|
||||
| `negativeMinutes` | integer | oui | | |
|
||||
| `incompleteMinutes` | integer | oui | | |
|
||||
| `computedAt` | string | non | instant avec décalage | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
|
||||
| `loads` | [`BaseloadLoadQuality`] | oui | | |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:BaseloadLoadQuality -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`BaseloadLoadQuality`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `loadId` | string | oui | | |
|
||||
| `source` | string | oui | `meter` · `device` · `none` | |
|
||||
| `minutesBySource` | object | oui | | |
|
||||
| `constantMeasurement` | boolean | non | | |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:Price -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`Price`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `currency` | string | oui | `"EUR"` | |
|
||||
| `importCtkWh` | [number \| null] | oui | | |
|
||||
| `exportCtkWh` | [number \| null] | oui | | |
|
||||
| `includes` | [string] | oui | `energy` · `grid` · `taxes` | |
|
||||
| `source` | string | oui | `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 | oui | instant avec décalage | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
### 5.3 Charges (`loads[]`)
|
||||
|
||||
@ -303,24 +637,21 @@ Règles :
|
||||
"loads":[
|
||||
{
|
||||
"id":"chauffe-eau","adapter":"relay-router","label":"Chauffe-eau","domain":"ecs",
|
||||
"group":{"groupRank":2,"rankInGroup":1},
|
||||
"priority":2,
|
||||
"declared":{"nominalPowerW":2400,"minPowerW":0,"maxPowerW":2400,"volumeL":200},
|
||||
"defaults":{"dailyEnergyWh":7000},
|
||||
"declared":{"minPowerW":0,"maxPowerW":2400},
|
||||
"learned":{"dailyEnergyWh":6400,"confidence":0.35},
|
||||
"telemetry":{"currentPowerW":1200,"available":true,"lockMinPowerW":1200,"lockMaxPowerW":-1,
|
||||
"measurement":{"source":"meter","powerW":1180}},
|
||||
"needs":{
|
||||
"dailyDeadline":"18:00","minEnergyWhPerDay":4000,
|
||||
"energyTargets":[{"by":"2026-06-07T18:00:00+02:00","minWh":4000,"source":"installer"}],
|
||||
"comfort":{"ecoMinC":50,"comfortC":60,"source":"installer"}
|
||||
"energyTargets":[{"by":"2026-06-07T18:00:00+02:00","minWh":4000,"source":"installer"}]
|
||||
},
|
||||
"limits":{"minOnS":300,"minOffS":300}
|
||||
},
|
||||
{
|
||||
"id":"ev-1","adapter":"evcharger","label":"Borne garage","domain":"ev",
|
||||
"declared":{"minA":6,"maxA":16,"phases":3},
|
||||
"telemetry":{"pluggedIn":true,"charging":true,"currentPowerW":4100,"sessionWh":5200,
|
||||
"telemetry":{"currentPowerW":4100,"available":true,"pluggedIn":true,"charging":true,"sessionWh":5200,
|
||||
"measurement":{"source":"device","powerW":4100}},
|
||||
"needs":{"targetSocPercent":80,"deadline":"2026-06-08T07:00:00+02:00"},
|
||||
"priority":1,
|
||||
@ -329,7 +660,8 @@ Règles :
|
||||
{
|
||||
"id":"pac-terrain","adapter":"sg-ready","label":"PAC","domain":"heating",
|
||||
"declared":{"states":[1,2,3,4],"estimatedPowerW":{"3":1800,"4":2600}},
|
||||
"telemetry":{"state":2,"minState":2,"maxState":4,"measurement":{"source":"none"}},
|
||||
"telemetry":{"currentPowerW":0,"available":true,"state":2,"minState":2,"maxState":4,
|
||||
"measurement":{"source":"none"}},
|
||||
"priority":3,
|
||||
"limits":{"minStateHoldS":900}
|
||||
}
|
||||
@ -338,7 +670,7 @@ Règles :
|
||||
|
||||
Notes de schéma :
|
||||
- `loads[].declared` : saisi par l'installateur (plaque signalétique, câblage). Fiable.
|
||||
- `loads[].defaults` : valeurs approximatives par catégorie. Point de départ.
|
||||
- `loads[].defaults` : valeurs approximatives par catégorie. Point de départ. **Non émis en 1.1.**
|
||||
- `loads[].learned` : estimations de l'optimiseur, **renvoyées** au plugin pour
|
||||
persistance et affichage, avec `confidence` (0–1). Le rule-based les ignore.
|
||||
- `loads[].priority` : **rang effectif** (1 = premier servi). Voir annexe C. Un optimiseur
|
||||
@ -369,9 +701,11 @@ Notes de schéma :
|
||||
- `energyTargets` : énergie minimale à avoir délivrée à une échéance. Forme générique
|
||||
(ECS, VE converti en Wh par le plugin). Plusieurs cibles possibles.
|
||||
- `comfort` : `ecoMinC` doit **toujours** être tenu ; `comfortC` seulement sur surplus
|
||||
(`specs/spec_loadmodel.md` §10, LM-1010).
|
||||
(`specs/spec_loadmodel.md` §10, LM-1010). **Non émis en 1.1** (aucune sonde de température
|
||||
n'entre dans le contexte).
|
||||
- `source` de chaque besoin : `installer` | `user-intent` | `learned`. Une intention client
|
||||
prime sur un besoin appris.
|
||||
prime sur un besoin appris. Le moteur 1.1 émet `installer` (obligation quotidienne) et
|
||||
`user-intent` (dérogation à cible d'énergie) ; `learned` est réservé.
|
||||
- Champs 1.0 (`dailyDeadline`, `minEnergyWhPerDay`, `targetSocPercent`, `deadline`) restent
|
||||
émis ; un optimiseur 1.1 préfère `energyTargets`.
|
||||
|
||||
@ -379,7 +713,10 @@ Notes de schéma :
|
||||
|
||||
- La priorité porte sur la **borne**, pas sur le véhicule.
|
||||
- `telemetry.vehicle` (optionnel) : `{"socPercent":54,"source":"myskoda","capacityWh":77000}`
|
||||
quand une intégration véhicule existe ; absent sinon.
|
||||
quand une intégration véhicule existe ; absent sinon — **non émis en 1.1** (aucune intégration).
|
||||
- Une borne **configurée dont l'appareil manque** n'a ni `declared`, ni `limits`, ni
|
||||
`pluggedIn`/`charging` : ses zéros diraient « inconnu ». `declared.phases` porte les phases
|
||||
**visées** ce cycle, pas celles de la plaque (E26).
|
||||
|
||||
### `loads[].telemetry` — champs ETM
|
||||
|
||||
@ -401,6 +738,163 @@ jamais supérieur au plafond.
|
||||
Ces champs sont exprimés **en watts**, jamais en index de palier : aucun identifiant de
|
||||
relais ni index de combinaison ne franchit cette frontière.
|
||||
|
||||
<!-- BEGIN GENERATED table:Load -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
Une charge arbitrée (§5.3) — la forme dépend du mécanisme (adapter).
|
||||
|
||||
**`LoadWatts`** — Charge pilotée en watts : combinaison de relais (relay-router) ou consigne continue (etmvariableload).
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `id` | string | oui | ≥ 1 car. | Identifiant de la charge — clé des actions. |
|
||||
| `label` | string | oui | | Nom lisible. |
|
||||
| `priority` | integer | oui | | Rang effectif (1 = premier servi). |
|
||||
| `domain` | string | non | `ecs` · `ev` · `heating` · `hvac` · `battery` | Intention déclarée par l'installateur, jamais déduite du mécanisme, non lue par l'arbitre (§5.3). GEN : LoadConfig::knownDomains(). |
|
||||
| `learned` | `LoadLearned` | non | | |
|
||||
| `needs` | `LoadNeeds` | non | | |
|
||||
| `adapter` | string | oui | `relay-router` · `etmvariableload` | |
|
||||
| `declared` | object | oui | | |
|
||||
| `declared.minPowerW` | number | oui | | 0 = aucun plancher. |
|
||||
| `declared.maxPowerW` | number | oui | | |
|
||||
| `telemetry` | object | oui | | |
|
||||
| `telemetry.currentPowerW` | number | oui | | Puissance courante (W) telle que l'adaptateur la publie. |
|
||||
| `telemetry.available` | boolean | oui | | Gouverne l'ALLOCATION, pas la COMPTABILITÉ (§5.3). |
|
||||
| `telemetry.measurement` | `Measurement` | non | | Mesure par charge (§5.3, LM-1105/1106) : décider sur source, jamais sur adapter. |
|
||||
| `telemetry.lockMinPowerW` | number | oui | | Plancher imposé MAINTENANT par un verrou. |
|
||||
| `telemetry.lockMaxPowerW` | number | oui | | Plafond imposé maintenant ; négatif = aucun plafond. |
|
||||
| `limits` | object | oui | | |
|
||||
| `limits.minOnS` | integer | oui | | |
|
||||
| `limits.minOffS` | integer | oui | | |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
**`LoadEvCharger`** — Borne. Une borne CONFIGURÉE dont l'appareil manque n'a ni declared, ni limits, ni pluggedIn/charging : ses zéros diraient « inconnu ».
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `id` | string | oui | ≥ 1 car. | Identifiant de la charge — clé des actions. |
|
||||
| `label` | string | oui | | Nom lisible. |
|
||||
| `priority` | integer | oui | | Rang effectif (1 = premier servi). |
|
||||
| `domain` | string | non | `ecs` · `ev` · `heating` · `hvac` · `battery` | Intention déclarée par l'installateur, jamais déduite du mécanisme, non lue par l'arbitre (§5.3). GEN : LoadConfig::knownDomains(). |
|
||||
| `learned` | `LoadLearned` | non | | |
|
||||
| `needs` | `LoadNeeds` | non | | |
|
||||
| `adapter` | string | oui | `"evcharger"` | |
|
||||
| `declared` | object | non | | |
|
||||
| `declared.minA` | number | oui | | |
|
||||
| `declared.maxA` | number | oui | | |
|
||||
| `declared.phases` | integer | oui | | Phases VISÉES ce cycle, pas celles de la plaque (E26). |
|
||||
| `telemetry` | object | oui | | |
|
||||
| `telemetry.currentPowerW` | number | oui | | Puissance courante (W) telle que l'adaptateur la publie. |
|
||||
| `telemetry.available` | boolean | oui | | Gouverne l'ALLOCATION, pas la COMPTABILITÉ (§5.3). |
|
||||
| `telemetry.measurement` | `Measurement` | non | | Mesure par charge (§5.3, LM-1105/1106) : décider sur source, jamais sur adapter. |
|
||||
| `telemetry.pluggedIn` | boolean | non | | |
|
||||
| `telemetry.charging` | boolean | non | | |
|
||||
| `telemetry.sessionWh` | number | non | | Absent si la borne ne compte pas sa session (LM-1009 C1). |
|
||||
| `limits` | object | non | | |
|
||||
| `limits.chargingEnabledLockS` | integer | oui | | |
|
||||
| `limits.currentChangeLockS` | integer | oui | | |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
**`LoadSgReady`** — PAC SG-Ready : 4 états normés, kind state.
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `id` | string | oui | ≥ 1 car. | Identifiant de la charge — clé des actions. |
|
||||
| `label` | string | oui | | Nom lisible. |
|
||||
| `priority` | integer | oui | | Rang effectif (1 = premier servi). |
|
||||
| `domain` | string | non | `ecs` · `ev` · `heating` · `hvac` · `battery` | Intention déclarée par l'installateur, jamais déduite du mécanisme, non lue par l'arbitre (§5.3). GEN : LoadConfig::knownDomains(). |
|
||||
| `learned` | `LoadLearned` | non | | |
|
||||
| `needs` | `LoadNeeds` | non | | |
|
||||
| `adapter` | string | oui | `"sg-ready"` | |
|
||||
| `declared` | object | oui | | |
|
||||
| `declared.states` | [integer] | oui | ≥ 1 ; ≤ 4 | |
|
||||
| `declared.estimatedPowerW` | object | oui | | |
|
||||
| `telemetry` | object | oui | | |
|
||||
| `telemetry.currentPowerW` | number | oui | | Puissance courante (W) telle que l'adaptateur la publie. |
|
||||
| `telemetry.available` | boolean | oui | | Gouverne l'ALLOCATION, pas la COMPTABILITÉ (§5.3). |
|
||||
| `telemetry.measurement` | `Measurement` | non | | Mesure par charge (§5.3, LM-1105/1106) : décider sur source, jamais sur adapter. |
|
||||
| `telemetry.state` | integer | oui | ≥ 0 ; ≤ 4 | État commandé (1-4) ; 0 = non applicable. |
|
||||
| `telemetry.minState` | integer | oui | | Fenêtre d'états autorisée MAINTENANT par minStateHold. |
|
||||
| `telemetry.maxState` | integer | oui | | |
|
||||
| `limits` | object | oui | | |
|
||||
| `limits.minStateHoldS` | integer | oui | | |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:Measurement -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
Mesure par charge (§5.3, LM-1105/1106) : décider sur source, jamais sur adapter.
|
||||
|
||||
**`Measurement, forme 1`** — Rien ne mesure : état positif, pas une erreur.
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `source` | string | oui | `"none"` | |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
**`Measurement, forme 2`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `source` | string | oui | `meter` · `device` | |
|
||||
| `powerW` | number | oui | | |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:LoadNeeds -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`LoadNeeds`** — Besoins (§5.3). Champs 1.0 émis tels quels quand ils sont déclarés ; un optimiseur 1.1 préfère energyTargets.
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `dailyDeadline` | string | non | motif `^[0-9]{2}:[0-9]{2}$` | Heure murale locale HH:MM (1.0). |
|
||||
| `minEnergyWhPerDay` | integer | non | > 0 | |
|
||||
| `targetSocPercent` | integer | non | > 0 | |
|
||||
| `deadline` | string | non | instant avec décalage | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
|
||||
| `energyTargets` | [`EnergyTarget`] | non | | |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:EnergyTarget -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`EnergyTarget`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `by` | string | oui | instant avec décalage | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
|
||||
| `minWh` | number | oui | > 0 | Énergie minimale à avoir délivrée à l'échéance. |
|
||||
| `source` | string | oui | `installer` · `user-intent` | learned n'est pas émis en 1.1. |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:LoadLearned -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`LoadLearned`** — Absent tant qu'aucun profil n'a été reçu (E12).
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `dailyEnergyWh` | number | oui | | |
|
||||
| `confidence` | number | oui | ≥ 0 ; ≤ 1 | |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
## 6. Réponse : plan par créneaux
|
||||
|
||||
```json
|
||||
@ -470,6 +964,162 @@ Règles d'exécution côté plugin :
|
||||
La frontière est en **watts** : `relay-router` choisit lui-même la combinaison de relais,
|
||||
`etmvariableload` reçoit une consigne continue. Aucun index de palier, aucun pourcentage.
|
||||
|
||||
Ce que le contrat déclare de la réponse (une réponse peut porter d'autres champs : ils sont
|
||||
ignorés, §9). Les codes émissibles et leurs seules clés de paramètres transmises :
|
||||
|
||||
<!-- BEGIN GENERATED table:GetPlanResult -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`Abstention`** — « Pas de plan maintenant » (§7) : repli pour ce cycle, pas un échec.
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `abstain` | boolean | oui | `true` | |
|
||||
| `reason` | string | non | | |
|
||||
|
||||
Champs non déclarés : ignorés (§9).
|
||||
|
||||
**`Plan`** — Plan par créneaux (§6).
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `abstain` | boolean | non | `false` | |
|
||||
| `planId` | string | non | | |
|
||||
| `strategy` | string | non | | Chaîne libre, informative. |
|
||||
| `computedAt` | string | non | instant avec décalage | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
|
||||
| `validUntil` | string \| null | non | instant avec décalage | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
|
||||
| `slots` | [`Slot`] | oui | | [] = abstention (empty-plan). Seul le créneau couvrant maintenant est exécuté. |
|
||||
| `diagnostics` | `Diagnostics` | non | | |
|
||||
|
||||
Champs non déclarés : ignorés (§9).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:Slot -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`Slot`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `from` | string | oui | instant avec décalage | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
|
||||
| `to` | string | oui | instant avec décalage | Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée. |
|
||||
| `actions` | [`Action`] \| null | non | | Absent ou null = aucune action. |
|
||||
| `expected` | `Expected` | non | | |
|
||||
|
||||
Champs non déclarés : ignorés (§9).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:Action -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`SetpointAction`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `loadId` | string | oui | ≥ 1 car. | Charge visée : connue du contexte (règle unknown-load), une fois par créneau (duplicate-load). |
|
||||
| `reason` | string | oui | ≥ 1 car. ; motif `\S` | Texte français, OBLIGATOIRE et non vide (règle missing-reason). Traces et journal seulement, jamais vers l'app. |
|
||||
| `reasonCode` | string | non | `SURPLUS_SETPOINT` · `SURPLUS_INSUFFICIENT` · `BELOW_MIN_POWER` · `EV_DEADLINE` · `SG_FORCED` · `SG_RECOMMENDED` · `SG_NORMAL` · `ECO_FLOOR_GRID` · `PRICE_SLOT` · `OPTIMIZER_DECISION` | 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. |
|
||||
| `reasonParams` | object \| null | non | | 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). |
|
||||
| `force` | quelconque | non | | INTERDIT sur le fil (§6) : ignoré, signalé une fois. |
|
||||
| `kind` | string | oui | `"setpoint"` | |
|
||||
| `powerW` | number | non | ≥ 0 | Consigne en watts (relay-router, etmvariableload ; ou borne). |
|
||||
| `currentA` | number | non | ≥ 0 | Borne : courant (0 = suspendre). |
|
||||
| `phases` | enum | non | `1` · `3` | Borne : phases visées ; absent = inchangé. |
|
||||
|
||||
Champs non déclarés : ignorés (§9).
|
||||
Au moins un de : `powerW`, `currentA`.
|
||||
|
||||
**`StateAction`**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `loadId` | string | oui | ≥ 1 car. | Charge visée : connue du contexte (règle unknown-load), une fois par créneau (duplicate-load). |
|
||||
| `reason` | string | oui | ≥ 1 car. ; motif `\S` | Texte français, OBLIGATOIRE et non vide (règle missing-reason). Traces et journal seulement, jamais vers l'app. |
|
||||
| `reasonCode` | string | non | `SURPLUS_SETPOINT` · `SURPLUS_INSUFFICIENT` · `BELOW_MIN_POWER` · `EV_DEADLINE` · `SG_FORCED` · `SG_RECOMMENDED` · `SG_NORMAL` · `ECO_FLOOR_GRID` · `PRICE_SLOT` · `OPTIMIZER_DECISION` | 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. |
|
||||
| `reasonParams` | object \| null | non | | 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). |
|
||||
| `force` | quelconque | non | | INTERDIT sur le fil (§6) : ignoré, signalé une fois. |
|
||||
| `kind` | string | oui | `"state"` | |
|
||||
| `state` | integer | oui | ≥ 1 ; ≤ 4 | 1 = verrouillage, 2 = normal, 3 = recommandation, 4 = forcé. |
|
||||
|
||||
Champs non déclarés : ignorés (§9).
|
||||
|
||||
**`ConstraintAction`** — RÉSERVÉ (annexe F) : forme décrite, toujours refusée (kind-mismatch).
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `loadId` | string | oui | ≥ 1 car. | Charge visée : connue du contexte (règle unknown-load), une fois par créneau (duplicate-load). |
|
||||
| `reason` | string | oui | ≥ 1 car. ; motif `\S` | Texte français, OBLIGATOIRE et non vide (règle missing-reason). Traces et journal seulement, jamais vers l'app. |
|
||||
| `reasonCode` | string | non | `SURPLUS_SETPOINT` · `SURPLUS_INSUFFICIENT` · `BELOW_MIN_POWER` · `EV_DEADLINE` · `SG_FORCED` · `SG_RECOMMENDED` · `SG_NORMAL` · `ECO_FLOOR_GRID` · `PRICE_SLOT` · `OPTIMIZER_DECISION` | 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. |
|
||||
| `reasonParams` | object \| null | non | | 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). |
|
||||
| `force` | quelconque | non | | INTERDIT sur le fil (§6) : ignoré, signalé une fois. |
|
||||
| `kind` | string | oui | `"constraint"` | |
|
||||
| `charge` | enum | non | `allow` · `forbid` | |
|
||||
| `discharge` | enum | non | `allow` · `forbid` | |
|
||||
|
||||
Champs non déclarés : ignorés (§9).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:ReasonParams -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
`ReasonParams` : object \| null — . 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).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:Expected -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`Expected`** — Trajectoire prévue : affichage et mesure d'écart, JAMAIS pour commander.
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `gridImportW` | number | non | | |
|
||||
| `gridExportW` | number | non | | |
|
||||
| `batterySocPercent` | quelconque | non | | RÉSERVÉ (annexe F) : ignoré. |
|
||||
|
||||
Champs non déclarés : ignorés (§9).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED table:Diagnostics -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`Diagnostics`** — Informatif : mode installateur et traces.
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `solver` | string | non | | |
|
||||
| `status` | string | non | `optimal` · `feasible` · `best-so-far` · `infeasible-relaxed` | |
|
||||
| `gapPercent` | number | non | | |
|
||||
| `computeMs` | number | non | ≥ 0 | |
|
||||
| `inputsUsed` | object | non | | |
|
||||
| `inputsUsed.pv` | string | non | `physics` · `hybrid` · `none` | |
|
||||
|
||||
Champs non déclarés : ignorés (§9).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
<!-- BEGIN GENERATED reason-codes -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
| `reasonCode` | Clés de `reasonParams` admises |
|
||||
|---|---|
|
||||
| `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` | — |
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
## 7. Abstention et repli
|
||||
|
||||
L'optimiseur peut répondre :
|
||||
@ -479,8 +1129,8 @@ L'optimiseur peut répondre :
|
||||
```
|
||||
|
||||
Sémantique : « je n'ai pas de plan à proposer maintenant ». Le plugin bascule sur la
|
||||
stratégie rule-based **pour ce cycle**, sans considérer l'optimiseur comme mort
|
||||
(`optimizerAlive` reste `true`).
|
||||
stratégie rule-based **pour ce cycle**, sans considérer l'optimiseur comme mort :
|
||||
`optimizerAlive` est **inchangé** (une abstention ne ressuscite pas un optimiseur mort, E-T4-1).
|
||||
|
||||
| Situation | optimizerAlive | Stratégie active |
|
||||
|---|---|---|
|
||||
@ -498,6 +1148,39 @@ requête remplacée par une plus récente n'est pas un échec. Avant le premier
|
||||
`optimizerAlive` vaut `false`. Un plan invalide remplace le cache (l'ancien plan valide n'est
|
||||
plus appliqué) et un socket fermé l'abandonne (E-T3-9).
|
||||
|
||||
**Ce que le validateur refuse en plus de la forme.** Le JSON Schema du contrat dit la forme ; le
|
||||
moteur juge ensuite chaque réponse sur le contexte de SA requête, dans cet ordre, et le premier
|
||||
contrôle qui échoue donne son code (`fallbackCode` vers l'app, `lastErrorCode` pour un refus) :
|
||||
|
||||
<!-- BEGIN GENERATED rules -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
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).
|
||||
|
||||
| Règle | Méthode | Issue | `fallbackCode` | Échec compté | Il faut aussi | Le schéma la voit aussi | Ce qui est refusé |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `abstain` | GetPlan | abstention | `abstain` | non | — | non | result.abstain vaut true. |
|
||||
| `malformed-plan` | GetPlan | invalid | `malformed-plan` | oui | — | oui | slots absent ou non liste ; un créneau qui n'est pas un objet ; actions non liste. |
|
||||
| `empty-plan` | GetPlan | abstention | `empty-plan` | non | — | non | slots est une liste vide. |
|
||||
| `bad-time` | GetPlan | invalid | `bad-time` | oui | — | oui | from/to illisibles ou sans décalage, ou from ≥ to ; validUntil illisible ou sans décalage. |
|
||||
| `slots-unordered` | GetPlan | invalid | `slots-unordered` | oui | — | non | un créneau commence avant la fin du précédent. |
|
||||
| `malformed-action` | GetPlan | invalid | `malformed-action` | oui | — | oui | action qui n'est pas un objet, ou sans loadId (chaîne non vide). |
|
||||
| `unknown-load` | GetPlan | invalid | `unknown-load` | oui | request | non | loadId absent des loads[] de la requête. |
|
||||
| `unknown-kind` | GetPlan | invalid | `unknown-kind` | oui | — | oui | kind hors ActionKind. |
|
||||
| `kind-mismatch` | GetPlan | invalid | `kind-mismatch` | oui | request | non | kind différent de celui du mécanisme de la charge (setpoint : evcharger, relay-router, etmvariableload ; state : sg-ready ; constraint : aucun). |
|
||||
| `kind-not-announced` | GetPlan | invalid | `kind-not-announced` | oui | handshake | non | capabilities.kinds annoncé au handshake et kind absent de la liste. |
|
||||
| `missing-reason` | GetPlan | invalid | `missing-reason` | oui | — | oui | reason absent, non textuel, ou vide une fois les caractères de contrôle retirés. |
|
||||
| `bad-value` | GetPlan | invalid | `bad-value` | oui | request | oui | 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. |
|
||||
| `duplicate-load` | GetPlan | invalid | `duplicate-load` | oui | — | non | deux actions pour la même charge dans un créneau. |
|
||||
| `expired` | GetPlan | expired | `expired` | non | request | non | validUntil ≤ timestamp de la requête. |
|
||||
| `no-current-slot` | GetPlan | abstention | `no-current-slot` | non | request | non | aucun créneau ne couvre le timestamp de la requête. |
|
||||
| `incomplete-slot` | GetPlan | invalid | `incomplete-slot` | oui | request | non | une charge available de la requête n'a pas d'action dans le créneau courant (décision Q1). |
|
||||
| `handshake-refused` | Handshake | connection-refused | `no-plan` | non | — | non | réponse error (token absent ou différent : -32001 recommandé) → connexion fermée, nouvelle tentative au backoff. |
|
||||
| `invalid-handshake` | Handshake | connection-refused | `no-plan` | non | — | oui | result non objet, protocolVersion absent ou vide, capabilities présent et non objet. |
|
||||
| `major-mismatch` | Handshake | connection-refused | `no-plan` | non | — | non | MAJEURE de protocolVersion (ce qui précède le premier point) différente de celle du moteur. |
|
||||
|
||||
<!-- END GENERATED -->
|
||||
|
||||
L'état exposé à l'app (`optimizerExpected`, `optimizerAlive`, `activeStrategy` ∈
|
||||
`optimizer` | `rules` | `none`) reflète cet état en continu ; chaque transition émet
|
||||
`NymeaEnergy.OptimizerStatusChanged` (`INTERFACE.md`). Il n'y a pas de `tier` : l'app déduit ce
|
||||
@ -512,6 +1195,19 @@ qu'elle affiche de ces trois champs.
|
||||
- Tant que `confidence < 0.7` (seuil indicatif), l'app peut afficher un badge
|
||||
« profil en apprentissage ».
|
||||
- Les données d'apprentissage restent **locales**. Rien ne quitte le site.
|
||||
|
||||
<!-- BEGIN GENERATED table:UpdateLearned.params -->
|
||||
<!-- Généré depuis docs/etm-powersync-optimizer-protocol.openrpc.json par tools/etm-powersync-gen-protocol-doc — ne pas éditer à la main. -->
|
||||
|
||||
**`UpdateLearned` — params**
|
||||
|
||||
| Champ | Type | Obligatoire | Valeurs | Sens |
|
||||
|---|---|---|---|---|
|
||||
| `loads` | [object] | non | | Non normé en 1.1 : intention §8 (learned + confidence par charge). |
|
||||
|
||||
Champs non déclarés : **refusés** (forme émise par le moteur, fermée).
|
||||
|
||||
<!-- END GENERATED -->
|
||||
- L'état des modèles de prévision (`forecast.pv.conversion`, `forecast.pv.corrections`,
|
||||
`forecast.baseload.model`) est exposé dans l'app au même titre que `confidence`.
|
||||
|
||||
@ -526,7 +1222,7 @@ qu'elle affiche de ces trois champs.
|
||||
## Annexe A — Session minimale
|
||||
|
||||
```
|
||||
→ {"jsonrpc":"2.0","id":1,"method":"Handshake","params":{"protocolVersion":"1.1","engine":"etm/0.9"}}
|
||||
→ {"jsonrpc":"2.0","id":1,"method":"Handshake","params":{"protocolVersion":"1.1","engine":"etm-powersync-energy-plugin-etm/1.15.2+etm70"}}
|
||||
← {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"1.1","name":"mon-optimiseur","planning":false}}
|
||||
→ {"jsonrpc":"2.0","id":2,"method":"GetPlan","params":{ ...SurplusContext... }}
|
||||
← {"jsonrpc":"2.0","id":2,"result":{"planId":"x","strategy":"custom","slots":[{ "from":"...","to":"...","actions":[...] }]}}
|
||||
@ -632,3 +1328,22 @@ Ils deviendront normatifs avec le lot qui les implémente.
|
||||
- `loads[].policies` : `noDischargeWhileEvCharging`, `holdChargeForDomains`,
|
||||
`gridChargeAllowed`.
|
||||
- `expected.batterySocPercent`.
|
||||
|
||||
## Annexe G — Contrat machine, validateur et conformité
|
||||
|
||||
- **Le contrat** : `docs/etm-powersync-optimizer-protocol.openrpc.json` (OpenRPC 1.3). Méthodes
|
||||
`Handshake`, `GetPlan`, `UpdateLearned` ; forme des messages en JSON Schema
|
||||
(`components.schemas`) ; ce qu'OpenRPC ne sait pas dire en extensions `x-etm-…` :
|
||||
`x-etm-transport` (unix/tcp, NDJSON, rôles, délais), `x-etm-direction` (sens d'une méthode),
|
||||
`x-etm-fallback-machine` (§7), `x-etm-semantic-rules` (ce que le validateur refuse en plus, avec
|
||||
`fallbackCode`), `x-etm-tolerances` (ce que le moteur accepte au-delà), `x-etm-reason-params`,
|
||||
`x-etm-generated` (listes fermées régénérées depuis le code). Forme côté moteur (requêtes)
|
||||
**fermée** ; côté optimiseur (réponses) **ouverte** (§9).
|
||||
- **Servi** par le moteur qui tourne : `NymeaEnergy.GetOptimizerProtocol` (`INTERFACE.md`). Pas de
|
||||
`rpc.discover` sur le socket : l'optimiseur se déclare au handshake, point.
|
||||
- **Validateur** : `tools/etm-powersync-schema-check` (Python, bibliothèque standard) — un message,
|
||||
une session NDJSON (les deux sens) ou des traces (annexe D) ; `--semantic` applique les règles.
|
||||
- **Conformité d'un optimiseur quelconque** : suite `etm-powersync-optimizer-conformance`
|
||||
(`tests/auto/conformance/README.md`) — le vrai client du moteur branché sur l'optimiseur que
|
||||
lance `ETM_POWERSYNC_OPTIMIZER_CMD`.
|
||||
- **Prose générée** : `tools/etm-powersync-gen-protocol-doc` (`--check` dans `tools/ci-quality.sh`).
|
||||
|
||||
@ -2089,31 +2089,11 @@
|
||||
]
|
||||
},
|
||||
"x-etm-reason-params": {
|
||||
"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",
|
||||
@ -2124,11 +2104,31 @@
|
||||
"minAlwaysW",
|
||||
"boundBy"
|
||||
],
|
||||
"EV_DEADLINE": [],
|
||||
"OPTIMIZER_DECISION": [],
|
||||
"PRICE_SLOT": [
|
||||
"priceCtkWh",
|
||||
"setpointW"
|
||||
],
|
||||
"OPTIMIZER_DECISION": []
|
||||
"SG_FORCED": [
|
||||
"budgetW",
|
||||
"estimatedW"
|
||||
],
|
||||
"SG_NORMAL": [
|
||||
"budgetW"
|
||||
],
|
||||
"SG_RECOMMENDED": [
|
||||
"budgetW",
|
||||
"estimatedW"
|
||||
],
|
||||
"SURPLUS_INSUFFICIENT": [
|
||||
"budgetW"
|
||||
],
|
||||
"SURPLUS_SETPOINT": [
|
||||
"budgetW",
|
||||
"setpointW",
|
||||
"stepped"
|
||||
]
|
||||
},
|
||||
"x-etm-generated": {
|
||||
"description": "Listes fermées RÉGÉNÉRÉES depuis le code (tools/etm-powersync-gen-protocol-doc --enums-from) ; la suite etm-powersync-protocolschema échoue si le code et le schéma divergent.",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user