Décision de Patrick du 2026-10-24. Dans les trois modes qui reportent en
heures creuses (Éco avec minimum, Éco min + échéance, Éco avec échéance) :
- le minimum servi au réseau en heures creuses est le PLAFOND de cycle de
la borne (declared.maxPowerW), plus son plancher. Il porte le courant
maximal, les phases et la limite de phase, et ne descend jamais sous le
plancher : la stratégie rabaisse, L4 coupe. 3 680 W en monophasé 16 A,
11 040 W en triphasé 16 A ;
- les phases se choisissent sur la pleine puissance, plus sur « courant +
surplus » (EnergyArbitrator::enHeuresCreuses(), lu par EvAdapter). La nuit,
une batterie qui couvre la maison et la voiture met le compteur à 0, et la
règle de bascule gardait le monophasé : 3,7 kW au lieu de 11 ;
- la passe éco n'oppose plus à cette borne la marge d'UNE phase de la maison
(LM-1500-h, déjà appliquée en tête de passe et en confort) : son plafond
porte déjà l'autorisation, sur ses phases. Restreint aux heures creuses ;
- motif EV_OFF_PEAK_FULL_POWER {gridW, maxW, phases, until, endsAt,
o:boundBy phaseLimit|floor}, rendu en français, à la place
d'ECO_FLOOR_GRID sur la borne.
Hors heures creuses et sans heure creuse dans le tarif : rien ne change.
Le blocage de la décharge se lève toujours sur la puissance MESURÉE (230 W) :
une borne commandée à 16 A dont la voiture ne prend plus rien le lève.
Tests : testOffPeakChargesAtFullPower, testOffPeakFullPowerSwitchesToThreePhases,
testOffPeakFullPowerYieldsToThePhaseLimit. Les tests du lot e1 en heures
creuses attendent le plafond, et leur compteur inclut ce que la borne tire
désormais (TirageHcW) — sinon il fabriquait un surplus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
177 lines
9.1 KiB
C++
177 lines
9.1 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). L'EV n'a pas de verrou de palier (hors
|
|
* waterfall ECS/SG-Ready) ; l'instant ne sert qu'à savoir s'il tombe en heures creuses
|
|
* (lot e2) — la borne y vise sa pleine puissance, et ses phases s'y choisissent.
|
|
* \return LoadContext incluant declared, limits, needs et télémétrie EV.
|
|
* \note **Lot e2** — dans les trois modes qui reportent en heures creuses, le minimum
|
|
* d'heures creuses (\c needs.offPeakMinPowerW) est le PLAFOND du cycle
|
|
* (\c declared.maxPowerW : courant maximal, phases, limite de phase), jamais sous le
|
|
* plancher ; et le nombre de phases se choisit sur ce plafond, plus sur le surplus.
|
|
*/
|
|
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).
|
|
//! \return Vrai si un défaut a réellement été levé ; faux s'il n'y en avait pas.
|
|
bool clearFault() override { return false; }
|
|
|
|
/*!
|
|
* \brief Latences MESURÉES sur la V2C : montée ~30 s (le véhicule négocie), descente ~7 s.
|
|
* \param montee Vrai si la consigne augmente.
|
|
* \return Le temps d'établissement correspondant, en secondes.
|
|
* \note L'asymétrie joue dans le bon sens — le refus qui coûte de l'argent est celui de la
|
|
* descente, et c'est le plus vite constaté.
|
|
*/
|
|
int commandSettleS(bool montee) const override { return montee ? 30 : 7; }
|
|
|
|
/*!
|
|
* \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 Mesure propre de la borne (§11 / LM-1105).
|
|
* \return La puissance que la borne rapporte elle-même, quand sa ThingClass porte l'état.
|
|
* \note Une borne est **traversée** par la puissance qu'elle délivre : sa mesure est bien
|
|
* celle de la charge, et non celle d'un organe de commande. C'est le cas qui a motivé
|
|
* LM-1105 — il fallait jusqu'ici désigner la borne comme son propre \c meterThingId
|
|
* pour publier une valeur qu'elle donne d'elle-même.
|
|
*/
|
|
LoadMeasurement deviceMeasurement() 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; }
|
|
|
|
/*!
|
|
* \brief Dernière commande envoyée au matériel par cet adaptateur.
|
|
* \param[out] ampere Courant commandé (A), 0 si la charge a été coupée.
|
|
* \param[out] phases Nombre de phases commandé.
|
|
* \return Faux si AUCUNE commande n'est jamais partie — l'écart est alors sans objet, et
|
|
* c'est le cas normal d'une borne en mode manuel que l'arbitre ne pilote pas.
|
|
*/
|
|
bool derniereCommande(uint &ere, uint &phases) const
|
|
{ ampere = m_commandeA; phases = m_commandePhases; return m_commandeValide; }
|
|
|
|
private:
|
|
/*!
|
|
* \brief Dernière commande RÉELLEMENT envoyée au matériel — écrêtée, pas demandée.
|
|
*
|
|
* \par Pourquoi ce souvenir n'existait pas
|
|
* Les adaptateurs de charges en watts gardent leur consigne parce que l'écrêtage de verrou
|
|
* l'exige. La borne, pilotée par le proxy amont, n'a jamais eu à se relire — et c'est
|
|
* précisément là que le relevé du 2026-08-31 mord : après une coupure Modbus, la borne est
|
|
* restée sur une consigne d'arbitre pendant que le proxy en écrivait une autre, sans que
|
|
* l'écart soit calculable par personne.
|
|
*
|
|
* \note La commande ÉCRÊTÉE, jamais la demande : c'est elle qui est partie au matériel.
|
|
* Comparer la demande ferait voir un écart là où l'adaptateur a fait son travail.
|
|
*/
|
|
uint m_commandeA = 0;
|
|
uint m_commandePhases = 0;
|
|
//! Faux tant qu'aucune commande n'est partie. L'absence de commande n'est pas une commande
|
|
//! à zéro : sans ce booléen, une borne jamais pilotée paraîtrait divergente dès qu'elle
|
|
//! consomme, ce qui est le cas NORMAL du mode manuel.
|
|
bool m_commandeValide = false;
|
|
|
|
EvCharger *m_charger;
|
|
EnergyArbitrator *m_parent;
|
|
QDateTime m_lastActionAt;
|
|
//! Nombre de phases VISÉ, décidé à la construction du contexte et réutilisé à
|
|
//! l'exécution. Les deux doivent parler du MÊME, sinon le plancher publié ne décrit pas
|
|
//! la borne qui sera réellement commandée (§12 / 3g-1).
|
|
mutable uint m_targetPhases = 1;
|
|
};
|