Budget voiture de la nuit : ce que la batterie peut donner sans EDF, arret au compteur, choix sans EDF / complete
This commit is contained in:
@@ -421,6 +421,48 @@ if (nightHcActive) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// BUDGET VOITURE DE LA NUIT (30/09/2026, decisions du proprietaire)
|
||||
//
|
||||
// « Definir combien la voiture peut prendre sans utiliser EDF, et brider la
|
||||
// charge a cette valeur ou autoriser une charge complete avec EDF. » Choix du
|
||||
// 30/09 : profil PRUDENT (p80) et plancher du bilan (22 %) ; par defaut « sans
|
||||
// EDF » ; en charge complete, le budget batterie d'abord, EDF pour le reste.
|
||||
//
|
||||
// Budget = la plus petite reserve au-dessus du plancher que la batterie
|
||||
// connaitra entre l'ouverture de la fenetre HC de nuit (ou maintenant si elle
|
||||
// est ouverte) et le debut de la fenetre de nuit SUIVANTE, sur le profil
|
||||
// prudent. Retirer X kWh a l'ouverture abaisse toute la trajectoire de X :
|
||||
// le budget est donc le creux de la trajectoire. L'horizon jusqu'a la nuit
|
||||
// suivante applique « la batterie tient jusqu'a la prochaine HC » ; la
|
||||
// batterie pleine ecrete la recharge (on ne stocke pas au-dela de 100 %).
|
||||
// Le 30/09 01:25 : SOC 57, prudent 44,7 -> ~3,7 kWh.
|
||||
// ---------------------------------------------------------------------
|
||||
let nightVehicleBudgetKwh = null;
|
||||
if (nightHcActive && Number.isFinite(socPct)) {
|
||||
const maintenant = num(input.nowMs, Date.now());
|
||||
const depuis = Math.max(nightHcStartMs, maintenant);
|
||||
const jusqua = nightHcStartMs + 24 * 3600000;
|
||||
const plein = energyBetweenSoc(socFloorPct, 100);
|
||||
let reserve = socPct >= socFloorPct
|
||||
? energyBetweenSoc(socFloorPct, socPct) : -energyBetweenSoc(socPct, socFloorPct);
|
||||
let creux = maintenant >= depuis ? reserve : Infinity;
|
||||
for (const sl of slotsP80) {
|
||||
const debut = num(sl.startMs, NaN);
|
||||
if (!Number.isFinite(debut)) continue;
|
||||
const a = Math.max(debut, maintenant), b = Math.min(debut + 3600000, jusqua);
|
||||
if (b <= a) continue;
|
||||
// La tranche qui contient l'ouverture est coupee en deux : le creux ne se
|
||||
// lit qu'a partir de l'ouverture.
|
||||
const coupes = depuis > a && depuis < b ? [a, depuis, b] : [a, b];
|
||||
for (let i = 0; i + 1 < coupes.length; i++) {
|
||||
reserve = Math.min(plein, reserve + sl.netKwh * (coupes[i + 1] - coupes[i]) / 3600000);
|
||||
if (coupes[i + 1] >= depuis) creux = Math.min(creux, reserve);
|
||||
}
|
||||
}
|
||||
nightVehicleBudgetKwh = Number.isFinite(creux) ? Math.max(0, creux) : null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// VEHICULE BRANCHE EN HC AVEC UN DEPART AVANT LA PROCHAINE HC (28/09/2026)
|
||||
//
|
||||
@@ -702,6 +744,12 @@ return [{
|
||||
houseKwh: nightHcHouseKwh === null ? null : round2(nightHcHouseKwh),
|
||||
},
|
||||
|
||||
// Ce que la voiture peut prendre sur la batterie cette nuit sans EDF (30/09).
|
||||
nightVehicleBudget: {
|
||||
active: nightVehicleBudgetKwh !== null,
|
||||
kwh: nightVehicleBudgetKwh === null ? null : round2(nightVehicleBudgetKwh),
|
||||
},
|
||||
|
||||
// Voiture branchee en HC, depart avant la prochaine HC (28/09).
|
||||
vehicleMidday: {
|
||||
active: vehicleRuleActive,
|
||||
|
||||
@@ -95,6 +95,10 @@ setNumber('input_number.evcs_balance_vehicle_solar_kwh', vm.active ? vm.solarKwh
|
||||
// Maison sur le reseau pendant la fenetre HC de nuit si la session s'engage (29/09).
|
||||
const nh = balance.nightHcImport || {};
|
||||
setNumber('input_number.evcs_balance_night_hc_house_kwh', nh.active && nh.houseKwh !== null ? nh.houseKwh : 0);
|
||||
// Budget voiture de la nuit sans EDF (30/09) : 0 si inconnu — le controleur
|
||||
// ne donne alors rien de la batterie.
|
||||
const nb = balance.nightVehicleBudget || {};
|
||||
setNumber('input_number.evcs_balance_night_vehicle_budget_kwh', nb.active && nb.kwh !== null ? nb.kwh : 0);
|
||||
setNumber('input_number.evcs_balance_energy_to_cover_kwh', c.energyToCoverKwh);
|
||||
setNumber('input_number.evcs_balance_grid_energy_needed_kwh', c.gridEnergyNeededKwh);
|
||||
setNumber('input_number.evcs_balance_vehicle_surplus_kwh', balance.vehicle.surplusAvailableKwh);
|
||||
|
||||
@@ -1127,6 +1127,13 @@ return [{
|
||||
// P12 — passe-plat du verrou anti-oscillateur vers la sortie.
|
||||
nightBatterySurplusExhausted: !!nightPrev.nightBatterySurplusExhausted,
|
||||
nightBatterySurplusAvailable: !!nightPrev.nightBatterySurplusAvailable,
|
||||
// Budget voiture de la nuit (30/09) — passe-plat vers la sortie.
|
||||
nightBudgetKwh: nightPrev.nightBudgetKwh ?? null,
|
||||
nightBudgetDeliveredKwh: nightPrev.nightBudgetDeliveredKwh ?? null,
|
||||
nightBudgetReached: !!nightPrev.nightBudgetReached,
|
||||
nightBudgetToWrite: nightPrev.nightBudgetToWrite ?? null,
|
||||
nightChargeMode: nightPrev.nightChargeMode || 'sans_edf',
|
||||
nightCarWaitsNoGrid: !!nightPrev.nightCarWaitsNoGrid,
|
||||
maxBatteryDischargePoints,
|
||||
gridTakeoverTriggerPct,
|
||||
|
||||
|
||||
@@ -300,6 +300,15 @@ if (manualPriorityOverrideActive && !evcsVehicleNotRequestingPowerState) {
|
||||
// =====================================================
|
||||
const reason = [...decision.reason];
|
||||
|
||||
// Budget voiture de la nuit (30/09) : dire pourquoi la voiture s'arrete.
|
||||
if (night.nightBudgetKwh !== null && night.nightBudgetKwh !== undefined) {
|
||||
const f = (x) => String(Math.round(x * 100) / 100).replace('.', ',');
|
||||
reason.push(`Budget batterie nuit ${f(night.nightBudgetDeliveredKwh ?? 0)}/${f(night.nightBudgetKwh)} kWh` +
|
||||
(night.nightCarWaitsNoGrid ? ' — atteint, sans EDF : la voiture attend le soleil'
|
||||
: night.nightBudgetReached ? ' — atteint, EDF finit la charge' : ''));
|
||||
} else if (night.nightCarWaitsNoGrid) {
|
||||
reason.push('Pas de budget batterie exploitable, sans EDF : la voiture attend le soleil');
|
||||
}
|
||||
reason.push(`Stratégie=${strategy.userStrategy}`);
|
||||
reason.push(`Profil=${context.profile}`);
|
||||
reason.push(`Source=${context.sourceMode}`);
|
||||
@@ -1374,6 +1383,17 @@ return [{
|
||||
}
|
||||
},
|
||||
|
||||
// Budget voiture de la nuit (30/09) : fige a l'ouverture, efface en fin.
|
||||
helper_night_budget: {
|
||||
execute: night.nightBudgetToWrite !== null && night.nightBudgetToWrite !== undefined,
|
||||
domain: 'input_text',
|
||||
service: 'set_value',
|
||||
data: {
|
||||
entity_id: 'input_text.victron_evcs_night_budget',
|
||||
value: night.nightBudgetToWrite ?? ''
|
||||
}
|
||||
},
|
||||
|
||||
helper_night_session_start_at: {
|
||||
execute: decision.setNightSessionStart || decision.clearNightSessionStart,
|
||||
domain: 'input_text',
|
||||
|
||||
@@ -148,6 +148,10 @@
|
||||
"balance_required_soc_prudent": states("input_number.evcs_balance_required_soc_prudent"),
|
||||
"night_battery_charge_enabled": states("input_boolean.victron_evcs_night_battery_charge"),
|
||||
"night_battery_done_prev": states("input_boolean.victron_evcs_night_battery_done"),
|
||||
"night_charge_mode": states("input_select.victron_evcs_night_charge_mode"),
|
||||
"balance_night_vehicle_budget_kwh": states("input_number.evcs_balance_night_vehicle_budget_kwh"),
|
||||
"night_budget_prev": states("input_text.victron_evcs_night_budget"),
|
||||
"evcs_total_energy_kwh": states("sensor.evcs_hq2239dnt7p_total_energy"),
|
||||
"balance_required_soc": states("input_number.evcs_balance_required_soc"),
|
||||
"balance_energy_to_cover_kwh": states("input_number.evcs_balance_energy_to_cover_kwh"),
|
||||
"balance_grid_energy_needed_kwh": states("input_number.evcs_balance_grid_energy_needed_kwh"),
|
||||
|
||||
@@ -504,6 +504,16 @@ const params = {
|
||||
balanceRequiredSocPrudentPct: num(input.balance_required_soc_prudent, NaN),
|
||||
nightBatteryChargeEnabled: low(input.night_battery_charge_enabled || 'off') === 'on',
|
||||
nightBatteryDonePrev: low(input.night_battery_done_prev || 'off') === 'on',
|
||||
// BUDGET VOITURE DE LA NUIT (30/09/2026, decisions du proprietaire) : ce
|
||||
// que la voiture peut prendre sur la batterie sans EDF, calcule par le bilan
|
||||
// (profil prudent, plancher 22 %, jusqu'a la nuit suivante), FIGE a
|
||||
// l'ouverture de la session et mesure au compteur de la borne. Choix de la
|
||||
// nuit : `sans_edf` (defaut : on s'arrete au budget) ou `complete` (budget
|
||||
// batterie puis EDF en HC pour le reste). Toute autre valeur = sans_edf.
|
||||
nightChargeMode: low(input.night_charge_mode || 'sans_edf') === 'complete' ? 'complete' : 'sans_edf',
|
||||
balanceNightVehicleBudgetKwh: num(input.balance_night_vehicle_budget_kwh, NaN),
|
||||
nightBudgetPrevRaw: cleanText(input.night_budget_prev),
|
||||
evcsTotalEnergyKwh: num(input.evcs_total_energy_kwh, NaN),
|
||||
balanceEnergyToCoverKwh: num(input.balance_energy_to_cover_kwh, NaN),
|
||||
// ⚠️ NE PAS CONFONDRE LES DEUX GRANDEURS DU BILAN.
|
||||
// `balanceEnergyToCoverKwh` est l'energie que la batterie devra FOURNIR
|
||||
|
||||
@@ -625,9 +625,44 @@ const nightBatterySurplusFloorPct = Math.max(
|
||||
// Sans verrou, la charge repartirait des qu'elle s'arrete : cinquieme
|
||||
// oscillateur du projet, par construction. UNE SEULE PASSE de surplus par
|
||||
// session de nuit, comme la recovery batterie depuis le 27/08.
|
||||
// BUDGET VOITURE DE LA NUIT (30/09/2026, decisions du proprietaire).
|
||||
// « Definir combien la voiture peut prendre sans utiliser EDF, et brider la
|
||||
// charge a cette valeur ou autoriser une charge complete avec EDF. »
|
||||
// Le bilan publie le budget ; il est FIGE au premier cycle de la session
|
||||
// (avec l'index du compteur de la borne) dans input_text
|
||||
// victron_evcs_night_budget = « budget|index », efface en fin de session.
|
||||
// L'arret se juge sur le COMPTEUR, qui ne fait que monter : condition
|
||||
// monotone, aucun oscillateur possible (meme logique que §40). Le plancher de
|
||||
// SOC prudent reste un second arret, jamais retire.
|
||||
const nightBudgetPrev = (() => {
|
||||
const m = String(params.nightBudgetPrevRaw || '').match(/^\s*([\d.]+)\|([\d.]+)\s*$/);
|
||||
return m ? { budgetKwh: Number(m[1]), refKwh: Number(m[2]) } : null;
|
||||
})();
|
||||
const nightBudgetCaptureOk =
|
||||
nightSessionActive && !nightBudgetPrev &&
|
||||
balanceMorningTargetApplied &&
|
||||
Number.isFinite(params.balanceNightVehicleBudgetKwh) &&
|
||||
Number.isFinite(params.evcsTotalEnergyKwh);
|
||||
const nightBudget = !nightSessionActive
|
||||
? null
|
||||
: nightBudgetPrev || (nightBudgetCaptureOk
|
||||
? { budgetKwh: Math.max(0, params.balanceNightVehicleBudgetKwh), refKwh: params.evcsTotalEnergyKwh }
|
||||
: null);
|
||||
const nightBudgetToWrite = nightSessionActive
|
||||
? (nightBudgetCaptureOk ? `${Math.round(nightBudget.budgetKwh * 100) / 100}|${nightBudget.refKwh}` : null)
|
||||
: (nightBudgetPrev ? '' : null);
|
||||
const nightBudgetDeliveredKwh = nightBudget && Number.isFinite(params.evcsTotalEnergyKwh)
|
||||
? Math.max(0, params.evcsTotalEnergyKwh - nightBudget.refKwh)
|
||||
: null;
|
||||
const nightBudgetReached =
|
||||
!!nightBudget && nightBudgetDeliveredKwh !== null &&
|
||||
nightBudgetDeliveredKwh >= nightBudget.budgetKwh - 0.05;
|
||||
const nightChargeComplete = params.nightChargeMode === 'complete';
|
||||
|
||||
const nightBatterySurplusExhausted =
|
||||
params.nightBatteryDonePrev ||
|
||||
(nightSessionActive && parsed.soc <= nightBatterySurplusFloorPct);
|
||||
(nightSessionActive && parsed.soc <= nightBatterySurplusFloorPct) ||
|
||||
(nightSessionActive && nightBudgetReached);
|
||||
|
||||
const nightBatterySurplusAvailable =
|
||||
nightSessionActive &&
|
||||
@@ -644,15 +679,26 @@ const nightBatterySurplusAvailable =
|
||||
// decrite par le proprietaire — « quitte a fermer EDF entre 6h et 7h24 pour
|
||||
// completer » — et elle devient automatique : l'epuisement du surplus rend
|
||||
// `nightGridHcPreferred` vrai, donc P7 prend le relais.
|
||||
// « Sans EDF » (defaut) : la voiture ne prend JAMAIS le reseau la nuit ; une
|
||||
// fois le budget batterie atteint (ou sans budget exploitable), elle attend le
|
||||
// soleil. « Charge complete » : EDF en HC finit la charge, comme avant.
|
||||
const nightGridHcPreferred =
|
||||
nightSessionActive &&
|
||||
params.allowGridHcNight &&
|
||||
nightChargeComplete &&
|
||||
!vehicleChargeCompleted &&
|
||||
!nightBatterySurplusAvailable;
|
||||
|
||||
const nightCarWaitsNoGrid =
|
||||
nightSessionActive &&
|
||||
!nightChargeComplete &&
|
||||
!vehicleChargeCompleted &&
|
||||
!nightBatterySurplusAvailable;
|
||||
|
||||
const batteryAssistAllowed =
|
||||
nightSessionActive &&
|
||||
!nightGridHcPreferred &&
|
||||
!nightCarWaitsNoGrid &&
|
||||
parsed.soc > batteryHardFloorPct &&
|
||||
parsed.soc > gridTakeoverTriggerPct &&
|
||||
!nightTakeoverLatched &&
|
||||
@@ -794,6 +840,12 @@ const nightPreferredSourceMode =
|
||||
!nightSessionActive
|
||||
? 'blocked'
|
||||
: vehicleChargeCompleted
|
||||
? 'blocked'
|
||||
// « Sans EDF » ne bloque la voiture que si EDF n'est PAS deja engage pour
|
||||
// la batterie (plancher dur, cible du matin, session du bilan, verrou) :
|
||||
// proteger le materiel prime (regle du proprietaire, 05/09), et EDF deja
|
||||
// ferme en HC, la voiture en profite au meme tarif.
|
||||
: nightCarWaitsNoGrid && !nightGridTakeoverNeeded
|
||||
? 'blocked'
|
||||
: batteryAssistAllowed
|
||||
? 'battery_assist_night'
|
||||
@@ -1189,6 +1241,12 @@ return [{
|
||||
nightBatterySurplusFloorPct,
|
||||
nightBatterySurplusAvailable,
|
||||
nightBatterySurplusExhausted,
|
||||
nightBudgetKwh: nightBudget ? nightBudget.budgetKwh : null,
|
||||
nightBudgetDeliveredKwh,
|
||||
nightBudgetReached,
|
||||
nightBudgetToWrite,
|
||||
nightChargeMode: params.nightChargeMode,
|
||||
nightCarWaitsNoGrid,
|
||||
nightGridSessionCommitted,
|
||||
middayVehicleGridNeeded,
|
||||
nightGridSessionUncovered,
|
||||
|
||||
Reference in New Issue
Block a user