// === 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 } } } } }];