fix(prévision PV): aucune adoption d'office — le motif rend le silence acceptable
Décision de Patrick : rien n'est jamais désigné automatiquement. Ma proposition d'adopter le site unique est écartée, et sa raison porte plus loin que la mienne. « Il n'y a rien à choisir » ne suffit pas, parce que LA DÉSIGNATION SE PERSISTE. Le jour où un second site apparaît, elle devient un choix que personne n'a fait et qui a l'air fait — plus rien ne distinguera « l'utilisateur a préféré celui-ci » de « le moteur a pris le seul qu'il voyait en 2026 ». Le précédent du rootmeter va dans ce sens (SetRootMeter exige une désignation explicite, et une adoption d'office sur la seule présence d'une interface a déjà coûté, cf. §14). Et la distinction avec l'auto-provisionnement des bornes (+etm23) tient : celui-là ENREGISTRE ce que le moteur pilote déjà, il ne choisit rien parmi plusieurs. MISSING_FORECAST_SITE : la lecture s'arrête, mais elle le DIT — c'est ce qui rend le silence acceptable, et l'écran peut nommer le geste au lieu d'afficher un vide. Le rendu français donne le geste et pas seulement le manque : « Aucun site de prévision désigné — choisissez-en un dans la configuration ». Au contrat c'est un code, jamais une phrase. Et il ne se publie QUE si un site existe. Aucun candidat : silence — annoncer une désignation manquante à une installation qui n'a rien à désigner serait un avertissement qu'aucun geste ne peut lever, donc du bruit permanent, et le bruit finit par couvrir le signal. PAS DE PARC, DONC PAS DE MIGRATION — mais une DATE. Aucun mécanisme n'est écrit, délibérément : un mécanisme sans parc à migrer serait du code jamais exercé, donc du code faux qu'on découvrirait le jour où il servirait. Si un parc existe avant le 2026-12-31, la question se rouvre — et la réponse ne sera pas l'adoption d'office pour autant : ce qui manquera est un chemin de configuration à la mise en service. forecastSiteChosen reste, sans plus armer d'adoption : il distingue une installation jamais configurée d'une dont l'utilisateur a délibérément coupé la prévision. Les deux se taisent de la même façon et ne veulent pas dire la même chose. loadmodel 32/32, ci-quality 4/4, build 0 erreur 0 warning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
This commit is contained in:
parent
17a222756b
commit
e0bfeb1788
@ -755,51 +755,72 @@ recevant une valeur hors catalogue ne saurait pas la rendre.)*
|
||||
|
||||
---
|
||||
|
||||
## 18. Migration vers `+etm63` — désigner d'office quand il n'y a rien à choisir
|
||||
## 18. Aucune adoption d'office — le motif rend le silence acceptable
|
||||
|
||||
`+etm62` lit **sans désignation** ; `+etm63` en exige une. Une installation qui avait commencé à
|
||||
remplir sa table s'arrêterait donc **silencieusement**, par construction. La règle proposée :
|
||||
> **Décision du 2026-09-09 : rien n'est jamais désigné automatiquement.** La proposition
|
||||
> précédente — adopter le site unique — est **écartée**, et la raison porte plus loin qu'elle.
|
||||
|
||||
| candidats | conduite |
|
||||
### 18.1 Pourquoi « il n'y a rien à choisir » ne suffit pas
|
||||
|
||||
Adopter le site unique paraissait sans risque : avec un seul candidat, désigner n'est pas
|
||||
choisir. **Mais la désignation se PERSISTE.** Le jour où un second site apparaît, elle devient un
|
||||
**choix que personne n'a fait et qui a l'air fait** — et plus rien ne distinguera « l'utilisateur
|
||||
a préféré celui-ci » de « le moteur a pris le seul qu'il voyait en 2026 ».
|
||||
|
||||
**Le précédent du rootmeter va dans ce sens** : `Energy.SetRootMeter` exige une désignation
|
||||
explicite, et une adoption d'office sur la seule présence d'une interface a déjà coûté (§14).
|
||||
|
||||
**Et la distinction avec l'auto-provisionnement des bornes (`+etm23`) tient** : celui-là
|
||||
**enregistre ce que le moteur pilote déjà**, il ne choisit rien parmi plusieurs. Ici, il y aurait
|
||||
choix — fût-il unique aujourd'hui.
|
||||
|
||||
### 18.2 `MISSING_FORECAST_SITE` — ce qui rend le silence acceptable
|
||||
|
||||
La lecture s'arrête, **mais elle le dit**. L'écran peut donc nommer le geste qui manque au lieu
|
||||
d'afficher un vide.
|
||||
|
||||
**Le motif dit QUOI FAIRE, pas seulement ce qui manque** — rendu français :
|
||||
|
||||
> *« Aucun site de prévision désigné — choisissez-en un dans la configuration »*
|
||||
|
||||
« Il en manque une » laisserait l'utilisateur devant un constat ; la phrase lui donne la marche à
|
||||
suivre. Même exigence que les trois causes d'`ECO_FLOOR_MISSED` : **trois causes, trois gestes.**
|
||||
Au contrat c'est un **code** (`fault: "MISSING_FORECAST_SITE"`), jamais une phrase — le client la
|
||||
fabrique dans sa langue.
|
||||
|
||||
### 18.3 Il ne se publie QUE si un site existe
|
||||
|
||||
| état | conduite |
|
||||
|---|---|
|
||||
| **0** | rien, **et rien au journal** — c'est le cas normal d'une installation sans prévision |
|
||||
| **1** | **désigné d'office**, persisté, **une** ligne au journal |
|
||||
| **≥ 2** | rien de désigné, **une** ligne au journal, et les candidats publiés au contrat |
|
||||
| aucun site candidat | **silence** — rien à désigner, et l'annoncer serait un avertissement qu'on ne peut pas satisfaire |
|
||||
| site présent, aucun désigné | **le motif**, journalisé **une fois**, et publié dans `GetForecastSite` |
|
||||
| site désigné | rien |
|
||||
|
||||
### 18.1 La distinction qui la fonde
|
||||
Un avertissement permanent qu'aucun geste ne peut lever est du **bruit**, et le bruit finit par
|
||||
couvrir le signal. C'est la même règle que le silence sur les bornes hors waterfall.
|
||||
|
||||
> **Le silence est la bonne réponse à « cette installation n'a pas de prévision ». Il n'est PAS
|
||||
> la bonne réponse à « cette installation en avait une et nous venons de la lui retirer ».**
|
||||
### 18.4 Pas de parc, donc pas de migration — mais une DATE
|
||||
|
||||
La première est une configuration légitime ; la seconde est une **régression que nous
|
||||
introduisons**. Les confondre ferait passer notre propre rupture pour un cas normal — exactement
|
||||
ce que la règle du silence existe pour ne pas couvrir.
|
||||
`.75` est le seul déploiement, et sa table sera reconstruite en une journée. **Aucun mécanisme de
|
||||
migration n'est écrit**, et c'est délibéré : un mécanisme sans parc à migrer serait du code
|
||||
jamais exercé, donc du code faux qu'on découvrirait le jour où il servirait.
|
||||
|
||||
### 18.2 Pourquoi le défaut d'office ne devine rien
|
||||
> **Date limite : si un parc existe avant le 2026-12-31**, la question se rouvre — et la réponse
|
||||
> ne sera pas l'adoption d'office pour autant. Ce qui manquera alors est un **chemin de
|
||||
> configuration à la mise en service**, pas un défaut deviné au démarrage.
|
||||
|
||||
**Avec un seul candidat, désigner n'est pas CHOISIR** — il n'y a rien à choisir. C'est la seule
|
||||
condition sous laquelle un défaut d'office ne suppose rien à la place de l'utilisateur.
|
||||
Écrit maintenant plutôt que repris dans six mois sans contexte.
|
||||
|
||||
Dès **deux** candidats, il y a un vrai choix, et le faire serait deviner. Le moteur ne désigne
|
||||
rien, le dit **une fois**, et `GetForecastSite` publie `candidateSiteThingIds` — ce qui permet à
|
||||
un écran de poser la question **sans second appel ni heuristique côté client**.
|
||||
### 18.5 « Jamais configuré » n'est pas « explicitement mis à rien »
|
||||
|
||||
### 18.3 « Jamais configuré » n'est pas « explicitement mis à rien »
|
||||
`forecastSiteChosen` est posé par **tout** appel à `SetForecastSite`, identifiant vide compris.
|
||||
Il ne sert plus à armer une adoption — il n'y en a plus — mais il reste utile : il distingue une
|
||||
installation **jamais configurée** d'une installation dont l'utilisateur a **délibérément coupé**
|
||||
la prévision. Les deux se taisent de la même façon ; elles ne veulent pas dire la même chose, et
|
||||
un futur mécanisme qui les confondrait rallumerait une fonction que quelqu'un avait éteinte.
|
||||
|
||||
La désignation d'office ne tourne que si **aucun choix n'a jamais été exprimé**. Un second
|
||||
réglage, `forecastSiteChosen`, est posé par **tout** appel à `SetForecastSite` — y compris avec
|
||||
un identifiant vide.
|
||||
### 18.6 Un détail qui a failli être faux
|
||||
|
||||
Sans lui, un utilisateur qui aurait délibérément coupé la prévision la verrait **se rallumer au
|
||||
prochain démarrage**, et sa décision serait écrasée en silence. C'est la même distinction
|
||||
absence/valeur que partout ici, appliquée à un réglage plutôt qu'à une mesure.
|
||||
|
||||
### 18.4 Deux détails d'implémentation qui ont failli être faux
|
||||
|
||||
**Un site candidat se reconnaît à une propriété OBSERVABLE** — un enfant qui publie
|
||||
`forecastCurveTransport` — jamais à un nom de ThingClass. Le producteur peut renommer la sienne
|
||||
ou en ajouter une seconde sans que cela nous concerne.
|
||||
|
||||
**Et la migration tourne au PREMIER CYCLE, pas au constructeur.** Au constructeur, le
|
||||
Le signalement tourne **au premier cycle, pas au constructeur**. Au constructeur, le
|
||||
`ThingManager` n'a pas encore chargé les Things : « aucun candidat » y serait toujours vrai, et
|
||||
le défaut d'office ne se déclencherait **jamais** — un mécanisme inerte qui aurait l'air posé.
|
||||
l'avertissement **ne se déclencherait jamais** — un mécanisme inerte qui aurait l'air posé.
|
||||
|
||||
@ -429,10 +429,10 @@ void EnergyArbitrator::update(const QDateTime ¤tDateTime)
|
||||
// La prévision se lit AVANT le contexte, et elle n'y entre pas : ce lot est un AFFICHAGE.
|
||||
// Elle ne pondère aucune échéance et ne déplace aucun plan — le jour où l'étalement éco la
|
||||
// lira, ce sera un lot à part, avec sa propre décision.
|
||||
// La désignation d'office attend le PREMIER CYCLE, pas le constructeur : au constructeur,
|
||||
// le ThingManager n'a pas encore chargé les Things, et « aucun candidat » y serait toujours
|
||||
// vrai — un défaut d'office qui ne se déclenche jamais.
|
||||
designerSiteAuPremierDemarrage();
|
||||
// Le signalement attend le PREMIER CYCLE, pas le constructeur : au constructeur, le
|
||||
// ThingManager n'a pas encore chargé les Things, et « aucun candidat » y serait toujours
|
||||
// vrai — un avertissement qui ne se déclencherait jamais.
|
||||
signalerSiteManquant();
|
||||
lireCourbePrevision();
|
||||
|
||||
SurplusContext ctx = buildContext(currentDateTime);
|
||||
@ -2274,39 +2274,30 @@ QStringList EnergyArbitrator::forecastSiteCandidates() const
|
||||
return sites;
|
||||
}
|
||||
|
||||
void EnergyArbitrator::designerSiteAuPremierDemarrage()
|
||||
void EnergyArbitrator::signalerSiteManquant()
|
||||
{
|
||||
// MIGRATION — ne tourne QUE si aucun choix n'a jamais été exprimé.
|
||||
// AUCUNE ADOPTION D'OFFICE — décision de Patrick, 2026-09-09, et la raison porte loin.
|
||||
//
|
||||
// Le silence est la bonne réponse à « cette installation n'a pas de prévision ». Il n'est
|
||||
// PAS la bonne réponse à « cette installation en avait une et nous venons de la lui
|
||||
// retirer » : la première est une configuration légitime, la seconde est une régression que
|
||||
// nous introduisons. Confondre les deux ferait passer notre propre rupture pour un cas
|
||||
// normal — exactement ce que la règle du silence existe pour ne pas couvrir.
|
||||
if (m_forecastSiteChoisi)
|
||||
return; // un choix existe, fût-il « aucun » — on n'y touche pas
|
||||
const QStringList candidats = forecastSiteCandidates();
|
||||
|
||||
// UN SEUL CANDIDAT : désigner n'est pas CHOISIR — il n'y a rien à choisir. C'est la seule
|
||||
// condition sous laquelle un défaut d'office ne devine rien.
|
||||
if (candidats.count() == 1) {
|
||||
m_forecastSiteId = candidats.first();
|
||||
m_forecastSiteChoisi = true;
|
||||
EnergySettings reglages;
|
||||
reglages.setValue(QStringLiteral("forecastSiteId"), m_forecastSiteId);
|
||||
reglages.setValue(QStringLiteral("forecastSiteChosen"), true);
|
||||
qCInfo(dcNymeaEnergy()) << "[Prévision] site unique désigné d'office :" << m_forecastSiteId
|
||||
<< "— une seule candidature, donc aucun choix fait à votre place";
|
||||
// Adopter le site unique paraîtrait sans risque : il n'y aurait « rien à choisir ». Mais la
|
||||
// désignation se PERSISTE, et le jour où un second site apparaît, elle devient un CHOIX QUE
|
||||
// PERSONNE N'A FAIT ET QUI A L'AIR FAIT. Rien ne distinguera plus « l'utilisateur a préféré
|
||||
// celui-ci » de « le moteur a pris le seul qu'il voyait en 2026 ».
|
||||
//
|
||||
// La distinction avec l'auto-provisionnement des bornes (+etm23) tient : celui-là ENREGISTRE
|
||||
// ce que le moteur pilote DÉJÀ, il ne choisit rien parmi plusieurs. Et le précédent du
|
||||
// rootmeter va dans ce sens — `SetRootMeter` exige une désignation explicite.
|
||||
if (!m_forecastSiteId.isEmpty() || m_forecastSiteSignale)
|
||||
return;
|
||||
}
|
||||
// PLUSIEURS CANDIDATS : là, il y a un vrai choix, et le faire serait deviner. On ne désigne
|
||||
// rien — mais on le DIT une fois, et GetForecastSite publie les candidats pour que l'écran
|
||||
// puisse poser la question. Un silence ici cacherait une fonction qui vient de s'arrêter.
|
||||
if (candidats.count() > 1)
|
||||
qCInfo(dcNymeaEnergy()) << "[Prévision]" << candidats.count()
|
||||
<< "sites candidats et aucun désigné — la prévision reste "
|
||||
"inactive jusqu'à un SetForecastSite (voir GetForecastSite)";
|
||||
// AUCUN CANDIDAT : rien à dire. C'est le cas normal d'une installation sans prévision.
|
||||
// SILENCE S'IL N'Y A RIEN À DÉSIGNER. Une installation sans plugin météo appairé n'a rien à
|
||||
// choisir : lui annoncer qu'il manque une désignation serait un avertissement qu'on ne peut
|
||||
// pas satisfaire, donc du bruit permanent.
|
||||
if (forecastSiteCandidates().isEmpty())
|
||||
return;
|
||||
m_forecastSiteSignale = true;
|
||||
qCInfo(dcNymeaEnergy()).noquote()
|
||||
<< QStringLiteral("[Prévision] %1 — la lecture reste arrêtée (motif %2)")
|
||||
.arg(renderFr({ DecisionCode::MissingForecastSite, {} }))
|
||||
.arg(QLatin1String(DecisionCode::MissingForecastSite));
|
||||
}
|
||||
|
||||
bool EnergyArbitrator::setForecastSiteId(const QString &siteId)
|
||||
@ -2320,6 +2311,7 @@ bool EnergyArbitrator::setForecastSiteId(const QString &siteId)
|
||||
// LE CHOIX EST MARQUÉ MÊME QUAND IL EST « AUCUN ». Sans cela, la désignation d'office se
|
||||
// réarmerait au prochain démarrage et écraserait une décision explicite, en silence.
|
||||
reglages.setValue(QStringLiteral("forecastSiteChosen"), true);
|
||||
m_forecastSiteSignale = false; // un nouveau vide se resignalera une fois
|
||||
qCInfo(dcNymeaEnergy()) << "[Prévision] site retenu :"
|
||||
<< (siteId.isEmpty() ? QStringLiteral("(aucun)") : siteId);
|
||||
return true;
|
||||
|
||||
@ -900,7 +900,10 @@ private:
|
||||
* l'utilisateur, en silence.
|
||||
*/
|
||||
bool m_forecastSiteChoisi = false;
|
||||
void designerSiteAuPremierDemarrage();
|
||||
//! Le manque a-t-il déjà été signalé ? Un avertissement par cycle noierait le journal.
|
||||
bool m_forecastSiteSignale = false;
|
||||
//! \brief Signale UNE FOIS qu'aucun site n'est désigné alors qu'il en existe.
|
||||
void signalerSiteManquant();
|
||||
QTimer *m_forecastPurge = nullptr;
|
||||
//! loadId → config AYANT SERVI à construire l'adaptateur. Permet le rebuild incrémental
|
||||
//! (ECS-412) : on ne reconstruit que si le matériel a changé.
|
||||
|
||||
@ -212,6 +212,13 @@ QString renderFr(const DecisionReason &reason, const QString &label)
|
||||
return QStringLiteral("Dérogation terminée sur %1 — %2").arg(label).arg(pourquoi);
|
||||
}
|
||||
|
||||
if (c == QLatin1String(DecisionCode::MissingForecastSite))
|
||||
// LE GESTE, pas seulement le manque. « Il en manque une » laisse l'utilisateur devant un
|
||||
// constat ; « choisissez-en un dans la configuration » lui donne la marche à suivre.
|
||||
// Même exigence que les trois causes d'ECO_FLOOR_MISSED : trois causes, trois gestes.
|
||||
return QStringLiteral("Aucun site de prévision désigné — choisissez-en un dans la "
|
||||
"configuration");
|
||||
|
||||
// --- Sécurité et cycle de vie ------------------------------------------
|
||||
if (c == QLatin1String(DecisionCode::DegradedL2))
|
||||
return QStringLiteral("Compteur muet depuis >90 s — consigne de repli (L2 watchdog)");
|
||||
|
||||
@ -280,6 +280,20 @@ constexpr const char *DerogationActive = "DEROGATION_ACTIVE";
|
||||
*/
|
||||
constexpr const char *DerogationEnded = "DEROGATION_ENDED";
|
||||
|
||||
/*!
|
||||
* \brief Des plans de prévision existent, mais AUCUN SITE n'est désigné — la lecture est arrêtée.
|
||||
* params : aucun.
|
||||
*
|
||||
* \note **Publié seulement si un site existe.** Une installation sans plugin météo appairé n'a
|
||||
* rien à désigner : lui annoncer qu'il manque une désignation serait un avertissement qu'on ne
|
||||
* peut pas satisfaire, donc du bruit permanent. Absence de site : silence. Site présent, aucun
|
||||
* désigné : ce motif.
|
||||
*
|
||||
* \note C'est lui qui rend le silence ACCEPTABLE : la lecture s'arrête, mais elle le dit — donc
|
||||
* l'écran peut nommer le geste qui manque au lieu d'afficher un vide.
|
||||
*/
|
||||
constexpr const char *MissingForecastSite = "MISSING_FORECAST_SITE";
|
||||
|
||||
// --- SG-Ready (PAC) ---------------------------------------------------------
|
||||
//! params: \c state — le verrou minStateHold maintient l'état (court-cycling compresseur).
|
||||
constexpr const char *LockMinStateHold = "LOCK_MIN_STATE_HOLD";
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
#ifdef ETM_ARBITRATOR
|
||||
#include "etm/energyarbitrator.h"
|
||||
#include "etm/forecast/forecaststore.h"
|
||||
#include "etm/types/decisionreason.h"
|
||||
#endif
|
||||
#include "types/charginginfo.h"
|
||||
#include "smartchargingmanager.h"
|
||||
@ -346,9 +347,19 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
|
||||
"publishing a curve; it is what a configuration screen offers. When exactly "
|
||||
"one candidate exists and nothing has ever been chosen, the engine designates "
|
||||
"it by itself - designating is not CHOOSING when there is nothing to choose. "
|
||||
"With two or more, it designates nothing and the choice is yours.";
|
||||
"NOTHING is ever designated automatically: the choice is persisted, and the "
|
||||
"day a second site appears it would look deliberate when nobody made it. "
|
||||
"'fault' carries MISSING_FORECAST_SITE when planes publish but no site is "
|
||||
"designated - it is ABSENT when no site exists at all, since announcing a "
|
||||
"missing designation to an installation that has nothing to designate would "
|
||||
"be an unsatisfiable warning.";
|
||||
returns.insert("o:forecastSiteThingId", enumValueName(Uuid));
|
||||
returns.insert("candidateSiteThingIds", QVariantList() << enumValueName(Uuid));
|
||||
// Le MOTIF, présent seulement quand il y a un geste à faire : des plans existent et aucun
|
||||
// site n'est désigné. Un CODE, jamais une phrase — le client la fabrique dans sa langue.
|
||||
// ABSENT quand aucun site n'existe : annoncer une désignation manquante à une installation
|
||||
// sans plugin météo serait un avertissement qu'on ne peut pas satisfaire.
|
||||
returns.insert("o:fault", enumValueName(String)); // MISSING_FORECAST_SITE
|
||||
registerMethod("GetForecastSite", description, params, returns, Types::PermissionScopeControlThings);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
@ -1130,8 +1141,15 @@ JsonReply *NymeaEnergyJsonHandler::GetForecastSite(const QVariantMap ¶ms)
|
||||
ret.insert(QStringLiteral("forecastSiteThingId"), id);
|
||||
// LES CANDIDATS, toujours rendus — même vides. C'est ce qui permet à un écran de poser
|
||||
// la question quand il y a un choix, sans un second appel ni une heuristique côté client.
|
||||
const QStringList candidats = arb->forecastSiteCandidates();
|
||||
ret.insert(QStringLiteral("candidateSiteThingIds"),
|
||||
QVariant::fromValue(arb->forecastSiteCandidates()).toList());
|
||||
QVariant::fromValue(candidats).toList());
|
||||
// Le motif ne sort que s'il y a un GESTE POSSIBLE : des plans publient, et personne n'a
|
||||
// désigné. Sans candidat, l'absence de désignation n'est pas un manque — c'est une
|
||||
// installation qui n'a pas de prévision, et c'est normal.
|
||||
if (id.isEmpty() && !candidats.isEmpty())
|
||||
ret.insert(QStringLiteral("fault"),
|
||||
QLatin1String(DecisionCode::MissingForecastSite));
|
||||
}
|
||||
#endif
|
||||
return createReply(ret);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user