version 2.2

This commit is contained in:
2026-04-12 05:41:42 +02:00
parent e2733b809b
commit 874aca1afa
9 changed files with 2287 additions and 0 deletions

View File

@@ -0,0 +1,381 @@
// === 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
}
}
}
}
}];