326 lines
11 KiB
C++
326 lines
11 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 "schedulersettings.h"
|
|
#include "types/manualslotconfig.h"
|
|
|
|
#include <nymeasettings.h>
|
|
|
|
#include <QSettings>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QLoggingCategory>
|
|
|
|
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
|
|
|
SchedulerSettings::SchedulerSettings(QObject *parent)
|
|
: QObject(parent)
|
|
{
|
|
load();
|
|
}
|
|
|
|
QString SchedulerSettings::settingsFilePath() const
|
|
{
|
|
return NymeaSettings::settingsPath() + QStringLiteral("/scheduler.conf");
|
|
}
|
|
|
|
// --- Strategy ---
|
|
|
|
QString SchedulerSettings::activeStrategyId() const
|
|
{
|
|
return m_activeStrategyId;
|
|
}
|
|
|
|
void SchedulerSettings::setActiveStrategyId(const QString &id)
|
|
{
|
|
if (m_activeStrategyId == id)
|
|
return;
|
|
m_activeStrategyId = id;
|
|
save();
|
|
}
|
|
|
|
// --- SchedulerConfig ---
|
|
|
|
SchedulerConfig SchedulerSettings::schedulerConfig() const
|
|
{
|
|
return m_config;
|
|
}
|
|
|
|
void SchedulerSettings::setSchedulerConfig(const SchedulerConfig &config)
|
|
{
|
|
m_config = config;
|
|
save();
|
|
}
|
|
|
|
// --- LoadConfig ---
|
|
|
|
SchedulerSettings::LoadConfig SchedulerSettings::loadConfig(const ThingId &thingId) const
|
|
{
|
|
return m_loadConfigs.value(thingId.toString());
|
|
}
|
|
|
|
void SchedulerSettings::setLoadConfig(const ThingId &thingId, const LoadConfig &config)
|
|
{
|
|
m_loadConfigs.insert(thingId.toString(), config);
|
|
save();
|
|
}
|
|
|
|
QList<ThingId> SchedulerSettings::configuredLoadIds() const
|
|
{
|
|
QList<ThingId> ids;
|
|
foreach (const QString &key, m_loadConfigs.keys())
|
|
ids.append(ThingId(key));
|
|
return ids;
|
|
}
|
|
|
|
// --- Overrides ---
|
|
|
|
QList<QDateTime> SchedulerSettings::overrideSlotStarts() const
|
|
{
|
|
QList<QDateTime> starts;
|
|
foreach (const QString &key, m_overrides.keys())
|
|
starts.append(QDateTime::fromString(key, Qt::ISODateWithMs).toUTC());
|
|
return starts;
|
|
}
|
|
|
|
SchedulerSettings::OverrideEntry SchedulerSettings::overrideEntry(const QDateTime &slotStart) const
|
|
{
|
|
return m_overrides.value(slotStart.toUTC().toString(Qt::ISODateWithMs));
|
|
}
|
|
|
|
void SchedulerSettings::setOverride(const QDateTime &slotStart, const OverrideEntry &entry)
|
|
{
|
|
m_overrides.insert(slotStart.toUTC().toString(Qt::ISODateWithMs), entry);
|
|
save();
|
|
}
|
|
|
|
void SchedulerSettings::removeOverride(const QDateTime &slotStart)
|
|
{
|
|
m_overrides.remove(slotStart.toUTC().toString(Qt::ISODateWithMs));
|
|
save();
|
|
}
|
|
|
|
void SchedulerSettings::clearExpiredOverrides()
|
|
{
|
|
QDateTime now = QDateTime::currentDateTimeUtc();
|
|
QStringList toRemove;
|
|
for (auto it = m_overrides.constBegin(); it != m_overrides.constEnd(); ++it) {
|
|
if (it.value().expiresAt.isValid() && it.value().expiresAt <= now)
|
|
toRemove.append(it.key());
|
|
}
|
|
foreach (const QString &key, toRemove)
|
|
m_overrides.remove(key);
|
|
if (!toRemove.isEmpty())
|
|
save();
|
|
}
|
|
|
|
// --- Manual slots ---
|
|
|
|
QList<ManualSlotConfig> SchedulerSettings::manualSlots() const
|
|
{
|
|
return m_manualSlots;
|
|
}
|
|
|
|
void SchedulerSettings::setManualSlot(const ManualSlotConfig &config)
|
|
{
|
|
const QString key = config.start.toUTC().toString(Qt::ISODateWithMs);
|
|
for (int i = 0; i < m_manualSlots.size(); ++i) {
|
|
if (m_manualSlots.at(i).start.toUTC().toString(Qt::ISODateWithMs) == key) {
|
|
m_manualSlots[i] = config;
|
|
save();
|
|
return;
|
|
}
|
|
}
|
|
m_manualSlots.append(config);
|
|
save();
|
|
}
|
|
|
|
void SchedulerSettings::removeManualSlot(const QDateTime &start)
|
|
{
|
|
const QString key = start.toUTC().toString(Qt::ISODateWithMs);
|
|
for (int i = 0; i < m_manualSlots.size(); ++i) {
|
|
if (m_manualSlots.at(i).start.toUTC().toString(Qt::ISODateWithMs) == key) {
|
|
m_manualSlots.removeAt(i);
|
|
save();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SchedulerSettings::clearManualSlots()
|
|
{
|
|
if (m_manualSlots.isEmpty())
|
|
return;
|
|
m_manualSlots.clear();
|
|
save();
|
|
}
|
|
|
|
// --- Persistence ---
|
|
|
|
void SchedulerSettings::load()
|
|
{
|
|
QSettings s(settingsFilePath(), QSettings::IniFormat);
|
|
|
|
s.beginGroup(QStringLiteral("scheduler"));
|
|
m_activeStrategyId = s.value("strategyId", QStringLiteral("rule-based")).toString();
|
|
m_config.chargePriceThreshold = s.value("chargePriceThreshold", m_config.chargePriceThreshold).toDouble();
|
|
m_config.solarSurplusThresholdW = s.value("solarSurplusThresholdW", m_config.solarSurplusThresholdW).toDouble();
|
|
m_config.planningHorizonHours = s.value("planningHorizonHours", m_config.planningHorizonHours).toInt();
|
|
m_config.recomputeIntervalMin = s.value("recomputeIntervalMin", m_config.recomputeIntervalMin).toInt();
|
|
m_config.selfSufficiencyTarget = s.value("selfSufficiencyTarget", m_config.selfSufficiencyTarget).toDouble();
|
|
s.endGroup();
|
|
|
|
// Load per-thing load configs
|
|
int loadCount = s.beginReadArray(QStringLiteral("loads"));
|
|
for (int i = 0; i < loadCount; ++i) {
|
|
s.setArrayIndex(i);
|
|
QString key = s.value("thingId").toString();
|
|
if (key.isEmpty()) continue;
|
|
LoadConfig lc;
|
|
lc.priority = s.value("priority", 0.5).toDouble();
|
|
lc.targetValue = s.value("targetValue", 0.0).toDouble();
|
|
lc.enabled = s.value("enabled", true).toBool();
|
|
QString dl = s.value("deadline").toString();
|
|
if (!dl.isEmpty())
|
|
lc.deadline = QDateTime::fromString(dl, Qt::ISODateWithMs).toUTC();
|
|
m_loadConfigs.insert(key, lc);
|
|
}
|
|
s.endArray();
|
|
|
|
// Load overrides
|
|
int overrideCount = s.beginReadArray(QStringLiteral("overrides"));
|
|
for (int i = 0; i < overrideCount; ++i) {
|
|
s.setArrayIndex(i);
|
|
QString slotKey = s.value("slotStart").toString();
|
|
if (slotKey.isEmpty()) continue;
|
|
OverrideEntry entry;
|
|
entry.source = loadSourceFromString(s.value("source").toString());
|
|
entry.powerW = s.value("powerW", 0.0).toDouble();
|
|
entry.reason = s.value("reason").toString();
|
|
QString exp = s.value("expiresAt").toString();
|
|
if (!exp.isEmpty())
|
|
entry.expiresAt = QDateTime::fromString(exp, Qt::ISODateWithMs).toUTC();
|
|
m_overrides.insert(slotKey, entry);
|
|
}
|
|
s.endArray();
|
|
|
|
// Load manual slots
|
|
m_manualSlots.clear();
|
|
int manualCount = s.beginReadArray(QStringLiteral("manualSlots"));
|
|
for (int i = 0; i < manualCount; ++i) {
|
|
s.setArrayIndex(i);
|
|
ManualSlotConfig cfg;
|
|
cfg.start = QDateTime::fromString(s.value("start").toString(), Qt::ISODateWithMs).toUTC();
|
|
cfg.end = QDateTime::fromString(s.value("end").toString(), Qt::ISODateWithMs).toUTC();
|
|
cfg.label = s.value("label").toString();
|
|
cfg.repeating = s.value("repeating", false).toBool();
|
|
const QString expStr = s.value("expiresAt").toString();
|
|
if (!expStr.isEmpty())
|
|
cfg.expiresAt = QDateTime::fromString(expStr, Qt::ISODateWithMs).toUTC();
|
|
|
|
// Allocations stored as alloc_ev, alloc_battery, …
|
|
const QStringList allocKeys = {
|
|
QStringLiteral("ev"), QStringLiteral("battery"),
|
|
QStringLiteral("dhw"), QStringLiteral("heatpump"), QStringLiteral("feedin")
|
|
};
|
|
foreach (const QString &key, allocKeys) {
|
|
double val = s.value(QStringLiteral("alloc_") + key, 0.0).toDouble();
|
|
if (val != 0.0)
|
|
cfg.powerAllocations.insert(manualSlotSourceFromKey(key), val);
|
|
}
|
|
|
|
if (!cfg.start.isValid()) continue;
|
|
|
|
if (cfg.isExpired()) {
|
|
qCDebug(dcNymeaEnergy()) << "SchedulerSettings: discarding expired manual slot"
|
|
<< cfg.label << "(expired" << cfg.expiresAt << ")";
|
|
continue;
|
|
}
|
|
m_manualSlots.append(cfg);
|
|
}
|
|
s.endArray();
|
|
|
|
qCDebug(dcNymeaEnergy()) << "SchedulerSettings: loaded from" << settingsFilePath()
|
|
<< "—" << m_manualSlots.size() << "manual slot(s)";
|
|
}
|
|
|
|
void SchedulerSettings::save()
|
|
{
|
|
QSettings s(settingsFilePath(), QSettings::IniFormat);
|
|
|
|
s.beginGroup(QStringLiteral("scheduler"));
|
|
s.setValue("strategyId", m_activeStrategyId);
|
|
s.setValue("chargePriceThreshold", m_config.chargePriceThreshold);
|
|
s.setValue("solarSurplusThresholdW", m_config.solarSurplusThresholdW);
|
|
s.setValue("planningHorizonHours", m_config.planningHorizonHours);
|
|
s.setValue("recomputeIntervalMin", m_config.recomputeIntervalMin);
|
|
s.setValue("selfSufficiencyTarget", m_config.selfSufficiencyTarget);
|
|
s.endGroup();
|
|
|
|
s.beginWriteArray(QStringLiteral("loads"), m_loadConfigs.size());
|
|
int idx = 0;
|
|
for (auto it = m_loadConfigs.constBegin(); it != m_loadConfigs.constEnd(); ++it, ++idx) {
|
|
s.setArrayIndex(idx);
|
|
s.setValue("thingId", it.key());
|
|
s.setValue("priority", it.value().priority);
|
|
s.setValue("targetValue", it.value().targetValue);
|
|
s.setValue("enabled", it.value().enabled);
|
|
if (it.value().deadline.isValid())
|
|
s.setValue("deadline", it.value().deadline.toUTC().toString(Qt::ISODateWithMs));
|
|
}
|
|
s.endArray();
|
|
|
|
s.beginWriteArray(QStringLiteral("overrides"), m_overrides.size());
|
|
idx = 0;
|
|
for (auto it = m_overrides.constBegin(); it != m_overrides.constEnd(); ++it, ++idx) {
|
|
s.setArrayIndex(idx);
|
|
s.setValue("slotStart", it.key());
|
|
s.setValue("source", loadSourceToString(it.value().source));
|
|
s.setValue("powerW", it.value().powerW);
|
|
s.setValue("reason", it.value().reason);
|
|
if (it.value().expiresAt.isValid())
|
|
s.setValue("expiresAt", it.value().expiresAt.toUTC().toString(Qt::ISODateWithMs));
|
|
}
|
|
s.endArray();
|
|
|
|
s.beginWriteArray(QStringLiteral("manualSlots"), m_manualSlots.size());
|
|
idx = 0;
|
|
foreach (const ManualSlotConfig &cfg, m_manualSlots) {
|
|
s.setArrayIndex(idx++);
|
|
s.setValue("start", cfg.start.toUTC().toString(Qt::ISODateWithMs));
|
|
s.setValue("end", cfg.end.toUTC().toString(Qt::ISODateWithMs));
|
|
s.setValue("label", cfg.label);
|
|
s.setValue("repeating", cfg.repeating);
|
|
if (cfg.expiresAt.isValid())
|
|
s.setValue("expiresAt", cfg.expiresAt.toUTC().toString(Qt::ISODateWithMs));
|
|
// Allocations
|
|
for (auto it = cfg.powerAllocations.constBegin();
|
|
it != cfg.powerAllocations.constEnd(); ++it) {
|
|
s.setValue(QStringLiteral("alloc_") + manualSlotAllocationKey(it.key()),
|
|
it.value());
|
|
}
|
|
}
|
|
s.endArray();
|
|
}
|