L Auto Charge Manager a enfin un filet

Troisieme workflow structurant, et le seul sans tests : c est lui qui decide ce
que la batterie a le droit de prendre, donc lui qui plafonnait le parc a 93 %,
et il n etait meme pas suivi dans le depot avant ce matin.

Vingt scenarios sur le patron de la suite hysteresis, avec le vrai code du node
dans une sandbox et un snapshot capture contre Home Assistant plutot
qu invente — la lecon du 24/08, quand un fixture portait les valeurs de repli du
code au lieu des seuils de production et masquait un bug de bande.

Ils suivent l ordre reel des branches et enferment chaque paire dans son
contrefactuel : hiver sombre contre hiver ensoleille, ete secours contre ete
repos, rattrapage de midi contre midi ou le soleil revient, override actif
contre override expire, et le takeover demande alors que le contacteur est
ouvert. Le releve reel du maintien a 2 A de cette nuit y figure, ainsi que le
topoff a 52,4 V pose ce matin.

Cinq mutations, chacune tombant sur un scenario different. Suite cablee au
push, bloquante comme les trois autres.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
Tallagrand Thierry
2026-09-06 15:31:32 +02:00
parent 20e0b17d6b
commit 0ba3cb81ee
6 changed files with 408 additions and 0 deletions

View File

@@ -4891,6 +4891,63 @@ jugement conditionne a la POINTE et non a l'energie**, parce qu'une pointe a
2 819 W pendant deux minutes n'a rien d'anodin meme si le total reste faible —
et les incidents techniques.
### Suite de tests pour l'Auto Charge Manager (0 -> 20 scenarios)
Troisieme workflow structurant, et le seul sans filet : il decide ce que la
BATTERIE a le droit de prendre (limites DVCC en courant et en tension), donc
c'est lui qui plafonnait le parc a 93 % — et il n'etait meme pas suivi dans le
depot avant ce matin.
`tests/golden-acm/`, sur le patron de `tests/golden-hysteresis/` : le VRAI code
du node « Compute Charge Targets » execute dans une sandbox `vm` avec horloge
epinglee, sur un **snapshot reel** obtenu en rendant le gabarit du node
« HA Snapshot via Template » contre Home Assistant (20 champs). Pas de fixture
invente : c'est la lecon du 24/08, ou le fixture de l'hysteresis portait les
valeurs de REPLI du code (22/35) au lieu des seuils de production (20/30) et
masquait un bug de bande.
Les vingt scenarios suivent l'ordre reel des branches — automatisation
desactivee, secours SOC < 20 hors HC, takeover EVCS, hiver+HC, ete+HC, HC midi,
fenetre jour, hors fenetre, puis l'override qui se superpose a tout :
- gardes : automatisation OFF (reglages conserves), secours a SOC 18 ;
- takeover : recharge vers la cible, **maintien a 2 A (le releve reel du 06/09
a 05:06)**, bornage d'une cible aberrante a 95 %, et le contrefactuel
« takeover demande mais contacteur OUVERT » qui ne doit pas mordre ;
- nuit : paire hiver sombre / hiver ensoleille (cibles 80 et 40 selon la bande
de prevision), paire ete secours / ete repos autour du seuil de 22 % ;
- midi : rattrapage reseau, et son contrefactuel a PV 2 000 W ou la garde
`pvPowerW < 1500` doit couper le rattrapage ;
- jour : boost, mid, **topoff a 52,4 V (le correctif du jour)**, et le hold
au-dessus de 95 % qui redescend a 50,6 V — on atteint le haut, on n'y sejourne
pas ;
- override actif contre override expire ;
- garde de publication : cible identique a l'existant, on n'ecrit rien.
Preuve par mutation, cinq directions, **chacune faisant tomber exactement UN
scenario, different a chaque fois** : tension de topoff ramenee a 51,3 · bornage
de la cible de takeover retire · secours SOC critique supprime · garde PV du
rattrapage midi relachee · echeance de l'override ignoree. Restauration -> 20/20.
Cablee dans `tools/n8n-push.mjs` (`isAcmWorkflow`), bloquante au push comme les
trois autres suites. Verifie : le push de l'ACM execute bien les 20 scenarios
avant le PUT.
### Suivre la conversation depuis le telephone
`claude --remote-control [nom]` est un drapeau de DEMARRAGE : une session deja
lancee ne peut pas etre rattachee apres coup. Il existe aussi
`claude --teleport [session]`. `ListAgents` confirme qu'une session Remote
Control existe deja sur le compte (« Configurer alertes electriques pour demo »,
hors ligne) — le mecanisme est donc en place cote compte.
Pour une session pilotable depuis le telephone, il faut donc la demarrer ainsi
sur le poste, **dans tmux** pour qu'elle survive a la fermeture du terminal :
`tmux new -s evcs 'claude --remote-control "EVCS"'`.
⚠️ La session courante n'a PAS ete demarree avec ce drapeau : elle ne sera pas
joignable depuis le telephone. Et une session Claude, meme pilotable a distance,
reste un poste de travail — ce n'est pas elle qui doit porter la surveillance de
nuit, d'ou le rapport par courriel loge dans le service d'historisation.
### Reste ouvert
- Detecteur de gel dans le service d'historisation : une grandeur qui ne bouge
pas alors qu'une autre du meme bilan bouge = alerte. Non fait.

