diff --git a/energyplugin/etm/energyarbitrator.cpp b/energyplugin/etm/energyarbitrator.cpp index 050550e..fd0bd99 100644 --- a/energyplugin/etm/energyarbitrator.cpp +++ b/energyplugin/etm/energyarbitrator.cpp @@ -415,7 +415,30 @@ SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const }; } // SurplusPv : interface inverter — déféré (remplissage prévu en 3d) - // SurplusBattery : déféré 3f + + // --- Stockage (3g-1) ------------------------------------------------------------------ + // Rempli ICI et plus dans le chemin VE. La prise en compte de la batterie n'a jamais été + // une règle « véhicule » : c'est une règle de BUDGET, et elle vivait dans + // planSurplusCharging() par accident d'histoire. Tant qu'elle y restait, une batterie sous + // sa réserve empêchait de charger la voiture mais laissait le chauffe-eau se servir. + // + // Toutes les batteries sont agrégées en une seule — même simplification que l'amont : un + // niveau moyen et une puissance totale. Distinguer deux stockages demanderait de savoir + // lequel arbitrer en premier, et rien dans le modèle ne le dit. + { + int n = 0; + double niveauTotal = 0, puissanceTotale = 0; + for (Thing *storage : m_tm->configuredThings().filterByInterface("energystorage")) { + ++n; + niveauTotal += storage->stateValue("batteryLevel").toDouble(); + puissanceTotale += storage->stateValue("currentPower").toDouble(); + } + if (n > 0) { + ctx.battery.present = true; + ctx.battery.socPercent = niveauTotal / n; + ctx.battery.powerW = puissanceTotale; // > 0 charge, < 0 décharge + } + } // --- loads[] : EV adapters --- (now = ctx.timestamp : source unique des verrous) for (auto it = m_adapters.constBegin(); it != m_adapters.constEnd(); ++it) diff --git a/energyplugin/etm/scheduler/rulebasedscheduler.cpp b/energyplugin/etm/scheduler/rulebasedscheduler.cpp index 45e8cf4..5b664df 100644 --- a/energyplugin/etm/scheduler/rulebasedscheduler.cpp +++ b/energyplugin/etm/scheduler/rulebasedscheduler.cpp @@ -96,6 +96,45 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx) // transitoire sous minOn est géré par le verrou de l'adaptateur, pas par le budget.) double remainingSurplusW = (ctx.meter.exportW - ctx.meter.importW) - evReservedW; + // ---- Prise en compte du stockage (3g-1) --------------------------------------- + // Remontée du chemin VE, où elle vivait par accident d'histoire. Ce n'a jamais été une + // règle « véhicule » : c'est une règle de BUDGET. Tant qu'elle restait dans + // planSurplusCharging(), une batterie sous sa réserve empêchait de charger la voiture mais + // laissait le chauffe-eau se servir — deux poids, deux mesures pour la même énergie. + // + // ⚠️ CHANGEMENT DE COMPORTEMENT pour l'ECS et la PAC, qui n'ont jamais connu cette règle. + // Une installation qui servait ses charges sous le seuil de réserve ne le fera plus. + if (ctx.battery.present) { + const double seuil = m_arbitrator->batteryLevelConsideration() * 100.0; + + if (ctx.battery.socPercent < seuil) { + // Sous la réserve : le stockage passe AVANT toute charge pilotée. On ne réduit pas + // le budget, on l'annule — servir « un peu » reviendrait à retarder la recharge de + // la batterie tout en ne démarrant probablement personne (planchers du §12). + if (remainingSurplusW > 0) + qCDebug(dcNymeaEnergy()).nospace() + << "[Waterfall] Stockage à " << ctx.battery.socPercent << "% sous la réserve de " + << seuil << "% : budget de surplus annulé (" << remainingSurplusW << " W)."; + remainingSurplusW = qMin(remainingSurplusW, 0.0); + + // La batterie charge sous son seuil : lui laisser de quoi le faire. Le tampon est + // repris tel quel de l'amont — il évite que le waterfall ne mange exactement ce + // que la batterie s'apprêtait à prendre. + if (ctx.battery.powerW > 0) + remainingSurplusW -= 500; + } else if (ctx.battery.powerW > 0) { + // Au-dessus de la réserve et en charge : cette puissance est disponible pour les + // charges pilotées — les servir vaut mieux que de remplir une batterie déjà sûre. + remainingSurplusW += ctx.battery.powerW; + } + + if (ctx.battery.powerW < 0) { + // Décharge : ne pas vider la batterie pour alimenter une charge pilotée, quel que + // soit le niveau. Ce qu'elle fournit n'est pas du surplus. + remainingSurplusW += ctx.battery.powerW; // powerW < 0 → retranche + } + } + // Comptabilité PUBLIÉE du budget (télémétrie RPC). Elle s'alimente DANS la cascade, // pendant que celle-ci se déroule : c'est le seul endroit qui sache ce qui a réellement // été alloué et ce qui a été recrédité. La recalculer ailleurs donnerait deux comptes à