etm-powersync-energy-plugin.../energyplugin/nymeaenergyjsonhandler.cpp
Patrick Schurig 92d0bef4ac fix(etm): ECS-410 — échec d'écriture relais, échelle bornée et défaut collant
writeRelay() jetait le ThingActionInfo* : aucun acquittement, aucun retour
arrière, available codé en dur à true, et m_currentStage mis à jour comme si tout
avait réussi — on annonçait une puissance non appliquée.

MODÈLE ASYNCHRONE. executeAction est asynchrone et update() ne doit jamais
attendre (AGENTS règle 5). « Attendre le résultat » ne veut donc pas dire bloquer
le cycle : l'écriture est émise, l'adaptateur retient combien d'acquittements il
attend (m_pending), le verdict tombe quand le compteur retombe à zéro, et la
conséquence est traitée au cycle suivant. Motif repris tel quel d'EvCharger
(evcharger.cpp:293-299), y compris `this` en contexte de connexion — si
l'adaptateur meurt, les callbacks sont coupés proprement.

INDÉTERMINATION. Pendant une transition, telemetry() annonce
max(m_stagePrev, m_stageTarget) : on ne sait pas ce qui est fermé, on annonce donc
la plus haute des deux puissances possibles. Même direction qu'ECS-411 (relais
injoignable supposé fermé) — ne jamais annoncer moins que ce qui peut être
appliqué. Sous-estimer fait sur-allouer les charges suivantes ; surestimer ne fait
que retarder une montée.

ÉCHELLE BORNÉE à trois barreaux, une tentative chacun, aucune boucle : cible →
retour arrière → arrêt total → défaut. Le retour arrière est asynchrone au même
titre et passe par le même compteur.

DÉFAUT COLLANT, pas clignotant. m_faulted est un verrou posé une seule fois, sans
délai ni expiration : available ne peut pas osciller d'un cycle à l'autre. Seul
NymeaEnergy.ClearLoadFault le lève — acte délibéré et journalisé de l'opérateur.
La reconstruction le lève aussi, mais par construction : un adaptateur neuf n'a
pas d'historique. À la levée, l'état matériel est RELU (ECS-411), pas supposé.

CANAL OUVERT. LoadContext n'avait AUCUN champ available : le publier aurait été
décoratif. Ajouté à LoadContextTelemetry, avec sa sémantique écrite noir sur
blanc — il gouverne l'allocation, PAS la comptabilité. Une charge en défaut ne
reçoit rien mais reste comptée : une puissance qu'on ne sait plus couper est de la
conso fixe, au même titre que la base de la maison. Le figeage est porté par
lockMin == lockMax == puissance crue engagée, jamais un plafond nul sous un
plancher non nul. Le même canal servira ECS-601.

OPTIMIZER_PROTOCOL.md mis à jour dans le MÊME lot, comme l'exige le §11 de la
spec : available, lockMinPowerW et lockMaxPowerW documentés avec leur sémantique.

clearFault() est PURE VIRTUELLE sur ILoadAdapter : les trois autres adaptateurs la
déclarent sans effet plutôt que d'hériter d'un défaut vide. Leur généralisation est
portée par ECS-414.

Test testEcsPartialFailure : cas nominal sans défaut, puis échelle complète via un
relais introuvable — défaut atteint, relais valide bien ramené à l'ouverture par la
tentative d'arrêt total, available faux, plancher == plafond == puissance comptée,
plus aucune commande une heure plus tard, puis levée délibérée.

Build amd64 0 erreur. Simulation : 16/16.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 11:32:51 +02:00

650 lines
30 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.";
returns.insert("chargingSchedules", QVariantList() << objectRef<ChargingSchedule>());
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));
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)
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);
// 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);
});
}
// [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 &params)
{
Q_UNUSED(params)
QVariantMap returns;
returns.insert("phasePowerLimit", m_smartChargingManager->phasePowerLimit());
return createReply(returns);
}
JsonReply *NymeaEnergyJsonHandler::SetPhasePowerLimit(const QVariantMap &params)
{
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 &params)
{
Q_UNUSED(params)
return createReply({{"acquisitionTolerance", m_smartChargingManager->acquisitionTolerance()}});
}
JsonReply *NymeaEnergyJsonHandler::SetAcquisitionTolerance(const QVariantMap &params)
{
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 &params)
{
Q_UNUSED(params)
return createReply({{"batteryLevelConsideration", m_smartChargingManager->batteryLevelConsideration()}});
}
JsonReply *NymeaEnergyJsonHandler::SetBatteryLevelConsideration(const QVariantMap &params)
{
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 &params)
{
Q_UNUSED(params)
QVariantMap returns;
returns.insert("chargingInfos", pack(m_smartChargingManager->chargingInfos()));
return createReply(returns);
}
JsonReply* NymeaEnergyJsonHandler::SetChargingInfo(const QVariantMap &params, 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 &params)
{
Q_UNUSED(params)
return createReply({{"lockOnUnplug", m_smartChargingManager->lockOnUnplug()}});
}
JsonReply *NymeaEnergyJsonHandler::SetLockOnUnplug(const QVariantMap &params)
{
bool lockOnUnplug = params.value("lockOnUnplug").toBool();
m_smartChargingManager->setLockOnUnplug(lockOnUnplug);
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
}
JsonReply *NymeaEnergyJsonHandler::GetAvailableSpotMarketProviders(const QVariantMap &params)
{
Q_UNUSED(params)
QVariantMap returns;
returns.insert("providers", pack(m_spotMarketManager->availableProviders()));
return createReply(returns);
}
JsonReply *NymeaEnergyJsonHandler::GetSpotMarketConfiguration(const QVariantMap &params)
{
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 &params)
{
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 &params)
{
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 &params)
{
Q_UNUSED(params)
QVariantMap returns;
QVariantList schedules;
foreach (const ChargingSchedule &schedule, m_smartChargingManager->chargingSchedules()) {
schedules << pack<ChargingSchedule>(schedule);
}
returns.insert("chargingSchedules", schedules);
return createReply(returns);
}
JsonReply *NymeaEnergyJsonHandler::GetLoadConfig(const QVariantMap &params)
{
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 &params)
{
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 &params)
{
#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);
}
JsonReply *NymeaEnergyJsonHandler::GetEnergyRatios(const QVariantMap &params)
{
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);
}