La consigne de puissance reseau appartient au controleur

Fermer le contacteur ne suffit pas : au-dessus du SOC minimal, l ESS en
autoconsommation ne preleve que sa consigne et laisse la batterie porter
maison et voiture. Nuit du 01/09 : charge HC demarree a 01:25, arretee a
01:26 par le garde batterie, contacteur ferme et inerte jusqu a 02:00.

La consigne a 4200 W du proprietaire n etait pas decorative : c est elle
qui faisait fonctionner la prise de relais nocturne depuis le 07/08. La
retirer le 31/08 a supprime la substitution de journee et casse la nuit.

Le controleur ecrit desormais la consigne lui-meme, a partir du besoin
mesure, seulement pendant une session reseau deliberee, avec une bande
asymetrique : trop basse se corrige tout de suite, trop haute ne coute
rien. Deux ecritures en 25 minutes au lieu d une toutes les 30 s.

Golden 51 -> 56. Verifie en reel : batterie a 0 A, voiture a 3500 W.

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-01 02:14:57 +02:00
parent 92f07fe885
commit a57ff19ead
8 changed files with 297 additions and 4 deletions

View File

@@ -2026,3 +2026,99 @@ Preuve par mutation, les deux volets séparément :
---
*Document mis à jour le : 31/08/2026 (~19:00) — séquence d'ouverture corrigée, golden 51/51, PUT n8n en attente de validation*
## 31. Session du 01/09/2026 (nuit) — La consigne réseau appartient désormais au contrôleur
### L'incident : charge HC démarrée puis morte en soixante secondes
Fenêtre HC ouverte normalement, tout notre pilotage a fait son travail :
| 01:24:03-04 | latch hystérésis ON, EDF fermé, AC-IN présent |
| 01:24:00 | takeover nuit ON, source `grid_hc_allowed` (P7) |
| 01:25:15 | ACM en `evcs_takeover_hold`, DVCC à 2 A |
| 01:25:30 | `start_grid_hc_min_current` — la charge démarre |
| 01:25:42 | le réseau prend la voiture : import **1 450 W** |
| 01:26:33 | import retombé à 763 W puis **négatif**, batterie de −7 à **−25 A** |
| 01:26:30 | garde « batterie trop sollicitée (−21,5 A) » → arrêt + verrou 17 min |
| 01:44 → 01:47 | `battery_current_hard_stop` puis `idle_wait_conditions` |
### Cause : l'ESS n'utilise le réseau que si on le lui demande
`ac_power_setpoint` = **50 W** (corrigé le 31/08 depuis 4 200 W),
`ess_min_soc_limit` = 20 %, SOC = 50 % ⇒
`ess_batterylife_state` = `self_consumption_soc_above_min`. Au-dessus du SOC
minimal, l'ESS fait servir la batterie et ignore le réseau. Le pic à 1 450 W
était le transitoire de reprise, pas le régime : en 50 s le régulateur a rendu
la charge à la batterie en convergeant vers les 50 W.
Preuve indépendante, à 01:49 sans voiture : contacteur fermé, maison 761 W,
tuile Réseau « – », **batterie −803 W**. Le réseau était branché et inerte.
**Le garde a eu raison** : la pente était monotone, sa fin était « la batterie
porte la voiture toute la nuit ». Le neutraliser aurait été pire.
**Et la consigne à 4 200 W du propriétaire était PORTEUSE** : c'est elle qui
faisait fonctionner toute la mécanique de prise de relais nocturne (P4/P7).
J'avais écrit en la retirant que le besoin couvert restait entier et devait être
repris ailleurs ; je ne l'avais pas repris. Règle qui en découle, désormais en
mémoire : ne jamais retirer un de ces contournements avant d'avoir mis son
remplaçant adaptable en production.
### Correctif (déployé 01/09 02:10, golden 56/56)
Le contrôleur écrit lui-même `number.gx_device_ac_power_setpoint`.
- **Snapshot** : `grid_setpoint_w`, `grid_setpoint_idle_w` (helper optionnel
`input_number.evcs_grid_setpoint_idle_w`, absent → 50 W, le défaut Victron).
- **INPUT_PARSE** : `parsed.gridSetpointW`, `params.gridSetpointIdleW` [0, 500].
- **HA_SERVICE_CALLS_OUTPUT** :
`gridSessionActive = acInAvailable && sourceMode ∈ {grid_forced, grid_hc_allowed}`
— donc P7 nuit, P9 départ et `force_grid_now`, et rien d'autre.
Besoin = `houseNonEvcsW + physicalCapA × acVoltageRef` : une grandeur
**mesurée**, pas une constante. Hors session, retour à la valeur de repos.
- **BUILD_ACTIONS** : appel `grid_power_setpoint` (`number.set_value`).
- Trace `Consigne reseau=XW` dans la raison pendant une session.
**La bande d'écriture est ASYMÉTRIQUE, et c'est le point.** Une consigne trop
basse fait porter la différence par la batterie — c'est le défaut de cette nuit,
corrigé sans attendre. Une consigne trop haute ne coûte rien : l'ESS ne prélève
que ce qu'il peut placer. On écrit donc avec une marge d'**un pas de courant
EVCS** (`currentStepUpA × acVoltageRef` ≈ 230 W — grandeur du système, pas un
chiffre inventé) et on ne redescend qu'au-delà de deux marges. Première version
sans cette asymétrie : réécriture toutes les 30 s en poursuivant le bruit de la
maison. Après : **deux écritures en 25 minutes**.
⚠️ **Deux leviers, deux rôles, à ne pas confondre** : la consigne dit ce que le
réseau a le DROIT de servir ; **DVCC (Auto Charge Manager) dit ce que la batterie
a le droit de prendre**. C'est DVCC à 2 A qui empêche une consigne généreuse de
gaver la batterie pendant un `evcs_takeover_hold`, et c'est DVCC à 30 A qui
autorise la recharge pendant un `evcs_takeover_recover`. Le couple se compose
correctement, mais la générosité de la consigne DÉPEND de ce cap : ne pas
supprimer l'un sans revoir l'autre.
### Golden 51 → 56
`summarize()` expose `gridSetpointW` (valeur écrite ce cycle, `null` si aucune
écriture). Scénarios : session HC → 5 525 W · contrefactuel hors session → 50 W ·
consigne déjà généreuse (5 600) → aucune écriture · **consigne 45 W sous le
besoin → correction immédiate** (le défaut de la nuit) · dérive externe à
4 000 W hors session → ramenée à 50 W.
Preuve par mutation : consigne figée au repos → 2 échecs ciblés ; branche
« trop basse » neutralisée → 1 échec ciblé ; restauration → 56/56.
### Vérification en réel
02:10 → 02:13, contacteur fermé : grid 4 350 W, **batterie ~0 A**, EVCS 3 500 W,
SOC stable à 49 %, consigne écrite à 5 276 puis 5 616 W. La voiture charge sur
le réseau en HC sans toucher à la réserve — ce que P7 était censé faire depuis
le 07/08 et qui ne tenait que par un réglage manuel.
### Risque résiduel assumé
Si le contrôleur s'arrête en pleine session (SPOF Synology, cf. §3), la consigne
reste haute et la substitution de journée peut revenir. C'est le même mode de
défaillance que le reste de la chaîne, mais il est nouveau pour ce paramètre :
à reprendre le jour où le SPOF sera traité.
### Non fait volontairement
La fenêtre d'établissement (§29) ne couvre que le chemin solaire ; les gardes
réseau ne sont pas gardés. Ce n'est pas la cause de cet incident et, la consigne
étant désormais correcte, le transitoire observé au redémarrage de 01:58 était
de 20 s avec la batterie à −1,6 A. À traiter séparément si un cas se présente.
---
*Document mis à jour le : 01/09/2026 (~02:15) — consigne de puissance réseau pilotée par le contrôleur, golden 56/56, charge HC vérifiée en réel*

