Constat d'origine, rejoué et daté au banc : 90 s d'écoute, arbitre actif, ~2 kW exportés
→ ZÉRO notification NymeaEnergy.*. L'état runtime de l'arbitre (allocation, budget, verrous,
paliers, disponibilité, motifs) ne franchissait aucune frontière RPC : il vivait exclusivement
dans qCInfo(dcNymeaEnergy). Expliquer les décisions au client n'avait donc aucun transport.
Après déploiement, même geste : 13 notifications en 480 s.
Aucune phrase composée par la box. La locale nymea est PAR CONNEXION (JSONRPC.Hello o:locale),
et NymeaEnergyJsonHandler est un JsonHandler sans pluginId — donc hors du catalogue de
traduction : rien ne rattraperait une phrase française émise là. La box transporte un CODE et
des PARAMÈTRES ; la phrase est fabriquée par le client.
LoadAction::reason devient DecisionReason{code, params}. La ligne française du journal est
RENDUE depuis ce couple par renderFr(), source unique — jamais maintenue en parallèle. Les 19
codes sont EXTRAITS des sites existants, pas inventés ; les motifs d'ECS-309 et ECS-309-b sont
repris mot pour mot. Prouvé par comparaison de traces (1726 lignes de journal identiques en
simulation), par une table d'or qui échoue au premier code sans phrase attendue, et au banc sur
7 motifs distincts. Le libellé de la charge n'entre pas dans la charge utile : il est substitué
au RENDU, par l'appelant qui le connaît.
Les verrous sortent en SECONDES RESTANTES, pas en watts. Ceci n'enfreint pas ECS-306 : cette
exigence interdit qu'un index de PALIER franchisse la frontière d'ALLOCATION ; une valeur
d'AFFICHAGE sortante est un autre chemin. Le mécanisme sort en union discriminée
(relay|variable|sgReady), toutes clés internes optionnelles au schéma (LM-302).
ILoadAdapter::runtimeView() est PURE VIRTUELLE à dessein — mais la pure virtuelle force à
RÉPONDRE, pas à répondre quelque chose. testRuntimeViewIsSubstantial() est le second verrou :
les quatre adaptateurs, amenés dans un état non trivial, doivent rendre les valeurs runtime
réelles, les verrous en secondes ET leur expiration, et les codes de défaut. Deux mutants
« vue vide » introduits puis retirés : le test échoue sur chacun.
budget publié : surplusW, evReservedW, recreditedW, allocatedW, remainingW, avec l'identité
surplus + recrédit − alloué == restant. recreditedW n'est pas décoratif : sans lui l'identité
serait fausse DÈS QU'UNE CHARGE CONSOMME, et le client chercherait un écart inexplicable. Le
banc l'a montré aussitôt — surplusW −250 W avec recreditedW 3000 W — rendant lisible l'artefact
de banc documenté dans AGENTS.md (relais sans mesure + rootmeter aveugle).
Champs omis, jamais nuls : timestamp absent = aucun cycle depuis le démarrage ; budget absent =
mode dégradé, planification suspendue. Publier un zéro se lirait « exécuté, rien à allouer ».
Cadence : changement qualitatif ou variation d'allocation > 50 W, plus un battement de 60 s.
C'est le battement qui rend un arbitre FIGÉ détectable — les notifications continuent tandis que
le timestamp cesse d'avancer. Observé 3 fois au banc. Volume mesuré : ≈ 1,7 Mo/jour, un client.
GetChargingSchedules rend enfin degradedMode : il était POUSSÉ en notification seule, donc
illisible par un client connecté après la bascule (famille ECS-411). Règles absolues 11 et 12
ajoutées à AGENTS.md.
LoadConfig : champ optionnel domain (ecs·ev·heating·hvac·battery), PURE métadonnée d'intention
que l'arbitre ne lit pas. Absente ou vide = non classée. Forme exacte découverte au banc et
corrigée dans la doc ET le test : GetLoadConfig sérialise via le méta-objet, donc la propriété
sort TOUJOURS, à "" — comme sgReady:{} ou powerLevels:[]. C'est le corollaire LM-302.
Règle 7-c, deux silences fermés : les retours idempotents du RelayRouter et du SgReadyAdapter
étaient MUETS — « consigne reçue, palier déjà bon » ne se distinguait pas de « l'action n'est
jamais parvenue à l'adaptateur », et c'est le cas ordinaire qu'on vient vérifier.
⚠️ Porté au contrat : loads[] n'est PAS en correspondance 1:1 avec GetLoadConfig. « pac-terrain »
est la PAC SG-Ready CODÉE EN DUR d'energypluginnymea.cpp (K1/K2 = Relay4/Relay5, {3:1500,
4:3000}) — arbitrée et publiée, mais inconfigurable : ni rang, ni domain, ni enabled:false.
INTERFACE.md porte la règle de détection pour l'app. Son sort reste À TRANCHER (RELEVE_LOTB §8-bis b).
Déploiement : la recette de TEST_TERRAIN.md §b portait un glob « dpkg -i /tmp/…_*_arm64.deb »,
de la même famille que celui qui a rétrogradé .75 de +etm12 à +etm9. Remplacée par
etm-powersync-deploy/deploy.sh (archivage avant dépôt, installation par nom, refus de
rétrogradation, contrôle de version après coup).
Vérifié sur .75 (1.15.2+etm13 puis +etm14) : critères 1, 2, 4, 5, 6, 7, 8 du brief. Critère 3
(épisode dégradé) vérifié en simulation seulement — le déclencher demande de faire taire un
simulateur SunSpec partagé, hors périmètre. Relevé complet, critère par critère, avec ce qui n'a
PAS été vérifié sur la machine et pourquoi : docs/RELEVE_LOTB.md.
Suites : simulation 38/38, charging 46/46, spotmarket 32/32, y compris en build DEBUG où les
Q_ASSERT_X du validateur JSON-RPC sont actifs (le schéma déclaré correspond donc à la charge
utile émise, retours ET notifications). Cross-arm64 : 0 avertissement. Doxygen : 10, inchangé.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016MfvSskkYrrFriYcj4683D
808 lines
40 KiB
C++
808 lines
40 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/>.
|
||
*
|
||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||
|
||
#include "nymeaenergyjsonhandler.h"
|
||
#ifdef ETM_ARBITRATOR
|
||
#include "etm/energyarbitrator.h"
|
||
#endif
|
||
#include "types/charginginfo.h"
|
||
#include "smartchargingmanager.h"
|
||
#include "spotmarket/spotmarketmanager.h"
|
||
#include "etm/types/loadconfig.h"
|
||
#include "etm/config/loadconfigstore.h"
|
||
|
||
#include <energymanager.h>
|
||
|
||
#include <QLoggingCategory>
|
||
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
||
|
||
NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketManager, SmartChargingManager *smartChargingManager, LoadConfigStore *loadConfigStore, EnergyManager *energyManager, QObject *parent):
|
||
JsonHandler{parent},
|
||
m_spotMarketManager{spotMarketManager},
|
||
m_smartChargingManager{smartChargingManager},
|
||
m_loadConfigStore{loadConfigStore},
|
||
m_energyManager{energyManager}
|
||
{
|
||
|
||
registerEnum<ChargingInfo::ChargingMode>();
|
||
registerEnum<ChargingInfo::ChargingState>();
|
||
registerEnum<ChargingAction::ChargingActionIssuer>();
|
||
|
||
registerObject<ChargingInfo, ChargingInfos>();
|
||
registerObject<SpotMarketProviderInfo, SpotMarketProviderInfos>();
|
||
registerObject<ScoreEntry, ScoreEntries>();
|
||
registerObject<ChargingAction>();
|
||
registerObject<ChargingSchedule, ChargingSchedules>();
|
||
registerObject<LoadConfigNeeds>();
|
||
registerObject<LoadConfig, LoadConfigs>();
|
||
|
||
QVariantMap params, returns;
|
||
QString description;
|
||
|
||
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);
|
||
|
||
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<EnergyManager::EnergyError>());
|
||
registerMethod("SetPhasePowerLimit", description, params, returns);
|
||
|
||
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);
|
||
|
||
params.clear(); returns.clear();
|
||
description = "Set the acquisition tolerance for the charging.";
|
||
params.insert("acquisitionTolerance", enumValueName(Double));
|
||
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
||
registerMethod("SetAcquisitionTolerance", description, params, returns, Types::PermissionScopeControlThings);
|
||
|
||
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);
|
||
|
||
params.clear(); returns.clear();
|
||
description = "Set the battery level consideration for the charging.";
|
||
params.insert("batteryLevelConsideration", enumValueName(Double));
|
||
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
||
registerMethod("SetBatteryLevelConsideration", description, params, returns);
|
||
|
||
params.clear(); returns.clear();
|
||
description = "Get the lock on unplug setting.";
|
||
returns.insert("lockOnUnplug", enumValueName(Bool));
|
||
registerMethod("GetLockOnUnplug", description, params, returns, Types::PermissionScopeControlThings);
|
||
|
||
params.clear(); returns.clear();
|
||
description = "Set the lock on unplug setting.";
|
||
params.insert("lockOnUnplug", enumValueName(Bool));
|
||
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
||
registerMethod("SetLockOnUnplug", description, params, returns, Types::PermissionScopeControlThings);
|
||
|
||
params.clear(); returns.clear();
|
||
description = "Get the charging info for all or a single EV charger";
|
||
params.insert("o:evChargerId", enumValueName(Uuid));
|
||
returns.insert("chargingInfos", QVariantList() << objectRef<ChargingInfo>());
|
||
registerMethod("GetChargingInfos", description, params, returns, Types::PermissionScopeControlThings);
|
||
|
||
params.clear(); returns.clear();
|
||
description = "Set the charging info for an EV charger. Only given properties will be set, others will be untouched.";
|
||
params.insert("chargingInfo", objectRef<ChargingInfo>());
|
||
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
||
registerMethod("SetChargingInfo", description, params, returns, Types::PermissionScopeControlThings);
|
||
|
||
params.clear(); returns.clear();
|
||
description = "Get the list of available spot market data providers.";
|
||
returns.insert("providers", QVariantList() << objectRef<SpotMarketProviderInfo>());
|
||
registerMethod("GetAvailableSpotMarketProviders", description, params, returns, Types::PermissionScopeControlThings);
|
||
|
||
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.";
|
||
params.insert("enabled", enumValueName(Bool));
|
||
params.insert("o:providerId", enumValueName(Uuid));
|
||
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
||
registerMethod("SetSpotMarketConfiguration", description, params, returns, Types::PermissionScopeControlThings);
|
||
|
||
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);
|
||
|
||
|
||
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<ScoreEntry>());
|
||
registerMethod("GetSpotMarketScoreEntries", description, params, returns, Types::PermissionScopeControlThings);
|
||
|
||
|
||
params.clear(); returns.clear();
|
||
description = "Get the planed charging schedules, and whether the L2 degraded mode is "
|
||
"currently active (meter silent > 90s, planning suspended).";
|
||
returns.insert("chargingSchedules", QVariantList() << objectRef<ChargingSchedule>());
|
||
// [ETM] degradedMode était POUSSÉ par ChargingSchedulesChanged sans être LISIBLE ici : un
|
||
// client connecté après la bascule ne pouvait pas savoir dans quel état il se trouvait.
|
||
// 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);
|
||
|
||
|
||
// [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<LoadConfig>());
|
||
registerMethod("GetLoadConfig", description, params, returns, Types::PermissionScopeControlThings);
|
||
|
||
params.clear(); returns.clear();
|
||
description = "Replace the full set of controllable load configs. Rejected as a whole "
|
||
"(energyError) if any entry is invalid; persisted and applied atomically.";
|
||
// Schéma inline (pas objectRef<LoadConfig>) : le contrat rév. 3 a DEUX formes exclusives
|
||
// (relay-router : relays[]+minOnS/minOffS — etmvariableload : powerLevels|maxPowerW).
|
||
// objectRef rendrait TOUS les champs requis → nymea rejetterait une des deux formes AVANT
|
||
// le handler. On marque donc TOUS les champs spécifiques "o:" ; la validation conditionnelle
|
||
// (par adapter/mode) vit dans LoadConfig::isValid(). Seuls id/label/adapter/mode/priority/
|
||
// enabled sont requis (présents dans les deux formes). GET reste typé objectRef<LoadConfig>.
|
||
QVariantMap needsItem;
|
||
needsItem.insert("o:dailyDeadline", enumValueName(String));
|
||
needsItem.insert("o:minEnergyWhPerDay", enumValueName(Uint));
|
||
QVariantMap relayItem;
|
||
relayItem.insert("thingId", enumValueName(String));
|
||
relayItem.insert("powerW", enumValueName(Uint));
|
||
QVariantMap loadItem;
|
||
loadItem.insert("id", enumValueName(String));
|
||
loadItem.insert("label", enumValueName(String));
|
||
loadItem.insert("adapter", enumValueName(String));
|
||
loadItem.insert("mode", enumValueName(String));
|
||
// Métadonnée d'INTENTION (§3 du lot) : un code d'énumération fermée — ecs · ev · heating ·
|
||
// hvac · battery — jamais un libellé affichable. Optionnelle : absente = charge non classée.
|
||
// L'arbitre ne la lit pas ; la validation de l'énumération vit dans LoadConfig::isValid(),
|
||
// comme pour « adapter » et « mode » (LM-302 : l'exigence réelle n'est jamais dans le schéma).
|
||
loadItem.insert("o:domain", enumValueName(String));
|
||
loadItem.insert("priority", enumValueName(Uint));
|
||
loadItem.insert("enabled", enumValueName(Bool));
|
||
loadItem.insert("o:powerLevels", QVariantList() << enumValueName(Uint)); // etmvariableload fixed
|
||
loadItem.insert("o:maxPowerW", enumValueName(Uint)); // etmvariableload dynamic
|
||
loadItem.insert("o:needs", needsItem);
|
||
loadItem.insert("o:relays", QVariantList() << relayItem); // relay-router (rév. 3)
|
||
// LM-300 — charge utile du mécanisme sg-ready. estimatedPowerW porte son nom : c'est une
|
||
// ESTIMATION (une PAC ne consomme pas pareil à −5 °C et +12 °C), jamais un engagement.
|
||
QVariantMap sgStateItem;
|
||
sgStateItem.insert("state", enumValueName(Uint));
|
||
sgStateItem.insert("relays", QVariantList() << enumValueName(String));
|
||
sgStateItem.insert("estimatedPowerW", enumValueName(Double));
|
||
QVariantMap sgItem;
|
||
// « states » est OPTIONNEL ici, et c'est le mur documenté en LM-302 : GET sérialise la
|
||
// charge via le méta-objet, donc TOUTES les propriétés déclarées — y compris un sgReady
|
||
// VIDE pour un relay-router. Exiger « states » à l'intérieur casserait le va-et-vient
|
||
// Get → Set de l'app sur toute charge non SG-Ready. Défaut constaté au banc le
|
||
// 2026-08-10, sur la première tentative réelle de SetLoadConfig.
|
||
// L'exigence réelle (states non vide, état 2 présent) est tenue par LoadConfig::isValid().
|
||
sgItem.insert("o:states", QVariantList() << sgStateItem);
|
||
sgItem.insert("o:minStateHoldS", enumValueName(Uint));
|
||
loadItem.insert("o:sgReady", sgItem); // sg-ready (LM-300)
|
||
loadItem.insert("o:minOnS", enumValueName(Uint));
|
||
loadItem.insert("o:minOffS", enumValueName(Uint));
|
||
params.insert("loadConfigs", QVariantList() << loadItem);
|
||
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
||
registerMethod("SetLoadConfig", description, params, returns, Types::PermissionScopeControlThings);
|
||
|
||
// [Phase 2] Ratios canoniques autoconsommation / autonomie (% [0,100]). Champ omis
|
||
// (jamais null) quand n/a (dénominateur ≤ 0). Source canonique de l'energymanager —
|
||
// remplace le seam interim app-side.
|
||
params.clear(); returns.clear();
|
||
description = "Get the current energy ratios (self-consumption / autonomy) in percent, "
|
||
"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.insert("o:selfConsumptionRate", enumValueName(Double));
|
||
returns.insert("o:autonomyRate", enumValueName(Double));
|
||
registerMethod("GetEnergyRatios", description, params, returns, Types::PermissionScopeControlThings);
|
||
|
||
// [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
|
||
// levée doit donc être un acte tracé, pas un effet de bord d'un autre appel.
|
||
params.clear(); returns.clear();
|
||
description = "Clear the fault latch of a controllable load (ECS-410). Deliberate operator "
|
||
"action: the fault never clears by itself.";
|
||
params.insert("loadId", enumValueName(String));
|
||
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
||
registerMethod("ClearLoadFault", description, params, returns, Types::PermissionScopeControlThings);
|
||
|
||
// [ETM] Frontière de télémétrie d'arbitrage. AUCUNE phrase composée ici : des codes et des
|
||
// paramètres. La locale nymea est par CONNEXION (JSONRPC.Hello o:locale) et ce handler n'a
|
||
// pas de pluginId, donc il est hors du catalogue de traduction : une phrase française émise
|
||
// ici ne serait rattrapée par rien. Cf. etm/types/decisionreason.h.
|
||
params.clear(); returns.clear();
|
||
description = "Get a snapshot of the arbitration runtime state: surplus budget, per-load "
|
||
"allocation, availability, fault code, active lock (seconds remaining), "
|
||
"mechanism payload and decision reason. Reason and fault are CODES with "
|
||
"numeric params - never human-readable sentences: the client renders them in "
|
||
"the user's language. Identical payload to the LoadTelemetryChanged "
|
||
"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);
|
||
|
||
|
||
// Notifications
|
||
params.clear();
|
||
description = "Emitted whenever the phase power limit configuration changes.";
|
||
params.insert("phasePowerLimit", enumValueName(Uint));
|
||
registerNotification("PhasePowerLimitChanged", description, params);
|
||
connect(m_smartChargingManager, &SmartChargingManager::phasePowerLimitChanged, this, [=](int phasePowerLimit){
|
||
emit PhasePowerLimitChanged({{"phasePowerLimit", phasePowerLimit}});
|
||
});
|
||
|
||
params.clear();
|
||
description = "Emitted whenever the acquisition tolerance changes.";
|
||
params.insert("acquisitionTolerance", enumValueName(Double));
|
||
registerNotification("AcquisitionToleranceChanged", description, params);
|
||
connect(m_smartChargingManager, &SmartChargingManager::acquisitionToleranceChanged, this, [=](double acquisitionTolerance){
|
||
emit AcquisitionToleranceChanged({{"acquisitionTolerance", acquisitionTolerance}});
|
||
});
|
||
|
||
params.clear();
|
||
description = "Emitted whenever the battery level consideration changes.";
|
||
params.insert("batteryLevelConsideration", enumValueName(Double));
|
||
registerNotification("BatteryLevelConsiderationChanged", description, params);
|
||
connect(m_smartChargingManager, &SmartChargingManager::batteryLevelConsiderationChanged, this, [=](double batteryLevelConsideration){
|
||
emit BatteryLevelConsiderationChanged({{"batteryLevelConsideration", batteryLevelConsideration}});
|
||
});
|
||
|
||
params.clear();
|
||
description = "Emitted whenever the lock on unplug setting changes.";
|
||
params.insert("lockOnUnplug", enumValueName(Bool));
|
||
registerNotification("LockOnUnplugChanged", description, params);
|
||
connect(m_smartChargingManager, &SmartChargingManager::lockOnUnplugChanged, this, [=](bool lockOnUnplug){
|
||
emit LockOnUnplugChanged({{"lockOnUnplug", lockOnUnplug}});
|
||
});
|
||
|
||
params.clear();
|
||
description = "Emitted whenever a charging info is added.";
|
||
params.insert("chargingInfo", objectRef<ChargingInfo>());
|
||
registerNotification("ChargingInfoAdded", description, params);
|
||
|
||
params.clear();
|
||
description = "Emitted whenever a charging info is removed.";
|
||
params.insert("evChargerThingId", enumValueName(Uuid));
|
||
registerNotification("ChargingInfoRemoved", description, params);
|
||
|
||
params.clear();
|
||
description = "Emitted whenever a charging info changes.";
|
||
params.insert("chargingInfo", objectRef<ChargingInfo>());
|
||
registerNotification("ChargingInfoChanged", description, params);
|
||
|
||
params.clear();
|
||
description = "Emitted whenever a spot market configuration has changed.";
|
||
params.insert("o:providerId", enumValueName(Uuid));
|
||
params.insert("enabled", enumValueName(Bool));
|
||
params.insert("available", enumValueName(Bool));
|
||
registerNotification("SpotMarketConfigurationChanged", description, params);
|
||
|
||
params.clear();
|
||
description = "Emitted whenever the planed charging schedules have changed.";
|
||
params.insert("chargingSchedules", QVariantList() << objectRef<ChargingSchedule>());
|
||
// [ETM] degradedMode : true quand le watchdog L2 a basculé en repli (compteur muet).
|
||
// Optionnel (additif, rétro-compatible) ; émis aussi aux transitions du mode dégradé.
|
||
params.insert("o:degradedMode", enumValueName(Bool));
|
||
registerNotification("ChargingSchedulesChanged", description, params);
|
||
|
||
// Charing manager
|
||
connect(m_smartChargingManager, &SmartChargingManager::chargingInfoAdded, this, [=](const ChargingInfo &chargingInfo) {
|
||
QVariantMap params;
|
||
params.insert("chargingInfo", pack(chargingInfo));
|
||
emit ChargingInfoAdded(params);
|
||
});
|
||
connect(m_smartChargingManager, &SmartChargingManager::chargingInfoRemoved, this, [=](const ThingId &evChargerThingId){
|
||
QVariantMap params;
|
||
params.insert("evChargerThingId", evChargerThingId);
|
||
emit ChargingInfoRemoved(params);
|
||
});
|
||
connect(m_smartChargingManager, &SmartChargingManager::chargingInfoChanged, this, [=](const ChargingInfo chargingInfo){
|
||
QVariantMap params;
|
||
params.insert("chargingInfo", pack(chargingInfo));
|
||
emit ChargingInfoChanged(params);
|
||
});
|
||
|
||
connect(m_smartChargingManager, &SmartChargingManager::chargingSchedulesChanged, this, [=](){
|
||
QVariantMap params;
|
||
QVariantList schedules;
|
||
foreach (const ChargingSchedule &schedule, m_smartChargingManager->chargingSchedules()) {
|
||
schedules << pack<ChargingSchedule>(schedule);
|
||
}
|
||
params.insert("chargingSchedules", schedules);
|
||
params.insert("degradedMode", m_smartChargingManager->degradedMode()); // [ETM] L2
|
||
emit ChargingSchedulesChanged(params);
|
||
});
|
||
|
||
// [ETM] LoadConfigChanged : émise après toute persistance réussie (SetLoadConfig ou
|
||
// édition externe du fichier relue). Porte la liste complète à jour.
|
||
params.clear();
|
||
description = "Emitted whenever the controllable load configuration changed.";
|
||
params.insert("loadConfigs", QVariantList() << objectRef<LoadConfig>());
|
||
registerNotification("LoadConfigChanged", description, params);
|
||
if (m_loadConfigStore) {
|
||
connect(m_loadConfigStore, &LoadConfigStore::changed, this, [this](){
|
||
QVariantMap params;
|
||
QVariantList list;
|
||
foreach (const LoadConfig &c, m_loadConfigStore->configs())
|
||
list << pack<LoadConfig>(c);
|
||
params.insert("loadConfigs", list);
|
||
emit LoadConfigChanged(params);
|
||
});
|
||
}
|
||
|
||
// [ETM] LoadTelemetryChanged : MÊME charge utile que GetLoadTelemetry (schéma partagé, une
|
||
// seule déclaration). Émise sur changement SIGNIFICATIF (mode dégradé, jeu de charges,
|
||
// disponibilité, défaut, nature de verrou, mécanisme, code de motif, ou variation
|
||
// d'allocation au-delà du seuil) ET à intervalle périodique même sans changement — c'est
|
||
// ce battement qui rend un arbitre FIGÉ détectable, le timestamp cessant d'avancer.
|
||
params.clear();
|
||
description = "Emitted whenever the arbitration runtime state changed significantly, and "
|
||
"periodically (heartbeat) even when unchanged, so that a frozen arbitrator is "
|
||
"detectable by a timestamp that stops advancing. Same payload as "
|
||
"GetLoadTelemetry.";
|
||
params = loadTelemetrySchema();
|
||
registerNotification("LoadTelemetryChanged", description, params);
|
||
#ifdef ETM_ARBITRATOR
|
||
if (EnergyArbitrator *arb = dynamic_cast<EnergyArbitrator *>(m_smartChargingManager)) {
|
||
connect(arb, &EnergyArbitrator::loadTelemetryChanged, this, [this](const QVariantMap &t) {
|
||
Q_UNUSED(t) // on republie la vue NORMALISÉE, pas la carte brute : c'est
|
||
// currentLoadTelemetry() qui garantit les clés requises du schéma.
|
||
emit LoadTelemetryChanged(currentLoadTelemetry());
|
||
});
|
||
}
|
||
#endif
|
||
|
||
// [Phase 2] EnergyRatiosChanged : émise sur powerBalanceChanged() de l'energymanager,
|
||
// uniquement quand une valeur (ou sa disponibilité) change. Mêmes champs que GetEnergyRatios.
|
||
params.clear();
|
||
description = "Emitted whenever the energy ratios (self-consumption / autonomy) change. "
|
||
"A rate field is omitted (never null) when not available.";
|
||
params.insert("o:selfConsumptionRate", enumValueName(Double));
|
||
params.insert("o:autonomyRate", enumValueName(Double));
|
||
registerNotification("EnergyRatiosChanged", description, params);
|
||
if (m_energyManager) {
|
||
connect(m_energyManager, &EnergyManager::powerBalanceChanged,
|
||
this, &NymeaEnergyJsonHandler::onPowerBalanceChanged);
|
||
}
|
||
|
||
// Spot market manager
|
||
params.clear();
|
||
description = "Emitted whenever the spot market manager status changed.";
|
||
params.insert("enabled", enumValueName(Bool));
|
||
params.insert("available", enumValueName(Bool));
|
||
registerNotification("SpotMarketStatusChanged", description, params);
|
||
connect(m_spotMarketManager, &SpotMarketManager::enabledChanged, this, [this](bool){
|
||
sendSpotMarketConfigurationChangedNotification();
|
||
});
|
||
|
||
connect(m_spotMarketManager, &SpotMarketManager::availableChanged, this, [this](bool){
|
||
sendSpotMarketConfigurationChangedNotification();
|
||
});
|
||
|
||
connect(m_spotMarketManager, &SpotMarketManager::currentProviderChanged, this, [this](SpotMarketDataProvider *){
|
||
sendSpotMarketConfigurationChangedNotification();
|
||
});
|
||
|
||
params.clear();
|
||
description = "Emitted whenever the score entries of the current spot market provider changed. The weighting of the available score entries goes from 0 (worst) to 1.0 (best).";
|
||
params.insert("spotMarketScoreEntries", QVariantList() << objectRef<ScoreEntry>());
|
||
registerNotification("SpotMarketScoreEntriesChanged", description, params);
|
||
connect(m_spotMarketManager, &SpotMarketManager::scoreEntriesUpdated, this, [=](){
|
||
|
||
QVariantList entries;
|
||
if (m_spotMarketManager->currentProvider() && m_spotMarketManager->enabled()) {
|
||
// Get all scores weighted (we want all available)
|
||
ScoreEntries weightedEntries = m_spotMarketManager->weightedScoreEntries();
|
||
foreach (const ScoreEntry &entry, weightedEntries) {
|
||
entries << pack<ScoreEntry>(entry);
|
||
}
|
||
}
|
||
|
||
QVariantMap params;
|
||
params.insert("spotMarketScoreEntries", entries);
|
||
emit SpotMarketScoreEntriesChanged(params);
|
||
});
|
||
}
|
||
|
||
QString NymeaEnergyJsonHandler::name() const
|
||
{
|
||
return "NymeaEnergy";
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::GetPhasePowerLimit(const QVariantMap ¶ms)
|
||
{
|
||
Q_UNUSED(params)
|
||
QVariantMap returns;
|
||
returns.insert("phasePowerLimit", m_smartChargingManager->phasePowerLimit());
|
||
return createReply(returns);
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::SetPhasePowerLimit(const QVariantMap ¶ms)
|
||
{
|
||
int phasePowerLimit = params.value("phasePowerLimit").toUInt();
|
||
m_smartChargingManager->setPhasePowerLimit(phasePowerLimit);
|
||
QVariantMap returns;
|
||
returns.insert("energyError", enumValueName(EnergyManager::EnergyErrorNoError));
|
||
return createReply(returns);
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::GetAcquisitionTolerance(const QVariantMap ¶ms)
|
||
{
|
||
Q_UNUSED(params)
|
||
return createReply({{"acquisitionTolerance", m_smartChargingManager->acquisitionTolerance()}});
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::SetAcquisitionTolerance(const QVariantMap ¶ms)
|
||
{
|
||
double acquisitionTolerance = params.value("acquisitionTolerance").toDouble();
|
||
if (acquisitionTolerance < 0.0 || acquisitionTolerance > 1.0)
|
||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
||
|
||
m_smartChargingManager->setAcquisitionTolerance(acquisitionTolerance);
|
||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::GetBatteryLevelConsideration(const QVariantMap ¶ms)
|
||
{
|
||
Q_UNUSED(params)
|
||
return createReply({{"batteryLevelConsideration", m_smartChargingManager->batteryLevelConsideration()}});
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::SetBatteryLevelConsideration(const QVariantMap ¶ms)
|
||
{
|
||
double batteryLevelConsideration = params.value("batteryLevelConsideration").toDouble();
|
||
if (batteryLevelConsideration < 0.0 || batteryLevelConsideration > 1.0)
|
||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
||
|
||
m_smartChargingManager->setBatteryLevelConsideration(batteryLevelConsideration);
|
||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
||
}
|
||
|
||
JsonReply* NymeaEnergyJsonHandler::GetChargingInfos(const QVariantMap ¶ms)
|
||
{
|
||
Q_UNUSED(params)
|
||
QVariantMap returns;
|
||
returns.insert("chargingInfos", pack(m_smartChargingManager->chargingInfos()));
|
||
return createReply(returns);
|
||
}
|
||
|
||
JsonReply* NymeaEnergyJsonHandler::SetChargingInfo(const QVariantMap ¶ms, const JsonContext &context)
|
||
{
|
||
ChargingInfo chargingInfo = unpack<ChargingInfo>(params.value("chargingInfo"));
|
||
chargingInfo.setLocale(context.locale());
|
||
EnergyManager::EnergyError status = m_smartChargingManager->setChargingInfo(chargingInfo);
|
||
QVariantMap returns;
|
||
returns.insert("energyError", enumValueName(status));
|
||
return createReply(returns);
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::GetLockOnUnplug(const QVariantMap ¶ms)
|
||
{
|
||
Q_UNUSED(params)
|
||
return createReply({{"lockOnUnplug", m_smartChargingManager->lockOnUnplug()}});
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::SetLockOnUnplug(const QVariantMap ¶ms)
|
||
{
|
||
bool lockOnUnplug = params.value("lockOnUnplug").toBool();
|
||
m_smartChargingManager->setLockOnUnplug(lockOnUnplug);
|
||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::GetAvailableSpotMarketProviders(const QVariantMap ¶ms)
|
||
{
|
||
Q_UNUSED(params)
|
||
QVariantMap returns;
|
||
returns.insert("providers", pack(m_spotMarketManager->availableProviders()));
|
||
return createReply(returns);
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::GetSpotMarketConfiguration(const QVariantMap ¶ms)
|
||
{
|
||
Q_UNUSED(params)
|
||
|
||
QVariantMap returns;
|
||
returns.insert("enabled", m_spotMarketManager->enabled());
|
||
returns.insert("available", m_spotMarketManager->available());
|
||
if (!m_spotMarketManager->currentProviderId().isNull())
|
||
returns.insert("providerId", m_spotMarketManager->currentProviderId());
|
||
|
||
return createReply(returns);
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::SetSpotMarketConfiguration(const QVariantMap ¶ms)
|
||
{
|
||
bool enable = params.value("enabled").toBool();
|
||
QUuid providerId = params.value("providerId").toUuid();
|
||
|
||
EnergyManager::EnergyError error = EnergyManager::EnergyErrorNoError;
|
||
|
||
if (enable && providerId.isNull()) {
|
||
qCWarning(dcNymeaEnergy()) << "Could not enable spot market because ther is no valid provider id given.";
|
||
error = EnergyManager::EnergyErrorInvalidParameter;
|
||
} else if (enable && !providerId.isNull()) {
|
||
if (!m_spotMarketManager->changeProvider(providerId)) {
|
||
error = EnergyManager::EnergyErrorInvalidParameter;
|
||
}
|
||
}
|
||
|
||
if (error == EnergyManager::EnergyErrorNoError) {
|
||
m_spotMarketManager->setEnabled(enable);
|
||
}
|
||
|
||
QVariantMap returns;
|
||
returns.insert("energyError", enumValueName(error));
|
||
return createReply(returns);
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::GetSpotMarketScoreEntries(const QVariantMap ¶ms)
|
||
{
|
||
Q_UNUSED(params)
|
||
|
||
QVariantMap returns;
|
||
if (m_spotMarketManager->currentProvider() && m_spotMarketManager->enabled()) {
|
||
QVariantList entries;
|
||
if (m_spotMarketManager->currentProvider() && m_spotMarketManager->enabled()) {
|
||
ScoreEntries weightedEntries = SpotMarketManager::weightScoreEntries(m_spotMarketManager->currentProvider()->scoreEntries());
|
||
foreach (const ScoreEntry &entry, weightedEntries) {
|
||
entries << pack<ScoreEntry>(entry);
|
||
}
|
||
}
|
||
returns.insert("spotMarketScoreEntries", entries);
|
||
} else {
|
||
returns.insert("spotMarketScoreEntries", QVariantList());
|
||
}
|
||
return createReply(returns);
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::GetChargingSchedules(const QVariantMap ¶ms)
|
||
{
|
||
Q_UNUSED(params)
|
||
|
||
QVariantMap returns;
|
||
QVariantList schedules;
|
||
foreach (const ChargingSchedule &schedule, m_smartChargingManager->chargingSchedules()) {
|
||
schedules << pack<ChargingSchedule>(schedule);
|
||
}
|
||
returns.insert("chargingSchedules", schedules);
|
||
returns.insert("degradedMode", m_smartChargingManager->degradedMode()); // [ETM] L2, cf. supra
|
||
return createReply(returns);
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::GetLoadConfig(const QVariantMap ¶ms)
|
||
{
|
||
Q_UNUSED(params)
|
||
QVariantMap returns;
|
||
QVariantList list;
|
||
if (m_loadConfigStore) {
|
||
foreach (const LoadConfig &c, m_loadConfigStore->configs())
|
||
list << pack<LoadConfig>(c); // inclut les charges enabled==false (éditables par l'app)
|
||
}
|
||
returns.insert("loadConfigs", list);
|
||
return createReply(returns);
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::SetLoadConfig(const QVariantMap ¶ms)
|
||
{
|
||
if (!m_loadConfigStore) {
|
||
qCWarning(dcNymeaEnergy()) << "[NymeaEnergy] SetLoadConfig sans store configuré.";
|
||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
||
}
|
||
|
||
LoadConfigs configs;
|
||
foreach (const QVariant &item, params.value("loadConfigs").toList())
|
||
configs.append(LoadConfig::fromMap(item.toMap()));
|
||
|
||
QString why;
|
||
if (!m_loadConfigStore->setConfigs(configs, &why)) {
|
||
qCWarning(dcNymeaEnergy()) << "[NymeaEnergy] SetLoadConfig rejetée:" << why;
|
||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
||
}
|
||
// store.setConfigs() a déjà persisté + émis changed() → l'arbitre reconstruit, la notif part.
|
||
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
||
}
|
||
|
||
// [Phase 2] Construit le map de ratios courant depuis les cumuls de l'energymanager.
|
||
// Champ omis (jamais null) quand n/a. Met à jour l'état caché (m_lastRatios).
|
||
QVariantMap NymeaEnergyJsonHandler::currentEnergyRatios()
|
||
{
|
||
QVariantMap result;
|
||
if (!m_energyManager)
|
||
return result;
|
||
|
||
const EnergyRatiosCalculator::Ratios r = m_ratiosCalculator.compute(
|
||
m_energyManager->totalProduction(),
|
||
m_energyManager->totalReturn(),
|
||
m_energyManager->totalConsumption(),
|
||
m_energyManager->totalAcquisition(),
|
||
QDateTime::currentDateTime());
|
||
|
||
m_lastRatios = r;
|
||
m_lastRatiosValid = true;
|
||
|
||
if (r.autoconsommationValid)
|
||
result.insert("selfConsumptionRate", r.autoconsommation);
|
||
if (r.autonomieValid)
|
||
result.insert("autonomyRate", r.autonomie);
|
||
return result;
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::ClearLoadFault(const QVariantMap ¶ms)
|
||
{
|
||
#ifdef ETM_ARBITRATOR
|
||
EnergyArbitrator *arb = dynamic_cast<EnergyArbitrator *>(m_smartChargingManager);
|
||
if (arb && arb->clearLoadFault(params.value("loadId").toString())) {
|
||
QVariantMap ret;
|
||
ret.insert("energyError", enumValueName(EnergyManager::EnergyErrorNoError));
|
||
return createReply(ret);
|
||
}
|
||
#else
|
||
Q_UNUSED(params)
|
||
#endif
|
||
QVariantMap ret;
|
||
ret.insert("energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter));
|
||
return createReply(ret);
|
||
}
|
||
|
||
QVariantMap NymeaEnergyJsonHandler::loadTelemetrySchema()
|
||
{
|
||
// Verrou : le TEMPS RESTANT, pas des watts. lockMinPowerW/lockMaxPowerW sont un artefact du
|
||
// mécanisme d'allocation, illisible pour un utilisateur. Ceci n'enfreint pas ECS-306 :
|
||
// cette exigence interdit qu'un index de PALIER franchisse la frontière d'ALLOCATION ; une
|
||
// valeur d'AFFICHAGE sortante est un autre chemin.
|
||
QVariantMap lockItem;
|
||
lockItem.insert("kind", enumValueName(String)); // "minOn" | "minOff" | "minStateHold"
|
||
lockItem.insert("remainingS", enumValueName(Int));
|
||
|
||
// UNION DISCRIMINÉE par mécanisme, comme LoadConfig : « kind » discrimine, chaque variante
|
||
// porte SA charge utile. LM-302 s'applique tel quel — TOUTES les clés internes sont
|
||
// optionnelles, sinon la charge utile d'un mécanisme contraindrait celle des autres.
|
||
QVariantMap mechItem;
|
||
mechItem.insert("kind", enumValueName(String)); // "relay" | "variable" | "sgReady"
|
||
mechItem.insert("o:stageW", enumValueName(Double)); // relay
|
||
mechItem.insert("o:maxStageW", enumValueName(Double)); // relay
|
||
mechItem.insert("o:setpointW", enumValueName(Double)); // variable
|
||
mechItem.insert("o:percent", enumValueName(Double)); // variable
|
||
mechItem.insert("o:maxPowerW", enumValueName(Double)); // variable
|
||
mechItem.insert("o:state", enumValueName(Int)); // sgReady
|
||
mechItem.insert("o:estimatedPowerW", enumValueName(Double)); // sgReady
|
||
mechItem.insert("o:chargingEnabled", enumValueName(Bool)); // evcharger (réservé 3g)
|
||
mechItem.insert("o:pluggedIn", enumValueName(Bool)); // evcharger (réservé 3g)
|
||
mechItem.insert("o:currentA", enumValueName(Double)); // evcharger (réservé 3g)
|
||
mechItem.insert("o:phaseCount", enumValueName(Uint)); // evcharger (réservé 3g)
|
||
|
||
// Motif : un CODE et ses paramètres. « params » est typé Variant parce que ses clés
|
||
// DÉPENDENT du code — les figer au schéma reviendrait à publier N objets au lieu d'un, et à
|
||
// casser le contrat à chaque motif ajouté. Le catalogue des codes et de leurs paramètres
|
||
// fait autorité dans INTERFACE.md et dans etm/types/decisionreason.h.
|
||
QVariantMap decisionItem;
|
||
decisionItem.insert("code", enumValueName(String));
|
||
decisionItem.insert("params", enumValueName(Variant));
|
||
|
||
QVariantMap loadItem;
|
||
loadItem.insert("loadId", enumValueName(String));
|
||
// ALLOCATION du waterfall (ce qui a décrémenté le budget), pas la mesure : la mesure vit
|
||
// dans la charge utile de mécanisme. La somme des allocatedW retombe sur budget.allocatedW.
|
||
loadItem.insert("allocatedW", enumValueName(Double));
|
||
loadItem.insert("available", enumValueName(Bool));
|
||
loadItem.insert("o:faultCode", enumValueName(String)); // absent = aucun défaut
|
||
loadItem.insert("o:lock", lockItem); // absent = aucun verrou actif
|
||
loadItem.insert("o:mechanism", mechItem);
|
||
loadItem.insert("decision", decisionItem);
|
||
|
||
QVariantMap budgetItem;
|
||
budgetItem.insert("surplusW", enumValueName(Double));
|
||
budgetItem.insert("evReservedW", enumValueName(Double));
|
||
budgetItem.insert("allocatedW", enumValueName(Double));
|
||
// Sans « recreditedW » l'identité surplus − alloué == restant serait fausse dès qu'une
|
||
// charge consomme déjà, et le client chercherait l'écart. Cf. PlanBudget.
|
||
budgetItem.insert("recreditedW", enumValueName(Double));
|
||
budgetItem.insert("remainingW", enumValueName(Double));
|
||
|
||
QVariantMap schema;
|
||
// Omis, jamais nul (règle maison, cf. GetEnergyRatios) :
|
||
// - timestamp absent = aucun cycle d'arbitrage depuis le démarrage. En forger un se
|
||
// lirait « cycle exécuté », soit exactement le mensonge que ce champ existe pour
|
||
// empêcher. Présent, c'est la FIN du dernier cycle — la preuve de fraîcheur.
|
||
// - budget absent = mode dégradé L2 : la planification est SUSPENDUE, il n'existe
|
||
// aucun budget pour ce cycle. Publier des zéros se lirait « rien à allouer ».
|
||
schema.insert("o:timestamp", enumValueName(String)); // ISO 8601 UTC
|
||
schema.insert("degradedMode", enumValueName(Bool));
|
||
schema.insert("o:budget", budgetItem);
|
||
schema.insert("loads", QVariantList() << loadItem);
|
||
return schema;
|
||
}
|
||
|
||
QVariantMap NymeaEnergyJsonHandler::currentLoadTelemetry() const
|
||
{
|
||
QVariantMap t;
|
||
#ifdef ETM_ARBITRATOR
|
||
if (EnergyArbitrator *arb = dynamic_cast<EnergyArbitrator *>(m_smartChargingManager))
|
||
t = arb->loadTelemetry();
|
||
#endif
|
||
// Carte minimale tant qu'aucun cycle n'a eu lieu : les clés REQUISES du schéma sont posées,
|
||
// « timestamp » ne l'est pas. Le client lit donc « pas encore de cycle », et non un cycle
|
||
// fictif à budget nul.
|
||
if (!t.contains(QStringLiteral("degradedMode")))
|
||
t.insert(QStringLiteral("degradedMode"),
|
||
m_smartChargingManager && m_smartChargingManager->degradedMode());
|
||
if (!t.contains(QStringLiteral("loads")))
|
||
t.insert(QStringLiteral("loads"), QVariantList());
|
||
return t;
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::GetLoadTelemetry(const QVariantMap ¶ms)
|
||
{
|
||
Q_UNUSED(params)
|
||
return createReply(currentLoadTelemetry());
|
||
}
|
||
|
||
JsonReply *NymeaEnergyJsonHandler::GetEnergyRatios(const QVariantMap ¶ms)
|
||
{
|
||
Q_UNUSED(params)
|
||
return createReply(currentEnergyRatios());
|
||
}
|
||
|
||
void NymeaEnergyJsonHandler::onPowerBalanceChanged()
|
||
{
|
||
const EnergyRatiosCalculator::Ratios prev = m_lastRatios;
|
||
const bool hadPrev = m_lastRatiosValid;
|
||
|
||
const QVariantMap ratios = currentEnergyRatios();
|
||
const EnergyRatiosCalculator::Ratios &now = m_lastRatios;
|
||
|
||
// N'émet que si une valeur (ou sa disponibilité) a réellement bougé.
|
||
auto changed = [](bool prevValid, double prevVal, bool nowValid, double nowVal) {
|
||
if (prevValid != nowValid)
|
||
return true;
|
||
return nowValid && qAbs(prevVal - nowVal) > 1e-6;
|
||
};
|
||
|
||
const bool moved = !hadPrev
|
||
|| changed(prev.autoconsommationValid, prev.autoconsommation, now.autoconsommationValid, now.autoconsommation)
|
||
|| changed(prev.autonomieValid, prev.autonomie, now.autonomieValid, now.autonomie);
|
||
|
||
if (moved)
|
||
emit EnergyRatiosChanged(ratios);
|
||
}
|
||
|
||
void NymeaEnergyJsonHandler::sendSpotMarketConfigurationChangedNotification()
|
||
{
|
||
QVariantMap params;
|
||
params.insert("enabled", m_spotMarketManager->enabled());
|
||
params.insert("available", m_spotMarketManager->available());
|
||
if (m_spotMarketManager->enabled())
|
||
params.insert("providerId", m_spotMarketManager->currentProviderId());
|
||
|
||
emit SpotMarketConfigurationChanged(params);
|
||
}
|
||
|