// 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")); } } }