docs/OPTIMIZER_PROTOCOL.md passe en v1.2 (mineure) et écrit les décisions du 2026-10-21 à leur place : - §2 : maxLineBytes (1 Mio, '\n' non compté) dans les deux sens ; lots interdits dans les deux sens ; /run/powersync appartient au paquet de l'optimiseur, le moteur ne le crée ni ne le suppose. Écart connu noté : le moteur ne borne pas encore ce qu'il émet. - §3 : la majeure se compare des deux côtés (-32602 puis fermeture) ; -32001 normé ; un token non exigé est accepté ; un handshake refusé est fermé des deux côtés ; GetPlan avant handshake → -32600, connexion gardée. - §7 : contexte GetPlan invalide → -32602 avec chemins et natures, jamais les valeurs, compté comme un échec ; codes JSON-RPC 2.0 standard sinon ; table des erreurs GÉNÉRÉE depuis le contrat. - §8 : UpdateLearned à sens unique, -32601 avec id ; forme non normée en 1.2. - §9, §0 (changelog, compatibilité 1.1), annexes A, E, G. Contrat OpenRPC : methods[].errors complétés (x-etm-connection), x-etm-errors-engine, x-etm-message-errors (nouveau), x-etm-transport (maxLineBytesScope, batches, runtimeDirectory), règle handshake-refused. La version (info.version, ProtocolVersion) régénérée depuis le code par --enums-from. Moteur : OptimizerProtocol::ProtocolVersion et OptimizerClient::kProtocolVersion passent à "1.2" ; testHandshakeAndPlan vérifie aussi que les deux constantes sont égales. Aucun autre comportement ne change. Tests suivis : version attendue dans testoptimizerclient et testsocketscheduler, handshake de la session de schema-check-test, et un test du bloc « errors » dans gen-protocol-doc-test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
5.1 KiB
C++
109 lines
5.1 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||
#pragma once
|
||
|
||
#include <QHash>
|
||
#include <QList>
|
||
#include <QString>
|
||
#include <QVariantMap>
|
||
#include <optional>
|
||
#include "../types/surpluscontext.h"
|
||
|
||
/*!
|
||
* \file optimizerprotocol.h
|
||
* \brief Sérialisation \c SurplusContext → paramètres de \c GetPlan (OPTIMIZER_PROTOCOL.md 1.1,
|
||
* §5.1 à §5.3). Fonction PURE : aucune E/S, aucune horloge, aucun accès à l'arbitre.
|
||
*
|
||
* \par Ce qui n'est pas dans \c SurplusContext, et pourquoi
|
||
* La prévision n'y entre pas (DESIGN_PREVISION_PV.md §11 : le rule-based ne doit pas pouvoir
|
||
* la lire). Elle voyage À CÔTÉ, dans \c ContexteFil, avec ce que l'arbitre sait et que le
|
||
* contexte de décision ne porte pas : déclencheur, compteur racine, domaine et mesure par charge.
|
||
*
|
||
* \par Inconnu = \c null ou absent, JAMAIS 0
|
||
* Un champ que rien ne remplit (puissance souscrite, PV instantanée, capacité batterie, tarif)
|
||
* est émis \c null ; un champ que le protocole rend optionnel et que rien ne porte (\c learned,
|
||
* \c comfort, \c group, champs réservés de l'annexe F) est ABSENT. Un zéro affirmerait une
|
||
* mesure ou un profil qui n'existent pas.
|
||
*/
|
||
namespace OptimizerProtocol {
|
||
|
||
//! Version du protocole parlée par ce sérialiseur (§3, §9).
|
||
constexpr const char *ProtocolVersion = "1.2";
|
||
|
||
//! Ressource Qt du contrat OpenRPC embarqué (\c docs/etm-powersync-optimizer-protocol.openrpc.json).
|
||
constexpr const char *SchemaResource = ":/etm-powersync/optimizer-protocol.openrpc.json";
|
||
|
||
/*!
|
||
* \brief Le contrat OpenRPC du protocole, tel qu'EMBARQUÉ dans ce binaire.
|
||
* \return Les octets de la ressource \c SchemaResource — ceux de \c docs/ au moment du build ;
|
||
* vide si la ressource manque (build cassé). Jamais lu sur disque à l'exécution.
|
||
*/
|
||
QByteArray schemaDocumentBytes();
|
||
|
||
/*!
|
||
* \brief Le contrat OpenRPC analysé, pour \c NymeaEnergy.GetOptimizerProtocol.
|
||
* \return L'objet JSON du document ; vide si la ressource manque ou ne s'analyse pas (dit au
|
||
* journal, en avertissement, à chaque appel — un contrat qui ne se sert pas ne doit pas se taire).
|
||
*/
|
||
QVariantMap schemaDocument();
|
||
|
||
//! Mesure d'une charge, telle que la résout l'arbitre (\c EnergyArbitrator::mesurerCharge).
|
||
struct MesureFil {
|
||
QString source = QStringLiteral("none"); //!< \c meter | \c device | \c none
|
||
double powerW = 0; //!< Sans objet, et non émis, sous \c none.
|
||
};
|
||
|
||
/*!
|
||
* \brief Ce que la requête porte EN PLUS du contexte de décision.
|
||
*
|
||
* Rempli par l'appelant (le futur \c SocketScheduler), jamais par le rule-based. Tout champ
|
||
* laissé à sa valeur par défaut se sérialise comme INCONNU.
|
||
*/
|
||
struct ContexteFil {
|
||
//! Cause de la requête (§5.1) : \c cycle | \c load-change | \c intent | \c config | \c forecast.
|
||
QString trigger = QStringLiteral("cycle");
|
||
//! ThingId du compteur racine (\c meter.source) ; vide = inconnu → \c null.
|
||
QString meterSource;
|
||
//! Production PV instantanée (\c pv.currentW) ; absente = inconnue → \c null. Remplie dès
|
||
//! qu'un Thing \c smartmeterproducer existe (onduleur ou compteur de production). Le champ
|
||
//! \c SurplusContext::pv n'est pas lu : il n'est jamais rempli, et son 0 ne distingue pas
|
||
//! « nuit » de « pas de producteur ».
|
||
std::optional<double> pvCurrentW;
|
||
//! Limite par phase en A (\c site.phaseLimitA) ; vide = celle du contexte, sinon \c null.
|
||
QList<double> phaseLimitA;
|
||
//! Réserve de stockage en % (\c battery.reserveSocPercent) ; absente = celle du contexte si
|
||
//! elle est renseignée (> 0), sinon \c null.
|
||
std::optional<int> reserveSocPercent;
|
||
//! Domaine déclaré par l'installateur, par charge (\c loads[].domain) ; absent = non émis.
|
||
QHash<QString, QString> domaines;
|
||
//! Mesure par charge (\c loads[].telemetry.measurement) ; absente = non émise.
|
||
QHash<QString, MesureFil> mesures;
|
||
/*!
|
||
* \brief L'instantané de prévision ASSEMBLÉ par \c EnergyArbitrator::instantanePrevision()
|
||
* — la charge utile de \c GetForecastSnapshot, un seul assemblage pour la RPC et le fil.
|
||
* \c std::nullopt → \c forecast: null.
|
||
*/
|
||
std::optional<QVariantMap> prevision;
|
||
};
|
||
|
||
/*!
|
||
* \brief Les \c params d'une requête \c GetPlan.
|
||
* \param ctx Contexte du cycle (le même que celui que reçoit le rule-based).
|
||
* \param fil Ce que la requête porte en plus (voir \c ContexteFil).
|
||
* \return Un objet JSON prêt à sérialiser ; clés et sens de OPTIMIZER_PROTOCOL.md §5.
|
||
* \note \c meter.exportW est la mesure BRUTE du compteur (AGENTS règle 8).
|
||
*/
|
||
QVariantMap getPlanParams(const SurplusContext &ctx, const ContexteFil &fil);
|
||
|
||
/*!
|
||
* \brief Le bloc \c forecast (§5.2) depuis l'instantané de prévision.
|
||
* \param instantane Sortie de \c EnergyArbitrator::instantanePrevision().
|
||
* \return \c start, \c resolutionS, \c steps, \c pv (\c null sans courbe), \c baseload,
|
||
* \c weather (\c null : aucune source), \c price.
|
||
* \note La courbe interne est en Wh PAR PAS ; le fil porte une puissance moyenne :
|
||
* \c p50W = Wh × 3600 / \c resolutionS. Une case sans point reste \c null.
|
||
*/
|
||
QVariantMap forecastDepuisInstantane(const QVariantMap &instantane);
|
||
|
||
}
|