Le parc se declare, la politique batterie suit

Quatre US5000 arrivent aujourd'hui. Les bandes de politique batterie sont
en amperes ABSOLUS (souple -50 A / dur -100 A au-dessus de 90 % de SOC,
etc.) et decrivent le parc de 4 modules sur lequel elles sont en vigueur
-- l'arret dur du 11/09 a mordu au bon endroit, 87 A pour une limite de
70. Les garder telles quelles briderait un parc trois fois plus gros au
niveau de l'ancien.

Deux lectures, aucune table reecrite :
- ECHELLE : le parc DECLARE (`evcs_balance_modules_en_service`, en
  equivalents US2000C -- un US5000 en vaut DEUX), le meme helper que la
  capacite utile et que le bilan energetique. Une seule valeur a tenir a
  jour, et les trois consommateurs restent forcement d'accord.
- PLAFOND : ce que le BMS declare accepter a l'instant
  (`maximum_allowed_discharge_current` / `_charge_current`, nouvellement
  lus dans le snapshot). On ne s'autorise jamais plus.

⚠️ On ne met PAS a l'echelle sur la limite BMS lue en direct : elle
s'effondre quand une cellule monte en tension (04/09 : plafond de charge
de 150 a 20 A en cinq secondes). Une regle de confort ne suit pas un
signal aussi nerveux ; elle se plafonne dessus, c'est tout.

Parc absent ou BMS illisible => echelle 1, plafond inactif, comportement
inchange. C'est pourquoi les 95 scenarios existants passent sans
modification.

Golden 95 -> 98. Mutation : echelle neutralisee -> seul
`parc-double-decharge-toleree` tombe ; plafond BMS ignore -> seul
`parc-double-plafond-bms` tombe.

Verifie en production : execution 05:28:30 en succes, le gabarit rend
bms_max_discharge_current_a=100, bms_max_charge_current_a=100,
modules_en_service=4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
Tallagrand Thierry
2026-09-12 05:29:07 +02:00
parent 2493b5baa1
commit 118d63b882
4 changed files with 131 additions and 20 deletions

View File

@@ -63,6 +63,9 @@
"battery_neutral_band_a": states("input_number.victron_evcs_battery_neutral_band_a"),
"soc_start_min": states("input_number.victron_evcs_soc_start_min"),
"modules_en_service": states("input_number.evcs_balance_modules_en_service"),
"bms_max_discharge_current_a": states("sensor.airtria_8_us2000c_maximum_allowed_discharge_current"),
"bms_max_charge_current_a": states("sensor.airtria_8_us2000c_maximum_allowed_charge_current"),
"soc_stop_min": states("input_number.victron_evcs_soc_stop_min"),
"current_min_a": states("input_number.victron_evcs_current_min_a"),
"current_max_auto_a": states("input_number.victron_evcs_current_max_auto_a"),

View File

