- OptimizerStatus::Vivacite : optimizerAlive, échecs comptés PAR REQUÊTE (E17), deux consécutifs = mort, socket fermé = mort, retour au premier plan valide ; une abstention rompt la série sans ressusciter. strategieActive() : none sous L2, prioritaire sur tout ; rules dès qu'un optimiseur n'est plus vivant, sans attendre le cycle suivant. - SocketScheduler : branchement de la vivacité, cache abandonné à la mort, UpdateLearned journalisé une fois et ignoré. - NymeaEnergy.GetOptimizerStatus + OptimizerStatusChanged (une notification par transition, charge utile déclarée une fois) ; champs installateur absents s'ils sont inconnus ; jamais le token, tokenConfigured seulement. Pas de tier (Q5). - SetOptimizerEndpoint (persisté, appliqué à chaud) et SetOptimizerToken (écriture seule, fichier 0600, Q2), tous deux PermissionScopeAdmin ; token déclaré Variant pour qu'un mauvais type ne soit pas recopié au journal par le validateur nymea. - NymeaEnergy.GetPlan : plan en vigueur (appliqué au créneau courant, demandé ensuite), codes et paramètres filtrés seulement, inForce toujours présent, fallbackCode quand un optimiseur configuré n'a pas commandé. - Tests : tableau §7 ligne par ligne, transitions notifiées une fois, RPC, token absent de tout journal du moteur et de toute réponse. Sans endpoint : decisions IDENTIQUE (23 792 lignes), suites inchangées, journal >= info inchangé (8 739 lignes). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
3.7 KiB
C++
104 lines
3.7 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
|
|
|
#include "optimizerstatus.h"
|
|
|
|
#include <QStringList>
|
|
#include <cmath>
|
|
|
|
namespace OptimizerStatus {
|
|
|
|
bool Vivacite::noter(Evenement e, const QString &code, const QDateTime &instant)
|
|
{
|
|
const bool avant = m_vivant;
|
|
switch (e) {
|
|
case Evenement::PlanValide:
|
|
m_vivant = true;
|
|
m_echecs = 0;
|
|
m_dernierPlanA = instant;
|
|
break;
|
|
case Evenement::Abstention:
|
|
// L'optimiseur a répondu : la série d'échecs est rompue. Il ne planifie pas pour autant —
|
|
// un optimiseur mort qui s'abstient reste mort jusqu'à son premier plan valide.
|
|
m_echecs = 0;
|
|
break;
|
|
case Evenement::Echec:
|
|
++m_echecs;
|
|
if (!code.isEmpty())
|
|
m_dernierCode = code;
|
|
if (m_echecs >= EchecsAvantMort)
|
|
m_vivant = false;
|
|
break;
|
|
case Evenement::ConnexionPerdue:
|
|
m_vivant = false;
|
|
if (!code.isEmpty())
|
|
m_dernierCode = code;
|
|
break;
|
|
}
|
|
return m_vivant != avant;
|
|
}
|
|
|
|
QString strategieActive(bool degrade, bool vivant, const QString &strategieDuCycle)
|
|
{
|
|
if (degrade)
|
|
return QString::fromLatin1(StrategieAucune);
|
|
if (!vivant && strategieDuCycle == QLatin1String(StrategieOptimiseur))
|
|
return QString::fromLatin1(StrategieRegles);
|
|
return strategieDuCycle;
|
|
}
|
|
|
|
namespace {
|
|
bool listeDeCodes(const QVariant &v, const QStringList &connus, QVariantList *out)
|
|
{
|
|
if (v.userType() != QMetaType::QVariantList && v.userType() != QMetaType::QStringList)
|
|
return false;
|
|
for (const QVariant &x : v.toList()) {
|
|
if (x.userType() != QMetaType::QString || !connus.contains(x.toString()))
|
|
continue; // un code inconnu ou une valeur non textuelle n'est pas recopiée
|
|
if (!out->contains(x))
|
|
out->append(x);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
QVariantMap capacitesPubliees(const QVariantMap &capabilities)
|
|
{
|
|
static const QStringList kinds{QStringLiteral("setpoint"), QStringLiteral("state"), QStringLiteral("constraint")};
|
|
static const QStringList sources{QStringLiteral("pv"), QStringLiteral("baseload"), QStringLiteral("price"),
|
|
QStringLiteral("temperature")};
|
|
QVariantMap out;
|
|
for (const char *cle : {"maxHorizonH", "resolutionS"}) {
|
|
const QVariant v = capabilities.value(QLatin1String(cle));
|
|
bool ok = false;
|
|
const double d = v.toDouble(&ok);
|
|
const bool nombre = v.userType() == QMetaType::Double || v.userType() == QMetaType::Int
|
|
|| v.userType() == QMetaType::LongLong || v.userType() == QMetaType::UInt
|
|
|| v.userType() == QMetaType::ULongLong;
|
|
if (nombre && ok && std::isfinite(d) && d >= 0)
|
|
out.insert(QLatin1String(cle), d);
|
|
}
|
|
QVariantList l;
|
|
if (listeDeCodes(capabilities.value(QStringLiteral("kinds")), kinds, &l))
|
|
out.insert(QStringLiteral("kinds"), l);
|
|
l.clear();
|
|
if (listeDeCodes(capabilities.value(QStringLiteral("usesForecast")), sources, &l))
|
|
out.insert(QStringLiteral("usesForecast"), l);
|
|
if (capabilities.value(QStringLiteral("reasonCodes")).userType() == QMetaType::Bool)
|
|
out.insert(QStringLiteral("reasonCodes"), capabilities.value(QStringLiteral("reasonCodes")).toBool());
|
|
return out;
|
|
}
|
|
|
|
QString identite(const QString &texte, int max)
|
|
{
|
|
QString s;
|
|
s.reserve(texte.size());
|
|
for (const QChar c : texte)
|
|
s.append(c.category() == QChar::Other_Control || c.category() == QChar::Other_Format
|
|
|| c.category() == QChar::Separator_Line || c.category() == QChar::Separator_Paragraph
|
|
? QChar(' ') : c);
|
|
return s.simplified().left(max);
|
|
}
|
|
|
|
}
|