- Add ManualSlotConfig type with weekly repeating support and auto-expiry - Add ManualStrategy (strategyId="manual"): applies user-defined allocations exactly; expired slots logged and skipped; inflexible/critical loads always applied as safety fallback; decisionReason never empty - Extend SchedulerSettings with manualSlots persistence section (INI array) - Extend SchedulerManager with setManualSlot/removeManualSlot/clearManualSlots methods; hydrates ManualStrategy from settings on registerStrategy() - Add JSON-RPC v11 methods: GetManualSlots, SetManualSlot, RemoveManualSlot, ClearManualSlots + ManualSlotActivated push notification - Register ManualStrategy in energypluginnymea.cpp::init() (no feature flag) - Add 5 unit tests: basicSlot, noConfig_fallback, expiredSlot, repeatingSlot, persistence (JSON round-trip) - Update doc.md section 11 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
898 lines
41 KiB
C++
898 lines
41 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"
|
|
#include "types/charginginfo.h"
|
|
#include "smartchargingmanager.h"
|
|
#include "spotmarket/spotmarketmanager.h"
|
|
#include "schedulermanager.h"
|
|
#include "schedulingstrategies/manualstrategy.h"
|
|
|
|
#include <energymanager.h>
|
|
|
|
#include <QLoggingCategory>
|
|
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
|
|
|
NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketManager,
|
|
SmartChargingManager *smartChargingManager,
|
|
SchedulerManager *schedulerManager,
|
|
QObject *parent):
|
|
JsonHandler{parent},
|
|
m_spotMarketManager{spotMarketManager},
|
|
m_smartChargingManager{smartChargingManager},
|
|
m_schedulerManager{schedulerManager}
|
|
{
|
|
|
|
registerEnum<ChargingInfo::ChargingMode>();
|
|
registerEnum<ChargingInfo::ChargingState>();
|
|
registerEnum<ChargingAction::ChargingActionIssuer>();
|
|
|
|
registerObject<ChargingInfo, ChargingInfos>();
|
|
registerObject<SpotMarketProviderInfo, SpotMarketProviderInfos>();
|
|
registerObject<ScoreEntry, ScoreEntries>();
|
|
registerObject<ChargingAction>();
|
|
registerObject<ChargingSchedule, ChargingSchedules>();
|
|
|
|
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);
|
|
|
|
|
|
// 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>());
|
|
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);
|
|
emit ChargingSchedulesChanged(params);
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
|
|
// --- Scheduler API (v10) ---
|
|
if (m_schedulerManager) {
|
|
|
|
params.clear(); returns.clear();
|
|
description = "Get the energy planning timeline for the next N hours (6/12/24/48). "
|
|
"Returns predicted allocations, costs, self-sufficiency and human-readable decision reasons.";
|
|
params.insert("hours", enumValueName(Uint));
|
|
returns.insert("timeline", QVariantList());
|
|
returns.insert("summary", QVariantMap());
|
|
returns.insert("activeStrategy", enumValueName(String));
|
|
returns.insert("lastComputedAt", enumValueName(String));
|
|
registerMethod("GetEnergyTimeline", description, params, returns, Types::PermissionScopeControlThings);
|
|
|
|
params.clear(); returns.clear();
|
|
description = "Get all flexible loads known to the Scheduler with their current state and planned allocation.";
|
|
returns.insert("loads", QVariantList());
|
|
registerMethod("GetFlexibleLoads", description, params, returns, Types::PermissionScopeControlThings);
|
|
|
|
params.clear(); returns.clear();
|
|
description = "Get the current status of the Scheduler (active strategy, health, overrides, last compute time).";
|
|
returns.insert("activeStrategy", enumValueName(String));
|
|
returns.insert("availableStrategies", QVariantList());
|
|
returns.insert("lastComputedAt", enumValueName(String));
|
|
returns.insert("nextRecomputeAt", enumValueName(String));
|
|
returns.insert("overridesActive", enumValueName(Uint));
|
|
returns.insert("planHealth", enumValueName(String));
|
|
registerMethod("GetSchedulerStatus", description, params, returns, Types::PermissionScopeControlThings);
|
|
|
|
params.clear(); returns.clear();
|
|
description = "Switch the active scheduling strategy.";
|
|
params.insert("strategyId", enumValueName(String));
|
|
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
|
registerMethod("SetSchedulerStrategy", description, params, returns, Types::PermissionScopeControlThings);
|
|
|
|
params.clear(); returns.clear();
|
|
description = "Update the Scheduler configuration (price thresholds, horizon, self-sufficiency target).";
|
|
params.insert("o:chargePriceThreshold", enumValueName(Double));
|
|
params.insert("o:solarSurplusThresholdW", enumValueName(Double));
|
|
params.insert("o:planningHorizonHours", enumValueName(Uint));
|
|
params.insert("o:recomputeIntervalMin", enumValueName(Uint));
|
|
params.insert("o:selfSufficiencyTarget", enumValueName(Double));
|
|
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
|
registerMethod("SetSchedulerConfig", description, params, returns, Types::PermissionScopeControlThings);
|
|
|
|
params.clear(); returns.clear();
|
|
description = "Update per-load configuration (priority, deadline, target SOC/temperature, enabled).";
|
|
params.insert("thingId", enumValueName(Uuid));
|
|
params.insert("o:priority", enumValueName(Double));
|
|
params.insert("o:deadline", enumValueName(String));
|
|
params.insert("o:targetValue", enumValueName(Double));
|
|
params.insert("o:enabled", enumValueName(Bool));
|
|
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
|
registerMethod("SetLoadConfig", description, params, returns, Types::PermissionScopeControlThings);
|
|
|
|
params.clear(); returns.clear();
|
|
description = "Manually override the Scheduler decision for a specific time slot and load source. "
|
|
"The Scheduler will not touch this slot until the override is cleared or expires.";
|
|
params.insert("slotStart", enumValueName(String));
|
|
params.insert("source", enumValueName(String));
|
|
params.insert("powerW", enumValueName(Double));
|
|
params.insert("reason", enumValueName(String));
|
|
params.insert("o:expiresAt",enumValueName(String));
|
|
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
|
registerMethod("OverrideSlot", description, params, returns, Types::PermissionScopeControlThings);
|
|
|
|
// Push notifications
|
|
params.clear();
|
|
description = "Emitted whenever the Scheduler recomputes the energy timeline.";
|
|
params.insert("changedSlots", QVariantList());
|
|
params.insert("summary", QVariantMap());
|
|
params.insert("triggerReason", enumValueName(String));
|
|
registerNotification("TimelineUpdated", description, params);
|
|
|
|
params.clear();
|
|
description = "Emitted when a scheduled slot becomes active and commands are applied to hardware.";
|
|
params.insert("slot", QVariantMap());
|
|
params.insert("appliedCommands", QVariantList());
|
|
registerNotification("SlotActivated", description, params);
|
|
|
|
params.clear();
|
|
description = "Emitted when a manual override prevents an optimal scheduling decision. "
|
|
"Shows the estimated savings the user is foregoing.";
|
|
params.insert("slot", QVariantMap());
|
|
params.insert("conflict", enumValueName(String));
|
|
params.insert("potentialSavingsEUR",enumValueName(Double));
|
|
registerNotification("OverrideConflict", description, params);
|
|
|
|
// Wire scheduler signals to notifications
|
|
connect(m_schedulerManager, &SchedulerManager::timelineUpdated,
|
|
this, [this](const QList<EnergyTimeSlot> &timeline) {
|
|
QVariantList slotList;
|
|
foreach (const EnergyTimeSlot &slot, timeline)
|
|
slotList.append(slotToVariantMap(slot));
|
|
QVariantMap p;
|
|
p.insert("changedSlots", slotList);
|
|
p.insert("summary", buildTimelineSummary(timeline));
|
|
p.insert("triggerReason", QStringLiteral("SchedulerRecompute"));
|
|
emit TimelineUpdated(p);
|
|
});
|
|
|
|
connect(m_schedulerManager, &SchedulerManager::slotExecuted,
|
|
this, [this](const EnergyTimeSlot &slot, bool success) {
|
|
Q_UNUSED(success)
|
|
QVariantMap p;
|
|
p.insert("slot", slotToVariantMap(slot));
|
|
p.insert("appliedCommands", QVariantList());
|
|
emit SlotActivated(p);
|
|
|
|
// Emit ManualSlotActivated when the executed slot was driven by ManualStrategy
|
|
if (!slot.decisionRules.contains(QStringLiteral("ManualSlot")))
|
|
return;
|
|
ManualStrategy *ms = manualStrategy();
|
|
ManualSlotConfig matchedConfig;
|
|
if (ms) {
|
|
foreach (const ManualSlotConfig &cfg, ms->manualSlots()) {
|
|
if (cfg.matchesSlot(slot.start)) {
|
|
matchedConfig = cfg;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
QVariantMap allocs;
|
|
allocs.insert("ev", slot.allocatedToEV);
|
|
allocs.insert("battery", slot.allocatedToBattery);
|
|
allocs.insert("heatpump", slot.allocatedToHP);
|
|
allocs.insert("dhw", slot.allocatedToDHW);
|
|
allocs.insert("feedin", slot.allocatedToFeedIn);
|
|
const QString reason = matchedConfig.label.isEmpty()
|
|
? QStringLiteral("Créneau manuel activé")
|
|
: QString("Créneau manuel '%1' activé").arg(matchedConfig.label);
|
|
QVariantMap mp;
|
|
mp.insert("slot", matchedConfig.toJson());
|
|
mp.insert("appliedAllocations", allocs);
|
|
mp.insert("reason", reason);
|
|
emit ManualSlotActivated(mp);
|
|
});
|
|
|
|
// Manual slot methods (v11)
|
|
params.clear(); returns.clear();
|
|
description = "Get all manually configured scheduling slots.";
|
|
returns.insert("slots", QVariantList());
|
|
registerMethod("GetManualSlots", description, params, returns,
|
|
Types::PermissionScopeControlThings);
|
|
|
|
params.clear(); returns.clear();
|
|
description = "Create or update a manual scheduling slot. "
|
|
"When repeating=true the slot recurs weekly based on day-of-week and time.";
|
|
params.insert("start", enumValueName(String));
|
|
params.insert("end", enumValueName(String));
|
|
params.insert("label", enumValueName(String));
|
|
params.insert("repeating", enumValueName(Bool));
|
|
params.insert("o:expiresAt",enumValueName(String));
|
|
params.insert("allocations",QVariantMap());
|
|
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
|
registerMethod("SetManualSlot", description, params, returns,
|
|
Types::PermissionScopeControlThings);
|
|
|
|
params.clear(); returns.clear();
|
|
description = "Remove the manual slot that starts at the given UTC timestamp.";
|
|
params.insert("start", enumValueName(String));
|
|
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
|
registerMethod("RemoveManualSlot", description, params, returns,
|
|
Types::PermissionScopeControlThings);
|
|
|
|
params.clear(); returns.clear();
|
|
description = "Remove all manually configured scheduling slots.";
|
|
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
|
|
registerMethod("ClearManualSlots", description, params, returns,
|
|
Types::PermissionScopeControlThings);
|
|
|
|
// ManualSlotActivated push notification
|
|
params.clear();
|
|
description = "Emitted when the Scheduler executes a slot driven by ManualStrategy.";
|
|
params.insert("slot", QVariantMap());
|
|
params.insert("appliedAllocations", QVariantMap());
|
|
params.insert("reason", enumValueName(String));
|
|
registerNotification("ManualSlotActivated", description, 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);
|
|
return createReply(returns);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Scheduler API implementations — NymeaEnergy v10
|
|
// ---------------------------------------------------------------------------
|
|
|
|
JsonReply *NymeaEnergyJsonHandler::GetEnergyTimeline(const QVariantMap ¶ms)
|
|
{
|
|
uint hours = params.value("hours", 24).toUInt();
|
|
if (hours == 0 || hours > 48)
|
|
hours = 24;
|
|
|
|
QVariantMap returns;
|
|
|
|
if (!m_schedulerManager) {
|
|
returns.insert("timeline", QVariantList());
|
|
returns.insert("summary", QVariantMap());
|
|
returns.insert("activeStrategy", QStringLiteral("none"));
|
|
returns.insert("lastComputedAt", QString());
|
|
return createReply(returns);
|
|
}
|
|
|
|
QList<EnergyTimeSlot> timeline = m_schedulerManager->currentTimeline();
|
|
QDateTime cutoff = QDateTime::currentDateTimeUtc().addSecs(static_cast<qint64>(hours) * 3600);
|
|
QVariantList slotList;
|
|
foreach (const EnergyTimeSlot &slot, timeline) {
|
|
if (slot.start <= cutoff)
|
|
slotList.append(slotToVariantMap(slot));
|
|
}
|
|
|
|
returns.insert("timeline", slotList);
|
|
returns.insert("summary", buildTimelineSummary(timeline));
|
|
returns.insert("activeStrategy", m_schedulerManager->currentStrategy()
|
|
? m_schedulerManager->currentStrategy()->strategyId()
|
|
: QString());
|
|
returns.insert("lastComputedAt", m_schedulerManager->lastComputedAt().toUTC().toString(Qt::ISODateWithMs));
|
|
return createReply(returns);
|
|
}
|
|
|
|
JsonReply *NymeaEnergyJsonHandler::GetFlexibleLoads(const QVariantMap ¶ms)
|
|
{
|
|
Q_UNUSED(params)
|
|
QVariantList loads;
|
|
if (m_schedulerManager) {
|
|
foreach (const FlexibleLoad &load, m_schedulerManager->flexibleLoads())
|
|
loads.append(load.toJson());
|
|
}
|
|
return createReply({{"loads", loads}});
|
|
}
|
|
|
|
JsonReply *NymeaEnergyJsonHandler::GetSchedulerStatus(const QVariantMap ¶ms)
|
|
{
|
|
Q_UNUSED(params)
|
|
QVariantMap returns;
|
|
|
|
if (!m_schedulerManager) {
|
|
returns.insert("activeStrategy", QString());
|
|
returns.insert("availableStrategies", QVariantList());
|
|
returns.insert("lastComputedAt", QString());
|
|
returns.insert("nextRecomputeAt", QString());
|
|
returns.insert("overridesActive", 0u);
|
|
returns.insert("planHealth", QStringLiteral("degraded"));
|
|
return createReply(returns);
|
|
}
|
|
|
|
QVariantList strategies;
|
|
foreach (ISchedulingStrategy *s, m_schedulerManager->availableStrategies()) {
|
|
QVariantMap sm;
|
|
sm.insert("strategyId", s->strategyId());
|
|
sm.insert("displayName", s->displayName());
|
|
sm.insert("description", s->description());
|
|
sm.insert("available", s->isAvailable());
|
|
strategies.append(sm);
|
|
}
|
|
|
|
returns.insert("activeStrategy", m_schedulerManager->currentStrategy()
|
|
? m_schedulerManager->currentStrategy()->strategyId()
|
|
: QString());
|
|
returns.insert("availableStrategies", strategies);
|
|
returns.insert("lastComputedAt", m_schedulerManager->lastComputedAt().toUTC().toString(Qt::ISODateWithMs));
|
|
returns.insert("nextRecomputeAt", m_schedulerManager->nextRecomputeAt().toUTC().toString(Qt::ISODateWithMs));
|
|
returns.insert("overridesActive", static_cast<uint>(m_schedulerManager->activeOverridesCount()));
|
|
returns.insert("planHealth", m_schedulerManager->planHealth());
|
|
return createReply(returns);
|
|
}
|
|
|
|
JsonReply *NymeaEnergyJsonHandler::SetSchedulerStrategy(const QVariantMap ¶ms)
|
|
{
|
|
if (!m_schedulerManager)
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
|
|
|
QString strategyId = params.value("strategyId").toString();
|
|
foreach (ISchedulingStrategy *s, m_schedulerManager->availableStrategies()) {
|
|
if (s->strategyId() == strategyId) {
|
|
m_schedulerManager->setStrategy(s);
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
|
}
|
|
}
|
|
qCWarning(dcNymeaEnergy()) << "SetSchedulerStrategy: unknown strategyId" << strategyId;
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
|
}
|
|
|
|
JsonReply *NymeaEnergyJsonHandler::SetSchedulerConfig(const QVariantMap ¶ms)
|
|
{
|
|
if (!m_schedulerManager)
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
|
|
|
SchedulerConfig cfg = m_schedulerManager->config();
|
|
if (params.contains("chargePriceThreshold"))
|
|
cfg.chargePriceThreshold = params.value("chargePriceThreshold").toDouble();
|
|
if (params.contains("solarSurplusThresholdW"))
|
|
cfg.solarSurplusThresholdW = params.value("solarSurplusThresholdW").toDouble();
|
|
if (params.contains("planningHorizonHours"))
|
|
cfg.planningHorizonHours = params.value("planningHorizonHours").toInt();
|
|
if (params.contains("recomputeIntervalMin"))
|
|
cfg.recomputeIntervalMin = qMax(1, params.value("recomputeIntervalMin").toInt());
|
|
if (params.contains("selfSufficiencyTarget"))
|
|
cfg.selfSufficiencyTarget = params.value("selfSufficiencyTarget").toDouble();
|
|
|
|
m_schedulerManager->setConfig(cfg);
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
|
}
|
|
|
|
JsonReply *NymeaEnergyJsonHandler::SetLoadConfig(const QVariantMap ¶ms)
|
|
{
|
|
if (!m_schedulerManager)
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
|
|
|
ThingId thingId(params.value("thingId").toUuid());
|
|
if (thingId.isNull())
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
|
|
|
QList<FlexibleLoad> loads = m_schedulerManager->flexibleLoads();
|
|
for (int i = 0; i < loads.size(); ++i) {
|
|
if (loads.at(i).thingId == thingId) {
|
|
FlexibleLoad load = loads.at(i);
|
|
if (params.contains("priority"))
|
|
load.priority = qBound(0.0, params.value("priority").toDouble(), 1.0);
|
|
if (params.contains("targetValue"))
|
|
load.targetValue = params.value("targetValue").toDouble();
|
|
if (params.contains("deadline")) {
|
|
QString dl = params.value("deadline").toString();
|
|
if (!dl.isEmpty())
|
|
load.deadline = QDateTime::fromString(dl, Qt::ISODateWithMs).toUTC();
|
|
}
|
|
m_schedulerManager->updateLoad(load);
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
|
}
|
|
}
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
|
}
|
|
|
|
JsonReply *NymeaEnergyJsonHandler::OverrideSlot(const QVariantMap ¶ms)
|
|
{
|
|
if (!m_schedulerManager)
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
|
|
|
QDateTime slotStart = QDateTime::fromString(params.value("slotStart").toString(), Qt::ISODateWithMs).toUTC();
|
|
if (!slotStart.isValid())
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
|
|
|
LoadSource source = loadSourceFromString(params.value("source").toString());
|
|
double powerW = params.value("powerW").toDouble();
|
|
QString reason = params.value("reason").toString();
|
|
|
|
m_schedulerManager->overrideSlot(slotStart, source, powerW, reason);
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Manual slot API — NymeaEnergy v11
|
|
// ---------------------------------------------------------------------------
|
|
|
|
JsonReply *NymeaEnergyJsonHandler::GetManualSlots(const QVariantMap ¶ms)
|
|
{
|
|
Q_UNUSED(params)
|
|
QVariantList configList;
|
|
if (m_schedulerManager) {
|
|
foreach (const ManualSlotConfig &cfg, m_schedulerManager->manualSlots())
|
|
configList.append(cfg.toJson());
|
|
}
|
|
return createReply({{"slots", configList}});
|
|
}
|
|
|
|
JsonReply *NymeaEnergyJsonHandler::SetManualSlot(const QVariantMap ¶ms)
|
|
{
|
|
if (!m_schedulerManager)
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
|
|
|
ManualSlotConfig cfg = ManualSlotConfig::fromJson(params);
|
|
if (!cfg.start.isValid() || !cfg.end.isValid() || cfg.end <= cfg.start)
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
|
|
|
m_schedulerManager->setManualSlot(cfg);
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
|
}
|
|
|
|
JsonReply *NymeaEnergyJsonHandler::RemoveManualSlot(const QVariantMap ¶ms)
|
|
{
|
|
if (!m_schedulerManager)
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
|
|
|
QDateTime start = QDateTime::fromString(
|
|
params.value("start").toString(), Qt::ISODateWithMs).toUTC();
|
|
if (!start.isValid())
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
|
|
|
|
m_schedulerManager->removeManualSlot(start);
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
|
}
|
|
|
|
JsonReply *NymeaEnergyJsonHandler::ClearManualSlots(const QVariantMap ¶ms)
|
|
{
|
|
Q_UNUSED(params)
|
|
if (m_schedulerManager)
|
|
m_schedulerManager->clearManualSlots();
|
|
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
|
|
}
|
|
|
|
ManualStrategy *NymeaEnergyJsonHandler::manualStrategy() const
|
|
{
|
|
if (!m_schedulerManager)
|
|
return nullptr;
|
|
foreach (ISchedulingStrategy *s, m_schedulerManager->availableStrategies()) {
|
|
if (s->strategyId() == QLatin1String("manual"))
|
|
return qobject_cast<ManualStrategy *>(s);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Private helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
QVariantMap NymeaEnergyJsonHandler::slotToVariantMap(const EnergyTimeSlot &slot) const
|
|
{
|
|
return slot.toJson();
|
|
}
|
|
|
|
QVariantMap NymeaEnergyJsonHandler::buildTimelineSummary(const QList<EnergyTimeSlot> &timeline) const
|
|
{
|
|
double totalCostEUR = 0;
|
|
double totalSolarKwh = 0;
|
|
double totalImportKwh = 0;
|
|
double totalExportKwh = 0;
|
|
double weightedSelfSuf = 0;
|
|
int count = 0;
|
|
|
|
foreach (const EnergyTimeSlot &slot, timeline) {
|
|
double slotHours = 1.0;
|
|
if (slot.start.isValid() && slot.end.isValid()) {
|
|
qint64 secs = slot.start.secsTo(slot.end);
|
|
if (secs > 0)
|
|
slotHours = secs / 3600.0;
|
|
}
|
|
totalCostEUR += slot.estimatedCostEUR;
|
|
totalSolarKwh += slot.solarForecastW * slotHours / 1000.0;
|
|
totalImportKwh += qMax(0.0, slot.netGridPowerW) * slotHours / 1000.0;
|
|
totalExportKwh += qMax(0.0, -slot.netGridPowerW) * slotHours / 1000.0;
|
|
weightedSelfSuf += slot.selfSufficiencyPct;
|
|
count++;
|
|
}
|
|
|
|
QVariantMap summary;
|
|
summary.insert("totalEstimatedCostEUR", totalCostEUR);
|
|
summary.insert("totalSelfSufficiencyPct", count > 0 ? weightedSelfSuf / count : 0.0);
|
|
summary.insert("totalSolarProductionKwh", totalSolarKwh);
|
|
summary.insert("totalGridImportKwh", totalImportKwh);
|
|
summary.insert("totalGridExportKwh", totalExportKwh);
|
|
return summary;
|
|
}
|