R2: git = source de vérité — src/nodes/ + build/extract intégrés au push
- src/nodes/*.js (6 nodes + template snapshot) = source de vérité unique. - n8n-push.mjs reconstruit le JSON depuis src/ avant syntaxe+golden+PUT (src gagne toujours; n8n-extract.mjs pour resynchroniser si édition UI). - n8n-build.mjs --check pour vérifier la synchro. - Copies historiques déplacées dans archive/ (mortes, dérivées de la prod). - .gitignore: backups locaux de rollback. - AGENTS.md: règle R2 pour toutes les futures sessions (dont Qwen local). - Validé: idempotence, mutation/restauration, pipeline push complet en réel.
This commit is contained in:
685
src/nodes/WINDOWS_SOURCEV2.js
Normal file
685
src/nodes/WINDOWS_SOURCEV2.js
Normal file
@@ -0,0 +1,685 @@
|
||||
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();
|
||||
const vehicleChargeCompleted =
|
||||
nightSessionActive &&
|
||||
(
|
||||
prevSurplusState === 'vehicle_full_or_paused' ||
|
||||
prevSurplusState === 'night_session_done_idle'
|
||||
);
|
||||
|
||||
// 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 ||
|
||||
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,
|
||||
nightPreferredSourceMode,
|
||||
}
|
||||
}
|
||||
}];
|
||||
Reference in New Issue
Block a user