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*
|
||||
|
||||
## 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({
|
||||
stateName: 'charging_step_down_system_cap',
|
||||
decisionName: 'step_down',
|
||||
currentA: Math.max(
|
||||
params.currentMinA,
|
||||
Math.min(
|
||||
derived.currentReferenceA - Math.max(params.currentStepDownA, 2),
|
||||
derived.physicalCapA
|
||||
)
|
||||
),
|
||||
reasonText: 'Réduction pour respecter la capacité système',
|
||||
decisionName: 'step_down_fast',
|
||||
currentA: Math.max(params.currentMinA, derived.physicalCapA),
|
||||
reasonText: `Réduction immédiate au plafond système ${derived.physicalCapA}A`,
|
||||
dynamic: true,
|
||||
mustStopFlag: false,
|
||||
});
|
||||
|
||||
@@ -117,25 +117,6 @@ const vebusNonEvcsW = Math.max(0, parsed.vebusAcOutPowerW - parsed.evcsAcPowerW)
|
||||
const totalVebusSoftCeilingW = Math.max(6500, params.acLoadsMaxSafeW || 8500);
|
||||
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)
|
||||
// =====================================================
|
||||
@@ -779,6 +760,50 @@ if (dayVehicleFullIdle && profile !== 'night_hc') {
|
||||
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;
|
||||
|
||||
// =====================================================
|
||||
@@ -827,6 +852,7 @@ return [{
|
||||
systemCapCurrentA,
|
||||
physicalCapA,
|
||||
canRunAtMinCurrent,
|
||||
evcsGridCeilingAllowed,
|
||||
gridSettling,
|
||||
gridTransitionAgeSec: parsed.gridTransitionAgeSec,
|
||||
endDayGuardSoc,
|
||||
|
||||
@@ -1203,4 +1203,107 @@ export const scenarios = [
|
||||
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