R2: git = source de vérité — src/nodes/ + build/extract intégrés au push

- 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.
This commit is contained in:
Tallagrand Thierry
2026-08-07 21:03:52 +02:00
parent b10c90c74f
commit 1ae8129658
78 changed files with 5132 additions and 0 deletions

View File

@@ -0,0 +1,419 @@
// =====================================================
// DECISION TREE
// =====================================================
if (!parsed.controllerEnabled) {
desiredCharging = parsed.evcsChargingSwitchOn;
desiredPermission = parsed.chargePermissionNow;
desiredLatch = parsed.controlLatch;
desiredCurrentA = clamp(
derived.currentReferenceA || params.currentMinA,
params.currentMinA,
params.currentMaxAutoA
);
state = 'controller_disabled';
currentDecision = 'hold';
dynamicOk = false;
mustStop = false;
reason.push('Contrôleur désactivé');
}
else if (strategy.isStopMode) {
setOff({
stateName: 'stopped_user',
decisionName: 'stop_stop_mode',
reasonText: 'Arrêt manuel via Stratégie STOP',
dynamic: false,
mustStopFlag: false,
blockReason: 'manual_stop',
blockedUntilStamp: now.stamp,
});
}
else if (parsed.evcsModeIsAuto) {
setOff({
stateName: 'mode_auto_conflict',
decisionName: 'stop_auto_mode_conflict',
reasonText: 'Borne en mode Auto interne',
dynamic: false,
mustStopFlag: true,
blockReason: 'mode_auto_conflict',
blockedUntilStamp: now.stamp,
});
}
else if (!parsed.evcsDeviceConnected || !parsed.evcsVehicleConnected) {
setOff({
stateName: 'vehicle_not_connected',
decisionName: 'stop_not_connected',
reasonText: 'Véhicule ou borne non connecté',
dynamic: false,
mustStopFlag: false,
blockReason: 'vehicle_not_connected',
blockedUntilStamp: now.stamp,
});
}
else if (safeguards.mustStopHard) {
setOff({
stateName:
derived.anyVebusLowBattery ? 'vebus_low_battery_hard_stop' :
derived.vebusDcVoltageHardLow ? 'vebus_dc_voltage_hard_stop' :
derived.vebusAcOutHardReached ? 'vebus_ac_hard_stop' :
safeguards.batteryCurrentHardExceeded ? 'battery_current_hard_stop' :
'hard_stop',
decisionName: 'stop_hard',
reasonText: 'Hard stop de protection',
dynamic: false,
mustStopFlag: true,
blockReason:
safeguards.batteryCurrentHardExceeded ? 'battery_current_hard_stop' :
derived.vebusDcVoltageHardLow ? 'vebus_dc_voltage_stop' :
derived.anyVebusLowBattery ? 'vebus_low_battery_stop' :
derived.vebusAcOutHardReached ? 'vebus_ac_hard_stop' :
'battery_protection_stop',
blockedUntilStamp: formatParisStampFromMs(nowMs + 15 * 60000),
});
}
else if (cloud.sessionObservedActive) {
if (cloud.enteringCloudDebt) {
setCloudDebtStartedAt = true;
}
if (conditions.shouldStopOverloadAtMin) {
setOff({
stateName: 'vebus_overload_stop_min_current',
decisionName: 'stop',
reasonText: 'Arrêt: surcharge convertisseur persistante au courant mini',
dynamic: false,
mustStopFlag: true,
blockReason: 'vebus_overload_persistent',
blockedUntilStamp: formatParisStampFromMs(
nowMs + batteryTune.global.overloadStopAfterSeconds * 1000
),
});
}
else if (safeguards.gridBatteryProtectionStop) {
setOff({
stateName: 'stop_grid_battery_guard',
decisionName: 'stop',
reasonText: `Arrêt charge réseau: batterie trop sollicitée (${round1(parsed.batteryCurrentA)}A)`,
dynamic: false,
mustStopFlag: true,
blockReason: 'grid_battery_guard',
blockedUntilStamp: formatParisStampFromMs(nowMs + 20 * 60000),
});
}
else if (conditions.probeFailedNow) {
setOff({
stateName: 'probe_failed_lock',
decisionName: 'stop',
reasonText: 'Mode sonde échoué',
dynamic: false,
mustStopFlag: true,
blockReason: 'probe_failed',
blockedUntilStamp: formatParisStampFromMs(nowMs + params.probeRetryMinutes * 60000),
});
}
else if (derived.physicalCapA < params.currentMinA) {
if (lifecycle.minRunElapsed) {
setOff({
stateName: 'stop_power_cap',
decisionName: 'stop_power_cap',
reasonText: 'Capacité système insuffisante',
dynamic: false,
mustStopFlag: true,
blockReason: 'system_capacity_stop',
blockedUntilStamp: formatParisStampFromMs(nowMs + 10 * 60000),
});
} else {
setOn({
stateName: 'hold_min_run_power_cap',
decisionName: 'hold',
currentA: params.currentMinA,
reasonText: "Maintien jusqu'au temps mini ON",
dynamic: false,
mustStopFlag: false,
});
}
}
else if (conditions.probeWindowActive && !conditions.probeSuccessNow) {
if (!conditions.probeHoldGuardOk) {
setOff({
stateName: 'probe_stop_guard',
decisionName: 'stop',
reasonText: 'Sonde interrompue: garde-fous non respectés',
dynamic: false,
mustStopFlag: true,
blockReason: 'probe_failed',
blockedUntilStamp: formatParisStampFromMs(nowMs + params.probeRetryMinutes * 60000),
});
} else {
setOn({
stateName: 'probe_hold',
decisionName: 'hold',
currentA: Math.min(params.probeCurrentA, derived.physicalCapA),
reasonText: `Mode sonde actif à ${round1(Math.min(params.probeCurrentA, derived.physicalCapA))}A`,
dynamic: true,
mustStopFlag: false,
});
}
}
else {
const activeSourceMode = conditions.probeSuccessNow ? 'solar_only' : context.sourceMode;
const holdConditionsOk =
(activeSourceMode === 'grid_hc_allowed' || activeSourceMode === 'grid_forced')
? conditions.gridHoldOk
: (activeSourceMode === 'solar_only' || activeSourceMode === 'solar_potential')
? conditions.solarHoldOk
: (activeSourceMode === 'hybrid_probe')
? conditions.probeHoldGuardOk
: false;
if (activeSourceMode === 'blocked') {
if (lifecycle.minRunElapsed) {
setOff({
stateName: 'stop_source_lost',
decisionName: 'stop',
reasonText: 'Source autorisée perdue',
dynamic: false,
mustStopFlag: true,
blockReason: context.profile === 'outside_window' ? 'outside_window' : 'source_unavailable',
blockedUntilStamp:
context.profile === 'outside_window' && Number.isFinite(locks.nextAllowedWindowStartMs)
? formatParisStampFromMs(locks.nextAllowedWindowStartMs)
: formatParisStampFromMs(nowMs + params.minStopMinutes * 60000),
});
} else {
setOn({
stateName: 'hold_min_run_source_lost',
decisionName: 'hold',
currentA: Math.min(derived.currentReferenceA, derived.physicalCapA),
reasonText: "Source perdue mais maintien jusqu'au mini ON",
dynamic: false,
mustStopFlag: false,
});
}
}
else if (!holdConditionsOk) {
if (lifecycle.minRunElapsed) {
setOff({
stateName: 'stop_conditions_lost',
decisionName: 'stop',
reasonText: 'Conditions de maintien perdues',
dynamic: false,
mustStopFlag: true,
blockReason: 'hold_conditions_lost',
blockedUntilStamp: formatParisStampFromMs(nowMs + 10 * 60000),
});
} else {
setOn({
stateName: 'hold_min_run',
decisionName: 'hold',
currentA: Math.min(derived.currentReferenceA, derived.physicalCapA),
reasonText: "Maintien jusqu'au temps mini ON",
dynamic: true,
mustStopFlag: false,
});
}
}
else if ((derived.currentReferenceA > derived.physicalCapA) && lifecycle.currentCooldownElapsed) {
setOn({
stateName: 'charging_step_down_system_cap',
decisionName: 'step_down',
currentA: Math.max(
params.currentMinA,
Math.min(
derived.currentReferenceA - Math.max(params.currentStepDownA, 2),
derived.physicalCapA
)
),
reasonText: 'Réduction pour respecter la capacité système',
dynamic: true,
mustStopFlag: false,
});
}
else if (conditions.shouldStepDownOverload) {
setOn({
stateName: 'charging_step_down_overload',
decisionName: 'step_down_fast',
currentA: Math.max(
params.currentMinA,
Math.min(
derived.currentReferenceA - Math.max(batteryTune.global.overloadStepDownA, params.currentStepDownA),
derived.physicalCapA
)
),
reasonText: 'Baisse rapide: surcharge convertisseur',
dynamic: true,
mustStopFlag: false,
});
}
else if (conditions.shouldStepDownVebusPower) {
setOn({
stateName: 'charging_step_down_vebus_power',
decisionName: 'step_down_fast',
currentA: Math.max(
params.currentMinA,
Math.min(
derived.currentReferenceA - Math.max(params.currentStepDownA + 1, 3),
derived.physicalCapA
)
),
reasonText: 'Baisse rapide: puissance VE.Bus élevée',
dynamic: true,
mustStopFlag: false,
});
}
else if (conditions.shouldStepDownVebusVoltage) {
setOn({
stateName: 'charging_step_down_dc_voltage',
decisionName: 'step_down_fast',
currentA: Math.max(
params.currentMinA,
Math.min(
derived.currentReferenceA - Math.max(params.currentStepDownA + 1, 3),
derived.physicalCapA
)
),
reasonText: 'Baisse rapide: tension DC basse',
dynamic: true,
mustStopFlag: false,
});
}
else if (
(activeSourceMode === 'grid_hc_allowed' || activeSourceMode === 'grid_forced') &&
conditions.gridShouldStepDownBattery
) {
setOn({
stateName: 'charging_step_down_grid_guard',
decisionName: 'step_down',
currentA: Math.max(
params.currentMinA,
Math.min(
derived.currentReferenceA - params.currentStepDownA,
derived.physicalCapA
)
),
reasonText: 'Réduction réseau: la batterie commence à participer',
dynamic: true,
mustStopFlag: false,
});
}
else if (
(activeSourceMode === 'solar_only' || activeSourceMode === 'solar_potential') &&
conditions.solarShouldStepDownCloudDebt
) {
setOn({
stateName: 'charging_step_down_cloud_debt',
decisionName: 'step_down',
currentA: Math.max(
params.currentMinA,
Math.min(
derived.currentReferenceA - Math.max(params.currentStepDownA, 2),
derived.physicalCapA
)
),
reasonText: 'Réduction: dette nuage trop élevée',
dynamic: true,
mustStopFlag: false,
});
}
else if (
(activeSourceMode === 'solar_only' || activeSourceMode === 'solar_potential') &&
conditions.solarShouldStepDownBattery
) {
setOn({
stateName: 'charging_step_down_battery',
decisionName: 'step_down',
currentA: Math.max(
params.currentMinA,
Math.min(
derived.currentReferenceA - params.currentStepDownA,
derived.physicalCapA
)
),
reasonText: 'Réduction: batterie trop sollicitée',
dynamic: true,
mustStopFlag: false,
});
}
else if (
(activeSourceMode === 'grid_hc_allowed' || activeSourceMode === 'grid_forced') &&
conditions.gridShouldStepUp
) {
setOn({
stateName:
activeSourceMode === 'grid_forced'
? 'charging_step_up_grid_forced'
: 'charging_step_up_grid_hc',
decisionName: 'step_up',
currentA: clamp(
derived.currentReferenceA + params.currentStepUpA,
params.currentMinA,
derived.physicalCapA
),
reasonText:
activeSourceMode === 'grid_forced'
? 'Hausse prudente du courant en charge réseau forcée'
: 'Hausse prudente du courant en HC réseau',
dynamic: true,
mustStopFlag: false,
});
}
else if (
(activeSourceMode === 'solar_only' || activeSourceMode === 'solar_potential') &&
conditions.solarShouldStepUp
) {
setOn({
stateName:
conditions.probeSuccessNow
? 'probe_success_step_up'
: (modes.batteryPriorityMode
? 'charging_step_up_battery'
: 'charging_step_up_forecast'),
decisionName: 'step_up',
currentA: clamp(
derived.currentReferenceA + params.currentStepUpA,
params.currentMinA,
derived.physicalCapA
),
reasonText: 'Hausse prudente du courant',
dynamic: true,
mustStopFlag: false,
});
}
else {
setOn({
stateName:
conditions.probeSuccessNow ? 'probe_success_hold' :
(activeSourceMode === 'grid_hc_allowed' || activeSourceMode === 'grid_forced') ? 'charging_hold_grid' :
cloud.cloudDebtActive ? 'charging_hold_cloud_tolerance' :
'charging_hold',
decisionName: 'hold',
currentA: Math.min(derived.currentReferenceA, derived.physicalCapA),
reasonText: 'Maintien du courant',
dynamic: true,
mustStopFlag: false,
});
}
}
}