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*
|
*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)
|
Number.isFinite(nightPrev.batteryMorningTargetPct)
|
||||||
? nightPrev.batteryMorningTargetPct
|
? nightPrev.batteryMorningTargetPct
|
||||||
: params.evcsNightTargetWeakForecastPct;
|
: params.evcsNightTargetWeakForecastPct;
|
||||||
|
const advisorNightTargetApplied = !!nightPrev.advisorNightTargetApplied;
|
||||||
const maxBatteryDischargePoints =
|
const maxBatteryDischargePoints =
|
||||||
Number.isFinite(nightPrev.maxBatteryDischargePoints) ? nightPrev.maxBatteryDischargePoints : 0;
|
Number.isFinite(nightPrev.maxBatteryDischargePoints) ? nightPrev.maxBatteryDischargePoints : 0;
|
||||||
const gridTakeoverTriggerPct =
|
const gridTakeoverTriggerPct =
|
||||||
@@ -824,6 +825,7 @@ return [{
|
|||||||
|
|
||||||
batteryHardFloorPct,
|
batteryHardFloorPct,
|
||||||
batteryMorningTargetPct,
|
batteryMorningTargetPct,
|
||||||
|
advisorNightTargetApplied,
|
||||||
maxBatteryDischargePoints,
|
maxBatteryDischargePoints,
|
||||||
gridTakeoverTriggerPct,
|
gridTakeoverTriggerPct,
|
||||||
|
|
||||||
|
|||||||
@@ -1273,7 +1273,7 @@ else {
|
|||||||
// =====================================================
|
// =====================================================
|
||||||
if (nightEvcs?.nightSessionActive) {
|
if (nightEvcs?.nightSessionActive) {
|
||||||
reason.push(
|
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)) {
|
if (Number.isFinite(nightEvcs.nightSessionStartSocPct)) {
|
||||||
|
|||||||
@@ -118,6 +118,10 @@
|
|||||||
"prev_surplus_state": states("input_text.victron_evcs_surplus_state"),
|
"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_soc": states("input_text.victron_evcs_night_session_start_soc"),
|
||||||
"evcs_night_session_start_at": states("input_text.victron_evcs_night_session_start_at"),
|
"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"),
|
"allow_grid_hc_midday": states("input_boolean.victron_evcs_allow_grid_hc_midday"),
|
||||||
"probe_enabled": states("input_boolean.victron_evcs_probe_enabled"),
|
"probe_enabled": states("input_boolean.victron_evcs_probe_enabled"),
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,33 @@ function cleanText(v, fallback = '') {
|
|||||||
return s;
|
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) {
|
function finiteOrNull(v) {
|
||||||
return Number.isFinite(v) ? v : null;
|
return Number.isFinite(v) ? v : null;
|
||||||
}
|
}
|
||||||
@@ -336,6 +363,14 @@ const params = {
|
|||||||
allowGridHcNight: low(input.allow_grid_hc_night || 'off') === 'on',
|
allowGridHcNight: low(input.allow_grid_hc_night || 'off') === 'on',
|
||||||
allowGridHcMidday: low(input.allow_grid_hc_midday || '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',
|
probeEnabled: low(input.probe_enabled || 'off') === 'on',
|
||||||
probeMinutes: Math.max(1, num(input.probe_minutes, 3)),
|
probeMinutes: Math.max(1, num(input.probe_minutes, 3)),
|
||||||
probeRetryMinutes: Math.max(5, num(input.probe_retry_minutes, 45)),
|
probeRetryMinutes: Math.max(5, num(input.probe_retry_minutes, 45)),
|
||||||
|
|||||||
@@ -257,13 +257,40 @@ const nightForecastKwh =
|
|||||||
? Math.max(parsed.todayRemainingKwh, parsed.tomorrowKwh || 0)
|
? Math.max(parsed.todayRemainingKwh, parsed.tomorrowKwh || 0)
|
||||||
: parsed.todayRemainingKwh;
|
: parsed.todayRemainingKwh;
|
||||||
|
|
||||||
const batteryMorningTargetPct =
|
const batteryMorningTargetRulePct =
|
||||||
nightForecastKwh >= params.evcsNightStrongForecastKwh
|
nightForecastKwh >= params.evcsNightStrongForecastKwh
|
||||||
? params.evcsNightTargetStrongForecastPct
|
? params.evcsNightTargetStrongForecastPct
|
||||||
: nightForecastKwh >= params.evcsNightMediumForecastKwh
|
: nightForecastKwh >= params.evcsNightMediumForecastKwh
|
||||||
? params.evcsNightTargetMediumForecastPct
|
? params.evcsNightTargetMediumForecastPct
|
||||||
: params.evcsNightTargetWeakForecastPct;
|
: 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 =
|
const maxBatteryDischargePoints =
|
||||||
nightSessionActive && Number.isFinite(nightSessionStartSocPct)
|
nightSessionActive && Number.isFinite(nightSessionStartSocPct)
|
||||||
? Math.max(0, nightSessionStartSocPct - batteryMorningTargetPct)
|
? Math.max(0, nightSessionStartSocPct - batteryMorningTargetPct)
|
||||||
@@ -706,6 +733,8 @@ return [{
|
|||||||
|
|
||||||
batteryHardFloorPct,
|
batteryHardFloorPct,
|
||||||
batteryMorningTargetPct,
|
batteryMorningTargetPct,
|
||||||
|
batteryMorningTargetRulePct,
|
||||||
|
advisorNightTargetApplied,
|
||||||
maxBatteryDischargePoints,
|
maxBatteryDischargePoints,
|
||||||
gridTakeoverTriggerPct,
|
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) =================
|
// ================= FIN DE CHARGE NUIT (P8) =================
|
||||||
{
|
{
|
||||||
id: 'night-vw-pleine-transition',
|
id: 'night-vw-pleine-transition',
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user