Patrick Schurig c1ce67942c feat(optimiseur): machine de repli §7, état et plan publiés vers l'app (brief 4 T4)
- 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>
2026-10-17 09:34:39 +02:00

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