View File

@@ -124,6 +124,17 @@ pushAction(
context.sourceMode === 'grid_hc_allowed' || context.sourceMode === 'grid_forced'
);
// =====================================================
// CONSIGNE DE PUISSANCE RESEAU
// Fermer le contacteur ne suffit pas : sans consigne, l'ESS laisse la
// batterie porter la maison et la voiture (nuit du 01/09/2026).
// =====================================================
pushAction(
'grid_power_setpoint',
calls.grid_power_setpoint,
!!calls.grid_power_setpoint?.execute
);
// =====================================================
// AUTO START OFF SI BESOIN
// =====================================================

View File

@@ -1,3 +1,10 @@
// Bornes de la consigne de puissance reseau. Le maximum est celui declare par
// l'entite elle-meme (number.airtria_mpii_v1_ac_power_setpoint_l1, max 10000 W),
// pas un chiffre choisi ici. La bande morte evite de reecrire la meme valeur
// toutes les 30 s sans empecher la correction d'une derive externe.
const GRID_SETPOINT_MAX_W = 10000;
const GRID_SETPOINT_DEADBAND_W = 100;
function round1(v) {
return Math.round(v * 10) / 10;
}
@@ -309,6 +316,67 @@ reason.push(`SOC=${round1(parsed.soc)}%`);
reason.push(`Batt=${round1(parsed.batteryCurrentA)}A`);
reason.push(`PV=${Math.round(parsed.pvPowerW)}W`);
reason.push(`PV10=${Math.round(parsed.pvPowerAvg10mW)}W`);
// =====================================================
// CONSIGNE DE PUISSANCE RESEAU (01/09/2026)
// =====================================================
// Fermer le contacteur ne suffit PAS. Au-dessus du SOC minimal, l'ESS en
// autoconsommation ne preleve que `ac_power_setpoint` et laisse la batterie
// porter tout le reste : nuit du 01/09, contacteur ferme, consigne 50 W,
// batterie a -803 W pour 761 W de maison, voiture arretee par le garde
// "batterie trop sollicitee". La consigne est donc le levier qui rend une
// session reseau REELLE, et il doit appartenir au controleur, pas rester un
// reglage manuel permanent (celui-ci provoquait la substitution du solaire
// par le reseau en journee, cf. HANDOFF 27/28).
//
// La valeur n'est pas une constante : c'est le besoin mesure du moment, la
// maison telle qu'elle consomme plus la voiture au courant que le controleur
// s'autorise. Le plafond joue le role de marge : quand la voiture n'est pas a
// son plafond, la consigne est deja genereuse, et l'exces ne peut pas partir
// dans la batterie (DVCC tenue a 2 A par l'Auto Charge Manager pendant un
// takeover). Hors session, retour a la valeur de repos.
const gridSessionActive =
parsed.acInAvailable &&
(context.sourceMode === 'grid_forced' || context.sourceMode === 'grid_hc_allowed');
const gridSetpointNeedW = gridSessionActive
? clamp(
Math.round(derived.houseNonEvcsW + derived.physicalCapA * derived.acVoltageRef),
params.gridSetpointIdleW,
GRID_SETPOINT_MAX_W
)
: params.gridSetpointIdleW;
// La bande est ASYMETRIQUE, et c'est le point. Une consigne trop BASSE fait
// porter la difference par la batterie : c'est le defaut de la nuit du 01/09,
// il se corrige immediatement. Une consigne trop HAUTE ne coute rien : l'ESS
// ne preleve que ce qu'il peut placer, et le courant de charge de la batterie
// est tenu par ailleurs (DVCC, Auto Charge Manager). Deux leviers, deux roles
// -- la consigne dit ce que le reseau a le DROIT de servir, DVCC dit ce que la
// batterie a le droit de prendre. Ne pas confondre les deux.
//
// On ecrit donc avec une marge d'un pas de courant EVCS (grandeur du systeme,
// pas une constante inventee) pour que le bruit de la maison ne rende pas la
// consigne insuffisante au cycle suivant, et on ne redescend que si elle est
// devenue franchement inutile. Sans cela la boucle 30 s reecrivait a chaque
// cycle en poursuivant le bruit.
const gridSetpointSlackW = gridSessionActive
? Math.max(GRID_SETPOINT_DEADBAND_W, Math.round(params.currentStepUpA * derived.acVoltageRef))
: 0;
const gridSetpointTargetW = clamp(
gridSetpointNeedW + gridSetpointSlackW,
0,
GRID_SETPOINT_MAX_W
);
const gridSetpointCurrentW = Number.isFinite(parsed.gridSetpointW)
? parsed.gridSetpointW
: null;
const gridSetpointNeedsWrite =
gridSetpointCurrentW === null ||
gridSetpointCurrentW < gridSetpointNeedW ||
gridSetpointCurrentW > gridSetpointNeedW + 2 * gridSetpointSlackW + GRID_SETPOINT_DEADBAND_W;
reason.push(`Grid=${parsed.gridAvailable ? 'on' : 'off'} (${Math.round(parsed.gridPowerW)}W)`);
// Trace de la fenetre d'etablissement : sans elle, une lecture du logbook
// pendant le transitoire donne l'impression que le systeme decide n'importe
@@ -316,6 +384,9 @@ reason.push(`Grid=${parsed.gridAvailable ? 'on' : 'off'} (${Math.round(parsed.gr
if (derived.gridSettling) {
reason.push(`ETABLISSEMENT reseau ${Math.round(parsed.gridTransitionAgeSec)}s/${params.gridSettleSeconds}s (mesures PV/batterie non fiables)`);
}
if (gridSessionActive) {
reason.push(`Consigne reseau=${gridSetpointTargetW}W`);
}
reason.push(`Cap=${derived.physicalCapA}A`);
reason.push(`Ref=${round1(derived.currentReferenceA)}A`);
reason.push(`Target=${round1(desiredCurrentA)}A`);
@@ -1001,6 +1072,16 @@ return [{
}
},
grid_power_setpoint: {
execute: gridSetpointNeedsWrite,
domain: 'number',
service: 'set_value',
data: {
entity_id: 'number.gx_device_ac_power_setpoint',
value: gridSetpointTargetW
}
},
evcs_auto_start_off: {
execute: autoStartAction === 'turn_off',
domain: 'switch',

View File

@@ -17,6 +17,8 @@
"acin1_last_changed_ts": as_timestamp(states.sensor.victron_ac_in1_available.last_changed, 0) if states.sensor.victron_ac_in1_available is not none else 0,
"grid_switch_last_changed_ts": as_timestamp(states.switch.edf_general_ok_switch.last_changed, 0) if states.switch.edf_general_ok_switch is not none else 0,
"grid_settle_seconds": states("input_number.evcs_grid_settle_seconds"),
"grid_setpoint_w": states("number.gx_device_ac_power_setpoint"),
"grid_setpoint_idle_w": states("input_number.evcs_grid_setpoint_idle_w"),
"today_remaining_kwh": states("sensor.energy_production_today_remaining"),
"tomorrow_kwh": states("sensor.energy_production_tomorrow"),

View File

@@ -245,6 +245,13 @@ const parsed = {
gridIsOn: low(input.grid_state || 'off') === 'on',
acInAvailable: num(input.victron_acin1_available ?? input.ac_in1_available, 0) === 1,
// Consigne de puissance réseau de l'ESS, lue telle qu'elle est appliquée.
// C'est ELLE qui décide si un contacteur fermé sert à quelque chose : au-dessus
// du SOC minimal, l'ESS en autoconsommation ne prélève que cette puissance et
// laisse la batterie porter le reste. Nuit du 01/09 : contacteur fermé,
// consigne 50 W, batterie à -803 W pour 761 W de maison.
gridSetpointW: num(input.grid_setpoint_w, NaN),
gridAvailable:
(low(input.grid_state || 'off') === 'on') ||
(num(input.victron_acin1_available ?? input.ac_in1_available, 0) === 1),
@@ -419,6 +426,10 @@ const params = {
// input_number.evcs_grid_settle_seconds ; absent => 180.
gridSettleSeconds: clamp(num(input.grid_settle_seconds, 180), 0, 900),
// Consigne réseau au repos. 50 W est le défaut Victron (légèrement positif
// pour ne jamais injecter), pas une valeur choisie ici.
gridSetpointIdleW: clamp(num(input.grid_setpoint_idle_w, 50), 0, 500),
probeEnabled: low(input.probe_enabled || 'off') === 'on',
probeMinutes: Math.max(1, num(input.probe_minutes, 3)),
probeRetryMinutes: Math.max(5, num(input.probe_retry_minutes, 45)),

View File

@@ -116,6 +116,10 @@ function summarize(out) {
takeoverNeeded: !!c.night_grid_takeover_needed,
edfOnAction: !!calls.edf_general_ok_on?.execute,
takeoverHelperService: calls.helper_night_grid_takeover_needed?.service || null,
// Consigne de puissance reseau ecrite ce cycle (null = aucune ecriture).
gridSetpointW: calls.grid_power_setpoint?.execute
? calls.grid_power_setpoint.data.value
: null,
};
}

View File

@@ -1306,4 +1306,92 @@ export const scenarios = [
takeoverHelperService: "turn_off",
},
},
// ===================================================================
// CONSIGNE DE PUISSANCE RESEAU (01/09/2026)
// Fermer le contacteur ne suffit pas : au-dessus du SOC minimal, l'ESS en
// autoconsommation ne preleve que `ac_power_setpoint` et laisse la batterie
// porter maison + voiture. Incident de la nuit du 01/09 : consigne a 50 W,
// charge HC demarree puis arretee en 60 s par le garde batterie.
// ===================================================================
{
id: 'consigne-session-hc-reseau-porte-la-charge',
label: 'Nuit HC, reseau present, session reseau : la consigne monte au besoin mesure (maison hors EVCS + voiture au plafond)',
time: NIGHT,
overrides: {
...VEHICLE_NIGHT,
soc_percent: '50',
allow_grid_hc_night: 'on',
grid_state: 'on',
victron_acin1_available: '1',
grid_setpoint_w: '50',
},
expect: {
sourceMode: 'grid_hc_allowed',
gridSetpointW: 5525,
},
},
{
id: 'consigne-hors-session-retour-au-repos',
label: 'CONTREFACTUEL — meme nuit sans session reseau : la consigne redescend a la valeur de repos',
time: NIGHT,
overrides: {
...VEHICLE_NIGHT,
soc_percent: '50',
allow_grid_hc_night: 'off',
grid_state: 'off',
victron_acin1_available: '0',
grid_setpoint_w: '4000',
},
expect: {
gridSetpointW: 50,
},
},
{
id: 'consigne-deja-genereuse-aucune-ecriture',
label: 'Session reseau, consigne a 5600 W pour un besoin de 5295 W : deja suffisante, aucune reecriture (le bruit de la maison ne la fait pas courir)',
time: NIGHT,
overrides: {
...VEHICLE_NIGHT,
soc_percent: '50',
allow_grid_hc_night: 'on',
grid_state: 'on',
victron_acin1_available: '1',
grid_setpoint_w: '5600',
},
expect: {
gridSetpointW: null,
},
},
{
id: 'consigne-insuffisante-corrigee-immediatement',
label: "LE DEFAUT DE LA NUIT DU 01/09 — consigne 45 W sous le besoin : la batterie porterait la difference, correction sans attendre",
time: NIGHT,
overrides: {
...VEHICLE_NIGHT,
soc_percent: '50',
allow_grid_hc_night: 'on',
grid_state: 'on',
victron_acin1_available: '1',
grid_setpoint_w: '5250',
},
expect: {
sourceMode: 'grid_hc_allowed',
gridSetpointW: 5525,
},
},
{
id: 'consigne-derive-externe-corrigee',
label: 'Hors session mais consigne laissee a 4000 W par un tiers : corrigee au cycle suivant',
time: MORNING,
overrides: {
soc_percent: '60',
grid_state: 'off',
victron_acin1_available: '0',
grid_setpoint_w: '4000',
},
expect: {
gridSetpointW: 50,
},
},
];

File diff suppressed because one or more lines are too long