Files
EVCS/src/nodes/WINDOWS_SOURCEV2.js
Tallagrand Thierry 9bc4af0ea1 Fixes A/B/C/D incident rebond EDF nuit 07-08/08 + GRAFCET doc
- A: état collant night_recovery_done_idle + bande cible−2 (anti-rebond takeover)
- B: snapshot relit night_session_start_soc/at (fin des réécritures 30s)
- C: hystérésis grid — turn_off_vehicle_unplugged (coupure immédiate, bypass min_on)
- D: evcs_status_raw==3 (charged) = fin de charge immédiate
- golden 19→23 scénarios (2 attendus mis à jour, 4 nouveaux)
- MANUEL_COMPLET §11.6 GRAFCET cycle nuit, HANDOFF §17, vision-notes incident
2026-08-08 14:20:21 +02:00

726 lines
22 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.
function minutesSince(ts, nowMs) {
if (ts === null || nowMs === null) return null;
return Math.max(0, (nowMs - ts) / 60000);
}
function secondsSince(ts, nowMs) {
if (ts === null || nowMs === null) return null;
return Math.max(0, (nowMs - ts) / 1000);
}
function isWithinWindow(nowMin, startMin, stopMin) {
if (!Number.isFinite(startMin) || !Number.isFinite(stopMin)) return false;
if (startMin === stopMin) return false;
if (startMin < stopMin) return nowMin >= startMin && nowMin < stopMin;
return nowMin >= startMin || nowMin < stopMin;
}
function clamp(v, min, max) {
return Math.min(Math.max(v, min), max);
}
const prev = $input.first().json;
const {
input,
nowMs,
now,
cfg,
parsed,
params,
history,
batteryTune
} = prev;
// =====================================================
// STRATEGY
// =====================================================
const userStrategy = (params.userStrategyRaw ?? 'auto_solar').toString().trim().toLowerCase();
const isStopMode = userStrategy === 'stop';
const forceGridRequested = userStrategy === 'force_grid_now';
const forceChargeRequested = userStrategy === 'force_charge_now';
const isForceMode = forceGridRequested || forceChargeRequested;
// =====================================================
// LIFECYCLE / TIMING
// =====================================================
const minutesSinceStart = minutesSince(history.lastStartMs, nowMs);
const minutesSinceStop = minutesSince(history.lastStopMs, nowMs);
const secondsSinceCurrentChange = secondsSince(history.lastCurrentChangeMs, nowMs);
const minutesSinceProbeStart = minutesSince(history.probeStartedMs, nowMs);
const minRunElapsed =
minutesSinceStart === null ? true : minutesSinceStart >= params.minRunMinutes;
const minStopElapsed =
minutesSinceStop === null ? true : minutesSinceStop >= params.minStopMinutes;
const currentCooldownBaseSec = parsed.evcsChargingSwitchOn
? batteryTune.global.stepDownCooldownSeconds
: batteryTune.global.stepUpCooldownSeconds;
const currentCooldownElapsed =
secondsSinceCurrentChange === null
? true
: secondsSinceCurrentChange >= Math.max(
params.currentChangeCooldownSeconds,
currentCooldownBaseSec
);
// =====================================================
// DEPARTURE PRIORITY
// =====================================================
const minutesUntilDeparture =
params.departureTimeMin === null
? null
: (() => {
const rawDiff = params.departureTimeMin - now.minNow;
return rawDiff >= 0 ? rawDiff : (1440 - now.minNow + params.departureTimeMin);
})();
const departurePriorityActive =
parsed.departurePriorityEnabled &&
Number.isFinite(minutesUntilDeparture) &&
minutesUntilDeparture >= 0 &&
minutesUntilDeparture <= cfg.departurePriorityWindowMinutes &&
parsed.evcsVehicleConnected;
// =====================================================
// MANUAL CHARGE SEMANTICS
// IMPORTANT:
// force_charge_now = bypass fenêtres pour la charge,
// MAIS PAS demande implicite de réseau.
// =====================================================
const manualChargeBypassWindows = forceChargeRequested;
const manualChargeLocalRequested = forceChargeRequested && !forceGridRequested;
// Compatibilité ascendante : on garde le champ dans context,
// mais on le neutralise volontairement.
const forceChargeDepartureGridOverride = false;
// =====================================================
// SYSTEM DERIVED VALUES
// =====================================================
const acVoltageRef =
Number.isFinite(parsed.vebusAcOutVoltageV) && parsed.vebusAcOutVoltageV > 180
? parsed.vebusAcOutVoltageV
: cfg.nominalAcVoltageV;
const batteryVoltageRef =
Number.isFinite(parsed.vebusDcVoltageV) && parsed.vebusDcVoltageV > 0
? parsed.vebusDcVoltageV
: 51.2;
const houseNonEvcsW = Math.max(0, parsed.houseAcPowerW - parsed.evcsAcPowerW);
const vebusNonEvcsW = Math.max(0, parsed.vebusAcOutPowerW - parsed.evcsAcPowerW);
const totalVebusSoftCeilingW = Math.max(6500, params.acLoadsMaxSafeW || 8500);
const totalVebusHardCeilingW = totalVebusSoftCeilingW + 400;
// P5 : plafond EVCS configurable quand le réseau alimente réellement AC-IN
// (input_number.evcs_plafond_courant_edf_on). Hors réseau : plafond câblé prudent.
const evcsDedicatedCapBaseW = Math.max(0, cfg.evcsDedicatedMaxW - cfg.systemReserveW);
const safeEvcsByDedicatedCapW = parsed.acInAvailable
? Math.max(0, params.gridCurrentMaxA * acVoltageRef)
: evcsDedicatedCapBaseW;
const safeEvcsByVebusW = Math.max(0, totalVebusSoftCeilingW - vebusNonEvcsW - cfg.systemReserveW);
const safeEvcsCombinedW = Math.max(0, Math.min(safeEvcsByDedicatedCapW, safeEvcsByVebusW));
const systemCapCurrentA = Math.max(0, Math.floor(safeEvcsCombinedW / acVoltageRef));
const physicalCapA = clamp(
systemCapCurrentA,
0,
parsed.acInAvailable
? Math.max(params.currentMaxAutoA, params.gridCurrentMaxA)
: params.currentMaxAutoA
);
const canRunAtMinCurrent = physicalCapA >= params.currentMinA;
const endDayGuardSoc = Math.max(params.endOfDaySocGuard, params.endOfDaySocTarget);
const energyNeededForEndDayGuardKwh = Math.max(
0,
((endDayGuardSoc - parsed.soc) / 100) * cfg.batteryUsableCapacityKwh
);
const forecastBufferKwh =
parsed.todayRemainingKwh -
params.baseLoadRemainingKwh -
params.flexibleSafetyMarginKwh -
params.requiredEnergyKwh -
energyNeededForEndDayGuardKwh;
const projectedEndSocAfterCharge = clamp(
parsed.soc + (
(
parsed.todayRemainingKwh -
params.baseLoadRemainingKwh -
params.flexibleSafetyMarginKwh -
params.requiredEnergyKwh
) / cfg.batteryUsableCapacityKwh
) * 100,
0,
100
);
const currentReferenceA = clamp(
Number.isFinite(parsed.evcsCommandCurrentA)
? parsed.evcsCommandCurrentA
: Number.isFinite(parsed.evcsSetCurrentA)
? parsed.evcsSetCurrentA
: params.targetCurrentAutoA,
0,
32
);
const anyVebusOverload = parsed.vebusOverload || parsed.vebusL1Overload;
const anyVebusLowBattery = parsed.vebusLowBattery || parsed.vebusL1LowBattery;
const vebusAcOutSoftReached = parsed.vebusAcOutPowerW >= totalVebusSoftCeilingW;
const vebusAcOutHardReached = parsed.vebusAcOutPowerW >= totalVebusHardCeilingW;
const vebusDcVoltageSoftLow =
Number.isFinite(parsed.vebusDcVoltageV) &&
parsed.vebusDcVoltageV > 0 &&
parsed.vebusDcVoltageV <= cfg.vebusDcVoltageSoftFloorV;
const vebusDcVoltageHardLow =
Number.isFinite(parsed.vebusDcVoltageV) &&
parsed.vebusDcVoltageV > 0 &&
parsed.vebusDcVoltageV <= cfg.vebusDcVoltageHardFloorV;
const houseLoadHighVsHeadroom = houseNonEvcsW > params.houseAcHeadroomW;
// =====================================================
// WINDOWS / PROFILE
// =====================================================
const inNightHcWindow = isWithinWindow(now.minNow, params.hcNightStartMin, params.hcNightStopMin);
const inMorningWindow = isWithinWindow(now.minNow, params.morningStartMin, params.morningStopMin);
const inMiddayWindow = isWithinWindow(now.minNow, params.hcMiddayStartMin, params.hcMiddayStopMin);
const inEveningWindow = isWithinWindow(now.minNow, params.eveningStartMin, params.eveningStopMin);
const profile =
inNightHcWindow ? 'night_hc' :
inMorningWindow ? 'morning_solar' :
inMiddayWindow ? 'midday_hc_or_solar' :
inEveningWindow ? 'evening_high_soc' :
'outside_window';
// =====================================================
// EVCS NIGHT PREVIEW ENGINE
// =====================================================
const nightSessionActive =
profile === 'night_hc' &&
parsed.evcsVehicleConnected;
const nightSessionStartSocPct =
nightSessionActive
? (
Number.isFinite(history.nightSessionStartSocPct)
? history.nightSessionStartSocPct
: parsed.soc
)
: null;
const nightSessionStartAt =
nightSessionActive
? (history.nightSessionStartAt || '')
: '';
const nightSessionStartMs =
nightSessionActive
? (history.nightSessionStartMs ?? null)
: null;
const nightSessionNeedsCapture =
nightSessionActive &&
(
!Number.isFinite(history.nightSessionStartSocPct) ||
!nightSessionStartAt ||
nightSessionStartMs === null
);
const nightSessionNeedsClear =
!nightSessionActive &&
(
Number.isFinite(history.nightSessionStartSocPct) ||
!!history.nightSessionStartAt
);
const batteryHardFloorPct =
params.evcsNightBatteryHardFloorPct;
// P4 : la nuit avant minuit, today_remaining ~ 0 => utiliser la prévision du lendemain.
const nightForecastHour = now.hour;
const nightForecastKwh =
(nightForecastHour >= 20 || nightForecastHour < 8)
? Math.max(parsed.todayRemainingKwh, parsed.tomorrowKwh || 0)
: parsed.todayRemainingKwh;
const batteryMorningTargetPct =
nightForecastKwh >= params.evcsNightStrongForecastKwh
? params.evcsNightTargetStrongForecastPct
: nightForecastKwh >= params.evcsNightMediumForecastKwh
? params.evcsNightTargetMediumForecastPct
: params.evcsNightTargetWeakForecastPct;
const maxBatteryDischargePoints =
nightSessionActive && Number.isFinite(nightSessionStartSocPct)
? Math.max(0, nightSessionStartSocPct - batteryMorningTargetPct)
: 0;
const gridTakeoverTriggerPct =
Math.max(batteryHardFloorPct, batteryMorningTargetPct);
const batteryBelowHardFloor =
parsed.soc <= batteryHardFloorPct;
const batteryBelowMorningTarget =
parsed.soc < batteryMorningTargetPct;
const batteryAtOrBelowGridTakeoverTrigger =
parsed.soc <= gridTakeoverTriggerPct;
const batteryKwhMissingToTarget =
nightSessionActive
? Math.max(
0,
((batteryMorningTargetPct - parsed.soc) / 100) * cfg.batteryUsableCapacityKwh
)
: 0;
const batteryRecoveryMinutesNeeded =
nightSessionActive && params.evcsNightBatteryRecoveryPowerKw > 0
? (batteryKwhMissingToTarget / params.evcsNightBatteryRecoveryPowerKw) * 60
: 0;
const batteryRecoveryUrgent =
nightSessionActive &&
Number.isFinite(minutesUntilDeparture) &&
minutesUntilDeparture >= 0 &&
(
batteryRecoveryMinutesNeeded + params.evcsNightDepartureSafetyMin
>= minutesUntilDeparture
);
// P2 ANTI-CRENEAUX : continuité du takeover réseau nocturne.
// Une fois le réseau HC engagé pendant une session nuit, on y reste
// jusqu'à la fin de la session (fenêtre/débranchement) ou batterie quasi pleine.
// Évite le cycle batterie->jeep->réseau->batterie (pertes + charge hachée).
const NIGHT_TAKEOVER_RELEASE_SOC_PCT = 90;
const nightTakeoverPrev =
(input.night_takeover_prev ?? 'off').toString().trim().toLowerCase() === 'on';
// P8 (07/08/2026) : "charge terminée" COLLANT via l'état surplus du cycle
// précédent (helper input_text.victron_evcs_surplus_state relu par le snapshot).
// Chaîne : VDF stoppe la charge (state vehicle_full_or_paused) → ce flag s'allume
// → DECISION émet night_session_done_idle → le flag s'auto-entretient jusqu'au
// débranchement (state vehicle_not_connected) ou à la fin de la fenêtre nuit.
const prevSurplusState = (input.prev_surplus_state ?? '').toString().trim().toLowerCase();
// Fix D (08/08/2026) : le statut natif EVCS "charged" (3) vaut aussi fin de
// charge, sans attendre le VehicleDemandFilter (désarmé si la session a été
// stoppée par une garde, ex battery_current_hard_stop → détection tardive
// observée la nuit du 07-08/08).
const evcsStatusCharged = Number.parseFloat(parsed.evcsStatusRaw) === 3;
const vehicleChargeCompleted =
nightSessionActive &&
(
prevSurplusState === 'vehicle_full_or_paused' ||
prevSurplusState === 'night_session_done_idle' ||
prevSurplusState === 'night_recovery_done_idle' ||
evcsStatusCharged
);
// Fix A (08/08/2026) : recovery batterie post-charge = UNE SEULE passe.
// Sans flag collant, SOC==cible est un point dégénéré (engage soc<=cible,
// release soc>cible, l'ACM recharge pile à la cible) → rebond EDF ON/OFF
// période ~10 min observé la nuit du 07-08/08. Une fois la cible atteinte,
// l'état 'night_recovery_done_idle' (auto-entretenu via prev_surplus_state)
// coupe le takeover. Ré-engagement uniquement sous cible − 2 pts (bande),
// hard floor et fenêtre/débranchement restent prioritaires.
// ⚠ 'night_recovery_done_idle' ne doit matcher AUCUN pattern no-demand de
// l'hystérésis (full_or_paused / not_requesting_power / charge_complete /
// no_demand) — même contrainte que 'night_session_done_idle'.
const NIGHT_RECOVERY_REARM_BAND_PCT = 2;
const nightRecoveryDonePrev = prevSurplusState === 'night_recovery_done_idle';
const nightRecoveryDone =
vehicleChargeCompleted &&
(
parsed.soc >= batteryMorningTargetPct ||
(
nightRecoveryDonePrev &&
parsed.soc > batteryMorningTargetPct - NIGHT_RECOVERY_REARM_BAND_PCT
)
);
// P8 : correction du fix 04/08 (logique inversée : "|| vehicleChargeCompleted"
// GARDAIT le latch après la charge au lieu de le libérer).
const nightTakeoverLatched =
nightSessionActive &&
nightTakeoverPrev &&
parsed.soc < NIGHT_TAKEOVER_RELEASE_SOC_PCT &&
!vehicleChargeCompleted;
const nightGridHcPreferred =
nightSessionActive &&
params.allowGridHcNight &&
!vehicleChargeCompleted;
const batteryAssistAllowed =
nightSessionActive &&
!nightGridHcPreferred &&
parsed.soc > batteryHardFloorPct &&
parsed.soc > gridTakeoverTriggerPct &&
!nightTakeoverLatched &&
!batteryRecoveryUrgent;
const nightGridTakeoverNeeded =
nightSessionActive &&
(
nightGridHcPreferred ||
batteryBelowHardFloor ||
(
// Fix A : après fin de charge, le seul motif réseau restant est la
// recovery batterie, et elle ne se ré-engage pas une fois done.
vehicleChargeCompleted
? !nightRecoveryDone
: (
batteryAtOrBelowGridTakeoverTrigger ||
batteryRecoveryUrgent ||
nightTakeoverLatched
)
)
);
const nightPreferredSourceMode =
!nightSessionActive
? 'blocked'
: vehicleChargeCompleted
? 'blocked'
: batteryAssistAllowed
? 'battery_assist_night'
: (
batteryBelowHardFloor ||
batteryBelowMorningTarget ||
batteryRecoveryUrgent
)
? 'grid_forced'
: 'grid_hc_allowed';
const batteryRecoveryAllowedNow =
nightSessionActive &&
nightGridTakeoverNeeded &&
(
batteryBelowMorningTarget ||
batteryRecoveryUrgent
);
const batteryTargetReached =
nightSessionActive &&
parsed.soc >= batteryMorningTargetPct;
// =====================================================
// PROBES / SOLAR POTENTIAL
// =====================================================
// IMPORTANT:
// On ne met plus effectivePvStartMinW à 0 en force_charge_now,
// sinon la nuit 0W >= 0W ferait croire à un faux signal solaire.
const effectivePvStartMinW = params.pvStartMinW;
const morningProbeEligible =
profile === 'morning_solar' &&
params.probeEnabled &&
parsed.pvPowerAvg10mW >= params.morningProbePvAvgMinW &&
parsed.pvTrendW >= params.morningProbePvTrendMinW &&
parsed.soc >= params.morningSocMin &&
parsed.batteryCurrentA >= params.morningBatteryMaxDischargeA;
const middayProbeEligible =
profile === 'midday_hc_or_solar' &&
params.probeEnabled &&
parsed.pvPowerAvg10mW >= params.middayProbePvAvgMinW &&
parsed.soc >= params.middaySocMin &&
parsed.batteryCurrentA >= params.middayBatteryMaxDischargeA;
const afternoonProbeEligible =
profile === 'outside_window' &&
params.probeEnabled &&
parsed.soc >= params.afternoonProbeSocMin &&
parsed.batteryCurrentA >= params.afternoonBatteryMaxDischargeA &&
parsed.pvPowerAvg10mW >= params.afternoonProbePvAvgMinW;
const cloudDebtOkForPotential = params.cloudDebtPrevKwh <= params.cloudDebtMaxKwh;
const batteryOkForPotential = parsed.batteryCurrentA >= -Math.max(1, params.batteryNeutralBandA);
// P3 PROVOCATION PV (installation sans injection) :
// Batteries quasi pleines => le MPPT bride la production à la conso maison.
// Les seuils PV observés, le forecast buffer et la cloud debt ne veulent
// alors plus rien dire (la dette ne peut d'ailleurs plus se rembourser,
// batterie pleine = courant nul). On autorise solar_potential sur la seule
// santé batterie : SOC très haut + batterie non déchargée.
// Les gardes aval (BATTERY_CLOUD_LOCKS / DECISION) restent actives.
const PV_PROVOCATION_SOC_MIN = 90;
const pvProvocationEligible =
parsed.soc >= PV_PROVOCATION_SOC_MIN &&
batteryOkForPotential;
const isHighBatteryForPotential =
pvProvocationEligible ||
(
parsed.soc >= 85 &&
forecastBufferKwh > 0 &&
cloudDebtOkForPotential &&
batteryOkForPotential
);
const potentialAllowed = userStrategy !== 'auto_solar';
// IMPORTANT:
// On impose un vrai PV > 0 pour le test sur pvPowerW.
// Ça évite qu'en force_charge_now la nuit, 0W soit pris pour un signal solaire.
const strongSolarSignal =
(
parsed.pvPowerW > 0 &&
parsed.pvPowerW >= effectivePvStartMinW
) ||
parsed.pvPowerAvg10mW >= params.pvHoldMinW ||
(departurePriorityActive && parsed.pvPowerAvg10mW >= params.pvHoldMinW);
// =====================================================
// SOURCE MODE
// SÉMANTIQUE CORRIGÉE:
// - force_charge_now bypass les fenêtres
// - mais ne devient jamais grid_forced à lui seul
// - outside_window = pas de réseau programmé
// - la charge locale peut rester autorisée
// =====================================================
let sourceMode = 'blocked';
if (isStopMode) {
sourceMode = 'blocked';
}
else if (forceGridRequested) {
// Demande explicite de réseau.
sourceMode = 'grid_forced';
}
else if (forceChargeRequested) {
// IMPORTANT:
// force_charge_now = intention de charger maintenant,
// sans exiger le réseau.
if (strongSolarSignal) {
sourceMode = 'solar_only';
}
else if (nightSessionActive && batteryAssistAllowed) {
sourceMode = 'battery_assist_night';
}
else if (isHighBatteryForPotential && potentialAllowed) {
sourceMode = 'solar_potential';
}
else if (profile === 'midday_hc_or_solar' && middayProbeEligible) {
sourceMode = 'hybrid_probe';
}
else if (profile === 'morning_solar' && morningProbeEligible) {
sourceMode = 'hybrid_probe';
}
else {
// Fallback manuel local :
// on n'impose pas le grid, on laisse l'étage aval décider
// selon les garde-fous physiques / batterie.
sourceMode = 'solar_potential';
}
}
else if (profile === 'night_hc') {
if (nightSessionActive) {
sourceMode = nightPreferredSourceMode;
} else {
sourceMode = 'blocked';
}
}
else if (profile === 'midday_hc_or_solar' || profile === 'morning_solar') {
if (strongSolarSignal) {
sourceMode = 'solar_only';
}
else if (isHighBatteryForPotential && potentialAllowed) {
sourceMode = 'solar_potential';
}
else if (profile === 'midday_hc_or_solar' && middayProbeEligible) {
sourceMode = 'hybrid_probe';
}
else if (profile === 'morning_solar' && morningProbeEligible) {
sourceMode = 'hybrid_probe';
}
else if (profile === 'midday_hc_or_solar' && params.allowGridHcMidday && parsed.gridAvailable) {
sourceMode = 'grid_hc_allowed';
}
else {
sourceMode = 'blocked';
}
}
else if (profile === 'evening_high_soc') {
if (strongSolarSignal) {
sourceMode = 'solar_only';
}
else if (isHighBatteryForPotential && potentialAllowed) {
sourceMode = 'solar_potential';
}
else {
sourceMode = 'blocked';
}
}
else if (profile === 'outside_window') {
if (strongSolarSignal) {
sourceMode = 'solar_only';
}
else if (isHighBatteryForPotential && potentialAllowed) {
sourceMode = 'solar_potential';
}
else if (afternoonProbeEligible) {
sourceMode = 'hybrid_probe';
}
else {
// IMPORTANT:
// hors fenêtre = pas de réseau programmé.
// Donc on ne bascule plus vers grid_forced juste parce que le réseau existe.
sourceMode = 'blocked';
}
}
const operatingMode = profile;
// =====================================================
// OUTPUT
// =====================================================
return [{
json: {
...prev,
strategy: {
userStrategy,
isStopMode,
isForceMode,
forceGridRequested,
forceChargeRequested,
},
lifecycle: {
minutesSinceStart,
minutesSinceStop,
secondsSinceCurrentChange,
minutesSinceProbeStart,
minRunElapsed,
minStopElapsed,
currentCooldownElapsed,
minutesUntilDeparture,
},
windows: {
inNightHcWindow,
inMorningWindow,
inMiddayWindow,
inEveningWindow,
},
derived: {
acVoltageRef,
batteryVoltageRef,
houseNonEvcsW,
vebusNonEvcsW,
totalVebusSoftCeilingW,
totalVebusHardCeilingW,
safeEvcsByDedicatedCapW,
safeEvcsByVebusW,
safeEvcsCombinedW,
systemCapCurrentA,
physicalCapA,
canRunAtMinCurrent,
endDayGuardSoc,
energyNeededForEndDayGuardKwh,
forecastBufferKwh,
projectedEndSocAfterCharge,
currentReferenceA,
anyVebusOverload,
anyVebusLowBattery,
vebusAcOutSoftReached,
vebusAcOutHardReached,
vebusDcVoltageSoftLow,
vebusDcVoltageHardLow,
houseLoadHighVsHeadroom,
},
context: {
departurePriorityActive,
forceChargeDepartureGridOverride,
manualChargeBypassWindows,
manualChargeLocalRequested,
morningProbeEligible,
middayProbeEligible,
afternoonProbeEligible,
pvProvocationEligible,
isHighBatteryForPotential,
strongSolarSignal,
effectivePvStartMinW,
profile,
operatingMode,
sourceMode,
},
// R1 : source de vérité UNIQUE du moteur nuit.
// BATTERY_CLOUD_LOCKS consomme cet objet, il ne recalcule plus rien.
nightEvcsPreview: {
nightSessionActive,
nightSessionNeedsCapture,
nightSessionNeedsClear,
nightSessionStartSocPct,
nightSessionStartAt,
nightSessionStartMs,
batteryHardFloorPct,
batteryMorningTargetPct,
maxBatteryDischargePoints,
gridTakeoverTriggerPct,
batteryBelowHardFloor,
batteryBelowMorningTarget,
batteryAtOrBelowGridTakeoverTrigger,
batteryKwhMissingToTarget,
batteryRecoveryMinutesNeeded,
batteryRecoveryUrgent,
batteryRecoveryAllowedNow,
batteryTargetReached,
batteryAssistAllowed,
nightGridTakeoverNeeded,
nightTakeoverLatched,
nightGridHcPreferred,
vehicleChargeCompleted,
nightRecoveryDone,
nightPreferredSourceMode,
}
}
}];