View File

@@ -440,3 +440,11 @@ heure_reelle,etat,courant_mA,c1_mV,c2_mV,c3_mV,c4_mV,c5_mV,c6_mV,c7_mV,c8_mV,c9_
2026-09-06T13:11:09.944Z,Y230202C30371425,,3328,3327,3327,3328,3328,3327,3327,3326,3327,3328,3328,3327,3326,3327,3327,2
2026-09-06T13:11:10.099Z,Y230202C30371431,,3323,3323,3324,3324,3325,3323,3323,3324,3324,3322,3324,3323,3324,3324,3325,3
2026-09-06T13:11:10.225Z,Y230202C30371427,,3328,3327,3327,3327,3326,3328,3326,3326,3327,3328,3328,3327,3328,3328,3326,2
2026-09-06T13:21:10.328Z,Y230202C30371426,,3320,3321,3321,3322,3322,3320,3321,3322,3319,3322,3321,3321,3322,3322,3322,3
2026-09-06T13:21:10.453Z,Y230202C30371425,,3363,3365,3363,3368,3364,3364,3365,3362,3366,3365,3364,3365,3358,3365,3363,10
2026-09-06T13:21:10.548Z,Y230202C30371431,,3366,3367,3367,3367,3366,3366,3366,3367,3367,3354,3366,3366,3367,3366,3366,13
2026-09-06T13:21:10.674Z,Y230202C30371427,,3368,3365,3369,3368,3361,3369,3366,3367,3370,3370,3367,3368,3371,3371,3364,10
2026-09-06T13:31:10.763Z,Y230202C30371426,,3365,3367,3367,3367,3366,3366,3367,3367,3355,3366,3366,3367,3367,3367,3365,12
2026-09-06T13:31:10.870Z,Y230202C30371425,,3312,3312,3312,3312,3311,3311,3312,3311,3312,3312,3312,3312,3311,3312,3311,1
2026-09-06T13:31:10.997Z,Y230202C30371431,,3312,3312,3312,3312,3312,3312,3312,3312,3312,3310,3312,3312,3312,3312,3312,2
2026-09-06T13:31:11.122Z,Y230202C30371427,,3301,3302,3302,3302,3300,3300,3302,3302,3302,3301,3301,3303,3303,3303,3300,3
1 heure_reelle etat courant_mA c1_mV c2_mV c3_mV c4_mV c5_mV c6_mV c7_mV c8_mV c9_mV c10_mV c11_mV c12_mV c13_mV c14_mV c15_mV ecart_mV
440 2026-09-06T13:11:09.944Z Y230202C30371425 3328 3327 3327 3328 3328 3327 3327 3326 3327 3328 3328 3327 3326 3327 3327 2
441 2026-09-06T13:11:10.099Z Y230202C30371431 3323 3323 3324 3324 3325 3323 3323 3324 3324 3322 3324 3323 3324 3324 3325 3
442 2026-09-06T13:11:10.225Z Y230202C30371427 3328 3327 3327 3327 3326 3328 3326 3326 3327 3328 3328 3327 3328 3328 3326 2
443 2026-09-06T13:21:10.328Z Y230202C30371426 3320 3321 3321 3322 3322 3320 3321 3322 3319 3322 3321 3321 3322 3322 3322 3
444 2026-09-06T13:21:10.453Z Y230202C30371425 3363 3365 3363 3368 3364 3364 3365 3362 3366 3365 3364 3365 3358 3365 3363 10
445 2026-09-06T13:21:10.548Z Y230202C30371431 3366 3367 3367 3367 3366 3366 3366 3367 3367 3354 3366 3366 3367 3366 3366 13
446 2026-09-06T13:21:10.674Z Y230202C30371427 3368 3365 3369 3368 3361 3369 3366 3367 3370 3370 3367 3368 3371 3371 3364 10
447 2026-09-06T13:31:10.763Z Y230202C30371426 3365 3367 3367 3367 3366 3366 3367 3367 3355 3366 3366 3367 3367 3367 3365 12
448 2026-09-06T13:31:10.870Z Y230202C30371425 3312 3312 3312 3312 3311 3311 3312 3311 3312 3312 3312 3312 3311 3312 3311 1
449 2026-09-06T13:31:10.997Z Y230202C30371431 3312 3312 3312 3312 3312 3312 3312 3312 3312 3310 3312 3312 3312 3312 3312 2
450 2026-09-06T13:31:11.122Z Y230202C30371427 3301 3302 3302 3302 3300 3300 3302 3302 3302 3301 3301 3303 3303 3303 3300 3

