logique de traitement erron
This commit is contained in:
553
decoupesurplus/V2/INPUTPARSE/inputparse.js
Normal file
553
decoupesurplus/V2/INPUTPARSE/inputparse.js
Normal file
@@ -0,0 +1,553 @@
|
||||
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 clamp(v, min, max) {
|
||||
return Math.min(Math.max(v, min), max);
|
||||
}
|
||||
|
||||
function round1(v) {
|
||||
return Math.round(v * 10) / 10;
|
||||
}
|
||||
|
||||
function round2(v) {
|
||||
return Math.round(v * 100) / 100;
|
||||
}
|
||||
|
||||
function to2(n) {
|
||||
return String(n).padStart(2, '0');
|
||||
}
|
||||
|
||||
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 parseTimeToMinutes(v, fallback = null) {
|
||||
const s = (v ?? '').toString().trim();
|
||||
const m = s.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
|
||||
if (!m) return fallback;
|
||||
|
||||
const hh = Number(m[1]);
|
||||
const mm = Number(m[2]);
|
||||
|
||||
if (!Number.isFinite(hh) || !Number.isFinite(mm)) return fallback;
|
||||
return (hh % 24) * 60 + mm;
|
||||
}
|
||||
|
||||
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}`;
|
||||
const stamp = `${date} ${to2(hour)}:${parts.minute}:${parts.second}`;
|
||||
|
||||
return {
|
||||
year: Number(parts.year),
|
||||
month: Number(parts.month),
|
||||
day: Number(parts.day),
|
||||
hour,
|
||||
minute: Number(parts.minute),
|
||||
second: Number(parts.second),
|
||||
date,
|
||||
stamp,
|
||||
minNow: hour * 60 + Number(parts.minute),
|
||||
};
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
if (![y, mo, d, h, mi, se].every(Number.isFinite)) return null;
|
||||
|
||||
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 cleanText(v, fallback = '') {
|
||||
const s = (v ?? '').toString().trim();
|
||||
const x = s.toLowerCase();
|
||||
if (['unknown', 'unavailable', 'none', 'null', 'undefined'].includes(x)) return fallback;
|
||||
return s;
|
||||
}
|
||||
|
||||
function finiteOrNull(v) {
|
||||
return Number.isFinite(v) ? v : null;
|
||||
}
|
||||
|
||||
const raw = $input.first().json;
|
||||
const input = extractSnapshot(raw);
|
||||
|
||||
const nowMs = Date.now();
|
||||
const now = getParisParts(nowMs);
|
||||
|
||||
const CFG = {
|
||||
mobileNotifyDomain: 'notify',
|
||||
mobileNotifyService: 'mobile_app_iphone_16_de_tt',
|
||||
|
||||
batteryUsableCapacityKwh: 17.5,
|
||||
nominalAcVoltageV: 230,
|
||||
evcsDedicatedMaxW: 4900,
|
||||
systemReserveW: 300,
|
||||
|
||||
vebusDcVoltageSoftFloorV: 49.0,
|
||||
vebusDcVoltageHardFloorV: 48.6,
|
||||
|
||||
departurePriorityWindowMinutes: 240,
|
||||
sampleSeconds: 30,
|
||||
|
||||
cloudDebtRecoveryThresholdKwh: 0.05,
|
||||
cloudDebtMeaningfulThresholdKwh: 0.01,
|
||||
};
|
||||
|
||||
// =====================================================
|
||||
// PARSED SNAPSHOT
|
||||
// =====================================================
|
||||
const parsed = {
|
||||
soc: clamp(num(input.soc_percent, 0), 0, 100),
|
||||
batteryCurrentA: num(input.battery_current_a, 0),
|
||||
|
||||
pvPowerW: Math.max(0, num(input.pv_power_w, 0)),
|
||||
pvPowerAvg10mW: Math.max(0, num(input.pv_power_10m_avg_w, num(input.pv_power_w, 0))),
|
||||
pvTrendW: num(input.pv_trend_w, 0),
|
||||
|
||||
houseAcPowerW: Math.max(0, num(input.house_ac_power_w, 0)),
|
||||
houseAcPowerPrevW: Math.max(0, num(input.house_ac_power_prev_w, num(input.house_ac_power_w, 0))),
|
||||
gridPowerW: num(input.grid_power_w, 0),
|
||||
|
||||
gridIsOn: low(input.grid_state || 'off') === 'on',
|
||||
acInAvailable: num(input.victron_acin1_available ?? input.ac_in1_available, 0) === 1,
|
||||
gridAvailable:
|
||||
(low(input.grid_state || 'off') === 'on') ||
|
||||
(num(input.victron_acin1_available ?? input.ac_in1_available, 0) === 1),
|
||||
|
||||
todayRemainingKwh: Math.max(0, num(input.today_remaining_kwh, 0)),
|
||||
|
||||
evcsDeviceConnected: low(input.evcs_device_connected || 'off') === 'on',
|
||||
evcsVehicleConnected: low(input.evcs_vehicle_connected || 'off') === 'on',
|
||||
evcsStatusRaw: cleanText(input.evcs_status_raw),
|
||||
evcsModeRaw: [
|
||||
input.evcs_mode_sensor,
|
||||
input.evcs_mode_fallback,
|
||||
input.evcs_mode
|
||||
]
|
||||
.map(v => cleanText(v))
|
||||
.find(v => v) || 'unknown',
|
||||
|
||||
evcsSetCurrentA: num(input.evcs_set_current_a, NaN),
|
||||
evcsCommandCurrentA: num(input.evcs_command_current_a, NaN),
|
||||
evcsCurrentA: Math.max(0, num(input.evcs_current_a, 0)),
|
||||
evcsAcPowerW: Math.max(0, num(input.evcs_ac_power_w, 0)),
|
||||
evcsChargingSwitchOn: low(input.evcs_charging_switch || 'off') === 'on',
|
||||
evcsAutoStartSwitchOn: low(input.evcs_auto_start_switch || 'off') === 'on',
|
||||
|
||||
controllerEnabled: low(input.controller_enabled || 'off') === 'on',
|
||||
notificationsEnabled: low(input.notifications_enabled || 'off') === 'on',
|
||||
hardStopEnabledLegacy: low(input.hard_stop_enabled || 'off') === 'on',
|
||||
chargePermissionNow: low(input.charge_permission_now || 'off') === 'on',
|
||||
controlLatch: low(input.control_latch || 'off') === 'on',
|
||||
departurePriorityEnabled: low(input.departure_priority_enabled || 'off') === 'on',
|
||||
forecastModeEnabled: low(input.forecast_mode_enabled || 'off') === 'on',
|
||||
|
||||
evcsPriorityManualOverride: low(input.evcs_priority_manual_override || 'off') === 'on',
|
||||
evcsPriorityManualMode: cleanText(input.evcs_priority_manual_mode || 'normal', 'normal').toLowerCase(),
|
||||
evcsPriorityManualReason: cleanText(input.evcs_priority_manual_reason),
|
||||
|
||||
vebusOverload: low(input.vebus_overload || 'off') === 'on',
|
||||
vebusL1Overload: low(input.vebus_l1_overload || 'off') === 'on',
|
||||
vebusLowBattery: low(input.vebus_low_battery || 'off') === 'on',
|
||||
vebusL1LowBattery: low(input.vebus_l1_low_battery || 'off') === 'on',
|
||||
|
||||
vebusAcOutPowerW: Math.max(0, num(input.vebus_ac_out_l1_power_w, 0)),
|
||||
vebusAcOutCurrentA: Math.max(0, num(input.vebus_ac_out_l1_current_a, 0)),
|
||||
vebusAcOutVoltageV: num(input.vebus_ac_out_l1_voltage_v, CFG.nominalAcVoltageV),
|
||||
|
||||
vebusDcVoltageV: num(input.vebus_dc_voltage_v, NaN),
|
||||
vebusDcCurrentA: num(input.vebus_dc_current_a, NaN),
|
||||
vebusDcPowerW: num(input.vebus_dc_power_w, NaN),
|
||||
|
||||
vebusState: num(input.vebus_state, NaN),
|
||||
vebusMainState: num(input.vebus_main_state, NaN),
|
||||
};
|
||||
|
||||
parsed.evcsModeIsAuto = low(parsed.evcsModeRaw).includes('auto');
|
||||
|
||||
// =====================================================
|
||||
// PARAMETERS
|
||||
// =====================================================
|
||||
const params = {
|
||||
userStrategyRaw: input.victron_evcs_strategy || 'auto_solar',
|
||||
|
||||
targetCurrentAutoA: clamp(num(input.target_current_auto_a, 6), 0, 32),
|
||||
pvStartMinW: Math.max(0, num(input.pv_start_min_w, 1800)),
|
||||
pvHoldMinW: Math.max(0, num(input.pv_hold_min_w, 1200)),
|
||||
houseAcHeadroomW: Math.max(0, num(input.house_ac_headroom_w, 1200)),
|
||||
|
||||
batteryDischargeSoftA: num(input.battery_discharge_soft_a, -5),
|
||||
batteryDischargeHardA: num(input.battery_discharge_hard_a, -15),
|
||||
batteryReserveUpA: Math.max(0, num(input.battery_reserve_up_a, 8)),
|
||||
batteryNeutralBandA: Math.max(0, num(input.battery_neutral_band_a, 3)),
|
||||
|
||||
socStartMin: clamp(num(input.soc_start_min, 60), 0, 100),
|
||||
socStopMin: clamp(num(input.soc_stop_min, 35), 0, 100),
|
||||
|
||||
currentMinA: clamp(num(input.current_min_a, 6), 6, 32),
|
||||
currentMaxAutoA: clamp(
|
||||
num(input.current_max_auto_a, 16),
|
||||
clamp(num(input.current_min_a, 6), 6, 32),
|
||||
32
|
||||
),
|
||||
currentStepUpA: clamp(num(input.current_step_up_a, 1), 1, 5),
|
||||
currentStepDownA: clamp(num(input.current_step_down_a, 2), 1, 8),
|
||||
|
||||
currentChangeCooldownSeconds: Math.max(5, num(input.current_change_cooldown_seconds, 60)),
|
||||
minRunMinutes: Math.max(0, num(input.min_run_minutes, 3)),
|
||||
minStopMinutes: Math.max(0, num(input.min_stop_minutes, 10)),
|
||||
|
||||
requiredEnergyKwh: Math.max(0, num(input.required_energy_kwh, 0)),
|
||||
socComfortTarget: clamp(num(input.soc_comfort_target, 85), 0, 100),
|
||||
endOfDaySocGuard: clamp(num(input.end_of_day_soc_guard, 85), 0, 100),
|
||||
baseLoadRemainingKwh: Math.max(0, num(input.base_load_remaining_kwh, 0)),
|
||||
flexibleSafetyMarginKwh: Math.max(0, num(input.flexible_safety_margin_kwh, 0)),
|
||||
endOfDaySocTarget: clamp(num(input.end_of_day_soc_target, 85), 0, 100),
|
||||
|
||||
acLoadsMaxSafeW: Math.max(0, num(input.ac_loads_max_safe_w, 8500)),
|
||||
batteryFullSoc: clamp(num(input.battery_full_soc, 90), 0, 100),
|
||||
batteryFullForecastMinKwh: Math.max(0, num(input.battery_full_forecast_min_kwh, 8)),
|
||||
forecastCommitSocFloor: clamp(num(input.forecast_commit_soc_floor, 35), 0, 100),
|
||||
forecastCommitMinKwh: Math.max(0, num(input.forecast_commit_min_kwh, 25)),
|
||||
|
||||
cloudDebtPrevKwh: Math.max(0, num(input.cloud_debt_kwh, 0)),
|
||||
cloudDebtMaxKwh: Math.max(0.05, num(input.cloud_debt_max_kwh, 1.2)),
|
||||
cloudToleranceMinutes: Math.max(1, num(input.cloud_tolerance_minutes, 20)),
|
||||
|
||||
departureTimeMin: parseTimeToMinutes(input.departure_time, null),
|
||||
dayStartTimeMin: parseTimeToMinutes(input.day_start_time, 7 * 60 + 30),
|
||||
dayStopTimeMin: parseTimeToMinutes(input.day_stop_time, 20 * 60 + 30),
|
||||
|
||||
hcNightStartMin: parseTimeToMinutes(input.hc_night_start_time, 23 * 60),
|
||||
hcNightStopMin: parseTimeToMinutes(input.hc_night_stop_time, 6 * 60),
|
||||
morningStartMin: parseTimeToMinutes(
|
||||
input.morning_start_time,
|
||||
parseTimeToMinutes(input.day_start_time, 7 * 60 + 30)
|
||||
),
|
||||
morningStopMin: parseTimeToMinutes(input.morning_stop_time, 11 * 60),
|
||||
hcMiddayStartMin: parseTimeToMinutes(input.hc_midday_start_time, 12 * 60 + 21),
|
||||
hcMiddayStopMin: parseTimeToMinutes(input.hc_midday_stop_time, 14 * 60 + 21),
|
||||
eveningStartMin: parseTimeToMinutes(input.evening_start_time, 17 * 60),
|
||||
eveningStopMin: parseTimeToMinutes(
|
||||
input.evening_stop_time,
|
||||
parseTimeToMinutes(input.day_stop_time, 20 * 60 + 30)
|
||||
),
|
||||
|
||||
allowGridHcNight: low(input.allow_grid_hc_night || 'off') === 'on',
|
||||
allowGridHcMidday: low(input.allow_grid_hc_midday || 'off') === 'on',
|
||||
|
||||
probeEnabled: low(input.probe_enabled || 'off') === 'on',
|
||||
probeMinutes: Math.max(1, num(input.probe_minutes, 3)),
|
||||
probeRetryMinutes: Math.max(5, num(input.probe_retry_minutes, 45)),
|
||||
probeCurrentA: clamp(
|
||||
num(input.probe_current_a, clamp(num(input.current_min_a, 6), 6, 32)),
|
||||
clamp(num(input.current_min_a, 6), 6, 32),
|
||||
clamp(num(input.current_max_auto_a, 16), clamp(num(input.current_min_a, 6), 6, 32), 32)
|
||||
),
|
||||
|
||||
gridBatterySoftA: num(input.grid_battery_soft_a, -3),
|
||||
gridBatteryHardA: num(input.grid_battery_hard_a, -6),
|
||||
|
||||
morningProbePvAvgMinW: Math.max(0, num(input.morning_probe_pv_avg_min_w, 1500)),
|
||||
morningProbePvTrendMinW: num(input.morning_probe_pv_trend_min_w, 0),
|
||||
middayProbePvAvgMinW: Math.max(0, num(input.midday_probe_pv_avg_min_w, 1200)),
|
||||
|
||||
morningSocMin: clamp(num(input.morning_soc_min, 35), 0, 100),
|
||||
middaySocMin: clamp(num(input.midday_soc_min, 75), 0, 100),
|
||||
eveningSocStartMin: clamp(num(input.evening_soc_start_min, 85), 0, 100),
|
||||
eveningSocStopMin: clamp(num(input.evening_soc_stop_min, 80), 0, 100),
|
||||
|
||||
morningBatteryMaxDischargeA: num(input.morning_battery_max_discharge_a, -20),
|
||||
middayBatteryMaxDischargeA: num(input.midday_battery_max_discharge_a, -40),
|
||||
eveningBatteryMaxDischargeA: num(input.evening_battery_max_discharge_a, -30),
|
||||
|
||||
// =====================================================
|
||||
// EVCS NIGHT ENGINE — NEW PARAMETERS
|
||||
// =====================================================
|
||||
evcsNightBatteryHardFloorPct: clamp(
|
||||
num(input.evcs_night_battery_hard_floor_pct, 22),
|
||||
0,
|
||||
100
|
||||
),
|
||||
|
||||
evcsNightStrongForecastKwh: Math.max(
|
||||
0,
|
||||
num(input.evcs_night_strong_forecast_kwh, 18)
|
||||
),
|
||||
|
||||
evcsNightMediumForecastKwh: Math.max(
|
||||
0,
|
||||
num(input.evcs_night_medium_forecast_kwh, 10)
|
||||
),
|
||||
|
||||
evcsNightTargetStrongForecastPct: clamp(
|
||||
num(input.evcs_night_target_strong_forecast_pct, 35),
|
||||
0,
|
||||
100
|
||||
),
|
||||
|
||||
evcsNightTargetMediumForecastPct: clamp(
|
||||
num(input.evcs_night_target_medium_forecast_pct, 55),
|
||||
0,
|
||||
100
|
||||
),
|
||||
|
||||
evcsNightTargetWeakForecastPct: clamp(
|
||||
num(input.evcs_night_target_weak_forecast_pct, 85),
|
||||
0,
|
||||
100
|
||||
),
|
||||
|
||||
evcsNightBatteryRecoveryPowerKw: Math.max(
|
||||
0.1,
|
||||
num(input.evcs_night_battery_recovery_power_kw, 2.5)
|
||||
),
|
||||
|
||||
evcsNightDepartureSafetyMin: Math.max(
|
||||
0,
|
||||
num(input.evcs_night_departure_safety_min, 30)
|
||||
),
|
||||
};
|
||||
|
||||
// =====================================================
|
||||
// HISTORY / PERSISTED HELPERS
|
||||
// =====================================================
|
||||
const nightSessionStartSocRaw = num(
|
||||
input.evcs_night_session_start_soc ?? input.night_session_start_soc,
|
||||
NaN
|
||||
);
|
||||
|
||||
const nightSessionStartAtRaw = cleanText(
|
||||
input.evcs_night_session_start_at ?? input.night_session_start_at
|
||||
);
|
||||
|
||||
const history = {
|
||||
lastBlockReason: cleanText(input.last_block_reason, 'none') || 'none',
|
||||
blockedUntilAt: cleanText(input.blocked_until),
|
||||
probeStartedAt: cleanText(input.probe_started_at),
|
||||
lastStartAt: cleanText(input.last_start_at),
|
||||
lastStopAt: cleanText(input.last_stop_at),
|
||||
lastCurrentChangeAt: cleanText(input.last_current_change_at),
|
||||
lastSignature: cleanText(input.last_signature),
|
||||
cloudDebtStartedAt: cleanText(input.cloud_debt_started_at),
|
||||
evcsPriorityManualUntilAt: cleanText(input.evcs_priority_manual_until),
|
||||
|
||||
blockedUntilMs: parseParisStampToMs(cleanText(input.blocked_until)),
|
||||
probeStartedMs: parseParisStampToMs(cleanText(input.probe_started_at)),
|
||||
lastStartMs: parseParisStampToMs(cleanText(input.last_start_at)),
|
||||
lastStopMs: parseParisStampToMs(cleanText(input.last_stop_at)),
|
||||
lastCurrentChangeMs: parseParisStampToMs(cleanText(input.last_current_change_at)),
|
||||
cloudDebtStartedMsPrev: parseParisStampToMs(cleanText(input.cloud_debt_started_at)),
|
||||
evcsPriorityManualUntilMs: parseParisStampToMs(cleanText(input.evcs_priority_manual_until)),
|
||||
|
||||
// =====================================================
|
||||
// EVCS NIGHT SESSION MEMORY
|
||||
// =====================================================
|
||||
nightSessionStartSocPct: Number.isFinite(nightSessionStartSocRaw)
|
||||
? clamp(nightSessionStartSocRaw, 0, 100)
|
||||
: null,
|
||||
|
||||
nightSessionStartAt: nightSessionStartAtRaw || '',
|
||||
nightSessionStartMs: parseParisStampToMs(nightSessionStartAtRaw),
|
||||
};
|
||||
|
||||
// =====================================================
|
||||
// BATTERY TUNING
|
||||
// =====================================================
|
||||
const batteryTune = {
|
||||
global: {
|
||||
houseSpikeGraceMinutes: Math.max(0, num(input.house_spike_grace_minutes, 10)),
|
||||
solarPotentialGraceMinutes: Math.max(0, num(input.solar_potential_grace_minutes, 12)),
|
||||
stepUpCooldownSeconds: Math.max(5, num(input.step_up_cooldown_seconds, params.currentChangeCooldownSeconds)),
|
||||
stepDownCooldownSeconds: Math.max(5, num(input.step_down_cooldown_seconds, params.currentChangeCooldownSeconds)),
|
||||
forceModeAutoRevertMinutes: Math.max(0, num(input.force_mode_auto_revert_minutes, 120)),
|
||||
overloadStepDownA: Math.max(1, num(input.overload_step_down_a, 2)),
|
||||
overloadHoldSeconds: Math.max(5, num(input.overload_hold_seconds, 30)),
|
||||
overloadStopAfterSeconds: Math.max(10, num(input.overload_stop_after_seconds, 120)),
|
||||
},
|
||||
|
||||
bands: {
|
||||
gt90: {
|
||||
dischargeSoftA: num(input.batt_discharge_soft_gt90_a, -50),
|
||||
dischargeHardA: num(input.batt_discharge_hard_gt90_a, -100),
|
||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_gt90_a, 5)),
|
||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_gt90_min, 12)),
|
||||
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_gt90_sec, 20)),
|
||||
recoveryMin: Math.max(0, num(input.batt_recovery_gt90_min, 6)),
|
||||
},
|
||||
|
||||
'80_90': {
|
||||
dischargeSoftA: num(input.batt_discharge_soft_80_90_a, -40),
|
||||
dischargeHardA: num(input.batt_discharge_hard_80_90_a, -80),
|
||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_80_90_a, 10)),
|
||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_80_90_min, 10)),
|
||||
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_80_90_sec, 20)),
|
||||
recoveryMin: Math.max(0, num(input.batt_recovery_80_90_min, 6)),
|
||||
},
|
||||
|
||||
'60_80': {
|
||||
dischargeSoftA: num(input.batt_discharge_soft_60_80_a, -20),
|
||||
dischargeHardA: num(input.batt_discharge_hard_60_80_a, -50),
|
||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_60_80_a, 40)),
|
||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_60_80_min, 6)),
|
||||
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_60_80_sec, 15)),
|
||||
recoveryMin: Math.max(0, num(input.batt_recovery_60_80_min, 8)),
|
||||
},
|
||||
|
||||
'40_60': {
|
||||
dischargeSoftA: num(input.batt_discharge_soft_40_60_a, -12),
|
||||
dischargeHardA: num(input.batt_discharge_hard_40_60_a, -30),
|
||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_40_60_a, 70)),
|
||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_40_60_min, 4)),
|
||||
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_40_60_sec, 10)),
|
||||
recoveryMin: Math.max(0, num(input.batt_recovery_40_60_min, 10)),
|
||||
},
|
||||
|
||||
'25_40': {
|
||||
dischargeSoftA: num(input.batt_discharge_soft_25_40_a, -5),
|
||||
dischargeHardA: num(input.batt_discharge_hard_25_40_a, -15),
|
||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_25_40_a, 110)),
|
||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_25_40_min, 2)),
|
||||
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_25_40_sec, 5)),
|
||||
recoveryMin: Math.max(0, num(input.batt_recovery_25_40_min, 12)),
|
||||
},
|
||||
|
||||
lt25: {
|
||||
dischargeSoftA: num(input.batt_discharge_soft_lt25_a, -3),
|
||||
dischargeHardA: num(input.batt_discharge_hard_lt25_a, -8),
|
||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_lt25_a, 150)),
|
||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_lt25_min, 1)),
|
||||
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_lt25_sec, 2)),
|
||||
recoveryMin: Math.max(0, num(input.batt_recovery_lt25_min, 15)),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// =====================================================
|
||||
// OPTIONAL DEBUG / SUMMARY
|
||||
// =====================================================
|
||||
const evcsNightConfig = {
|
||||
batteryHardFloorPct: params.evcsNightBatteryHardFloorPct,
|
||||
strongForecastKwh: params.evcsNightStrongForecastKwh,
|
||||
mediumForecastKwh: params.evcsNightMediumForecastKwh,
|
||||
targetStrongForecastPct: params.evcsNightTargetStrongForecastPct,
|
||||
targetMediumForecastPct: params.evcsNightTargetMediumForecastPct,
|
||||
targetWeakForecastPct: params.evcsNightTargetWeakForecastPct,
|
||||
batteryRecoveryPowerKw: params.evcsNightBatteryRecoveryPowerKw,
|
||||
departureSafetyMin: params.evcsNightDepartureSafetyMin,
|
||||
sessionStartSocPct: history.nightSessionStartSocPct,
|
||||
sessionStartAt: history.nightSessionStartAt || null,
|
||||
};
|
||||
|
||||
// =====================================================
|
||||
// OUTPUT
|
||||
// =====================================================
|
||||
return [{
|
||||
json: {
|
||||
raw,
|
||||
input,
|
||||
nowMs,
|
||||
now,
|
||||
cfg: CFG,
|
||||
parsed,
|
||||
params,
|
||||
history,
|
||||
batteryTune,
|
||||
evcsNightConfig,
|
||||
}
|
||||
}];
|
||||
|
||||
260
decoupesurplus/V2/INPUTPARSE/inputparseP1.js
Normal file
260
decoupesurplus/V2/INPUTPARSE/inputparseP1.js
Normal file
@@ -0,0 +1,260 @@
|
||||
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 clamp(v, min, max) {
|
||||
return Math.min(Math.max(v, min), max);
|
||||
}
|
||||
|
||||
function round1(v) {
|
||||
return Math.round(v * 10) / 10;
|
||||
}
|
||||
|
||||
function round2(v) {
|
||||
return Math.round(v * 100) / 100;
|
||||
}
|
||||
|
||||
function to2(n) {
|
||||
return String(n).padStart(2, '0');
|
||||
}
|
||||
|
||||
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 parseTimeToMinutes(v, fallback = null) {
|
||||
const s = (v ?? '').toString().trim();
|
||||
const m = s.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
|
||||
if (!m) return fallback;
|
||||
|
||||
const hh = Number(m[1]);
|
||||
const mm = Number(m[2]);
|
||||
|
||||
if (!Number.isFinite(hh) || !Number.isFinite(mm)) return fallback;
|
||||
return (hh % 24) * 60 + mm;
|
||||
}
|
||||
|
||||
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}`;
|
||||
const stamp = `${date} ${to2(hour)}:${parts.minute}:${parts.second}`;
|
||||
|
||||
return {
|
||||
year: Number(parts.year),
|
||||
month: Number(parts.month),
|
||||
day: Number(parts.day),
|
||||
hour,
|
||||
minute: Number(parts.minute),
|
||||
second: Number(parts.second),
|
||||
date,
|
||||
stamp,
|
||||
minNow: hour * 60 + Number(parts.minute),
|
||||
};
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
if (![y, mo, d, h, mi, se].every(Number.isFinite)) return null;
|
||||
|
||||
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 cleanText(v, fallback = '') {
|
||||
const s = (v ?? '').toString().trim();
|
||||
const x = s.toLowerCase();
|
||||
if (['unknown', 'unavailable', 'none', 'null', 'undefined'].includes(x)) return fallback;
|
||||
return s;
|
||||
}
|
||||
|
||||
function finiteOrNull(v) {
|
||||
return Number.isFinite(v) ? v : null;
|
||||
}
|
||||
|
||||
const raw = $input.first().json;
|
||||
const input = extractSnapshot(raw);
|
||||
|
||||
const nowMs = Date.now();
|
||||
const now = getParisParts(nowMs);
|
||||
|
||||
const CFG = {
|
||||
mobileNotifyDomain: 'notify',
|
||||
mobileNotifyService: 'mobile_app_iphone_16_de_tt',
|
||||
|
||||
batteryUsableCapacityKwh: 17.5,
|
||||
nominalAcVoltageV: 230,
|
||||
evcsDedicatedMaxW: 4900,
|
||||
systemReserveW: 300,
|
||||
|
||||
vebusDcVoltageSoftFloorV: 49.0,
|
||||
vebusDcVoltageHardFloorV: 48.6,
|
||||
|
||||
departurePriorityWindowMinutes: 240,
|
||||
sampleSeconds: 30,
|
||||
|
||||
cloudDebtRecoveryThresholdKwh: 0.05,
|
||||
cloudDebtMeaningfulThresholdKwh: 0.01,
|
||||
};
|
||||
|
||||
// =====================================================
|
||||
// PARSED SNAPSHOT
|
||||
// =====================================================
|
||||
const parsed = {
|
||||
soc: clamp(num(input.soc_percent, 0), 0, 100),
|
||||
batteryCurrentA: num(input.battery_current_a, 0),
|
||||
|
||||
pvPowerW: Math.max(0, num(input.pv_power_w, 0)),
|
||||
pvPowerAvg10mW: Math.max(0, num(input.pv_power_10m_avg_w, num(input.pv_power_w, 0))),
|
||||
pvTrendW: num(input.pv_trend_w, 0),
|
||||
|
||||
houseAcPowerW: Math.max(0, num(input.house_ac_power_w, 0)),
|
||||
houseAcPowerPrevW: Math.max(0, num(input.house_ac_power_prev_w, num(input.house_ac_power_w, 0))),
|
||||
gridPowerW: num(input.grid_power_w, 0),
|
||||
|
||||
gridIsOn: low(input.grid_state || 'off') === 'on',
|
||||
acInAvailable: num(input.victron_acin1_available ?? input.ac_in1_available, 0) === 1,
|
||||
gridAvailable:
|
||||
(low(input.grid_state || 'off') === 'on') ||
|
||||
(num(input.victron_acin1_available ?? input.ac_in1_available, 0) === 1),
|
||||
|
||||
todayRemainingKwh: Math.max(0, num(input.today_remaining_kwh, 0)),
|
||||
|
||||
evcsDeviceConnected: low(input.evcs_device_connected || 'off') === 'on',
|
||||
evcsVehicleConnected: low(input.evcs_vehicle_connected || 'off') === 'on',
|
||||
evcsStatusRaw: cleanText(input.evcs_status_raw),
|
||||
evcsModeRaw: [
|
||||
input.evcs_mode_sensor,
|
||||
input.evcs_mode_fallback,
|
||||
input.evcs_mode
|
||||
]
|
||||
.map(v => cleanText(v))
|
||||
.find(v => v) || 'unknown',
|
||||
|
||||
evcsSetCurrentA: num(input.evcs_set_current_a, NaN),
|
||||
evcsCommandCurrentA: num(input.evcs_command_current_a, NaN),
|
||||
evcsCurrentA: Math.max(0, num(input.evcs_current_a, 0)),
|
||||
evcsAcPowerW: Math.max(0, num(input.evcs_ac_power_w, 0)),
|
||||
evcsChargingSwitchOn: low(input.evcs_charging_switch || 'off') === 'on',
|
||||
evcsAutoStartSwitchOn: low(input.evcs_auto_start_switch || 'off') === 'on',
|
||||
|
||||
controllerEnabled: low(input.controller_enabled || 'off') === 'on',
|
||||
notificationsEnabled: low(input.notifications_enabled || 'off') === 'on',
|
||||
hardStopEnabledLegacy: low(input.hard_stop_enabled || 'off') === 'on',
|
||||
chargePermissionNow: low(input.charge_permission_now || 'off') === 'on',
|
||||
controlLatch: low(input.control_latch || 'off') === 'on',
|
||||
departurePriorityEnabled: low(input.departure_priority_enabled || 'off') === 'on',
|
||||
forecastModeEnabled: low(input.forecast_mode_enabled || 'off') === 'on',
|
||||
|
||||
evcsPriorityManualOverride: low(input.evcs_priority_manual_override || 'off') === 'on',
|
||||
evcsPriorityManualMode: cleanText(input.evcs_priority_manual_mode || 'normal', 'normal').toLowerCase(),
|
||||
evcsPriorityManualReason: cleanText(input.evcs_priority_manual_reason),
|
||||
|
||||
vebusOverload: low(input.vebus_overload || 'off') === 'on',
|
||||
vebusL1Overload: low(input.vebus_l1_overload || 'off') === 'on',
|
||||
vebusLowBattery: low(input.vebus_low_battery || 'off') === 'on',
|
||||
vebusL1LowBattery: low(input.vebus_l1_low_battery || 'off') === 'on',
|
||||
|
||||
vebusAcOutPowerW: Math.max(0, num(input.vebus_ac_out_l1_power_w, 0)),
|
||||
vebusAcOutCurrentA: Math.max(0, num(input.vebus_ac_out_l1_current_a, 0)),
|
||||
vebusAcOutVoltageV: num(input.vebus_ac_out_l1_voltage_v, CFG.nominalAcVoltageV),
|
||||
|
||||
vebusDcVoltageV: num(input.vebus_dc_voltage_v, NaN),
|
||||
vebusDcCurrentA: num(input.vebus_dc_current_a, NaN),
|
||||
vebusDcPowerW: num(input.vebus_dc_power_w, NaN),
|
||||
|
||||
vebusState: num(input.vebus_state, NaN),
|
||||
vebusMainState: num(input.vebus_main_state, NaN),
|
||||
};
|
||||
|
||||
parsed.evcsModeIsAuto = low(parsed.evcsModeRaw).includes('auto');
|
||||
292
decoupesurplus/V2/INPUTPARSE/inputparseP2.js
Normal file
292
decoupesurplus/V2/INPUTPARSE/inputparseP2.js
Normal file
@@ -0,0 +1,292 @@
|
||||
// =====================================================
|
||||
// PARAMETERS
|
||||
// =====================================================
|
||||
const params = {
|
||||
userStrategyRaw: input.victron_evcs_strategy || 'auto_solar',
|
||||
|
||||
targetCurrentAutoA: clamp(num(input.target_current_auto_a, 6), 0, 32),
|
||||
pvStartMinW: Math.max(0, num(input.pv_start_min_w, 1800)),
|
||||
pvHoldMinW: Math.max(0, num(input.pv_hold_min_w, 1200)),
|
||||
houseAcHeadroomW: Math.max(0, num(input.house_ac_headroom_w, 1200)),
|
||||
|
||||
batteryDischargeSoftA: num(input.battery_discharge_soft_a, -5),
|
||||
batteryDischargeHardA: num(input.battery_discharge_hard_a, -15),
|
||||
batteryReserveUpA: Math.max(0, num(input.battery_reserve_up_a, 8)),
|
||||
batteryNeutralBandA: Math.max(0, num(input.battery_neutral_band_a, 3)),
|
||||
|
||||
socStartMin: clamp(num(input.soc_start_min, 60), 0, 100),
|
||||
socStopMin: clamp(num(input.soc_stop_min, 35), 0, 100),
|
||||
|
||||
currentMinA: clamp(num(input.current_min_a, 6), 6, 32),
|
||||
currentMaxAutoA: clamp(
|
||||
num(input.current_max_auto_a, 16),
|
||||
clamp(num(input.current_min_a, 6), 6, 32),
|
||||
32
|
||||
),
|
||||
currentStepUpA: clamp(num(input.current_step_up_a, 1), 1, 5),
|
||||
currentStepDownA: clamp(num(input.current_step_down_a, 2), 1, 8),
|
||||
|
||||
currentChangeCooldownSeconds: Math.max(5, num(input.current_change_cooldown_seconds, 60)),
|
||||
minRunMinutes: Math.max(0, num(input.min_run_minutes, 3)),
|
||||
minStopMinutes: Math.max(0, num(input.min_stop_minutes, 10)),
|
||||
|
||||
requiredEnergyKwh: Math.max(0, num(input.required_energy_kwh, 0)),
|
||||
socComfortTarget: clamp(num(input.soc_comfort_target, 85), 0, 100),
|
||||
endOfDaySocGuard: clamp(num(input.end_of_day_soc_guard, 85), 0, 100),
|
||||
baseLoadRemainingKwh: Math.max(0, num(input.base_load_remaining_kwh, 0)),
|
||||
flexibleSafetyMarginKwh: Math.max(0, num(input.flexible_safety_margin_kwh, 0)),
|
||||
endOfDaySocTarget: clamp(num(input.end_of_day_soc_target, 85), 0, 100),
|
||||
|
||||
acLoadsMaxSafeW: Math.max(0, num(input.ac_loads_max_safe_w, 8500)),
|
||||
batteryFullSoc: clamp(num(input.battery_full_soc, 90), 0, 100),
|
||||
batteryFullForecastMinKwh: Math.max(0, num(input.battery_full_forecast_min_kwh, 8)),
|
||||
forecastCommitSocFloor: clamp(num(input.forecast_commit_soc_floor, 35), 0, 100),
|
||||
forecastCommitMinKwh: Math.max(0, num(input.forecast_commit_min_kwh, 25)),
|
||||
|
||||
cloudDebtPrevKwh: Math.max(0, num(input.cloud_debt_kwh, 0)),
|
||||
cloudDebtMaxKwh: Math.max(0.05, num(input.cloud_debt_max_kwh, 1.2)),
|
||||
cloudToleranceMinutes: Math.max(1, num(input.cloud_tolerance_minutes, 20)),
|
||||
|
||||
departureTimeMin: parseTimeToMinutes(input.departure_time, null),
|
||||
dayStartTimeMin: parseTimeToMinutes(input.day_start_time, 7 * 60 + 30),
|
||||
dayStopTimeMin: parseTimeToMinutes(input.day_stop_time, 20 * 60 + 30),
|
||||
|
||||
hcNightStartMin: parseTimeToMinutes(input.hc_night_start_time, 23 * 60),
|
||||
hcNightStopMin: parseTimeToMinutes(input.hc_night_stop_time, 6 * 60),
|
||||
morningStartMin: parseTimeToMinutes(
|
||||
input.morning_start_time,
|
||||
parseTimeToMinutes(input.day_start_time, 7 * 60 + 30)
|
||||
),
|
||||
morningStopMin: parseTimeToMinutes(input.morning_stop_time, 11 * 60),
|
||||
hcMiddayStartMin: parseTimeToMinutes(input.hc_midday_start_time, 12 * 60 + 21),
|
||||
hcMiddayStopMin: parseTimeToMinutes(input.hc_midday_stop_time, 14 * 60 + 21),
|
||||
eveningStartMin: parseTimeToMinutes(input.evening_start_time, 17 * 60),
|
||||
eveningStopMin: parseTimeToMinutes(
|
||||
input.evening_stop_time,
|
||||
parseTimeToMinutes(input.day_stop_time, 20 * 60 + 30)
|
||||
),
|
||||
|
||||
allowGridHcNight: low(input.allow_grid_hc_night || 'off') === 'on',
|
||||
allowGridHcMidday: low(input.allow_grid_hc_midday || 'off') === 'on',
|
||||
|
||||
probeEnabled: low(input.probe_enabled || 'off') === 'on',
|
||||
probeMinutes: Math.max(1, num(input.probe_minutes, 3)),
|
||||
probeRetryMinutes: Math.max(5, num(input.probe_retry_minutes, 45)),
|
||||
probeCurrentA: clamp(
|
||||
num(input.probe_current_a, clamp(num(input.current_min_a, 6), 6, 32)),
|
||||
clamp(num(input.current_min_a, 6), 6, 32),
|
||||
clamp(num(input.current_max_auto_a, 16), clamp(num(input.current_min_a, 6), 6, 32), 32)
|
||||
),
|
||||
|
||||
gridBatterySoftA: num(input.grid_battery_soft_a, -3),
|
||||
gridBatteryHardA: num(input.grid_battery_hard_a, -6),
|
||||
|
||||
morningProbePvAvgMinW: Math.max(0, num(input.morning_probe_pv_avg_min_w, 1500)),
|
||||
morningProbePvTrendMinW: num(input.morning_probe_pv_trend_min_w, 0),
|
||||
middayProbePvAvgMinW: Math.max(0, num(input.midday_probe_pv_avg_min_w, 1200)),
|
||||
|
||||
morningSocMin: clamp(num(input.morning_soc_min, 35), 0, 100),
|
||||
middaySocMin: clamp(num(input.midday_soc_min, 75), 0, 100),
|
||||
eveningSocStartMin: clamp(num(input.evening_soc_start_min, 85), 0, 100),
|
||||
eveningSocStopMin: clamp(num(input.evening_soc_stop_min, 80), 0, 100),
|
||||
|
||||
morningBatteryMaxDischargeA: num(input.morning_battery_max_discharge_a, -20),
|
||||
middayBatteryMaxDischargeA: num(input.midday_battery_max_discharge_a, -40),
|
||||
eveningBatteryMaxDischargeA: num(input.evening_battery_max_discharge_a, -30),
|
||||
|
||||
// =====================================================
|
||||
// EVCS NIGHT ENGINE — NEW PARAMETERS
|
||||
// =====================================================
|
||||
evcsNightBatteryHardFloorPct: clamp(
|
||||
num(input.evcs_night_battery_hard_floor_pct, 22),
|
||||
0,
|
||||
100
|
||||
),
|
||||
|
||||
evcsNightStrongForecastKwh: Math.max(
|
||||
0,
|
||||
num(input.evcs_night_strong_forecast_kwh, 18)
|
||||
),
|
||||
|
||||
evcsNightMediumForecastKwh: Math.max(
|
||||
0,
|
||||
num(input.evcs_night_medium_forecast_kwh, 10)
|
||||
),
|
||||
|
||||
evcsNightTargetStrongForecastPct: clamp(
|
||||
num(input.evcs_night_target_strong_forecast_pct, 35),
|
||||
0,
|
||||
100
|
||||
),
|
||||
|
||||
evcsNightTargetMediumForecastPct: clamp(
|
||||
num(input.evcs_night_target_medium_forecast_pct, 55),
|
||||
0,
|
||||
100
|
||||
),
|
||||
|
||||
evcsNightTargetWeakForecastPct: clamp(
|
||||
num(input.evcs_night_target_weak_forecast_pct, 85),
|
||||
0,
|
||||
100
|
||||
),
|
||||
|
||||
evcsNightBatteryRecoveryPowerKw: Math.max(
|
||||
0.1,
|
||||
num(input.evcs_night_battery_recovery_power_kw, 2.5)
|
||||
),
|
||||
|
||||
evcsNightDepartureSafetyMin: Math.max(
|
||||
0,
|
||||
num(input.evcs_night_departure_safety_min, 30)
|
||||
),
|
||||
};
|
||||
|
||||
// =====================================================
|
||||
// HISTORY / PERSISTED HELPERS
|
||||
// =====================================================
|
||||
const nightSessionStartSocRaw = num(
|
||||
input.evcs_night_session_start_soc ?? input.night_session_start_soc,
|
||||
NaN
|
||||
);
|
||||
|
||||
const nightSessionStartAtRaw = cleanText(
|
||||
input.evcs_night_session_start_at ?? input.night_session_start_at
|
||||
);
|
||||
|
||||
const history = {
|
||||
lastBlockReason: cleanText(input.last_block_reason, 'none') || 'none',
|
||||
blockedUntilAt: cleanText(input.blocked_until),
|
||||
probeStartedAt: cleanText(input.probe_started_at),
|
||||
lastStartAt: cleanText(input.last_start_at),
|
||||
lastStopAt: cleanText(input.last_stop_at),
|
||||
lastCurrentChangeAt: cleanText(input.last_current_change_at),
|
||||
lastSignature: cleanText(input.last_signature),
|
||||
cloudDebtStartedAt: cleanText(input.cloud_debt_started_at),
|
||||
evcsPriorityManualUntilAt: cleanText(input.evcs_priority_manual_until),
|
||||
|
||||
blockedUntilMs: parseParisStampToMs(cleanText(input.blocked_until)),
|
||||
probeStartedMs: parseParisStampToMs(cleanText(input.probe_started_at)),
|
||||
lastStartMs: parseParisStampToMs(cleanText(input.last_start_at)),
|
||||
lastStopMs: parseParisStampToMs(cleanText(input.last_stop_at)),
|
||||
lastCurrentChangeMs: parseParisStampToMs(cleanText(input.last_current_change_at)),
|
||||
cloudDebtStartedMsPrev: parseParisStampToMs(cleanText(input.cloud_debt_started_at)),
|
||||
evcsPriorityManualUntilMs: parseParisStampToMs(cleanText(input.evcs_priority_manual_until)),
|
||||
|
||||
// =====================================================
|
||||
// EVCS NIGHT SESSION MEMORY
|
||||
// =====================================================
|
||||
nightSessionStartSocPct: Number.isFinite(nightSessionStartSocRaw)
|
||||
? clamp(nightSessionStartSocRaw, 0, 100)
|
||||
: null,
|
||||
|
||||
nightSessionStartAt: nightSessionStartAtRaw || '',
|
||||
nightSessionStartMs: parseParisStampToMs(nightSessionStartAtRaw),
|
||||
};
|
||||
|
||||
// =====================================================
|
||||
// BATTERY TUNING
|
||||
// =====================================================
|
||||
const batteryTune = {
|
||||
global: {
|
||||
houseSpikeGraceMinutes: Math.max(0, num(input.house_spike_grace_minutes, 10)),
|
||||
solarPotentialGraceMinutes: Math.max(0, num(input.solar_potential_grace_minutes, 12)),
|
||||
stepUpCooldownSeconds: Math.max(5, num(input.step_up_cooldown_seconds, params.currentChangeCooldownSeconds)),
|
||||
stepDownCooldownSeconds: Math.max(5, num(input.step_down_cooldown_seconds, params.currentChangeCooldownSeconds)),
|
||||
forceModeAutoRevertMinutes: Math.max(0, num(input.force_mode_auto_revert_minutes, 120)),
|
||||
overloadStepDownA: Math.max(1, num(input.overload_step_down_a, 2)),
|
||||
overloadHoldSeconds: Math.max(5, num(input.overload_hold_seconds, 30)),
|
||||
overloadStopAfterSeconds: Math.max(10, num(input.overload_stop_after_seconds, 120)),
|
||||
},
|
||||
|
||||
bands: {
|
||||
gt90: {
|
||||
dischargeSoftA: num(input.batt_discharge_soft_gt90_a, -50),
|
||||
dischargeHardA: num(input.batt_discharge_hard_gt90_a, -100),
|
||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_gt90_a, 5)),
|
||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_gt90_min, 12)),
|
||||
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_gt90_sec, 20)),
|
||||
recoveryMin: Math.max(0, num(input.batt_recovery_gt90_min, 6)),
|
||||
},
|
||||
|
||||
'80_90': {
|
||||
dischargeSoftA: num(input.batt_discharge_soft_80_90_a, -40),
|
||||
dischargeHardA: num(input.batt_discharge_hard_80_90_a, -80),
|
||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_80_90_a, 10)),
|
||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_80_90_min, 10)),
|
||||
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_80_90_sec, 20)),
|
||||
recoveryMin: Math.max(0, num(input.batt_recovery_80_90_min, 6)),
|
||||
},
|
||||
|
||||
'60_80': {
|
||||
dischargeSoftA: num(input.batt_discharge_soft_60_80_a, -20),
|
||||
dischargeHardA: num(input.batt_discharge_hard_60_80_a, -50),
|
||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_60_80_a, 40)),
|
||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_60_80_min, 6)),
|
||||
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_60_80_sec, 15)),
|
||||
recoveryMin: Math.max(0, num(input.batt_recovery_60_80_min, 8)),
|
||||
},
|
||||
|
||||
'40_60': {
|
||||
dischargeSoftA: num(input.batt_discharge_soft_40_60_a, -12),
|
||||
dischargeHardA: num(input.batt_discharge_hard_40_60_a, -30),
|
||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_40_60_a, 70)),
|
||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_40_60_min, 4)),
|
||||
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_40_60_sec, 10)),
|
||||
recoveryMin: Math.max(0, num(input.batt_recovery_40_60_min, 10)),
|
||||
},
|
||||
|
||||
'25_40': {
|
||||
dischargeSoftA: num(input.batt_discharge_soft_25_40_a, -5),
|
||||
dischargeHardA: num(input.batt_discharge_hard_25_40_a, -15),
|
||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_25_40_a, 110)),
|
||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_25_40_min, 2)),
|
||||
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_25_40_sec, 5)),
|
||||
recoveryMin: Math.max(0, num(input.batt_recovery_25_40_min, 12)),
|
||||
},
|
||||
|
||||
lt25: {
|
||||
dischargeSoftA: num(input.batt_discharge_soft_lt25_a, -3),
|
||||
dischargeHardA: num(input.batt_discharge_hard_lt25_a, -8),
|
||||
chargeReserveA: Math.max(0, num(input.batt_charge_reserve_lt25_a, 150)),
|
||||
softToleranceMin: Math.max(0, num(input.batt_soft_tol_lt25_min, 1)),
|
||||
hardToleranceSec: Math.max(0, num(input.batt_hard_tol_lt25_sec, 2)),
|
||||
recoveryMin: Math.max(0, num(input.batt_recovery_lt25_min, 15)),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// =====================================================
|
||||
// OPTIONAL DEBUG / SUMMARY
|
||||
// =====================================================
|
||||
const evcsNightConfig = {
|
||||
batteryHardFloorPct: params.evcsNightBatteryHardFloorPct,
|
||||
strongForecastKwh: params.evcsNightStrongForecastKwh,
|
||||
mediumForecastKwh: params.evcsNightMediumForecastKwh,
|
||||
targetStrongForecastPct: params.evcsNightTargetStrongForecastPct,
|
||||
targetMediumForecastPct: params.evcsNightTargetMediumForecastPct,
|
||||
targetWeakForecastPct: params.evcsNightTargetWeakForecastPct,
|
||||
batteryRecoveryPowerKw: params.evcsNightBatteryRecoveryPowerKw,
|
||||
departureSafetyMin: params.evcsNightDepartureSafetyMin,
|
||||
sessionStartSocPct: history.nightSessionStartSocPct,
|
||||
sessionStartAt: history.nightSessionStartAt || null,
|
||||
};
|
||||
|
||||
// =====================================================
|
||||
// OUTPUT
|
||||
// =====================================================
|
||||
return [{
|
||||
json: {
|
||||
raw,
|
||||
input,
|
||||
nowMs,
|
||||
now,
|
||||
cfg: CFG,
|
||||
parsed,
|
||||
params,
|
||||
history,
|
||||
batteryTune,
|
||||
evcsNightConfig,
|
||||
}
|
||||
}];
|
||||
|
||||
Reference in New Issue
Block a user