Les trois premiers constats de BRIEF_agent_plugin.md, dans l'ordre de ce qu'ils coûtent.
§2 — Renommer une charge n'atteignait jamais l'adaptateur. updateSoftConfig() ne
portait que priority et needs, donc un changement de libellé seul était compté
« inchangée » : l'adaptateur gardait son ancien nom et le journal contredisait
GetLoadConfig jusqu'à la prochaine reconstruction. `label` entre dans la signature et
dans le prédicat soft de l'arbitre, sans entrer dans sameHardware() — mise à jour EN
PLACE, pas reconstruction. La vraie prise était le Q_UNUSED de SgReadyAdapter, hérité
d'avant le lot B-bis : sans lui, renommer la PAC — le cas même qu'on allait tester —
n'aurait rien changé, et on aurait conclu que le correctif ne marchait pas.
§3 — relays[] n'est plus comparé par index. Un réordonnancement reconstruisait la
charge : contacts ouverts, verrous réarmés à froid. Comparer comme un ENSEMBLE aurait
été faux dans l'autre sens — à somme égale la table retient la première combinaison
rencontrée, donc échanger deux relais de même puissance change le contact qui sert ce
palier. sameHardware() compare la TABLE DÉRIVÉE : deux listes qui produisent les mêmes
paliers avec les mêmes contacts sont matériellement identiques, par construction.
§4 — mechanism.stagesW publie les paliers atteignables, en télémétrie et pas dans
GetLoadConfig. La télémétrie n'a aucun chemin d'écriture, donc « lecture seule » y est
vrai par construction ; GetLoadConfig aurait cassé l'aller-retour neutre dont l'app
dépend pour écrire — elle renvoie verbatim ce qu'elle a lu, le champ serait revenu
dans SetLoadConfig, et un refus en bloc aurait emporté toutes les charges.
deriveStages() est l'implémentation unique de la combinatoire, partagée par les trois
usages. C'était le fond de la demande du §4 : une seule règle, pas de divergence.
tests/auto/loadmodel : cible neuve sans serveur nymea, 2 ms. Elle a attrapé un défaut
de la correction elle-même avant le banc — operator== comparait les listes de ThingIds
et voyait une différence entre fermer {K1,K2} et fermer {K2,K1}.
tools/repair_energylogs.py : §1, et ce n'est PAS un correctif de ce plugin. La cause
est extérieure et déjà corrigée (SunSpec float32 aux mots permutés, d08cf50 daté
05:30:14 UTC — la seconde du premier échantillon empoisonné). La séquelle, elle, était
définitive : à 7,5e33 kWh le plus petit incrément représentable vaut 1,67e18, donc le
compteur n'était pas faux mais GELÉ, et rechargé tel quel à chaque démarrage.
Appliqué sur .75 : 62 726 anomalies → 0, accumulation reprise et vérifiée en direct.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
118 lines
5.6 KiB
C++
118 lines
5.6 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
|
#pragma once
|
|
|
|
#include <QObject>
|
|
#include <QDateTime>
|
|
#include "iloadadapter.h"
|
|
|
|
class EvCharger;
|
|
class EnergyArbitrator;
|
|
|
|
/*!
|
|
* \brief Adaptateur pour une borne de recharge VE (interface evcharger nymea).
|
|
*
|
|
* Traduit les LoadAction(Setpoint) en appels matériels via
|
|
* EnergyArbitrator::doExecuteChargingAction() — le seul chemin d'exécution.
|
|
*
|
|
* \invariant applyAction() rejette silencieusement toute LoadAction dont \c reason est vide.
|
|
* \invariant currentA est écrêté à [maxChargingCurrentMinValue, maxChargingCurrentMaxValue].
|
|
* \invariant phaseCount est écrêté selon canSetPhaseCount() et phaseCount() du EvCharger.
|
|
* \invariant Les kinds autres que Setpoint sont retournés sans effet.
|
|
*/
|
|
class EvAdapter : public QObject, public ILoadAdapter
|
|
{
|
|
Q_OBJECT
|
|
public:
|
|
/*!
|
|
* \brief Constructeur.
|
|
* \param evCharger Borne VE à piloter (doit rester valide tant que l'adaptateur existe).
|
|
* \param parent EnergyArbitrator propriétaire — utilisé pour l'exécution matérielle.
|
|
*/
|
|
explicit EvAdapter(EvCharger *evCharger, EnergyArbitrator *parent);
|
|
|
|
/*!
|
|
* \brief Retourne la description statique de la charge.
|
|
* \return LoadDescriptor construit depuis les capacités actuelles du EvCharger.
|
|
* \note Recalculé à chaque appel depuis l'état nymea du Thing.
|
|
*/
|
|
LoadDescriptor descriptor() const override;
|
|
|
|
/*!
|
|
* \brief Retourne la télémétrie runtime (puissance mesurée, disponibilité).
|
|
* \return LoadTelemetry avec currentPowerW, available et lastActionAt.
|
|
*/
|
|
LoadTelemetry telemetry() const override;
|
|
|
|
/*!
|
|
* \brief Construit l'entrée loads[] §5 du SurplusContext.
|
|
* \param now Temps de cycle (\c ctx.timestamp). Inutilisé ici : l'EV n'a pas de verrou
|
|
* de palier (hors waterfall ECS/SG-Ready). Présent pour l'uniformité de \c ILoadAdapter.
|
|
* \return LoadContext incluant declared, limits, needs et télémétrie EV.
|
|
*/
|
|
LoadContext toLoadContext(const QDateTime &now) const override;
|
|
|
|
/*!
|
|
* \brief Applique une consigne Setpoint sur la borne VE.
|
|
*
|
|
* **Inactif jusqu'à 3g** : non appelée par \c EnergyArbitrator::update() en beta.
|
|
* Le dispatch EV passe par \c adjustEvChargers() amont (hérité). Cette méthode
|
|
* sera câblée lors de la transplantation EV dans \c RuleBasedScheduler (phase 3g).
|
|
* \c descriptor() et \c telemetry() sont eux actifs dès maintenant pour le SurplusContext.
|
|
*
|
|
* \param action LoadAction de kind Setpoint. Autres kinds : retour sans effet.
|
|
* \param now Temps de cycle (\c ctx.timestamp) — passé à \c doExecuteChargingAction()
|
|
* (locks anti-rebond de la borne). MÊME source que toLoadContext() (contrat ILoadAdapter).
|
|
* \return L'action après écrêtage matériel (currentA, phaseCount bornés).
|
|
*
|
|
* \invariant action.reason non vide requis — log warning et retour sans effet sinon.
|
|
* \invariant currentA écrêté à [minValue, maxValue] avant envoi à executeChargingAction.
|
|
* \invariant phaseCount ajusté selon canSetPhaseCount() du EvCharger.
|
|
*/
|
|
LoadAction applyAction(const LoadAction &action, const QDateTime &now) override;
|
|
|
|
//! \brief Sans effet : l'EV n'est pas construit depuis \c LoadConfig (rang fixé à 100,
|
|
//! cf. « Dette 3g »), et son nom d'affichage vient du Thing, pas de la config.
|
|
//! \param label Ignoré. \param priority Ignoré. \param needs Ignoré.
|
|
void updateSoftConfig(const QString &label, int priority, const LoadNeeds &needs) override
|
|
{ Q_UNUSED(label) Q_UNUSED(priority) Q_UNUSED(needs) }
|
|
|
|
//! \brief Sans effet : l'EV n'implémente pas encore l'échelle de défaut ECS-410.
|
|
//! La généralisation est portée par **ECS-414** (lot de mise en configuration
|
|
//! du SgReadyAdapter). Déclaré explicitement plutôt qu'hérité d'un défaut vide.
|
|
//! Aucun mécanisme de défaut côté EV : il n'y a jamais rien à lever. Le dire
|
|
//! explicitement plutôt que de laisser un corps vide (règle 7-c).
|
|
bool clearFault() override { return false; }
|
|
|
|
/*!
|
|
* \brief Vue runtime — déclarée, mais HORS de la charge utile \c GetLoadTelemetry.
|
|
*
|
|
* \param now Ignoré : les verrous EV (chargingEnabledLock, currentChangeLock) vivent dans
|
|
* \c SmartChargingManager amont, pas ici.
|
|
* \return \c mechanismKind "evcharger" et l'état de la borne.
|
|
*
|
|
* \note L'EV n'apparaît PAS dans \c NymeaEnergy.GetLoadTelemetry en beta : il est décidé
|
|
* par l'amont EN AMONT du waterfall (décision B), sa puissance est réservée dans
|
|
* \c PlanBudget::evReservedW et non allouée depuis le budget publié. L'y faire figurer
|
|
* ferait une somme \c loads[] qui ne retomberait pas sur \c budget.allocatedW. La
|
|
* méthode est néanmoins IMPLÉMENTÉE, et non laissée en \c Q_UNIMPLEMENTED : quand 3g
|
|
* transplantera l'EV dans le waterfall unifié, il n'y aura rien à écrire ici.
|
|
*/
|
|
LoadRuntimeView runtimeView(const QDateTime &now) const override;
|
|
|
|
//! \brief Sans effet : l'EV n'est pas construit depuis \c LoadConfig et ne peut donc pas
|
|
//! être « désactivé » par ce chemin. \param now Ignoré.
|
|
void applySafeState(const QDateTime &now) override { Q_UNUSED(now) }
|
|
|
|
/*!
|
|
* \brief Borne VE sous-jacente (lecture).
|
|
* \return Pointeur non-propriétaire vers l'EvCharger ; jamais nul (fixé à la construction).
|
|
*/
|
|
EvCharger *evCharger() const { return m_charger; }
|
|
|
|
private:
|
|
EvCharger *m_charger;
|
|
EnergyArbitrator *m_parent;
|
|
QDateTime m_lastActionAt;
|
|
};
|