OptimizerProtocol::getPlanParams(SurplusContext, ContexteFil) rend les params d'une requête GetPlan. Fonction pure, compilée dans le plugin mais appelée par personne : sans endpoint, rien ne change. - La prévision voyage À CÔTÉ du contexte (§11) : ContexteFil::prevision porte la sortie de EnergyArbitrator::instantanePrevision(), l'assemblage partagé avec GetForecastSnapshot. forecastDepuisInstantane() la pose sur la grille §5.2 : Wh par pas → p50W moyens (Wh × 3600 / resolutionS), case sans point = null, pv null sans courbe, weather null. - ContexteFil porte aussi trigger (sa source viendra en T3), meter.source, la PV instantanée, la limite de phase, la réserve, le domaine et la mesure par charge. - Inconnu = null ou absent, jamais 0 : contractedPowerW, pv.currentW, capacité et puissances batterie, tarif → null ; learned, comfort, group, champs réservés, force, powerLevels → absents ; borne configurée sans appareil → ni déclaration ni verrous. - meter.exportW brut (règle 8). Champs 1.0 conservés ; energyTargets dérivés par resolveDeadline (installer) et de la dérogation à cible (user-intent). Tests (loadmodel, logique pure) : 4 goldens dans data/optimizer (site sans PV ; PV + ECS relay-router + borne + SG-Ready ; prévision absente ; charge available=false figée), régénérables sous ETM_REGENERER_GOLDEN=1, plus 3 tests d'assertion. Contre-épreuves : contractedPowerW à 0, Wh non convertis, exportW net → chacune fait échouer au moins un test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
91 lines
4.2 KiB
C++
91 lines
4.2 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";
|
||
|
||
//! 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. Le champ
|
||
//! \c SurplusContext::pv n'est pas lu : il n'est jamais rempli, et son 0 ne distingue pas
|
||
//! « nuit » de « pas d'onduleur ».
|
||
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);
|
||
|
||
}
|