La provocation solaire se demande aux chargeurs, pas au SOC

Le correctif precedent bloque l incident de 19:44, mais un excedent proche de
zero est ambigu : c est la signature de la censure ET celle de l equilibre de
fin de journee. Le releve du 03/09 contient les deux avec le meme excedent —
18:50 : +14 W, ressource refusee ; 19:41 : +1 W, plus rien a donner. Rejeu du
19:41 : batteryAssistSolarStartOk etait encore vrai. Trois minutes avant
l incident, le systeme aurait commis la meme faute.

Le plan annoncait de lire evcs_balance_pv_censored. Lecture de CENSORSHIP.js :
ce drapeau est un OU de motifs dont l un est exactement le proxy qu on voulait
remplacer (socPct >= 90 et batterie sans puits). Et il est reste ON jusqu au
calcul de 19:50, soit cinq minutes APRES le demarrage de la charge — ce qui l a
fait basculer n est pas la censure mais sun_elevation passant sous 10 degres.
Il n aurait pas bloque l incident.

Le signal juste, ce sont les chargeurs eux-memes : sept entites
sensor.*_mppt_operation_mode, jamais lues par aucun workflow. mppt_active = je
suis a mon point de puissance maximale, je donne tout ; voltage_current_limited
ou off = je me retiens faute de preneur. Le 03/09 elles ont bascule a 19:20,
soit 24 minutes avant pv_censored, sans aucun proxy et a la cadence de la
boucle. Contre-verification le 01/09 a 14:22 : les trackers restaient limites
PENDANT une charge de 3,4 kW sous 6,9 kW de PV, donc le signal ne s auto-annule
pas a midi.

Collecte sans aucun entity_id code en dur (selectattr sur mppt et
operation_mode), expression testee sur /api/template puis gabarit complet
re-rendu : 191 champs, JSON valide, aucun null. Lectures inexploitables
ecartees, et repli sur le comportement anterieur quand il n en reste aucune.

Garde de DEMARRAGE uniquement, structurellement : la lier au sourceMode ou au
maintien ferait que la charge, en donnant un preneur au MPPT, detruirait sa
propre condition d autorisation — le bistable de l abondance P6 corrige le
24/08.

Golden 64 -> 66. Mutation dans trois directions : garde neutralisee -> 1 echec
cible, provocation toujours refusee -> 3 echecs, lecture illisible traitee
comme "a fond" -> 1 echec sur le scenario de repli.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
Tallagrand Thierry
2026-09-03 21:41:31 +02:00
parent ef3d7ed7b2
commit 7a26078e73
7 changed files with 239 additions and 4 deletions

View File