View File

@@ -0,0 +1,22 @@
{
"auto_enabled": "on",
"battery_current_a": "-3.5999999046325684",
"battery_voltage_v": "50.4900016784668",
"current_max_charge_current_a": "30",
"current_max_charge_voltage_v": "50.6",
"edf_switch": "off",
"last_signature": "2026-09-06|day_pv_hold|pv_hold|i=30|v=50.6",
"night_takeover": "off",
"night_target_soc": "35.0",
"notifications_enabled": "on",
"override_current_a": "113.0",
"override_enabled": "off",
"override_profile": "manual",
"override_reason": "test override",
"override_until": "2026-03-16 01:00:00",
"override_voltage_v": "51.3",
"pv_power_w": "4740.237852870943",
"soc_percent": "97.0",
"today_remaining_kwh": "22.27",
"tomorrow_kwh": "29.909"
}

113
tests/golden-acm/run.mjs Normal file
View File

@@ -0,0 +1,113 @@
#!/usr/bin/env node
// Suite de tests GOLDEN du workflow "Victron - Auto Charge Manager V1.2".
// Execute le VRAI code du node "Compute Charge Targets" sur un snapshot HA reel
// modifie par scenario, avec horloge simulee.
//
// Ce workflow decide ce que la batterie a le droit de PRENDRE (limites DVCC en
// courant et en tension). Il gouvernait donc le plafond a 93 % de SOC corrige
// le 06/09, et il n'avait aucun test : il n'etait meme pas suivi dans le depot.
//
// Usage :
// node tests/golden-acm/run.mjs # assertions (exit 1 si echec)
// node tests/golden-acm/run.mjs --print # affiche les resumes obtenus
// node tests/golden-acm/run.mjs --only=topoff
//
// REGLE : cette suite DOIT passer avant tout push du workflow
// (integree a tools/n8n-push.mjs).
import fs from 'fs';
import vm from 'vm';
import path from 'path';
import { fileURLToPath } from 'url';
import { scenarios } from './scenarios.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '../..');
const WORKFLOW_FILE = path.join(ROOT, 'workflows/victron-auto-charge-manager-v1.2.json');
const PRINT = process.argv.includes('--print');
const only = (process.argv.find((a) => a.startsWith('--only=')) || '').slice(7);
const wf = JSON.parse(fs.readFileSync(WORKFLOW_FILE, 'utf8'));
const fixture = JSON.parse(fs.readFileSync(path.join(__dirname, 'fixture-snapshot.json'), 'utf8'));
const nodeCode = wf.nodes.find((n) => n.name === 'Compute Charge Targets').parameters.jsCode;
// Le node lit l'heure par new Date() sans argument ; les autres usages de Date
// prennent des arguments et ne doivent PAS etre epingles.
function makeFakeDate(fixedMs) {
return class FakeDate extends Date {
constructor(...a) { if (a.length === 0) super(fixedMs); else super(...a); }
static now() { return fixedMs; }
};
}
function runNode(jsCode, inputJson, fixedMs) {
const ctx = {
$input: { first: () => ({ json: inputJson }) },
console, Intl, Math, Number, Object, String, JSON, Array, Boolean, RegExp,
Date: makeFakeDate(fixedMs),
};
vm.createContext(ctx);
return vm.runInContext(`(function(){\n${jsCode}\n})()`, ctx)[0].json;
}
function summarize(out) {
const c = out.computed || {};
const d = out.decision || {};
return {
mode: d.mode,
profil: d.effective_profile,
courantA: d.target_current_a,
tensionV: d.target_voltage_v,
alerte: !!d.critical_alert,
publieCourant: !!d.should_publish_current,
publieTension: !!d.should_publish_voltage,
overrideActif: !!c.override_active,
};
}
let echecs = 0, joues = 0;
for (const sc of scenarios) {
if (only && !sc.id.includes(only)) continue;
joues++;
const fixedMs = new Date(sc.time).getTime();
if (!Number.isFinite(fixedMs)) throw new Error(`${sc.id} : time invalide`);
const raw = { ...fixture, ...sc.overrides };
let out;
try {
out = runNode(nodeCode, { data: JSON.stringify(raw) }, fixedMs);
} catch (e) {
echecs++; console.log(`💥 ${sc.id} : CRASH — ${e.message}`); continue;
}
const got = summarize(out);
if (PRINT) {
console.log(`\n─── ${sc.id} | ${sc.label}`);
console.log(' expect:', JSON.stringify(got));
if (out.decision?.short_reason) console.log(' raison:', out.decision.short_reason);
continue;
}
const diffs = [];
for (const [k, v] of Object.entries(sc.expect || {})) {
if (JSON.stringify(got[k]) !== JSON.stringify(v)) {
diffs.push(`${k} : attendu ${JSON.stringify(v)}, obtenu ${JSON.stringify(got[k])}`);
}
}
if (diffs.length) {
echecs++;
console.log(`❌ ${sc.id} | ${sc.label}`);
for (const d of diffs) console.log(` ${d}`);
if (out.decision?.short_reason) console.log(` raison: ${out.decision.short_reason}`);
} else {
console.log(`✅ ${sc.id}`);
}
}
if (!PRINT) {
console.log(echecs
? `\n${echecs}/${joues} ÉCHEC(S) — NE PAS POUSSER EN PRODUCTION`
: `\nGOLDEN ACM OK — ${joues}/${joues} scénarios conformes`);
process.exit(echecs ? 1 : 0);
}

