etm-powersync-energy-plugin.../energyplugin/nymeaenergyjsonhandler.h
Patrick Schurig 8a055eee9c feat(rpc): droits de NymeaEnergy.* séparés — lire, commander, configurer, administrer
Chaque méthode déclare son droit au registerMethod ; plus aucune ne tombe en Admin par la
valeur par défaut de libnymea.

- None (tout compte authentifié) : tous les Get* sauf GetOptimizerTrace, dont
  GetBatteryLevelConsideration (Admin par oubli jusqu'ici). GetOptimizerStatus ne porte ni
  l'endpoint ni le token.
- ControlThings : SetChargingInfo, SetLoadDerogation, ClearLoadDerogation, ClearLoadFault,
  SetLockOnUnplug.
- ConfigureThings : SetLoadConfig, SetTariff, SetForecastSite, SetAcquisitionTolerance,
  SetSpotMarketConfiguration.
- Admin, désormais écrit : SetPhasePowerLimit, SetBatteryLevelConsideration,
  SetOptimizerEndpoint, SetOptimizerToken, GetOptimizerTrace, SetOptimizerTrace.

Un registerMethod privé SANS valeur par défaut masque celui de JsonHandler : oublier le droit
ne compile plus. C'est la seule garde possible pour une méthode Admin, le défaut étant Admin.

Changement de contrat RPC (JSONRPC.Introspect publie permissionScope). Les droits ne mordent
que sur un transport avec authentification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 08:26:24 +02:00

245 lines
14 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 &params);
Q_INVOKABLE JsonReply *SetPhasePowerLimit(const QVariantMap &params);
Q_INVOKABLE JsonReply *GetAcquisitionTolerance(const QVariantMap &params);
Q_INVOKABLE JsonReply *SetAcquisitionTolerance(const QVariantMap &params);
Q_INVOKABLE JsonReply *GetBatteryLevelConsideration(const QVariantMap &params);
Q_INVOKABLE JsonReply *SetBatteryLevelConsideration(const QVariantMap &params);
Q_INVOKABLE JsonReply *GetChargingInfos(const QVariantMap &params);
Q_INVOKABLE JsonReply *SetChargingInfo(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *GetLockOnUnplug(const QVariantMap &params);
Q_INVOKABLE JsonReply *SetLockOnUnplug(const QVariantMap &params);
Q_INVOKABLE JsonReply *GetAvailableSpotMarketProviders(const QVariantMap &params);
Q_INVOKABLE JsonReply *GetSpotMarketConfiguration(const QVariantMap &params);
Q_INVOKABLE JsonReply *SetSpotMarketConfiguration(const QVariantMap &params);
Q_INVOKABLE JsonReply *GetSpotMarketScoreEntries(const QVariantMap &params);
Q_INVOKABLE JsonReply *GetChargingSchedules(const QVariantMap &params);
Q_INVOKABLE JsonReply *GetLoadConfig(const QVariantMap &params);
/*!
* \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 &params);
// [Phase 2] Ratios canoniques (autoconsommation / autonomie), % [0,100].
Q_INVOKABLE JsonReply *GetEnergyRatios(const QVariantMap &params);
/*!
* \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 &params);
/*!
* \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 &params);
//! \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 &params);
//! \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 &params);
//! \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 &params);
Q_INVOKABLE JsonReply *SetForecastSite(const QVariantMap &params);
Q_INVOKABLE JsonReply *SetLoadDerogation(const QVariantMap &params);
//! \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 &params);
//! \brief Le tarif saisi (brief 3 T4). \param params Aucun.
//! \return \c o:zones, \c o:exportCtkWh, \c o:source — OMIS sans tarif ; \c currency,
//! \c includes, \c timeZone toujours.
Q_INVOKABLE JsonReply *GetTariff(const QVariantMap &params);
//! \brief Remplace le tarif saisi, à chaud. \param params \c zones (vide = effacer), \c o:exportCtkWh.
//! \return \c energyError ; sur refus \c fault \c TARIFF_REFUSED et \c faultParams — rien n'est modifié.
Q_INVOKABLE JsonReply *SetTariff(const QVariantMap &params);
//! \brief [ETM] État de la machine de repli de l'optimiseur (brief 4 T4). \param params Aucun.
//! \return \c EnergyArbitrator::optimizerStatus() — la charge utile d'\c OptimizerStatusChanged.
Q_INVOKABLE JsonReply *GetOptimizerStatus(const QVariantMap &params);
//! \brief [ETM] Pose l'endpoint de l'optimiseur, à chaud. \param params \c endpoint (vide = aucun).
//! \return \c energyError ; sur refus \c fault \c invalid-endpoint — rien n'est modifié.
Q_INVOKABLE JsonReply *SetOptimizerEndpoint(const QVariantMap &params);
//! \brief [ETM] Pose le token de l'optimiseur — ÉCRITURE SEULE (Q2). \param params \c token (vide = retrait).
//! \return \c energyError ; sur refus \c fault \c token-refused. Le token n'est jamais rendu.
Q_INVOKABLE JsonReply *SetOptimizerToken(const QVariantMap &params);
//! \brief [ETM] Réglage des traces de décision (brief 4 T6, annexe D). \param params Aucun.
//! \return \c EnergyArbitrator::optimizerTrace().
Q_INVOKABLE JsonReply *GetOptimizerTrace(const QVariantMap &params);
//! \brief [ETM] Change le réglage des traces, à chaud. \param params \c o:enabled,
//! \c o:retentionDays, \c o:maxBytesPerDay. \return \c energyError ; sur refus \c fault
//! \c invalid-trace-setting — rien n'est modifié.
Q_INVOKABLE JsonReply *SetOptimizerTrace(const QVariantMap &params);
//! \brief [ETM] Le plan en vigueur au dernier cycle (exigence app 2). \param params Aucun.
//! \return \c EnergyArbitrator::publishedPlan() ; \c {inForce: false} sans plan en vigueur.
Q_INVOKABLE JsonReply *GetPlan(const QVariantMap &params);
//! \brief [ETM] Le contrat OpenRPC du protocole optimiseur, embarqué (lot contrat OpenRPC).
//! \param params Aucun. \return \c protocolVersion et \c schema (\c OptimizerProtocol::schemaDocument()).
Q_INVOKABLE JsonReply *GetOptimizerProtocol(const QVariantMap &params);
signals:
void PhasePowerLimitChanged(const QVariantMap &params);
void AcquisitionToleranceChanged(const QVariantMap &params);
void LockOnUnplugChanged(const QVariantMap &params);
void ChargingInfoAdded(const QVariantMap &params);
void ChargingInfoRemoved(const QVariantMap &params);
void ChargingInfoChanged(const QVariantMap &params);
void SpotMarketConfigurationChanged(const QVariantMap &params);
void SpotMarketScoreEntriesChanged(const QVariantMap &params);
void ChargingSchedulesChanged(const QVariantMap &params);
void BatteryLevelConsiderationChanged(const QVariantMap &params);
void LoadConfigChanged(const QVariantMap &params);
void EnergyRatiosChanged(const QVariantMap &params);
//! [ETM] État runtime d'arbitrage — sur changement significatif ET au battement périodique.
void LoadTelemetryChanged(const QVariantMap &params);
//! [ETM] Transition de la machine de repli de l'optimiseur (§7).
void OptimizerStatusChanged(const QVariantMap &params);
private slots:
void onPowerBalanceChanged();
private:
/*!
* \brief [ETM] Enregistre une méthode AVEC son droit — le seul \c registerMethod visible ici.
* \param name Nom de la méthode, sans le namespace.
* \param description Description publiée par \c JSONRPC.Introspect.
* \param params Schéma des paramètres.
* \param returns Schéma du retour.
* \param permissionScope Droit exigé par nymea, SANS valeur par défaut.
*
* \par Pourquoi masquer celui de JsonHandler
* Le \c registerMethod de libnymea prend \c PermissionScopeAdmin par défaut : une méthode
* enregistrée sans droit tombait en Admin sans que personne l'ait décidé
* (\c GetBatteryLevelConsideration, une lecture). Déclaré ici, ce nom masque celui de la
* classe de base : oublier le droit ne compile plus. Le test
* \c TestCharging::testNymeaEnergyPermissionScopes fige la table publiée.
*/
void registerMethod(const QString &name, const QString &description, const QVariantMap &params,
const QVariantMap &returns, Types::PermissionScope permissionScope);
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 de l'état de l'optimiseur — déclarée une fois, pour \c GetOptimizerStatus
//! et \c OptimizerStatusChanged. \return Le schéma des champs.
static QVariantMap optimizerStatusSchema();
//! \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