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:
216
archive/decoupesurplus/WINDOWS_SOURCE.js
Normal file
216
archive/decoupesurplus/WINDOWS_SOURCE.js
Normal file
@@ -0,0 +1,216 @@
|
||||
// === STRATÉGIE UTILISATEUR (V2.0 - NEW) ===
|
||||
const userStrategyRaw = input.victron_evcs_strategy || 'auto_solar';
|
||||
const userStrategy = low(userStrategyRaw); // auto_solar | auto_solar_plus | force_grid_now | force_charge_now | stop
|
||||
// Mode Force (Ignore HC strict & PV minimaux)
|
||||
const isForceMode = (userStrategy === 'force_grid_now' || userStrategy === 'force_charge_now');
|
||||
const isStopMode = (userStrategy === 'stop');
|
||||
// Override for Force modes
|
||||
let effectivePvStartMinW = pvStartMinW;
|
||||
let effectiveAllowGridMidday = low(input.allow_grid_hc_midday || 'off') === 'on';
|
||||
if (isForceMode) {
|
||||
effectivePvStartMinW = 0;
|
||||
effectiveAllowGridMidday = true;
|
||||
}
|
||||
|
||||
// === OPTIONAL V1.4 INPUTS ===
|
||||
const hcNightStartMin = parseTimeToMinutes(input.hc_night_start_time, 23 * 60);
|
||||
const hcNightStopMin = parseTimeToMinutes(input.hc_night_stop_time, 6 * 60);
|
||||
const morningStartMin = parseTimeToMinutes(input.morning_start_time, dayStartTimeMin);
|
||||
const morningStopMin = parseTimeToMinutes(input.morning_stop_time, 11 * 60);
|
||||
const hcMiddayStartMin = parseTimeToMinutes(input.hc_midday_start_time, 12 * 60 + 24);
|
||||
const hcMiddayStopMin = parseTimeToMinutes(input.hc_midday_stop_time, 14 * 60 + 24);
|
||||
const eveningStartMin = parseTimeToMinutes(input.evening_start_time, 17 * 60);
|
||||
const eveningStopMin = parseTimeToMinutes(input.evening_stop_time, dayStopTimeMin);
|
||||
const allowGridHcNight = low(input.allow_grid_hc_night || 'off') === 'on';
|
||||
const allowGridHcMidday = effectiveAllowGridMidday; // Utilise le override si force mode
|
||||
const probeEnabled = low(input.probe_enabled || 'off') === 'on';
|
||||
const probeMinutes = Math.max(1, num(input.probe_minutes, 3));
|
||||
const probeRetryMinutes = Math.max(5, num(input.probe_retry_minutes, 45));
|
||||
const probeCurrentA = clamp(num(input.probe_current_a, currentMinA), currentMinA, currentMaxAutoA);
|
||||
const gridBatterySoftA = num(input.grid_battery_soft_a, -3);
|
||||
const gridBatteryHardA = num(input.grid_battery_hard_a, -6);
|
||||
const morningProbePvAvgMinW = Math.max(0, num(input.morning_probe_pv_avg_min_w, 1500));
|
||||
const morningProbePvTrendMinW = num(input.morning_probe_pv_trend_min_w, 0);
|
||||
const middayProbePvAvgMinW = Math.max(0, num(input.midday_probe_pv_avg_min_w, 1200));
|
||||
const morningSocMin = clamp(num(input.morning_soc_min, 35), 0, 100);
|
||||
const middaySocMin = clamp(num(input.midday_soc_min, 75), 0, 100);
|
||||
const eveningSocStartMin = clamp(num(input.evening_soc_start_min, 85), 0, 100);
|
||||
const eveningSocStopMin = clamp(num(input.evening_soc_stop_min, 80), 0, 100);
|
||||
const morningBatteryMaxDischargeA = num(input.morning_battery_max_discharge_a, -20);
|
||||
const middayBatteryMaxDischargeA = num(input.midday_battery_max_discharge_a, -40);
|
||||
const eveningBatteryMaxDischargeA = num(input.evening_battery_max_discharge_a, -30);
|
||||
const lastBlockReason = (input.last_block_reason ?? '').toString().trim() || 'none';
|
||||
const blockedUntilAt = (input.blocked_until ?? '').toString().trim();
|
||||
const probeStartedAt = (input.probe_started_at ?? '').toString().trim();
|
||||
const lastStartAt = (input.last_start_at ?? '').toString().trim();
|
||||
const lastStopAt = (input.last_stop_at ?? '').toString().trim();
|
||||
const lastCurrentChangeAt = (input.last_current_change_at ?? '').toString().trim();
|
||||
const lastSignature = (input.last_signature ?? '').toString().trim();
|
||||
const cloudDebtStartedAt = (input.cloudDebtStartedAt ?? '').toString().trim();
|
||||
const lastStartMs = parseParisStampToMs(lastStartAt);
|
||||
const lastStopMs = parseParisStampToMs(lastStopAt);
|
||||
const lastCurrentChangeMs = parseParisStampToMs(lastCurrentChangeAt);
|
||||
const blockedUntilMs = parseParisStampToMs(blockedUntilAt);
|
||||
const probeStartedMs = parseParisStampToMs(probeStartedAt);
|
||||
const cloudDebtStartedMsPrev = parseParisStampToMs(cloudDebtStartedAt);
|
||||
const minutesSinceStart = minutesSince(lastStartMs, nowMs);
|
||||
const minutesSinceStop = minutesSince(lastStopMs, nowMs);
|
||||
const secondsSinceCurrentChange = secondsSince(lastCurrentChangeMs, nowMs);
|
||||
const minutesSinceProbeStart = minutesSince(probeStartedMs, nowMs);
|
||||
const minRunElapsed = minutesSinceStart === null ? true : minutesSinceStart >= minRunMinutes;
|
||||
const minStopElapsed = minutesSinceStop === null ? true : minutesSinceStop >= minStopMinutes;
|
||||
const currentCooldownElapsed = secondsSinceCurrentChange === null ? true : secondsSinceCurrentChange >= currentChangeCooldownSeconds;
|
||||
const minutesUntilDeparture =
|
||||
departureTimeMin === null
|
||||
? null
|
||||
: (() => {
|
||||
const rawDiff = departureTimeMin - now.minNow;
|
||||
return rawDiff >= 0 ? rawDiff : (1440 - now.minNow + departureTimeMin);
|
||||
})();
|
||||
const departurePriorityActive =
|
||||
departurePriorityEnabled &&
|
||||
Number.isFinite(minutesUntilDeparture) &&
|
||||
minutesUntilDeparture >= 0 &&
|
||||
minutesUntilDeparture <= CFG.departurePriorityWindowMinutes &&
|
||||
evcsVehicleConnected;
|
||||
|
||||
// === VE.BUS INPUTS ===
|
||||
const vebusOverload = low(input.vebus_overload || 'off') === 'on';
|
||||
const vebusL1Overload = low(input.vebus_l1_overload || 'off') === 'on';
|
||||
const vebusLowBattery = low(input.vebus_low_battery || 'off') === 'on';
|
||||
const vebusL1LowBattery = low(input.vebus_l1_low_battery || 'off') === 'on';
|
||||
const vebusAcOutPowerW = Math.max(0, num(input.vebus_ac_out_l1_power_w, 0));
|
||||
const vebusAcOutCurrentA = Math.max(0, num(input.vebus_ac_out_l1_current_a, 0));
|
||||
const vebusAcOutVoltageV = num(input.vebus_ac_out_l1_voltage_v, CFG.nominalAcVoltageV);
|
||||
const vebusDcVoltageV = num(input.vebus_dc_voltage_v, NaN);
|
||||
const vebusDcCurrentA = num(input.vebus_dc_current_a, NaN);
|
||||
const vebusDcPowerW = num(input.vebus_dc_power_w, NaN);
|
||||
const vebusState = num(input.vebus_state, NaN);
|
||||
const vebusMainState = num(input.vebus_main_state, NaN);
|
||||
|
||||
// === DERIVED ===
|
||||
const acVoltageRef = Number.isFinite(vebusAcOutVoltageV) && vebusAcOutVoltageV > 180
|
||||
? vebusAcOutVoltageV
|
||||
: CFG.nominalAcVoltageV;
|
||||
const batteryVoltageRef = Number.isFinite(vebusDcVoltageV) && vebusDcVoltageV > 0
|
||||
? vebusDcVoltageV
|
||||
: 51.2;
|
||||
const houseNonEvcsW = Math.max(0, houseAcPowerW - evcsAcPowerW);
|
||||
const vebusNonEvcsW = Math.max(0, vebusAcOutPowerW - evcsAcPowerW);
|
||||
const totalVebusSoftCeilingW = Math.max(6500, acLoadsMaxSafeW || 8500);
|
||||
const totalVebusHardCeilingW = totalVebusSoftCeilingW + 400;
|
||||
const safeEvcsByDedicatedCapW = Math.max(0, CFG.evcsDedicatedMaxW - CFG.systemReserveW);
|
||||
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, currentMaxAutoA);
|
||||
const canRunAtMinCurrent = physicalCapA >= currentMinA;
|
||||
const endDayGuardSoc = Math.max(endOfDaySocGuard, endOfDaySocTarget);
|
||||
const energyNeededForEndDayGuardKwh = Math.max(0, ((endDayGuardSoc - soc) / 100) * CFG.batteryUsableCapacityKwh);
|
||||
const forecastBufferKwh =
|
||||
todayRemainingKwh -
|
||||
baseLoadRemainingKwh -
|
||||
flexibleSafetyMarginKwh -
|
||||
requiredEnergyKwh -
|
||||
energyNeededForEndDayGuardKwh;
|
||||
const projectedEndSocAfterCharge = clamp(
|
||||
soc + (
|
||||
(
|
||||
todayRemainingKwh -
|
||||
baseLoadRemainingKwh -
|
||||
flexibleSafetyMarginKwh -
|
||||
requiredEnergyKwh
|
||||
) / CFG.batteryUsableCapacityKwh
|
||||
) * 100,
|
||||
0,
|
||||
100
|
||||
);
|
||||
const currentReferenceA = clamp(
|
||||
Number.isFinite(evcsCommandCurrentA)
|
||||
? evcsCommandCurrentA
|
||||
: Number.isFinite(evcsSetCurrentA)
|
||||
? evcsSetCurrentA
|
||||
: targetCurrentAutoA,
|
||||
0,
|
||||
32
|
||||
);
|
||||
const anyVebusOverload = vebusOverload || vebusL1Overload;
|
||||
const anyVebusLowBattery = vebusLowBattery || vebusL1LowBattery;
|
||||
const vebusAcOutSoftReached = vebusAcOutPowerW >= totalVebusSoftCeilingW;
|
||||
const vebusAcOutHardReached = vebusAcOutPowerW >= totalVebusHardCeilingW;
|
||||
const vebusDcVoltageSoftLow =
|
||||
Number.isFinite(vebusDcVoltageV) &&
|
||||
vebusDcVoltageV > 0 &&
|
||||
vebusDcVoltageV <= CFG.vebusDcVoltageSoftFloorV;
|
||||
const vebusDcVoltageHardLow =
|
||||
Number.isFinite(vebusDcVoltageV) &&
|
||||
vebusDcVoltageV > 0 &&
|
||||
vebusDcVoltageV <= CFG.vebusDcVoltageHardFloorV;
|
||||
const houseLoadHighVsHeadroom = houseNonEvcsW > houseAcHeadroomW;
|
||||
|
||||
// === WINDOWS / PROFILE / SOURCE MODE (With Solar Potential Logic) ===
|
||||
const inNightHcWindow = isWithinWindow(now.minNow, hcNightStartMin, hcNightStopMin);
|
||||
const inMorningWindow = isWithinWindow(now.minNow, morningStartMin, morningStopMin);
|
||||
const inMiddayWindow = isWithinWindow(now.minNow, hcMiddayStartMin, hcMiddayStopMin);
|
||||
const inEveningWindow = isWithinWindow(now.minNow, eveningStartMin, eveningStopMin);
|
||||
const profile =
|
||||
inNightHcWindow ? 'night_hc' :
|
||||
inMorningWindow ? 'morning_solar' :
|
||||
inMiddayWindow ? 'midday_hc_or_solar' :
|
||||
inEveningWindow ? 'evening_high_soc' :
|
||||
'outside_window';
|
||||
const morningProbeEligible =
|
||||
profile === 'morning_solar' &&
|
||||
probeEnabled &&
|
||||
pvPowerAvg10mW >= morningProbePvAvgMinW &&
|
||||
pvTrendW >= morningProbePvTrendMinW &&
|
||||
soc >= morningSocMin &&
|
||||
batteryCurrentA >= morningBatteryMaxDischargeA;
|
||||
const middayProbeEligible =
|
||||
profile === 'midday_hc_or_solar' &&
|
||||
probeEnabled &&
|
||||
pvPowerAvg10mW >= middayProbePvAvgMinW &&
|
||||
soc >= middaySocMin &&
|
||||
batteryCurrentA >= middayBatteryMaxDischargeA;
|
||||
// Solar Potential Condition (High Battery + Forecast)
|
||||
const cloudDebtOkForPotential = cloudDebtPrevKwh <= cloudDebtMaxKwh;
|
||||
const batteryOkForPotential = batteryCurrentA >= -Math.max(1, batteryNeutralBandA);
|
||||
const isHighBatteryForPotential =
|
||||
soc >= 85 &&
|
||||
forecastBufferKwh > 0 &&
|
||||
cloudDebtOkForPotential &&
|
||||
batteryOkForPotential;
|
||||
const potentialAllowed = userStrategy !== 'auto_solar'; // auto_solar_plus autorise aussi
|
||||
const isForcePotential = userStrategy === 'force_charge_now' || userStrategy === 'force_grid_now';
|
||||
const strongSolarSignal =
|
||||
pvPowerW >= effectivePvStartMinW ||
|
||||
pvPowerAvg10mW >= pvHoldMinW ||
|
||||
(departurePriorityActive && pvPowerAvg10mW >= pvHoldMinW);
|
||||
let sourceMode = 'blocked';
|
||||
if (isForcePotential) {
|
||||
sourceMode = 'grid_forced'; // Priorité à l'utilisateur
|
||||
} else if (profile === 'midday_hc_or_solar' || profile === 'morning_solar') {
|
||||
if (strongSolarSignal) {
|
||||
sourceMode = 'solar_only';
|
||||
} else if (isHighBatteryForPotential && potentialAllowed) {
|
||||
sourceMode = 'solar_potential'; // Le nouveau comportement clé !
|
||||
} else if (profile === 'midday_hc_or_solar' && middayProbeEligible) {
|
||||
sourceMode = 'hybrid_probe';
|
||||
} else if (allowGridHcMidday && gridIsOn) {
|
||||
sourceMode = 'grid_hc_allowed';
|
||||
} else {
|
||||
sourceMode = 'blocked'; // Fallback strict si PV bridé et pas de potentiel clair
|
||||
}
|
||||
} else if (profile === 'night_hc') {
|
||||
sourceMode = allowGridHcNight && gridIsOn ? 'grid_hc_allowed' : 'blocked';
|
||||
} else if (profile === 'evening_high_soc') {
|
||||
sourceMode = strongSolarSignal ? 'solar_only' : 'blocked';
|
||||
} else if (profile === 'outside_window' && userStrategy !== 'auto_solar') {
|
||||
// Nouvelle condition : autorise charge en dehors fenêtre si stratégie auto (pas strict auto_solar)
|
||||
if (strongSolarSignal) {
|
||||
sourceMode = 'solar_only';
|
||||
} else if (isHighBatteryForPotential && potentialAllowed) {
|
||||
sourceMode = 'solar_potential';
|
||||
}
|
||||
}
|
||||
const operatingMode = profile;
|
||||
Reference in New Issue
Block a user