correction logique commande priorisation
This commit is contained in:
279
DECISION/decision_engineP1.js
Normal file
279
DECISION/decision_engineP1.js
Normal file
@@ -0,0 +1,279 @@
|
||||
function clamp(v, min, max) {
|
||||
return Math.min(Math.max(v, min), max);
|
||||
}
|
||||
|
||||
function round1(v) {
|
||||
return Math.round(v * 10) / 10;
|
||||
}
|
||||
|
||||
function round3(v) {
|
||||
return Math.round(v * 1000) / 1000;
|
||||
}
|
||||
|
||||
function to2(n) {
|
||||
return String(n).padStart(2, '0');
|
||||
}
|
||||
|
||||
function finiteOr(v, fallback) {
|
||||
return Number.isFinite(v) ? v : fallback;
|
||||
}
|
||||
|
||||
function sigmoid(x) {
|
||||
return 1 / (1 + Math.exp(-x));
|
||||
}
|
||||
|
||||
function alphaFromTau(dtSec, tauSec) {
|
||||
const dt = Math.max(1, finiteOr(dtSec, 60));
|
||||
const tau = Math.max(1, finiteOr(tauSec, 60));
|
||||
return Math.min(1, Math.max(0.001, 1 - Math.exp(-dt / tau)));
|
||||
}
|
||||
|
||||
function ema(prev, sample, alpha) {
|
||||
if (!Number.isFinite(sample)) return Number.isFinite(prev) ? prev : 0;
|
||||
if (!Number.isFinite(prev)) return sample;
|
||||
return prev + alpha * (sample - prev);
|
||||
}
|
||||
|
||||
function formatParisStampFromMs(ms) {
|
||||
const fmt = new Intl.DateTimeFormat('fr-FR', {
|
||||
timeZone: 'Europe/Paris',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
|
||||
const parts = Object.fromEntries(
|
||||
fmt.formatToParts(new Date(ms))
|
||||
.filter(p => p.type !== 'literal')
|
||||
.map(p => [p.type, p.value])
|
||||
);
|
||||
|
||||
const hour = Number(parts.hour) === 24 ? 0 : Number(parts.hour);
|
||||
return `${parts.year}-${parts.month}-${parts.day} ${to2(hour)}:${parts.minute}:${parts.second}`;
|
||||
}
|
||||
|
||||
const prev = $input.first().json;
|
||||
|
||||
const {
|
||||
now,
|
||||
nowMs,
|
||||
parsed,
|
||||
params,
|
||||
history,
|
||||
strategy,
|
||||
lifecycle,
|
||||
context,
|
||||
derived,
|
||||
locks,
|
||||
cloud,
|
||||
modes,
|
||||
safeguards,
|
||||
conditions,
|
||||
batteryTune,
|
||||
nightEvcs,
|
||||
} = prev;
|
||||
|
||||
// =====================================================
|
||||
// DECISION INIT
|
||||
// =====================================================
|
||||
let desiredCharging = false;
|
||||
let desiredPermission = false;
|
||||
let desiredLatch = false;
|
||||
let desiredCurrentA = params.currentMinA;
|
||||
|
||||
let state = 'idle';
|
||||
let currentDecision = 'hold';
|
||||
let dynamicOk = false;
|
||||
let mustStop = false;
|
||||
|
||||
let reason = [];
|
||||
|
||||
let setCloudDebtStartedAt = false;
|
||||
let setProbeStartedAt = false;
|
||||
|
||||
// EVCS night session memory
|
||||
let setNightSessionStart = false;
|
||||
let clearNightSessionStart = false;
|
||||
|
||||
let notifyWanted = false;
|
||||
let notifyTitle = 'Victron EVCS Surplus';
|
||||
let notifyMessage = '';
|
||||
|
||||
let nextBlockReason = 'none';
|
||||
let nextBlockedUntilStamp = now.stamp;
|
||||
|
||||
// =====================================================
|
||||
// HELPERS
|
||||
// =====================================================
|
||||
function setOff({
|
||||
stateName,
|
||||
decisionName = 'stop',
|
||||
reasonText,
|
||||
dynamic = false,
|
||||
mustStopFlag = false,
|
||||
blockReason = 'none',
|
||||
blockedUntilStamp = now.stamp,
|
||||
}) {
|
||||
desiredCharging = false;
|
||||
desiredPermission = false;
|
||||
desiredLatch = false;
|
||||
desiredCurrentA = params.currentMinA;
|
||||
|
||||
state = stateName;
|
||||
currentDecision = decisionName;
|
||||
dynamicOk = dynamic;
|
||||
mustStop = mustStopFlag;
|
||||
nextBlockReason = blockReason;
|
||||
nextBlockedUntilStamp = blockedUntilStamp;
|
||||
|
||||
if (reasonText) reason.push(reasonText);
|
||||
}
|
||||
|
||||
function setOn({
|
||||
stateName,
|
||||
decisionName = 'hold',
|
||||
currentA = params.currentMinA,
|
||||
reasonText,
|
||||
dynamic = true,
|
||||
mustStopFlag = false,
|
||||
}) {
|
||||
desiredCharging = true;
|
||||
desiredPermission = true;
|
||||
desiredLatch = true;
|
||||
desiredCurrentA = clamp(
|
||||
currentA,
|
||||
params.currentMinA,
|
||||
Math.max(params.currentMinA, derived.physicalCapA || params.currentMinA)
|
||||
);
|
||||
|
||||
state = stateName;
|
||||
currentDecision = decisionName;
|
||||
dynamicOk = dynamic;
|
||||
mustStop = mustStopFlag;
|
||||
|
||||
if (reasonText) reason.push(reasonText);
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// SESSION MEMORY SIDE EFFECTS
|
||||
// =====================================================
|
||||
if (nightEvcs?.nightSessionNeedsCapture) {
|
||||
setNightSessionStart = true;
|
||||
}
|
||||
|
||||
if (nightEvcs?.nightSessionNeedsClear) {
|
||||
clearNightSessionStart = true;
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// EVCS NUMERIC NO-DEMAND FILTER
|
||||
// Objectif:
|
||||
// détecter "voiture branchée + session armée + commande claire
|
||||
// mais plus de demande réelle" sans seuils fixes bruts.
|
||||
// IMPORTANT:
|
||||
// on n'utilise PAS le PV comme critère.
|
||||
// =====================================================
|
||||
const prevDeliveryRatioEma = finiteOr(history.evcsDeliveryRatioEma, NaN);
|
||||
const prevDeliveryNoiseEma = finiteOr(history.evcsDeliveryNoiseEma, NaN);
|
||||
const prevNoDemandScoreEma = finiteOr(history.evcsNoDemandScoreEma, NaN);
|
||||
const prevNoDemandSemanticActive = !!history.evcsNoDemandSemanticActive;
|
||||
const prevNoDemandSinceMs = finiteOr(history.evcsNoDemandSinceMs, NaN);
|
||||
const prevNoDemandFilterEvalMs = finiteOr(history.evcsNoDemandFilterEvalMs, NaN);
|
||||
|
||||
const noDemandDtSec =
|
||||
Number.isFinite(prevNoDemandFilterEvalMs) && Number.isFinite(nowMs)
|
||||
? clamp((nowMs - prevNoDemandFilterEvalMs) / 1000, 1, 600)
|
||||
: 60;
|
||||
|
||||
// Constantes dynamiques du filtre = temps de réponse du filtre,
|
||||
// pas des seuils métiers bruts.
|
||||
const deliveryTauSec = Math.max(15, finiteOr(params.evcsDeliveryTauSec, 45));
|
||||
const noiseTauSec = Math.max(deliveryTauSec, finiteOr(params.evcsDeliveryNoiseTauSec, 180));
|
||||
const scoreTauSec = Math.max(20, finiteOr(params.evcsNoDemandScoreTauSec, 150));
|
||||
const armTauSec = Math.max(20, finiteOr(params.evcsNoDemandArmTauSec, 180));
|
||||
|
||||
const alphaDelivery = alphaFromTau(noDemandDtSec, deliveryTauSec);
|
||||
const alphaNoise = alphaFromTau(noDemandDtSec, noiseTauSec);
|
||||
const alphaScore = alphaFromTau(noDemandDtSec, scoreTauSec);
|
||||
|
||||
const acVoltageRef = finiteOr(derived.acVoltageRef, 230);
|
||||
|
||||
const commandCurrentA = clamp(
|
||||
finiteOr(
|
||||
derived.currentReferenceA,
|
||||
finiteOr(
|
||||
parsed.evcsCommandCurrentA,
|
||||
finiteOr(parsed.evcsSetCurrentA, finiteOr(params.currentMinA, 6))
|
||||
)
|
||||
),
|
||||
0,
|
||||
32
|
||||
);
|
||||
|
||||
const expectedPowerW = Math.max(1, commandCurrentA * acVoltageRef);
|
||||
|
||||
const realCurrentA = Math.abs(
|
||||
finiteOr(
|
||||
parsed.evcsCurrentRealA,
|
||||
acVoltageRef > 180
|
||||
? finiteOr(parsed.evcsAcPowerW, 0) / acVoltageRef
|
||||
: 0
|
||||
)
|
||||
);
|
||||
|
||||
const realPowerW = Math.max(0, finiteOr(parsed.evcsAcPowerW, 0));
|
||||
|
||||
const currentDeliveryRatioRaw = clamp(
|
||||
realCurrentA / Math.max(commandCurrentA, 0.1),
|
||||
0,
|
||||
1.25
|
||||
);
|
||||
|
||||
const powerDeliveryRatioRaw = clamp(
|
||||
realPowerW / expectedPowerW,
|
||||
0,
|
||||
1.25
|
||||
);
|
||||
|
||||
const deliveryRatioRaw = clamp(
|
||||
Math.max(currentDeliveryRatioRaw, powerDeliveryRatioRaw),
|
||||
0,
|
||||
1
|
||||
);
|
||||
|
||||
const deliveryRatioEma = ema(prevDeliveryRatioEma, deliveryRatioRaw, alphaDelivery);
|
||||
|
||||
const deliveryResidualAbs = Math.abs(deliveryRatioRaw - deliveryRatioEma);
|
||||
const deliveryNoiseEma = ema(prevDeliveryNoiseEma, deliveryResidualAbs, alphaNoise);
|
||||
|
||||
const adaptiveQuietBand = clamp(
|
||||
0.02 + 3.0 * deliveryNoiseEma,
|
||||
0.02,
|
||||
0.35
|
||||
);
|
||||
|
||||
const noDemandEvidenceRaw = clamp(
|
||||
(adaptiveQuietBand - deliveryRatioEma) / adaptiveQuietBand,
|
||||
0,
|
||||
1
|
||||
);
|
||||
|
||||
const sessionElapsedSec = Math.max(
|
||||
0,
|
||||
finiteOr(lifecycle.minutesSinceStart, 0) * 60
|
||||
);
|
||||
|
||||
const sessionArmFactor = 1 - Math.exp(-sessionElapsedSec / armTauSec);
|
||||
|
||||
const currentMinRefA = Math.max(1, finiteOr(params.currentMinA, 6));
|
||||
const commandStrength = commandCurrentA / currentMinRefA;
|
||||
|
||||
const commandFactor = clamp(
|
||||
sigmoid((commandStrength - 1.0) * 2.2),
|
||||
0,
|
||||
1
|
||||
);
|
||||
Reference in New Issue
Block a user