Le contrat OpenRPC est une ressource Qt du plugin (jamais lu sur disque à
l'exécution) ; GetOptimizerProtocol rend { protocolVersion, schema }, lecture,
PermissionScopeControlThings, aucun secret, sans *Changed (statique pour un
binaire). Documenté dans INTERFACE.md.
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.1";
|
||
|
||
//! 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);
|
||
|
||
}
|