avant analyse et doc
This commit is contained in:
13143
Notes/kimievcs.md
Normal file
13143
Notes/kimievcs.md
Normal file
File diff suppressed because one or more lines are too long
955
modified/BATTERY_CLOUD_LOCKS_patched.js
Normal file
955
modified/BATTERY_CLOUD_LOCKS_patched.js
Normal file
@@ -0,0 +1,955 @@
|
||||
function clamp(v, min, max) {
|
||||
return Math.min(Math.max(v, min), max);
|
||||
}
|
||||
|
||||
function to2(n) {
|
||||
return String(n).padStart(2, '0');
|
||||
}
|
||||
|
||||
function getParisParts(ms = Date.now()) {
|
||||
const fmt = new Intl.DateTimeFormat('fr-FR', {
|
||||
timeZone: 'Europe/Paris',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
|
||||
const parts = Object.fromEntries(
|
||||
fmt.formatToParts(new Date(ms))
|
||||
.filter(p => p.type !== 'literal')
|
||||
.map(p => [p.type, p.value])
|
||||
);
|
||||
|
||||
const hour = Number(parts.hour) === 24 ? 0 : Number(parts.hour);
|
||||
const date = `${parts.year}-${parts.month}-${parts.day}`;
|
||||
|
||||
return {
|
||||
date,
|
||||
hour,
|
||||
minute: Number(parts.minute),
|
||||
second: Number(parts.second),
|
||||
};
|
||||
}
|
||||
|
||||
function getTzOffsetMs(utcTs, timeZone = 'Europe/Paris') {
|
||||
const dtf = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
|
||||
const parts = Object.fromEntries(
|
||||
dtf.formatToParts(new Date(utcTs))
|
||||
.filter(p => p.type !== 'literal')
|
||||
.map(p => [p.type, p.value])
|
||||
);
|
||||
|
||||
const asIfUtc = Date.UTC(
|
||||
Number(parts.year),
|
||||
Number(parts.month) - 1,
|
||||
Number(parts.day),
|
||||
Number(parts.hour) === 24 ? 0 : Number(parts.hour),
|
||||
Number(parts.minute),
|
||||
Number(parts.second)
|
||||
);
|
||||
|
||||
const d = new Date(utcTs);
|
||||
const actualUtc = Date.UTC(
|
||||
d.getUTCFullYear(),
|
||||
d.getUTCMonth(),
|
||||
d.getUTCDate(),
|
||||
d.getUTCHours(),
|
||||
d.getUTCMinutes(),
|
||||
d.getUTCSeconds()
|
||||
);
|
||||
|
||||
return asIfUtc - actualUtc;
|
||||
}
|
||||
|
||||
function parseParisStampToMs(s) {
|
||||
const text = (s ?? '').toString().trim();
|
||||
const m = text.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
|
||||
if (!m) return null;
|
||||
|
||||
const y = Number(m[1]);
|
||||
const mo = Number(m[2]);
|
||||
const d = Number(m[3]);
|
||||
const h = Number(m[4]);
|
||||
const mi = Number(m[5]);
|
||||
const se = Number(m[6]);
|
||||
|
||||
const guessUtc = Date.UTC(y, mo - 1, d, h, mi, se);
|
||||
const off1 = getTzOffsetMs(guessUtc, 'Europe/Paris');
|
||||
let ts = guessUtc - off1;
|
||||
const off2 = getTzOffsetMs(ts, 'Europe/Paris');
|
||||
|
||||
if (off2 !== off1) ts = guessUtc - off2;
|
||||
return ts;
|
||||
}
|
||||
|
||||
function hhmmssFromMinutes(minOfDay) {
|
||||
const mm = ((minOfDay % 1440) + 1440) % 1440;
|
||||
const hh = Math.floor(mm / 60);
|
||||
const mi = mm % 60;
|
||||
return `${to2(hh)}:${to2(mi)}:00`;
|
||||
}
|
||||
|
||||
function nextDailyOccurrenceMs(startMin, refMs) {
|
||||
if (!Number.isFinite(startMin)) return null;
|
||||
|
||||
const today = getParisParts(refMs).date;
|
||||
const todayStamp = `${today} ${hhmmssFromMinutes(startMin)}`;
|
||||
const todayMs = parseParisStampToMs(todayStamp);
|
||||
|
||||
if (todayMs !== null && todayMs > refMs + 1000) return todayMs;
|
||||
|
||||
const tomorrow = getParisParts(refMs + 36 * 3600 * 1000).date;
|
||||
const tomorrowStamp = `${tomorrow} ${hhmmssFromMinutes(startMin)}`;
|
||||
return parseParisStampToMs(tomorrowStamp);
|
||||
}
|
||||
|
||||
const prev = $input.first().json;
|
||||
|
||||
const {
|
||||
nowMs,
|
||||
cfg,
|
||||
parsed,
|
||||
params,
|
||||
history,
|
||||
batteryTune,
|
||||
strategy,
|
||||
derived,
|
||||
context,
|
||||
lifecycle,
|
||||
} = prev;
|
||||
|
||||
// =====================================================
|
||||
// SESSION / SOURCE
|
||||
// =====================================================
|
||||
const sessionObservedActive =
|
||||
parsed.evcsChargingSwitchOn ||
|
||||
parsed.controlLatch ||
|
||||
parsed.chargePermissionNow;
|
||||
|
||||
const isGridSource =
|
||||
context.sourceMode === 'grid_hc_allowed' ||
|
||||
context.sourceMode === 'grid_forced';
|
||||
|
||||
const isLocalSolarSource =
|
||||
context.sourceMode === 'solar_only' ||
|
||||
context.sourceMode === 'solar_potential';
|
||||
|
||||
// =====================================================
|
||||
// SOC BAND / BATTERY DYNAMIC POLICY
|
||||
// =====================================================
|
||||
const socBand =
|
||||
parsed.soc > 90 ? 'gt90' :
|
||||
parsed.soc > 80 ? '80_90' :
|
||||
parsed.soc > 60 ? '60_80' :
|
||||
parsed.soc > 40 ? '40_60' :
|
||||
parsed.soc > 25 ? '25_40' :
|
||||
'lt25';
|
||||
|
||||
const band = batteryTune.bands[socBand];
|
||||
|
||||
const dischargeModeFactor =
|
||||
strategy.forceChargeRequested ? 1.2 :
|
||||
strategy.forceGridRequested ? 0.7 :
|
||||
strategy.userStrategy === 'auto_solar' ? 0.8 :
|
||||
1.0;
|
||||
|
||||
const chargeReserveModeFactor =
|
||||
strategy.forceChargeRequested ? 0.8 :
|
||||
strategy.forceGridRequested ? 1.1 :
|
||||
strategy.userStrategy === 'auto_solar' ? 1.1 :
|
||||
1.0;
|
||||
|
||||
const toleranceModeFactor =
|
||||
strategy.forceChargeRequested ? 1.25 :
|
||||
strategy.forceGridRequested ? 0.8 :
|
||||
strategy.userStrategy === 'auto_solar' ? 0.8 :
|
||||
1.0;
|
||||
|
||||
const batteryDischargeSoftDynamicA = band.dischargeSoftA * dischargeModeFactor;
|
||||
const batteryDischargeHardDynamicA = band.dischargeHardA * dischargeModeFactor;
|
||||
const batteryChargeReserveDynamicA = Math.max(0, band.chargeReserveA * chargeReserveModeFactor);
|
||||
const batterySoftToleranceMinutes = band.softToleranceMin * toleranceModeFactor;
|
||||
const batteryHardToleranceSeconds = band.hardToleranceSec;
|
||||
const batteryRecoveryMinutes = band.recoveryMin;
|
||||
|
||||
// =====================================================
|
||||
// CONTEXT HELPERS
|
||||
// =====================================================
|
||||
const houseSpikeThresholdW = 1500;
|
||||
const houseSpikeDetected =
|
||||
(parsed.houseAcPowerW - parsed.houseAcPowerPrevW) >= houseSpikeThresholdW;
|
||||
|
||||
const solarPotentialContext =
|
||||
context.sourceMode === 'solar_potential' ||
|
||||
(
|
||||
parsed.soc >= 80 &&
|
||||
derived.forecastBufferKwh > 0 &&
|
||||
parsed.pvPowerW < context.effectivePvStartMinW &&
|
||||
derived.houseNonEvcsW < 1200
|
||||
);
|
||||
|
||||
// =====================================================
|
||||
// OUTSIDE-WINDOW LOCAL BYPASS
|
||||
// IMPORTANT:
|
||||
// en auto_solar_plus ou force_charge_now, un contexte local solaire
|
||||
// ne doit plus rester bloqué juste parce qu'on est hors fenêtre.
|
||||
// =====================================================
|
||||
const outsideWindowLocalBypass =
|
||||
context.profile === 'outside_window' &&
|
||||
!strategy.forceGridRequested &&
|
||||
(
|
||||
strategy.forceChargeRequested ||
|
||||
strategy.userStrategy === 'auto_solar_plus'
|
||||
) &&
|
||||
isLocalSolarSource;
|
||||
|
||||
// =====================================================
|
||||
// REASON-BASED LOCKS
|
||||
// =====================================================
|
||||
const nextWindowCandidates = [
|
||||
nextDailyOccurrenceMs(params.morningStartMin, nowMs),
|
||||
nextDailyOccurrenceMs(params.hcMiddayStartMin, nowMs),
|
||||
nextDailyOccurrenceMs(params.eveningStartMin, nowMs),
|
||||
nextDailyOccurrenceMs(params.hcNightStartMin, nowMs),
|
||||
].filter(v => Number.isFinite(v));
|
||||
|
||||
const nextAllowedWindowStartMs =
|
||||
nextWindowCandidates.length > 0 ? Math.min(...nextWindowCandidates) : null;
|
||||
|
||||
const blockActive =
|
||||
history.blockedUntilMs !== null &&
|
||||
history.blockedUntilMs > nowMs + 1000;
|
||||
|
||||
const blockStillRelevant =
|
||||
history.lastBlockReason === 'outside_window'
|
||||
? (context.profile === 'outside_window' && !outsideWindowLocalBypass)
|
||||
: history.lastBlockReason === 'probe_failed'
|
||||
? context.sourceMode === 'hybrid_probe'
|
||||
: history.lastBlockReason === 'battery_protection_stop'
|
||||
? (
|
||||
parsed.batteryCurrentA < -params.batteryNeutralBandA ||
|
||||
derived.anyVebusLowBattery ||
|
||||
derived.vebusDcVoltageSoftLow
|
||||
)
|
||||
: history.lastBlockReason === 'grid_battery_guard'
|
||||
? isGridSource
|
||||
: history.lastBlockReason === 'vebus_overload_persistent'
|
||||
? derived.anyVebusOverload
|
||||
: false;
|
||||
|
||||
const startBlockedByReason = blockActive && blockStillRelevant;
|
||||
|
||||
// =====================================================
|
||||
// CLOUD DEBT
|
||||
// =====================================================
|
||||
const batteryNetPowerW = parsed.batteryCurrentA * derived.batteryVoltageRef;
|
||||
let cloudDebtNextKwh = params.cloudDebtPrevKwh;
|
||||
|
||||
if (sessionObservedActive) {
|
||||
if (batteryNetPowerW < 0 && !isGridSource) {
|
||||
cloudDebtNextKwh += (Math.abs(batteryNetPowerW) * cfg.sampleSeconds) / 3600000;
|
||||
} else if (batteryNetPowerW > 0) {
|
||||
cloudDebtNextKwh -= (batteryNetPowerW * cfg.sampleSeconds) / 3600000;
|
||||
}
|
||||
} else if (batteryNetPowerW > 0) {
|
||||
cloudDebtNextKwh -= (batteryNetPowerW * cfg.sampleSeconds) / 3600000;
|
||||
}
|
||||
|
||||
cloudDebtNextKwh = clamp(cloudDebtNextKwh, 0, params.cloudDebtMaxKwh * 4);
|
||||
|
||||
const cloudDebtMeaningful =
|
||||
cloudDebtNextKwh > cfg.cloudDebtMeaningfulThresholdKwh;
|
||||
|
||||
const cloudEventDischargingNow =
|
||||
sessionObservedActive &&
|
||||
!isGridSource &&
|
||||
parsed.batteryCurrentA < -Math.max(1, params.batteryNeutralBandA) &&
|
||||
cloudDebtMeaningful;
|
||||
|
||||
const continuingCloudEvent =
|
||||
cloudEventDischargingNow &&
|
||||
history.cloudDebtStartedMsPrev !== null &&
|
||||
params.cloudDebtPrevKwh > cfg.cloudDebtRecoveryThresholdKwh;
|
||||
|
||||
const enteringCloudDebt =
|
||||
cloudEventDischargingNow && !continuingCloudEvent;
|
||||
|
||||
const cloudDebtActive = cloudEventDischargingNow;
|
||||
|
||||
const cloudDebtStartedMs =
|
||||
cloudEventDischargingNow
|
||||
? (continuingCloudEvent ? history.cloudDebtStartedMsPrev : nowMs)
|
||||
: null;
|
||||
|
||||
const cloudDebtDurationMinutes =
|
||||
cloudDebtActive && cloudDebtStartedMs !== null
|
||||
? Math.max(0, (nowMs - cloudDebtStartedMs) / 60000)
|
||||
: 0;
|
||||
|
||||
const effectiveCloudToleranceMinutes =
|
||||
params.cloudToleranceMinutes +
|
||||
(solarPotentialContext ? batteryTune.global.solarPotentialGraceMinutes : 0) +
|
||||
(houseSpikeDetected ? batteryTune.global.houseSpikeGraceMinutes : 0);
|
||||
|
||||
const cloudToleranceOk =
|
||||
cloudDebtNextKwh <= params.cloudDebtMaxKwh &&
|
||||
(
|
||||
!cloudDebtActive ||
|
||||
cloudDebtDurationMinutes <= effectiveCloudToleranceMinutes
|
||||
);
|
||||
|
||||
// =====================================================
|
||||
// LEGACY / FORECAST MODES
|
||||
// =====================================================
|
||||
const batteryFullHarvestMode =
|
||||
parsed.forecastModeEnabled &&
|
||||
context.profile !== 'night_hc' &&
|
||||
context.profile !== 'outside_window' &&
|
||||
parsed.soc >= params.batteryFullSoc &&
|
||||
parsed.todayRemainingKwh >= params.batteryFullForecastMinKwh &&
|
||||
derived.projectedEndSocAfterCharge >= (derived.endDayGuardSoc - 2);
|
||||
|
||||
const forecastCommitMode =
|
||||
parsed.forecastModeEnabled &&
|
||||
context.profile !== 'night_hc' &&
|
||||
context.profile !== 'outside_window' &&
|
||||
parsed.soc >= params.forecastCommitSocFloor &&
|
||||
parsed.todayRemainingKwh >= params.forecastCommitMinKwh &&
|
||||
derived.projectedEndSocAfterCharge >= derived.endDayGuardSoc;
|
||||
|
||||
const batteryPriorityMode =
|
||||
!batteryFullHarvestMode && !forecastCommitMode;
|
||||
|
||||
// =====================================================
|
||||
// EVCS NIGHT ENGINE
|
||||
// =====================================================
|
||||
const nightSessionActive =
|
||||
context.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;
|
||||
|
||||
const batteryMorningTargetPct =
|
||||
parsed.todayRemainingKwh >= params.evcsNightStrongForecastKwh
|
||||
? params.evcsNightTargetStrongForecastPct
|
||||
: parsed.todayRemainingKwh >= 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 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(lifecycle.minutesUntilDeparture) &&
|
||||
lifecycle.minutesUntilDeparture >= 0 &&
|
||||
(
|
||||
batteryRecoveryMinutesNeeded + params.evcsNightDepartureSafetyMin
|
||||
>= lifecycle.minutesUntilDeparture
|
||||
);
|
||||
|
||||
const batteryBelowHardFloor =
|
||||
parsed.soc <= batteryHardFloorPct;
|
||||
|
||||
const batteryBelowMorningTarget =
|
||||
parsed.soc < batteryMorningTargetPct;
|
||||
|
||||
const batteryAtOrBelowGridTakeoverTrigger =
|
||||
parsed.soc <= gridTakeoverTriggerPct;
|
||||
|
||||
const batteryAssistAllowed =
|
||||
nightSessionActive &&
|
||||
!batteryBelowHardFloor &&
|
||||
!batteryRecoveryUrgent &&
|
||||
parsed.soc > gridTakeoverTriggerPct;
|
||||
|
||||
const nightGridTakeoverNeeded =
|
||||
nightSessionActive &&
|
||||
(
|
||||
batteryBelowHardFloor ||
|
||||
batteryAtOrBelowGridTakeoverTrigger ||
|
||||
batteryRecoveryUrgent
|
||||
);
|
||||
|
||||
const nightPreferredSourceMode =
|
||||
!nightSessionActive
|
||||
? '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;
|
||||
|
||||
// =====================================================
|
||||
// PROFILE-BASED THRESHOLDS
|
||||
// =====================================================
|
||||
const profileSocStartFloor =
|
||||
context.profile === 'morning_solar' ? params.morningSocMin :
|
||||
context.profile === 'midday_hc_or_solar' ? params.middaySocMin :
|
||||
context.profile === 'evening_high_soc' ? params.eveningSocStartMin :
|
||||
params.socStartMin;
|
||||
|
||||
const profileSocHoldFloor =
|
||||
context.profile === 'morning_solar' ? Math.max(params.socStopMin, params.morningSocMin - 10) :
|
||||
context.profile === 'midday_hc_or_solar' ? Math.max(params.socStopMin, params.middaySocMin - 15) :
|
||||
context.profile === 'evening_high_soc' ? params.eveningSocStopMin :
|
||||
params.socStopMin;
|
||||
|
||||
const profileSpecificDischargeFloorA =
|
||||
context.profile === 'morning_solar' ? params.morningBatteryMaxDischargeA :
|
||||
context.profile === 'midday_hc_or_solar' ? params.middayBatteryMaxDischargeA :
|
||||
context.profile === 'evening_high_soc' ? params.eveningBatteryMaxDischargeA :
|
||||
params.batteryDischargeSoftA;
|
||||
|
||||
const profileBatteryDischargeFloorA =
|
||||
Math.max(profileSpecificDischargeFloorA, batteryDischargeSoftDynamicA);
|
||||
|
||||
// =====================================================
|
||||
// GRID RELAX IN force_charge_now
|
||||
// =====================================================
|
||||
// Assouplissement uniquement pour la garde réseau,
|
||||
// sans toucher aux sécurités dures globales.
|
||||
const gridForceChargeRelaxSoftA =
|
||||
strategy.forceChargeRequested && isGridSource
|
||||
? (
|
||||
parsed.soc >= 90 ? 28 :
|
||||
parsed.soc >= 80 ? 16 :
|
||||
parsed.soc >= 60 ? 8 :
|
||||
parsed.soc >= 40 ? 4 :
|
||||
0
|
||||
)
|
||||
: 0;
|
||||
|
||||
const gridForceChargeRelaxHardA =
|
||||
strategy.forceChargeRequested && isGridSource
|
||||
? (
|
||||
parsed.soc >= 90 ? 39 :
|
||||
parsed.soc >= 80 ? 29 :
|
||||
parsed.soc >= 60 ? 18 :
|
||||
parsed.soc >= 40 ? 8 :
|
||||
0
|
||||
)
|
||||
: 0;
|
||||
|
||||
const gridBatterySoftDynamicABase =
|
||||
Math.max(params.gridBatterySoftA, batteryDischargeSoftDynamicA);
|
||||
|
||||
const gridBatteryHardDynamicABase =
|
||||
Math.max(params.gridBatteryHardA, batteryDischargeHardDynamicA);
|
||||
|
||||
let gridBatterySoftDynamicA =
|
||||
gridBatterySoftDynamicABase - gridForceChargeRelaxSoftA;
|
||||
|
||||
let gridBatteryHardDynamicA =
|
||||
gridBatteryHardDynamicABase - gridForceChargeRelaxHardA;
|
||||
|
||||
// Garantit que le hard reste plus permissif que le soft d'au moins 3 A
|
||||
if (gridBatteryHardDynamicA > (gridBatterySoftDynamicA - 3)) {
|
||||
gridBatteryHardDynamicA = gridBatterySoftDynamicA - 3;
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// BASE SAFETY
|
||||
// =====================================================
|
||||
const commonSafetyStartOk =
|
||||
derived.canRunAtMinCurrent &&
|
||||
!derived.anyVebusOverload &&
|
||||
!derived.anyVebusLowBattery &&
|
||||
!derived.vebusDcVoltageSoftLow &&
|
||||
!derived.vebusAcOutSoftReached &&
|
||||
parsed.evcsDeviceConnected &&
|
||||
parsed.evcsVehicleConnected &&
|
||||
!parsed.evcsModeIsAuto;
|
||||
|
||||
const commonSafetyHoldOk =
|
||||
derived.canRunAtMinCurrent &&
|
||||
!derived.anyVebusLowBattery &&
|
||||
!derived.vebusDcVoltageHardLow &&
|
||||
parsed.evcsDeviceConnected &&
|
||||
parsed.evcsVehicleConnected &&
|
||||
!parsed.evcsModeIsAuto;
|
||||
|
||||
// =====================================================
|
||||
// BATTERY-ASSISTED SOLAR START/HOLD
|
||||
// IMPORTANT:
|
||||
// Installation sans injection:
|
||||
// le PV visible peut rester bas tant que l'EVCS ne démarre pas.
|
||||
// On autorise donc un démarrage/maintien local si la batterie est haute
|
||||
// et que la dette nuage reste faible.
|
||||
// =====================================================
|
||||
const batteryAssistSolarPolicyEligible =
|
||||
!strategy.forceGridRequested &&
|
||||
(
|
||||
strategy.forceChargeRequested ||
|
||||
strategy.userStrategy === 'auto_solar_plus'
|
||||
) &&
|
||||
isLocalSolarSource;
|
||||
|
||||
const batteryAssistSolarStartSocMin = 90;
|
||||
const batteryAssistSolarHoldSocMin = 85;
|
||||
|
||||
const batteryAssistSolarDebtStartMaxKwh = Math.max(
|
||||
cfg.cloudDebtRecoveryThresholdKwh,
|
||||
Math.min(params.cloudDebtMaxKwh, 0.05)
|
||||
);
|
||||
|
||||
const batteryAssistSolarStartOk =
|
||||
batteryAssistSolarPolicyEligible &&
|
||||
commonSafetyStartOk &&
|
||||
!isGridSource &&
|
||||
parsed.soc >= Math.max(profileSocStartFloor, batteryAssistSolarStartSocMin) &&
|
||||
parsed.soc > batteryAssistSolarHoldSocMin &&
|
||||
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
||||
cloudDebtNextKwh <= batteryAssistSolarDebtStartMaxKwh &&
|
||||
!cloudDebtActive;
|
||||
|
||||
const batteryAssistSolarHoldOk =
|
||||
batteryAssistSolarPolicyEligible &&
|
||||
commonSafetyHoldOk &&
|
||||
!isGridSource &&
|
||||
parsed.soc >= Math.max(profileSocHoldFloor, batteryAssistSolarHoldSocMin) &&
|
||||
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
||||
cloudToleranceOk;
|
||||
|
||||
// =====================================================
|
||||
// START / HOLD CONDITIONS
|
||||
// =====================================================
|
||||
const rawSolarStartOk =
|
||||
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
|
||||
commonSafetyStartOk &&
|
||||
parsed.soc >= profileSocStartFloor &&
|
||||
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
||||
(
|
||||
context.sourceMode === 'solar_potential' ||
|
||||
parsed.pvPowerW >= context.effectivePvStartMinW
|
||||
);
|
||||
|
||||
const solarStartOk =
|
||||
rawSolarStartOk || batteryAssistSolarStartOk;
|
||||
|
||||
const rawSolarHoldOk =
|
||||
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
|
||||
commonSafetyHoldOk &&
|
||||
parsed.soc >= profileSocHoldFloor &&
|
||||
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
|
||||
(
|
||||
parsed.pvPowerW >= params.pvHoldMinW ||
|
||||
cloudToleranceOk
|
||||
);
|
||||
|
||||
const solarHoldOk =
|
||||
rawSolarHoldOk || batteryAssistSolarHoldOk;
|
||||
|
||||
// FIX V2.5: Garde SOC - pas de grid pour EVCS si SOC > 35% sauf force_grid_now
|
||||
const gridAllowedBySoc = parsed.soc <= 35 || strategy.forceGridRequested;
|
||||
|
||||
const gridStartOk =
|
||||
gridAllowedBySoc &&
|
||||
isGridSource &&
|
||||
commonSafetyStartOk &&
|
||||
parsed.gridAvailable &&
|
||||
parsed.batteryCurrentA >= gridBatterySoftDynamicA;
|
||||
|
||||
const gridHoldOk =
|
||||
gridAllowedBySoc &&
|
||||
isGridSource &&
|
||||
commonSafetyHoldOk &&
|
||||
parsed.gridAvailable &&
|
||||
parsed.batteryCurrentA >= gridBatteryHardDynamicA;
|
||||
|
||||
const batteryAssistNightStartOk =
|
||||
context.sourceMode === 'battery_assist_night' &&
|
||||
commonSafetyStartOk &&
|
||||
nightSessionActive &&
|
||||
batteryAssistAllowed &&
|
||||
parsed.soc > batteryHardFloorPct &&
|
||||
parsed.soc > gridTakeoverTriggerPct &&
|
||||
parsed.batteryCurrentA >= profileBatteryDischargeFloorA;
|
||||
|
||||
const batteryAssistNightHoldOk =
|
||||
context.sourceMode === 'battery_assist_night' &&
|
||||
commonSafetyHoldOk &&
|
||||
nightSessionActive &&
|
||||
batteryAssistAllowed &&
|
||||
parsed.soc > batteryHardFloorPct &&
|
||||
parsed.soc > gridTakeoverTriggerPct &&
|
||||
parsed.batteryCurrentA >= batteryDischargeHardDynamicA;
|
||||
|
||||
// =====================================================
|
||||
// PROBE LOGIC — STRICTEMENT LIMITÉE AU MODE PROBE
|
||||
// =====================================================
|
||||
const probeModeActive = context.sourceMode === 'hybrid_probe';
|
||||
|
||||
const probeStartOk =
|
||||
probeModeActive &&
|
||||
commonSafetyStartOk &&
|
||||
(context.morningProbeEligible || context.middayProbeEligible);
|
||||
|
||||
const probeHoldGuardOk =
|
||||
probeModeActive &&
|
||||
commonSafetyHoldOk &&
|
||||
parsed.batteryCurrentA >= profileBatteryDischargeFloorA;
|
||||
|
||||
const probeWindowActive =
|
||||
probeModeActive &&
|
||||
sessionObservedActive &&
|
||||
history.probeStartedMs !== null &&
|
||||
lifecycle.minutesSinceProbeStart !== null &&
|
||||
lifecycle.minutesSinceProbeStart < params.probeMinutes;
|
||||
|
||||
const probeElapsedReached =
|
||||
probeModeActive &&
|
||||
sessionObservedActive &&
|
||||
history.probeStartedMs !== null &&
|
||||
lifecycle.minutesSinceProbeStart !== null &&
|
||||
lifecycle.minutesSinceProbeStart >= params.probeMinutes;
|
||||
|
||||
const probeSuccessNow =
|
||||
probeModeActive &&
|
||||
sessionObservedActive &&
|
||||
history.probeStartedMs !== null &&
|
||||
(
|
||||
parsed.pvPowerW >= params.pvHoldMinW &&
|
||||
parsed.batteryCurrentA >= Math.min(0, batteryDischargeSoftDynamicA)
|
||||
);
|
||||
|
||||
const probeFailedNow =
|
||||
probeModeActive &&
|
||||
probeElapsedReached &&
|
||||
!probeSuccessNow;
|
||||
|
||||
// =====================================================
|
||||
// STEP-UP LOGIC
|
||||
// =====================================================
|
||||
const stepUpByBatteryReserve =
|
||||
batteryPriorityMode &&
|
||||
parsed.batteryCurrentA >= batteryChargeReserveDynamicA;
|
||||
|
||||
const stepUpByForecastMode =
|
||||
!batteryPriorityMode &&
|
||||
(
|
||||
parsed.pvPowerW >= params.pvHoldMinW ||
|
||||
parsed.pvPowerAvg10mW >= params.pvHoldMinW
|
||||
) &&
|
||||
derived.forecastBufferKwh >= -1 &&
|
||||
derived.projectedEndSocAfterCharge >= (derived.endDayGuardSoc - 5) &&
|
||||
cloudToleranceOk;
|
||||
|
||||
const solarShouldStepUp =
|
||||
lifecycle.currentCooldownElapsed &&
|
||||
derived.currentReferenceA < derived.physicalCapA &&
|
||||
derived.physicalCapA >= params.currentMinA &&
|
||||
!derived.vebusAcOutSoftReached &&
|
||||
!derived.vebusDcVoltageSoftLow &&
|
||||
!derived.houseLoadHighVsHeadroom &&
|
||||
(
|
||||
stepUpByBatteryReserve ||
|
||||
stepUpByForecastMode ||
|
||||
parsed.pvPowerW >= (context.effectivePvStartMinW + 400)
|
||||
);
|
||||
|
||||
const gridShouldStepUp =
|
||||
lifecycle.currentCooldownElapsed &&
|
||||
derived.currentReferenceA < derived.physicalCapA &&
|
||||
derived.physicalCapA >= params.currentMinA &&
|
||||
!derived.vebusAcOutSoftReached &&
|
||||
!derived.vebusDcVoltageSoftLow &&
|
||||
parsed.batteryCurrentA >= gridBatterySoftDynamicA &&
|
||||
!derived.houseLoadHighVsHeadroom;
|
||||
|
||||
const batteryAssistNightShouldStepUp =
|
||||
context.sourceMode === 'battery_assist_night' &&
|
||||
lifecycle.currentCooldownElapsed &&
|
||||
derived.currentReferenceA < derived.physicalCapA &&
|
||||
derived.physicalCapA >= params.currentMinA &&
|
||||
!derived.vebusAcOutSoftReached &&
|
||||
!derived.vebusDcVoltageSoftLow &&
|
||||
!derived.houseLoadHighVsHeadroom &&
|
||||
parsed.batteryCurrentA >= batteryChargeReserveDynamicA &&
|
||||
batteryAssistAllowed;
|
||||
|
||||
// =====================================================
|
||||
// STEP-DOWN LOGIC
|
||||
// =====================================================
|
||||
const solarShouldStepDownBattery =
|
||||
parsed.batteryCurrentA <= batteryDischargeSoftDynamicA &&
|
||||
lifecycle.currentCooldownElapsed &&
|
||||
derived.currentReferenceA > params.currentMinA;
|
||||
|
||||
const solarShouldStepDownCloudDebt =
|
||||
!cloudToleranceOk &&
|
||||
lifecycle.currentCooldownElapsed &&
|
||||
derived.currentReferenceA > params.currentMinA;
|
||||
|
||||
const gridShouldStepDownBattery =
|
||||
parsed.batteryCurrentA < gridBatterySoftDynamicA &&
|
||||
lifecycle.currentCooldownElapsed &&
|
||||
derived.currentReferenceA > params.currentMinA;
|
||||
|
||||
const batteryAssistNightShouldStepDown =
|
||||
context.sourceMode === 'battery_assist_night' &&
|
||||
lifecycle.currentCooldownElapsed &&
|
||||
derived.currentReferenceA > params.currentMinA &&
|
||||
(
|
||||
parsed.batteryCurrentA <= batteryDischargeSoftDynamicA ||
|
||||
parsed.soc <= gridTakeoverTriggerPct ||
|
||||
batteryRecoveryUrgent
|
||||
);
|
||||
|
||||
const shouldStepDownVebusPower =
|
||||
derived.vebusAcOutSoftReached &&
|
||||
lifecycle.currentCooldownElapsed &&
|
||||
derived.currentReferenceA > params.currentMinA;
|
||||
|
||||
const shouldStepDownVebusVoltage =
|
||||
derived.vebusDcVoltageSoftLow &&
|
||||
lifecycle.currentCooldownElapsed &&
|
||||
derived.currentReferenceA > params.currentMinA;
|
||||
|
||||
const shouldStepDownOverload =
|
||||
derived.anyVebusOverload &&
|
||||
lifecycle.currentCooldownElapsed &&
|
||||
derived.currentReferenceA > params.currentMinA;
|
||||
|
||||
const shouldStopOverloadAtMin =
|
||||
derived.anyVebusOverload &&
|
||||
derived.currentReferenceA <= params.currentMinA &&
|
||||
sessionObservedActive;
|
||||
|
||||
// =====================================================
|
||||
// HARD STOPS / PROTECTIONS
|
||||
// =====================================================
|
||||
const batteryCurrentHardExceeded =
|
||||
parsed.batteryCurrentA <= batteryDischargeHardDynamicA;
|
||||
|
||||
// overload volontairement exclu du hard stop immédiat
|
||||
const mustStopHard =
|
||||
derived.anyVebusLowBattery ||
|
||||
derived.vebusDcVoltageHardLow ||
|
||||
derived.vebusAcOutHardReached ||
|
||||
batteryCurrentHardExceeded;
|
||||
|
||||
const gridBatteryProtectionStop =
|
||||
sessionObservedActive &&
|
||||
isGridSource &&
|
||||
parsed.batteryCurrentA < gridBatteryHardDynamicA;
|
||||
|
||||
// =====================================================
|
||||
// OUTPUT
|
||||
// =====================================================
|
||||
return [{
|
||||
json: {
|
||||
...prev,
|
||||
|
||||
batteryPolicy: {
|
||||
socBand,
|
||||
batteryDischargeSoftDynamicA,
|
||||
batteryDischargeHardDynamicA,
|
||||
batteryChargeReserveDynamicA,
|
||||
batterySoftToleranceMinutes,
|
||||
batteryHardToleranceSeconds,
|
||||
batteryRecoveryMinutes,
|
||||
|
||||
gridBatterySoftDynamicA,
|
||||
gridBatteryHardDynamicA,
|
||||
gridForceChargeRelaxSoftA,
|
||||
gridForceChargeRelaxHardA,
|
||||
|
||||
batteryAssistSolarStartSocMin,
|
||||
batteryAssistSolarHoldSocMin,
|
||||
batteryAssistSolarDebtStartMaxKwh,
|
||||
|
||||
houseSpikeDetected,
|
||||
solarPotentialContext,
|
||||
outsideWindowLocalBypass,
|
||||
},
|
||||
|
||||
locks: {
|
||||
nextAllowedWindowStartMs,
|
||||
blockActive,
|
||||
blockStillRelevant,
|
||||
startBlockedByReason,
|
||||
},
|
||||
|
||||
cloud: {
|
||||
sessionObservedActive,
|
||||
batteryNetPowerW,
|
||||
cloudDebtNextKwh,
|
||||
cloudDebtMeaningful,
|
||||
cloudEventDischargingNow,
|
||||
continuingCloudEvent,
|
||||
enteringCloudDebt,
|
||||
cloudDebtActive,
|
||||
cloudDebtStartedMs,
|
||||
cloudDebtDurationMinutes,
|
||||
effectiveCloudToleranceMinutes,
|
||||
cloudToleranceOk,
|
||||
},
|
||||
|
||||
modes: {
|
||||
batteryFullHarvestMode,
|
||||
forecastCommitMode,
|
||||
batteryPriorityMode,
|
||||
},
|
||||
|
||||
nightEvcs: {
|
||||
nightSessionActive,
|
||||
nightSessionNeedsCapture,
|
||||
nightSessionNeedsClear,
|
||||
nightSessionStartSocPct,
|
||||
nightSessionStartAt,
|
||||
nightSessionStartMs,
|
||||
|
||||
batteryHardFloorPct,
|
||||
batteryMorningTargetPct,
|
||||
maxBatteryDischargePoints,
|
||||
gridTakeoverTriggerPct,
|
||||
|
||||
batteryBelowHardFloor,
|
||||
batteryBelowMorningTarget,
|
||||
batteryAtOrBelowGridTakeoverTrigger,
|
||||
|
||||
batteryKwhMissingToTarget,
|
||||
batteryRecoveryMinutesNeeded,
|
||||
batteryRecoveryUrgent,
|
||||
batteryRecoveryAllowedNow,
|
||||
batteryTargetReached,
|
||||
|
||||
batteryAssistAllowed,
|
||||
nightGridTakeoverNeeded,
|
||||
nightPreferredSourceMode,
|
||||
},
|
||||
|
||||
safeguards: {
|
||||
isGridSource,
|
||||
commonSafetyStartOk,
|
||||
commonSafetyHoldOk,
|
||||
profileSocStartFloor,
|
||||
profileSocHoldFloor,
|
||||
profileBatteryDischargeFloorA,
|
||||
batteryCurrentHardExceeded,
|
||||
mustStopHard,
|
||||
gridBatteryProtectionStop,
|
||||
},
|
||||
|
||||
conditions: {
|
||||
rawSolarStartOk,
|
||||
rawSolarHoldOk,
|
||||
batteryAssistSolarPolicyEligible,
|
||||
batteryAssistSolarStartOk,
|
||||
batteryAssistSolarHoldOk,
|
||||
|
||||
solarStartOk,
|
||||
solarHoldOk,
|
||||
gridStartOk,
|
||||
gridHoldOk,
|
||||
|
||||
batteryAssistNightStartOk,
|
||||
batteryAssistNightHoldOk,
|
||||
|
||||
probeStartOk,
|
||||
probeHoldGuardOk,
|
||||
probeWindowActive,
|
||||
probeElapsedReached,
|
||||
probeSuccessNow,
|
||||
probeFailedNow,
|
||||
|
||||
solarShouldStepUp,
|
||||
gridShouldStepUp,
|
||||
batteryAssistNightShouldStepUp,
|
||||
|
||||
solarShouldStepDownBattery,
|
||||
solarShouldStepDownCloudDebt,
|
||||
gridShouldStepDownBattery,
|
||||
batteryAssistNightShouldStepDown,
|
||||
|
||||
shouldStepDownVebusPower,
|
||||
shouldStepDownVebusVoltage,
|
||||
shouldStepDownOverload,
|
||||
shouldStopOverloadAtMin,
|
||||
}
|
||||
}
|
||||
}];
|
||||
615
modified/EVCS_V2.5_PATCHED.json
Normal file
615
modified/EVCS_V2.5_PATCHED.json
Normal file
File diff suppressed because one or more lines are too long
645
modified/GRID_COMPUTE_patched.js
Normal file
645
modified/GRID_COMPUTE_patched.js
Normal file
@@ -0,0 +1,645 @@
|
||||
function num(v, fallback = NaN) {
|
||||
const n = Number.parseFloat(v);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
function low(v) {
|
||||
return (v ?? '').toString().trim().toLowerCase();
|
||||
}
|
||||
|
||||
function parseMaybeJson(v) {
|
||||
if (typeof v !== 'string') return v;
|
||||
try {
|
||||
return JSON.parse(v);
|
||||
} catch {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
function extractSnapshot(input) {
|
||||
if (typeof input === 'string') return parseMaybeJson(input);
|
||||
if (input?.data && typeof input.data === 'string') return parseMaybeJson(input.data);
|
||||
if (input?.body && typeof input.body === 'string') return parseMaybeJson(input.body);
|
||||
if (input?.data && typeof input.data === 'object') return input.data;
|
||||
if (input?.body && typeof input.body === 'object') return input.body;
|
||||
return input;
|
||||
}
|
||||
|
||||
function round1(v) {
|
||||
return Math.round(v * 10) / 10;
|
||||
}
|
||||
|
||||
function clamp(v, min, max) {
|
||||
return Math.min(Math.max(v, min), max);
|
||||
}
|
||||
|
||||
function pickDefined(...values) {
|
||||
for (const v of values) {
|
||||
if (v === undefined || v === null) continue;
|
||||
if (typeof v === 'string' && v.trim() === '') continue;
|
||||
return v;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isOnLike(v) {
|
||||
const s = low(v);
|
||||
return s === 'on' || s === 'true' || s === '1' || s === 'yes';
|
||||
}
|
||||
|
||||
function getParisNowParts() {
|
||||
const fmt = new Intl.DateTimeFormat('fr-FR', {
|
||||
timeZone: 'Europe/Paris',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
|
||||
const parts = Object.fromEntries(
|
||||
fmt.formatToParts(new Date())
|
||||
.filter(p => p.type !== 'literal')
|
||||
.map(p => [p.type, p.value])
|
||||
);
|
||||
|
||||
const date = `${parts.year}-${parts.month}-${parts.day}`;
|
||||
const stamp = `${date} ${parts.hour}:${parts.minute}:${parts.second}`;
|
||||
|
||||
return {
|
||||
year: Number(parts.year),
|
||||
month: Number(parts.month),
|
||||
day: Number(parts.day),
|
||||
hour: Number(parts.hour),
|
||||
minute: Number(parts.minute),
|
||||
second: Number(parts.second),
|
||||
date,
|
||||
stamp,
|
||||
};
|
||||
}
|
||||
|
||||
function parseLocalStampToMs(s) {
|
||||
const text = (s ?? '').toString().trim();
|
||||
const m = text.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
|
||||
if (!m) return null;
|
||||
|
||||
const y = Number(m[1]);
|
||||
const mo = Number(m[2]);
|
||||
const d = Number(m[3]);
|
||||
const h = Number(m[4]);
|
||||
const mi = Number(m[5]);
|
||||
const se = Number(m[6]);
|
||||
|
||||
if (![y, mo, d, h, mi, se].every(Number.isFinite)) return null;
|
||||
|
||||
return new Date(y, mo - 1, d, h, mi, se).getTime();
|
||||
}
|
||||
|
||||
const cfg = {
|
||||
mobileNotifyDomain: 'notify',
|
||||
mobileNotifyService: 'mobile_app_iphone_16_de_tt',
|
||||
};
|
||||
|
||||
const raw = $input.first().json;
|
||||
const input = extractSnapshot(raw);
|
||||
const now = getParisNowParts();
|
||||
const nowMs = parseLocalStampToMs(now.stamp);
|
||||
|
||||
// ======================================================
|
||||
// INPUTS PRINCIPAUX
|
||||
// ======================================================
|
||||
const soc = clamp(num(input.soc_percent, 0), 0, 100);
|
||||
const gridIsOn = low(input.grid_state || 'off') === 'on';
|
||||
const pvPowerW = Math.max(0, num(input.pv_power_w, 0));
|
||||
|
||||
const houseAcPowerW = Math.max(0, num(input.house_ac_power_w, 0));
|
||||
const gridPowerW = num(input.grid_power_w, 0);
|
||||
|
||||
const evcsAcPowerW = Math.max(0, num(input.evcs_ac_power_w, 0));
|
||||
const evcsCharging = low(input.evcs_charging || 'off') === 'on';
|
||||
const evcsStatusRaw = num(input.evcs_status_raw, NaN);
|
||||
|
||||
const hysteresisEnabled = low(input.hysteresis_enabled || 'off') === 'on';
|
||||
const notificationsEnabled = low(input.notifications_enabled || 'off') === 'on';
|
||||
const latchIsOn = low(input.grid_latch || 'off') === 'on';
|
||||
|
||||
const gridSocOn = clamp(num(input.grid_soc_on, 22), 0, 100);
|
||||
const gridSocOff = clamp(num(input.grid_soc_off, 35), 0, 100);
|
||||
const gridMinOnMinutes = Math.max(0, num(input.grid_min_on_minutes, 45));
|
||||
const gridCooldownMinutes = Math.max(0, num(input.grid_cooldown_minutes, 15));
|
||||
const acLoadsMaxSafeW = Math.max(0, num(input.ac_loads_max_safe_w, 7800));
|
||||
|
||||
const lastOnAt = (input.last_on_at ?? '').toString().trim();
|
||||
const lastOffAt = (input.last_off_at ?? '').toString().trim();
|
||||
const lastSignature = (input.last_signature ?? '').toString().trim();
|
||||
|
||||
const lastOnMs = parseLocalStampToMs(lastOnAt);
|
||||
const lastOffMs = parseLocalStampToMs(lastOffAt);
|
||||
|
||||
const minutesSinceLastOn =
|
||||
lastOnMs !== null && nowMs !== null ? Math.max(0, (nowMs - lastOnMs) / 60000) : null;
|
||||
|
||||
const minutesSinceLastOff =
|
||||
lastOffMs !== null && nowMs !== null ? Math.max(0, (nowMs - lastOffMs) / 60000) : null;
|
||||
|
||||
const minOnElapsed =
|
||||
minutesSinceLastOn === null ? true : minutesSinceLastOn >= gridMinOnMinutes;
|
||||
|
||||
const cooldownElapsed =
|
||||
minutesSinceLastOff === null ? true : minutesSinceLastOff >= gridCooldownMinutes;
|
||||
|
||||
const socBand =
|
||||
soc <= gridSocOn ? 'low' :
|
||||
soc < gridSocOff ? 'mid' :
|
||||
'high';
|
||||
|
||||
const evcsSeemsActive = evcsCharging || evcsAcPowerW > 300;
|
||||
const acOverSafeLimit = houseAcPowerW >= acLoadsMaxSafeW;
|
||||
|
||||
// ======================================================
|
||||
// EVCS OVERRIDE INPUTS
|
||||
// ======================================================
|
||||
const evcsPriorityActive = isOnLike(
|
||||
pickDefined(
|
||||
input.evcs_priority_active,
|
||||
input.victron_evcs_priority_active,
|
||||
input['input_boolean.victron_evcs_priority_active']
|
||||
)
|
||||
);
|
||||
|
||||
const evcsNightGridTakeoverNeeded = isOnLike(
|
||||
pickDefined(
|
||||
input.night_grid_takeover_needed,
|
||||
input.victron_evcs_night_grid_takeover_needed,
|
||||
input['input_boolean.victron_evcs_night_grid_takeover_needed']
|
||||
)
|
||||
);
|
||||
|
||||
const evcsSourceModeRaw = (
|
||||
pickDefined(
|
||||
input.evcs_source_mode,
|
||||
input.victron_evcs_source_mode,
|
||||
input['input_text.victron_evcs_source_mode']
|
||||
) ?? ''
|
||||
).toString().trim();
|
||||
|
||||
const evcsSourceMode = low(evcsSourceModeRaw);
|
||||
|
||||
const userStrategyRaw = (
|
||||
pickDefined(
|
||||
input.user_strategy,
|
||||
input.victron_evcs_user_strategy,
|
||||
input['input_select.victron_evcs_user_strategy']
|
||||
) ?? ''
|
||||
).toString().trim();
|
||||
|
||||
const userStrategy = low(userStrategyRaw);
|
||||
|
||||
// ======================================================
|
||||
// EVCS SEMANTIC STATE
|
||||
// Ici on ne détecte PAS la voiture pleine par seuils.
|
||||
// On consomme seulement l'état sémantique déjà produit
|
||||
// par le workflow EVCS.
|
||||
// ======================================================
|
||||
const evcsSurplusStateRaw = (
|
||||
pickDefined(
|
||||
input.evcs_surplus_state,
|
||||
input.victron_evcs_surplus_state,
|
||||
input['input_text.victron_evcs_surplus_state']
|
||||
) ?? ''
|
||||
).toString().trim();
|
||||
|
||||
const evcsSurplusState = low(evcsSurplusStateRaw);
|
||||
|
||||
const evcsVehicleNoDemandSemantic =
|
||||
evcsSurplusState === 'vehicle_full_or_paused' ||
|
||||
evcsSurplusState === 'vehicle_not_requesting_power' ||
|
||||
evcsSurplusState === 'vehicle_full' ||
|
||||
evcsSurplusState === 'charge_complete' ||
|
||||
evcsSurplusState.includes('full_or_paused') ||
|
||||
evcsSurplusState.includes('not_requesting_power') ||
|
||||
evcsSurplusState.includes('charge_complete') ||
|
||||
evcsSurplusState.includes('no_demand');
|
||||
|
||||
// ======================================================
|
||||
// OVERRIDE EVCS VERS GRID
|
||||
// CORRECTION:
|
||||
// - evcsPriorityActive seul ne doit PLUS suffire à allumer EDF
|
||||
// - seuls les cas explicitement réseau peuvent forcer le grid
|
||||
// - ainsi "Réseau HC midi = OFF" redevient réellement respecté
|
||||
// ======================================================
|
||||
const evcsExplicitGridRequest =
|
||||
userStrategy === 'force_grid_now' ||
|
||||
userStrategy === 'force_charge_now' ||
|
||||
evcsNightGridTakeoverNeeded ||
|
||||
evcsSourceMode === 'réseau forcé' ||
|
||||
evcsSourceMode === 'réseau hc autorisé' ||
|
||||
evcsSourceMode === 'grid_forced' ||
|
||||
evcsSourceMode === 'grid_hc_allowed';
|
||||
|
||||
const evcsGridOverride =
|
||||
!evcsVehicleNoDemandSemantic &&
|
||||
evcsExplicitGridRequest;
|
||||
|
||||
// ======================================================
|
||||
// DÉCISION
|
||||
// ======================================================
|
||||
let state = 'observe_only';
|
||||
let reason = [];
|
||||
let desiredGrid = gridIsOn ? 'on' : 'off';
|
||||
let desiredLatch = latchIsOn ? 'on' : 'off';
|
||||
|
||||
// ======================================================
|
||||
// FORCE_GRID + VEHICLE FULL IMMEDIATE CUT
|
||||
// FIX V2.5: Si force_grid_now actif et voiture pleine,
|
||||
// couper EDF immédiatement sans attendre minOnRuntime.
|
||||
// ======================================================
|
||||
if (
|
||||
userStrategy === 'force_grid_now' &&
|
||||
evcsVehicleNoDemandSemantic &&
|
||||
latchIsOn
|
||||
) {
|
||||
desiredGrid = 'off';
|
||||
desiredLatch = 'off';
|
||||
state = 'turn_off_force_grid_vehicle_full';
|
||||
reason.push('Force grid + voiture pleine : coupure immediate du reseau');
|
||||
}
|
||||
|
||||
|
||||
// ======================================================
|
||||
// CORE HYSTERESIS LOGIC
|
||||
// ======================================================
|
||||
if (evcsGridOverride) {
|
||||
desiredGrid = 'on';
|
||||
desiredLatch = 'on';
|
||||
|
||||
if (!gridIsOn) {
|
||||
state = 'turn_on_evcs_override';
|
||||
reason.push('Override EVCS actif : activation EDF forcée');
|
||||
} else if (!latchIsOn) {
|
||||
state = 'adopt_existing_grid_on_evcs_override';
|
||||
reason.push('Override EVCS actif : EDF déjà ON, adoption par latch');
|
||||
} else {
|
||||
state = 'hold_on_evcs_override';
|
||||
reason.push('Override EVCS actif : maintien EDF ON');
|
||||
}
|
||||
|
||||
if (userStrategy === 'force_grid_now') {
|
||||
reason.push('Cause override : force_grid_now');
|
||||
} else if (userStrategy === 'force_charge_now') {
|
||||
reason.push('Cause override : force_charge_now');
|
||||
} else if (evcsNightGridTakeoverNeeded) {
|
||||
reason.push('Cause override : night_grid_takeover_needed');
|
||||
} else {
|
||||
reason.push(`Cause override : source_mode=${evcsSourceModeRaw || 'n/a'}`);
|
||||
}
|
||||
|
||||
} else if (!hysteresisEnabled) {
|
||||
state = 'hysteresis_disabled';
|
||||
desiredGrid = gridIsOn ? 'on' : 'off';
|
||||
desiredLatch = latchIsOn ? 'on' : 'off';
|
||||
reason.push('Automatisme hystérésis réseau désactivé');
|
||||
|
||||
} else if (soc <= gridSocOn) {
|
||||
desiredGrid = 'on';
|
||||
desiredLatch = 'on';
|
||||
|
||||
if (!cooldownElapsed && !gridIsOn) {
|
||||
state = 'turn_on_low_soc_cooldown_bypassed';
|
||||
reason.push(`SOC ${round1(soc)}% <= seuil ON ${gridSocOn}% : activation EDF malgré cooldown`);
|
||||
} else if (!gridIsOn) {
|
||||
state = 'turn_on_low_soc';
|
||||
reason.push(`SOC ${round1(soc)}% <= seuil ON ${gridSocOn}% : activation EDF`);
|
||||
} else if (!latchIsOn) {
|
||||
state = 'adopt_existing_grid_on_low_soc';
|
||||
reason.push(`SOC ${round1(soc)}% <= seuil ON ${gridSocOn}% : EDF déjà ON, prise de contrôle par latch`);
|
||||
} else {
|
||||
state = 'hold_on_low_soc';
|
||||
reason.push(`SOC encore bas ${round1(soc)}% : maintien EDF ON`);
|
||||
}
|
||||
|
||||
} else if (gridIsOn && !latchIsOn) {
|
||||
if (soc < gridSocOff) {
|
||||
desiredGrid = 'on';
|
||||
desiredLatch = 'on';
|
||||
state = 'adopt_existing_grid_on';
|
||||
reason.push(`EDF déjà ON avec latch OFF et SOC ${round1(soc)}% < seuil OFF ${gridSocOff}% : adoption de l'état ON`);
|
||||
} else {
|
||||
desiredGrid = 'off';
|
||||
desiredLatch = 'off';
|
||||
state = 'turn_off_stray_grid_on';
|
||||
reason.push(`EDF ON avec latch OFF et SOC ${round1(soc)}% >= seuil OFF ${gridSocOff}% : coupure de reprise de contrôle`);
|
||||
}
|
||||
|
||||
} else if (latchIsOn) {
|
||||
if (!minOnElapsed) {
|
||||
desiredGrid = 'on';
|
||||
desiredLatch = 'on';
|
||||
state = 'hold_on_min_runtime';
|
||||
reason.push(`EDF maintenu ON : durée mini ${gridMinOnMinutes} min non atteinte (${round1(minutesSinceLastOn || 0)} min)`);
|
||||
} else if (soc < gridSocOff) {
|
||||
desiredGrid = 'on';
|
||||
desiredLatch = 'on';
|
||||
state = 'hold_on_until_soc_off';
|
||||
reason.push(`EDF maintenu ON : SOC ${round1(soc)}% < seuil OFF ${gridSocOff}%`);
|
||||
} else {
|
||||
desiredGrid = 'off';
|
||||
desiredLatch = 'off';
|
||||
state = 'turn_off_recovered';
|
||||
reason.push(`SOC ${round1(soc)}% >= seuil OFF ${gridSocOff}% : coupure EDF`);
|
||||
}
|
||||
|
||||
} else {
|
||||
desiredGrid = 'off';
|
||||
desiredLatch = 'off';
|
||||
state = 'observe_only';
|
||||
reason.push('Aucune action : hors zone d’hystérésis');
|
||||
}
|
||||
|
||||
// ======================================================
|
||||
// COMPARE DESIRED VS CURRENT
|
||||
// ======================================================
|
||||
const gridAction =
|
||||
desiredGrid === (gridIsOn ? 'on' : 'off')
|
||||
? 'none'
|
||||
: desiredGrid === 'on'
|
||||
? 'turn_on'
|
||||
: 'turn_off';
|
||||
|
||||
const latchAction =
|
||||
desiredLatch === (latchIsOn ? 'on' : 'off')
|
||||
? 'none'
|
||||
: desiredLatch === 'on'
|
||||
? 'turn_on'
|
||||
: 'turn_off';
|
||||
|
||||
// ======================================================
|
||||
// TIMESTAMPS
|
||||
// ======================================================
|
||||
const setLastOnAt =
|
||||
gridAction === 'turn_on' ||
|
||||
state === 'adopt_existing_grid_on' ||
|
||||
state === 'adopt_existing_grid_on_low_soc' ||
|
||||
state === 'adopt_existing_grid_on_evcs_override';
|
||||
|
||||
const setLastOffAt =
|
||||
gridAction === 'turn_off';
|
||||
|
||||
// ======================================================
|
||||
// CONTEXT / ALERTS
|
||||
// ======================================================
|
||||
reason.push(`Grid actuel ${gridIsOn ? 'ON' : 'OFF'}`);
|
||||
reason.push(`Latch actuel ${latchIsOn ? 'ON' : 'OFF'}`);
|
||||
reason.push(`SOC ${round1(soc)}% | seuils ${gridSocOn}%/${gridSocOff}%`);
|
||||
reason.push(`PV ${Math.round(pvPowerW)}W`);
|
||||
reason.push(`Charge AC maison ${Math.round(houseAcPowerW)}W`);
|
||||
reason.push(`Puissance réseau ${Math.round(gridPowerW)}W`);
|
||||
reason.push(`EVCS ${evcsSeemsActive ? 'actif' : 'inactif'} | ${Math.round(evcsAcPowerW)}W | status_raw=${Number.isFinite(evcsStatusRaw) ? evcsStatusRaw : 'n/a'}`);
|
||||
|
||||
if (minutesSinceLastOn !== null) {
|
||||
reason.push(`Dernier ON il y a ${round1(minutesSinceLastOn)} min`);
|
||||
}
|
||||
if (minutesSinceLastOff !== null) {
|
||||
reason.push(`Dernier OFF il y a ${round1(minutesSinceLastOff)} min`);
|
||||
}
|
||||
|
||||
if (acOverSafeLimit) {
|
||||
reason.push(`⚠️ Charge AC maison ${Math.round(houseAcPowerW)}W >= limite sûre ${Math.round(acLoadsMaxSafeW)}W`);
|
||||
}
|
||||
|
||||
reason.push(`EVCS override ${evcsGridOverride ? 'ON' : 'OFF'}`);
|
||||
reason.push(`EVCS explicit grid request ${evcsExplicitGridRequest ? 'ON' : 'OFF'}`);
|
||||
reason.push(`EVCS priority ${evcsPriorityActive ? 'ON' : 'OFF'}`);
|
||||
reason.push(`EVCS night takeover ${evcsNightGridTakeoverNeeded ? 'ON' : 'OFF'}`);
|
||||
reason.push(`EVCS source ${evcsSourceModeRaw || 'n/a'}`);
|
||||
reason.push(`EVCS strategy ${userStrategyRaw || 'n/a'}`);
|
||||
reason.push(`EVCS surplus state ${evcsSurplusStateRaw || 'n/a'}`);
|
||||
reason.push(`EVCS no-demand semantic ${evcsVehicleNoDemandSemantic ? 'ON' : 'OFF'}`);
|
||||
|
||||
// ======================================================
|
||||
// NOTIFICATIONS
|
||||
// ======================================================
|
||||
let notifyTitle = 'Victron Grid Hysteresis';
|
||||
let notifyMessage = '';
|
||||
let notifyWanted = false;
|
||||
|
||||
if (
|
||||
state === 'turn_on_evcs_override' ||
|
||||
state === 'adopt_existing_grid_on_evcs_override' ||
|
||||
state === 'hold_on_evcs_override'
|
||||
) {
|
||||
notifyTitle = '🚗 EDF forcé par EVCS';
|
||||
notifyMessage = `Priorité EVCS active. EDF maintenu ON pour la recharge véhicule. Cause: ${userStrategyRaw || evcsSourceModeRaw || 'override EVCS'}.`;
|
||||
notifyWanted = true;
|
||||
|
||||
} else if (state === 'turn_on_low_soc' || state === 'turn_on_low_soc_cooldown_bypassed') {
|
||||
notifyTitle = '⚡ EDF activé sur SOC bas';
|
||||
notifyMessage = `SOC ${round1(soc)}% <= ${gridSocOn}%. EDF activé et maintenu jusqu'à au moins ${gridSocOff}% (min ${gridMinOnMinutes} min).`;
|
||||
notifyWanted = true;
|
||||
|
||||
} else if (state === 'adopt_existing_grid_on_low_soc' || state === 'adopt_existing_grid_on') {
|
||||
notifyTitle = '🔁 EDF ON repris par hystérésis';
|
||||
notifyMessage = `EDF était déjà ON. Le workflow reprend la main et le maintient jusqu'à ${gridSocOff}% (min ${gridMinOnMinutes} min).`;
|
||||
notifyWanted = true;
|
||||
|
||||
} else if (state === 'turn_off_recovered') {
|
||||
notifyTitle = '✅ EDF coupé après recharge';
|
||||
notifyMessage = `SOC ${round1(soc)}% >= ${gridSocOff}%. Retour hors réseau autorisé.`;
|
||||
notifyWanted = true;
|
||||
|
||||
} else if (state === 'turn_off_stray_grid_on') {
|
||||
notifyTitle = '✅ EDF coupé (reprise de contrôle)';
|
||||
notifyMessage = `EDF était ON sans latch. SOC ${round1(soc)}% >= ${gridSocOff}%, coupure réseau appliquée.`;
|
||||
notifyWanted = true;
|
||||
|
||||
} else if (acOverSafeLimit && !gridIsOn) {
|
||||
notifyTitle = '🚨 Charge AC élevée hors réseau';
|
||||
notifyMessage = `Charge AC maison ${Math.round(houseAcPowerW)}W au-dessus de la limite sûre ${Math.round(acLoadsMaxSafeW)}W alors que EDF est OFF.`;
|
||||
notifyWanted = true;
|
||||
|
||||
} else if (acOverSafeLimit && gridIsOn) {
|
||||
notifyTitle = '⚠️ Charge AC élevée';
|
||||
notifyMessage = `Charge AC maison ${Math.round(houseAcPowerW)}W proche/au-dessus de la limite sûre ${Math.round(acLoadsMaxSafeW)}W.`;
|
||||
notifyWanted = true;
|
||||
}
|
||||
|
||||
const shortReason = reason.join(' | ').slice(0, 255);
|
||||
|
||||
const signature = [
|
||||
now.date,
|
||||
state,
|
||||
`dg=${desiredGrid}`,
|
||||
`dl=${desiredLatch}`,
|
||||
`sb=${socBand}`,
|
||||
`ac=${acOverSafeLimit ? '1' : '0'}`,
|
||||
`ov=${evcsGridOverride ? '1' : '0'}`,
|
||||
`eg=${evcsExplicitGridRequest ? '1' : '0'}`,
|
||||
`ep=${evcsPriorityActive ? '1' : '0'}`,
|
||||
`es=${evcsSurplusState || 'na'}`
|
||||
].join('|');
|
||||
|
||||
const signatureChanged = signature !== lastSignature;
|
||||
|
||||
// ======================================================
|
||||
// OUTPUT
|
||||
// ======================================================
|
||||
const result = {
|
||||
timestamp_paris: now.stamp,
|
||||
|
||||
inputs: {
|
||||
soc_percent: soc,
|
||||
grid_state: gridIsOn ? 'on' : 'off',
|
||||
pv_power_w: pvPowerW,
|
||||
house_ac_power_w: houseAcPowerW,
|
||||
grid_power_w: gridPowerW,
|
||||
evcs_ac_power_w: evcsAcPowerW,
|
||||
evcs_charging: evcsCharging,
|
||||
evcs_status_raw: Number.isFinite(evcsStatusRaw) ? evcsStatusRaw : null,
|
||||
|
||||
hysteresis_enabled: hysteresisEnabled,
|
||||
notifications_enabled: notificationsEnabled,
|
||||
grid_latch: latchIsOn,
|
||||
grid_soc_on: gridSocOn,
|
||||
grid_soc_off: gridSocOff,
|
||||
grid_min_on_minutes: gridMinOnMinutes,
|
||||
grid_cooldown_minutes: gridCooldownMinutes,
|
||||
ac_loads_max_safe_w: acLoadsMaxSafeW,
|
||||
|
||||
last_on_at: lastOnAt || null,
|
||||
last_off_at: lastOffAt || null,
|
||||
last_signature: lastSignature || null,
|
||||
|
||||
evcs_priority_active: evcsPriorityActive,
|
||||
night_grid_takeover_needed: evcsNightGridTakeoverNeeded,
|
||||
evcs_source_mode: evcsSourceModeRaw || null,
|
||||
user_strategy: userStrategyRaw || null,
|
||||
evcs_surplus_state: evcsSurplusStateRaw || null
|
||||
},
|
||||
|
||||
computed: {
|
||||
desired_grid: desiredGrid,
|
||||
desired_latch: desiredLatch,
|
||||
minutes_since_last_on: minutesSinceLastOn !== null ? round1(minutesSinceLastOn) : null,
|
||||
minutes_since_last_off: minutesSinceLastOff !== null ? round1(minutesSinceLastOff) : null,
|
||||
min_on_elapsed: minOnElapsed,
|
||||
cooldown_elapsed: cooldownElapsed,
|
||||
ac_over_safe_limit: acOverSafeLimit,
|
||||
evcs_seems_active: evcsSeemsActive,
|
||||
|
||||
evcs_grid_override: evcsGridOverride,
|
||||
evcs_explicit_grid_request: evcsExplicitGridRequest,
|
||||
evcs_priority_active: evcsPriorityActive,
|
||||
evcs_night_grid_takeover_needed: evcsNightGridTakeoverNeeded,
|
||||
evcs_source_mode: evcsSourceModeRaw || null,
|
||||
evcs_user_strategy: userStrategyRaw || null,
|
||||
evcs_surplus_state: evcsSurplusStateRaw || null,
|
||||
evcs_vehicle_no_demand_semantic: evcsVehicleNoDemandSemantic
|
||||
},
|
||||
|
||||
decision: {
|
||||
state,
|
||||
grid_action: gridAction,
|
||||
latch_action: latchAction,
|
||||
set_last_on_at: setLastOnAt,
|
||||
set_last_off_at: setLastOffAt,
|
||||
should_notify: notificationsEnabled && signatureChanged && notifyWanted,
|
||||
signature,
|
||||
signature_changed: signatureChanged,
|
||||
reason,
|
||||
short_reason: shortReason
|
||||
},
|
||||
|
||||
ha_service_calls: {
|
||||
helper_state: {
|
||||
domain: 'input_text',
|
||||
service: 'set_value',
|
||||
data: {
|
||||
entity_id: 'input_text.victron_grid_control_state',
|
||||
value: state
|
||||
}
|
||||
},
|
||||
|
||||
helper_reason: {
|
||||
domain: 'input_text',
|
||||
service: 'set_value',
|
||||
data: {
|
||||
entity_id: 'input_text.victron_grid_control_reason',
|
||||
value: shortReason || 'Aucune raison'
|
||||
}
|
||||
},
|
||||
|
||||
helper_signature: {
|
||||
domain: 'input_text',
|
||||
service: 'set_value',
|
||||
data: {
|
||||
entity_id: 'input_text.victron_grid_control_last_signature',
|
||||
value: signature
|
||||
}
|
||||
},
|
||||
|
||||
helper_latch: {
|
||||
domain: 'input_boolean',
|
||||
service: desiredLatch === 'on' ? 'turn_on' : 'turn_off',
|
||||
data: {
|
||||
entity_id: 'input_boolean.victron_grid_hysteresis_latch'
|
||||
}
|
||||
},
|
||||
|
||||
helper_last_on_at: {
|
||||
domain: 'input_datetime',
|
||||
service: 'set_datetime',
|
||||
data: {
|
||||
entity_id: 'input_datetime.victron_grid_last_on_at',
|
||||
datetime: now.stamp
|
||||
}
|
||||
},
|
||||
|
||||
helper_last_off_at: {
|
||||
domain: 'input_datetime',
|
||||
service: 'set_datetime',
|
||||
data: {
|
||||
entity_id: 'input_datetime.victron_grid_last_off_at',
|
||||
datetime: now.stamp
|
||||
}
|
||||
},
|
||||
|
||||
grid: {
|
||||
domain: 'switch',
|
||||
service: desiredGrid === 'on' ? 'turn_on' : 'turn_off',
|
||||
data: {
|
||||
entity_id: 'switch.edf_general_ok_switch'
|
||||
}
|
||||
},
|
||||
|
||||
logbook: {
|
||||
domain: 'logbook',
|
||||
service: 'log',
|
||||
data: {
|
||||
name: 'Victron Grid Hysteresis',
|
||||
message: `${state} | Grid=${desiredGrid} | Latch=${desiredLatch} | SOC=${round1(soc)}% | AC=${Math.round(houseAcPowerW)}W | ${shortReason}`,
|
||||
entity_id: 'switch.edf_general_ok_switch'
|
||||
}
|
||||
},
|
||||
|
||||
mobile_notification: {
|
||||
domain: cfg.mobileNotifyDomain,
|
||||
service: cfg.mobileNotifyService,
|
||||
data: {
|
||||
title: notifyTitle,
|
||||
message: notifyMessage,
|
||||
data: {
|
||||
push: {
|
||||
'interruption-level': 'time-sensitive',
|
||||
sound: { name: 'default' }
|
||||
},
|
||||
ttl: 0,
|
||||
priority: 'high'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return [{ json: result }];
|
||||
425
modified/GRID_HYSTERESIS_PATCHED.json
Normal file
425
modified/GRID_HYSTERESIS_PATCHED.json
Normal file
File diff suppressed because one or more lines are too long
32
modified/GRID_SNAPSHOT_patched.js
Normal file
32
modified/GRID_SNAPSHOT_patched.js
Normal file
@@ -0,0 +1,32 @@
|
||||
{{ {
|
||||
"soc_percent": states("sensor.victron_battery_soc"),
|
||||
"grid_state": states("switch.edf_general_ok_switch"),
|
||||
"pv_power_w": states("sensor.victron_pv_power_system"),
|
||||
|
||||
"house_ac_power_w": states("sensor.victron_consumption_power_l1"),
|
||||
"grid_power_w": states("sensor.victron_grid_power_l1"),
|
||||
|
||||
"evcs_ac_power_w": states("sensor.victron_evcs_ac_power"),
|
||||
"evcs_charging": states("switch.victron_evcs_charging"),
|
||||
"evcs_status_raw": states("sensor.victron_evcs_status_raw"),
|
||||
|
||||
"hysteresis_enabled": states("input_boolean.victron_grid_hysteresis_enabled"),
|
||||
"notifications_enabled": states("input_boolean.victron_grid_hysteresis_notifications"),
|
||||
"grid_latch": states("input_boolean.victron_grid_hysteresis_latch"),
|
||||
|
||||
"grid_soc_on": states("input_number.victron_grid_soc_on"),
|
||||
"grid_soc_off": states("input_number.victron_grid_soc_off"),
|
||||
"grid_min_on_minutes": states("input_number.victron_grid_min_on_minutes"),
|
||||
"grid_cooldown_minutes": states("input_number.victron_grid_cooldown_minutes"),
|
||||
"ac_loads_max_safe_w": states("input_number.victron_ac_loads_max_safe_w"),
|
||||
|
||||
"last_on_at": states("input_datetime.victron_grid_last_on_at"),
|
||||
"last_off_at": states("input_datetime.victron_grid_last_off_at"),
|
||||
"last_signature": states("input_text.victron_grid_control_last_signature"),
|
||||
|
||||
"evcs_priority_active": states("input_boolean.victron_evcs_priority_active"),
|
||||
"night_grid_takeover_needed": states("input_boolean.victron_evcs_night_grid_takeover_needed"),
|
||||
"evcs_source_mode": states("input_text.victron_evcs_source_mode"),
|
||||
"user_strategy": states("input_select.victron_evcs_user_strategy"),
|
||||
"evcs_surplus_state": states("input_text.victron_evcs_surplus_state")
|
||||
} | tojson }}
|
||||
579
modified/WINDOWS_SOURCEV2_patched.js
Normal file
579
modified/WINDOWS_SOURCEV2_patched.js
Normal file
@@ -0,0 +1,579 @@
|
||||
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;
|
||||
|
||||
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, 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 nightSessionNeedsCapture =
|
||||
nightSessionActive &&
|
||||
(
|
||||
!Number.isFinite(history.nightSessionStartSocPct) ||
|
||||
!history.nightSessionStartAt
|
||||
);
|
||||
|
||||
const nightSessionNeedsClear =
|
||||
!nightSessionActive &&
|
||||
(
|
||||
Number.isFinite(history.nightSessionStartSocPct) ||
|
||||
!!history.nightSessionStartAt
|
||||
);
|
||||
|
||||
const batteryHardFloorPct =
|
||||
params.evcsNightBatteryHardFloorPct;
|
||||
|
||||
const batteryMorningTargetPct =
|
||||
parsed.todayRemainingKwh >= params.evcsNightStrongForecastKwh
|
||||
? params.evcsNightTargetStrongForecastPct
|
||||
: parsed.todayRemainingKwh >= 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
|
||||
);
|
||||
|
||||
const batteryAssistAllowed =
|
||||
nightSessionActive &&
|
||||
parsed.soc > batteryHardFloorPct &&
|
||||
parsed.soc > gridTakeoverTriggerPct &&
|
||||
!batteryRecoveryUrgent;
|
||||
|
||||
const nightGridTakeoverNeeded =
|
||||
nightSessionActive &&
|
||||
(
|
||||
batteryBelowHardFloor ||
|
||||
batteryAtOrBelowGridTakeoverTrigger ||
|
||||
batteryRecoveryUrgent
|
||||
);
|
||||
|
||||
const nightPreferredSourceMode =
|
||||
!nightSessionActive
|
||||
? 'blocked'
|
||||
: batteryAssistAllowed
|
||||
? 'battery_assist_night'
|
||||
: (
|
||||
batteryBelowHardFloor ||
|
||||
batteryBelowMorningTarget ||
|
||||
batteryRecoveryUrgent
|
||||
)
|
||||
? 'grid_forced'
|
||||
: 'grid_hc_allowed';
|
||||
|
||||
// =====================================================
|
||||
// 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 cloudDebtOkForPotential = params.cloudDebtPrevKwh <= params.cloudDebtMaxKwh;
|
||||
const batteryOkForPotential = parsed.batteryCurrentA >= -Math.max(1, params.batteryNeutralBandA);
|
||||
|
||||
const isHighBatteryForPotential =
|
||||
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';
|
||||
}
|
||||
|
||||
// FIX V2.5: En auto_solar_plus, hors fenêtre, batteries hautes (>=85%),
|
||||
// et peu de charge maison : permettre solar_potential pour provoquer le PV.
|
||||
else if (userStrategy === 'auto_solar_plus' && parsed.soc >= 85 && houseNonEvcsW < 1500) {
|
||||
sourceMode = 'solar_potential';
|
||||
}
|
||||
|
||||
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,
|
||||
isHighBatteryForPotential,
|
||||
strongSolarSignal,
|
||||
effectivePvStartMinW,
|
||||
profile,
|
||||
operatingMode,
|
||||
sourceMode,
|
||||
},
|
||||
|
||||
nightEvcsPreview: {
|
||||
nightSessionActive,
|
||||
nightSessionNeedsCapture,
|
||||
nightSessionNeedsClear,
|
||||
nightSessionStartSocPct,
|
||||
nightSessionStartAt,
|
||||
|
||||
batteryHardFloorPct,
|
||||
batteryMorningTargetPct,
|
||||
maxBatteryDischargePoints,
|
||||
gridTakeoverTriggerPct,
|
||||
|
||||
batteryBelowHardFloor,
|
||||
batteryBelowMorningTarget,
|
||||
batteryAtOrBelowGridTakeoverTrigger,
|
||||
|
||||
batteryKwhMissingToTarget,
|
||||
batteryRecoveryMinutesNeeded,
|
||||
batteryRecoveryUrgent,
|
||||
|
||||
batteryAssistAllowed,
|
||||
nightGridTakeoverNeeded,
|
||||
nightPreferredSourceMode,
|
||||
}
|
||||
}
|
||||
}];
|
||||
Reference in New Issue
Block a user