@@ -664,6 +664,44 @@ const history = {
evcsNoDemandFilterEvalMs: parseParisStampToMs(evcsNoDemandFilterEvalAtRaw),
};
// =====================================================
// MISE A L'ECHELLE DU PARC (12/09/2026)
// =====================================================
// Les bandes ci-dessous sont en AMPERES ABSOLUS, et elles decrivent un parc
// de 4 modules -- celui sur lequel elles sont aujourd'hui en vigueur et
// observees justes (l'arret dur du 11/09 a mordu au bon endroit, 87 A pour
// une limite de 70). Avec quatre US5000 en plus, le parc triple : garder ces
// ampere-la briderait le nouveau parc au niveau de l'ancien.
//
// On les met donc a l'echelle du parc DECLARE — le meme helper que la
// capacite utile et que le bilan energetique, exprime en equivalents
// US2000C (un US5000 en vaut DEUX). Une seule valeur a tenir a jour, et les
// trois consommateurs restent forcement d'accord entre eux.
//
// ⚠️ On ne met PAS a l'echelle sur la limite BMS lue en direct : elle
// s'effondre quand une cellule monte en tension (04/09 : plafond de charge
// tombe de 150 a 20 A en cinq secondes). Une regle de confort ne doit pas
// suivre un signal aussi nerveux. En revanche le BMS PLAFONNE : on ne
// s'autorise jamais plus que ce que le parc declare accepter a l'instant.
//
// Parc absent ou illisible => echelle 1 et plafond inactif, donc comportement
// d'aujourd'hui strictement inchange.
const parkScale = (() => {
const n = Number(input.modules_en_service);
return Number.isFinite(n) && n >= 1 && n <= 32 ? n / 4 : 1;
})();
const bmsDischargeCapA = (() => {
const n = Number(input.bms_max_discharge_current_a);
return Number.isFinite(n) && n > 0 ? n : Infinity;
})();
const bmsChargeCapA = (() => {
const n = Number(input.bms_max_charge_current_a);
return Number.isFinite(n) && n > 0 ? n : Infinity;
})();
// Decharge : valeurs NEGATIVES, donc le plafond est un `Math.max`.
const scaleDischargeA = v => Math.max(-bmsDischargeCapA, v * parkScale);
const scaleChargeA = v => Math.min(bmsChargeCapA, Math.max(0, v) * parkScale);
// =====================================================
// BATTERY TUNING
// =====================================================
@@ -681,54 +719,54 @@ const batteryTune = {
bands: {
gt90: {
dischargeSoftA: num(input.batt_discharge_soft_gt90_a, -50),
dischargeHardA: num(input.batt_discharge_hard_gt90_a, -100),
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_gt90_a, 5)),
dischargeSoftA: scaleDischargeA(num(input.batt_discharge_soft_gt90_a, -50)),
dischargeHardA: scaleDischargeA(num(input.batt_discharge_hard_gt90_a, -100)),
chargeReserveA: scaleChargeA(num(input.batt_charge_reserve_gt90_a, 5)),
softToleranceMin: Math.max(0, num(input.batt_soft_tol_gt90_min, 12)),
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_gt90_sec, 20)),
recoveryMin: Math.max(0, num(input.batt_recovery_gt90_min, 6)),
},
'80_90': {
dischargeSoftA: num(input.batt_discharge_soft_80_90_a, -40),
dischargeHardA: num(input.batt_discharge_hard_80_90_a, -80),
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_80_90_a, 10)),
dischargeSoftA: scaleDischargeA(num(input.batt_discharge_soft_80_90_a, -40)),
dischargeHardA: scaleDischargeA(num(input.batt_discharge_hard_80_90_a, -80)),
chargeReserveA: scaleChargeA(num(input.batt_charge_reserve_80_90_a, 10)),
softToleranceMin: Math.max(0, num(input.batt_soft_tol_80_90_min, 10)),
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_80_90_sec, 20)),
recoveryMin: Math.max(0, num(input.batt_recovery_80_90_min, 6)),
},
'60_80': {
dischargeSoftA: num(input.batt_discharge_soft_60_80_a, -20),
dischargeHardA: num(input.batt_discharge_hard_60_80_a, -50),
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_60_80_a, 40)),
dischargeSoftA: scaleDischargeA(num(input.batt_discharge_soft_60_80_a, -20)),
dischargeHardA: scaleDischargeA(num(input.batt_discharge_hard_60_80_a, -50)),
chargeReserveA: scaleChargeA(num(input.batt_charge_reserve_60_80_a, 40)),
softToleranceMin: Math.max(0, num(input.batt_soft_tol_60_80_min, 6)),
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_60_80_sec, 15)),
recoveryMin: Math.max(0, num(input.batt_recovery_60_80_min, 8)),
},
'40_60': {
dischargeSoftA: num(input.batt_discharge_soft_40_60_a, -12),
dischargeHardA: num(input.batt_discharge_hard_40_60_a, -30),
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_40_60_a, 70)),
dischargeSoftA: scaleDischargeA(num(input.batt_discharge_soft_40_60_a, -12)),
dischargeHardA: scaleDischargeA(num(input.batt_discharge_hard_40_60_a, -30)),
chargeReserveA: scaleChargeA(num(input.batt_charge_reserve_40_60_a, 70)),
softToleranceMin: Math.max(0, num(input.batt_soft_tol_40_60_min, 4)),
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_40_60_sec, 10)),
recoveryMin: Math.max(0, num(input.batt_recovery_40_60_min, 10)),
},
'25_40': {
dischargeSoftA: num(input.batt_discharge_soft_25_40_a, -5),
dischargeHardA: num(input.batt_discharge_hard_25_40_a, -15),
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_25_40_a, 110)),
dischargeSoftA: scaleDischargeA(num(input.batt_discharge_soft_25_40_a, -5)),
dischargeHardA: scaleDischargeA(num(input.batt_discharge_hard_25_40_a, -15)),
chargeReserveA: scaleChargeA(num(input.batt_charge_reserve_25_40_a, 110)),
softToleranceMin: Math.max(0, num(input.batt_soft_tol_25_40_min, 2)),
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_25_40_sec, 5)),
recoveryMin: Math.max(0, num(input.batt_recovery_25_40_min, 12)),
},
lt25: {
dischargeSoftA: num(input.batt_discharge_soft_lt25_a, -3),
dischargeHardA: num(input.batt_discharge_hard_lt25_a, -8),
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_lt25_a, 150)),
dischargeSoftA: scaleDischargeA(num(input.batt_discharge_soft_lt25_a, -3)),
dischargeHardA: scaleDischargeA(num(input.batt_discharge_hard_lt25_a, -8)),
chargeReserveA: scaleChargeA(num(input.batt_charge_reserve_lt25_a, 150)),
softToleranceMin: Math.max(0, num(input.batt_soft_tol_lt25_min, 1)),
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_lt25_sec, 2)),
recoveryMin: Math.max(0, num(input.batt_recovery_lt25_min, 15)),

View File

@@ -2439,4 +2439,74 @@ export const scenarios = [
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',
},
},
];

File diff suppressed because one or more lines are too long