211 lines
7.6 KiB
C++
211 lines
7.6 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 "manualstrategy.h"
|
|
|
|
#include <QLoggingCategory>
|
|
|
|
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
|
|
|
|
ManualStrategy::ManualStrategy(QObject *parent)
|
|
: ISchedulingStrategy(parent)
|
|
{
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// computeSchedule
|
|
// ---------------------------------------------------------------------------
|
|
|
|
QList<EnergyTimeSlot> ManualStrategy::computeSchedule(
|
|
const QList<EnergyTimeSlot> &forecast,
|
|
const QList<FlexibleLoad> &loads,
|
|
const SchedulerConfig &config)
|
|
{
|
|
Q_UNUSED(config)
|
|
|
|
QList<EnergyTimeSlot> timeline = forecast;
|
|
|
|
for (int i = 0; i < timeline.size(); ++i) {
|
|
EnergyTimeSlot &slot = timeline[i];
|
|
|
|
if (slot.manualOverride)
|
|
continue; // respect human decisions
|
|
|
|
// Clear previous allocations
|
|
slot.allocatedToEV = 0;
|
|
slot.allocatedToHP = 0;
|
|
slot.allocatedToDHW = 0;
|
|
slot.allocatedToBattery = 0;
|
|
slot.allocatedToFeedIn = 0;
|
|
slot.decisionReason.clear();
|
|
slot.decisionRules.clear();
|
|
|
|
// Look for a matching manual config
|
|
const ManualSlotConfig *matched = nullptr;
|
|
bool expiredMatch = false;
|
|
|
|
foreach (const ManualSlotConfig &cfg, m_manualSlots) {
|
|
if (!cfg.matchesSlotIgnoreExpiry(slot.start))
|
|
continue;
|
|
|
|
if (cfg.isExpired()) {
|
|
expiredMatch = true;
|
|
qCDebug(dcNymeaEnergy())
|
|
<< "ManualStrategy: expired config" << cfg.label
|
|
<< "— skipped for slot" << slot.start;
|
|
} else {
|
|
matched = &cfg;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (matched) {
|
|
// Apply exactly the user-defined allocations
|
|
slot.allocatedToEV = matched->powerAllocations.value(LoadSource::SmartCharging, 0);
|
|
slot.allocatedToHP = matched->powerAllocations.value(LoadSource::HeatPump, 0);
|
|
slot.allocatedToDHW = matched->powerAllocations.value(LoadSource::DHW, 0);
|
|
slot.allocatedToBattery = matched->powerAllocations.value(LoadSource::Battery, 0);
|
|
slot.allocatedToFeedIn = matched->powerAllocations.value(LoadSource::FeedIn, 0);
|
|
|
|
if (!matched->label.isEmpty())
|
|
slot.decisionReason =
|
|
QString("Créneau manuel '%1' configuré par l'utilisateur")
|
|
.arg(matched->label);
|
|
else
|
|
slot.decisionReason =
|
|
QStringLiteral("Créneau manuel configuré par l'utilisateur");
|
|
|
|
slot.decisionRules.append(QStringLiteral("ManualSlot"));
|
|
|
|
} else if (expiredMatch) {
|
|
// Apply safe fallback first, then override reason to show expiry
|
|
applyInflexibleLoads(slot, loads);
|
|
slot.decisionReason = QStringLiteral("Créneau expiré — ignoré");
|
|
if (!slot.decisionRules.contains(QStringLiteral("ExpiredSlot")))
|
|
slot.decisionRules.prepend(QStringLiteral("ExpiredSlot"));
|
|
|
|
} else {
|
|
applyInflexibleLoads(slot, loads);
|
|
// applyInflexibleLoads sets reason if critical loads were found;
|
|
// fall back to generic message if still empty.
|
|
if (slot.decisionReason.isEmpty()) {
|
|
slot.decisionReason =
|
|
QStringLiteral("Aucune configuration — charges critiques uniquement");
|
|
slot.decisionRules.append(QStringLiteral("ManualDefault"));
|
|
}
|
|
}
|
|
}
|
|
|
|
return timeline;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// explainDecision
|
|
// ---------------------------------------------------------------------------
|
|
|
|
QString ManualStrategy::explainDecision(
|
|
const EnergyTimeSlot &slot,
|
|
const FlexibleLoad &load) const
|
|
{
|
|
Q_UNUSED(load)
|
|
|
|
if (slot.manualOverride)
|
|
return QStringLiteral("Décision manuelle : ") + slot.overrideReason;
|
|
|
|
if (slot.decisionRules.contains(QStringLiteral("ManualSlot")))
|
|
return slot.decisionReason;
|
|
|
|
if (slot.decisionRules.contains(QStringLiteral("ExpiredSlot")))
|
|
return QStringLiteral("La configuration de ce créneau a expiré — "
|
|
"seules les charges critiques restent actives.");
|
|
|
|
return QStringLiteral("Aucune configuration manuelle pour ce créneau.");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Manual slot management
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void ManualStrategy::setManualSlot(const ManualSlotConfig &slotConfig)
|
|
{
|
|
for (int i = 0; i < m_manualSlots.size(); ++i) {
|
|
if (m_manualSlots.at(i).start == slotConfig.start) {
|
|
m_manualSlots[i] = slotConfig;
|
|
return;
|
|
}
|
|
}
|
|
m_manualSlots.append(slotConfig);
|
|
}
|
|
|
|
void ManualStrategy::removeManualSlot(const QDateTime &slotStart)
|
|
{
|
|
for (int i = 0; i < m_manualSlots.size(); ++i) {
|
|
if (m_manualSlots.at(i).start == slotStart) {
|
|
m_manualSlots.removeAt(i);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ManualStrategy::clearAllManualSlots()
|
|
{
|
|
m_manualSlots.clear();
|
|
}
|
|
|
|
QList<ManualSlotConfig> ManualStrategy::manualSlots() const
|
|
{
|
|
return m_manualSlots;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Private helper
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void ManualStrategy::applyInflexibleLoads(EnergyTimeSlot &slot,
|
|
const QList<FlexibleLoad> &loads) const
|
|
{
|
|
foreach (const FlexibleLoad &load, loads) {
|
|
if (load.type != LoadType::Inflexible)
|
|
continue;
|
|
|
|
if (load.source == LoadSource::HeatPump && load.priority >= 0.9) {
|
|
slot.allocatedToHP = qMax(slot.allocatedToHP, load.currentPowerW);
|
|
if (slot.decisionReason.isEmpty())
|
|
slot.decisionReason =
|
|
QStringLiteral("Chauffage critique — PAC toujours active");
|
|
if (!slot.decisionRules.contains(QStringLiteral("CriticalHeating")))
|
|
slot.decisionRules.append(QStringLiteral("CriticalHeating"));
|
|
}
|
|
|
|
if (load.source == LoadSource::DHW && load.priority >= 0.9) {
|
|
slot.allocatedToDHW = qMax(slot.allocatedToDHW, load.currentPowerW);
|
|
if (slot.decisionReason.isEmpty())
|
|
slot.decisionReason =
|
|
QStringLiteral("ECS critique (sécurité légionellose)");
|
|
if (!slot.decisionRules.contains(QStringLiteral("CriticalDHW")))
|
|
slot.decisionRules.append(QStringLiteral("CriticalDHW"));
|
|
}
|
|
}
|
|
}
|