From 8a055eee9cd7fad8ce48acb77538cd50c50f0b37 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Mon, 19 Oct 2026 08:26:24 +0200 Subject: [PATCH] =?UTF-8?q?feat(rpc):=20droits=20de=20NymeaEnergy.*=20s?= =?UTF-8?q?=C3=A9par=C3=A9s=20=E2=80=94=20lire,=20commander,=20configurer,?= =?UTF-8?q?=20administrer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- energyplugin/nymeaenergyjsonhandler.cpp | 77 +++++++++++++++---------- energyplugin/nymeaenergyjsonhandler.h | 18 ++++++ 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/energyplugin/nymeaenergyjsonhandler.cpp b/energyplugin/nymeaenergyjsonhandler.cpp index f03a5c0..67f0b4e 100644 --- a/energyplugin/nymeaenergyjsonhandler.cpp +++ b/energyplugin/nymeaenergyjsonhandler.cpp @@ -68,43 +68,54 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana QVariantMap params, returns; QString description; + // [ETM] DROITS — chaque méthode écrit le sien (consigne droits lecture/écriture, 2026-10-19). + // None lire : tous les Get*, sauf GetOptimizerTrace. « None » veut dire « tout + // compte AUTHENTIFIÉ », pas « sans jeton ». + // ControlThings commander : les gestes de l'usager (mode, dérogation, défaut, verrou). + // ConfigureThings décrire l'installation : charges, tarif, site, tolérance, spot market. + // Admin raccordement, secret ou chemin système. + // Les droits ne mordent que sur un transport AVEC authentification : sans, nymea laisse tout + // passer. Les notifications, elles, ne sont filtrées par aucun droit — d'où les lectures en + // None : les garder plus haut ne cacherait rien de ce que les *Changed diffusent déjà. + // Table figée par TestCharging::testNymeaEnergyPermissionScopes. + params.clear(); returns.clear(); description = "Get the phase power consumption limit. 0 if unset. This needs to be set in order for the smart charging to do anything."; returns.insert("phasePowerLimit", enumValueName(Uint)); - registerMethod("GetPhasePowerLimit", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetPhasePowerLimit", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Set the phase power consumption limit. This is the maximum allowed Ampere per phase. A value of 0 means unset. All smart charging will be disabled."; params.insert("phasePowerLimit", enumValueName(Uint)); returns.insert("energyError", enumRef()); - registerMethod("SetPhasePowerLimit", description, params, returns); + registerMethod("SetPhasePowerLimit", description, params, returns, Types::PermissionScopeAdmin); params.clear(); returns.clear(); description = "Get the acquisition tolerance. This value will control how much solar surplus is required to start the charging."; returns.insert("acquisitionTolerance", enumValueName(Double)); - registerMethod("GetAcquisitionTolerance", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetAcquisitionTolerance", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Set the acquisition tolerance for the charging."; params.insert("acquisitionTolerance", enumValueName(Double)); returns.insert("energyError", enumRef()); - registerMethod("SetAcquisitionTolerance", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("SetAcquisitionTolerance", description, params, returns, Types::PermissionScopeConfigureThings); params.clear(); returns.clear(); description = "Get the battery level consideration. This value will control how much the energy storage is taking in account"; returns.insert("batteryLevelConsideration", enumValueName(Double)); - registerMethod("GetBatteryLevelConsideration", description, params, returns); + registerMethod("GetBatteryLevelConsideration", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Set the battery level consideration for the charging."; params.insert("batteryLevelConsideration", enumValueName(Double)); returns.insert("energyError", enumRef()); - registerMethod("SetBatteryLevelConsideration", description, params, returns); + registerMethod("SetBatteryLevelConsideration", description, params, returns, Types::PermissionScopeAdmin); params.clear(); returns.clear(); description = "Get the lock on unplug setting."; returns.insert("lockOnUnplug", enumValueName(Bool)); - registerMethod("GetLockOnUnplug", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetLockOnUnplug", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Set the lock on unplug setting."; @@ -119,7 +130,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana "charger' and nothing else."; params.insert("o:evChargerId", enumValueName(Uuid)); returns.insert("chargingInfos", QVariantList() << objectRef()); - registerMethod("GetChargingInfos", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetChargingInfos", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Set the charging info for an EV charger. The given object REPLACES the stored " @@ -136,7 +147,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana params.clear(); returns.clear(); description = "Get the list of available spot market data providers."; returns.insert("providers", QVariantList() << objectRef()); - registerMethod("GetAvailableSpotMarketProviders", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetAvailableSpotMarketProviders", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Set the current spotmarket configuration. If enabled, the provideId must be valid. You can get the available providers using GetAvailableSpotMarketProviders. " @@ -148,20 +159,20 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana params.insert("o:providerId", enumValueName(Uuid)); returns.insert("energyError", enumRef()); returns.insert("o:fault", enumValueName(String)); // [ETM] SPOT_MARKET_NOT_IN_BUILD - registerMethod("SetSpotMarketConfiguration", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("SetSpotMarketConfiguration", description, params, returns, Types::PermissionScopeConfigureThings); params.clear(); returns.clear(); description = "Get the current spotmarket configuration."; returns.insert("o:providerId", enumValueName(Uuid)); returns.insert("enabled", enumValueName(Bool)); returns.insert("available", enumValueName(Bool)); - registerMethod("GetSpotMarketConfiguration", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetSpotMarketConfiguration", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Get the current score entries from the current spot market. If spot market is not enabled or not available, the list will be empty. The weighting of the available score entries goes from 0 (worst) to 1.0 (best)."; returns.insert("spotMarketScoreEntries", QVariantList() << objectRef()); - registerMethod("GetSpotMarketScoreEntries", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetSpotMarketScoreEntries", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); @@ -173,14 +184,14 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana // C'est la famille ECS-411 — un silence ambigu. Tout élément runtime a un Get* ET un // *Changed ; jamais une notification seule. returns.insert("degradedMode", enumValueName(Bool)); - registerMethod("GetChargingSchedules", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetChargingSchedules", description, params, returns, Types::PermissionScopeNone); // [ETM] Configuration des charges pilotées (interface etmvariableload, contrat §4). params.clear(); returns.clear(); description = "Get the configured controllable loads (etmvariableload)."; returns.insert("loadConfigs", QVariantList() << objectRef()); - registerMethod("GetLoadConfig", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetLoadConfig", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Replace the full set of controllable load configs. Rejected as a whole " @@ -273,7 +284,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana loadItem.insert("o:minOffS", enumValueName(Uint)); params.insert("loadConfigs", QVariantList() << loadItem); returns.insert("energyError", enumRef()); - registerMethod("SetLoadConfig", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("SetLoadConfig", description, params, returns, Types::PermissionScopeConfigureThings); // [Phase 2] Ratios canoniques autoconsommation / autonomie (% [0,100]). Champ omis // (jamais null) quand n/a (dénominateur ≤ 0). Source canonique de l'energymanager — @@ -283,7 +294,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana "derived from the energy manager's cumulative counters over the current local " "day. A rate field is omitted (never null) when not available (denominator <= 0)."; returns = energyRatiosSchema(); - registerMethod("GetEnergyRatios", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetEnergyRatios", description, params, returns, Types::PermissionScopeNone); // [ETM] ECS-410 — levée du verrou de défaut d'une charge. Délibérée et journalisée : le // défaut est collant par conception (« cesser toute commande jusqu'à intervention »), sa @@ -308,7 +319,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana "notification. 'timestamp' is the end of the last arbitration cycle and is " "omitted while no cycle has run yet."; returns = loadTelemetrySchema(); - registerMethod("GetLoadTelemetry", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetLoadTelemetry", description, params, returns, Types::PermissionScopeNone); // --- Prévision PV ------------------------------------------------------------------------ // @@ -358,7 +369,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana returns.insert("energyError", enumRef()); returns.insert("o:fault", enumValueName(String)); // FORECAST_DATE_UNREADABLE returns.insert("o:faultParams", enumValueName(Variant)); // {fields: [from|to|knownAt]} - registerMethod("GetPvForecast", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetPvForecast", description, params, returns, Types::PermissionScopeNone); // 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. La prévision est une propriété du SITE, pas @@ -385,7 +396,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana // ABSENT quand aucun site n'existe : annoncer une désignation manquante à une installation // sans plugin météo serait un avertissement qu'on ne peut pas satisfaire. returns.insert("o:fault", enumValueName(String)); // MISSING_FORECAST_SITE - registerMethod("GetForecastSite", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetForecastSite", description, params, returns, Types::PermissionScopeNone); // 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. @@ -452,7 +463,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana // Variant, PAS Object : sans tarif saisi la valeur est `null` (brief 3 T4), et un null n'est // pas un objet. La forme de l'objet est dans la description et dans INTERFACE.md. returns.insert("o:price", enumValueName(Variant)); - registerMethod("GetForecastSnapshot", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetForecastSnapshot", description, params, returns, Types::PermissionScopeNone); // --- Tarif saisi (brief 3 T4) -------------------------------------------------------------- // Un RÉGLAGE de l'installateur, persisté dans les réglages du moteur. Aucune décision ne le @@ -478,7 +489,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana returns.insert("currency", enumValueName(String)); returns.insert("includes", QVariantList() << enumValueName(String)); returns.insert("timeZone", enumValueName(String)); - registerMethod("GetTariff", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetTariff", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Replace the tariff, effective immediately and persisted. One zone covering " @@ -495,7 +506,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana returns.insert("energyError", enumRef()); returns.insert("o:fault", enumValueName(String)); returns.insert("o:faultParams", enumValueName(Variant)); - registerMethod("SetTariff", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("SetTariff", description, params, returns, Types::PermissionScopeConfigureThings); params.clear(); returns.clear(); description = "Designate the site whose PV forecast is kept. ALL the PV planes under that " @@ -519,7 +530,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana // de l'absence — inchangé. Le type est donc contrôlé dans le handler. params.insert("o:acLimitW", enumValueName(Variant)); returns.insert("energyError", enumRef()); - registerMethod("SetForecastSite", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("SetForecastSite", description, params, returns, Types::PermissionScopeConfigureThings); // --- Dérogations ------------------------------------------------------------------------- // @@ -561,7 +572,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana "unknown value is OMITTED, never 0. The token is never returned: tokenConfigured " "only says whether one is set. Same payload as OptimizerStatusChanged."; returns = optimizerStatusSchema(); - registerMethod("GetOptimizerStatus", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetOptimizerStatus", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Set the external-optimizer endpoint, persisted and applied immediately: " @@ -574,8 +585,8 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana registerMethod("SetOptimizerEndpoint", description, params, returns, Types::PermissionScopeAdmin); // WRITE-ONLY (décision Q2) : aucune méthode ne relit le token. Admin : c'est un secret, et il - // ouvre l'optimiseur à qui le détient — même niveau que les réglages d'installation du - // handler restés au défaut nymea (SetPhasePowerLimit, SetBatteryLevelConsideration). + // ouvre l'optimiseur à qui le détient — même niveau que SetPhasePowerLimit et + // SetBatteryLevelConsideration. // Le TYPE de `token` est vérifié ici, pas par nymea : sous Qt 6, le validateur ne contrôle // plus le type des paramètres de base (jsonvalidator.cpp, #if QT_VERSION >= 6), un nombre // arrive donc au handler. Une clé EN TROP, elle, est refusée par le validateur, qui recopie @@ -650,11 +661,11 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana returns.insert("o:slots", QVariantList() << planSlot); returns.insert("o:diagnostics", enumValueName(Variant)); // status, inputsUsed.pv returns.insert("o:fallbackCode", enumValueName(String)); - registerMethod("GetPlan", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetPlan", description, params, returns, Types::PermissionScopeNone); // [ETM] Le contrat OpenRPC du protocole optimiseur, EMBARQUÉ (ressource Qt) : celui du moteur qui - // tourne, jamais un fichier relu sur disque. Lecture, aucun secret — même permission que les - // autres Get* de l'optimiseur. Statique pour un binaire donné : pas de *Changed (invariant 11 + // tourne, jamais un fichier relu sur disque. Lecture, aucun secret — même droit que + // GetOptimizerStatus et GetPlan. Statique pour un binaire donné : pas de *Changed (invariant 11 // vise un état runtime ; celui-ci ne change qu'avec le paquet). params.clear(); returns.clear(); description = "The OpenRPC 1.3 document of the engine <-> optimizer protocol spoken by THIS " @@ -665,7 +676,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana "Static for a given binary: no notification."; returns.insert("protocolVersion", enumValueName(String)); returns.insert("schema", enumValueName(Object)); - registerMethod("GetOptimizerProtocol", description, params, returns, Types::PermissionScopeControlThings); + registerMethod("GetOptimizerProtocol", description, params, returns, Types::PermissionScopeNone); // Notifications @@ -870,6 +881,12 @@ QString NymeaEnergyJsonHandler::name() const return "NymeaEnergy"; } +void NymeaEnergyJsonHandler::registerMethod(const QString &name, const QString &description, const QVariantMap ¶ms, + const QVariantMap &returns, Types::PermissionScope permissionScope) +{ + JsonHandler::registerMethod(name, description, params, returns, permissionScope); +} + JsonReply *NymeaEnergyJsonHandler::GetPhasePowerLimit(const QVariantMap ¶ms) { Q_UNUSED(params) diff --git a/energyplugin/nymeaenergyjsonhandler.h b/energyplugin/nymeaenergyjsonhandler.h index bd43783..0b6ecdf 100644 --- a/energyplugin/nymeaenergyjsonhandler.h +++ b/energyplugin/nymeaenergyjsonhandler.h @@ -188,6 +188,24 @@ 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 ¶ms, + const QVariantMap &returns, Types::PermissionScope permissionScope); + SpotMarketManager *m_spotMarketManager; SmartChargingManager *m_smartChargingManager = nullptr; LoadConfigStore *m_loadConfigStore = nullptr;