Agent IA planificateur EVCS en MODE OMBRE (Advisor Shadow V1)

- Workflow n8n tdfInZns7cqo8fU2 actif (15 min): snapshot HA compact -> prompt FR
  -> LLM local Spark (modèle dynamique via /v1/models) avec fallback OpenRouter
  claude-haiku-4.5 (credential chiffré, clé hors git) -> validation stricte
  (enums, bornes, rejet total si invalide) -> helpers advisor_* + divergence
  + compteurs accord/divergence + logbook.
- GARANTIE OMBRE structurelle: écritures limitées à advisor_*/logbook,
  vérifiée par tests/advisor/run.mjs (16 tests, bloquants au push).
- Kill-switch input_boolean.evcs_advisor_enabled.
- Extension R2 multi-workflows (lib-node-map WORKFLOWS[], build/extract/push
  par fichier, n8n-create.mjs, gen-advisor-workflow.mjs).
- Helpers HA via package NFS + carte dashboard EVCS Master (backup fait).
- 1re reco réelle: auto_solar_plus, cible 35%, accord avec le réel.
- Fix sécurité n8n-push: plus de fallback d'id implicite vers le workflow EVCS.
This commit is contained in:
Tallagrand Thierry
2026-08-07 22:42:06 +02:00
parent 1ae8129658
commit 63d56bf284
13 changed files with 1282 additions and 69 deletions

View File

@@ -0,0 +1,70 @@
// =====================================================
// EVCS ADVISOR SHADOW V1 — BUILD_HA_CALLS
// Écrit les helpers advisor_* + divergence + compteurs + logbook.
// GARANTIE MODE OMBRE : ce node ne produit QUE des écritures vers
// des entités advisor_* et le logbook. AUCUN switch, AUCUNE stratégie
// réelle, AUCUNE commande borne. Ne pas ajouter d'autres entités ici.
// =====================================================
const { status, detail, advice, modelUsed, context } = $input.first().json;
function stampParis() {
const p = new Intl.DateTimeFormat('fr-FR', {
timeZone: 'Europe/Paris', day: '2-digit', month: '2-digit',
hour: '2-digit', minute: '2-digit', hour12: false,
}).formatToParts(new Date());
const g = t => p.find(x => x.type === t)?.value || '';
return `${g('day')}/${g('month')} ${g('hour')}:${g('minute')}`;
}
const calls = [];
function push(domain, service, data) {
calls.push({ json: { domain, service, data } });
}
function setText(entity, value) {
push('input_text', 'set_value', { entity_id: entity, value: (value ?? '').toString().slice(0, 250) });
}
function setNumber(entity, value) {
push('input_number', 'set_value', { entity_id: entity, value });
}
// Statut + horodatage + modèle : écrits à CHAQUE run
setText('input_text.evcs_advisor_status', status + (detail ? `: ${detail}` : ''));
setText('input_text.evcs_advisor_last_run', stampParis());
setText('input_text.evcs_advisor_model', modelUsed);
if (status === 'ok' && advice) {
// Recommandations
setText('input_text.evcs_advisor_strategy', advice.strategy);
setNumber('input_number.evcs_advisor_night_target_soc', advice.nightTargetSoc);
setNumber('input_number.evcs_advisor_current_max_a', advice.currentMaxA);
setText('input_text.evcs_advisor_ev_charge_tonight', advice.evChargeTonight);
setText('input_text.evcs_advisor_reason', `[${Math.round(advice.confidence * 100)}%] ${advice.reason}`);
// Divergence vs réel (stratégie + cible nuit)
const diffs = [];
if (advice.strategy !== context.userStrategy) {
diffs.push(`stratégie: agent=${advice.strategy} ≠ réel=${context.userStrategy}`);
}
if (Math.abs(advice.nightTargetSoc - context.nightTargetActual) > 10) {
diffs.push(`cible nuit: agent=${advice.nightTargetSoc}% vs réel=${context.nightTargetActual}%`);
}
const divergenceText = diffs.length ? diffs.join(' | ') : 'accord avec le réel';
setText('input_text.evcs_advisor_divergence', divergenceText);
// Compteurs accord/divergence sur la stratégie (base des critères de bascule)
if (advice.strategy === context.userStrategy) {
setNumber('input_number.evcs_advisor_agree_count', context.agreeCount + 1);
} else {
setNumber('input_number.evcs_advisor_diverge_count', context.divergeCount + 1);
}
// Trace logbook pour l'historique long terme
push('logbook', 'log', {
name: 'EVCS Advisor (ombre)',
message: `${advice.strategy} | cible nuit ${advice.nightTargetSoc}% | ${advice.evChargeTonight === 'oui' ? 'charge VE cette nuit' : advice.evChargeTonight === 'non' ? 'pas de charge nuit' : 'VE sans objet'} | ${divergenceText} | ${advice.reason}`,
entity_id: 'input_text.evcs_advisor_strategy',
});
}
return calls;