Files
EVCS/tests/golden/scenarios.mjs
Tallagrand Thierry 7a26078e73 La provocation solaire se demande aux chargeurs, pas au SOC
Le correctif precedent bloque l incident de 19:44, mais un excedent proche de
zero est ambigu : c est la signature de la censure ET celle de l equilibre de
fin de journee. Le releve du 03/09 contient les deux avec le meme excedent —
18:50 : +14 W, ressource refusee ; 19:41 : +1 W, plus rien a donner. Rejeu du
19:41 : batteryAssistSolarStartOk etait encore vrai. Trois minutes avant
l incident, le systeme aurait commis la meme faute.

Le plan annoncait de lire evcs_balance_pv_censored. Lecture de CENSORSHIP.js :
ce drapeau est un OU de motifs dont l un est exactement le proxy qu on voulait
remplacer (socPct >= 90 et batterie sans puits). Et il est reste ON jusqu au
calcul de 19:50, soit cinq minutes APRES le demarrage de la charge — ce qui l a
fait basculer n est pas la censure mais sun_elevation passant sous 10 degres.
Il n aurait pas bloque l incident.

Le signal juste, ce sont les chargeurs eux-memes : sept entites
sensor.*_mppt_operation_mode, jamais lues par aucun workflow. mppt_active = je
suis a mon point de puissance maximale, je donne tout ; voltage_current_limited
ou off = je me retiens faute de preneur. Le 03/09 elles ont bascule a 19:20,
soit 24 minutes avant pv_censored, sans aucun proxy et a la cadence de la
boucle. Contre-verification le 01/09 a 14:22 : les trackers restaient limites
PENDANT une charge de 3,4 kW sous 6,9 kW de PV, donc le signal ne s auto-annule
pas a midi.

Collecte sans aucun entity_id code en dur (selectattr sur mppt et
operation_mode), expression testee sur /api/template puis gabarit complet
re-rendu : 191 champs, JSON valide, aucun null. Lectures inexploitables
ecartees, et repli sur le comportement anterieur quand il n en reste aucune.

Garde de DEMARRAGE uniquement, structurellement : la lier au sourceMode ou au
maintien ferait que la charge, en donnant un preneur au MPPT, detruirait sa
propre condition d autorisation — le bistable de l abondance P6 corrige le
24/08.

Golden 64 -> 66. Mutation dans trois directions : garde neutralisee -> 1 echec
cible, provocation toujours refusee -> 3 echecs, lecture illisible traitee
comme "a fond" -> 1 echec sur le scenario de repli.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
2026-09-03 21:41:31 +02:00

1676 lines
62 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',
},
},
// ===== ADVISOR IA — premier câblage réel (09/08/2026, HANDOFF §20) =====
// Référence : night-soc-sous-cible (SOC 30 < cible règle 35 → grid_forced).
// Avec la cible advisor 22 APPLIQUÉE : SOC 30 ≥ 22 → grid_hc_allowed.
{
id: 'night-advisor-target-applique',
label: 'Nuit HC, SOC 30, advisor cible 22 appliquée (toggle ON, ok, frais) → hc_allowed au lieu de 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_hc_allowed',
state: 'night_wait_grid_takeover',
decision: 'hold',
desiredCharging: false,
currentA: 6,
takeoverNeeded: true,
edfOnAction: true,
takeoverHelperService: 'turn_on',
},
},
{
id: 'night-advisor-target-stale',
label: 'Nuit HC, SOC 30, advisor cible 22 mais recommandation > 120 min → fallback règles (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: '07/08 20:00',
},
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-advisor-toggle-off',
label: 'Nuit HC, SOC 30, advisor cible 22 mais toggle APPLIQUER OFF → règles inchangées (grid_forced)',
time: NIGHT,
overrides: {
...VEHICLE_NIGHT, soc_percent: '30', allow_grid_hc_night: 'on',
advisor_apply_night_target: 'off',
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',
},
},
// ===== P9 09/08/2026 : charge réseau avant départ (hors HC) + sélecteur véhicule =====
// VW départ 08:50 (helper existant), JEEP départ 06:30 (nouveau helper).
// À 08:00 (morning_solar, PV nul) : VW → fenêtre départ active (50 min) ;
// JEEP → départ passé depuis 90 min → rien.
{
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',
},
},
{
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.
{
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',
},
},
// ================= 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',
},
},
{
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,
},
},
// ===== 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: 50,
},
},
{
id: 'consigne-deja-genereuse-aucune-ecriture',
label: 'Session reseau, consigne a 5600 W pour un besoin de 5295 W : deja suffisante, aucune reecriture (le bruit de la maison ne la fait pas courir)',
time: NIGHT,
overrides: {
...VEHICLE_NIGHT,
soc_percent: '50',
allow_grid_hc_night: 'on',
grid_state: 'on',
victron_acin1_available: '1',
grid_setpoint_w: '5600',
},
expect: {
gridSetpointW: null,
},
},
{
id: 'consigne-insuffisante-corrigee-immediatement',
label: "LE DEFAUT DE LA NUIT DU 01/09 — consigne 45 W sous le besoin : la batterie porterait la difference, correction sans attendre",
time: NIGHT,
overrides: {
...VEHICLE_NIGHT,
soc_percent: '50',
allow_grid_hc_night: 'on',
grid_state: 'on',
victron_acin1_available: '1',
grid_setpoint_w: '5250',
},
expect: {
sourceMode: 'grid_hc_allowed',
gridSetpointW: 5525,
},
},
{
id: 'consigne-derive-externe-corrigee',
label: 'Hors session mais consigne laissee a 4000 W par un tiers : corrigee au cycle suivant',
time: MORNING,
overrides: {
soc_percent: '60',
grid_state: 'off',
victron_acin1_available: '0',
grid_setpoint_w: '4000',
},
expect: {
gridSetpointW: 50,
},
},
];