fix(arbitre): un filtre par interface qui ne trouve rien doit le DIRE

Un filterByInterface qui rend une liste vide est toujours ambigu entre « il n'y a pas de
batterie » et « il y en a une, mal déclarée ». Le second désactive une règle de sécurité
sans produire ni erreur ni panne — même mode de défaillance qu'un seuil de réserve mal
réglé : rien ne se signale.

Constaté sur le banc : le Thing « SunSpec Storage » déclare battery mais pas
energystorage. La réserve ne s'est donc JAMAIS appliquée — ni dans le waterfall que je
viens d'écrire, ni dans le proxy amont qui utilisait le même filtre depuis toujours, ni
dans le bilan de puissance de l'experience-plugin qui filtre pareil. GetPowerBalance rend
currentPowerStorage: 0 pendant que la batterie affiche 50 % et « Holding » : elle n'est
pas à zéro, elle est invisible.

L'arbitre émet maintenant un avertissement quand des appareils portent 'battery' sans
'energystorage'. Il ne corrige rien — la déclaration appartient au plugin — mais il rend
la cause lisible au lieu de la laisser deviner.

Brief émis vers etm-powersync-plugins-modbus : ce n'est pas une ligne de JSON, l'interface
exige currentPower et capacity que la classe ne fournit pas, et un currentPower de
batterie à zéro serait pire que son absence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
This commit is contained in:
Patrick Schurig 2026-08-26 11:57:24 +02:00
parent 9157ebdfe7
commit 5685ee8596

View File

@ -437,6 +437,28 @@ SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const
ctx.battery.present = true;
ctx.battery.socPercent = niveauTotal / n;
ctx.battery.powerW = puissanceTotale; // > 0 charge, < 0 décharge
} else {
// Un filtre par interface qui ne trouve rien est TOUJOURS ambigu : « il n'y a pas
// de batterie » et « il y en a une, mal déclarée » se ressemblent, et la seconde
// désactive une règle de sécurité EN SILENCE. C'est le même mode de défaillance
// qu'un seuil de réserve mal réglé : rien ne se signale.
//
// Constaté sur le banc le 2026-08-26 : le Thing « SunSpec Storage » déclare
// `battery` mais pas `energystorage`, donc la réserve ne s'est jamais appliquée —
// ni ici, ni dans le proxy amont qui utilisait le même filtre, ni dans le bilan de
// puissance de l'experience-plugin qui filtre pareil.
const Things candidates = m_tm->configuredThings().filterByInterface("battery");
if (!candidates.isEmpty()) {
QStringList noms;
for (Thing *t : candidates)
noms << t->name();
qCWarning(dcNymeaEnergy())
<< "[Arbitre] Réserve de stockage INACTIVE :" << candidates.count()
<< "appareil(s) portant l'interface 'battery' mais PAS 'energystorage' —"
<< noms.join(", ") << ". Le waterfall ne peut pas les prendre en compte, et"
<< "le bilan de puissance de nymea non plus. À corriger dans le plugin qui"
<< "déclare la classe, pas ici.";
}
}
}