View File

@@ -0,0 +1,206 @@
// Scenarios golden pour le node "Compute Charge Targets"
// (workflow "Victron - Auto Charge Manager V1.2", id cTeeHy4ePiVeELLh).
//
// Ce workflow decide ce que la BATTERIE a le droit de prendre : les limites
// DVCC en courant et en tension. Il gouverne donc le haut de la reserve — c'est
// lui qui plafonnait le parc a 93 % de SOC jusqu'au 06/09 — et il n'avait aucun
// test : il n'etait meme pas suivi dans le depot.
//
// Le fixture est un snapshot REEL, capture le 06/09/2026 en rendant le gabarit
// du node "HA Snapshot via Template" contre Home Assistant. Ne pas le remplacer
// par des valeurs inventees : c'est la lecon du 24/08, ou le fixture de
// l'hysteresis portait les valeurs de REPLI du code (22/35) au lieu des seuils
// de production (20/30), ce qui masquait un bug de bande.
//
// ⚠️ ORDRE DES BRANCHES, a garder en tete en lisant les attendus :
// 1. automatisation desactivee
// 2. secours SOC < 20 hors fenetres HC
// 3. takeover EVCS nuit (recover / hold)
// 4. hiver + HC nuit
// 5. ete + HC nuit
// 6. HC midi (rattrapage, seulement si SOC < 85 ET forecast < 8 ET PV < 1500)
// 7. fenetre jour (boost / mid / topoff / hold selon le SOC)
// 8. hors fenetre
// puis l'override se superpose a tout.
const ETE = '2026-07-15';
const HIVER = '2026-01-15';
const h = (jour, hhmm) => `${jour}T${hhmm}:00+02:00`;
const hHiver = (hhmm) => `${HIVER}T${hhmm}:00+01:00`;
// Le parc a quatre modules du 06/09 : le BMS autorise 53,2 V, et nos profils
// restent tous en dessous.
const parcNeuf = { soc_percent: '60', battery_voltage_v: '51.0', battery_current_a: '20' };
export const scenarios = [
// ---------------------------------------------------------------- gardes
{
id: 'acm-automatisation-desactivee',
label: "Toggle d'automatisation OFF : les reglages en place sont conserves tels quels",
time: h(ETE, '11:00'),
overrides: { ...parcNeuf, auto_enabled: 'off',
current_max_charge_current_a: '77', current_max_charge_voltage_v: '50.9' },
expect: { mode: 'automation_disabled', profil: 'manual_keep_current',
courantA: 77, tensionV: 50.9, publieCourant: false, publieTension: false },
},
{
id: 'acm-secours-soc-critique',
label: 'SOC 18 % hors fenetres HC : secours a 60 A / 50,4 V et alerte critique',
time: h(ETE, '10:00'),
overrides: { ...parcNeuf, soc_percent: '18' },
expect: { mode: 'emergency_rescue', profil: 'emergency_rescue',
courantA: 60, tensionV: 50.4, alerte: true },
},
// ------------------------------------------------------- takeover de nuit
{
id: 'acm-takeover-recharge-vers-la-cible',
label: 'Takeover EVCS, EDF ferme, SOC 30 sous la cible 50 : recharge a 30 A',
time: h(ETE, '02:30'),
overrides: { ...parcNeuf, soc_percent: '30', night_takeover: 'on',
edf_switch: 'on', night_target_soc: '50' },
expect: { mode: 'evcs_takeover_recover', profil: 'night_slow',
courantA: 30, tensionV: 50.1 },
},
{
id: 'acm-takeover-maintien-a-la-cible',
label: 'RELEVE 06/09 05:06 : cible atteinte, la batterie est tenue a 2 A pendant que le reseau porte la maison',
time: h(ETE, '05:06'),
overrides: { ...parcNeuf, soc_percent: '55', night_takeover: 'on',
edf_switch: 'on', night_target_soc: '49.4' },
expect: { mode: 'evcs_takeover_hold', profil: 'takeover_hold',
courantA: 2, tensionV: 50.1 },
},
{
id: 'acm-takeover-cible-plafonnee-a-95',
label: 'GARDE : une cible aberrante de 120 % est ramenee a 95, donc SOC 96 tient deja',
time: h(ETE, '03:00'),
overrides: { ...parcNeuf, soc_percent: '96', night_takeover: 'on',
edf_switch: 'on', night_target_soc: '120' },
expect: { mode: 'evcs_takeover_hold', courantA: 2 },
},
{
id: 'acm-takeover-sans-edf-ne-mord-pas',
label: "CONTREFACTUEL : takeover demande mais contacteur OUVERT — on ne bride pas la batterie sur du reseau absent",
time: h(ETE, '03:00'),
overrides: { ...parcNeuf, soc_percent: '30', night_takeover: 'on',
edf_switch: 'off', night_target_soc: '50' },
expect: { mode: 'summer_night_idle' },
},
// ------------------------------------------------------------ nuit HC
{
id: 'acm-hiver-nuit-forecast-sombre',
label: 'Hiver, HC nuit, demain sombre (5 kWh) : on remonte vers 80 %',
time: hHiver('03:00'),
overrides: { ...parcNeuf, soc_percent: '50',
today_remaining_kwh: '0', tomorrow_kwh: '5' },
expect: { mode: 'winter_night_charge_window', profil: 'night_slow', courantA: 30 },
},
{
id: 'acm-hiver-nuit-forecast-ensoleille',
label: 'PAIRE : meme nuit d hiver mais demain ensoleille (30 kWh), cible 40 % deja depassee',
time: hHiver('03:00'),
overrides: { ...parcNeuf, soc_percent: '50',
today_remaining_kwh: '0', tomorrow_kwh: '30' },
expect: { mode: 'winter_night_idle' },
},
{
id: 'acm-ete-nuit-secours',
label: 'Ete, HC nuit, SOC 20 sous le seuil de 22 : secours',
time: h(ETE, '03:00'),
overrides: { ...parcNeuf, soc_percent: '20' },
expect: { mode: 'summer_night_rescue_window', profil: 'night_slow' },
},
{
id: 'acm-ete-nuit-repos',
label: 'PAIRE : meme nuit d ete a SOC 50, aucun besoin reseau',
time: h(ETE, '03:00'),
overrides: { ...parcNeuf, soc_percent: '50' },
expect: { mode: 'summer_night_idle' },
},
// ------------------------------------------------------------ midi HC
{
id: 'acm-midi-rattrapage',
label: 'HC midi, SOC 60, journee sombre (5 kWh) et PV a 800 W : rattrapage reseau a 30 A',
time: h(ETE, '13:00'),
overrides: { ...parcNeuf, soc_percent: '60', today_remaining_kwh: '5', pv_power_w: '800' },
expect: { mode: 'midday_catchup_window', profil: 'midday_grid_catchup',
courantA: 30, tensionV: 50.8 },
},
{
id: 'acm-midi-pas-de-rattrapage-si-le-soleil-revient',
label: 'CONTREFACTUEL : meme midi mais PV a 2000 W — le rattrapage ne mord plus, on repasse en profil jour',
time: h(ETE, '13:00'),
overrides: { ...parcNeuf, soc_percent: '60', today_remaining_kwh: '5', pv_power_w: '2000' },
expect: { mode: 'day_pv_boost', profil: 'pv_boost' },
},
// ------------------------------------------------------------ jour
{
id: 'acm-jour-boost',
label: 'Jour, SOC 60 sous 80 : capture PV maximale, 130 A a 51,5 V',
time: h(ETE, '11:00'),
overrides: { ...parcNeuf, soc_percent: '60', pv_power_w: '4000' },
expect: { mode: 'day_pv_boost', profil: 'pv_boost', courantA: 130, tensionV: 51.5 },
},
{
id: 'acm-jour-mid',
label: 'Jour, SOC 85 entre 80 et 90 : 110 A a 51,2 V',
time: h(ETE, '11:00'),
overrides: { ...parcNeuf, soc_percent: '85', pv_power_w: '4000', battery_voltage_v: '50.5' },
expect: { mode: 'day_pv_mid', profil: 'pv_mid', courantA: 110, tensionV: 51.2 },
},
{
id: 'acm-jour-topoff-52v4',
label: "LE CORRECTIF DU 06/09 : SOC 93 dans la tranche 90-95, la tension monte a 52,4 V et le parc reprend de la charge",
time: h(ETE, '15:00'),
overrides: { ...parcNeuf, soc_percent: '93', pv_power_w: '4000', battery_voltage_v: '51.1' },
expect: { mode: 'day_pv_topoff', profil: 'pv_topoff', courantA: 80, tensionV: 52.4 },
},
{
id: 'acm-jour-hold-au-dessus-de-95',
label: "GARDE : au-dessus de 95 % on redescend a 50,6 V — on atteint le haut, on n'y sejourne pas",
time: h(ETE, '15:00'),
overrides: { ...parcNeuf, soc_percent: '96', pv_power_w: '4000' },
expect: { mode: 'day_pv_hold', profil: 'pv_hold', courantA: 30, tensionV: 50.6 },
},
{
id: 'acm-hors-fenetre',
label: 'Hors fenetre utile (21 h en ete) : profil de maintien',
time: h(ETE, '21:00'),
overrides: { ...parcNeuf, soc_percent: '70' },
expect: { mode: 'off_window_idle', profil: 'pv_hold', courantA: 30, tensionV: 50.6 },
},
// ------------------------------------------------------------ override
{
id: 'acm-override-manuel-actif',
label: 'Override manuel valide : les valeurs saisies prennent la main sur le profil calcule',
time: h(ETE, '11:00'),
overrides: { ...parcNeuf, soc_percent: '60', override_enabled: 'on',
override_profile: 'manual', override_current_a: '12', override_voltage_v: '50.0',
override_until: '2026-07-15 23:00:00' },
expect: { overrideActif: true, profil: 'override_manual', courantA: 12, tensionV: 50 },
},
{
id: 'acm-override-expire-sans-effet',
label: "CONTREFACTUEL : meme override mais l'echeance est passee — le profil calcule reprend",
time: h(ETE, '11:00'),
overrides: { ...parcNeuf, soc_percent: '60', override_enabled: 'on',
override_profile: 'manual', override_current_a: '12', override_voltage_v: '50.0',
override_until: '2026-07-15 09:00:00' },
expect: { overrideActif: false, mode: 'day_pv_boost', courantA: 130 },
},
// ------------------------------------------------------------ publication
{
id: 'acm-pas-de-publication-si-identique',
label: "GARDE : cible egale a ce qui est deja regle — on n'ecrit ni le courant ni la tension",
time: h(ETE, '21:00'),
overrides: { ...parcNeuf, soc_percent: '70',
current_max_charge_current_a: '30', current_max_charge_voltage_v: '50.6' },
expect: { publieCourant: false, publieTension: false },
},
];

