Désignation GLOBALE sur le modèle d'Energy.SetRootMeter — même nature : une source unique pour toute l'installation, désignée une fois. Un forecastThingId par charge supposerait que deux charges s'appuient sur des prévisions différentes ; il n'y a qu'un ciel au-dessus de l'installation, et le champ dit d'où vient la PRODUCTION, pas ce que la charge consomme. GetForecastSite / SetForecastSite, persisté dans energy.conf. PLUSIEURS PLANS : LA SOMME, et ce n'était pas un choix libre. La courbe est superposée à la production MESURÉE, qui est le total. N'en retenir qu'un donnerait une prévision systématiquement SOUS la mesure — qui se lirait comme une installation qui sous-performe, soit le mode d'erreur déjà nommé au §3 pour le biais optimiste, obtenu cette fois par le découpage. UN DÉFAUT TROUVÉ EN ÉCRIVANT, ET IL ÉTAIT DÉJÀ LÀ. L'implémentation lisait tous les Things portant forecastCurveTransport et les enregistrait l'un après l'autre. Avec deux plans du même run, les deux révisions entrent en collision sur la clé (targetAt, firstIssuedAt) : INSERT OR REPLACE garde LE DERNIER LU, en silence. Ni somme ni choix — un tirage. Le banc n'a qu'un plan, donc rien ne l'aurait montré. ON NE SOMME QUE DES PLANS DU MÊME RUN. Un plan rafraîchi et l'autre pas donneraient une chimère : une partie du toit à jour, l'autre périmée, publiées comme le tout. Le moteur attend la passe suivante. Même règle si UN plan est refusé — additionner le reste publierait une partie du toit sous le nom du tout. L'ABSENCE DE DÉSIGNATION EST SILENCIEUSE. Rien retenu, rien journalisé : une installation sans prévision est un cas normal, et un avertissement par cycle ferait chercher une panne là où il n'y a qu'une fonction non configurée. GetForecastSite OMET le champ plutôt que de rendre une chaîne vide. Note de contrat : EnergyError de l'amont n'a pas de ThingNotFound, donc un identifiant inconnu rend InvalidParameter et la description le dit. Inventer une valeur d'énumération ferait diverger le contrat de celui de nymea-energy. loadmodel 32/32, charging 48/48, spotmarket 32/32, simulation 69/69, 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
189 lines
9.5 KiB
C++
189 lines
9.5 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.
|
|
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
|