Sequence d ouverture : baisser le courant avant que le reseau parte
Le plafond majore (28 A en BOOST) n est legitime que tant que le reseau alimente AC-IN : sinon les deux MultiPlus portent seuls la voiture plus la maison, le mecanisme du W-105. Personne ne baissait avant l ouverture -- c est l hysteresis qui ouvre, l EVCS l apprenait apres coup -- et quand il l apprenait, la reduction attendait le cooldown de courant et descendait d un pas a la fois. Correctif sans poignee de main ni helper nouveau : le plafond majore exige que l EVCS DEMANDE lui-meme le reseau. Sa demande tombe au cycle ou la fenetre se ferme (30 s), l hysteresis ne peut ouvrir qu ensuite (override disparu PUIS min_on, cycle 1 min) : l EVCS baisse toujours avant, mecaniquement. Second volet : un plafond n est pas une cible qu on approche. Reduction immediate et directe au plafond, sans cooldown. Le calcul du plafond a ete deplace apres sourceMode dans WINDOWS_SOURCEV2 ; rien entre les deux ne le lisait, verifie avant de bouger. Helper a 20 A par defaut : comportement inchange. Le correctif ne mord que sur le BOOST 28 A, le cas dangereux. Golden 48 -> 51, chaque cas apparie a son contrefactuel. Mutations des deux volets separement : 1 puis 2 echecs cibles, restauration 51/51. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
75
HANDOFF.md
75
HANDOFF.md
@@ -1951,3 +1951,78 @@ restant verts puisqu'ils n'en dépendent pas. Restauration → 48/48.
|
|||||||
|
|
||||||
---
|
---
|
||||||
*Document mis à jour le : 31/08/2026 (~18:20) — fenêtre d'établissement déployée dans src/, golden 48/48, PUT n8n en attente de validation*
|
*Document mis à jour le : 31/08/2026 (~18:20) — fenêtre d'établissement déployée dans src/, golden 48/48, PUT n8n en attente de validation*
|
||||||
|
|
||||||
|
## 30. Session du 31/08/2026 (suite) — Séquence d'ouverture du contacteur
|
||||||
|
|
||||||
|
### Le défaut, en deux morceaux
|
||||||
|
Le plafond de courant EVCS majoré (P5, `input_number.evcs_plafond_courant_edf_on`,
|
||||||
|
jusqu'à **28 A en BOOST**) n'est légitime que tant que le réseau alimente AC-IN :
|
||||||
|
hors réseau, les deux MultiPlus portent seuls la voiture **plus** la maison.
|
||||||
|
C'est le mécanisme du W-105 déjà consigné (§3, maintenance du 09/07).
|
||||||
|
|
||||||
|
1. **Personne ne baissait avant l'ouverture.** C'est l'hystérésis (cycle 1 min)
|
||||||
|
qui ouvre le contacteur ; le contrôleur EVCS ne l'apprenait qu'après coup, via
|
||||||
|
`parsed.acInAvailable`. Les deux automates n'ont aucune poignée de main.
|
||||||
|
2. **Et quand il l'apprenait, il ne baissait pas vite.** La branche
|
||||||
|
`charging_step_down_system_cap` de `DECISION_ENGINEV2` exigeait
|
||||||
|
`lifecycle.currentCooldownElapsed` — un compteur de **confort** — et ne
|
||||||
|
descendait que de `currentStepDownA` par cycle. De 28 A à 20 A : plusieurs
|
||||||
|
cooldowns. L'exposition ne se comptait donc pas en secondes mais en minutes.
|
||||||
|
|
||||||
|
### Le correctif
|
||||||
|
**Aucune poignée de main entre les deux automates, aucun helper nouveau.** Le
|
||||||
|
plafond majoré exige désormais que **l'EVCS demande lui-même le réseau** :
|
||||||
|
|
||||||
|
```js
|
||||||
|
const evcsGridCeilingAllowed =
|
||||||
|
parsed.acInAvailable &&
|
||||||
|
(sourceMode === 'grid_forced' || sourceMode === 'grid_hc_allowed' || forceGridRequested);
|
||||||
|
```
|
||||||
|
|
||||||
|
L'ordonnancement devient mécanique : la demande de l'EVCS tombe au cycle où la
|
||||||
|
fenêtre se ferme (30 s), alors que l'hystérésis ne peut ouvrir qu'**ensuite**
|
||||||
|
(sa branche `turn_off_recovered` exige justement que l'override ait disparu, puis
|
||||||
|
min_on écoulé, sur un cycle d'une minute). L'EVCS baisse donc toujours avant,
|
||||||
|
sans que rien ne soit coordonné.
|
||||||
|
|
||||||
|
⚠️ **Le calcul du plafond a été DÉPLACÉ après le calcul de `sourceMode`** dans
|
||||||
|
`WINDOWS_SOURCEV2` (juste avant `const operatingMode = profile;`). Vérifié avant
|
||||||
|
de bouger : `physicalCapA`, `canRunAtMinCurrent` et les quatre variables
|
||||||
|
intermédiaires n'étaient utilisés qu'à leur définition et dans l'export — rien
|
||||||
|
entre les deux ne les lisait. Ne pas les remonter.
|
||||||
|
|
||||||
|
Second volet, dans `DECISION_ENGINEV2` : **un plafond n'est pas une cible qu'on
|
||||||
|
approche, c'est une limite qu'on respecte.** La réduction ne dépend plus du
|
||||||
|
cooldown et descend directement au plafond (`decision: 'step_down_fast'`, valeur
|
||||||
|
déjà utilisée par la branche surcharge). Réduire est toujours sûr ; c'est la
|
||||||
|
montée qui doit être temporisée, et elle l'est.
|
||||||
|
|
||||||
|
### Portée réelle, et ce qui reste exposé
|
||||||
|
Avec le helper à sa valeur par défaut (20 A), **le comportement est strictement
|
||||||
|
inchangé** : le plafond valait déjà 20 A dans les deux branches. Le correctif ne
|
||||||
|
mord que sur le BOOST 28 A, c'est-à-dire exactement le cas dangereux.
|
||||||
|
|
||||||
|
Reste irréductible : une ouverture **manuelle** (EDF URGENCE) ou une perte
|
||||||
|
physique du réseau ne se laissent pas anticiper. L'exposition y passe de
|
||||||
|
plusieurs minutes à **≤ 30 s plus le temps de rampe de la borne**, ce qui est le
|
||||||
|
minimum atteignable sans signal matériel.
|
||||||
|
|
||||||
|
### Golden 48 → 51
|
||||||
|
| scénario | attendu |
|
||||||
|
|---|---|
|
||||||
|
| `ouverture-plafond-tombe-hors-demande-reseau` (AC-IN présent, BOOST 28, matin solaire, cooldown NON écoulé) | `charging_step_down_system_cap` → **20 A immédiatement** |
|
||||||
|
| `ouverture-plafond-maintenu-en-demande-reseau` (CONTREFACTUEL, `force_grid_now`) | `charging_hold_grid` **28 A**, BOOST préservé |
|
||||||
|
| `ouverture-reseau-disparu-reduction-immediate` (AC-IN absent, cooldown non écoulé) | réduction immédiate à 20 A |
|
||||||
|
|
||||||
|
Preuve par mutation, les deux volets séparément :
|
||||||
|
- plafond majoré reconditionné au seul `acInAvailable` → 1 échec ciblé ;
|
||||||
|
- réduction remise sous `currentCooldownElapsed` → 2 échecs ciblés ;
|
||||||
|
- restauration → 51/51.
|
||||||
|
|
||||||
|
### Suite
|
||||||
|
3. priorité solaire pour la voiture à batterie pleine (coût marginal nul :
|
||||||
|
6-7 trackers sur 7 bridés = ressource refusée) et interdiction de fermer le
|
||||||
|
contacteur pour charger une voiture en journée batterie pleine (~150 Wh).
|
||||||
|
|
||||||
|
---
|
||||||
|
*Document mis à jour le : 31/08/2026 (~19:00) — séquence d'ouverture corrigée, golden 51/51, PUT n8n en attente de validation*
|
||||||
|
|||||||
@@ -834,18 +834,20 @@ else if (cloud.sessionObservedActive) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if ((derived.currentReferenceA > derived.physicalCapA) && lifecycle.currentCooldownElapsed) {
|
// SÉQUENCE D'OUVERTURE (31/08/2026) : un plafond n'est pas une cible qu'on
|
||||||
|
// approche, c'est une limite qu'on respecte. Cette réduction attendait le
|
||||||
|
// cooldown de courant — un compteur de confort — et ne descendait que d'un
|
||||||
|
// pas à la fois : de 28 A à 20 A elle prenait plusieurs cooldowns, pendant
|
||||||
|
// lesquels les onduleurs portaient un courant qu'ils n'ont plus le droit de
|
||||||
|
// porter (le plafond retombe à la disparition du réseau). Désormais :
|
||||||
|
// descente IMMÉDIATE et DIRECTE au plafond. Réduire est toujours sûr ; c'est
|
||||||
|
// la montée qui doit être temporisée, et elle l'est.
|
||||||
|
else if (derived.currentReferenceA > derived.physicalCapA) {
|
||||||
setOn({
|
setOn({
|
||||||
stateName: 'charging_step_down_system_cap',
|
stateName: 'charging_step_down_system_cap',
|
||||||
decisionName: 'step_down',
|
decisionName: 'step_down_fast',
|
||||||
currentA: Math.max(
|
currentA: Math.max(params.currentMinA, derived.physicalCapA),
|
||||||
params.currentMinA,
|
reasonText: `Réduction immédiate au plafond système ${derived.physicalCapA}A`,
|
||||||
Math.min(
|
|
||||||
derived.currentReferenceA - Math.max(params.currentStepDownA, 2),
|
|
||||||
derived.physicalCapA
|
|
||||||
)
|
|
||||||
),
|
|
||||||
reasonText: 'Réduction pour respecter la capacité système',
|
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
mustStopFlag: false,
|
mustStopFlag: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -117,25 +117,6 @@ const vebusNonEvcsW = Math.max(0, parsed.vebusAcOutPowerW - parsed.evcsAcPowerW)
|
|||||||
const totalVebusSoftCeilingW = Math.max(6500, params.acLoadsMaxSafeW || 8500);
|
const totalVebusSoftCeilingW = Math.max(6500, params.acLoadsMaxSafeW || 8500);
|
||||||
const totalVebusHardCeilingW = totalVebusSoftCeilingW + 400;
|
const totalVebusHardCeilingW = totalVebusSoftCeilingW + 400;
|
||||||
|
|
||||||
// P5 : plafond EVCS configurable quand le réseau alimente réellement AC-IN
|
|
||||||
// (input_number.evcs_plafond_courant_edf_on). Hors réseau : plafond câblé prudent.
|
|
||||||
const evcsDedicatedCapBaseW = Math.max(0, cfg.evcsDedicatedMaxW - cfg.systemReserveW);
|
|
||||||
const safeEvcsByDedicatedCapW = parsed.acInAvailable
|
|
||||||
? Math.max(0, params.gridCurrentMaxA * acVoltageRef)
|
|
||||||
: evcsDedicatedCapBaseW;
|
|
||||||
const safeEvcsByVebusW = Math.max(0, totalVebusSoftCeilingW - vebusNonEvcsW - cfg.systemReserveW);
|
|
||||||
const safeEvcsCombinedW = Math.max(0, Math.min(safeEvcsByDedicatedCapW, safeEvcsByVebusW));
|
|
||||||
|
|
||||||
const systemCapCurrentA = Math.max(0, Math.floor(safeEvcsCombinedW / acVoltageRef));
|
|
||||||
const physicalCapA = clamp(
|
|
||||||
systemCapCurrentA,
|
|
||||||
0,
|
|
||||||
parsed.acInAvailable
|
|
||||||
? Math.max(params.currentMaxAutoA, params.gridCurrentMaxA)
|
|
||||||
: params.currentMaxAutoA
|
|
||||||
);
|
|
||||||
const canRunAtMinCurrent = physicalCapA >= params.currentMinA;
|
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// FENETRE D'ETABLISSEMENT DU RESEAU (31/08/2026)
|
// FENETRE D'ETABLISSEMENT DU RESEAU (31/08/2026)
|
||||||
// =====================================================
|
// =====================================================
|
||||||
@@ -779,6 +760,50 @@ if (dayVehicleFullIdle && profile !== 'night_hc') {
|
|||||||
sourceMode = 'blocked';
|
sourceMode = 'blocked';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// PLAFOND DE COURANT EVCS — calculé APRÈS sourceMode (31/08/2026)
|
||||||
|
// =====================================================
|
||||||
|
// P5 : plafond majoré (input_number.evcs_plafond_courant_edf_on, jusqu'à 28 A
|
||||||
|
// en BOOST) quand le réseau alimente réellement AC-IN ; sinon plafond câblé
|
||||||
|
// prudent, celui que les deux MultiPlus peuvent porter SEULS.
|
||||||
|
//
|
||||||
|
// SÉQUENCE D'OUVERTURE (31/08/2026) — pourquoi ce bloc a été déplacé ici.
|
||||||
|
// Le plafond majoré n'est légitime que tant que le réseau est là. Or c'est
|
||||||
|
// l'hystérésis (cycle 1 min) qui ouvre le contacteur, et le contrôleur EVCS ne
|
||||||
|
// l'apprenait qu'après coup : pendant ce temps les deux onduleurs portaient
|
||||||
|
// seuls la voiture au courant majoré plus la maison — le mécanisme du W-105.
|
||||||
|
//
|
||||||
|
// Correctif sans poignée de main entre les deux automates : le plafond majoré
|
||||||
|
// exige désormais que l'EVCS DEMANDE lui-même le réseau. Sa demande tombe au
|
||||||
|
// cycle où la fenêtre se ferme (30 s), alors que l'hystérésis ne peut ouvrir
|
||||||
|
// qu'ensuite (override disparu ET min_on écoulé, cycle 1 min). L'EVCS baisse
|
||||||
|
// donc AVANT, mécaniquement, sans rien coordonner.
|
||||||
|
// Le calcul doit venir après sourceMode ; rien entre les deux ne l'utilisait.
|
||||||
|
const evcsGridCeilingAllowed =
|
||||||
|
parsed.acInAvailable &&
|
||||||
|
(
|
||||||
|
sourceMode === 'grid_forced' ||
|
||||||
|
sourceMode === 'grid_hc_allowed' ||
|
||||||
|
forceGridRequested
|
||||||
|
);
|
||||||
|
|
||||||
|
const evcsDedicatedCapBaseW = Math.max(0, cfg.evcsDedicatedMaxW - cfg.systemReserveW);
|
||||||
|
const safeEvcsByDedicatedCapW = evcsGridCeilingAllowed
|
||||||
|
? Math.max(0, params.gridCurrentMaxA * acVoltageRef)
|
||||||
|
: evcsDedicatedCapBaseW;
|
||||||
|
const safeEvcsByVebusW = Math.max(0, totalVebusSoftCeilingW - vebusNonEvcsW - cfg.systemReserveW);
|
||||||
|
const safeEvcsCombinedW = Math.max(0, Math.min(safeEvcsByDedicatedCapW, safeEvcsByVebusW));
|
||||||
|
|
||||||
|
const systemCapCurrentA = Math.max(0, Math.floor(safeEvcsCombinedW / acVoltageRef));
|
||||||
|
const physicalCapA = clamp(
|
||||||
|
systemCapCurrentA,
|
||||||
|
0,
|
||||||
|
evcsGridCeilingAllowed
|
||||||
|
? Math.max(params.currentMaxAutoA, params.gridCurrentMaxA)
|
||||||
|
: params.currentMaxAutoA
|
||||||
|
);
|
||||||
|
const canRunAtMinCurrent = physicalCapA >= params.currentMinA;
|
||||||
|
|
||||||
const operatingMode = profile;
|
const operatingMode = profile;
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
@@ -827,6 +852,7 @@ return [{
|
|||||||
systemCapCurrentA,
|
systemCapCurrentA,
|
||||||
physicalCapA,
|
physicalCapA,
|
||||||
canRunAtMinCurrent,
|
canRunAtMinCurrent,
|
||||||
|
evcsGridCeilingAllowed,
|
||||||
gridSettling,
|
gridSettling,
|
||||||
gridTransitionAgeSec: parsed.gridTransitionAgeSec,
|
gridTransitionAgeSec: parsed.gridTransitionAgeSec,
|
||||||
endDayGuardSoc,
|
endDayGuardSoc,
|
||||||
|
|||||||
@@ -1203,4 +1203,107 @@ export const scenarios = [
|
|||||||
takeoverHelperService: "turn_off",
|
takeoverHelperService: "turn_off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// ======= SÉQUENCE D'OUVERTURE DU CONTACTEUR (31/08/2026) =======
|
||||||
|
// Le plafond majoré (28 A en BOOST) n'est légitime que tant que le réseau est
|
||||||
|
// là. C'est l'hystérésis qui ouvre, une minute après que la demande réseau de
|
||||||
|
// l'EVCS a disparu : l'EVCS doit donc baisser AVANT, de lui-même.
|
||||||
|
{
|
||||||
|
id: 'ouverture-plafond-tombe-hors-demande-reseau',
|
||||||
|
label: "Matin solaire, AC-IN encore présent mais l'EVCS ne demande plus le réseau : le plafond retombe à 20 A et la voiture est réduite IMMÉDIATEMENT",
|
||||||
|
time: MORNING,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
evcs_charging_switch: 'on', control_latch: 'on', charge_permission_now: 'on',
|
||||||
|
last_start_at: '2026-08-08 08:00:00',
|
||||||
|
// BOOST EDF : plafond réseau 28 A, voiture effectivement à 28 A.
|
||||||
|
grid_current_max_a: '28', current_max_auto_a: '22',
|
||||||
|
evcs_current_a: '28', evcs_set_current_a: '28', evcs_command_current_a: '28',
|
||||||
|
evcs_ac_power_w: '6440',
|
||||||
|
house_ac_power_w: '8440', vebus_ac_out_l1_power_w: '8440',
|
||||||
|
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '2000',
|
||||||
|
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
|
||||||
|
battery_current_a: '10',
|
||||||
|
// Cooldown de courant NON écoulé : la réduction ne doit pas l'attendre.
|
||||||
|
last_current_change_at: '2026-08-08 08:59:50',
|
||||||
|
acin1_last_changed_ts: String(MORNING_TS - 4000),
|
||||||
|
grid_switch_last_changed_ts: String(MORNING_TS - 4000),
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: "morning_solar",
|
||||||
|
sourceMode: "solar_only",
|
||||||
|
state: "charging_step_down_system_cap",
|
||||||
|
decision: "step_down_fast",
|
||||||
|
desiredCharging: true,
|
||||||
|
currentA: 20,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: "turn_off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ouverture-plafond-maintenu-en-demande-reseau',
|
||||||
|
label: "CONTREFACTUEL : force_grid_now, l'EVCS demande le réseau → le plafond BOOST 28 A est conservé, aucune réduction",
|
||||||
|
time: MORNING,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
evcs_charging_switch: 'on', control_latch: 'on', charge_permission_now: 'on',
|
||||||
|
last_start_at: '2026-08-08 08:00:00',
|
||||||
|
// BOOST EDF : plafond réseau 28 A, voiture effectivement à 28 A.
|
||||||
|
grid_current_max_a: '28', current_max_auto_a: '22',
|
||||||
|
evcs_current_a: '28', evcs_set_current_a: '28', evcs_command_current_a: '28',
|
||||||
|
evcs_ac_power_w: '6440',
|
||||||
|
house_ac_power_w: '8440', vebus_ac_out_l1_power_w: '8440',
|
||||||
|
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '2000',
|
||||||
|
victron_evcs_strategy: 'force_grid_now',
|
||||||
|
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
|
||||||
|
battery_current_a: '10',
|
||||||
|
last_current_change_at: '2026-08-08 08:59:50',
|
||||||
|
acin1_last_changed_ts: String(MORNING_TS - 4000),
|
||||||
|
grid_switch_last_changed_ts: String(MORNING_TS - 4000),
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: "morning_solar",
|
||||||
|
sourceMode: "grid_forced",
|
||||||
|
state: "charging_hold_grid",
|
||||||
|
decision: "hold",
|
||||||
|
desiredCharging: true,
|
||||||
|
currentA: 28,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: "turn_off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ouverture-reseau-disparu-reduction-immediate',
|
||||||
|
label: 'Réseau DISPARU : plafond câblé 20 A, voiture encore à 28 A, cooldown non écoulé → réduction immédiate, sans attendre le compteur de confort',
|
||||||
|
time: MORNING,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
evcs_charging_switch: 'on', control_latch: 'on', charge_permission_now: 'on',
|
||||||
|
last_start_at: '2026-08-08 08:00:00',
|
||||||
|
// BOOST EDF : plafond réseau 28 A, voiture effectivement à 28 A.
|
||||||
|
grid_current_max_a: '28', current_max_auto_a: '22',
|
||||||
|
evcs_current_a: '28', evcs_set_current_a: '28', evcs_command_current_a: '28',
|
||||||
|
evcs_ac_power_w: '6440',
|
||||||
|
house_ac_power_w: '8440', vebus_ac_out_l1_power_w: '8440',
|
||||||
|
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '2000',
|
||||||
|
grid_state: 'off', victron_acin1_available: '0', grid_power_w: '0',
|
||||||
|
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
|
||||||
|
battery_current_a: '10',
|
||||||
|
last_current_change_at: '2026-08-08 08:59:50',
|
||||||
|
acin1_last_changed_ts: String(MORNING_TS - 20),
|
||||||
|
grid_switch_last_changed_ts: String(MORNING_TS - 20),
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: "morning_solar",
|
||||||
|
sourceMode: "solar_only",
|
||||||
|
state: "charging_step_down_system_cap",
|
||||||
|
decision: "step_down_fast",
|
||||||
|
desiredCharging: true,
|
||||||
|
currentA: 20,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: "turn_off",
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user