powersync-docs/docs/optimiseur/etm-powersync-optimizer-protocol.openrpc.json
Patrick Schurig 209e2d441b docs(optimiseur): section Optimiseur et partie technique en trois sections (fr)
Navigation : « EMS — moteur à règles » (pages de fonctionnalités, précédées
de « Comment le moteur décide »), « Optimiseur » (Présentation, Protocole,
Contrat, Écrire un optimiseur), « API » (ex-« Référence »). Aucun fichier
existant déplacé : les URL publiées restent valides.

Protocole : prose courte qui renvoie à OPTIMIZER_PROTOCOL.md du moteur
pour le sens ; tableaux, listes fermées et règles nommées générés depuis
le contrat. Contrat servi tel quel (copie octet pour octet).
reference.md, resté un bouchon, devient l'accueil de la section API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 13:06:04 +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()"
}
]
}
}