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,404 @@
// =====================================================
// GRID RELAX IN force_charge_now
// =====================================================
// Assouplissement uniquement pour la garde réseau,
// sans toucher aux sécurités dures globales.
const gridForceChargeRelaxSoftA =
strategy.forceChargeRequested && isGridSource
? (
parsed.soc >= 90 ? 28 :
parsed.soc >= 80 ? 16 :
parsed.soc >= 60 ? 8 :
parsed.soc >= 40 ? 4 :
0
)
: 0;
const gridForceChargeRelaxHardA =
strategy.forceChargeRequested && isGridSource
? (
parsed.soc >= 90 ? 39 :
parsed.soc >= 80 ? 29 :
parsed.soc >= 60 ? 18 :
parsed.soc >= 40 ? 8 :
0
)
: 0;
const gridBatterySoftDynamicABase =
Math.max(params.gridBatterySoftA, batteryDischargeSoftDynamicA);
const gridBatteryHardDynamicABase =
Math.max(params.gridBatteryHardA, batteryDischargeHardDynamicA);
let gridBatterySoftDynamicA =
gridBatterySoftDynamicABase - gridForceChargeRelaxSoftA;
let gridBatteryHardDynamicA =
gridBatteryHardDynamicABase - gridForceChargeRelaxHardA;
// Garantit que le hard reste plus permissif que le soft d'au moins 3 A
if (gridBatteryHardDynamicA > (gridBatterySoftDynamicA - 3)) {
gridBatteryHardDynamicA = gridBatterySoftDynamicA - 3;
}
// =====================================================
// BASE SAFETY
// =====================================================
const commonSafetyStartOk =
derived.canRunAtMinCurrent &&
!derived.anyVebusOverload &&
!derived.anyVebusLowBattery &&
!derived.vebusDcVoltageSoftLow &&
!derived.vebusAcOutSoftReached &&
parsed.evcsDeviceConnected &&
parsed.evcsVehicleConnected &&
!parsed.evcsModeIsAuto;
const commonSafetyHoldOk =
derived.canRunAtMinCurrent &&
!derived.anyVebusLowBattery &&
!derived.vebusDcVoltageHardLow &&
parsed.evcsDeviceConnected &&
parsed.evcsVehicleConnected &&
!parsed.evcsModeIsAuto;
// =====================================================
// START / HOLD CONDITIONS
// =====================================================
const solarStartOk =
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
commonSafetyStartOk &&
parsed.soc >= profileSocStartFloor &&
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
(
context.sourceMode === 'solar_potential' ||
parsed.pvPowerW >= context.effectivePvStartMinW
);
const solarHoldOk =
(context.sourceMode === 'solar_only' || context.sourceMode === 'solar_potential') &&
commonSafetyHoldOk &&
parsed.soc >= profileSocHoldFloor &&
parsed.batteryCurrentA >= profileBatteryDischargeFloorA &&
(
parsed.pvPowerW >= params.pvHoldMinW ||
cloudToleranceOk
);
const gridStartOk =
isGridSource &&
commonSafetyStartOk &&
parsed.gridAvailable &&
parsed.batteryCurrentA >= gridBatterySoftDynamicA;
const gridHoldOk =
isGridSource &&
commonSafetyHoldOk &&
parsed.gridAvailable &&
parsed.batteryCurrentA >= gridBatteryHardDynamicA;
const batteryAssistNightStartOk =
context.sourceMode === 'battery_assist_night' &&
commonSafetyStartOk &&
nightSessionActive &&
batteryAssistAllowed &&
parsed.soc > batteryHardFloorPct &&
parsed.soc > gridTakeoverTriggerPct &&
parsed.batteryCurrentA >= profileBatteryDischargeFloorA;
const batteryAssistNightHoldOk =
context.sourceMode === 'battery_assist_night' &&
commonSafetyHoldOk &&
nightSessionActive &&
batteryAssistAllowed &&
parsed.soc > batteryHardFloorPct &&
parsed.soc > gridTakeoverTriggerPct &&
parsed.batteryCurrentA >= batteryDischargeHardDynamicA;
// =====================================================
// PROBE LOGIC — STRICTEMENT LIMITÉE AU MODE PROBE
// =====================================================
const probeModeActive = context.sourceMode === 'hybrid_probe';
const probeStartOk =
probeModeActive &&
commonSafetyStartOk &&
(context.morningProbeEligible || context.middayProbeEligible);
const probeHoldGuardOk =
probeModeActive &&
commonSafetyHoldOk &&
parsed.batteryCurrentA >= profileBatteryDischargeFloorA;
const probeWindowActive =
probeModeActive &&
sessionObservedActive &&
history.probeStartedMs !== null &&
lifecycle.minutesSinceProbeStart !== null &&
lifecycle.minutesSinceProbeStart < params.probeMinutes;
const probeElapsedReached =
probeModeActive &&
sessionObservedActive &&
history.probeStartedMs !== null &&
lifecycle.minutesSinceProbeStart !== null &&
lifecycle.minutesSinceProbeStart >= params.probeMinutes;
const probeSuccessNow =
probeModeActive &&
sessionObservedActive &&
history.probeStartedMs !== null &&
(
parsed.pvPowerW >= params.pvHoldMinW &&
parsed.batteryCurrentA >= Math.min(0, batteryDischargeSoftDynamicA)
);
const probeFailedNow =
probeModeActive &&
probeElapsedReached &&
!probeSuccessNow;
// =====================================================
// STEP-UP LOGIC
// =====================================================
const stepUpByBatteryReserve =
batteryPriorityMode &&
parsed.batteryCurrentA >= batteryChargeReserveDynamicA;
const stepUpByForecastMode =
!batteryPriorityMode &&
(
parsed.pvPowerW >= params.pvHoldMinW ||
parsed.pvPowerAvg10mW >= params.pvHoldMinW
) &&
derived.forecastBufferKwh >= -1 &&
derived.projectedEndSocAfterCharge >= (derived.endDayGuardSoc - 5) &&
cloudToleranceOk;
const solarShouldStepUp =
lifecycle.currentCooldownElapsed &&
derived.currentReferenceA < derived.physicalCapA &&
derived.physicalCapA >= params.currentMinA &&
!derived.vebusAcOutSoftReached &&
!derived.vebusDcVoltageSoftLow &&
!derived.houseLoadHighVsHeadroom &&
(
stepUpByBatteryReserve ||
stepUpByForecastMode ||
parsed.pvPowerW >= (context.effectivePvStartMinW + 400)
);
const gridShouldStepUp =
lifecycle.currentCooldownElapsed &&
derived.currentReferenceA < derived.physicalCapA &&
derived.physicalCapA >= params.currentMinA &&
!derived.vebusAcOutSoftReached &&
!derived.vebusDcVoltageSoftLow &&
parsed.batteryCurrentA >= gridBatterySoftDynamicA &&
!derived.houseLoadHighVsHeadroom;
const batteryAssistNightShouldStepUp =
context.sourceMode === 'battery_assist_night' &&
lifecycle.currentCooldownElapsed &&
derived.currentReferenceA < derived.physicalCapA &&
derived.physicalCapA >= params.currentMinA &&
!derived.vebusAcOutSoftReached &&
!derived.vebusDcVoltageSoftLow &&
!derived.houseLoadHighVsHeadroom &&
parsed.batteryCurrentA >= batteryChargeReserveDynamicA &&
batteryAssistAllowed;
// =====================================================
// STEP-DOWN LOGIC
// =====================================================
const solarShouldStepDownBattery =
parsed.batteryCurrentA <= batteryDischargeSoftDynamicA &&
lifecycle.currentCooldownElapsed &&
derived.currentReferenceA > params.currentMinA;
const solarShouldStepDownCloudDebt =
!cloudToleranceOk &&
lifecycle.currentCooldownElapsed &&
derived.currentReferenceA > params.currentMinA;
const gridShouldStepDownBattery =
parsed.batteryCurrentA < gridBatterySoftDynamicA &&
lifecycle.currentCooldownElapsed &&
derived.currentReferenceA > params.currentMinA;
const batteryAssistNightShouldStepDown =
context.sourceMode === 'battery_assist_night' &&
lifecycle.currentCooldownElapsed &&
derived.currentReferenceA > params.currentMinA &&
(
parsed.batteryCurrentA <= batteryDischargeSoftDynamicA ||
parsed.soc <= gridTakeoverTriggerPct ||
batteryRecoveryUrgent
);
const shouldStepDownVebusPower =
derived.vebusAcOutSoftReached &&
lifecycle.currentCooldownElapsed &&
derived.currentReferenceA > params.currentMinA;
const shouldStepDownVebusVoltage =
derived.vebusDcVoltageSoftLow &&
lifecycle.currentCooldownElapsed &&
derived.currentReferenceA > params.currentMinA;
const shouldStepDownOverload =
derived.anyVebusOverload &&
lifecycle.currentCooldownElapsed &&
derived.currentReferenceA > params.currentMinA;
const shouldStopOverloadAtMin =
derived.anyVebusOverload &&
derived.currentReferenceA <= params.currentMinA &&
sessionObservedActive;
// =====================================================
// HARD STOPS / PROTECTIONS
// =====================================================
const batteryCurrentHardExceeded =
parsed.batteryCurrentA <= batteryDischargeHardDynamicA;
// overload volontairement exclu du hard stop immédiat
const mustStopHard =
derived.anyVebusLowBattery ||
derived.vebusDcVoltageHardLow ||
derived.vebusAcOutHardReached ||
batteryCurrentHardExceeded;
const gridBatteryProtectionStop =
sessionObservedActive &&
isGridSource &&
parsed.batteryCurrentA < gridBatteryHardDynamicA;
// =====================================================
// OUTPUT
// =====================================================
return [{
json: {
...prev,
batteryPolicy: {
socBand,
batteryDischargeSoftDynamicA,
batteryDischargeHardDynamicA,
batteryChargeReserveDynamicA,
batterySoftToleranceMinutes,
batteryHardToleranceSeconds,
batteryRecoveryMinutes,
gridBatterySoftDynamicA,
gridBatteryHardDynamicA,
gridForceChargeRelaxSoftA,
gridForceChargeRelaxHardA,
houseSpikeDetected,
solarPotentialContext,
},
locks: {
nextAllowedWindowStartMs,
blockActive,
blockStillRelevant,
startBlockedByReason,
},
cloud: {
sessionObservedActive,
batteryNetPowerW,
cloudDebtNextKwh,
cloudDebtMeaningful,
cloudEventDischargingNow,
continuingCloudEvent,
enteringCloudDebt,
cloudDebtActive,
cloudDebtStartedMs,
cloudDebtDurationMinutes,
effectiveCloudToleranceMinutes,
cloudToleranceOk,
},
modes: {
batteryFullHarvestMode,
forecastCommitMode,
batteryPriorityMode,
},
nightEvcs: {
nightSessionActive,
nightSessionNeedsCapture,
nightSessionNeedsClear,
nightSessionStartSocPct,
nightSessionStartAt,
nightSessionStartMs,
batteryHardFloorPct,
batteryMorningTargetPct,
maxBatteryDischargePoints,
gridTakeoverTriggerPct,
batteryBelowHardFloor,
batteryBelowMorningTarget,
batteryAtOrBelowGridTakeoverTrigger,
batteryKwhMissingToTarget,
batteryRecoveryMinutesNeeded,
batteryRecoveryUrgent,
batteryRecoveryAllowedNow,
batteryTargetReached,
batteryAssistAllowed,
nightGridTakeoverNeeded,
nightPreferredSourceMode,
},
safeguards: {
isGridSource,
commonSafetyStartOk,
commonSafetyHoldOk,
profileSocStartFloor,
profileSocHoldFloor,
profileBatteryDischargeFloorA,
batteryCurrentHardExceeded,
mustStopHard,
gridBatteryProtectionStop,
},
conditions: {
solarStartOk,
solarHoldOk,
gridStartOk,
gridHoldOk,
batteryAssistNightStartOk,
batteryAssistNightHoldOk,
probeStartOk,
probeHoldGuardOk,
probeWindowActive,
probeElapsedReached,
probeSuccessNow,
probeFailedNow,
solarShouldStepUp,
gridShouldStepUp,
batteryAssistNightShouldStepUp,
solarShouldStepDownBattery,
solarShouldStepDownCloudDebt,
gridShouldStepDownBattery,
batteryAssistNightShouldStepDown,
shouldStepDownVebusPower,
shouldStepDownVebusVoltage,
shouldStepDownOverload,
shouldStopOverloadAtMin,
}
}
}];