Patrick Schurig 053263bc38 feat(rpc): NymeaEnergy.GetOptimizerProtocol sert le contrat embarqué
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>
2026-10-17 21:30:44 +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.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);
}