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,217 @@
// === REASON-BASED LOCKS ===
const nextWindowCandidates = [
nextDailyOccurrenceMs(morningStartMin, nowMs),
nextDailyOccurrenceMs(hcMiddayStartMin, nowMs),
nextDailyOccurrenceMs(eveningStartMin, nowMs),
nextDailyOccurrenceMs(hcNightStartMin, nowMs),
].filter(v => Number.isFinite(v));
const nextAllowedWindowStartMs =
nextWindowCandidates.length > 0 ? Math.min(...nextWindowCandidates) : null;
const blockActive = blockedUntilMs !== null && blockedUntilMs > nowMs + 1000;
const blockStillRelevant =
lastBlockReason === 'outside_window' ? profile === 'outside_window' :
lastBlockReason === 'probe_failed' ? (profile === 'morning_solar' || profile === 'midday_hc_or_solar' || profile === 'outside_window') :
lastBlockReason === 'battery_protection_stop' ? (
batteryCurrentA < -batteryNeutralBandA || anyVebusLowBattery || vebusDcVoltageSoftLow
) :
lastBlockReason === 'grid_battery_guard' ? sourceMode === 'grid_hc_allowed' :
false;
const startBlockedByReason = blockActive && blockStillRelevant;
// === CLOUD DEBT ===
const sessionObservedActive = evcsChargingSwitchOn || controlLatch || chargePermissionNow;
const batteryNetPowerW = batteryCurrentA * batteryVoltageRef;
let cloudDebtNextKwh = cloudDebtPrevKwh;
if (sessionObservedActive) {
if (batteryNetPowerW < 0 && sourceMode !== 'grid_hc_allowed') {
cloudDebtNextKwh += (Math.abs(batteryNetPowerW) * CFG.sampleSeconds) / 3600000;
} else if (batteryNetPowerW > 0) {
cloudDebtNextKwh -= (batteryNetPowerW * CFG.sampleSeconds) / 3600000;
}
} else if (batteryNetPowerW > 0) {
cloudDebtNextKwh -= (batteryNetPowerW * CFG.sampleSeconds) / 3600000;
}
cloudDebtNextKwh = clamp(cloudDebtNextKwh, 0, cloudDebtMaxKwh * 4);
const cloudDebtMeaningful = cloudDebtNextKwh > CFG.cloudDebtMeaningfulThresholdKwh;
const cloudEventDischargingNow =
sessionObservedActive &&
sourceMode !== 'grid_hc_allowed' &&
batteryCurrentA < -Math.max(1, batteryNeutralBandA) &&
cloudDebtMeaningful;
const continuingCloudEvent =
cloudEventDischargingNow &&
cloudDebtStartedMsPrev !== null &&
cloudDebtPrevKwh > CFG.cloudDebtRecoveryThresholdKwh;
const enteringCloudDebt = cloudEventDischargingNow && !continuingCloudEvent;
const cloudDebtActive = cloudEventDischargingNow;
const cloudDebtStartedMs =
cloudEventDischargingNow
? (continuingCloudEvent ? cloudDebtStartedMsPrev : nowMs)
: null;
const cloudDebtDurationMinutes =
cloudDebtActive && cloudDebtStartedMs !== null
? Math.max(0, (nowMs - cloudDebtStartedMs) / 60000)
: 0;
const cloudToleranceOk =
cloudDebtNextKwh <= cloudDebtMaxKwh &&
(
!cloudDebtActive ||
cloudDebtDurationMinutes <= cloudToleranceMinutes
);
// === LEGACY SOLAR MODES KEPT ===
const batteryFullHarvestMode =
forecastModeEnabled &&
profile !== 'night_hc' &&
profile !== 'outside_window' &&
soc >= batteryFullSoc &&
todayRemainingKwh >= batteryFullForecastMinKwh &&
projectedEndSocAfterCharge >= (endDayGuardSoc - 2);
const forecastCommitMode =
forecastModeEnabled &&
profile !== 'night_hc' &&
profile !== 'outside_window' &&
soc >= forecastCommitSocFloor &&
todayRemainingKwh >= forecastCommitMinKwh &&
projectedEndSocAfterCharge >= endDayGuardSoc;
const batteryPriorityMode = !batteryFullHarvestMode && !forecastCommitMode;
// === BASE CONDITIONS ===
const commonSafetyStartOk =
canRunAtMinCurrent &&
!anyVebusOverload &&
!anyVebusLowBattery &&
!vebusDcVoltageSoftLow &&
!vebusAcOutSoftReached &&
evcsDeviceConnected &&
evcsVehicleConnected &&
!evcsModeIsAuto;
const commonSafetyHoldOk =
canRunAtMinCurrent &&
!anyVebusOverload &&
!anyVebusLowBattery &&
!vebusDcVoltageHardLow &&
evcsDeviceConnected &&
evcsVehicleConnected &&
!evcsModeIsAuto;
const profileSocStartFloor =
profile === 'morning_solar' ? morningSocMin :
profile === 'midday_hc_or_solar' ? middaySocMin :
profile === 'evening_high_soc' ? eveningSocStartMin :
socStartMin;
const profileSocHoldFloor =
profile === 'morning_solar' ? Math.max(socStopMin, morningSocMin - 10) :
profile === 'midday_hc_or_solar' ? Math.max(socStopMin, middaySocMin - 15) :
profile === 'evening_high_soc' ? eveningSocStopMin :
socStopMin;
const profileBatteryDischargeFloorA =
profile === 'morning_solar' ? morningBatteryMaxDischargeA :
profile === 'midday_hc_or_solar' ? middayBatteryMaxDischargeA :
profile === 'evening_high_soc' ? eveningBatteryMaxDischargeA :
batteryDischargeSoftA;
const solarStartOk =
sourceMode === 'solar_only' || sourceMode === 'solar_potential' &&
commonSafetyStartOk &&
soc >= profileSocStartFloor &&
batteryCurrentA >= profileBatteryDischargeFloorA &&
(sourceMode === 'solar_potential' || pvPowerW >= effectivePvStartMinW);
const solarHoldOk =
commonSafetyHoldOk &&
soc >= profileSocHoldFloor &&
batteryCurrentA >= profileBatteryDischargeFloorA &&
(pvPowerW >= pvHoldMinW || cloudToleranceOk);
const gridStartOk =
(sourceMode === 'grid_hc_allowed' || sourceMode === 'grid_forced') &&
commonSafetyStartOk &&
gridIsOn &&
batteryCurrentA >= gridBatterySoftA;
const gridHoldOk =
commonSafetyHoldOk &&
(sourceMode === 'grid_hc_allowed' || sourceMode === 'grid_forced') &&
gridIsOn &&
batteryCurrentA >= gridBatteryHardA;
const probeStartOk =
sourceMode === 'hybrid_probe' &&
commonSafetyStartOk &&
(morningProbeEligible || middayProbeEligible || afternoonProbeEligible);
const probeHoldGuardOk =
commonSafetyHoldOk &&
batteryCurrentA >= profileBatteryDischargeFloorA;
const probeWindowActive =
sessionObservedActive &&
probeStartedMs !== null &&
minutesSinceProbeStart !== null &&
minutesSinceProbeStart < probeMinutes;
const probeElapsedReached =
sessionObservedActive &&
probeStartedMs !== null &&
minutesSinceProbeStart !== null &&
minutesSinceProbeStart >= probeMinutes;
const probeSuccessNow =
sessionObservedActive &&
probeStartedMs !== null &&
(
pvPowerW >= pvHoldMinW &&
batteryCurrentA >= Math.min(0, batteryDischargeSoftA)
);
const probeFailedNow = probeElapsedReached && !probeSuccessNow;
// === STEP LOGIC ===
const stepUpByBatteryReserve =
batteryPriorityMode &&
batteryCurrentA >= batteryReserveUpA;
const stepUpByForecastMode =
!batteryPriorityMode &&
(pvPowerW >= pvHoldMinW || pvPowerAvg10mW >= pvHoldMinW) &&
forecastBufferKwh >= -1 &&
projectedEndSocAfterCharge >= (endDayGuardSoc - 5) &&
cloudToleranceOk;
const solarShouldStepUp =
currentCooldownElapsed &&
currentReferenceA < physicalCapA &&
physicalCapA >= currentMinA &&
!vebusAcOutSoftReached &&
!vebusDcVoltageSoftLow &&
!houseLoadHighVsHeadroom &&
(stepUpByBatteryReserve || stepUpByForecastMode || pvPowerW >= (effectivePvStartMinW + 400));
const gridShouldStepUp =
currentCooldownElapsed &&
currentReferenceA < physicalCapA &&
physicalCapA >= currentMinA &&
!vebusAcOutSoftReached &&
!vebusDcVoltageSoftLow &&
batteryCurrentA >= gridBatterySoftA &&
!houseLoadHighVsHeadroom;
const solarShouldStepDownBattery =
batteryCurrentA <= batteryDischargeSoftA &&
currentCooldownElapsed &&
currentReferenceA > currentMinA;
const solarShouldStepDownCloudDebt =
!cloudToleranceOk &&
currentCooldownElapsed &&
currentReferenceA > currentMinA;
const gridShouldStepDownBattery =
batteryCurrentA < gridBatterySoftA &&
currentCooldownElapsed &&
currentReferenceA > currentMinA;
const shouldStepDownVebusPower =
vebusAcOutSoftReached &&
currentCooldownElapsed &&
currentReferenceA > currentMinA;
const shouldStepDownVebusVoltage =
vebusDcVoltageSoftLow &&
currentCooldownElapsed &&
currentReferenceA > currentMinA;
const batteryCurrentHardExceeded = batteryCurrentA <= batteryDischargeHardA;
// === HARD STOPS ===
const mustStopHard =
anyVebusOverload ||
anyVebusLowBattery ||
vebusDcVoltageHardLow ||
vebusAcOutHardReached ||
batteryCurrentHardExceeded;
const gridBatteryProtectionStop =
sessionObservedActive &&
sourceMode === 'grid_hc_allowed' &&
batteryCurrentA < gridBatteryHardA;