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:
@@ -63,6 +63,9 @@
|
|||||||
"battery_neutral_band_a": states("input_number.victron_evcs_battery_neutral_band_a"),
|
"battery_neutral_band_a": states("input_number.victron_evcs_battery_neutral_band_a"),
|
||||||
"soc_start_min": states("input_number.victron_evcs_soc_start_min"),
|
"soc_start_min": states("input_number.victron_evcs_soc_start_min"),
|
||||||
"modules_en_service": states("input_number.evcs_balance_modules_en_service"),
|
"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"),
|
"soc_stop_min": states("input_number.victron_evcs_soc_stop_min"),
|
||||||
"current_min_a": states("input_number.victron_evcs_current_min_a"),
|
"current_min_a": states("input_number.victron_evcs_current_min_a"),
|
||||||
"current_max_auto_a": states("input_number.victron_evcs_current_max_auto_a"),
|
"current_max_auto_a": states("input_number.victron_evcs_current_max_auto_a"),
|
||||||
|
|||||||
@@ -664,6 +664,44 @@ const history = {
|
|||||||
evcsNoDemandFilterEvalMs: parseParisStampToMs(evcsNoDemandFilterEvalAtRaw),
|
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
|
// BATTERY TUNING
|
||||||
// =====================================================
|
// =====================================================
|
||||||
@@ -681,54 +719,54 @@ const batteryTune = {
|
|||||||
|
|
||||||
bands: {
|
bands: {
|
||||||
gt90: {
|
gt90: {
|
||||||
dischargeSoftA: num(input.batt_discharge_soft_gt90_a, -50),
|
dischargeSoftA: scaleDischargeA(num(input.batt_discharge_soft_gt90_a, -50)),
|
||||||
dischargeHardA: num(input.batt_discharge_hard_gt90_a, -100),
|
dischargeHardA: scaleDischargeA(num(input.batt_discharge_hard_gt90_a, -100)),
|
||||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_gt90_a, 5)),
|
chargeReserveA: scaleChargeA(num(input.batt_charge_reserve_gt90_a, 5)),
|
||||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_gt90_min, 12)),
|
softToleranceMin: Math.max(0, num(input.batt_soft_tol_gt90_min, 12)),
|
||||||
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_gt90_sec, 20)),
|
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_gt90_sec, 20)),
|
||||||
recoveryMin: Math.max(0, num(input.batt_recovery_gt90_min, 6)),
|
recoveryMin: Math.max(0, num(input.batt_recovery_gt90_min, 6)),
|
||||||
},
|
},
|
||||||
|
|
||||||
'80_90': {
|
'80_90': {
|
||||||
dischargeSoftA: num(input.batt_discharge_soft_80_90_a, -40),
|
dischargeSoftA: scaleDischargeA(num(input.batt_discharge_soft_80_90_a, -40)),
|
||||||
dischargeHardA: num(input.batt_discharge_hard_80_90_a, -80),
|
dischargeHardA: scaleDischargeA(num(input.batt_discharge_hard_80_90_a, -80)),
|
||||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_80_90_a, 10)),
|
chargeReserveA: scaleChargeA(num(input.batt_charge_reserve_80_90_a, 10)),
|
||||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_80_90_min, 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)),
|
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)),
|
recoveryMin: Math.max(0, num(input.batt_recovery_80_90_min, 6)),
|
||||||
},
|
},
|
||||||
|
|
||||||
'60_80': {
|
'60_80': {
|
||||||
dischargeSoftA: num(input.batt_discharge_soft_60_80_a, -20),
|
dischargeSoftA: scaleDischargeA(num(input.batt_discharge_soft_60_80_a, -20)),
|
||||||
dischargeHardA: num(input.batt_discharge_hard_60_80_a, -50),
|
dischargeHardA: scaleDischargeA(num(input.batt_discharge_hard_60_80_a, -50)),
|
||||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_60_80_a, 40)),
|
chargeReserveA: scaleChargeA(num(input.batt_charge_reserve_60_80_a, 40)),
|
||||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_60_80_min, 6)),
|
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)),
|
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)),
|
recoveryMin: Math.max(0, num(input.batt_recovery_60_80_min, 8)),
|
||||||
},
|
},
|
||||||
|
|
||||||
'40_60': {
|
'40_60': {
|
||||||
dischargeSoftA: num(input.batt_discharge_soft_40_60_a, -12),
|
dischargeSoftA: scaleDischargeA(num(input.batt_discharge_soft_40_60_a, -12)),
|
||||||
dischargeHardA: num(input.batt_discharge_hard_40_60_a, -30),
|
dischargeHardA: scaleDischargeA(num(input.batt_discharge_hard_40_60_a, -30)),
|
||||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_40_60_a, 70)),
|
chargeReserveA: scaleChargeA(num(input.batt_charge_reserve_40_60_a, 70)),
|
||||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_40_60_min, 4)),
|
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)),
|
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)),
|
recoveryMin: Math.max(0, num(input.batt_recovery_40_60_min, 10)),
|
||||||
},
|
},
|
||||||
|
|
||||||
'25_40': {
|
'25_40': {
|
||||||
dischargeSoftA: num(input.batt_discharge_soft_25_40_a, -5),
|
dischargeSoftA: scaleDischargeA(num(input.batt_discharge_soft_25_40_a, -5)),
|
||||||
dischargeHardA: num(input.batt_discharge_hard_25_40_a, -15),
|
dischargeHardA: scaleDischargeA(num(input.batt_discharge_hard_25_40_a, -15)),
|
||||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_25_40_a, 110)),
|
chargeReserveA: scaleChargeA(num(input.batt_charge_reserve_25_40_a, 110)),
|
||||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_25_40_min, 2)),
|
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)),
|
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)),
|
recoveryMin: Math.max(0, num(input.batt_recovery_25_40_min, 12)),
|
||||||
},
|
},
|
||||||
|
|
||||||
lt25: {
|
lt25: {
|
||||||
dischargeSoftA: num(input.batt_discharge_soft_lt25_a, -3),
|
dischargeSoftA: scaleDischargeA(num(input.batt_discharge_soft_lt25_a, -3)),
|
||||||
dischargeHardA: num(input.batt_discharge_hard_lt25_a, -8),
|
dischargeHardA: scaleDischargeA(num(input.batt_discharge_hard_lt25_a, -8)),
|
||||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_lt25_a, 150)),
|
chargeReserveA: scaleChargeA(num(input.batt_charge_reserve_lt25_a, 150)),
|
||||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_lt25_min, 1)),
|
softToleranceMin: Math.max(0, num(input.batt_soft_tol_lt25_min, 1)),
|
||||||
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_lt25_sec, 2)),
|
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_lt25_sec, 2)),
|
||||||
recoveryMin: Math.max(0, num(input.batt_recovery_lt25_min, 15)),
|
recoveryMin: Math.max(0, num(input.batt_recovery_lt25_min, 15)),
|
||||||
|
|||||||
@@ -2439,4 +2439,74 @@ export const scenarios = [
|
|||||||
sessionCommittedService: null,
|
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
Reference in New Issue
Block a user