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:
96
HANDOFF.md
96
HANDOFF.md
@@ -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*
|
||||
|
||||
@@ -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
|
||||
// =====================================================
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user