// 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 "manualstrategy.h"
#include
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
ManualStrategy::ManualStrategy(QObject *parent)
: ISchedulingStrategy(parent)
{
}
// ---------------------------------------------------------------------------
// computeSchedule
// ---------------------------------------------------------------------------
QList ManualStrategy::computeSchedule(
const QList &forecast,
const QList &loads,
const SchedulerConfig &config)
{
Q_UNUSED(config)
QList 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 ManualStrategy::manualSlots() const
{
return m_manualSlots;
}
// ---------------------------------------------------------------------------
// Private helper
// ---------------------------------------------------------------------------
void ManualStrategy::applyInflexibleLoads(EnergyTimeSlot &slot,
const QList &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"));
}
}
}