Files
EVCS/tests/golden/scenarios.mjs
Tallagrand Thierry be1b12969e Vehicule branche a midi : JEEP, VW et moto ZERO (calendrier, capacite et puissance par vehicule)
ZERO ajoutee au selecteur avec son heure de depart (P9). Le bilan lit la
capacite et la puissance de charge mesurees du vehicule selectionne ; 0 =
inconnue (regle muette / pas de borne). Bilan 64/64, golden 123/123.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011uJ47NxgfpGnuuMr3f2n7Z
2026-09-28 14:05:04 +02:00

3200 lines
129 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// R3 — Scénarios golden EVCS. Chaque scénario = instant simulé + overrides du
// snapshot HA réel (tests/golden/fixture-snapshot.json) + résumé attendu.
// Les attendus sont FIGÉS après vérification manuelle : un diff = régression
// (ou évolution volontaire → mettre à jour l'attendu dans le même commit).
//
// Fenêtres (helpers réels) : night_hc 01:24-07:24 | morning 07:30-11:00 |
// midday 12:24-14:24 | evening 17:00-20:30 | sinon outside_window.
// Seuils : hard floor nuit 22 | cible matin forecast fort 35 (tomorrow_kwh 57) |
// soc_start_min 80 | morning_soc_min 35 | midday_soc_min 75.
const NIGHT = '2026-08-08T01:30:00+02:00';
const MORNING = '2026-08-08T09:00:00+02:00';
const MIDDAY = '2026-08-08T13:00:00+02:00';
const AFTERNOON = '2026-08-08T15:30:00+02:00';
const EVENING = '2026-08-08T18:00:00+02:00';
// Fenêtre d'établissement du réseau (31/08/2026) : horodatage de la dernière
// transition d'AC-IN, en secondes epoch. Le harnais fige l'horloge sur `time`,
// donc on exprime l'âge de la transition par rapport à cet instant simulé.
const MORNING_TS = Date.parse(MORNING) / 1000;
const VEHICLE_NIGHT = {
evcs_vehicle_connected: 'on',
evcs_device_connected: 'on',
pv_power_w: '0',
pv_power_10m_avg_w: '0',
pv_trend_w: '0',
house_ac_power_w: '700',
vebus_ac_out_l1_power_w: '700',
battery_current_a: '-14',
};
export const scenarios = [
// ================= NUIT HC (P7 + historique) =================
{
id: 'night-hc-p7-toggle-on',
label: 'Nuit HC, VW branchée, toggle HC nuit ON, SOC 53, EDF OFF → demande réseau (P7)',
time: NIGHT,
overrides: { ...VEHICLE_NIGHT, soc_percent: '53', allow_grid_hc_night: 'on' },
expect: {
profile: 'night_hc',
sourceMode: 'grid_hc_allowed',
state: 'night_wait_grid_takeover',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: true,
edfOnAction: true,
takeoverHelperService: 'turn_on',
},
},
{
id: 'night-hc-p7-edf-on-start',
label: 'Nuit HC, cycle suivant : EDF ON, batterie tenue +2A → démarrage charge HC',
time: NIGHT,
overrides: {
...VEHICLE_NIGHT, soc_percent: '53', allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2', night_takeover_prev: 'on',
},
expect: {
profile: 'night_hc',
sourceMode: 'grid_hc_allowed',
state: 'start_grid_hc_min_current',
decision: 'start',
desiredCharging: true,
currentA: 6,
takeoverNeeded: true,
edfOnAction: false,
takeoverHelperService: 'turn_on',
},
},
{
id: 'night-hc-toggle-off-historique',
label: 'Nuit HC, toggle OFF → comportement historique battery_assist_night (idle)',
time: NIGHT,
overrides: { ...VEHICLE_NIGHT, soc_percent: '53', allow_grid_hc_night: 'off' },
expect: {
profile: 'night_hc',
sourceMode: 'battery_assist_night',
state: 'idle_wait_conditions',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'night-soc-sous-cible',
label: 'Nuit HC, SOC 30 < cible matin 35 → grid_forced',
time: NIGHT,
overrides: { ...VEHICLE_NIGHT, soc_percent: '30', allow_grid_hc_night: 'on' },
expect: {
profile: 'night_hc',
sourceMode: 'grid_forced',
state: 'night_wait_grid_takeover',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: true,
edfOnAction: true,
takeoverHelperService: 'turn_on',
},
},
{
id: 'night-hard-floor',
label: 'Nuit HC, SOC 20 ≤ hard floor 22 → hard stop protection batterie + demande réseau',
time: NIGHT,
overrides: { ...VEHICLE_NIGHT, soc_percent: '20', allow_grid_hc_night: 'on' },
expect: {
profile: 'night_hc',
sourceMode: 'grid_forced',
state: 'battery_current_hard_stop',
decision: 'stop_hard',
desiredCharging: false,
currentA: 6,
takeoverNeeded: true,
edfOnAction: true,
takeoverHelperService: 'turn_on',
},
},
{
id: 'night-sans-vehicule',
label: 'Nuit HC sans véhicule → bloqué, takeover relâché',
time: NIGHT,
overrides: { ...VEHICLE_NIGHT, evcs_vehicle_connected: 'off', soc_percent: '53' },
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
state: 'vehicle_not_connected',
decision: 'stop_not_connected',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
// ===== LE CONSEILLER NE FIXE PLUS LA CIBLE (13/09/2026) =====
// Les trois scenarios du premier cablage (applique / perime / toggle OFF)
// sont remplaces par celui-ci, qui pinne l'ABSENCE de cablage : toutes les
// conditions de l'ancienne regle sont reunies — toggle ON, statut ok,
// horodatage frais, valeur dans les bornes — et la cible doit rester celle
// des regles de prevision. Si quelqu'un recable un jour, ce test tombe.
// Reference : night-soc-sous-cible (SOC 30 < cible regle 35 → grid_forced).
{
id: 'advisor-cible-ignoree',
label: 'Nuit HC, SOC 30, helpers advisor au complet et frais → IGNORES, cible des regles (grid_forced)',
time: NIGHT,
overrides: {
...VEHICLE_NIGHT, soc_percent: '30', allow_grid_hc_night: 'on',
advisor_apply_night_target: 'on',
advisor_night_target_soc: '22',
advisor_status: 'ok',
advisor_last_run: '08/08 01:20',
},
expect: {
profile: 'night_hc',
sourceMode: 'grid_forced',
state: 'night_wait_grid_takeover',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: true,
edfOnAction: true,
takeoverHelperService: 'turn_on',
},
},
{
id: 'p9-depart-vw-grid',
label: 'Matin 08:00, VW sélectionnée, départ 08:50, toggle P9 ON, SOC 45, PV nul → réseau autorisé + EDF demandé',
time: '2026-08-10T08:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '45',
evcs_vehicle: 'VW',
departure_time: '08:50:00',
departure_time_jeep: '06:30:00',
allow_grid_departure_urgent: 'on',
},
expect: {
profile: 'morning_solar',
sourceMode: 'grid_hc_allowed',
state: 'wait_grid_source',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: true,
takeoverHelperService: 'turn_on',
},
},
{
id: 'p9-depart-jeep-passe',
label: 'Matin 08:00, JEEP sélectionnée (départ 06:30 passé), toggle P9 ON → aucune fenêtre, bloqué',
time: '2026-08-10T08:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '45',
evcs_vehicle: 'JEEP',
departure_time: '08:50:00',
departure_time_jeep: '06:30:00',
allow_grid_departure_urgent: 'on',
},
expect: {
profile: 'morning_solar',
sourceMode: 'blocked',
state: 'idle_wait_source',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
// ZERO (moto, 28/09/2026) : elle a SA propre heure de depart. Meme matin que
// p9-depart-vw-grid, VW a 08:50 : la ZERO part a 08:30, donc la fenetre P9
// est ouverte pour elle aussi.
id: 'p9-depart-zero-sa-propre-heure',
label: 'Matin 08:00, ZERO sélectionnée, départ ZERO 08:30, toggle P9 ON → réseau autorisé sur SON heure',
time: '2026-08-10T08:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '45',
evcs_vehicle: 'ZERO',
departure_time: '08:50:00',
departure_time_jeep: '06:30:00',
departure_time_zero: '08:30:00',
allow_grid_departure_urgent: 'on',
},
expect: { profile: 'morning_solar', sourceMode: 'grid_hc_allowed', edfOnAction: true },
},
{
id: 'p9-depart-zero-passe',
label: 'CONTREFACTUEL — ZERO partie a 07:00 : sa fenetre est passee, meme si celle de la VW (08:50) serait ouverte',
time: '2026-08-10T08:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '45',
evcs_vehicle: 'ZERO',
departure_time: '08:50:00',
departure_time_jeep: '06:30:00',
departure_time_zero: '07:00:00',
allow_grid_departure_urgent: 'on',
},
expect: { profile: 'morning_solar', sourceMode: 'blocked', edfOnAction: false },
},
{
id: 'p9-done-idle-collant',
label: 'Matin 08:10, VW pleine détectée (prev=vehicle_full_or_paused) pendant la fenêtre départ → veille collante, réseau relâché',
time: '2026-08-10T08:10:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '45',
evcs_vehicle: 'VW',
departure_time: '08:50:00',
departure_time_jeep: '06:30:00',
allow_grid_departure_urgent: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2',
prev_surplus_state: 'vehicle_full_or_paused',
},
expect: {
profile: 'morning_solar',
sourceMode: 'blocked',
state: 'departure_done_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'p9-toggle-off',
label: 'Matin 08:00, VW départ 08:50 mais toggle P9 OFF → comportement historique (bloqué, pas de réseau)',
time: '2026-08-10T08:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '45',
evcs_vehicle: 'VW',
departure_time: '08:50:00',
departure_time_jeep: '06:30:00',
allow_grid_departure_urgent: 'off',
},
expect: {
profile: 'morning_solar',
sourceMode: 'blocked',
state: 'idle_wait_source',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
// ===== P10 09/08/2026 : mémoire "véhicule plein" en journée =====
// Incident 09/08 09:49 : VW pleine, le surplus solaire retentait une charge
// toutes les ~15 min (statut "Charged", 0 W) → usure relais. Désormais
// collant jusqu'au débranchement, avec héritage des états done de la nuit.
// ===== Fix H (07/09/2026) : credibilite du statut "charged" =====
// Releve reel : la borne a annonce "charged" a 14:28:18 apres 2 min 42 s de
// charge et 0,10 kWh livre, ce qui a bloque le vehicule deux heures. Les deux
// coupures (vraie fin de charge et interruption) sont ELECTRIQUEMENT
// identiques, mesurees a la seconde : seule la duree de session les separe.
{
id: 'charged-interrompu-ne-latche-pas',
label: 'Statut 3 apres 2 min 42 s et 0,10 kWh livre (releve 07/09) -> interruption, PAS de collant',
time: AFTERNOON,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '96', pv_power_w: '2600', pv_power_10m_avg_w: '2500',
pv_trend_w: '0', battery_current_a: '2',
house_ac_power_w: '2100', vebus_ac_out_l1_power_w: '2100',
evcs_charging_switch: 'on',
charge_permission_now: 'on',
control_latch: 'on',
evcs_current_a: '0',
evcs_ac_power_w: '0',
evcs_status_raw: '3',
evcs_charging_time: '2m 42s',
evcs_session_energy_kwh: '0.1',
prev_surplus_state: 'charging_hold',
},
expect: {
profile: 'outside_window',
sourceMode: 'solar_only',
state: 'charging_step_up_battery',
decision: 'step_up',
desiredCharging: true,
currentA: 7,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'charged-session-etablie-latche',
label: 'CONTREFACTUEL : meme statut 3 apres 1 h 32 et 4,30 kWh (releve VW 06/09) -> vraie fin, collant',
time: AFTERNOON,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '96', pv_power_w: '2600', pv_power_10m_avg_w: '2500',
pv_trend_w: '0', battery_current_a: '2',
house_ac_power_w: '2100', vebus_ac_out_l1_power_w: '2100',
evcs_charging_switch: 'on',
charge_permission_now: 'on',
control_latch: 'on',
evcs_current_a: '0',
evcs_ac_power_w: '0',
evcs_status_raw: '3',
evcs_charging_time: '1h 32m 0s',
evcs_session_energy_kwh: '4.3',
prev_surplus_state: 'charging_hold',
},
expect: {
profile: 'outside_window',
sourceMode: 'blocked',
state: 'day_vehicle_full_idle',
decision: 'stop_vehicle_full_or_paused',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'charged-energie-nulle-latche',
label: 'CONTREFACTUEL : statut 3 avec ZERO kWh livre (vehicule plein a l arrivee, cas P10) -> collant',
time: AFTERNOON,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '96', pv_power_w: '2600', pv_power_10m_avg_w: '2500',
pv_trend_w: '0', battery_current_a: '2',
house_ac_power_w: '2100', vebus_ac_out_l1_power_w: '2100',
evcs_charging_switch: 'on',
charge_permission_now: 'on',
control_latch: 'on',
evcs_current_a: '0',
evcs_ac_power_w: '0',
evcs_status_raw: '3',
evcs_charging_time: '3m 20s',
evcs_session_energy_kwh: '0',
prev_surplus_state: 'charging_hold',
},
expect: {
profile: 'outside_window',
sourceMode: 'blocked',
state: 'day_vehicle_full_idle',
decision: 'stop_vehicle_full_or_paused',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'night-charged-interrompu-ne-latche-pas',
label: 'Nuit HC : statut 3 apres 3 min et 0,10 kWh -> interruption, la charge HC continue',
time: NIGHT,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
night_session_active: 'on',
allow_grid_hc_night: 'on',
soc_percent: '55', pv_power_w: '0', pv_power_10m_avg_w: '0',
pv_trend_w: '0', battery_current_a: '0',
house_ac_power_w: '800', vebus_ac_out_l1_power_w: '800',
evcs_charging_switch: 'on',
charge_permission_now: 'on',
control_latch: 'on',
grid_switch: 'on',
acin1_available: '1',
evcs_current_a: '0',
evcs_ac_power_w: '0',
evcs_status_raw: '3',
evcs_charging_time: '3m 0s',
evcs_session_energy_kwh: '0.1',
prev_surplus_state: 'charging_step_up_grid_hc',
},
expect: {
profile: 'night_hc',
sourceMode: 'grid_hc_allowed',
state: 'night_wait_grid_takeover_runtime',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: true,
edfOnAction: true,
takeoverHelperService: 'turn_on',
},
},
{
id: 'day-vw-pleine-statut3',
label: 'Matin PV fort, session armée 0 W, statut 3 → day_vehicle_full_idle collant (stop propre, plus de solaire)',
time: MORNING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
pv_trend_w: '100', battery_current_a: '10',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_charging_switch: 'on',
charge_permission_now: 'on',
control_latch: 'on',
evcs_current_a: '0',
evcs_ac_power_w: '0',
evcs_status_raw: '3',
prev_surplus_state: 'charging_hold',
},
expect: {
profile: 'morning_solar',
sourceMode: 'blocked',
state: 'day_vehicle_full_idle',
decision: 'stop_vehicle_full_or_paused',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'day-full-heritage-nuit',
label: 'Matin PV fort, prev=night_recovery_done_idle (nuit finie pleine) → AUCUNE re-tentative solaire',
time: MORNING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
pv_trend_w: '100', battery_current_a: '10',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_status_raw: '1',
prev_surplus_state: 'night_recovery_done_idle',
},
expect: {
profile: 'morning_solar',
sourceMode: 'blocked',
state: 'day_vehicle_full_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'day-full-echappatoire-force-charge',
label: 'Matin PV fort, mémoire plein active MAIS force_charge_now → la charge repart (échappatoire utilisateur)',
time: MORNING,
overrides: {
victron_evcs_strategy: 'force_charge_now',
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
pv_trend_w: '100', battery_current_a: '10',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_status_raw: '1',
prev_surplus_state: 'day_vehicle_full_idle',
},
expect: {
profile: 'morning_solar',
sourceMode: 'solar_only',
state: 'start_manual_local_min_current',
decision: 'start',
desiredCharging: true,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
// ===== P11 12/08/2026 : reset du collant au changement de véhicule =====
// Incident : JEEP chargée (night_recovery_done_idle) → débranchée → VW
// branchée à sa place → l'état collant survivait et bloquait la VW neuve.
// Signal : charging_time repart de 0 (< 90 s) = session neuve → on neutralise.
{
id: 'p11-changement-vehicule-nuit',
label: 'Nuit HC, prev=night_recovery_done_idle (JEEP finie) MAIS charging_time 15 s (VW fraîche) → collant neutralisé, retest',
time: NIGHT,
overrides: {
...VEHICLE_NIGHT, soc_percent: '54', allow_grid_hc_night: 'on',
prev_surplus_state: 'night_recovery_done_idle',
evcs_charging_time: '15s',
},
expect: {
profile: 'night_hc',
sourceMode: 'grid_hc_allowed',
state: 'night_wait_grid_takeover',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: true,
edfOnAction: true,
takeoverHelperService: 'turn_on',
},
},
{
id: 'p11-changement-vehicule-jour',
label: 'Jour PV fort, prev=day_vehicle_full_idle MAIS charging_time 20 s (nouveau véhicule) → retest solaire',
time: MORNING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
pv_trend_w: '100', battery_current_a: '10',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_status_raw: '1',
prev_surplus_state: 'day_vehicle_full_idle',
evcs_charging_time: '20s',
},
expect: {
profile: 'morning_solar',
sourceMode: 'solar_only',
state: 'start_min_current',
decision: 'start',
desiredCharging: true,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'p11-session-etablie-reste-collant',
label: 'Jour PV fort, prev=day_vehicle_full_idle, charging_time 2h (session établie) → reste collant (pas de reset abusif)',
time: MORNING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
pv_trend_w: '100', battery_current_a: '10',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_status_raw: '1',
prev_surplus_state: 'day_vehicle_full_idle',
evcs_charging_time: '2h 05m 12s',
},
expect: {
profile: 'morning_solar',
sourceMode: 'blocked',
state: 'day_vehicle_full_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
// ============ P11-bis : SWAP RAPIDE, BORNE FIGEE (19/09/2026) ============
// Incident du 19/09 : VW debranchee ~14:50, JEEP branchee, mais le swap n'a
// dure que 14 s → la borne a GELE son etat (charging_time reste a 1h25, status
// fige). P11 (base sur charging_time <= 90s) restait aveugle → le collant VW
// bloquait la JEEP. Le last_changed du capteur de connexion, lui, a bouge :
// c'est le second signal de fraicheur.
{
id: 'p11bis-swap-rapide-borne-figee',
label: 'Jour PV fort, prev=day_vehicle_full_idle, charging_time FIGE 1h25 MAIS reconnexion il y a 30 s → retest (second signal)',
time: MORNING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
pv_trend_w: '100', battery_current_a: '10',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_status_raw: '1',
prev_surplus_state: 'day_vehicle_full_idle',
// la borne n'a PAS reinitialise : charging_time fige haut
evcs_charging_time: '1h 25m',
// mais la connexion a change il y a 30 s (le swap rapide)
evcs_vehicle_connected_ts: String(MORNING_TS - 30),
},
expect: {
profile: 'morning_solar',
sourceMode: 'solar_only',
state: 'start_min_current',
decision: 'start',
desiredCharging: true,
currentA: 6,
},
},
{
id: 'p11bis-session-etablie-reconnexion-vieille',
label: 'CONTREFACTUEL : charging_time 1h25 ET reconnexion il y a 2h → PAS frais → reste collant (pas de reset abusif)',
time: MORNING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
pv_trend_w: '100', battery_current_a: '10',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_status_raw: '1',
prev_surplus_state: 'day_vehicle_full_idle',
evcs_charging_time: '1h 25m',
evcs_vehicle_connected_ts: String(MORNING_TS - 7200),
},
expect: {
profile: 'morning_solar',
sourceMode: 'blocked',
state: 'day_vehicle_full_idle',
decision: 'hold',
desiredCharging: false,
},
},
{
id: 'reset-borne-force-retest',
label: 'Bouton reset borne ON : session pourtant etablie (charging_time 1h25 + reconnexion vieille) → retest force + ack off',
time: MORNING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
pv_trend_w: '100', battery_current_a: '10',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_status_raw: '1',
prev_surplus_state: 'day_vehicle_full_idle',
evcs_charging_time: '1h 25m',
evcs_vehicle_connected_ts: String(MORNING_TS - 7200),
reset_borne: 'on',
},
expect: {
state: 'start_min_current',
decision: 'start',
desiredCharging: true,
resetBorneAck: 'turn_off',
},
},
// ================= FIN DE CHARGE NUIT (P8) =================
{
id: 'night-vw-pleine-transition',
label: 'Nuit HC 04:00, cycle après vehicle_full_or_paused, SOC ≥ cible → recovery done, takeover relâché',
time: '2026-08-08T04:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '60', allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2', night_takeover_prev: 'on',
evcs_charging_switch: 'off',
prev_surplus_state: 'vehicle_full_or_paused',
},
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
state: 'night_recovery_done_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'night-vw-pleine-autoentretien',
label: 'Nuit HC 05:00, veille auto-entretenue SOC ≥ cible, EDF déjà coupé → reste en recovery done',
time: '2026-08-08T05:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '55', allow_grid_hc_night: 'on',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_session_done_idle',
},
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
state: 'night_recovery_done_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'night-vw-pleine-batterie-basse-edf-maintenu',
label: 'Nuit HC 05:00, VW pleine MAIS SOC 30 ≤ cible 35 → takeover maintenu (recharge batterie), EVCS en veille',
time: '2026-08-08T05:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '30', allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2', night_takeover_prev: 'on',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_session_done_idle',
},
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
state: 'night_session_done_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: true,
edfOnAction: false,
takeoverHelperService: 'turn_on',
},
},
// ===== FIX A/D 08/08/2026 : anti-rebond EDF post-charge (incident nuit 07-08/08) =====
{
id: 'night-recovery-atteinte-soc-cible',
label: 'Nuit HC 04:15, VW pleine, SOC 35 == cible (le cas du rebond) → recovery done, takeover coupé une fois pour toutes',
time: '2026-08-08T04:15:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '35', allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2', night_takeover_prev: 'on',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_session_done_idle',
},
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
state: 'night_recovery_done_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'night-recovery-done-bande-collante',
label: 'Nuit HC 04:45, recovery done, SOC 34 dans la bande (cible−2 < 34 < 35) → PAS de ré-engagement (anti-rebond)',
time: '2026-08-08T04:45:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '34', allow_grid_hc_night: 'on',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_recovery_done_idle',
},
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
state: 'night_recovery_done_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'night-recovery-done-pas-de-reengagement',
label: 'Nuit HC 05:30, recovery done et SOC 32 sous la cible → AUCUN ré-engagement (la recovery est une passe unique, 27/08)',
time: '2026-08-08T05:30:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '32', allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2', night_takeover_prev: 'on',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_recovery_done_idle',
},
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
// Collant désormais : la recovery ayant eu lieu, l'état reste
// 'night_recovery_done_idle' même sous la cible — c'est lui qui autorise
// la coupure EDF immédiate côté hystérésis (E2) dès SOC >= grid_soc_off.
state: 'night_recovery_done_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
// L'oscillateur mesuré : nuit du 26-27/08, Jeep pleine restée branchée,
// cible 35. L'ancienne bande de 2 points ré-engageait le takeover pile à
// SOC 33 -> EDF ON 04:24/05:31/07:05, OFF 05:16/06:50/07:30, 2,5 kWh
// importés une nuit où le lendemain était annoncé à 43 kWh.
id: 'night-oscillateur-nuit-2627',
label: 'Nuit HC 05:31, recovery done et SOC 33 (cible 35 − 2) → le takeover NE se ré-engage pas : plus d\'oscillateur',
time: '2026-08-27T05:31:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '33', allow_grid_hc_night: 'on',
grid_state: 'off', victron_acin1_available: '0',
battery_current_a: '-11', night_takeover_prev: 'off',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_recovery_done_idle',
},
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
state: 'night_recovery_done_idle',
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
// Le filet de sécurité doit rester intact : sous le plancher dur, le
// takeover se ré-engage même si la recovery a déjà eu lieu, parce que
// batteryBelowHardFloor est un terme INDÉPENDANT de nightGridTakeoverNeeded.
id: 'night-recovery-done-plancher-dur',
label: 'Nuit HC 05:31, recovery done mais SOC 21 sous le plancher dur 22 → ré-engagement du takeover (sécurité)',
time: '2026-08-27T05:31:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '21', allow_grid_hc_night: 'on',
grid_state: 'off', victron_acin1_available: '0',
battery_current_a: '-11', night_takeover_prev: 'off',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_recovery_done_idle',
},
expect: {
profile: 'night_hc',
takeoverNeeded: true,
takeoverHelperService: 'turn_on',
},
},
{
id: 'night-vw-statut-charged',
label: 'Nuit HC 03:00, statut EVCS brut 3 (charged), sans état VDF préalable → fin de charge immédiate (Fix D)',
time: '2026-08-08T03:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '60', allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2', night_takeover_prev: 'on',
evcs_charging_switch: 'off',
evcs_status_raw: '3',
prev_surplus_state: '',
},
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
state: 'night_recovery_done_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
// ===== FIX E 09/08/2026 : latch "charge terminée" PENDANT la session active =====
// Incident nuit 08-09/08 : status 3 en session armée → source-lost zombie 25 min,
// état collant jamais écrit, statut retombe à l'ouverture du relais → rebond EDF.
{
id: 'night-charged-en-session-active',
label: 'Nuit HC 04:30, session ARMÉE (relais fermé, 0 W), statut 3 → arrêt propre + latch collant immédiat',
time: '2026-08-09T04:30:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '51', allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2', night_takeover_prev: 'on',
evcs_charging_switch: 'on',
charge_permission_now: 'on',
control_latch: 'on',
evcs_current_a: '0',
evcs_ac_power_w: '0',
evcs_status_raw: '3',
prev_surplus_state: 'charging_hold',
},
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
state: 'night_recovery_done_idle',
decision: 'stop_vehicle_full_or_paused',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'night-flap-statut-apres-latch',
label: 'Nuit HC 05:00, relais ouvert, statut retombé à 1, prev=night_recovery_done_idle → collant, AUCUN re-P7/EDF',
time: '2026-08-09T05:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '51', allow_grid_hc_night: 'on',
evcs_charging_switch: 'off',
evcs_status_raw: '1',
prev_surplus_state: 'night_recovery_done_idle',
},
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
state: 'night_recovery_done_idle',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'night-hard-stop-necrase-pas-done',
label: 'Nuit HC 06:55, VW pleine relance une micro-charge sur batterie (−60 A) → le hard stop N\'ÉCRASE PAS le collant (Fix F)',
time: '2026-08-09T06:55:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '40', allow_grid_hc_night: 'on',
battery_current_a: '-60',
evcs_charging_switch: 'on',
charge_permission_now: 'on',
control_latch: 'on',
evcs_current_a: '6',
evcs_ac_power_w: '1400',
evcs_status_raw: '2',
prev_surplus_state: 'night_recovery_done_idle',
},
expect: {
profile: 'night_hc',
sourceMode: 'blocked',
state: 'night_recovery_done_idle',
decision: 'stop_vehicle_full_or_paused',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
// ================= JOUR SOLAIRE =================
{
id: 'morning-solaire-fort',
label: 'Matin, PV 4,5 kW, SOC 60, batterie +10A → charge solaire',
time: MORNING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
pv_trend_w: '100', battery_current_a: '10',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
},
expect: {
profile: 'morning_solar',
sourceMode: 'solar_only',
state: 'start_min_current',
decision: 'start',
desiredCharging: true,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'morning-nuageux',
label: 'Matin nuageux, PV 300 W, SOC 40 → pas de démarrage',
time: MORNING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '40', pv_power_w: '300', pv_power_10m_avg_w: '250',
pv_trend_w: '-50', battery_current_a: '-3',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
},
expect: {
profile: 'morning_solar',
sourceMode: 'blocked',
state: 'idle_wait_source',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'midday-abondance-p6',
label: 'Midi abondance P6 : PV avg 4,5 kW + batterie +25A, SOC 66 < plancher 75 → démarre quand même',
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '66', pv_power_w: '5000', pv_power_10m_avg_w: '4500',
pv_trend_w: '200', battery_current_a: '25',
house_ac_power_w: '1200', vebus_ac_out_l1_power_w: '1200',
},
expect: {
profile: 'midday_hc_or_solar',
sourceMode: 'solar_only',
state: 'start_min_current',
decision: 'start',
desiredCharging: true,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'midday-inversion-start-hold',
label: 'INVARIANT maintien<=demarrage : midi, SOC 58 dans la bande [40,60], charge 18 A en cours sous abondance → doit CONTINUER (incident 24/08 12:24-12:28)',
time: MIDDAY,
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 11:30:00', // min_run largement écoulé
// Relevé VRM du 24/08 12:25 : PV 10,24 kW, charges 3815 W (dont EVCS 3502 W),
// batterie +127 A. SOC 58 % : au-dessus du plancher de démarrage sous
// abondance (40 %) mais sous le plancher de maintien de la règle midi
// (max(29, 75-15) = 60 %). Avant correctif : arrêt, puis redémarrage au
// cycle suivant → cycle limite de 3 min et PV écrêtée.
soc_percent: '58', pv_power_w: '10240', pv_power_10m_avg_w: '9800',
pv_trend_w: '200', battery_current_a: '127',
evcs_current_a: '18', evcs_set_current_a: '18', evcs_command_current_a: '18',
evcs_ac_power_w: '3502',
house_ac_power_w: '3815', vebus_ac_out_l1_power_w: '3815',
},
expect: {
profile: 'midday_hc_or_solar',
sourceMode: 'solar_only',
state: 'charging_step_up_battery',
decision: 'step_up',
desiredCharging: true,
currentA: 19,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
// ======== REPLI SANS BILAN : LA BORNE N'EST PLUS EMPRUNTEE (18/09/2026) ========
// Quand la garde du bilan tombe, la consigne revenait a `maison + plafond de
// borne` meme sans voiture -- le defaut d'origine, revenu au moment ou le
// systeme est deja degrade. Ce scenario fige le comportement corrige, et rien
// d'autre dans la suite ne le couvre : sans lui, remettre le plafond brut
// dans la branche de repli laisse les 105 autres scenarios verts.
// ⚠️ L'attendu est calcule sur la FIXTURE, jamais sur une mesure de
// production : maison 900 W + part voiture nulle, plus la marge d'un pas de
// courant. Le rejouer sur l'installation vivante donnerait autre chose sans
// que rien ne soit casse.
{
id: 'consigne-repli-sans-vehicule-n-emprunte-plus-la-borne',
label: "REPLI 18/09 — midi, fenetre HC autorisee, AUCUN vehicule, bilan indisponible : la consigne couvre la maison au lieu d'emprunter le plafond de la borne",
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'off', evcs_device_connected: 'on',
soc_percent: '50', pv_power_w: '100', pv_power_10m_avg_w: '100',
pv_trend_w: '0', battery_current_a: '-4',
grid_state: 'on', victron_acin1_available: '1',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_ac_power_w: '0', evcs_current_a: '0',
grid_setpoint_w: '700',
// Aucun champ `balance_*` : la garde tombe, on est dans la branche de repli.
},
expect: {
profile: 'midday_hc_or_solar',
sourceMode: 'grid_hc_allowed',
gridSetpointW: 1130,
},
},
{
id: 'midday-hc-garde-soc',
label: 'Midi sans PV, SOC 50 > 35, HC midi autorisé + EDF ON → grid proposé mais gardé (soc>35)',
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '50', pv_power_w: '100', pv_power_10m_avg_w: '100',
pv_trend_w: '0', battery_current_a: '-4',
grid_state: 'on', victron_acin1_available: '1',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
},
expect: {
profile: 'midday_hc_or_solar',
sourceMode: 'grid_hc_allowed',
state: 'idle_wait_conditions',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'apresmidi-provocation-p3',
// 03/09/2026 — echantillon rendu COHERENT EN ENERGIE. Il portait PV 300 W,
// maison 600 W et batterie +2 A : un trou de 300 W que rien n'alimente,
// etat physiquement inatteignable reseau ouvert. Il passait quand meme,
// parce que le demarrage ne regardait que la production. Corrige en gardant
// l'intention d'origine — production mesuree SOUS pv_hold_min (500) et sous
// pv_start_min (700), donc seul solar_potential peut demarrer — avec une
// maison qui, elle, ferme le bilan : c'est la signature de la censure
// (batterie pleine, MPPT cale sur la maison, excedent ~0).
label: 'Après-midi hors fenêtre, SOC 95, PV bridé 430 W (< hold 500) mais maison 400 W → censure, excédent +30 W → provocation PV (solar_potential)',
time: AFTERNOON,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
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',
sourceMode: 'solar_potential',
state: 'start_forecast_solar_potential',
decision: 'start',
desiredCharging: true,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
// ===== 03/09/2026 : un demarrage se decide sur l'EXCEDENT, pas sur la PV =====
// Les deux premiers scenarios sont des releves REELS du 03/09, pris a 54 min
// d'intervalle. Leurs lectures de production ne different que de 11 W
// (769 W puis 758 W) : pour l'ancien code, deux fois "758 >= 700, il y a du
// soleil". Ce qui les separe est la MAISON — 755 W puis 1202 W — donc
// l'excedent : +14 W (production refusee, batterie pleine, rien a absorber)
// puis -444 W (la batterie porte deja la maison, la nuit tombe).
// La paire est le coeur du correctif : sans elle, aucune preuve que la
// nouvelle grandeur discrimine ce que l'ancienne confondait.
{
id: 'excedent-censure-demarre',
label: "Relevé du 03/09 18:50 (pv_censored=ON) : PV 769 W, maison 755 W, batterie 0 A, SOC 94 → excédent +14 W = ressource refusée → la voiture DOIT démarrer",
time: '2026-08-08T18:50:00+02:00',
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '94', battery_current_a: '0',
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',
sourceMode: 'solar_only',
state: 'start_min_current',
decision: 'start',
desiredCharging: true,
currentA: 6,
},
},
{
id: 'excedent-deficit-ne-demarre-pas',
label: "Relevé du 03/09 19:44 (l'incident) : PV 758 W mais maison 1202 W, batterie déjà à -12 A, SOC 94 → excédent -444 W → AUCUN démarrage, ni chemin solaire ni assistance batterie",
time: '2026-08-08T19:44:30+02:00',
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '94', battery_current_a: '-12.2',
pv_power_w: '758.3', pv_power_10m_avg_w: '790', pv_trend_w: '0',
house_ac_power_w: '1202', vebus_ac_out_l1_power_w: '1202',
evcs_ac_power_w: '0',
},
expect: {
profile: 'evening_high_soc',
sourceMode: 'solar_only',
state: 'idle_wait_conditions',
decision: 'hold',
desiredCharging: false,
},
},
{
// Exerce le chemin "soleil reel" (rawSolarStartOk), que la paire ci-dessus
// ne touche pas : elle passe par l'assistance batterie. Batterie a 60 %,
// donc pas de censure possible — la production mesuree EST la ressource, et
// elle est deja prise par la maison. L'ancien code demarrait (3000 >= 700).
id: 'excedent-grosse-production-grosse-maison',
label: "Matin, PV 3000 W mais maison 2800 W et SOC 60 (pas de censure possible) → excédent 200 W < seuil de démarrage → pas de charge malgré une forte production",
time: MORNING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '60', battery_current_a: '-4',
pv_power_w: '3000', pv_power_10m_avg_w: '3000', pv_trend_w: '0',
house_ac_power_w: '2800', vebus_ac_out_l1_power_w: '2800',
evcs_ac_power_w: '0',
},
expect: {
profile: 'morning_solar',
sourceMode: 'solar_only',
state: 'idle_wait_conditions',
decision: 'hold',
desiredCharging: false,
},
},
// ===== 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,
},
},
// ===== 03/09/2026 : la cible du matin est une energie, pas un palier =====
// Releve de la nuit 02-03/09 a 03:00 : le bilan demandait 50,4 % pour tenir
// jusqu'au croisement de 9 h, la regle des paliers disait 35 %, le SOC etait
// a 45 %. EDF a ete relache 5 points sous la ligne, la batterie s'est videe
// a 20 % vers 06:30, et le reseau est revenu de 07:24 a 10:33 AU PLEIN TARIF.
{
id: 'cible-bilan-tient-le-reseau',
label: "Nuit HC 03:00 (relevé du 03/09), VW pleine, SOC 45 : le bilan demande 50,4 % → cible relevée → le réseau est MAINTENU au lieu d'être relâché",
time: '2026-08-08T03:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2', night_takeover_prev: 'on',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_session_done_idle',
soc_percent: '45',
balance_required_soc: '50.4', balance_energy_to_cover_kwh: '4.3',
balance_status: 'ok', balance_last_run: '08/08 02:58',
},
expect: {
profile: 'night_hc',
state: 'night_session_done_idle',
takeoverNeeded: true,
takeoverHelperService: 'turn_on',
},
},
{
// CONTREFACTUEL : exactement la nuit du 03/09. Bilan indisponible => regle
// des paliers (35 %) => SOC 45 >= 35 => relache. C'est ce qui s'est passe.
id: 'cible-bilan-perime-relache-comme-avant',
label: "Même relevé, bilan vieux de 2 h → repli sur le palier 35 % → EDF relâché (le comportement qui a coûté 1,49 kWh au plein tarif)",
time: '2026-08-08T03:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2', night_takeover_prev: 'on',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_session_done_idle',
soc_percent: '45',
balance_required_soc: '50.4', balance_energy_to_cover_kwh: '4.3',
balance_status: 'ok', balance_last_run: '08/08 01:00',
},
expect: {
profile: 'night_hc',
state: 'night_recovery_done_idle',
takeoverNeeded: false,
takeoverHelperService: 'turn_off',
},
},
{
// SENS UNIQUE : le bilan ne doit jamais ABAISSER la cible.
id: 'cible-bilan-ne-baisse-pas-la-cible',
label: "Nuit HC, SOC 30, le bilan ne demande que 25 % → le palier 35 % reste maître → le réseau est maintenu",
time: '2026-08-08T03:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2', night_takeover_prev: 'on',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_session_done_idle',
soc_percent: '30',
balance_required_soc: '25', balance_energy_to_cover_kwh: '0.5',
balance_status: 'ok', balance_last_run: '08/08 02:58',
},
expect: {
profile: 'night_hc',
state: 'night_session_done_idle',
takeoverNeeded: true,
takeoverHelperService: 'turn_on',
},
},
{
// PLAFOND 90 % : le bilan publie sans ecretage des valeurs > 100 (c'est
// voulu, §26 : signal de dimensionnement). Sans plafond ici, la cible
// deviendrait inatteignable et EDF resterait allume toute la nuit a
// charger la batterie — en evincant au passage la production du matin.
id: 'cible-bilan-plafonnee-a-90',
label: "Nuit HC, SOC 92, le bilan demande 118,9 % (stockage insuffisant) → cible plafonnée à 90 % → recovery terminée, EDF relâché",
time: '2026-08-08T03:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2', night_takeover_prev: 'on',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_session_done_idle',
soc_percent: '92',
balance_required_soc: '118.9', balance_energy_to_cover_kwh: '14.54',
balance_status: 'ok', balance_last_run: '08/08 02:58',
},
expect: {
profile: 'night_hc',
state: 'night_recovery_done_idle',
takeoverNeeded: false,
takeoverHelperService: 'turn_off',
},
},
// ===== 04/09/2026 : LA SESSION TENAIT LE CONTACTEUR, PAS L'ENERGIE =====
// Releve de la nuit du 03-04/09. La session unique a fonctionne — une seule
// bascule du contacteur, EDF ferme jusqu'a 07:24. Mais a 02:50, la voiture
// finie, le sourceMode est passe a 'blocked' et la consigne reseau est
// retombee de 5078 W a 50 W. Contacteur ferme, heures creuses, et la maison
// a tire 770 W SUR LA BATTERIE pendant trois heures et demie :
// SOC 44 % -> 17 %, 2,60 kWh pris a la batterie quand le reseau etait la.
{
id: 'consigne-session-engagee-couvre-la-maison',
label: "Nuit 03:30, session engagée mais voiture finie (source bloquée) → la consigne DOIT couvrir la maison au lieu de retomber au repos",
time: '2026-08-08T03:30:00+02:00',
overrides: {
...VEHICLE_NIGHT, allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '-16', night_takeover_prev: 'on',
evcs_charging_switch: 'off', evcs_ac_power_w: '0',
house_ac_power_w: '770', vebus_ac_out_l1_power_w: '770',
prev_surplus_state: 'night_session_done_idle',
night_grid_session_committed_prev: 'on',
soc_percent: '40',
balance_required_soc: '57.2', balance_energy_to_cover_kwh: '7.58',
balance_status: 'ok', balance_last_run: '08/08 03:28',
},
expect: {
profile: 'night_hc',
takeoverNeeded: true,
// 770 W de maison + le plafond de courant en marge. Ce qui compte n'est
// pas la valeur exacte mais qu'on ne retombe PAS au repos.
gridSetpointW: 5595,
},
},
{
// CONTREFACTUEL. La difference est PHYSIQUE et non un drapeau force : le
// SOC est au-dessus de la ligne du bilan, donc la nuit est couverte, donc
// aucune session ne s'engage — et la consigne doit bien retomber au repos.
// Sans ce scenario, le correctif pourrait fuir hors session.
//
// ⚠️ Premiere redaction fausse : elle portait DEUX fois `balance_status`
// dans le meme objet. En JavaScript la derniere gagne, donc le scenario
// testait l'inverse de ce que son libelle annoncait — et il l'a signale en
// renvoyant 5595. Ne jamais construire un contrefactuel en surchargeant une
// clef deja presente.
// ⚠️ ATTENDU MIS A JOUR LE 06/09/2026 : la consigne de repos est passee de
// 200 a 700 W. Elle ne s'applique que contacteur FERME sans session reseau,
// et a 200 W une plaque qui cycle faisait passer le point de fonctionnement
// sous zero (mesure du 06/09, fenetre 12:07-12:22 : 9 % d'echantillons
// negatifs, minimum -477 W, mediane des negatifs -146 W). Les deux seules
// raisons de fermer le contacteur sans session EVCS sont « batterie au
// plancher » et « la maison demande plus que le parc ne sait donner » :
// dans les deux cas on veut que le reseau contribue plus que 200 W.
id: 'consigne-sans-engagement-retombe-au-repos',
label: "Même relevé mais SOC 70 au-dessus de la ligne : aucune session engagée → la consigne retombe à la valeur de repos",
time: '2026-08-08T03:30:00+02:00',
overrides: {
...VEHICLE_NIGHT, allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '-16', night_takeover_prev: 'off',
evcs_charging_switch: 'off', evcs_ac_power_w: '0',
house_ac_power_w: '770', vebus_ac_out_l1_power_w: '770',
prev_surplus_state: 'night_recovery_done_idle',
night_grid_session_committed_prev: 'off',
soc_percent: '70',
balance_required_soc: '57.2', balance_energy_to_cover_kwh: '7.58',
balance_status: 'ok', balance_last_run: '08/08 03:28',
},
expect: {
profile: 'night_hc',
takeoverNeeded: false,
gridSetpointW: 700,
},
},
// ===== 03/09/2026 : SESSION RESEAU UNIQUE DE LA NUIT =====
// Engagement sur le bilan, relachement a la FIN DE LA FENETRE — jamais sur
// un seuil de SOC. Deux raisons, toutes deux mesurees le 03/09 : un seuil de
// relachement que le SOC repasse ensuite rouvre l'oscillateur du 27/08, et
// il porterait de toute facon sur une grandeur fausse (la conversion
// kWh -> points du bilan est lineaire a 15 kWh alors que la jauge vaut
// ~17 kWh au-dessus de 42 % et 8 a 12 kWh en dessous).
//
// Le premier scenario etait, quelques heures plus tot, epingle comme LIMITE
// CONNUE : le reseau restait relache alors que le SOC etait repasse sous la
// ligne. C'est exactement ce que ce correctif change.
{
id: 'session-unique-tient-sous-la-ligne',
label: "Nuit HC 05:30, recovery déjà latchée, SOC 30 sous le requis 37,1 % → la session réseau s'ENGAGE et tient",
time: '2026-08-08T05:30:00+02:00',
overrides: {
...VEHICLE_NIGHT, allow_grid_hc_night: 'on',
grid_state: 'off', victron_acin1_available: '0',
battery_current_a: '-14', night_takeover_prev: 'off',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_recovery_done_idle',
soc_percent: '30',
balance_required_soc: '37.1', balance_energy_to_cover_kwh: '2.3',
balance_status: 'ok', balance_last_run: '08/08 05:28',
},
expect: {
profile: 'night_hc',
takeoverNeeded: true,
takeoverHelperService: 'turn_on',
sessionCommittedService: 'turn_on',
},
},
{
// LA PREUVE ANTI-OSCILLATEUR. Une fois engagee, la session ne se relache
// PAS parce que le SOC est repasse au-dessus de la ligne. Sans ce
// scenario, un relachement sur seuil passerait les tests et recreerait le
// cycle de 31 min corrige le 27/08.
id: 'session-unique-ne-relache-pas-sur-le-soc',
label: "Session déjà engagée, SOC 45 repassé AU-DESSUS du requis 37,1 % → le réseau reste tenu, et rien n'est réécrit",
time: '2026-08-08T05:30:00+02:00',
overrides: {
...VEHICLE_NIGHT, allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2', night_takeover_prev: 'on',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_recovery_done_idle',
night_grid_session_committed_prev: 'on',
soc_percent: '45',
balance_required_soc: '37.1', balance_energy_to_cover_kwh: '2.3',
balance_status: 'ok', balance_last_run: '08/08 05:28',
},
expect: {
profile: 'night_hc',
takeoverNeeded: true,
takeoverHelperService: 'turn_on',
sessionCommittedService: null,
},
},
{
// LA SORTIE, et la seule : la fenetre se ferme. Condition monotone, donc
// aucune oscillation possible.
id: 'session-unique-fin-de-fenetre',
label: "08:00, hors fenêtre HC, session encore marquée engagée → réseau relâché et bit nettoyé",
time: '2026-08-08T08:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, allow_grid_hc_night: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '2', night_takeover_prev: 'on',
evcs_charging_switch: 'off',
night_grid_session_committed_prev: 'on',
soc_percent: '45',
balance_required_soc: '37.1', balance_energy_to_cover_kwh: '2.3',
balance_status: 'ok', balance_last_run: '08/08 07:58',
},
expect: {
takeoverNeeded: false,
takeoverHelperService: 'turn_off',
sessionCommittedService: 'turn_off',
},
},
{
id: 'session-unique-bilan-indisponible',
label: "Même relevé qu'en tête, bilan périmé → aucun engagement, comportement historique (réseau relâché)",
time: '2026-08-08T05:30:00+02:00',
overrides: {
...VEHICLE_NIGHT, allow_grid_hc_night: 'on',
grid_state: 'off', victron_acin1_available: '0',
battery_current_a: '-14', night_takeover_prev: 'off',
evcs_charging_switch: 'off',
prev_surplus_state: 'night_recovery_done_idle',
soc_percent: '30',
balance_required_soc: '37.1', balance_energy_to_cover_kwh: '2.3',
balance_status: 'ok', balance_last_run: '08/08 03:00',
},
expect: {
profile: 'night_hc',
takeoverNeeded: false,
takeoverHelperService: 'turn_off',
sessionCommittedService: null,
},
},
// ===== 03/09/2026 : le plancher du soir vient de l'energie a couvrir =====
// Tous ces scenarios ont un VRAI excedent (PV 2000 W, maison 1000 W) et des
// chargeurs qui se retiennent : les gardes de §36 et §37 sont donc passantes,
// et le plancher est le SEUL discriminant.
{
id: 'bilan-plancher-soir-durci',
label: "Soir, SOC 94 et vrai excédent, mais le bilan demande 99,7 % pour tenir jusqu'au croisement → le plancher passe de 85 à 99,7 → pas de charge",
time: EVENING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
battery_current_a: '2',
pv_power_w: '2000', pv_power_10m_avg_w: '2000', pv_trend_w: '0',
house_ac_power_w: '1000', vebus_ac_out_l1_power_w: '1000',
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',
soc_percent: '94',
balance_required_soc: '99.7', balance_energy_to_cover_kwh: '11.66',
balance_status: 'ok', balance_last_run: '08/08 17:58',
},
expect: {
profile: 'evening_high_soc',
state: 'idle_wait_conditions',
decision: 'hold',
desiredCharging: false,
},
},
{
// SENS UNIQUE : le bilan ne doit JAMAIS relacher le plancher historique.
// Sans ce scenario, remplacer Math.max par une affectation directe
// passerait inapercu et autoriserait des charges qui n'ont pas lieu
// aujourd'hui.
id: 'bilan-plancher-soir-ne-relache-pas',
label: "Soir, SOC 70, le bilan ne demande que 60 % → le plancher historique 85 % reste maître, pas de charge",
time: EVENING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
battery_current_a: '2',
pv_power_w: '2000', pv_power_10m_avg_w: '2000', pv_trend_w: '0',
house_ac_power_w: '1000', vebus_ac_out_l1_power_w: '1000',
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',
soc_percent: '70',
balance_required_soc: '60', balance_energy_to_cover_kwh: '4.2',
balance_status: 'ok', balance_last_run: '08/08 17:58',
},
expect: {
profile: 'evening_high_soc',
state: 'idle_wait_conditions',
decision: 'hold',
desiredCharging: false,
},
},
{
id: 'bilan-perime-regle-historique',
label: "Soir, SOC 94, bilan exigeant (99,7 %) mais vieux de 2 h → repli sur la règle historique 85 % → la charge démarre",
time: EVENING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
battery_current_a: '2',
pv_power_w: '2000', pv_power_10m_avg_w: '2000', pv_trend_w: '0',
house_ac_power_w: '1000', vebus_ac_out_l1_power_w: '1000',
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',
soc_percent: '94',
balance_required_soc: '99.7', balance_energy_to_cover_kwh: '11.66',
balance_status: 'ok', balance_last_run: '08/08 16:00',
},
expect: {
profile: 'evening_high_soc',
state: 'start_min_current',
decision: 'start',
desiredCharging: true,
currentA: 6,
},
},
{
id: 'bilan-statut-degrade-regle-historique',
label: "Soir, SOC 94, bilan frais mais statut non 'ok' → repli sur la règle historique → la charge démarre",
time: EVENING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
battery_current_a: '2',
pv_power_w: '2000', pv_power_10m_avg_w: '2000', pv_trend_w: '0',
house_ac_power_w: '1000', vebus_ac_out_l1_power_w: '1000',
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',
soc_percent: '94',
balance_required_soc: '99.7', balance_energy_to_cover_kwh: '11.66',
balance_status: 'donnees_insuffisantes', balance_last_run: '08/08 17:58',
},
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
// comparait un echantillon instantane a un plancher de 0 A toutes les 30 s :
// pile ou face. Chaque arret coutait ~0,45 kWh de production refusee, la
// voiture etant le seul preneur d'une batterie pleine.
// La table des bandes SOC declarait deja une tolerance de 12 min a SOC > 90 ;
// elle n'etait branchee nulle part. Aucun seuil n'est deplace ici.
{
id: 'batt-float-bruit-ne-coupe-pas',
label: "Relevé du 01/09 14:26 : SOC 94, batterie en float à -1 A, charge à 20 A → le bruit de régulation NE DOIT PAS arrêter la charge",
time: AFTERNOON,
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 14:00:00',
soc_percent: '94',
evcs_current_a: '20', evcs_set_current_a: '20', evcs_command_current_a: '20',
evcs_ac_power_w: '3600',
pv_power_w: '6600', pv_power_10m_avg_w: '6000', pv_trend_w: '0',
house_ac_power_w: '6000', vebus_ac_out_l1_power_w: '6000',
battery_current_a: '-1',
},
expect: {
profile: 'outside_window',
sourceMode: 'solar_only',
state: 'charging_hold',
decision: 'hold',
desiredCharging: true,
currentA: 20,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'batt-decharge-soutenue-coupe',
label: "CONTREFACTUEL : mêmes mesures, mais le dépassement dure depuis 20 min > tolérance 12 min de la bande gt90 → la garde reprend la main et arrête",
time: AFTERNOON,
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 14:00:00',
soc_percent: '94',
evcs_current_a: '20', evcs_set_current_a: '20', evcs_command_current_a: '20',
evcs_ac_power_w: '3600',
pv_power_w: '6600', pv_power_10m_avg_w: '6000', pv_trend_w: '0',
house_ac_power_w: '6000', vebus_ac_out_l1_power_w: '6000',
battery_current_a: '-1',
battery_soft_breach_at: '2026-08-08 15:10:00',
},
expect: {
profile: 'outside_window',
sourceMode: 'solar_only',
state: 'stop_conditions_lost',
decision: 'stop',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'batt-pointe-maison-ne-coupe-pas',
label: "Relevé du 01/09 14:56 : pointe maison à 8 kW, batterie à -42 A → réduction graduée au plafond système (18 A), PAS un arrêt",
time: AFTERNOON,
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 14:00:00',
soc_percent: '94',
evcs_current_a: '20', evcs_set_current_a: '20', evcs_command_current_a: '20',
evcs_ac_power_w: '3600',
pv_power_w: '9100', pv_power_10m_avg_w: '6000', pv_trend_w: '0',
house_ac_power_w: '8000', vebus_ac_out_l1_power_w: '8000',
battery_current_a: '-42',
},
expect: {
profile: 'outside_window',
sourceMode: 'solar_only',
state: 'charging_step_down_system_cap',
decision: 'step_down_fast',
desiredCharging: true,
currentA: 18,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'batt-decharge-dure-arret-immediat',
label: "SÉCURITÉ : batterie à -120 A, sous le seuil DUR de la bande (-100 A) → arrêt immédiat, aucune tolérance. La tolérance est du confort, jamais du risque",
time: AFTERNOON,
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 14:00:00',
soc_percent: '94',
evcs_current_a: '20', evcs_set_current_a: '20', evcs_command_current_a: '20',
evcs_ac_power_w: '3600',
pv_power_w: '1000', pv_power_10m_avg_w: '6000', pv_trend_w: '0',
house_ac_power_w: '9000', vebus_ac_out_l1_power_w: '9000',
battery_current_a: '-120',
},
expect: {
profile: 'outside_window',
sourceMode: 'solar_only',
state: 'battery_current_hard_stop',
decision: 'stop_hard',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'batt-plancher-demarrage-reste-instantane',
label: "FRONTIÈRE : véhicule branché mais à l'arrêt, batterie à -1 A → on NE DÉMARRE PAS sur une batterie qui se décharge. Le plancher de démarrage reste instantané",
time: AFTERNOON,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '94',
pv_power_w: '6600', pv_power_10m_avg_w: '6000', pv_trend_w: '0',
house_ac_power_w: '6000', vebus_ac_out_l1_power_w: '6000',
battery_current_a: '-1',
},
expect: {
profile: 'outside_window',
sourceMode: 'solar_only',
state: 'idle_wait_conditions',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
// ================= STRATÉGIES MANUELLES =================
{
id: 'force-grid-jour',
label: 'force_grid_now l\'après-midi, EDF ON, SOC 60 → charge réseau forcée',
time: AFTERNOON,
overrides: {
victron_evcs_strategy: 'force_grid_now',
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '60', pv_power_w: '500', pv_power_10m_avg_w: '500',
pv_trend_w: '0', battery_current_a: '2',
grid_state: 'on', victron_acin1_available: '1',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
},
expect: {
profile: 'outside_window',
sourceMode: 'grid_forced',
state: 'start_grid_forced_min_current',
decision: 'start',
desiredCharging: true,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'force-charge-nuit',
label: 'force_charge_now la nuit (toggle HC ON) → démarrage manuel local (non-régression P7)',
time: NIGHT,
overrides: {
...VEHICLE_NIGHT, victron_evcs_strategy: 'force_charge_now',
soc_percent: '60', allow_grid_hc_night: 'on',
},
expect: {
profile: 'night_hc',
sourceMode: 'solar_potential',
state: 'start_manual_local_min_current',
decision: 'start',
desiredCharging: true,
currentA: 6,
takeoverNeeded: true,
edfOnAction: false,
takeoverHelperService: 'turn_on',
},
},
{
id: 'strategie-stop',
label: 'Stratégie stop en journée → tout bloqué',
time: AFTERNOON,
overrides: {
victron_evcs_strategy: 'stop',
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '80', pv_power_w: '3000', pv_power_10m_avg_w: '3000',
battery_current_a: '10',
},
expect: {
profile: 'outside_window',
sourceMode: 'blocked',
state: 'stopped_user',
decision: 'stop_stop_mode',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
// ================= SÉCURITÉ =================
{
id: 'securite-overload',
label: 'Charge solaire en cours + surcharge VE.Bus → protection (stop/step-down)',
time: EVENING,
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 17:30:00',
soc_percent: '90', pv_power_w: '3500', pv_power_10m_avg_w: '3400',
battery_current_a: '5', evcs_current_a: '13', evcs_set_current_a: '13',
vebus_overload: 'on', vebus_l1_overload: 'on',
house_ac_power_w: '4800', vebus_ac_out_l1_power_w: '4800',
},
expect: {
profile: 'evening_high_soc',
sourceMode: 'solar_only',
state: 'vebus_overload_stop_min_current',
decision: 'stop',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
{
id: 'vehicule-debranche-jour',
label: 'Après-midi véhicule débranché → stop_not_connected (état de repos)',
time: AFTERNOON,
overrides: {
evcs_vehicle_connected: 'off', evcs_device_connected: 'on',
soc_percent: '93', pv_power_w: '1800', pv_power_10m_avg_w: '1800',
battery_current_a: '3',
},
expect: {
profile: 'outside_window',
sourceMode: 'solar_only',
state: 'vehicle_not_connected',
decision: 'stop_not_connected',
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: 'turn_off',
},
},
// ======= FENÊTRE D'ÉTABLISSEMENT DU RÉSEAU (31/08/2026) =======
// Mesuré ce jour-là : à la fermeture du contacteur, le réseau reprend toute la
// charge de la maison, le solaire bascule vers la batterie (ou se fait brider
// s'il n'y a plus de tampon : 2663 -> 559 W), puis l'import redescend à la
// consigne en ~150 s. Pendant ce temps la boucle de 30 s prend cinq décisions
// sur des mesures qui ne décrivent aucun régime permanent.
// Chaque cas est apparié à son contrefactuel hors fenêtre : sans cet appariement
// le test ne prouverait rien.
{
id: 'etablissement-maintien-charge',
label: 'Réseau fermé il y a 30 s, PV effondrée à 600 W, batterie à -15 A depuis 10 min : dans la fenêtre, la mesure batterie est neutralisée, la charge NE DOIT PAS être arrêtée',
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',
soc_percent: '60',
evcs_current_a: '16', evcs_set_current_a: '16', evcs_command_current_a: '16',
evcs_ac_power_w: '3680',
house_ac_power_w: '4400', vebus_ac_out_l1_power_w: '4400',
pv_power_w: '600', pv_power_10m_avg_w: '3900', pv_trend_w: '-2000',
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '1800',
battery_current_a: '-15',
battery_soft_breach_at: '2026-08-08 08:50:00',
acin1_last_changed_ts: String(MORNING_TS - 30),
grid_switch_last_changed_ts: String(MORNING_TS - 30),
},
expect: {
profile: "morning_solar",
sourceMode: "solar_only",
state: "charging_step_down_battery",
decision: "step_down",
desiredCharging: true,
currentA: 14,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: "turn_off",
},
},
{
id: 'etablissement-termine-arret-normal',
label: 'CONTREFACTUEL : mêmes mesures ET même dépassement de 10 min, transition vieille de 400 s → fenêtre fermée, la garde reprend la main et arrête (10 min > tolérance 4 min de la bande 40_60)',
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',
soc_percent: '60',
evcs_current_a: '16', evcs_set_current_a: '16', evcs_command_current_a: '16',
evcs_ac_power_w: '3680',
house_ac_power_w: '4400', vebus_ac_out_l1_power_w: '4400',
pv_power_w: '600', pv_power_10m_avg_w: '3900', pv_trend_w: '-2000',
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '1800',
battery_current_a: '-15',
battery_soft_breach_at: '2026-08-08 08:50:00',
acin1_last_changed_ts: String(MORNING_TS - 400),
grid_switch_last_changed_ts: String(MORNING_TS - 400),
},
expect: {
profile: "morning_solar",
sourceMode: "solar_only",
state: "stop_conditions_lost",
decision: "stop",
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: "turn_off",
},
},
{
id: 'etablissement-securite-non-suspendue',
label: "SÉCURITÉ : dans la fenêtre, batterie à -40 A → l'arrêt dur reste actif. La fenêtre suspend le confort, jamais le risque",
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',
soc_percent: '60',
evcs_current_a: '16', evcs_set_current_a: '16', evcs_command_current_a: '16',
evcs_ac_power_w: '3680',
house_ac_power_w: '4400', vebus_ac_out_l1_power_w: '4400',
pv_power_w: '600', pv_power_10m_avg_w: '3900', pv_trend_w: '-2000',
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '1800',
battery_current_a: '-40',
acin1_last_changed_ts: String(MORNING_TS - 30),
grid_switch_last_changed_ts: String(MORNING_TS - 30),
},
expect: {
profile: "morning_solar",
sourceMode: "solar_only",
state: "battery_current_hard_stop",
decision: "stop_hard",
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: "turn_off",
},
},
{
id: 'etablissement-ouverture-aucune-fenetre',
label: "ASYMÉTRIE : réseau DISPARU il y a 30 s → aucune fenêtre. Les onduleurs reprennent tout, les gardes doivent agir plus vite, pas moins (dépassement de 10 min > tolérance 4 min)",
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',
soc_percent: '60',
evcs_current_a: '16', evcs_set_current_a: '16', evcs_command_current_a: '16',
evcs_ac_power_w: '3680',
house_ac_power_w: '4400', vebus_ac_out_l1_power_w: '4400',
pv_power_w: '600', pv_power_10m_avg_w: '3900', pv_trend_w: '-2000',
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '1800',
battery_current_a: '-15',
grid_state: 'off', victron_acin1_available: '0', grid_power_w: '0',
battery_soft_breach_at: '2026-08-08 08:50:00',
acin1_last_changed_ts: String(MORNING_TS - 30),
grid_switch_last_changed_ts: String(MORNING_TS - 30),
},
expect: {
profile: "morning_solar",
sourceMode: "solar_only",
state: "stop_conditions_lost",
decision: "stop",
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: "turn_off",
},
},
{
id: 'etablissement-pas-de-demarrage',
label: "Réseau fermé il y a 30 s, rien en charge : on ne démarre pas non plus sur des mesures d'établissement",
time: MORNING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
pv_trend_w: '100', battery_current_a: '10',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '1800',
acin1_last_changed_ts: String(MORNING_TS - 30),
grid_switch_last_changed_ts: String(MORNING_TS - 30),
},
expect: {
profile: "morning_solar",
sourceMode: "solar_only",
state: "idle_wait_conditions",
decision: "hold",
desiredCharging: false,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
takeoverHelperService: "turn_off",
},
},
{
id: 'etablissement-demarrage-hors-fenetre',
label: 'CONTREFACTUEL : mêmes mesures, transition vieille de 400 s → le démarrage solaire a bien lieu',
time: MORNING,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
pv_trend_w: '100', battery_current_a: '10',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '1800',
acin1_last_changed_ts: String(MORNING_TS - 400),
grid_switch_last_changed_ts: String(MORNING_TS - 400),
},
expect: {
profile: "morning_solar",
sourceMode: "solar_only",
state: "start_min_current",
decision: "start",
desiredCharging: true,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
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",
},
},
// ===================================================================
// 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: 700,
},
},
{
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: 700,
},
},
// ================= CREDIT SOLAIRE SUR LA CONSIGNE (11/09/2026) =================
// Releve reel : BOOST 28 A en plein soleil, consigne 8260 W, PV effondre a
// 121 W, reseau 7697 W, batterie 0 A. La consigne evincait le solaire.
{
id: 'consigne-credit-solaire-descend',
label: "Releve 11/09 17:02 — BOOST 28 A, 6 trackers brides, batterie neutre : la consigne DESCEND pour rendre la main au solaire",
time: '2026-08-08T17:02:00+02:00',
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 16:30:00',
victron_evcs_strategy: 'force_grid_now',
grid_state: 'on', victron_acin1_available: '1',
grid_current_max_a: '28', current_max_auto_a: '22',
soc_percent: '96',
battery_current_a: '0',
pv_power_w: '121', pv_power_10m_avg_w: '150', pv_trend_w: '0',
house_ac_power_w: '7695', vebus_ac_out_l1_power_w: '7695',
evcs_ac_power_w: '5893', evcs_current_a: '28',
grid_setpoint_w: '8260',
mppt_operation_modes: 'voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited',
},
expect: {
sourceMode: 'grid_forced',
gridSetpointW: 6799,
},
},
{
id: 'consigne-credit-trackers-a-fond-pas-de-descente',
label: "CONTREFACTUEL — meme instant, mais les 6 trackers en mppt_active : plus rien a liberer, la consigne reste au plafond",
time: '2026-08-08T17:02:00+02:00',
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 16:30:00',
victron_evcs_strategy: 'force_grid_now',
grid_state: 'on', victron_acin1_available: '1',
grid_current_max_a: '28', current_max_auto_a: '22',
soc_percent: '96',
battery_current_a: '0',
pv_power_w: '121', pv_power_10m_avg_w: '150', pv_trend_w: '0',
house_ac_power_w: '7695', vebus_ac_out_l1_power_w: '7695',
evcs_ac_power_w: '5893', evcs_current_a: '28',
grid_setpoint_w: '8260',
mppt_operation_modes: 'mppt_active,mppt_active,mppt_active,mppt_active,mppt_active,mppt_active',
},
expect: {
sourceMode: 'grid_forced',
gridSetpointW: null,
},
},
{
id: 'consigne-credit-batterie-porte-remonte',
label: "CONTREFACTUEL — trackers brides MAIS batterie en depassement souple (-60 A) : securite d'abord, la consigne remonte au plafond",
time: '2026-08-08T17:02:00+02:00',
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 16:30:00',
victron_evcs_strategy: 'force_grid_now',
grid_state: 'on', victron_acin1_available: '1',
grid_current_max_a: '28', current_max_auto_a: '22',
soc_percent: '96',
battery_current_a: '-60',
pv_power_w: '121', pv_power_10m_avg_w: '150', pv_trend_w: '0',
house_ac_power_w: '7695', vebus_ac_out_l1_power_w: '7695',
evcs_ac_power_w: '5893', evcs_current_a: '28',
grid_setpoint_w: '2000',
mppt_operation_modes: 'voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited',
},
expect: {
sourceMode: 'grid_forced',
gridSetpointW: 8465,
},
},
{
id: 'consigne-credit-nuit-trackers-eteints',
label: "ANTI-OSCILLATEUR — nuit HC, session reseau, les 6 trackers `off` : rien a liberer, comportement de nuit STRICTEMENT inchange (§31, §57)",
time: NIGHT,
overrides: {
...VEHICLE_NIGHT,
soc_percent: '50',
allow_grid_hc_night: 'on',
grid_state: 'on',
victron_acin1_available: '1',
grid_setpoint_w: '5000',
mppt_operation_modes: 'off,off,off,off,off,off',
},
expect: {
sourceMode: 'grid_hc_allowed',
gridSetpointW: 5525,
},
},
{
id: 'consigne-credit-plancher-maison',
label: "La descente s'arrete a la maison hors voiture : le reseau continue de la couvrir (garantie du 01/09 et du 04/09)",
time: '2026-08-08T17:02:00+02:00',
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 16:30:00',
victron_evcs_strategy: 'force_grid_now',
grid_state: 'on', victron_acin1_available: '1',
grid_current_max_a: '6', current_max_auto_a: '6',
soc_percent: '96',
battery_current_a: '0',
pv_power_w: '4500', pv_power_10m_avg_w: '4500', pv_trend_w: '0',
house_ac_power_w: '7695', vebus_ac_out_l1_power_w: '7695',
evcs_ac_power_w: '5893', evcs_current_a: '28',
grid_setpoint_w: '2000',
mppt_operation_modes: 'voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited,voltage_current_limited',
},
expect: {
sourceMode: 'grid_forced',
gridSetpointW: 1802,
},
},
{
id: 'consigne-credit-converge-tient',
label: "ANTI-OSCILLATEUR (11/09 17:47) — consigne deja descendue a 2241 W et les 6 trackers repasses mppt_active : on TIENT, on ne repousse pas au plafond",
time: '2026-08-08T17:02:00+02:00',
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 16:30:00',
victron_evcs_strategy: 'force_grid_now',
grid_state: 'on', victron_acin1_available: '1',
grid_current_max_a: '28', current_max_auto_a: '22',
soc_percent: '92',
battery_current_a: '36',
pv_power_w: '7074', pv_power_10m_avg_w: '6000', pv_trend_w: '0',
house_ac_power_w: '6895', vebus_ac_out_l1_power_w: '6895',
evcs_ac_power_w: '6059', evcs_current_a: '28',
grid_setpoint_w: '5500',
mppt_operation_modes: 'mppt_active,mppt_active,mppt_active,mppt_active,mppt_active,mppt_active',
},
expect: {
sourceMode: 'grid_forced',
gridSetpointW: null,
},
},
{
id: 'consigne-credit-plancher-nuage',
label: "INCIDENT 11/09 18:00 — consigne descendue a 1281 W, voiture a 28 A : un nuage a fait chuter la PV de 4,6 kW et la batterie a pris 87 A (limite dure 70) -> hard stop. La consigne doit etre RELEVEE avant le nuage.",
time: '2026-08-08T17:02:00+02:00',
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 16:30:00',
victron_evcs_strategy: 'force_grid_now',
grid_state: 'on', victron_acin1_available: '1',
grid_current_max_a: '28', current_max_auto_a: '22',
soc_percent: '96',
battery_current_a: '-1',
pv_power_w: '6058', pv_power_10m_avg_w: '5800', pv_trend_w: '0',
house_ac_power_w: '7170', vebus_ac_out_l1_power_w: '7170',
evcs_ac_power_w: '6133', evcs_current_a: '28',
grid_setpoint_w: '1281',
mppt_operation_modes: 'mppt_active,mppt_active,mppt_active,mppt_active,mppt_active,mppt_active',
},
expect: {
sourceMode: 'grid_forced',
gridSetpointW: 4829,
},
},
// ======== SESSION RESEAU DE NUIT SANS VEHICULE (12/09/2026) ========
// Nuit du 11-12/09, voiture rendue la veille : le bilan a reclame 92 % puis
// 59 % de SOC toute la nuit, `reseau_necessaire` vrai a chaque calcul, et
// rien ne s'est passe -- SOC fige a 20 %, consigne reseau a 700 W depuis
// 18:36. La session exigeait un vehicule branche.
{
id: 'nuit-sans-vehicule-session-engage',
label: "INCIDENT 12/09 04:00 — HC, AUCUN vehicule, SOC 20, le bilan demande 59,1 % : la session reseau doit s'engager pour la MAISON",
time: '2026-08-08T04:00:00+02:00',
overrides: {
evcs_vehicle_connected: 'off', evcs_device_connected: 'on',
grid_state: 'on', victron_acin1_available: '1',
soc_percent: '20', battery_current_a: '0',
pv_power_w: '0', pv_power_10m_avg_w: '0', pv_trend_w: '0',
house_ac_power_w: '612', vebus_ac_out_l1_power_w: '612',
evcs_ac_power_w: '0',
grid_setpoint_w: '700',
balance_required_soc: '59.1', balance_energy_to_cover_kwh: '3.13',
balance_status: 'ok', balance_last_run: '08/08 03:58',
// Le MANQUE, pose explicitement : sans lui le scenario heriterait du 0 de
// la fixture, ce qui decrirait une batterie couverte alors qu'il declare
// un SOC de 20 % pour un requis de 59,1 % — un etat impossible.
// Valeur calculee avec la courbe mesuree, parc de reference (7 modules,
// echelle 1) : 13 points sous le genou de 33 % a 0,097 = 1,261 kWh, puis
// 26,1 points a 0,169 = 4,411 kWh, soit 5,672 kWh. Ce scenario est le seul
// du corpus dont le deficit TRAVERSE le genou.
balance_grid_energy_needed_kwh: '5.672',
},
expect: {
profile: 'night_hc',
takeoverNeeded: true,
takeoverHelperService: 'turn_on',
sessionCommittedService: 'turn_on',
// 18/09/2026 : 5437 -> 2380. Les 5437 valaient 612 de maison + 4825
// empruntes au plafond d'une borne ou aucun vehicule n'est branche. La
// nouvelle valeur vaut 612 + le manque reel borne par DVCC (30 A, soit
// 1538 W a la tension de la fixture), plus la marge d'ecriture d'un pas
// de courant. ⚠️ Attendu calcule sur la FIXTURE et jamais sur la tension
// du jour : le recalculer sur l'installation vivante rendrait la suite
// rouge un matin ou la batterie est a 49 V sans que rien ne soit casse.
gridSetpointW: 2380,
},
},
{
id: 'consigne-bilan-fin-de-fenetre-bornee-par-dvcc',
label: "CONSIGNE 18/09 — meme manque a 07:10, 14 min de fenetre : c'est DVCC qui borne, pas le temps",
time: '2026-08-08T07:10:00+02:00',
overrides: {
evcs_vehicle_connected: 'off', evcs_device_connected: 'on',
grid_state: 'on', victron_acin1_available: '1',
soc_percent: '20', battery_current_a: '0',
pv_power_w: '0', pv_power_10m_avg_w: '0', pv_trend_w: '0',
house_ac_power_w: '612', vebus_ac_out_l1_power_w: '612',
evcs_ac_power_w: '0',
grid_setpoint_w: '700',
balance_required_soc: '59.1', balance_energy_to_cover_kwh: '3.13',
balance_status: 'ok', balance_last_run: '08/08 07:08',
balance_grid_energy_needed_kwh: '5.672',
},
expect: {
profile: 'night_hc',
// ⚠️ LE SCENARIO QUI PROTEGE CONTRE LA REGRESSION LA PLUS GRAVE de cette
// conception. 5,672 kWh a combler en 14 minutes font 17 kW : avec un
// simple plancher d'horizon, la consigne saturait sur GRID_SETPOINT_MAX_W
// (10 000 W), soit PIRE que les 5437 du plafond emprunte qu'on remplace.
// Le plafond DVCC ramene le terme a 1538 W, et la consigne a la meme
// valeur qu'a 04:00 — parce que ce qui borne n'est pas le temps qui
// reste, c'est ce que la batterie a le droit de prendre.
// Le plafond BMS n'aurait rien borne : 250 A a 49 V font 12,2 kW,
// au-dessus des 10 000 de l'ecretage.
gridSetpointW: 2380,
},
},
{
id: 'consigne-bilan-max-la-voiture-domine',
label: 'CONTREFACTUEL 18/09 — meme nuit, meme manque, mais VEHICULE BRANCHE : le max retient la voiture, pas la somme',
time: '2026-08-08T04:00:00+02:00',
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
grid_state: 'on', victron_acin1_available: '1',
soc_percent: '20', battery_current_a: '0',
pv_power_w: '0', pv_power_10m_avg_w: '0', pv_trend_w: '0',
house_ac_power_w: '612', vebus_ac_out_l1_power_w: '612',
evcs_ac_power_w: '0',
grid_setpoint_w: '700',
balance_required_soc: '59.1', balance_energy_to_cover_kwh: '3.13',
balance_status: 'ok', balance_last_run: '08/08 03:58',
balance_grid_energy_needed_kwh: '5.672',
},
expect: {
profile: 'night_hc',
// Terme voiture 4825 W contre terme batterie 1538 W : le `max` retient la
// voiture, et la consigne vaut ce qu'elle vaut aujourd'hui. Avec une
// ADDITION elle monterait de 1538 W de plus — une autorisation en plus sur
// le chemin DOMINANT (85 nuits sur 88 ont un vehicule branche) pour
// corriger un chemin emprunte zero fois. C'est ce scenario qui epingle le
// choix du max, et il doit rester a la valeur historique.
gridSetpointW: 5437,
},
},
{
// ======== LA FENETRE DE MIDI EST UNE FENETRE D'HEURES CREUSES (18/09) ========
// L'engagement de session etait gate sur `night_hc` seul : la charge HC
// manuelle demandee par le proprietaire ne pouvait rien faire a midi. Le
// plancher relevait bien la cible du bilan, mais aucune session ne
// s'engageait — donc pas de contacteur, pas de consigne, et un plancher du
// soir releve pour rien.
id: 'hc-midi-bilan-reclame-engage-la-session',
label: 'MIDI 18/09 — journee sombre, le bilan reclame 85 % pour un SOC de 40 : la session HC de midi doit s engager',
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'off', evcs_device_connected: 'on',
soc_percent: '40', battery_current_a: '-8',
pv_power_w: '200', pv_power_10m_avg_w: '200', pv_trend_w: '0',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_ac_power_w: '0',
balance_required_soc: '85', balance_energy_to_cover_kwh: '7.6',
balance_grid_energy_needed_kwh: '7.605',
balance_status: 'ok', balance_last_run: '08/08 12:50',
},
expect: {
profile: 'midday_hc_or_solar',
sessionCommittedService: 'turn_on',
takeoverNeeded: true,
takeoverHelperService: 'turn_on',
},
},
{
id: 'hc-midi-soleil-aucun-engagement',
label: 'CONTREFACTUEL — meme midi en plein soleil : le bilan ne reclame rien (22 %), aucune session malgre un SOC sous le palier du matin',
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'off', evcs_device_connected: 'on',
soc_percent: '30', battery_current_a: '60',
pv_power_w: '7000', pv_power_10m_avg_w: '6800', pv_trend_w: '0',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_ac_power_w: '0',
// Le croisement PV est passe : le bilan retombe au plancher dur.
balance_required_soc: '22', balance_energy_to_cover_kwh: '0',
balance_grid_energy_needed_kwh: '0',
balance_status: 'ok', balance_last_run: '08/08 12:50',
},
expect: {
profile: 'midday_hc_or_solar',
// ⚠️ LE SCENARIO QUI JUSTIFIE LE CHOIX DU CRITERE. A midi on compare le SOC
// au SOC REQUIS du bilan (22), et non a `batteryMorningTargetPct` qui
// contient le palier de prevision — celui-ci vaudrait 35 par beau temps,
// donc ce SOC de 30 % engagerait une session reseau EN PLEIN SOLEIL,
// c'est-a-dire la substitution qu'on a mis des semaines a eteindre.
takeoverNeeded: false,
},
},
// ======== JEEP BRANCHEE A MIDI, DEPART 15:30 (28/09/2026) ========
// « JEEP branchee engage le reseau en HC midi si l'analyse de production le
// justifie » : le BILAN fait l'analyse et publie un verdict, le controleur le
// relaie sous les memes gardes (statut, fraicheur) que la cible. Ici la
// batterie n'a rien a reclamer (SOC 45 > 30) : seul le verdict voiture engage.
{
id: 'hc-midi-jeep-verdict-bilan-engage-la-session',
label: 'MIDI 28/09 — JEEP branchee, le bilan juge le solaire insuffisant avant 15:30 : la session HC de midi s engage',
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'on',
evcs_device_connected: 'on',
soc_percent: '45', battery_current_a: '10',
pv_power_w: '3000', pv_power_10m_avg_w: '3000', pv_trend_w: '0',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_ac_power_w: '0',
balance_required_soc: '30', balance_energy_to_cover_kwh: '0',
balance_grid_energy_needed_kwh: '0',
balance_status: 'ok', balance_last_run: '08/08 12:50',
balance_vehicle_grid_needed: 'on',
},
expect: { profile: 'midday_hc_or_solar', sessionCommittedService: 'turn_on', takeoverNeeded: true },
},
{
id: 'hc-midi-jeep-sans-verdict-aucun-engagement',
label: 'CONTREFACTUEL — meme midi, le bilan juge le solaire suffisant : aucune session',
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'on',
evcs_device_connected: 'on',
soc_percent: '45', battery_current_a: '10',
pv_power_w: '3000', pv_power_10m_avg_w: '3000', pv_trend_w: '0',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_ac_power_w: '0',
balance_required_soc: '30', balance_energy_to_cover_kwh: '0',
balance_grid_energy_needed_kwh: '0',
balance_status: 'ok', balance_last_run: '08/08 12:50',
balance_vehicle_grid_needed: 'off',
},
expect: { profile: 'midday_hc_or_solar', takeoverNeeded: false },
},
{
id: 'hc-midi-jeep-verdict-voiture-debranchee',
label: 'CONTREFACTUEL — verdict encore a ON mais voiture debranchee : aucune session',
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'off',
evcs_device_connected: 'on',
soc_percent: '45', battery_current_a: '10',
pv_power_w: '3000', pv_power_10m_avg_w: '3000', pv_trend_w: '0',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_ac_power_w: '0',
balance_required_soc: '30', balance_energy_to_cover_kwh: '0',
balance_grid_energy_needed_kwh: '0',
balance_status: 'ok', balance_last_run: '08/08 12:50',
balance_vehicle_grid_needed: 'on',
},
expect: { profile: 'midday_hc_or_solar', takeoverNeeded: false },
},
{
id: 'hc-midi-jeep-verdict-bilan-perime',
label: 'CONTREFACTUEL — verdict a ON mais bilan vieux de 2 h : on ne relaie pas un verdict perime',
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'on',
evcs_device_connected: 'on',
soc_percent: '45', battery_current_a: '10',
pv_power_w: '3000', pv_power_10m_avg_w: '3000', pv_trend_w: '0',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_ac_power_w: '0',
balance_required_soc: '30', balance_energy_to_cover_kwh: '0',
balance_grid_energy_needed_kwh: '0',
balance_status: 'ok', balance_last_run: '08/08 11:00',
balance_vehicle_grid_needed: 'on',
},
expect: { profile: 'midday_hc_or_solar', takeoverNeeded: false },
},
{
id: 'hc-midi-session-engagee-voiture-sur-reseau',
label: 'MIDI — session engagee, contacteur ferme, plein soleil : la voiture passe sur le reseau (batterie et voiture ensemble)',
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'on',
grid_state: 'on', victron_acin1_available: '1',
night_grid_session_committed_prev: 'on',
evcs_device_connected: 'on',
soc_percent: '45', battery_current_a: '10',
pv_power_w: '6000', pv_power_10m_avg_w: '6000', pv_trend_w: '0',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_ac_power_w: '0',
balance_required_soc: '30', balance_energy_to_cover_kwh: '0',
balance_grid_energy_needed_kwh: '0',
balance_status: 'ok', balance_last_run: '08/08 12:50',
balance_vehicle_grid_needed: 'on',
},
expect: { profile: 'midday_hc_or_solar', sourceMode: 'grid_hc_allowed', takeoverNeeded: true },
},
{
id: 'consigne-bilan-plancher-nuage-exclut-la-batterie',
label: 'CONSIGNE 18/09 — midi, soleil bride, manque batterie, PAS de voiture : le terme batterie ne doit PAS remonter le plancher nuage',
time: MIDDAY,
overrides: {
// ⚠️ LA FENETRE CREUSE N'EST PAS FORCEMENT LA NUIT. Celle de midi
// (12:24-14:24) admet simultanement ce qu'on croyait incompatible : du
// soleil fort, des chargeurs brides, et un manque batterie. C'est la
// seule configuration qui distingue un plancher nuage correct d'un
// plancher pollue par le terme batterie — et le discriminant n'est pas
// l'heure, c'est un DVCC HAUT. A 30 A le terme batterie plafonnerait sous
// le budget de surtension et les deux variantes rendraient la meme chose.
//
// Forme reelle : c'est l'episode du 17/09 — force_grid pose a midi,
// soleil, aucune voiture — avec en plus un manque batterie et l'ACM en
// boost de journee (180 A, son profil de 07:30).
victron_evcs_strategy: 'force_grid_now',
evcs_vehicle_connected: 'off', evcs_device_connected: 'on',
grid_state: 'on', victron_acin1_available: '1',
soc_percent: '35', battery_current_a: '20',
pv_power_w: '7000', pv_power_10m_avg_w: '6500', pv_trend_w: '0',
mppt_operation_modes: 'voltage_current_limited,voltage_current_limited,mppt_active,mppt_active,mppt_active,mppt_active,mppt_active',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
evcs_ac_power_w: '0',
grid_setpoint_w: '8000',
dvcc_max_charge_current_a: '180',
// 35 % -> 90 % entierement au-dessus du genou : 55 points x 0,169 = 9,295 kWh.
balance_required_soc: '90', balance_grid_energy_needed_kwh: '9.295',
balance_energy_to_cover_kwh: '9.295',
balance_status: 'ok', balance_last_run: '08/08 12:30',
},
expect: {
// Des chargeurs brides prouvent qu'il reste de la ressource : la consigne
// DESCEND. Le plancher qui borne cette descente vaut maison + voiture,
// soit 900 W ici. Si le terme batterie y entrait, le plancher remonterait
// de plusieurs kW et BLOQUERAIT la descente — la consigne resterait haute
// et evincerait le solaire, c'est-a-dire le defaut du 11/09 reintroduit
// par la porte de derriere. Ce scenario ne teste donc pas une bizarrerie
// de forme, il teste le retour d'un incident connu.
gridSetpointW: 4219,
},
},
{
id: 'nuit-sans-vehicule-batterie-couverte',
label: "CONTREFACTUEL — meme nuit sans vehicule mais SOC 70 au-dessus des 59,1 % requis : aucune session, la batterie passe la nuit seule",
time: '2026-08-08T04:00:00+02:00',
overrides: {
evcs_vehicle_connected: 'off', evcs_device_connected: 'on',
grid_state: 'on', victron_acin1_available: '1',
soc_percent: '70', battery_current_a: '-12',
pv_power_w: '0', pv_power_10m_avg_w: '0', pv_trend_w: '0',
house_ac_power_w: '612', vebus_ac_out_l1_power_w: '612',
evcs_ac_power_w: '0',
grid_setpoint_w: '700',
balance_required_soc: '59.1', balance_energy_to_cover_kwh: '3.13',
balance_status: 'ok', balance_last_run: '08/08 03:58',
},
expect: {
profile: 'night_hc',
takeoverNeeded: false,
sessionCommittedService: null,
},
},
{
id: 'nuit-sans-vehicule-hors-fenetre',
label: "CONTREFACTUEL — meme deficit mais a 23:00, HORS fenetre HC : aucune session. Le relachement reste la fin de fenetre, donc monotone.",
time: '2026-08-08T23:00:00+02:00',
overrides: {
evcs_vehicle_connected: 'off', evcs_device_connected: 'on',
grid_state: 'on', victron_acin1_available: '1',
soc_percent: '20', battery_current_a: '-12',
pv_power_w: '0', pv_power_10m_avg_w: '0', pv_trend_w: '0',
house_ac_power_w: '612', vebus_ac_out_l1_power_w: '612',
evcs_ac_power_w: '0',
grid_setpoint_w: '700',
balance_required_soc: '92.2', balance_energy_to_cover_kwh: '6.33',
balance_status: 'ok', balance_last_run: '08/08 22:58',
},
expect: {
takeoverNeeded: false,
sessionCommittedService: null,
},
},
{
id: 'nuit-sans-vehicule-bilan-perime',
label: "CONTREFACTUEL — HC sans vehicule, SOC 20, mais bilan vieux de 2 h : comportement historique, aucune session",
time: '2026-08-08T04:00:00+02:00',
overrides: {
evcs_vehicle_connected: 'off', evcs_device_connected: 'on',
grid_state: 'on', victron_acin1_available: '1',
soc_percent: '20', battery_current_a: '0',
pv_power_w: '0', pv_power_10m_avg_w: '0', pv_trend_w: '0',
house_ac_power_w: '612', vebus_ac_out_l1_power_w: '612',
evcs_ac_power_w: '0',
grid_setpoint_w: '700',
balance_required_soc: '59.1', balance_energy_to_cover_kwh: '3.13',
balance_status: 'ok', balance_last_run: '08/08 02:00',
},
expect: {
takeoverNeeded: false,
sessionCommittedService: null,
},
},
// ======== MISE A L'ECHELLE DU PARC (12/09/2026) ========
// Les bandes de politique batterie sont en amperes absolus, decrivant le parc
// de 4 modules. Ajouter 4 US5000 (soit 12 equivalents US2000C) doit les
// mettre a l'echelle, sinon le nouveau parc reste bride au niveau de l'ancien.
{
id: 'parc-double-decharge-toleree',
label: "Parc porte a 12 equivalents (4 US2000C + 4 US5000), BMS declarant 300 A : les -120 A qui arretaient tout a 4 modules deviennent tolerables",
time: AFTERNOON,
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 14:00:00',
soc_percent: '94',
evcs_current_a: '20', evcs_set_current_a: '20', evcs_command_current_a: '20',
evcs_ac_power_w: '3600',
pv_power_w: '1000', pv_power_10m_avg_w: '6000', pv_trend_w: '0',
house_ac_power_w: '9000', vebus_ac_out_l1_power_w: '9000',
battery_current_a: '-120',
modules_en_service: '12',
bms_max_discharge_current_a: '300', bms_max_charge_current_a: '300',
},
expect: {
profile: 'outside_window',
state: 'charging_step_down_system_cap',
},
},
{
id: 'parc-double-plafond-bms',
label: "CONTREFACTUEL — parc declare a 12 mais le BMS n'autorise que 100 A (cellule faible, cablage) : le plafond BMS prime, -120 A reste un arret dur",
time: AFTERNOON,
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 14:00:00',
soc_percent: '94',
evcs_current_a: '20', evcs_set_current_a: '20', evcs_command_current_a: '20',
evcs_ac_power_w: '3600',
pv_power_w: '1000', pv_power_10m_avg_w: '6000', pv_trend_w: '0',
house_ac_power_w: '9000', vebus_ac_out_l1_power_w: '9000',
battery_current_a: '-120',
modules_en_service: '12',
bms_max_discharge_current_a: '100', bms_max_charge_current_a: '100',
},
expect: {
profile: 'outside_window',
state: 'battery_current_hard_stop',
decision: 'stop_hard',
},
},
{
id: 'parc-non-declare-comportement-inchange',
label: "CONTREFACTUEL — aucun parc declare, aucune limite BMS lisible : echelle 1, plafond inactif, comportement d'avant ce chantier",
time: AFTERNOON,
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 14:00:00',
soc_percent: '94',
evcs_current_a: '20', evcs_set_current_a: '20', evcs_command_current_a: '20',
evcs_ac_power_w: '3600',
pv_power_w: '1000', pv_power_10m_avg_w: '6000', pv_trend_w: '0',
house_ac_power_w: '9000', vebus_ac_out_l1_power_w: '9000',
battery_current_a: '-120',
},
expect: {
profile: 'outside_window',
state: 'battery_current_hard_stop',
decision: 'stop_hard',
},
},
// =====================================================
// P12 — CHARGE NUIT SUR LA BATTERIE (15/09/2026)
// =====================================================
// Idee du proprietaire : plutot qu'acheter du reseau la nuit pour charger la
// voiture, lui donner le solaire DEJA STOCKE, et laisser ainsi de la place
// dans la batterie pour la production du lendemain. Releve de sa nuit du 14
// au 15/09 : 8 kWh achetes au reseau, batterie encore a 69 % a 5 h du matin,
// et une prevision de 38,7 kWh le lendemain dont 10,5 auraient ete ecretes.
//
// Le mode `battery_assist_night` existait pour exactement ca et etait MORT
// depuis toujours : il exigeait `batteryCurrentA >= 0`, impossible la nuit
// puisque la maison tire sur la batterie. P12 remplace ce test de COURANT par
// un test d'ENERGIE — rester au-dessus de `required_soc_prudent`, que le
// bilan publie deja. Aucune constante nouvelle.
{
id: 'p12-charge-nuit-sur-batterie',
label: 'Nuit HC, P12 ON, SOC 69 bien au-dessus du plancher 38,7 -> la voiture puise dans la batterie',
time: '2026-09-15T02:30:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '69',
allow_grid_hc_night: 'on',
night_battery_charge_enabled: 'on', night_battery_done_prev: 'off',
grid_state: 'off', victron_acin1_available: '0',
battery_current_a: '-16',
evcs_charging_switch: 'off',
balance_status: 'ok', balance_last_run: '15/09 02:25',
balance_required_soc: '36.8', balance_required_soc_prudent: '38.7',
balance_energy_to_cover_kwh: '3.18',
},
expect: {
profile: 'night_hc',
sourceMode: 'battery_assist_night',
// ⚠️ Verifier que la voiture CHARGE REELLEMENT, pas seulement que le mode
// est choisi. Sans ces trois lignes, neutraliser la condition de demarrage
// dans BATTERY_CLOUD_LOCKS ne cassait aucun scenario — constate par
// mutation le 15/09.
state: 'start_battery_assist_night_min_current',
decision: 'start',
desiredCharging: true,
currentA: 6,
takeoverNeeded: false,
edfOnAction: false,
},
},
// CONTREFACTUEL — surplus epuise : la bascule vers le reseau se fait TOUTE
// SEULE. C'est la soupape decrite par le proprietaire, sans regle horaire.
{
id: 'p12-surplus-epuise-le-reseau-prend-le-relais',
label: 'Nuit HC, P12 ON mais SOC 37 sous le plancher 38,7 -> P7 reprend, EDF se ferme',
time: '2026-09-15T02:30:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '37',
allow_grid_hc_night: 'on',
night_battery_charge_enabled: 'on', night_battery_done_prev: 'off',
grid_state: 'off', victron_acin1_available: '0',
battery_current_a: '-16',
evcs_charging_switch: 'off',
balance_status: 'ok', balance_last_run: '15/09 02:25',
balance_required_soc: '36.8', balance_required_soc_prudent: '38.7',
balance_energy_to_cover_kwh: '3.18',
},
expect: {
profile: 'night_hc',
sourceMode: 'grid_hc_allowed',
takeoverNeeded: true,
edfOnAction: true,
},
},
// ⚠️ LE SCENARIO ANTI-OSCILLATEUR, le plus important des quatre.
// Voiture arretee, le plancher descend PLUS VITE que le SOC (-3,7 pt/h
// contre -2,6) : le surplus reapparaitrait tout seul. Ici le SOC est
// repasse AU-DESSUS du plancher, mais le verrou est pose — pas de reprise.
{
id: 'p12-verrou-pas-de-reprise-du-surplus',
label: 'Nuit HC, P12 ON, SOC 45 repasse au-dessus du plancher MAIS verrou pose -> pas de reprise',
time: '2026-09-15T04:00:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '45',
allow_grid_hc_night: 'on',
night_battery_charge_enabled: 'on', night_battery_done_prev: 'on',
grid_state: 'off', victron_acin1_available: '0',
battery_current_a: '-16',
evcs_charging_switch: 'off',
balance_status: 'ok', balance_last_run: '15/09 03:55',
balance_required_soc: '36.8', balance_required_soc_prudent: '38.7',
balance_energy_to_cover_kwh: '3.18',
},
expect: {
profile: 'night_hc',
sourceMode: 'grid_hc_allowed',
takeoverNeeded: true,
},
},
// CONTREFACTUEL du toggle : a off, exactement le comportement d'avant.
{
id: 'p12-toggle-off-comportement-historique',
label: 'Nuit HC, memes conditions mais toggle P12 OFF -> P7 comme avant',
time: '2026-09-15T02:30:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '69',
allow_grid_hc_night: 'on',
night_battery_charge_enabled: 'off', night_battery_done_prev: 'off',
grid_state: 'off', victron_acin1_available: '0',
battery_current_a: '-16',
evcs_charging_switch: 'off',
balance_status: 'ok', balance_last_run: '15/09 02:25',
balance_required_soc: '36.8', balance_required_soc_prudent: '38.7',
balance_energy_to_cover_kwh: '3.18',
},
expect: {
profile: 'night_hc',
sourceMode: 'grid_hc_allowed',
takeoverNeeded: true,
edfOnAction: true,
},
},
// ⚠️ GARDE DE FRAICHEUR. Le plancher vient du bilan ; s'il est perime, il ne
// decrit plus la nuit en cours et vider la batterie dessus serait aveugle.
// Ajoute apres qu'une mutation eut montre que rien ne testait cette garde.
{
id: 'p12-bilan-perime-pas-de-charge-batterie',
label: 'Nuit HC, P12 ON, SOC 69 mais bilan vieux de 3 h -> P7 comme avant, pas de puisage batterie',
time: '2026-09-15T02:30:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '69',
allow_grid_hc_night: 'on',
night_battery_charge_enabled: 'on', night_battery_done_prev: 'off',
grid_state: 'off', victron_acin1_available: '0',
battery_current_a: '-16',
evcs_charging_switch: 'off',
balance_status: 'ok', balance_last_run: '14/09 23:25',
balance_required_soc: '36.8', balance_required_soc_prudent: '38.7',
balance_energy_to_cover_kwh: '3.18',
},
expect: {
profile: 'night_hc',
sourceMode: 'grid_hc_allowed',
takeoverNeeded: true,
edfOnAction: true,
},
},
// ⚠️ P12 DOIT GAGNER LE PREMIER CYCLE DE LA NUIT, OU IL NE GAGNE PAS.
//
// Decouvert en essai reel le 15/09 a 05:30. P7 s'etait engage a 05:13, avant
// que le proprietaire n'active P12 : le helper `night_grid_takeover_needed`
// etait donc deja ON, et le verrou anti-creneaux de juillet (P2) tenait le
// reseau — `nightGridHcPreferred` passe bien a false grace a P12, mais
// `nightTakeoverLatched` maintient `nightGridTakeoverNeeded` de son cote.
//
// Et c'est le comportement SUR : basculer reseau -> batterie en pleine
// session, c'est exactement le creneau que ce verrou existe pour empecher.
// Aucun des cinq scenarios P12 ne le montrait — ils partent tous d'une nuit
// vierge. Celui-ci epingle la contrainte pour qu'un changement futur soit
// delibere.
{
id: 'p12-verrou-p2-tient-si-le-reseau-est-deja-engage',
label: 'Nuit HC, P7 deja engage (takeover ON) puis P12 active -> le verrou anti-creneaux tient, pas de bascule',
time: '2026-09-15T05:30:00+02:00',
overrides: {
...VEHICLE_NIGHT, soc_percent: '69',
allow_grid_hc_night: 'on',
night_battery_charge_enabled: 'on', night_battery_done_prev: 'off',
// LE point du scenario : la reprise reseau est deja engagee.
night_takeover_prev: 'on',
grid_state: 'on', victron_acin1_available: '1',
battery_current_a: '1',
evcs_charging_switch: 'on',
balance_status: 'ok', balance_last_run: '15/09 05:25',
balance_required_soc: '36.8', balance_required_soc_prudent: '36.2',
balance_energy_to_cover_kwh: '3.18',
},
expect: {
profile: 'night_hc',
sourceMode: 'grid_hc_allowed',
takeoverNeeded: true,
},
},
// ===================================================================
// BOOST 28 A du 18/09/2026 — le verrou survivait a sa cause (HANDOFF)
// Releve reel : clic BOOST 13:08:50, contacteur ferme 13:09:53, garde
// batterie a -32,4 A a 13:09:30 (23 s AVANT l'arrivee d'EDF), verrou
// 20 min reseau present. Trois volets, un scenario chacun + contrefactuel.
// ===================================================================
{
id: 'boost-attente-reseau-courant-mini',
label: "Session active, force_grid demande, reseau PAS ENCORE la → on retombe au courant MINI (le 13 A tenu sur la batterie est le defaut du 18/09)",
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
evcs_charging_switch: 'on', control_latch: 'on', charge_permission_now: 'on',
// Charge solaire en cours a 13 A, min_run PAS ecoule (start recent).
last_start_at: '2026-08-08 12:58:30',
evcs_current_a: '13', evcs_set_current_a: '13', evcs_command_current_a: '13',
evcs_ac_power_w: '2990',
victron_evcs_strategy: 'force_grid_now',
// Le contacteur n'est PAS encore ferme : c'est la fenetre de l'incident.
grid_state: 'off', victron_acin1_available: '0', grid_power_w: '0',
soc_percent: '55', pv_power_w: '1200', pv_power_10m_avg_w: '1400',
house_ac_power_w: '3900', vebus_ac_out_l1_power_w: '3900',
// Batterie sollicitee mais SOUS le garde dur : la charge continue.
battery_current_a: '-20',
},
expect: {
sourceMode: 'grid_forced',
state: 'hold_min_run_wait_grid_takeover',
decision: 'hold',
desiredCharging: true,
currentA: 6,
},
},
{
id: 'boost-garde-sans-reseau-emet-no-grid',
label: "Le garde batterie mord RESEAU ABSENT → blockReason 'grid_battery_guard_no_grid' (et pas le verrou plein)",
time: MIDDAY,
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 12:58:30',
evcs_current_a: '13', evcs_set_current_a: '13', evcs_command_current_a: '13',
evcs_ac_power_w: '2990',
victron_evcs_strategy: 'force_grid_now',
grid_state: 'off', victron_acin1_available: '0', grid_power_w: '0',
// Le releve reel du 13:09:30 : -32,4 A, sous le garde dur de sa bande.
soc_percent: '55', pv_power_w: '300', pv_power_10m_avg_w: '400',
house_ac_power_w: '3900', vebus_ac_out_l1_power_w: '3900',
// ⚠️ Fenetre etroite VOULUE : le garde reseau (grid_battery_hard_a -20)
// doit mordre SANS que le hard general de la bande 40-60 (~-20,9 dans ce
// fixture) ne prenne la main — c'est exactement la hierarchie de
// production du 18/09 (-32,4 A avec un parc 13/4 ou le hard general est
// a -98 A). Le fixture n'etant pas a l'echelle du parc reel, on se place
// entre les deux seuils DU FIXTURE.
battery_current_a: '-20.5',
},
expect: {
state: 'stop_grid_battery_guard',
decision: 'stop',
desiredCharging: false,
blockReason: 'grid_battery_guard_no_grid',
},
},
{
id: 'boost-verrou-no-grid-leve-a-l-arrivee-du-reseau',
label: "Verrou '_no_grid' pose, le reseau ARRIVE → le verrou se leve, la charge repart (le 18/09 elle restait bloquee 20 min)",
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
evcs_charging_switch: 'off', control_latch: 'off', charge_permission_now: 'off',
victron_evcs_strategy: 'force_grid_now',
// Le verrou du releve reel : pose il y a 30 s, 20 min devant lui.
last_block_reason: 'grid_battery_guard_no_grid',
blocked_until: '2026-08-08 13:19:30',
last_stop_at: '2026-08-08 12:59:30',
prev_surplus_state: 'stop_grid_battery_guard',
// Le contacteur s'est ferme 23 s apres le stop : reseau PRESENT.
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '900',
soc_percent: '55', pv_power_w: '1200', pv_power_10m_avg_w: '1400',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
battery_current_a: '2',
acin1_last_changed_ts: String(Date.parse(MIDDAY) / 1000 - 400),
grid_switch_last_changed_ts: String(Date.parse(MIDDAY) / 1000 - 400),
},
expect: {
sourceMode: 'grid_forced',
state: 'start_grid_forced_min_current',
decision: 'start',
desiredCharging: true,
},
},
{
id: 'boost-verrou-plein-tient-reseau-present',
label: "CONTREFACTUEL anti-oscillateur : verrou 'grid_battery_guard' (garde mordu RESEAU PRESENT, incident du 01/09) → il tient ses 20 min malgre le reseau",
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
evcs_charging_switch: 'off', control_latch: 'off', charge_permission_now: 'off',
victron_evcs_strategy: 'force_grid_now',
last_block_reason: 'grid_battery_guard',
blocked_until: '2026-08-08 13:19:30',
last_stop_at: '2026-08-08 12:59:30',
prev_surplus_state: 'stop_grid_battery_guard',
grid_state: 'on', victron_acin1_available: '1', grid_power_w: '900',
soc_percent: '55', pv_power_w: '1200', pv_power_10m_avg_w: '1400',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
battery_current_a: '2',
acin1_last_changed_ts: String(Date.parse(MIDDAY) / 1000 - 400),
grid_switch_last_changed_ts: String(Date.parse(MIDDAY) / 1000 - 400),
},
expect: {
state: 'wait_reason_lock',
decision: 'hold',
desiredCharging: false,
blockReason: 'grid_battery_guard',
},
},
{
id: 'boost-verrou-no-grid-tient-reseau-toujours-absent',
// NOTE : ce n'est pas le verrou qui protege ici mais la branche anterieure
// wait_grid_source (source reseau demandee, reseau absent => arret). La
// clause de pertinence '_no_grid && !gridAvailable' dans BCL est donc une
// ceinture DEFENSIVE (elle couvre un futur reordonnancement de branches) ;
// la propriete observable epinglee est : AUCUNE tentative sur la batterie.
label: "Verrou '_no_grid', reseau toujours absent → arret en attente du reseau, aucun retry sur la batterie",
time: MIDDAY,
overrides: {
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
evcs_charging_switch: 'off', control_latch: 'off', charge_permission_now: 'off',
victron_evcs_strategy: 'force_grid_now',
last_block_reason: 'grid_battery_guard_no_grid',
blocked_until: '2026-08-08 13:19:30',
last_stop_at: '2026-08-08 12:59:30',
prev_surplus_state: 'stop_grid_battery_guard',
grid_state: 'off', victron_acin1_available: '0', grid_power_w: '0',
soc_percent: '55', pv_power_w: '300', pv_power_10m_avg_w: '400',
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
battery_current_a: '-4',
},
expect: {
state: 'wait_grid_source',
desiredCharging: false,
blockReason: 'grid_unavailable',
},
},
];