// 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 . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "schedulersettings.h" #include "types/manualslotconfig.h" #include #include #include #include #include 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 SchedulerSettings::configuredLoadIds() const { QList ids; foreach (const QString &key, m_loadConfigs.keys()) ids.append(ThingId(key)); return ids; } // --- Overrides --- QList SchedulerSettings::overrideSlotStarts() const { QList 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 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(); }