Advisor: premier câblage réel — cible SOC matin (toggle, bornes, fraîcheur 120min)
- nouveau toggle input_boolean.evcs_advisor_apply_night_target (package + carte, OFF par défaut) - snapshot/INPUT_PARSE: advisor_apply/target/status/last_run (+parseAdvisorRunToMs DD/MM HH:mm) - WINDOWS: batteryMorningTargetPct = advisor si ON+ok+frais, clamp [max(22,hardFloor),90], sinon règles forecast; traçabilité '(ADVISOR)' dans la reason DECISION - golden 26→29 (appliqué / stale / toggle off) - HANDOFF §20 (surveillance + rollback 3 niveaux)
This commit is contained in:
64
HANDOFF.md
64
HANDOFF.md
@@ -896,3 +896,67 @@ VRAI signal de fin (VDF no-demand ou status 3) — et tiendra désormais.
|
||||
|
||||
---
|
||||
*Document mis à jour le : 09/08/2026 (~07:05) — Fix F déployé, golden 26/26*
|
||||
|
||||
## 20. Session du 09/08/2026 — PREMIER CÂBLAGE RÉEL de l'advisor : cible SOC matin
|
||||
|
||||
### Décision utilisateur
|
||||
Météo Bretagne au beau fixe (47-58 kWh/j prévus) = conditions idéales pour
|
||||
tester le câblage avec risque minimal. Périmètre STRICTEMENT limité au plan
|
||||
§16 : `night_target_soc` uniquement, derrière un toggle, rollback 1 clic.
|
||||
|
||||
### Ce qui est câblé
|
||||
`batteryMorningTargetPct` (WINDOWS_SOURCEV2) suit
|
||||
`input_number.evcs_advisor_night_target_soc` SI ET SEULEMENT SI :
|
||||
1. `input_boolean.evcs_advisor_apply_night_target` = ON (nouveau toggle,
|
||||
initial OFF, package evcs_advisor_shadow.yaml + carte dashboard) ;
|
||||
2. `input_text.evcs_advisor_status` = 'ok' (exact) ;
|
||||
3. recommandation fraîche ≤ 120 min (parse `DD/MM HH:mm` de advisor_last_run,
|
||||
année courante, garde passage d'année — parseAdvisorRunToMs INPUT_PARSE) ;
|
||||
4. valeur clampée [max(22, hard floor), 90].
|
||||
Sinon → règles forecast historiques (`batteryMorningTargetRulePct`), aucun
|
||||
changement de comportement. Le toggle OFF = état d'origine.
|
||||
|
||||
### Chaîne technique
|
||||
- Snapshot : +advisor_apply_night_target, +advisor_night_target_soc,
|
||||
+advisor_status, +advisor_last_run (states() d'une entité absente →
|
||||
'unknown' → toggle vu OFF → sûr AVANT le reload HA).
|
||||
- INPUT_PARSE : params.advisorApplyNightTarget / advisorNightTargetSocPct /
|
||||
advisorStatusOk / advisorLastRunMs.
|
||||
- WINDOWS : override borné + export advisorNightTargetApplied +
|
||||
batteryMorningTargetRulePct (preview) ; BCL passthrough ; DECISION : la
|
||||
reason affiche `target=X% (ADVISOR)` quand appliqué → visible logbook/carte.
|
||||
- Impact aval automatique : gridTakeoverTriggerPct, grid_forced vs hc_allowed,
|
||||
recovery post-charge, bande anti-rebond — tous dérivés de la cible.
|
||||
|
||||
### Golden 26 → 29
|
||||
- `night-advisor-target-applique` : SOC 30, advisor 22 ON/ok/frais →
|
||||
grid_hc_allowed (au lieu de grid_forced par la règle 35) ;
|
||||
- `night-advisor-target-stale` : > 120 min → fallback règles (grid_forced) ;
|
||||
- `night-advisor-toggle-off` : toggle OFF → règles inchangées.
|
||||
|
||||
### Sécurités NON modifiées
|
||||
Hard floor 22 %, hysteresis grid ON ≤ 20 %, recovery urgent départ, latch
|
||||
anti-créneaux, collant fin de charge (Fix A/E2/F). L'advisor ne peut RIEN
|
||||
piloter d'autre (VALIDATE_PARSE borne night_target_soc 22-90 côté advisor,
|
||||
clamp identique côté contrôleur).
|
||||
|
||||
### Rollback (3 niveaux)
|
||||
1. Toggle « APPLIQUER cible SOC matin » OFF → effet au cycle suivant (30 s) ;
|
||||
2. Kill-switch advisor OFF → plus de runs → fraîcheur > 120 min → fallback
|
||||
auto en ≤ 2 h même si le toggle reste ON ;
|
||||
3. `git revert` du commit + `node tools/n8n-push.mjs workflows/evcs-surplus-controller-v2.6.json`.
|
||||
|
||||
### Surveillance (2-3 nuits)
|
||||
- Carte Conseiller IA : « Cible SOC matin (agent) » vs « (réel) » doivent
|
||||
converger quand le toggle est ON ;
|
||||
- Logbook « Victron EVCS - Reason » la nuit : `target=X% (ADVISOR)` ;
|
||||
- Le matin : SOC batterie au lever du soleil ≈ cible advisor, remontée solaire OK ;
|
||||
- Si SOC matin < 22 % ou EDF nuit anormal → toggle OFF + capture + analyse.
|
||||
|
||||
### Action utilisateur requise après ce déploiement
|
||||
Recharger HA pour créer le toggle : Outils développeur → YAML →
|
||||
« ENTRÉES BOOLÉENNES (input_boolean) » Recharger (ou redémarrage HA), puis
|
||||
activer le toggle sur la carte Conseiller IA (dashboard EVCS Master rechargé).
|
||||
|
||||
---
|
||||
*Document mis à jour le : 09/08/2026 (~07:35) — câblage advisor night_target déployé (toggle OFF par défaut), golden 29/29*
|
||||
|
||||
@@ -360,6 +360,7 @@ const batteryMorningTargetPct =
|
||||
Number.isFinite(nightPrev.batteryMorningTargetPct)
|
||||
? nightPrev.batteryMorningTargetPct
|
||||
: params.evcsNightTargetWeakForecastPct;
|
||||
const advisorNightTargetApplied = !!nightPrev.advisorNightTargetApplied;
|
||||
const maxBatteryDischargePoints =
|
||||
Number.isFinite(nightPrev.maxBatteryDischargePoints) ? nightPrev.maxBatteryDischargePoints : 0;
|
||||
const gridTakeoverTriggerPct =
|
||||
@@ -824,6 +825,7 @@ return [{
|
||||
|
||||
batteryHardFloorPct,
|
||||
batteryMorningTargetPct,
|
||||
advisorNightTargetApplied,
|
||||
maxBatteryDischargePoints,
|
||||
gridTakeoverTriggerPct,
|
||||
|
||||
|
||||
@@ -1273,7 +1273,7 @@ else {
|
||||
// =====================================================
|
||||
if (nightEvcs?.nightSessionActive) {
|
||||
reason.push(
|
||||
`Nuit EVCS: target=${round1(nightEvcs.batteryMorningTargetPct)}% | trigger réseau=${round1(nightEvcs.gridTakeoverTriggerPct)}%`
|
||||
`Nuit EVCS: target=${round1(nightEvcs.batteryMorningTargetPct)}%${nightEvcs.advisorNightTargetApplied ? ' (ADVISOR)' : ''} | trigger réseau=${round1(nightEvcs.gridTakeoverTriggerPct)}%`
|
||||
);
|
||||
|
||||
if (Number.isFinite(nightEvcs.nightSessionStartSocPct)) {
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
"prev_surplus_state": states("input_text.victron_evcs_surplus_state"),
|
||||
"evcs_night_session_start_soc": states("input_text.victron_evcs_night_session_start_soc"),
|
||||
"evcs_night_session_start_at": states("input_text.victron_evcs_night_session_start_at"),
|
||||
"advisor_apply_night_target": states("input_boolean.evcs_advisor_apply_night_target"),
|
||||
"advisor_night_target_soc": states("input_number.evcs_advisor_night_target_soc"),
|
||||
"advisor_status": states("input_text.evcs_advisor_status"),
|
||||
"advisor_last_run": states("input_text.evcs_advisor_last_run"),
|
||||
"allow_grid_hc_midday": states("input_boolean.victron_evcs_allow_grid_hc_midday"),
|
||||
"probe_enabled": states("input_boolean.victron_evcs_probe_enabled"),
|
||||
|
||||
|
||||
@@ -158,6 +158,33 @@ function cleanText(v, fallback = '') {
|
||||
return s;
|
||||
}
|
||||
|
||||
// Advisor IA : input_text.evcs_advisor_last_run au format "DD/MM HH:mm"
|
||||
// (pas d'année). On suppose l'année courante ; si le résultat est dans le
|
||||
// futur (> 1 jour), c'est un passage d'année → on retire un an.
|
||||
function parseAdvisorRunToMs(s) {
|
||||
const m = (s ?? '').toString().trim().match(/^(\d{2})\/(\d{2}) (\d{2}):(\d{2})$/);
|
||||
if (!m) return null;
|
||||
|
||||
const nowMsLocal = Date.now();
|
||||
const year = Number(
|
||||
new Intl.DateTimeFormat('fr-FR', { timeZone: 'Europe/Paris', year: 'numeric' })
|
||||
.format(new Date(nowMsLocal))
|
||||
);
|
||||
|
||||
const build = (y) => {
|
||||
const guessUtc = Date.UTC(y, Number(m[2]) - 1, Number(m[1]), Number(m[3]), Number(m[4]), 0);
|
||||
const off1 = getTzOffsetMs(guessUtc, 'Europe/Paris');
|
||||
let ts = guessUtc - off1;
|
||||
const off2 = getTzOffsetMs(ts, 'Europe/Paris');
|
||||
if (off2 !== off1) ts = guessUtc - off2;
|
||||
return ts;
|
||||
};
|
||||
|
||||
let ts = build(year);
|
||||
if (ts - nowMsLocal > 24 * 3600 * 1000) ts = build(year - 1);
|
||||
return ts;
|
||||
}
|
||||
|
||||
function finiteOrNull(v) {
|
||||
return Number.isFinite(v) ? v : null;
|
||||
}
|
||||
@@ -336,6 +363,14 @@ const params = {
|
||||
allowGridHcNight: low(input.allow_grid_hc_night || 'off') === 'on',
|
||||
allowGridHcMidday: low(input.allow_grid_hc_midday || 'off') === 'on',
|
||||
|
||||
// Advisor IA — premier câblage réel (09/08/2026, HANDOFF §20) :
|
||||
// cible SOC matin recommandée, appliquée UNIQUEMENT si le toggle est ON.
|
||||
// Garde-fous consommés dans WINDOWS_SOURCEV2 (statut ok + fraîcheur + bornes).
|
||||
advisorApplyNightTarget: low(input.advisor_apply_night_target || 'off') === 'on',
|
||||
advisorNightTargetSocPct: num(input.advisor_night_target_soc, NaN),
|
||||
advisorStatusOk: cleanText(input.advisor_status).toLowerCase() === 'ok',
|
||||
advisorLastRunMs: parseAdvisorRunToMs(cleanText(input.advisor_last_run)),
|
||||
|
||||
probeEnabled: low(input.probe_enabled || 'off') === 'on',
|
||||
probeMinutes: Math.max(1, num(input.probe_minutes, 3)),
|
||||
probeRetryMinutes: Math.max(5, num(input.probe_retry_minutes, 45)),
|
||||
|
||||
@@ -257,13 +257,40 @@ const nightForecastKwh =
|
||||
? Math.max(parsed.todayRemainingKwh, parsed.tomorrowKwh || 0)
|
||||
: parsed.todayRemainingKwh;
|
||||
|
||||
const batteryMorningTargetPct =
|
||||
const batteryMorningTargetRulePct =
|
||||
nightForecastKwh >= params.evcsNightStrongForecastKwh
|
||||
? params.evcsNightTargetStrongForecastPct
|
||||
: nightForecastKwh >= params.evcsNightMediumForecastKwh
|
||||
? params.evcsNightTargetMediumForecastPct
|
||||
: params.evcsNightTargetWeakForecastPct;
|
||||
|
||||
// =====================================================
|
||||
// ADVISOR IA — PREMIER CÂBLAGE RÉEL (09/08/2026, HANDOFF §20)
|
||||
// La cible SOC matin suit la recommandation de l'advisor UNIQUEMENT si :
|
||||
// - toggle input_boolean.evcs_advisor_apply_night_target ON (rollback 1 clic)
|
||||
// - dernier run advisor en statut 'ok'
|
||||
// - recommandation fraîche (≤ 120 min, l'advisor tourne toutes les 15 min)
|
||||
// - valeur bornée [max(22, hard floor), 90]
|
||||
// Sinon : règles forecast historiques (comportement inchangé).
|
||||
// =====================================================
|
||||
const ADVISOR_TARGET_MAX_AGE_MIN = 120;
|
||||
const advisorTargetAgeMin = minutesSince(params.advisorLastRunMs, nowMs);
|
||||
const advisorNightTargetApplied =
|
||||
params.advisorApplyNightTarget &&
|
||||
params.advisorStatusOk &&
|
||||
Number.isFinite(params.advisorNightTargetSocPct) &&
|
||||
advisorTargetAgeMin !== null &&
|
||||
advisorTargetAgeMin <= ADVISOR_TARGET_MAX_AGE_MIN;
|
||||
|
||||
const batteryMorningTargetPct =
|
||||
advisorNightTargetApplied
|
||||
? clamp(
|
||||
params.advisorNightTargetSocPct,
|
||||
Math.max(22, batteryHardFloorPct),
|
||||
90
|
||||
)
|
||||
: batteryMorningTargetRulePct;
|
||||
|
||||
const maxBatteryDischargePoints =
|
||||
nightSessionActive && Number.isFinite(nightSessionStartSocPct)
|
||||
? Math.max(0, nightSessionStartSocPct - batteryMorningTargetPct)
|
||||
@@ -706,6 +733,8 @@ return [{
|
||||
|
||||
batteryHardFloorPct,
|
||||
batteryMorningTargetPct,
|
||||
batteryMorningTargetRulePct,
|
||||
advisorNightTargetApplied,
|
||||
maxBatteryDischargePoints,
|
||||
gridTakeoverTriggerPct,
|
||||
|
||||
|
||||
@@ -135,6 +135,79 @@ export const scenarios = [
|
||||
},
|
||||
},
|
||||
|
||||
// ===== ADVISOR IA — premier câblage réel (09/08/2026, HANDOFF §20) =====
|
||||
// Référence : night-soc-sous-cible (SOC 30 < cible règle 35 → grid_forced).
|
||||
// Avec la cible advisor 22 APPLIQUÉE : SOC 30 ≥ 22 → grid_hc_allowed.
|
||||
{
|
||||
id: 'night-advisor-target-applique',
|
||||
label: 'Nuit HC, SOC 30, advisor cible 22 appliquée (toggle ON, ok, frais) → hc_allowed au lieu de grid_forced',
|
||||
time: NIGHT,
|
||||
overrides: {
|
||||
...VEHICLE_NIGHT, soc_percent: '30', allow_grid_hc_night: 'on',
|
||||
advisor_apply_night_target: 'on',
|
||||
advisor_night_target_soc: '22',
|
||||
advisor_status: 'ok',
|
||||
advisor_last_run: '08/08 01:20',
|
||||
},
|
||||
expect: {
|
||||
profile: 'night_hc',
|
||||
sourceMode: 'grid_hc_allowed',
|
||||
state: 'night_wait_grid_takeover',
|
||||
decision: 'hold',
|
||||
desiredCharging: false,
|
||||
currentA: 6,
|
||||
takeoverNeeded: true,
|
||||
edfOnAction: true,
|
||||
takeoverHelperService: 'turn_on',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'night-advisor-target-stale',
|
||||
label: 'Nuit HC, SOC 30, advisor cible 22 mais recommandation > 120 min → fallback règles (grid_forced)',
|
||||
time: NIGHT,
|
||||
overrides: {
|
||||
...VEHICLE_NIGHT, soc_percent: '30', allow_grid_hc_night: 'on',
|
||||
advisor_apply_night_target: 'on',
|
||||
advisor_night_target_soc: '22',
|
||||
advisor_status: 'ok',
|
||||
advisor_last_run: '07/08 20:00',
|
||||
},
|
||||
expect: {
|
||||
profile: 'night_hc',
|
||||
sourceMode: 'grid_forced',
|
||||
state: 'night_wait_grid_takeover',
|
||||
decision: 'hold',
|
||||
desiredCharging: false,
|
||||
currentA: 6,
|
||||
takeoverNeeded: true,
|
||||
edfOnAction: true,
|
||||
takeoverHelperService: 'turn_on',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'night-advisor-toggle-off',
|
||||
label: 'Nuit HC, SOC 30, advisor cible 22 mais toggle APPLIQUER OFF → règles inchangées (grid_forced)',
|
||||
time: NIGHT,
|
||||
overrides: {
|
||||
...VEHICLE_NIGHT, soc_percent: '30', allow_grid_hc_night: 'on',
|
||||
advisor_apply_night_target: 'off',
|
||||
advisor_night_target_soc: '22',
|
||||
advisor_status: 'ok',
|
||||
advisor_last_run: '08/08 01:20',
|
||||
},
|
||||
expect: {
|
||||
profile: 'night_hc',
|
||||
sourceMode: 'grid_forced',
|
||||
state: 'night_wait_grid_takeover',
|
||||
decision: 'hold',
|
||||
desiredCharging: false,
|
||||
currentA: 6,
|
||||
takeoverNeeded: true,
|
||||
edfOnAction: true,
|
||||
takeoverHelperService: 'turn_on',
|
||||
},
|
||||
},
|
||||
|
||||
// ================= FIN DE CHARGE NUIT (P8) =================
|
||||
{
|
||||
id: 'night-vw-pleine-transition',
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user