GetPvForecast sert la TABLE ; GetForecastSnapshot sert la DÉCISION. Un arbitre qui lit la table lirait sa propre trace au lieu de son entrée : il ne mesure plus, il se relit. Le snapshot lit le canal, directement, sans persistance. Écrit maintenant parce que la distinction est nette maintenant : dans six semaines, le premier consommateur prendrait GetPvForecast sans y penser. LES DEUX ABSENCES, TRANCHÉES. Aucun candidat : silence. Site désignable non désigné : MISSING_FORECAST_SITE. Charge utile refusée : FORECAST_PAYLOAD_REFUSED avec sa cause. Canal muet ou plans de runs différents : ni courbe ni motif. Dans tous les cas forecastPoints est ABSENT, jamais une liste de zéros — une prévision manquante et une prévision nulle sont deux affirmations différentes. Deux motifs, deux gestes : configurer, ou regarder le producteur. DATER LE CONTENU, PAS SEULEMENT LA LECTURE. capturedAt date la réponse et distingue « pas répondu » de « répondu, et rien ». Mais un producteur FIGÉ se lirait comme une prévision stable — le silence qui ressemble à un résultat, sous sa forme la plus coûteuse puisque celui qui le lit décide d'ACHETER. D'où contentChangedAt, qui vient du magasin non comme entrée mais parce qu'il SURVIT AU REDÉMARRAGE : en mémoire, il se serait remis à zéro à chaque relance, et un producteur figé aurait paru frais après le premier redémarrage venu. Omis quand le magasin n'a jamais vu ce contenu, plutôt que deviné. ET UN MÉCANISME ENTIER ÉTAIT EXCLU DU BANC D'ESSAI PAR UNE ACCOLADE. En écrivant le test de frontière, forecastStore() s'est révélé NUL : le magasin était créé dans un #ifndef ENERGY_SIMULATION, par accident de placement — ajouté à la suite du battement de télémétrie, qui lui est bien un déclencheur d'horloge murale. Toute la prévision était donc absente du binaire de test. Aucune suite ne pouvait l'exercer à travers l'arbitre, et le balayage des getters voyait GetPvForecast répondre correctement… par sa branche « magasin absent ». Un mécanisme entier hors du banc, tous les voyants au vert. Le test qui l'a trouvé ne le cherchait pas : il voulait vérifier une frontière et a buté sur l'absence de ce qu'il devait franchir. Le magasin sort de la garde — il n'est pas un déclencheur d'horloge murale. Sa purge, si, et elle y reste. loadmodel 31/31, charging 48/48, spotmarket 32/32, simulation 71/71, ci-quality 4/4, build 0 erreur 0 warning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
192 lines
9.8 KiB
C++
192 lines
9.8 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
*
|
|
* Copyright (C) 2013 - 2024, nymea GmbH
|
|
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
|
|
*
|
|
* This file is part of nymea-energy-plugin-nymea.
|
|
*
|
|
* nymea-energy-plugin-nymea.s free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* nymea-energy-plugin-nymea.s distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with nymea-energy-plugin-nymea. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
#ifndef NYMEAENERGYJSONHANDLER_H
|
|
#define NYMEAENERGYJSONHANDLER_H
|
|
|
|
#include <QObject>
|
|
|
|
#include <jsonrpc/jsonhandler.h>
|
|
|
|
#include "types/scoreentry.h"
|
|
#include "etm/ratios/energyratioscalculator.h"
|
|
|
|
class SmartChargingManager;
|
|
class SpotMarketManager;
|
|
class LoadConfigStore;
|
|
class LoadConfig;
|
|
class EnergyManager;
|
|
|
|
class NymeaEnergyJsonHandler : public JsonHandler
|
|
{
|
|
Q_OBJECT
|
|
public:
|
|
explicit NymeaEnergyJsonHandler(SpotMarketManager *spotMarketManager, SmartChargingManager *smartChargingManager, LoadConfigStore *loadConfigStore = nullptr, EnergyManager *energyManager = nullptr, QObject *parent = nullptr);
|
|
|
|
QString name() const override;
|
|
|
|
Q_INVOKABLE JsonReply *GetPhasePowerLimit(const QVariantMap ¶ms);
|
|
Q_INVOKABLE JsonReply *SetPhasePowerLimit(const QVariantMap ¶ms);
|
|
|
|
Q_INVOKABLE JsonReply *GetAcquisitionTolerance(const QVariantMap ¶ms);
|
|
Q_INVOKABLE JsonReply *SetAcquisitionTolerance(const QVariantMap ¶ms);
|
|
|
|
Q_INVOKABLE JsonReply *GetBatteryLevelConsideration(const QVariantMap ¶ms);
|
|
Q_INVOKABLE JsonReply *SetBatteryLevelConsideration(const QVariantMap ¶ms);
|
|
|
|
Q_INVOKABLE JsonReply *GetChargingInfos(const QVariantMap ¶ms);
|
|
Q_INVOKABLE JsonReply *SetChargingInfo(const QVariantMap ¶ms, const JsonContext &context);
|
|
|
|
Q_INVOKABLE JsonReply *GetLockOnUnplug(const QVariantMap ¶ms);
|
|
Q_INVOKABLE JsonReply *SetLockOnUnplug(const QVariantMap ¶ms);
|
|
|
|
Q_INVOKABLE JsonReply *GetAvailableSpotMarketProviders(const QVariantMap ¶ms);
|
|
Q_INVOKABLE JsonReply *GetSpotMarketConfiguration(const QVariantMap ¶ms);
|
|
Q_INVOKABLE JsonReply *SetSpotMarketConfiguration(const QVariantMap ¶ms);
|
|
Q_INVOKABLE JsonReply *GetSpotMarketScoreEntries(const QVariantMap ¶ms);
|
|
|
|
Q_INVOKABLE JsonReply *GetChargingSchedules(const QVariantMap ¶ms);
|
|
|
|
Q_INVOKABLE JsonReply *GetLoadConfig(const QVariantMap ¶ms);
|
|
|
|
/*!
|
|
* \brief L'avancement d'une obligation est-il MESURABLE pour cette charge ?
|
|
* \param c Configuration de la charge. \param[out] cause Code opaque si non mesurable.
|
|
* \return Vrai si un cumul d'énergie existe pour juger l'avancement.
|
|
*
|
|
* \par Pourquoi ce signal vit dans GetLoadConfig et non dans la télémétrie
|
|
* \c measurement n'existe que pour les charges de \c loads[] ; or une charge qu'on DÉCLARE
|
|
* est précisément celle qui n'y est pas encore — pas arbitrée, ou créée à l'instant.
|
|
* L'écran serait muet au moment exact où il doit parler : celui de la saisie.
|
|
*
|
|
* \par Trois causes, trois gestes
|
|
* \c noMeter (réparable en une manipulation), \c meterWithoutEnergy (compteur à remplacer),
|
|
* \c noSessionEnergy (rien à faire, c'est le matériel). Sans la cause, l'avertissement ne
|
|
* peut dire que « ça ne marchera pas ».
|
|
*
|
|
* \note \c meterThingId ne permet PAS de le déduire : un compteur peut publier
|
|
* \c currentPower sans \c totalEnergyConsumed (mesuré sur l'ECS du banc).
|
|
*/
|
|
bool progressMesurable(const LoadConfig &c, QString &cause) const;
|
|
Q_INVOKABLE JsonReply *SetLoadConfig(const QVariantMap ¶ms);
|
|
|
|
// [Phase 2] Ratios canoniques (autoconsommation / autonomie), % [0,100].
|
|
Q_INVOKABLE JsonReply *GetEnergyRatios(const QVariantMap ¶ms);
|
|
|
|
/*!
|
|
* \brief [ETM] Lève le verrou de défaut d'une charge pilotée (ECS-410).
|
|
* \param params \c {"loadId": string}.
|
|
* \return \c energyError — \c EnergyErrorNoError si la charge existe et son défaut est
|
|
* levé, \c EnergyErrorInvalidParameter si l'identifiant est inconnu.
|
|
* \note Acte DÉLIBÉRÉ de l'opérateur : le défaut d'ECS-410 est collant par conception, et
|
|
* cet appel est le seul moyen de le lever hors reconstruction de l'adaptateur. Il est
|
|
* journalisé côté arbitre — c'est ce qui rend le diagnostic possible après coup.
|
|
*/
|
|
Q_INVOKABLE JsonReply *ClearLoadFault(const QVariantMap ¶ms);
|
|
|
|
/*!
|
|
* \brief [ETM] Instantané de l'état runtime de l'arbitrage (allocation, budget, verrous,
|
|
* mécanismes, motifs).
|
|
* \param params Aucun.
|
|
* \return La MÊME charge utile que la notification \c LoadTelemetryChanged — pas un
|
|
* recalcul : l'objet rendu est celui qui a été publié.
|
|
* \note Répond à la règle « tout élément runtime a un \c Get* ET un \c *Changed ». Un
|
|
* client connecté après un changement doit pouvoir lire l'état, sans quoi la
|
|
* notification seule le laisse dans un silence ambigu (famille ECS-411).
|
|
*/
|
|
Q_INVOKABLE JsonReply *GetLoadTelemetry(const QVariantMap ¶ms);
|
|
//! \brief Pose une dérogation (servie au maximum, financée au réseau, en tête de passe éco).
|
|
//! \param params \c loadId, \c durationS (obligatoire, borné), \c targetWh (optionnel).
|
|
//! \return \c energyError — \c InvalidParameter si la charge est inconnue ou la durée hors bornes.
|
|
//! \brief Lit la courbe de prévision PV stockée. \param params \c from, \c to, \c o:knownAt.
|
|
//! \return \c forecastPoints — les heures SANS prévision sont absentes, jamais à zéro.
|
|
//! \note Un \c Get* SANS \c *Changed : l'invariant 11 vise l'état vivant, pas l'historique.
|
|
Q_INVOKABLE JsonReply *GetPvForecast(const QVariantMap ¶ms);
|
|
//! \brief Lit le site dont la prévision est retenue. \param params Aucun.
|
|
//! \return \c o:forecastSiteThingId — OMIS quand aucun n'est désigné, jamais vide.
|
|
Q_INVOKABLE JsonReply *GetForecastSite(const QVariantMap ¶ms);
|
|
//! \brief Désigne ce site. \param params \c o:forecastSiteThingId (vide = aucun).
|
|
//! \return \c energyError — \c ThingNotFound si l'identifiant n'est pas celui d'un Thing.
|
|
//! \brief La prévision COURANTE, lue sur le canal — l'entrée d'une décision, pas la trace.
|
|
//! \param params Aucun. \return \c capturedAt, et la courbe OU un motif, jamais des zéros.
|
|
Q_INVOKABLE JsonReply *GetForecastSnapshot(const QVariantMap ¶ms);
|
|
Q_INVOKABLE JsonReply *SetForecastSite(const QVariantMap ¶ms);
|
|
Q_INVOKABLE JsonReply *SetLoadDerogation(const QVariantMap ¶ms);
|
|
//! \brief Lève une dérogation avant son terme et rend le mode mémorisé.
|
|
//! \param params \c loadId. \return \c energyError — \c InvalidParameter si aucune n'était en cours.
|
|
Q_INVOKABLE JsonReply *ClearLoadDerogation(const QVariantMap ¶ms);
|
|
|
|
signals:
|
|
void PhasePowerLimitChanged(const QVariantMap ¶ms);
|
|
void AcquisitionToleranceChanged(const QVariantMap ¶ms);
|
|
void LockOnUnplugChanged(const QVariantMap ¶ms);
|
|
void ChargingInfoAdded(const QVariantMap ¶ms);
|
|
void ChargingInfoRemoved(const QVariantMap ¶ms);
|
|
void ChargingInfoChanged(const QVariantMap ¶ms);
|
|
void SpotMarketConfigurationChanged(const QVariantMap ¶ms);
|
|
void SpotMarketScoreEntriesChanged(const QVariantMap ¶ms);
|
|
void ChargingSchedulesChanged(const QVariantMap ¶ms);
|
|
void BatteryLevelConsiderationChanged(const QVariantMap ¶ms);
|
|
void LoadConfigChanged(const QVariantMap ¶ms);
|
|
void EnergyRatiosChanged(const QVariantMap ¶ms);
|
|
//! [ETM] État runtime d'arbitrage — sur changement significatif ET au battement périodique.
|
|
void LoadTelemetryChanged(const QVariantMap ¶ms);
|
|
|
|
private slots:
|
|
void onPowerBalanceChanged();
|
|
|
|
private:
|
|
SpotMarketManager *m_spotMarketManager;
|
|
SmartChargingManager *m_smartChargingManager = nullptr;
|
|
LoadConfigStore *m_loadConfigStore = nullptr;
|
|
EnergyManager *m_energyManager = nullptr;
|
|
|
|
EnergyRatiosCalculator m_ratiosCalculator;
|
|
EnergyRatiosCalculator::Ratios m_lastRatios;
|
|
bool m_lastRatiosValid = false; // un calcul a-t-il déjà été émis ?
|
|
|
|
QVariantMap currentEnergyRatios();
|
|
/*!
|
|
* \brief Schéma de la charge utile de télémétrie — UNE déclaration, deux usages.
|
|
* \return Le map de schéma, posé à l'identique dans les \c returns de
|
|
* \c GetLoadTelemetry et dans les \c params de \c LoadTelemetryChanged.
|
|
* \note C'est la règle « snapshot ≡ notification » rendue impossible à enfreindre :
|
|
* deux déclarations divergeraient, et c'est l'appel de reprise à froid qui casserait —
|
|
* celui qu'on ne teste pas tous les jours.
|
|
*/
|
|
static QVariantMap loadTelemetrySchema();
|
|
|
|
//! \brief Charge utile des ratios — DÉCLARÉE UNE FOIS, servie au \c Get et au \c Changed.
|
|
//! Deux déclarations divergent, et c'est l'appel de reprise à froid qui casse (invariant 11).
|
|
//! \return Le schéma des champs, prêt pour \c registerMethod et \c registerNotification.
|
|
static QVariantMap energyRatiosSchema();
|
|
//! \return Charge utile courante ; carte MINIMALE (sans \c timestamp) si aucun cycle
|
|
//! d'arbitrage n'a encore eu lieu — cf. \c EnergyArbitrator::loadTelemetry().
|
|
QVariantMap currentLoadTelemetry() const;
|
|
void sendSpotMarketConfigurationChangedNotification();
|
|
|
|
};
|
|
|
|
#endif // NYMEAENERGYJSONHANDLER_H
|