View File

@@ -109,6 +109,7 @@ console.log('Syntaxe OK sur tous les nodes jsCode.');
const isEvcsWorkflow = path.basename(INPUT) === 'evcs-surplus-controller-v2.6.json';
const isAdvisorWorkflow = path.basename(INPUT) === 'evcs-advisor-shadow-v1.json';
const isHysteresisWorkflow = path.basename(INPUT) === 'victron-grid-hysteresis-v1.json';
const isAcmWorkflow = path.basename(INPUT) === 'victron-auto-charge-manager-v1.2.json';
const isBalanceWorkflow = path.basename(INPUT) === 'evcs-energy-balance-v1.json'
|| path.basename(INPUT) === 'evcs-energy-profiles-daily-v1.json'
|| path.basename(INPUT) === 'evcs-solar-forecast-v1.json';
@@ -117,6 +118,7 @@ const testSuite =
isEvcsWorkflow && fs.existsSync('tests/golden/run.mjs') ? 'tests/golden/run.mjs' :
isAdvisorWorkflow && fs.existsSync('tests/advisor/run.mjs') ? 'tests/advisor/run.mjs' :
isHysteresisWorkflow && fs.existsSync('tests/golden-hysteresis/run.mjs') ? 'tests/golden-hysteresis/run.mjs' :
isAcmWorkflow && fs.existsSync('tests/golden-acm/run.mjs') ? 'tests/golden-acm/run.mjs' :
isBalanceWorkflow && fs.existsSync('tests/energy-balance/run.mjs') ? 'tests/energy-balance/run.mjs' :
null;
if (testSuite) {