- src/nodes/*.js (6 nodes + template snapshot) = source de vérité unique. - n8n-push.mjs reconstruit le JSON depuis src/ avant syntaxe+golden+PUT (src gagne toujours; n8n-extract.mjs pour resynchroniser si édition UI). - n8n-build.mjs --check pour vérifier la synchro. - Copies historiques déplacées dans archive/ (mortes, dérivées de la prod). - .gitignore: backups locaux de rollback. - AGENTS.md: règle R2 pour toutes les futures sessions (dont Qwen local). - Validé: idempotence, mutation/restauration, pipeline push complet en réel.
382 lines
12 KiB
JavaScript
382 lines
12 KiB
JavaScript
// === FINAL ACTIONS & BLOQUAGE PROPRE ===
|
|
desiredCurrentA = desiredCharging
|
|
? clamp(desiredCurrentA, currentMinA, Math.max(currentMinA, physicalCapA || currentMinA))
|
|
: currentMinA;
|
|
const chargingAction =
|
|
desiredCharging === evcsChargingSwitchOn
|
|
? 'none'
|
|
: desiredCharging ? 'turn_on' : 'turn_off';
|
|
const permissionAction =
|
|
desiredPermission === chargePermissionNow
|
|
? 'none'
|
|
: desiredPermission ? 'turn_on' : 'turn_off';
|
|
const latchAction =
|
|
desiredLatch === controlLatch
|
|
? 'none'
|
|
: desiredLatch ? 'turn_on' : 'turn_off';
|
|
const autoStartAction =
|
|
controllerEnabled &&
|
|
evcsVehicleConnected &&
|
|
evcsAutoStartSwitchOn
|
|
? 'turn_off'
|
|
: 'none';
|
|
const currentAction =
|
|
desiredCharging &&
|
|
(
|
|
!Number.isFinite(currentReferenceA) ||
|
|
Math.abs(desiredCurrentA - currentReferenceA) >= 0.5
|
|
)
|
|
? 'set'
|
|
: 'none';
|
|
setLastStartAt = chargingAction === 'turn_on';
|
|
setLastStopAt = chargingAction === 'turn_off';
|
|
setLastCurrentChangeAt = currentAction === 'set';
|
|
|
|
// === BLOQUAGE PROPRE (Clean Blocking Logic) ===
|
|
const needsBlockReasonUpdate = (nextBlockReason !== 'none');
|
|
|
|
// === REASONS / SIGNATURE ===
|
|
const strategyLabel = isForceMode ? userStrategy : (userStrategy === 'auto_solar_plus' ? 'auto_plus' : 'auto');
|
|
reason.push(`Stratégie=${strategyLabel}`);
|
|
reason.push(`Profil=${profile}`);
|
|
reason.push(`Source=${sourceMode}`);
|
|
reason.push(`Mode=${batteryFullHarvestMode ? 'battery_full_harvest_mode' : forecastCommitMode ? 'forecast_commit_mode' : 'battery_priority_mode'}`);
|
|
reason.push(`SOC=${round1(soc)}%`);
|
|
reason.push(`Batt=${round1(batteryCurrentA)}A`);
|
|
reason.push(`PV=${Math.round(pvPowerW)}W`);
|
|
reason.push(`PV10=${Math.round(pvPowerAvg10mW)}W`);
|
|
reason.push(`Grid=${gridIsOn ? 'on' : 'off'} (${Math.round(gridPowerW)}W)`);
|
|
reason.push(`Cap=${physicalCapA}A`);
|
|
reason.push(`Ref=${round1(currentReferenceA)}A`);
|
|
reason.push(`Target=${round1(desiredCurrentA)}A`);
|
|
reason.push(`Debt=${round2(cloudDebtNextKwh)}kWh`);
|
|
reason.push(`Vehicle=${evcsVehicleConnected ? 'on' : 'off'}`);
|
|
reason.push(`EVCS mode=${evcsModeRaw}`);
|
|
reason.push(`VE.Bus AC=${Math.round(vebusAcOutPowerW)}W`);
|
|
reason.push(`VE.Bus DC=${Number.isFinite(vebusDcVoltageV) ? round1(vebusDcVoltageV) : 'n/a'}V`);
|
|
const shortReason = reason.join(' | ').slice(0, 255);
|
|
const sourceModeLabel =
|
|
sourceMode === 'blocked' ? 'Bloqué' :
|
|
sourceMode === 'solar_only' ? 'Solaire' :
|
|
sourceMode === 'solar_potential' ? 'Potentiel solaire' :
|
|
sourceMode === 'hybrid_probe' ? 'Probe hybride' :
|
|
sourceMode === 'grid_hc_allowed' ? 'Réseau HC autorisé' :
|
|
sourceMode === 'grid_forced' ? 'Réseau forcé' :
|
|
sourceMode;
|
|
const signature = [
|
|
state,
|
|
`profile=${profile}`,
|
|
`source=${sourceMode}`,
|
|
`chg=${desiredCharging ? '1' : '0'}`,
|
|
`perm=${desiredPermission ? '1' : '0'}`,
|
|
`l=${desiredLatch ? '1' : '0'}`,
|
|
`cur=${round1(desiredCurrentA)}`,
|
|
`block=${nextBlockReason}`,
|
|
`stop=${mustStop ? '1' : '0'}`
|
|
].join('|');
|
|
const signatureChanged = signature !== lastSignature;
|
|
|
|
// === NOTIFICATIONS ===
|
|
if (notificationsEnabled && signatureChanged) {
|
|
if (currentDecision === 'start') {
|
|
notifyWanted = true;
|
|
notifyTitle = '⚡ EVCS démarrage';
|
|
notifyMessage = shortReason;
|
|
} else if (currentDecision === 'step_up') {
|
|
notifyWanted = true;
|
|
notifyTitle = '⬆️ EVCS hausse courant';
|
|
notifyMessage = shortReason;
|
|
} else if (currentDecision === 'step_down' || currentDecision === 'step_down_fast') {
|
|
notifyWanted = true;
|
|
notifyTitle = '⬇️ EVCS baisse courant';
|
|
notifyMessage = shortReason;
|
|
} else if (mustStop || currentDecision === 'stop' || currentDecision === 'stop_hard' || currentDecision === 'stop_power_cap') {
|
|
notifyWanted = true;
|
|
notifyTitle = '⛔ EVCS arrêt';
|
|
notifyMessage = shortReason;
|
|
} else if (state === 'mode_auto_conflict') {
|
|
notifyWanted = true;
|
|
notifyTitle = '⚠️ EVCS mode Auto';
|
|
notifyMessage = shortReason;
|
|
}
|
|
}
|
|
|
|
// === OUTPUT ===
|
|
return [{
|
|
json: {
|
|
timestamp_paris: now.stamp,
|
|
inputs: input,
|
|
computed: {
|
|
operating_mode: operatingMode,
|
|
profile,
|
|
source_mode: sourceMode,
|
|
in_night_hc_window: inNightHcWindow,
|
|
in_morning_window: inMorningWindow,
|
|
in_midday_window: inMiddayWindow,
|
|
in_evening_window: inEveningWindow,
|
|
departure_priority_active: departurePriorityActive,
|
|
minutes_until_departure: minutesUntilDeparture,
|
|
battery_priority_mode: batteryPriorityMode,
|
|
battery_full_harvest_mode: batteryFullHarvestMode,
|
|
forecast_commit_mode: forecastCommitMode,
|
|
physical_cap_current_a: physicalCapA,
|
|
can_run_at_min_current: canRunAtMinCurrent,
|
|
forecast_buffer_kwh: round2(forecastBufferKwh),
|
|
projected_end_soc_after_charge: round1(projectedEndSocAfterCharge),
|
|
cloud_debt_next_kwh: round2(cloudDebtNextKwh),
|
|
cloud_debt_duration_minutes: round1(cloudDebtDurationMinutes),
|
|
cloud_debt_active: cloudDebtActive,
|
|
cloud_tolerance_ok: cloudToleranceOk,
|
|
start_blocked_by_reason: startBlockedByReason,
|
|
probe_window_active: probeWindowActive,
|
|
probe_success_now: probeSuccessNow,
|
|
probe_failed_now: probeFailedNow,
|
|
any_vebus_overload: anyVebusOverload,
|
|
any_vebus_low_battery: anyVebusLowBattery,
|
|
vebus_ac_out_soft_reached: vebusAcOutSoftReached,
|
|
vebus_ac_out_hard_reached: vebusAcOutHardReached,
|
|
vebus_dc_voltage_soft_low: vebusDcVoltageSoftLow,
|
|
vebus_dc_voltage_hard_low: vebusDcVoltageHardLow,
|
|
},
|
|
decision: {
|
|
state,
|
|
current_decision: currentDecision,
|
|
desired_charging: desiredCharging,
|
|
desired_permission: desiredPermission,
|
|
desired_latch: desiredLatch,
|
|
desired_current_a: round1(desiredCurrentA),
|
|
charging_action: chargingAction,
|
|
permission_action: permissionAction,
|
|
latch_action: latchAction,
|
|
current_action: currentAction,
|
|
auto_start_action: autoStartAction,
|
|
set_last_start_at: setLastStartAt,
|
|
set_last_stop_at: setLastStopAt,
|
|
set_last_current_change_at: setLastCurrentChangeAt,
|
|
set_cloud_debt_started_at: setCloudDebtStartedAt,
|
|
set_probe_started_at: setProbeStartedAt,
|
|
// MODIFIÉ: Ne met à jour que si besoin réel
|
|
set_block_reason: needsBlockReasonUpdate || !controllerEnabled,
|
|
block_reason: nextBlockReason,
|
|
// FORÇAGE : Mettre à jour systématiquement pour éviter les timestamps passés
|
|
set_blocked_until: true,
|
|
// Si blocage actif = heure futur. Si pas de blocage = Maintenant (pour effacer l'ancien).
|
|
blocked_until: nextBlockReason === 'none' ? now.stamp : nextBlockedUntilStamp,
|
|
dynamic_ok: dynamicOk,
|
|
must_stop: mustStop,
|
|
short_reason: shortReason,
|
|
reason,
|
|
signature,
|
|
signature_changed: signatureChanged,
|
|
should_notify: notifyWanted,
|
|
notify_title: notifyTitle,
|
|
notify_message: notifyMessage,
|
|
},
|
|
ha_service_calls: {
|
|
helper_state: {
|
|
domain: 'input_text',
|
|
service: 'set_value',
|
|
data: {
|
|
entity_id: 'input_text.victron_evcs_surplus_state',
|
|
value: state
|
|
}
|
|
},
|
|
helper_current_decision: {
|
|
domain: 'input_text',
|
|
service: 'set_value',
|
|
data: {
|
|
entity_id: 'input_text.victron_evcs_surplus_current_decision',
|
|
value: currentDecision
|
|
}
|
|
},
|
|
helper_reason: {
|
|
domain: 'input_text',
|
|
service: 'set_value',
|
|
data: {
|
|
entity_id: 'input_text.victron_evcs_surplus_reason',
|
|
value: shortReason
|
|
}
|
|
},
|
|
helper_operating_mode: {
|
|
domain: 'input_text',
|
|
service: 'set_value',
|
|
data: {
|
|
entity_id: 'input_text.victron_evcs_operating_mode',
|
|
value: operatingMode
|
|
}
|
|
},
|
|
helper_target_current: {
|
|
domain: 'input_number',
|
|
service: 'set_value',
|
|
data: {
|
|
entity_id: 'input_number.victron_evcs_target_current_auto',
|
|
value: round1(desiredCurrentA)
|
|
}
|
|
},
|
|
helper_budget_remaining: {
|
|
domain: 'input_number',
|
|
service: 'set_value',
|
|
data: {
|
|
entity_id: 'input_number.victron_evcs_budget_remaining_kwh',
|
|
value: round2(forecastBufferKwh)
|
|
}
|
|
},
|
|
helper_projected_soc: {
|
|
domain: 'input_number',
|
|
service: 'set_value',
|
|
data: {
|
|
entity_id: 'input_number.victron_evcs_projected_end_soc_after_charge',
|
|
value: round1(projectedEndSocAfterCharge)
|
|
}
|
|
},
|
|
helper_cloud_debt: {
|
|
domain: 'input_number',
|
|
service: 'set_value',
|
|
data: {
|
|
entity_id: 'input_number.victron_evcs_cloud_debt_kwh',
|
|
value: round2(cloudDebtNextKwh)
|
|
}
|
|
},
|
|
helper_dynamic_ok: {
|
|
domain: 'input_boolean',
|
|
service: dynamicOk ? 'turn_on' : 'turn_off',
|
|
data: {
|
|
entity_id: 'input_boolean.victron_evcs_dynamic_ok'
|
|
}
|
|
},
|
|
helper_must_stop: {
|
|
domain: 'input_boolean',
|
|
service: mustStop ? 'turn_on' : 'turn_off',
|
|
data: {
|
|
entity_id: 'input_boolean.victron_evcs_must_stop'
|
|
}
|
|
},
|
|
helper_cloud_debt_started_at: {
|
|
domain: 'input_datetime',
|
|
service: 'set_datetime',
|
|
data: {
|
|
entity_id: 'input_datetime.victron_evcs_cloud_debt_started_at',
|
|
datetime: cloudDebtStartedMs ? formatParisStampFromMs(cloudDebtStartedMs) : now.stamp
|
|
}
|
|
},
|
|
helper_last_current_change_at: {
|
|
domain: 'input_datetime',
|
|
service: 'set_datetime',
|
|
data: {
|
|
entity_id: 'input_datetime.victron_evcs_last_current_change_at',
|
|
datetime: now.stamp
|
|
}
|
|
},
|
|
helper_last_start_at: {
|
|
domain: 'input_datetime',
|
|
service: 'set_datetime',
|
|
data: {
|
|
entity_id: 'input_datetime.victron_evcs_last_start_at',
|
|
datetime: now.stamp
|
|
}
|
|
},
|
|
helper_last_stop_at: {
|
|
domain: 'input_datetime',
|
|
service: 'set_datetime',
|
|
data: {
|
|
entity_id: 'input_datetime.victron_evcs_last_stop_at',
|
|
datetime: now.stamp
|
|
}
|
|
},
|
|
helper_signature: {
|
|
domain: 'input_text',
|
|
service: 'set_value',
|
|
data: {
|
|
entity_id: 'input_text.victron_evcs_control_last_signature',
|
|
value: signature
|
|
}
|
|
},
|
|
helper_block_reason: {
|
|
domain: 'input_text',
|
|
service: 'set_value',
|
|
data: {
|
|
entity_id: 'input_text.victron_evcs_last_block_reason',
|
|
value: nextBlockReason
|
|
}
|
|
},
|
|
helper_blocked_until: {
|
|
domain: 'input_datetime',
|
|
service: 'set_datetime',
|
|
data: {
|
|
entity_id: 'input_datetime.victron_evcs_blocked_until',
|
|
datetime: nextBlockedUntilStamp
|
|
}
|
|
},
|
|
helper_probe_started_at: {
|
|
domain: 'input_datetime',
|
|
service: 'set_datetime',
|
|
data: {
|
|
entity_id: 'input_datetime.victron_evcs_probe_started_at',
|
|
datetime: now.stamp
|
|
}
|
|
},
|
|
evcs_auto_start_off: {
|
|
domain: 'switch',
|
|
service: 'turn_off',
|
|
data: {
|
|
entity_id: 'switch.victron_evcs_auto_start'
|
|
}
|
|
},
|
|
evcs_charging: {
|
|
domain: 'switch',
|
|
service: desiredCharging ? 'turn_on' : 'turn_off',
|
|
data: {
|
|
entity_id: 'switch.victron_evcs_charging'
|
|
}
|
|
},
|
|
helper_permission: {
|
|
domain: 'input_boolean',
|
|
service: desiredPermission ? 'turn_on' : 'turn_off',
|
|
data: {
|
|
entity_id: 'input_boolean.victron_evcs_charge_permission_now'
|
|
}
|
|
},
|
|
helper_latch: {
|
|
domain: 'input_boolean',
|
|
service: desiredLatch ? 'turn_on' : 'turn_off',
|
|
data: {
|
|
entity_id: 'input_boolean.victron_evcs_control_latch'
|
|
}
|
|
},
|
|
helper_source_mode: {
|
|
execute: true,
|
|
domain: 'input_text',
|
|
service: 'set_value',
|
|
data: {
|
|
entity_id: 'input_text.victron_evcs_source_mode',
|
|
value: sourceModeLabel
|
|
}
|
|
},
|
|
evcs_set_current: {
|
|
domain: 'number',
|
|
service: 'set_value',
|
|
data: {
|
|
entity_id: 'number.victron_evcs_set_current_command',
|
|
value: round1(desiredCurrentA)
|
|
}
|
|
},
|
|
logbook: {
|
|
domain: 'logbook',
|
|
service: 'log',
|
|
data: {
|
|
name: 'Victron EVCS Surplus',
|
|
message: shortReason
|
|
}
|
|
},
|
|
mobile_notification: {
|
|
domain: CFG.mobileNotifyDomain,
|
|
service: CFG.mobileNotifyService,
|
|
data: {
|
|
title: notifyTitle,
|
|
message: notifyMessage || shortReason
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}];
|