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

@@ -2622,3 +2622,101 @@ l'energie arrive — demain matin est creux : 0,03 kWh a 7 h, 0,77 a 8 h, 1,81 a
---
*Document mis a jour le : 03/09/2026 (~20:55) — demarrage sur l'excedent, golden 64/64, deploye*
## 37. Session du 03/09/2026 (suite) — La provocation solaire demandee aux chargeurs eux-memes
### Le trou que laissait §36
Le correctif sur l'excedent bloque bien l'incident de 19:44 (excedent −444 W).
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 :
| | PV | maison | excedent | batterie |
|---|---:|---:|---:|---:|
| 18:50 — censure | 769 W | 755 W | **+14 W** | 0 A |
| 19:41 — equilibre | 817 W | 816 W | **+1 W** | −1,4 A |
Rejeu du 19:41 apres §36 : `batteryAssistSolarStartOk` **encore vrai**. Trois
minutes avant l'incident, le systeme aurait commis la meme faute. Aucun seuil
de puissance ne separe ces deux etats.
### Pourquoi ce n'est PAS `pv_censored` qu'il fallait lire
Le plan annoncait « lire `input_boolean.evcs_balance_pv_censored` ». Lecture du
code (`src/energy-balance/CENSORSHIP.js`) : ce drapeau est un **OU de motifs**,
et l'un d'eux est exactement le proxy qu'on voulait remplacer —
`socPct >= 90 && batteryPowerW < 500` (motif `batterie_pleine_sans_puits`).
Le remplacer par lui serait remplacer un proxy par un sur-ensemble qui le
contient.
Chronologie du 03/09, verifiee : le bilan tourne toutes les **5 min** ;
`pv_censored` est reste **ON jusqu'au calcul de 19:50**, soit **5 minutes APRES
le demarrage de la charge**. Ce qui a fini par le faire basculer n'est pas la
censure mais `sun_elevation` passant sous 10°. **Le point 2 tel qu'annonce
n'aurait pas bloque l'incident.**
### Le signal qui marche : le mode de fonctionnement des chargeurs
`sensor.*_mppt_operation_mode` et `*_pv_tracker_solar_N_operation_mode` — sept
entites, jamais lues par aucun workflow (deja signalees §28). Chaque chargeur
declare lui-meme son regime :
- `mppt_active` = je suis a mon point de puissance maximale, **je donne tout** ;
- `voltage_current_limited` / `off` = **je me retiens**, faute de preneur.
Le 03/09 : sept trackers `voltage_current_limited` jusqu'a 19:10, tous
`mppt_active` a partir de **19:20**. Soit **24 minutes avant** `pv_censored`,
sans aucun proxy — ni SOC, ni elevation solaire, ni seuil de puissance. Signal
DIRECT (le materiel), INSTANTANE (30 s, la cadence de la boucle).
Contre-verification indispensable, le 01/09 a 14:22-14:34 : les trackers sont
restes `voltage_current_limited` **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, et `voltage_current_limited` en cours de charge veut
dire « il reste de la ressource refusee ».
### Le correctif (deploye 03/09 21:40, golden 66/66)
- **Gabarit snapshot** : `mppt_operation_modes`, collecte par
`states.sensor | selectattr('entity_id','search','mppt') | selectattr('entity_id','search','operation_mode')`
— **aucun entity_id code en dur**, un chargeur ajoute demain est pris en
compte tout seul. Expression testee sur `/api/template` AVANT integration,
puis gabarit complet re-rendu : 191 champs, JSON valide, aucun null.
- **INPUT_PARSE** : `mpptReadableCount` / `mpptAtMaxPowerCount` /
`mpptHoldingBackCount`. Les lectures `unavailable`/`unknown`/vides sont
ecartees — on ne devine jamais un etat.
- **BATTERY_CLOUD_LOCKS** :
`solarProvocationJustified = !(lectures exploitables && aucun tracker ne se retient)`,
exige par `batteryAssistSolarStartOk` et par la branche `solar_potential` de
`solarSurplusStartOk`.
⚠️ **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, et le chemin `stop_source_lost`
de la §36. La garde n'est lue que quand la voiture est a l'arret, instant ou
l'etat des trackers decrit la situation SANS elle.
**Repli** : aucune lecture exploitable => `mpptReadableCount === 0` =>
`solarProvocationJustified` vrai => comportement anterieur. Degradation vers
l'existant, jamais vers une supposition.
### Golden 64 → 66
- `provocation-trackers-a-fond-ne-demarre-pas` : le releve du 19:41 (excedent
+1 W, SOC 94, sept trackers `mppt_active`) → `idle_wait_conditions` ;
- `provocation-trackers-illisibles-comportement-anterieur` : meme releve,
lectures `unavailable` → `start_min_current` (le repli est teste, pas
seulement affirme).
- `excedent-censure-demarre` et `apresmidi-provocation-p3` portent desormais
l'etat reel des trackers (limites), et continuent de demarrer.
Mutation, trois directions : garde neutralisee → 1 echec cible (le 19:41
seulement) ; provocation toujours refusee → 3 echecs (les cas qui provoquent
legitimement) ; lecture illisible traitee comme « a fond » → 1 echec (celui du
repli). Restauration → 66/66.
### Reste du plan
3. **Lire `evcs_balance_required_soc` / `_energy_to_cover_kwh`** pour que le
plancher du soir cesse d'etre un niveau fixe (`evening_soc_start_min` = 85).
Le constat structurel de la §36 reste entier : `grep evcs_balance` sur les
deux workflows de decision renvoie toujours **0**.
---
*Document mis a jour le : 03/09/2026 (~21:45) — provocation solaire gagee sur le mode des chargeurs, golden 66/66, deploye*