@@ -607,6 +607,43 @@ const commonSafetyHoldOk =
// recreerait l'oscillateur qu'on vient d'eteindre.
const solarSurplusW = parsed.pvPowerW - derived.houseNonEvcsW;
// LA PROVOCATION DOIT ETRE JUSTIFIEE PAR LES CHARGEURS EUX-MEMES (03/09/2026)
// =====================================================================
// Un excedent proche de zero est AMBIGU : c'est la signature de la censure
// (production bridee, calee sur la maison) ET celle de l'equilibre de fin de
// journee (production reelle qui rejoint la consommation). Le releve du 03/09
// contient les deux a 51 minutes d'ecart, avec le meme excedent :
//
// 18:50 PV 769 W, maison 755 W, excedent +14 W -> 7 trackers LIMITES
// 19:41 PV 817 W, maison 816 W, excedent +1 W -> 7 trackers MPPT_ACTIVE
//
// Le premier cache de la ressource refusee, le second n'a plus rien a donner.
// Aucun seuil de puissance ne les separe. Les chargeurs, eux, le disent :
// 'mppt_active' = je suis a mon point de puissance maximale, je donne tout ;
// 'voltage_current_limited' / 'off' = je me retiens faute de preneur.
//
// Signal DIRECT (le materiel lui-meme, pas un proxy), INSTANTANE (30 s, la
// cadence de la boucle) et, sur l'incident du 03/09, il a bascule a 19:20 —
// 24 minutes avant le drapeau `pv_censored` du bilan energetique, qui tourne
// toutes les 5 min et dont l'un des motifs est justement le proxy SOC >= 90
// qu'on cherchait a remplacer.
//
// ⚠️ DEMARRAGE UNIQUEMENT, et c'est structurel, pas une precaution :
// gager le sourceMode ou le MAINTIEN sur ce signal ferait que la charge, en
// donnant un preneur au MPPT, detruirait sa propre condition d'autorisation —
// exactement le bistable de l'abondance P6 corrige le 24/08. La garde n'est
// lue que quand la voiture est a l'arret, instant ou l'etat des trackers
// decrit la situation SANS elle. Verifie le 01/09 a 14:22-14:34 : les trackers
// restaient limites PENDANT une charge de 3,4 kW sous 6,9 kW de PV — a midi la
// ressource depasse ce que la voiture peut prendre, le signal ne s'auto-annule
// donc pas non plus.
//
// Aucune lecture exploitable (integration muette) => on ne sait pas => on
// retombe sur le comportement anterieur. On ne devine jamais un etat.
const mpptReadable = parsed.mpptReadableCount > 0;
const mpptAllAtMaxPower = mpptReadable && parsed.mpptHoldingBackCount === 0;
const solarProvocationJustified = !mpptAllAtMaxPower;
// Chemin PROVOCATION (batterie haute, ou sourceMode solar_potential) : on
// n'exige pas d'excedent mesurable — il est censure par construction — mais on
// exige que la batterie ne porte pas deja la maison.
@@ -615,7 +652,7 @@ const solarSurplusProvocationOk = solarSurplusW >= 0;
// Chemin SOLEIL REEL : l'excedent doit atteindre le seuil de demarrage.
const solarSurplusStartOk =
context.sourceMode === 'solar_potential'
? solarSurplusProvocationOk
? (solarSurplusProvocationOk && solarProvocationJustified)
: solarSurplusW >= context.effectivePvStartMinW;
// =====================================================
@@ -650,6 +687,7 @@ const batteryAssistSolarStartOk =
parsed.soc > batteryAssistSolarHoldSocMin &&
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
solarSurplusProvocationOk &&
solarProvocationJustified &&
cloudDebtNextKwh <= batteryAssistSolarDebtStartMaxKwh &&
!cloudDebtActive;
@@ -1023,6 +1061,12 @@ return [{
solarSurplusW,
solarSurplusProvocationOk,
solarSurplusStartOk,
// Etat declare par les chargeurs solaires (03/09/2026).
mpptReadableCount: parsed.mpptReadableCount,
mpptHoldingBackCount: parsed.mpptHoldingBackCount,
mpptAllAtMaxPower,
solarProvocationJustified,
batteryCurrentHardExceeded,
mustStopHard,
gridBatteryProtectionStop,

View File

@@ -6,6 +6,11 @@
"pv_power_10m_avg_w": states("sensor.victron_pv_power_10m_avg"),
"pv_trend_w": states("sensor.victron_pv_power_trend_w"),
"mppt_operation_modes": states.sensor
| selectattr('entity_id','search','mppt')
| selectattr('entity_id','search','operation_mode')
| map(attribute='state') | join(','),
"house_ac_power_w": states("sensor.victron_consumption_power_l1"),
"house_ac_power_prev_w": states("sensor.victron_consumption_power_l1"),

View File

@@ -236,6 +236,25 @@ const parsed = {
batteryCurrentA: num(input.battery_current_a, 0),
pvPowerW: Math.max(0, num(input.pv_power_w, 0)),
// MODE DE FONCTIONNEMENT DES CHARGEURS SOLAIRES (03/09/2026)
// Chaque tracker declare lui-meme s'il est a son point de puissance maximale
// ('mppt_active' : il donne tout ce qu'il a) ou s'il se retient
// ('voltage_current_limited', 'off' : il y a de la ressource REFUSEE).
// Lectures inexploitables (integration muette) => listes vides => les gardes
// aval retombent sur le comportement anterieur. On ne devine pas un etat.
...(() => {
const modes = String(input.mppt_operation_modes || '')
.split(',')
.map(s => s.trim().toLowerCase())
.filter(s => s !== '' && s !== 'unavailable' && s !== 'unknown');
const atMaxPower = modes.filter(s => s === 'mppt_active').length;
return {
mpptReadableCount: modes.length,
mpptAtMaxPowerCount: atMaxPower,
mpptHoldingBackCount: modes.length - atMaxPower,
};
})(),
pvPowerAvg10mW: Math.max(0, num(input.pv_power_10m_avg_w, num(input.pv_power_w, 0))),
pvTrendW: num(input.pv_trend_w, 0),