Patrick Schurig 8f24709874 protocole optimiseur 1.2 : les règles Q2 à Q9, et le moteur annonce 1.2
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>
2026-10-21 14:01:46 +02:00

109 lines
5.1 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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);
}