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

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

2179 lines
66 KiB
JSON

{
"openrpc": "1.3.2",
"info": {
"title": "etm-powersync — protocole moteur ↔ optimiseur",
"version": "1.1",
"license": {
"name": "GPL-3.0-or-later",
"url": "https://www.gnu.org/licenses/gpl-3.0.html"
},
"description": "Contrat MACHINE du protocole entre le moteur d'énergie etm-powersync-energy-plugin-etm (client) et un optimiseur externe (serveur) — docs/OPTIMIZER_PROTOCOL.md en est la prose. Ce document est la source de la forme des messages ; il est servi par le moteur qui tourne (NymeaEnergy.GetOptimizerProtocol) et les deux côtés sont testés contre lui.\n\nCe qu'OpenRPC ne dit pas, et où cela se trouve :\n- TRANSPORT : unix:// (défaut, socket local 0660) ou tcp:// (LAN), JSON-RPC 2.0, un message par ligne (NDJSON), UTF-8 — x-etm-transport ;\n- SENS : le moteur est le CLIENT, il se connecte et envoie Handshake puis GetPlan ; la box n'ouvre aucun port. UpdateLearned va dans l'autre sens (x-etm-direction) ;\n- HEARTBEAT IMPLICITE et machine de repli §7 — x-etm-fallback-machine ;\n- VALIDATION SÉMANTIQUE du plan : le JSON Schema dit la forme ; ce que le validateur du moteur refuse en plus (charge inconnue, créneau courant incomplet…) est listé, règle par règle, dans x-etm-semantic-rules avec son fallbackCode ;\n- listes fermées : régénérées DEPUIS LE CODE (x-etm-generated), jamais recopiées à la main.\n\nForme côté moteur (requêtes) : STRICTE — additionalProperties: false ; un champ ajouté au sérialiseur sans le schéma fait échouer les tests. Forme côté optimiseur (réponses) : ouverte — un champ inconnu est ignoré (§9) ; le schéma dit ce qu'un optimiseur CONFORME envoie, x-etm-tolerances ce que le moteur accepte en plus."
},
"servers": [
{
"name": "unix",
"url": "unix:///run/powersync/optimizer.sock",
"summary": "Défaut : optimiseur local, socket possédé par root:powersync, mode 0660."
},
{
"name": "tcp",
"url": "tcp://{host}:{port}",
"summary": "Optimiseur sur le LAN. Token recommandé ; il circule en clair sans TLS (E16).",
"variables": {
"host": {
"default": "192.168.1.10"
},
"port": {
"default": "7400"
}
}
}
],
"methods": [
{
"name": "Handshake",
"summary": "Ouverture de session, à chaque (re)connexion (§3).",
"description": "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é.",
"paramStructure": "by-name",
"params": [
{
"name": "protocolVersion",
"required": true,
"schema": {
"$ref": "#/components/schemas/ProtocolVersion"
},
"description": "Version du moteur ; seule la majeure est comparée."
},
{
"name": "engine",
"required": true,
"schema": {
"type": "string",
"pattern": "^etm-powersync-energy-plugin-etm/"
},
"description": "Moteur et sa version."
},
{
"name": "token",
"required": false,
"schema": {
"$ref": "#/components/schemas/HandshakeToken"
}
}
],
"result": {
"name": "handshake",
"schema": {
"$ref": "#/components/schemas/HandshakeResult"
}
},
"errors": [
{
"code": -32001,
"message": "token refusé",
"data": "Recommandé (optimiseur de référence). Toute réponse error est traitée pareil : handshake-refused, connexion fermée."
}
],
"x-etm-direction": "engine→optimizer",
"x-etm-timeout-s": 5
},
{
"name": "GetPlan",
"summary": "Demande d'un plan pour le contexte courant (§4-§6).",
"description": "À 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.",
"paramStructure": "by-name",
"params": [
{
"name": "timestamp",
"required": true,
"schema": {
"type": "string",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2}(\\.[0-9]+)?)?(Z|[+-][0-9]{2}:[0-9]{2})$",
"description": "Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée."
},
"description": "Instant du cycle — le « maintenant » du validateur."
},
{
"name": "trigger",
"required": true,
"schema": {
"$ref": "#/components/schemas/Trigger"
}
},
{
"name": "site",
"required": true,
"schema": {
"$ref": "#/components/schemas/Site"
}
},
{
"name": "meter",
"required": true,
"schema": {
"$ref": "#/components/schemas/Meter"
}
},
{
"name": "pv",
"required": true,
"schema": {
"$ref": "#/components/schemas/Pv"
}
},
{
"name": "battery",
"required": true,
"schema": {
"$ref": "#/components/schemas/Battery"
}
},
{
"name": "loads",
"required": true,
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Load"
}
},
"description": "Charges arbitrées, ⊆ GetLoadConfig."
},
{
"name": "forecast",
"required": true,
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/Forecast"
},
{
"type": "null"
}
]
},
"description": "null sans instantané de prévision."
},
{
"name": "tariff",
"required": true,
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/Tariff"
},
{
"type": "null"
}
]
}
}
],
"result": {
"name": "plan",
"schema": {
"$ref": "#/components/schemas/GetPlanResult"
}
},
"errors": [
{
"code": -32603,
"message": "erreur interne",
"data": "Toute réponse error compte comme un échec de la requête (rpc-error, §7) ; le code n'est pas lu."
}
],
"x-etm-direction": "engine→optimizer",
"x-etm-timeout-s": 10
},
{
"name": "UpdateLearned",
"summary": "Estimations apprises de l'optimiseur (§8) — notification, sans id.",
"description": "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.",
"paramStructure": "by-name",
"params": [
{
"name": "loads",
"required": false,
"schema": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
},
"description": "Non normé en 1.1 : intention §8 (learned + confidence par charge)."
}
],
"x-etm-direction": "optimizer→engine",
"x-etm-notification": true
}
],
"components": {
"schemas": {
"ProtocolVersion": {
"description": "Version du protocole parlée par le moteur (GEN : OptimizerProtocol::ProtocolVersion).",
"const": "1.1"
},
"Trigger": {
"description": "Cause de la requête (§5.1). GEN : SocketScheduler::knownTriggers() — du moins au plus significatif.",
"type": "string",
"enum": [
"cycle",
"forecast",
"config",
"load-change",
"intent"
]
},
"Domain": {
"description": "Intention déclarée par l'installateur, jamais déduite du mécanisme, non lue par l'arbitre (§5.3). GEN : LoadConfig::knownDomains().",
"type": "string",
"enum": [
"ecs",
"ev",
"heating",
"hvac",
"battery"
]
},
"ActionKind": {
"description": "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.",
"type": "string",
"enum": [
"setpoint",
"state",
"constraint"
]
},
"ReasonCode": {
"description": "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.",
"type": "string",
"enum": [
"SURPLUS_SETPOINT",
"SURPLUS_INSUFFICIENT",
"BELOW_MIN_POWER",
"EV_DEADLINE",
"SG_FORCED",
"SG_RECOMMENDED",
"SG_NORMAL",
"ECO_FLOOR_GRID",
"PRICE_SLOT",
"OPTIMIZER_DECISION"
]
},
"PriceInclude": {
"description": "Composante incluse dans le prix (§5.2). GEN : TariffConfig::includes().",
"type": "string",
"enum": [
"energy",
"grid",
"taxes"
]
},
"FallbackCode": {
"description": "Côté app (NymeaEnergy.GetPlan.fallbackCode, INTERFACE.md) : pourquoi l'optimiseur n'a pas commandé le cycle. Hors fil. GEN : SocketScheduler::knownFallbackCodes().",
"type": "string",
"enum": [
"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"
]
},
"LastErrorCode": {
"description": "Côté app (NymeaEnergy.GetOptimizerStatus.lastErrorCode, INTERFACE.md) : dernier code d'erreur vu (connexion, requête, plan). Hors fil. GEN : SocketScheduler::knownLastErrorCodes().",
"type": "string",
"enum": [
"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"
]
},
"HandshakeToken": {
"description": "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",
"minLength": 16,
"pattern": "^[^\\s\\x00-\\x1f\\x7f]+$"
},
"Capabilities": {
"description": "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).",
"type": "object",
"properties": {
"maxHorizonH": {
"description": "Horizon maximal planifié, en heures.",
"type": "number",
"minimum": 0
},
"resolutionS": {
"description": "Pas des créneaux, en secondes.",
"type": "number",
"minimum": 0
},
"kinds": {
"description": "PRÉSENT, il restreint les kinds acceptés (règle kind-not-announced).",
"type": "array",
"items": {
"$ref": "#/components/schemas/ActionKind"
}
},
"usesForecast": {
"type": "array",
"items": {
"type": "string",
"enum": [
"pv",
"baseload",
"price",
"temperature"
]
}
},
"reasonCodes": {
"description": "L'optimiseur émet des reasonCode du catalogue.",
"type": "boolean"
}
},
"required": [],
"additionalProperties": true
},
"HandshakeResult": {
"type": "object",
"properties": {
"protocolVersion": {
"description": "Version semver de l'optimiseur ; même MAJEURE que le moteur exigée (règle major-mismatch).",
"type": "string",
"minLength": 1,
"pattern": "^[0-9]+(\\.[0-9]+)*$"
},
"name": {
"description": "Nom affiché (mode installateur), non contrôlé par le moteur (E-T7-3).",
"type": "string"
},
"planning": {
"description": "Vrai si l'optimiseur produit des plans multi-créneaux. Informatif.",
"type": "boolean"
},
"capabilities": {
"$ref": "#/components/schemas/Capabilities"
}
},
"required": [
"protocolVersion"
],
"additionalProperties": true
},
"Site": {
"type": "object",
"properties": {
"contractedPowerW": {
"description": "Puissance souscrite (W) ; null si inconnue (E1), jamais 0.",
"type": [
"number",
"null"
]
},
"gridCapW": {
"description": "RÉSERVÉ (signaux réseau type §14a EnWG) : toujours null en 1.1.",
"type": "null"
},
"phaseLimitA": {
"description": "Limite par phase en A ; null si inconnue.",
"type": [
"array",
"null"
],
"items": {
"type": "number"
}
}
},
"required": [
"contractedPowerW",
"gridCapW",
"phaseLimitA"
],
"additionalProperties": false
},
"Meter": {
"type": "object",
"properties": {
"source": {
"description": "ThingId du compteur racine désigné ; null si inconnu.",
"type": [
"string",
"null"
]
},
"importW": {
"description": "Puissance importée (W).",
"type": "number",
"minimum": 0
},
"exportW": {
"description": "Puissance exportée (W), mesure BRUTE (AGENTS.md règle 8) — jamais le net après pilotage.",
"type": "number",
"minimum": 0
},
"perPhaseA": {
"description": "Courant par phase (A).",
"type": "array",
"items": {
"type": "number"
}
}
},
"required": [
"source",
"importW",
"exportW",
"perPhaseA"
],
"additionalProperties": false
},
"Pv": {
"type": "object",
"properties": {
"currentW": {
"description": "Production instantanée (W), positive ; null sans producteur (jamais 0).",
"type": [
"number",
"null"
],
"minimum": 0
}
},
"required": [
"currentW"
],
"additionalProperties": false
},
"BatteryAbsent": {
"description": "Aucun stockage déclaré : rien d'autre n'est émis.",
"type": "object",
"properties": {
"present": {
"const": false
}
},
"required": [
"present"
],
"additionalProperties": false
},
"BatteryPresent": {
"type": "object",
"properties": {
"present": {
"const": true
},
"socPercent": {
"type": "number"
},
"powerW": {
"description": "Positif = charge, négatif = décharge.",
"type": "number"
},
"capacityWh": {
"description": "null si inconnue.",
"type": [
"number",
"null"
]
},
"maxChargeW": {
"description": "null si inconnue.",
"type": [
"number",
"null"
]
},
"maxDischargeW": {
"description": "null si inconnue.",
"type": [
"number",
"null"
]
},
"reserveSocPercent": {
"description": "Réserve de secours (%) ; null si inconnue.",
"type": [
"integer",
"null"
]
}
},
"required": [
"present",
"socPercent",
"powerW",
"capacityWh",
"maxChargeW",
"maxDischargeW",
"reserveSocPercent"
],
"additionalProperties": false
},
"Battery": {
"oneOf": [
{
"$ref": "#/components/schemas/BatteryAbsent"
},
{
"$ref": "#/components/schemas/BatteryPresent"
}
]
},
"TariffEntry": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"priceCtkWh": {
"type": "number"
}
},
"required": [
"label",
"priceCtkWh"
],
"additionalProperties": false
},
"TariffNextEntry": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"priceCtkWh": {
"type": "number"
},
"from": {
"type": [
"string",
"null"
],
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2}(\\.[0-9]+)?)?(Z|[+-][0-9]{2}:[0-9]{2})$",
"description": "Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée."
}
},
"required": [
"label",
"priceCtkWh",
"from"
],
"additionalProperties": false
},
"Tariff": {
"description": "Tarif 1.0, conservé pour compatibilité ; un optimiseur 1.1 lit forecast.price. Aucun écrivain aujourd'hui (E3) : null en pratique.",
"type": "object",
"properties": {
"provider": {
"type": [
"string",
"null"
]
},
"current": {
"$ref": "#/components/schemas/TariffEntry"
},
"next": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TariffNextEntry"
}
}
},
"required": [
"provider",
"current",
"next"
],
"additionalProperties": false
},
"Correction": {
"type": "string",
"enum": [
"temperature",
"ac-limit"
]
},
"PvField": {
"type": "object",
"properties": {
"id": {
"description": "ThingId du plan.",
"type": "string"
},
"corrections": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Correction"
}
}
},
"required": [
"id",
"corrections"
],
"additionalProperties": false
},
"PvForecast": {
"type": "object",
"properties": {
"p50W": {
"description": "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).",
"type": "array",
"items": {
"type": [
"number",
"null"
]
}
},
"conversion": {
"description": "Seule valeur émise par le moteur GPL ; un consommateur ignore une valeur inconnue.",
"type": "string",
"enum": [
"physics"
]
},
"corrections": {
"description": "[] = conversion brute.",
"type": "array",
"items": {
"$ref": "#/components/schemas/Correction"
}
},
"weatherModel": {
"description": "Modèle qui a RÉPONDU ; null si inconnu.",
"type": [
"string",
"null"
]
},
"issuedAt": {
"type": [
"string",
"null"
],
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2}(\\.[0-9]+)?)?(Z|[+-][0-9]{2}:[0-9]{2})$",
"description": "Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée."
},
"contentChangedAt": {
"type": [
"string",
"null"
],
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2}(\\.[0-9]+)?)?(Z|[+-][0-9]{2}:[0-9]{2})$",
"description": "Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée."
},
"acLimitW": {
"description": "Plafond AC du site (W), s'il est configuré.",
"type": "integer"
},
"perField": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PvField"
}
}
},
"required": [
"p50W",
"conversion",
"corrections",
"weatherModel",
"issuedAt",
"contentChangedAt"
],
"additionalProperties": false
},
"BaseloadLoadQuality": {
"type": "object",
"properties": {
"loadId": {
"type": "string"
},
"source": {
"type": "string",
"enum": [
"meter",
"device",
"none"
]
},
"minutesBySource": {
"type": "object",
"propertyNames": {
"enum": [
"meter",
"device",
"none"
]
},
"additionalProperties": {
"type": "integer"
}
},
"constantMeasurement": {
"type": "boolean"
}
},
"required": [
"loadId",
"source",
"minutesBySource"
],
"additionalProperties": false
},
"BaseloadQuality": {
"type": "object",
"properties": {
"windowDays": {
"type": "integer"
},
"minDays": {
"type": "integer"
},
"resolutionS": {
"type": "integer"
},
"daysCovered": {
"type": "object",
"properties": {
"workday": {
"type": "integer"
},
"weekend": {
"type": "integer"
}
},
"required": [
"workday",
"weekend"
],
"additionalProperties": false
},
"minutes": {
"type": "integer"
},
"commandedMinutes": {
"type": "integer"
},
"negativeMinutes": {
"type": "integer"
},
"incompleteMinutes": {
"type": "integer"
},
"computedAt": {
"type": "string",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2}(\\.[0-9]+)?)?(Z|[+-][0-9]{2}:[0-9]{2})$",
"description": "Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée."
},
"loads": {
"type": "array",
"items": {
"$ref": "#/components/schemas/BaseloadLoadQuality"
}
}
},
"required": [
"windowDays",
"minDays",
"resolutionS",
"daysCovered",
"minutes",
"commandedMinutes",
"negativeMinutes",
"incompleteMinutes",
"loads"
],
"additionalProperties": false
},
"Baseload": {
"type": "object",
"properties": {
"model": {
"description": "Le moteur GPL émet profile ou none (learned n'est pas émis).",
"type": "string",
"enum": [
"profile",
"none"
]
},
"p50W": {
"type": "array",
"items": {
"type": [
"number",
"null"
]
}
},
"quality": {
"$ref": "#/components/schemas/BaseloadQuality"
}
},
"required": [
"model",
"p50W"
],
"additionalProperties": false
},
"Price": {
"type": "object",
"properties": {
"currency": {
"const": "EUR"
},
"importCtkWh": {
"type": "array",
"items": {
"type": [
"number",
"null"
]
}
},
"exportCtkWh": {
"type": "array",
"items": {
"type": [
"number",
"null"
]
}
},
"includes": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PriceInclude"
}
},
"source": {
"description": "Tarif SAISI : une zone (fixed) ou des zones horaires (tou). tempo et day-ahead ne sont pas émis en 1.1.",
"type": "string",
"enum": [
"fixed",
"tou"
]
},
"knownUntil": {
"type": "string",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2}(\\.[0-9]+)?)?(Z|[+-][0-9]{2}:[0-9]{2})$",
"description": "Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée."
}
},
"required": [
"currency",
"importCtkWh",
"exportCtkWh",
"includes",
"source",
"knownUntil"
],
"additionalProperties": false
},
"Forecast": {
"description": "Prévisions sur UNE grille (§5.2) ; tableaux alignés index par index.",
"type": "object",
"properties": {
"start": {
"type": "string",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2}(\\.[0-9]+)?)?(Z|[+-][0-9]{2}:[0-9]{2})$",
"description": "Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée."
},
"resolutionS": {
"description": "Pas de la grille (s) — fait foi, jamais supposé.",
"type": "integer",
"minimum": 1
},
"steps": {
"description": "Nombre de cases ; toutes les séries ont cette longueur.",
"type": "integer",
"minimum": 0
},
"pv": {
"description": "null sans courbe (site non désigné, charge utile refusée, canal muet).",
"anyOf": [
{
"$ref": "#/components/schemas/PvForecast"
},
{
"type": "null"
}
]
},
"baseload": {
"$ref": "#/components/schemas/Baseload"
},
"weather": {
"description": "Aucune source de température en 1.1 : toujours null.",
"type": "null"
},
"price": {
"description": "null sans tarif saisi.",
"anyOf": [
{
"$ref": "#/components/schemas/Price"
},
{
"type": "null"
}
]
}
},
"required": [
"start",
"resolutionS",
"steps",
"pv",
"baseload",
"weather",
"price"
],
"additionalProperties": false
},
"Measurement": {
"oneOf": [
{
"description": "Rien ne mesure : état positif, pas une erreur.",
"type": "object",
"properties": {
"source": {
"const": "none"
}
},
"required": [
"source"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"source": {
"type": "string",
"enum": [
"meter",
"device"
]
},
"powerW": {
"type": "number"
}
},
"required": [
"source",
"powerW"
],
"additionalProperties": false
}
],
"description": "Mesure par charge (§5.3, LM-1105/1106) : décider sur source, jamais sur adapter."
},
"EnergyTarget": {
"type": "object",
"properties": {
"by": {
"type": "string",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2}(\\.[0-9]+)?)?(Z|[+-][0-9]{2}:[0-9]{2})$",
"description": "Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée."
},
"minWh": {
"description": "Énergie minimale à avoir délivrée à l'échéance.",
"type": "number",
"exclusiveMinimum": 0
},
"source": {
"description": "learned n'est pas émis en 1.1.",
"type": "string",
"enum": [
"installer",
"user-intent"
]
}
},
"required": [
"by",
"minWh",
"source"
],
"additionalProperties": false
},
"LoadNeeds": {
"description": "Besoins (§5.3). Champs 1.0 émis tels quels quand ils sont déclarés ; un optimiseur 1.1 préfère energyTargets.",
"type": "object",
"properties": {
"dailyDeadline": {
"description": "Heure murale locale HH:MM (1.0).",
"type": "string",
"pattern": "^[0-9]{2}:[0-9]{2}$"
},
"minEnergyWhPerDay": {
"type": "integer",
"exclusiveMinimum": 0
},
"targetSocPercent": {
"type": "integer",
"exclusiveMinimum": 0
},
"deadline": {
"type": "string",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2}(\\.[0-9]+)?)?(Z|[+-][0-9]{2}:[0-9]{2})$",
"description": "Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée."
},
"energyTargets": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/components/schemas/EnergyTarget"
}
}
},
"required": [],
"additionalProperties": false
},
"LoadLearned": {
"description": "Absent tant qu'aucun profil n'a été reçu (E12).",
"type": "object",
"properties": {
"dailyEnergyWh": {
"type": "number"
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
}
},
"required": [
"dailyEnergyWh",
"confidence"
],
"additionalProperties": false
},
"LoadWatts": {
"description": "Charge pilotée en watts : combinaison de relais (relay-router) ou consigne continue (etmvariableload).",
"type": "object",
"properties": {
"id": {
"description": "Identifiant de la charge — clé des actions.",
"type": "string",
"minLength": 1
},
"label": {
"description": "Nom lisible.",
"type": "string"
},
"priority": {
"description": "Rang effectif (1 = premier servi).",
"type": "integer"
},
"domain": {
"$ref": "#/components/schemas/Domain"
},
"learned": {
"$ref": "#/components/schemas/LoadLearned"
},
"needs": {
"$ref": "#/components/schemas/LoadNeeds"
},
"adapter": {
"type": "string",
"enum": [
"relay-router",
"etmvariableload"
]
},
"declared": {
"type": "object",
"properties": {
"minPowerW": {
"description": "0 = aucun plancher.",
"type": "number"
},
"maxPowerW": {
"type": "number"
}
},
"required": [
"minPowerW",
"maxPowerW"
],
"additionalProperties": false
},
"telemetry": {
"type": "object",
"properties": {
"currentPowerW": {
"description": "Puissance courante (W) telle que l'adaptateur la publie.",
"type": "number"
},
"available": {
"description": "Gouverne l'ALLOCATION, pas la COMPTABILITÉ (§5.3).",
"type": "boolean"
},
"measurement": {
"$ref": "#/components/schemas/Measurement"
},
"lockMinPowerW": {
"description": "Plancher imposé MAINTENANT par un verrou.",
"type": "number"
},
"lockMaxPowerW": {
"description": "Plafond imposé maintenant ; négatif = aucun plafond.",
"type": "number"
}
},
"required": [
"currentPowerW",
"available",
"lockMinPowerW",
"lockMaxPowerW"
],
"additionalProperties": false
},
"limits": {
"type": "object",
"properties": {
"minOnS": {
"type": "integer"
},
"minOffS": {
"type": "integer"
}
},
"required": [
"minOnS",
"minOffS"
],
"additionalProperties": false
}
},
"required": [
"id",
"adapter",
"label",
"priority",
"declared",
"telemetry",
"limits"
],
"additionalProperties": false
},
"LoadEvCharger": {
"description": "Borne. Une borne CONFIGURÉE dont l'appareil manque n'a ni declared, ni limits, ni pluggedIn/charging : ses zéros diraient « inconnu ».",
"type": "object",
"properties": {
"id": {
"description": "Identifiant de la charge — clé des actions.",
"type": "string",
"minLength": 1
},
"label": {
"description": "Nom lisible.",
"type": "string"
},
"priority": {
"description": "Rang effectif (1 = premier servi).",
"type": "integer"
},
"domain": {
"$ref": "#/components/schemas/Domain"
},
"learned": {
"$ref": "#/components/schemas/LoadLearned"
},
"needs": {
"$ref": "#/components/schemas/LoadNeeds"
},
"adapter": {
"const": "evcharger"
},
"declared": {
"type": "object",
"properties": {
"minA": {
"type": "number"
},
"maxA": {
"type": "number"
},
"phases": {
"description": "Phases VISÉES ce cycle, pas celles de la plaque (E26).",
"type": "integer"
}
},
"required": [
"minA",
"maxA",
"phases"
],
"additionalProperties": false
},
"telemetry": {
"type": "object",
"properties": {
"currentPowerW": {
"description": "Puissance courante (W) telle que l'adaptateur la publie.",
"type": "number"
},
"available": {
"description": "Gouverne l'ALLOCATION, pas la COMPTABILITÉ (§5.3).",
"type": "boolean"
},
"measurement": {
"$ref": "#/components/schemas/Measurement"
},
"pluggedIn": {
"type": "boolean"
},
"charging": {
"type": "boolean"
},
"sessionWh": {
"description": "Absent si la borne ne compte pas sa session (LM-1009 C1).",
"type": "number"
}
},
"required": [
"currentPowerW",
"available"
],
"additionalProperties": false
},
"limits": {
"type": "object",
"properties": {
"chargingEnabledLockS": {
"type": "integer"
},
"currentChangeLockS": {
"type": "integer"
}
},
"required": [
"chargingEnabledLockS",
"currentChangeLockS"
],
"additionalProperties": false
}
},
"required": [
"id",
"adapter",
"label",
"priority",
"telemetry"
],
"additionalProperties": false
},
"LoadSgReady": {
"description": "PAC SG-Ready : 4 états normés, kind state.",
"type": "object",
"properties": {
"id": {
"description": "Identifiant de la charge — clé des actions.",
"type": "string",
"minLength": 1
},
"label": {
"description": "Nom lisible.",
"type": "string"
},
"priority": {
"description": "Rang effectif (1 = premier servi).",
"type": "integer"
},
"domain": {
"$ref": "#/components/schemas/Domain"
},
"learned": {
"$ref": "#/components/schemas/LoadLearned"
},
"needs": {
"$ref": "#/components/schemas/LoadNeeds"
},
"adapter": {
"const": "sg-ready"
},
"declared": {
"type": "object",
"properties": {
"states": {
"type": "array",
"items": {
"type": "integer",
"minimum": 1,
"maximum": 4
}
},
"estimatedPowerW": {
"type": "object",
"propertyNames": {
"pattern": "^[1-4]$"
},
"additionalProperties": {
"type": "number"
}
}
},
"required": [
"states",
"estimatedPowerW"
],
"additionalProperties": false
},
"telemetry": {
"type": "object",
"properties": {
"currentPowerW": {
"description": "Puissance courante (W) telle que l'adaptateur la publie.",
"type": "number"
},
"available": {
"description": "Gouverne l'ALLOCATION, pas la COMPTABILITÉ (§5.3).",
"type": "boolean"
},
"measurement": {
"$ref": "#/components/schemas/Measurement"
},
"state": {
"description": "État commandé (1-4) ; 0 = non applicable.",
"type": "integer",
"minimum": 0,
"maximum": 4
},
"minState": {
"description": "Fenêtre d'états autorisée MAINTENANT par minStateHold.",
"type": "integer"
},
"maxState": {
"type": "integer"
}
},
"required": [
"currentPowerW",
"available",
"state",
"minState",
"maxState"
],
"additionalProperties": false
},
"limits": {
"type": "object",
"properties": {
"minStateHoldS": {
"type": "integer"
}
},
"required": [
"minStateHoldS"
],
"additionalProperties": false
}
},
"required": [
"id",
"adapter",
"label",
"priority",
"declared",
"telemetry",
"limits"
],
"additionalProperties": false
},
"Load": {
"oneOf": [
{
"$ref": "#/components/schemas/LoadWatts"
},
{
"$ref": "#/components/schemas/LoadEvCharger"
},
{
"$ref": "#/components/schemas/LoadSgReady"
}
],
"description": "Une charge arbitrée (§5.3) — la forme dépend du mécanisme (adapter)."
},
"ReasonParams": {
"description": "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": [
"object",
"null"
],
"additionalProperties": {
"type": [
"number",
"boolean",
"string"
],
"pattern": "^[A-Za-z]+$"
}
},
"SetpointAction": {
"type": "object",
"properties": {
"loadId": {
"description": "Charge visée : connue du contexte (règle unknown-load), une fois par créneau (duplicate-load).",
"type": "string",
"minLength": 1
},
"reason": {
"description": "Texte français, OBLIGATOIRE et non vide (règle missing-reason). Traces et journal seulement, jamais vers l'app.",
"type": "string",
"minLength": 1,
"pattern": "\\S"
},
"reasonCode": {
"$ref": "#/components/schemas/ReasonCode"
},
"reasonParams": {
"$ref": "#/components/schemas/ReasonParams"
},
"force": {
"description": "INTERDIT sur le fil (§6) : ignoré, signalé une fois."
},
"kind": {
"const": "setpoint"
},
"powerW": {
"description": "Consigne en watts (relay-router, etmvariableload ; ou borne).",
"type": "number",
"minimum": 0
},
"currentA": {
"description": "Borne : courant (0 = suspendre).",
"type": "number",
"minimum": 0
},
"phases": {
"description": "Borne : phases visées ; absent = inchangé.",
"enum": [
1,
3
]
}
},
"required": [
"loadId",
"kind",
"reason"
],
"additionalProperties": true,
"anyOf": [
{
"required": [
"powerW"
]
},
{
"required": [
"currentA"
]
}
]
},
"StateAction": {
"type": "object",
"properties": {
"loadId": {
"description": "Charge visée : connue du contexte (règle unknown-load), une fois par créneau (duplicate-load).",
"type": "string",
"minLength": 1
},
"reason": {
"description": "Texte français, OBLIGATOIRE et non vide (règle missing-reason). Traces et journal seulement, jamais vers l'app.",
"type": "string",
"minLength": 1,
"pattern": "\\S"
},
"reasonCode": {
"$ref": "#/components/schemas/ReasonCode"
},
"reasonParams": {
"$ref": "#/components/schemas/ReasonParams"
},
"force": {
"description": "INTERDIT sur le fil (§6) : ignoré, signalé une fois."
},
"kind": {
"const": "state"
},
"state": {
"description": "1 = verrouillage, 2 = normal, 3 = recommandation, 4 = forcé.",
"type": "integer",
"minimum": 1,
"maximum": 4
}
},
"required": [
"loadId",
"kind",
"reason",
"state"
],
"additionalProperties": true
},
"ConstraintAction": {
"description": "RÉSERVÉ (annexe F) : forme décrite, toujours refusée (kind-mismatch).",
"type": "object",
"properties": {
"loadId": {
"description": "Charge visée : connue du contexte (règle unknown-load), une fois par créneau (duplicate-load).",
"type": "string",
"minLength": 1
},
"reason": {
"description": "Texte français, OBLIGATOIRE et non vide (règle missing-reason). Traces et journal seulement, jamais vers l'app.",
"type": "string",
"minLength": 1,
"pattern": "\\S"
},
"reasonCode": {
"$ref": "#/components/schemas/ReasonCode"
},
"reasonParams": {
"$ref": "#/components/schemas/ReasonParams"
},
"force": {
"description": "INTERDIT sur le fil (§6) : ignoré, signalé une fois."
},
"kind": {
"const": "constraint"
},
"charge": {
"enum": [
"allow",
"forbid"
]
},
"discharge": {
"enum": [
"allow",
"forbid"
]
}
},
"required": [
"loadId",
"kind",
"reason"
],
"additionalProperties": true
},
"Action": {
"oneOf": [
{
"$ref": "#/components/schemas/SetpointAction"
},
{
"$ref": "#/components/schemas/StateAction"
},
{
"$ref": "#/components/schemas/ConstraintAction"
}
]
},
"Expected": {
"description": "Trajectoire prévue : affichage et mesure d'écart, JAMAIS pour commander.",
"type": "object",
"properties": {
"gridImportW": {
"type": "number"
},
"gridExportW": {
"type": "number"
},
"batterySocPercent": {
"description": "RÉSERVÉ (annexe F) : ignoré."
}
},
"required": [],
"additionalProperties": true
},
"Slot": {
"type": "object",
"properties": {
"from": {
"description": "Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée.",
"type": "string",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2}(\\.[0-9]+)?)?(Z|[+-][0-9]{2}:[0-9]{2})$"
},
"to": {
"description": "Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée.",
"type": "string",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2}(\\.[0-9]+)?)?(Z|[+-][0-9]{2}:[0-9]{2})$"
},
"actions": {
"description": "Absent ou null = aucune action.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/components/schemas/Action"
}
},
"expected": {
"$ref": "#/components/schemas/Expected"
}
},
"required": [
"from",
"to"
],
"additionalProperties": true
},
"Diagnostics": {
"description": "Informatif : mode installateur et traces.",
"type": "object",
"properties": {
"solver": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"optimal",
"feasible",
"best-so-far",
"infeasible-relaxed"
]
},
"gapPercent": {
"type": "number"
},
"computeMs": {
"type": "number",
"minimum": 0
},
"inputsUsed": {
"type": "object",
"properties": {
"pv": {
"type": "string",
"enum": [
"physics",
"hybrid",
"none"
]
}
},
"required": [],
"additionalProperties": true
}
},
"required": [],
"additionalProperties": true
},
"Plan": {
"description": "Plan par créneaux (§6).",
"type": "object",
"properties": {
"abstain": {
"const": false
},
"planId": {
"type": "string"
},
"strategy": {
"description": "Chaîne libre, informative.",
"type": "string"
},
"computedAt": {
"type": "string",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2}(\\.[0-9]+)?)?(Z|[+-][0-9]{2}:[0-9]{2})$",
"description": "Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée."
},
"validUntil": {
"description": "Instant ISO 8601 AVEC son décalage (Z ou ±hh:mm) — une heure murale nue est refusée.",
"type": [
"string",
"null"
],
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2}(\\.[0-9]+)?)?(Z|[+-][0-9]{2}:[0-9]{2})$"
},
"slots": {
"description": "[] = abstention (empty-plan). Seul le créneau couvrant maintenant est exécuté.",
"type": "array",
"items": {
"$ref": "#/components/schemas/Slot"
}
},
"diagnostics": {
"$ref": "#/components/schemas/Diagnostics"
}
},
"required": [
"slots"
],
"additionalProperties": true
},
"Abstention": {
"description": "« Pas de plan maintenant » (§7) : repli pour ce cycle, pas un échec.",
"type": "object",
"properties": {
"abstain": {
"const": true
},
"reason": {
"type": "string"
}
},
"required": [
"abstain"
],
"additionalProperties": true
},
"GetPlanResult": {
"oneOf": [
{
"$ref": "#/components/schemas/Abstention"
},
{
"$ref": "#/components/schemas/Plan"
}
]
}
}
},
"x-etm-transport": {
"description": "Ce qu'OpenRPC ne dit pas du fil (§2-§4).",
"schemes": [
"unix",
"tcp"
],
"defaultEndpoint": "unix:///run/powersync/optimizer.sock",
"jsonrpc": "2.0",
"framing": "ndjson",
"encoding": "utf-8",
"roles": {
"client": "engine",
"server": "optimizer"
},
"maxLineBytes": 1048576,
"handshakeTimeoutS": 5,
"requestTimeoutS": 10,
"backoffS": {
"initial": 1,
"max": 60,
"factor": 2
},
"requestsInFlight": 1,
"ids": "entiers croissants sur toute la vie du client, à travers les reconnexions",
"lineErrors": "ligne non JSON, sans id, id jamais émis, ligne > maxLineBytes : comptée, connexion CONSERVÉE",
"tokenMasking": "toute occurrence du token dans un message reçu est remplacée par « [token masqué] » dès l'analyse"
},
"x-etm-fallback-machine": {
"description": "Machine de repli §7 (OptimizerStatus) : heartbeat implicite — une réponse valide à GetPlan = optimiseur vivant. Les échecs se comptent PAR REQUÊTE.",
"failuresBeforeDead": 2,
"rows": [
{
"situation": "réponse plan valide",
"optimizerAlive": "true",
"activeStrategy": "optimizer"
},
{
"situation": "abstain, plan vide, aucun créneau courant",
"optimizerAlive": "inchangé",
"activeStrategy": "rules (ce cycle)"
},
{
"situation": "plan expiré (validUntil dépassé)",
"optimizerAlive": "inchangé",
"activeStrategy": "rules jusqu'au prochain plan valide"
},
{
"situation": "timeout / erreur / plan invalide sur deux requêtes consécutives",
"optimizerAlive": "false",
"activeStrategy": "rules"
},
{
"situation": "socket absent / fermé / handshake refusé",
"optimizerAlive": "false",
"activeStrategy": "rules"
},
{
"situation": "aucun endpoint configuré",
"optimizerAlive": "false",
"activeStrategy": "rules"
},
{
"situation": "mode dégradé L2 (compteur muet > 90 s)",
"optimizerAlive": "inchangé",
"activeStrategy": "none"
}
]
},
"x-etm-semantic-rules": {
"description": "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).",
"rules": [
{
"name": "abstain",
"method": "GetPlan",
"issue": "abstention",
"fallbackCode": "abstain",
"countsAsFailure": false,
"needs": [],
"schemaAlsoCatches": false,
"description": "result.abstain vaut true."
},
{
"name": "malformed-plan",
"method": "GetPlan",
"issue": "invalid",
"fallbackCode": "malformed-plan",
"countsAsFailure": true,
"needs": [],
"schemaAlsoCatches": true,
"description": "slots absent ou non liste ; un créneau qui n'est pas un objet ; actions non liste.",
"lastErrorCode": "malformed-plan"
},
{
"name": "empty-plan",
"method": "GetPlan",
"issue": "abstention",
"fallbackCode": "empty-plan",
"countsAsFailure": false,
"needs": [],
"schemaAlsoCatches": false,
"description": "slots est une liste vide."
},
{
"name": "bad-time",
"method": "GetPlan",
"issue": "invalid",
"fallbackCode": "bad-time",
"countsAsFailure": true,
"needs": [],
"schemaAlsoCatches": true,
"description": "from/to illisibles ou sans décalage, ou from ≥ to ; validUntil illisible ou sans décalage.",
"lastErrorCode": "bad-time"
},
{
"name": "slots-unordered",
"method": "GetPlan",
"issue": "invalid",
"fallbackCode": "slots-unordered",
"countsAsFailure": true,
"needs": [],
"schemaAlsoCatches": false,
"description": "un créneau commence avant la fin du précédent.",
"lastErrorCode": "slots-unordered"
},
{
"name": "malformed-action",
"method": "GetPlan",
"issue": "invalid",
"fallbackCode": "malformed-action",
"countsAsFailure": true,
"needs": [],
"schemaAlsoCatches": true,
"description": "action qui n'est pas un objet, ou sans loadId (chaîne non vide).",
"lastErrorCode": "malformed-action"
},
{
"name": "unknown-load",
"method": "GetPlan",
"issue": "invalid",
"fallbackCode": "unknown-load",
"countsAsFailure": true,
"needs": [
"request"
],
"schemaAlsoCatches": false,
"description": "loadId absent des loads[] de la requête.",
"lastErrorCode": "unknown-load"
},
{
"name": "unknown-kind",
"method": "GetPlan",
"issue": "invalid",
"fallbackCode": "unknown-kind",
"countsAsFailure": true,
"needs": [],
"schemaAlsoCatches": true,
"description": "kind hors ActionKind.",
"lastErrorCode": "unknown-kind"
},
{
"name": "kind-mismatch",
"method": "GetPlan",
"issue": "invalid",
"fallbackCode": "kind-mismatch",
"countsAsFailure": true,
"needs": [
"request"
],
"schemaAlsoCatches": false,
"description": "kind différent de celui du mécanisme de la charge (setpoint : evcharger, relay-router, etmvariableload ; state : sg-ready ; constraint : aucun).",
"lastErrorCode": "kind-mismatch"
},
{
"name": "kind-not-announced",
"method": "GetPlan",
"issue": "invalid",
"fallbackCode": "kind-not-announced",
"countsAsFailure": true,
"needs": [
"handshake"
],
"schemaAlsoCatches": false,
"description": "capabilities.kinds annoncé au handshake et kind absent de la liste.",
"lastErrorCode": "kind-not-announced"
},
{
"name": "missing-reason",
"method": "GetPlan",
"issue": "invalid",
"fallbackCode": "missing-reason",
"countsAsFailure": true,
"needs": [],
"schemaAlsoCatches": true,
"description": "reason absent, non textuel, ou vide une fois les caractères de contrôle retirés.",
"lastErrorCode": "missing-reason"
},
{
"name": "bad-value",
"method": "GetPlan",
"issue": "invalid",
"fallbackCode": "bad-value",
"countsAsFailure": true,
"needs": [
"request"
],
"schemaAlsoCatches": true,
"description": "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.",
"lastErrorCode": "bad-value"
},
{
"name": "duplicate-load",
"method": "GetPlan",
"issue": "invalid",
"fallbackCode": "duplicate-load",
"countsAsFailure": true,
"needs": [],
"schemaAlsoCatches": false,
"description": "deux actions pour la même charge dans un créneau.",
"lastErrorCode": "duplicate-load"
},
{
"name": "expired",
"method": "GetPlan",
"issue": "expired",
"fallbackCode": "expired",
"countsAsFailure": false,
"needs": [
"request"
],
"schemaAlsoCatches": false,
"description": "validUntil ≤ timestamp de la requête."
},
{
"name": "no-current-slot",
"method": "GetPlan",
"issue": "abstention",
"fallbackCode": "no-current-slot",
"countsAsFailure": false,
"needs": [
"request"
],
"schemaAlsoCatches": false,
"description": "aucun créneau ne couvre le timestamp de la requête."
},
{
"name": "incomplete-slot",
"method": "GetPlan",
"issue": "invalid",
"fallbackCode": "incomplete-slot",
"countsAsFailure": true,
"needs": [
"request"
],
"schemaAlsoCatches": false,
"description": "une charge available de la requête n'a pas d'action dans le créneau courant (décision Q1).",
"lastErrorCode": "incomplete-slot"
},
{
"name": "handshake-refused",
"method": "Handshake",
"issue": "connection-refused",
"fallbackCode": "no-plan",
"lastErrorCode": "handshake-refused",
"countsAsFailure": false,
"needs": [],
"schemaAlsoCatches": false,
"description": "réponse error (token absent ou différent : -32001 recommandé) → connexion fermée, nouvelle tentative au backoff."
},
{
"name": "invalid-handshake",
"method": "Handshake",
"issue": "connection-refused",
"fallbackCode": "no-plan",
"lastErrorCode": "invalid-handshake",
"countsAsFailure": false,
"needs": [],
"schemaAlsoCatches": true,
"description": "result non objet, protocolVersion absent ou vide, capabilities présent et non objet."
},
{
"name": "major-mismatch",
"method": "Handshake",
"issue": "connection-refused",
"fallbackCode": "no-plan",
"lastErrorCode": "major-mismatch",
"countsAsFailure": false,
"needs": [],
"schemaAlsoCatches": false,
"description": "MAJEURE de protocolVersion (ce qui précède le premier point) différente de celle du moteur."
}
]
},
"x-etm-tolerances": {
"description": "Ce que le moteur ACCEPTE au-delà du schéma — un optimiseur conforme ne s'en sert pas.",
"items": [
"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)."
]
},
"x-etm-reason-params": {
"BELOW_MIN_POWER": [
"budgetW",
"minPowerW",
"state"
],
"ECO_FLOOR_GRID": [
"gridW",
"atMaximum",
"remainingWh",
"targetWh",
"remainingS",
"regime",
"minAlwaysW",
"boundBy"
],
"EV_DEADLINE": [],
"OPTIMIZER_DECISION": [],
"PRICE_SLOT": [
"priceCtkWh",
"setpointW"
],
"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.",
"items": [
{
"pointer": "/info/version",
"from": "OptimizerProtocol::ProtocolVersion"
},
{
"pointer": "/components/schemas/ProtocolVersion/const",
"from": "OptimizerProtocol::ProtocolVersion"
},
{
"pointer": "/components/schemas/Trigger/enum",
"from": "SocketScheduler::knownTriggers()"
},
{
"pointer": "/components/schemas/Domain/enum",
"from": "LoadConfig::knownDomains()"
},
{
"pointer": "/components/schemas/ActionKind/enum",
"from": "OptimizerPlan::kinds()"
},
{
"pointer": "/components/schemas/ReasonCode/enum",
"from": "DecisionCode::optimizerEmittableCodes()"
},
{
"pointer": "/x-etm-reason-params",
"from": "DecisionCode::optimizerParamKeys()"
},
{
"pointer": "/components/schemas/PriceInclude/enum",
"from": "TariffConfig::includes()"
},
{
"pointer": "/components/schemas/FallbackCode/enum",
"from": "SocketScheduler::knownFallbackCodes()"
},
{
"pointer": "/components/schemas/LastErrorCode/enum",
"from": "SocketScheduler::knownLastErrorCodes()"
}
]
}
}