powersync-energy-plugin-etm/energyplugin/schedulersettings.cpp

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();
}