View File

@@ -785,6 +785,23 @@ Golden 61 → 64, autour d'une paire de releves reels pris a 54 minutes d'ecart
dont les lectures PV ne different que de **11 W** (769 puis 758) : le premier
demarre, le second non — ce qui les separe est la maison. Detail : HANDOFF §36.
**Complement du 03/09 au soir — un excedent nul est ambigu.** L'excedent proche
de zero est la signature de la censure ET celle de l'equilibre de fin de
journee : 18:50 (+14 W, ressource refusee) et 19:41 (+1 W, plus rien a donner)
sont indiscernables par la puissance. Ce sont les **chargeurs eux-memes** qui
tranchent : `sensor.*_mppt_operation_mode` vaut `mppt_active` quand le tracker
donne tout, `voltage_current_limited` ou `off` quand il se retient. Sept
entites, que ne lisait aucun workflow. Le 03/09 elles ont bascule a 19:20 —
**24 minutes avant** le drapeau `pv_censored` du bilan (qui tourne toutes les
5 min et dont l'un des motifs est justement le proxy SOC ≥ 90 qu'on cherchait a
remplacer). La provocation `solar_potential` et l'assistance batterie exigent
desormais qu'au moins un chargeur se retienne.
⚠️ Garde de DEMARRAGE uniquement, structurellement : la lier au maintien ferait
que la charge, en donnant un preneur au MPPT, detruirait sa propre autorisation
— le bistable P6 du 24/08. Lectures indisponibles → repli sur le comportement
anterieur. Golden 64 → 66. Detail : HANDOFF §37.
⚠️ Point structurel mis au jour par cet incident : le bilan energetique publie
l'heure de croisement, l'energie a couvrir, le SOC requis, `grid_needed` et
`pv_censored` — et **aucun des deux workflows de decision n'en lit une seule

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),

View File

@@ -903,6 +903,7 @@ export const scenarios = [
soc_percent: '95', pv_power_w: '430', pv_power_10m_avg_w: '430',
pv_trend_w: '0', battery_current_a: '2',
house_ac_power_w: '400', vebus_ac_out_l1_power_w: '400',
mppt_operation_modes: 'voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited',
},
expect: {
profile: 'outside_window',
@@ -936,6 +937,7 @@ export const scenarios = [
pv_power_w: '769.1', pv_power_10m_avg_w: '800', pv_trend_w: '0',
house_ac_power_w: '755', vebus_ac_out_l1_power_w: '755',
evcs_ac_power_w: '0',
mppt_operation_modes: 'voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited',
},
expect: {
profile: 'evening_high_soc',
@@ -991,6 +993,56 @@ export const scenarios = [
},
},
// ===== 03/09/2026 : la provocation doit etre justifiee par les chargeurs =====
// Un excedent proche de zero est AMBIGU. Meme releve, meme excedent, deux
// situations opposees a 51 min d'ecart le 03/09 :
// 18:50 excedent +14 W, trackers LIMITES -> ressource refusee, on y va
// 19:41 excedent +1 W, trackers MPPT_ACTIVE -> plus rien a donner, on reste
// Aucun seuil de puissance ne les separe ; les chargeurs, eux, le disent.
{
id: 'provocation-trackers-a-fond-ne-demarre-pas',
label: "Relevé du 03/09 19:41 : excédent +1 W (équilibre au crépuscule), SOC 94, mais les 7 trackers en mppt_active → plus rien à révéler → PAS de provocation",
time: '2026-08-08T19:41:00+02:00',
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '94', battery_current_a: '-1.4',
pv_power_w: '816.9', pv_power_10m_avg_w: '850', pv_trend_w: '0',
house_ac_power_w: '816', vebus_ac_out_l1_power_w: '816',
evcs_ac_power_w: '0',
mppt_operation_modes: 'mppt_active,mppt_active,mppt_active,mppt_active,mppt_active,mppt_active,mppt_active',
},
expect: {
profile: 'evening_high_soc',
state: 'idle_wait_conditions',
decision: 'hold',
desiredCharging: false,
},
},
{
// Le repli doit etre sur le comportement ANTERIEUR, jamais sur une
// supposition : integration muette => on ne sait pas => on autorise comme
// avant. Meme releve que ci-dessus, seules les lectures changent.
id: 'provocation-trackers-illisibles-comportement-anterieur',
label: "Même relevé, mais chargeurs illisibles (unavailable) → on ne sait pas → repli sur le comportement antérieur, la charge démarre",
time: '2026-08-08T19:41:00+02:00',
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '94', battery_current_a: '-1.4',
pv_power_w: '816.9', pv_power_10m_avg_w: '850', pv_trend_w: '0',
house_ac_power_w: '816', vebus_ac_out_l1_power_w: '816',
evcs_ac_power_w: '0',
mppt_operation_modes: 'unavailable,unknown,',
},
expect: {
profile: 'evening_high_soc',
state: 'start_min_current',
decision: 'start',
desiredCharging: true,
currentA: 6,
},
},
// ===== 01/09/2026 : le depassement souple de la batterie doit DURER =====
// Incident du 01/09 (14:26 et 14:56) : SOC 94, batterie en float, courant
// oscillant entre +1,8 et -2,6 A d'un echantillon a l'autre. Le controleur

File diff suppressed because one or more lines are too long