Capteur de charge : origine explicite par libelle (incident 25/09, batterie publiee en solaire)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uJ47NxgfpGnuuMr3f2n7Z
This commit is contained in:
18
HANDOFF.md
18
HANDOFF.md
@@ -602,5 +602,21 @@ deja ailleurs (controleur EVCS), l'agent la RELAIE, il ne la recalcule jamais.
|
|||||||
Limite connue : si le service d'historisation meurt, les quatre capteurs se
|
Limite connue : si le service d'historisation meurt, les quatre capteurs se
|
||||||
figent ; l'agent verifie `sensor.evcs_historisation_heartbeat`.
|
figent ; l'agent verifie `sensor.evcs_historisation_heartbeat`.
|
||||||
|
|
||||||
|
### Incident du matin 25/09 — le capteur de charge disait « solaire » la nuit
|
||||||
|
Le proprio : « l'agent a delire ce matin sur la charge voiture ». Cause = MON
|
||||||
|
capteur, pas l'agent. `chargeSolaire` classait toute source qui ne commence pas
|
||||||
|
par « Reseau » comme solaire : de **01:25 a 06:00**, `sensor.evcs_charge_solaire`
|
||||||
|
a publie `en_charge_solaire` alors que la JEEP chargeait sur la BATTERIE
|
||||||
|
(« Assistance batterie nuit », puis reseau force de 06:02 a 06:42 ; session
|
||||||
|
7,9 kWh dont 2,2 reseau). L'agent a relaye fidelement une fausse verite.
|
||||||
|
Correctif : correspondance EXPLICITE libelle par libelle de `sourceModeLabel`
|
||||||
|
(HA_SERVICE_CALLS_OUTPUT.js), nouveaux etats `en_charge_batterie`,
|
||||||
|
`en_charge_sonde`, et `en_charge` sans origine pour un libelle inconnu. Test
|
||||||
|
garde-fou qui lit la table DANS le code du controleur : un libelle ajoute la-bas
|
||||||
|
fait tomber les tests (mutation verifiee). Tests 32/32.
|
||||||
|
⚠️ Lecon : un capteur-verdict deplace la fragilite de la fiche vers le code ; le
|
||||||
|
code doit donc etre teste sur TOUT le domaine d'entree (ici les sept libelles),
|
||||||
|
pas seulement sur les cas du banc.
|
||||||
|
|
||||||
---
|
---
|
||||||
*Document mis a jour le : 25/09/2026 — agent vocal : la partie energie relaie des capteurs-verdict (banc 15/15), cadrage multi-agents*
|
*Document mis a jour le : 25/09/2026 — capteur de charge corrige (origine explicite par libelle), incident du matin*
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// Tests des diagnostics publies pour l'agent vocal (tools/diagnostics-agent.mjs).
|
// Tests des diagnostics publies pour l'agent vocal (tools/diagnostics-agent.mjs).
|
||||||
// Les cas PV reprennent les gammes du banc OpenClaw (HANDOFF §77) : G3, G4, G5, G7.
|
// Les cas PV reprennent les gammes du banc OpenClaw (HANDOFF §77) : G3, G4, G5, G7.
|
||||||
// node tests/diagnostics-agent/run.mjs
|
// node tests/diagnostics-agent/run.mjs
|
||||||
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, MPPT_MODES } from '../../tools/diagnostics-agent.mjs';
|
import fs from 'node:fs';
|
||||||
|
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, MPPT_MODES, ORIGINE_PAR_SOURCE } from '../../tools/diagnostics-agent.mjs';
|
||||||
|
|
||||||
const LIM = 'voltage_current_limited', ACT = 'mppt_active';
|
const LIM = 'voltage_current_limited', ACT = 'mppt_active';
|
||||||
function st({ pv, conso, reseau, soc = 80, courant = 0, edf = 'off', modes }) {
|
function st({ pv, conso, reseau, soc = 80, courant = 0, edf = 'off', modes }) {
|
||||||
@@ -114,6 +115,14 @@ const charge = [
|
|||||||
(r) => r.etat === 'en_attente' && r.attributs.raison_controleur === 'Excedent insuffisant' && r.attributs.excedent_w === -1854],
|
(r) => r.etat === 'en_attente' && r.attributs.raison_controleur === 'Excedent insuffisant' && r.attributs.excedent_w === -1854],
|
||||||
['en charge sur source solaire -> en_charge_solaire', borne({ p: 3600, source: 'Solaire' }), (r) => r.etat === 'en_charge_solaire'],
|
['en charge sur source solaire -> en_charge_solaire', borne({ p: 3600, source: 'Solaire' }), (r) => r.etat === 'en_charge_solaire'],
|
||||||
['en charge source reseau -> en_charge_reseau', borne({ p: 3600, source: 'Réseau HC autorisé' }), (r) => r.etat === 'en_charge_reseau'],
|
['en charge source reseau -> en_charge_reseau', borne({ p: 3600, source: 'Réseau HC autorisé' }), (r) => r.etat === 'en_charge_reseau'],
|
||||||
|
// Incident du 25/09 01:25-06:00 : JEEP sur batterie la nuit, publie « solaire ».
|
||||||
|
['INCIDENT 25/09 : assistance batterie nuit -> en_charge_batterie, JAMAIS solaire',
|
||||||
|
borne({ p: 1198, source: 'Assistance batterie nuit', pv: 0, conso: 900, modes: 'off' }),
|
||||||
|
(r) => r.etat === 'en_charge_batterie' && !/solaire/i.test(r.attributs.explication.split('(')[0])],
|
||||||
|
['sonde hybride -> en_charge_sonde', borne({ p: 2300, source: 'Probe hybride' }), (r) => r.etat === 'en_charge_sonde'],
|
||||||
|
['potentiel solaire -> en_charge_solaire', borne({ p: 2300, source: 'Potentiel solaire' }), (r) => r.etat === 'en_charge_solaire'],
|
||||||
|
['reseau force -> en_charge_reseau', borne({ p: 3600, source: 'Réseau forcé' }), (r) => r.etat === 'en_charge_reseau'],
|
||||||
|
['libelle inconnu -> en_charge sans origine devinee', borne({ p: 3600, source: 'nouveau_mode' }), (r) => r.etat === 'en_charge'],
|
||||||
['non branchee -> non_branchee', borne({ branche: 'off' }), (r) => r.etat === 'non_branchee'],
|
['non branchee -> non_branchee', borne({ branche: 'off' }), (r) => r.etat === 'non_branchee'],
|
||||||
['G7 excedent positif et bride, mais controleur bloque -> en_attente (on RELAIE, on ne rejuge pas)',
|
['G7 excedent positif et bride, mais controleur bloque -> en_attente (on RELAIE, on ne rejuge pas)',
|
||||||
borne({ pv: 5000, conso: 1500, modes: LIM, source: 'Bloqué', raison: 'Vehicule plein' }),
|
borne({ pv: 5000, conso: 1500, modes: LIM, source: 'Bloqué', raison: 'Vehicule plein' }),
|
||||||
@@ -127,6 +136,16 @@ for (const [nom, etats, test] of charge) {
|
|||||||
console.log(`${ok ? 'OK ' : 'KO '} ${nom}`);
|
console.log(`${ok ? 'OK ' : 'KO '} ${nom}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const n = cas.length + cel.length + prod.length + charge.length;
|
// Chaque libelle publie par le controleur doit avoir une origine explicite :
|
||||||
|
// lu dans SON code, pour qu'un libelle ajoute la-bas fasse tomber ce test.
|
||||||
|
const srcCtrl = fs.readFileSync(new URL('../../src/nodes/HA_SERVICE_CALLS_OUTPUT.js', import.meta.url), 'utf8');
|
||||||
|
const bloc = srcCtrl.slice(srcCtrl.indexOf('const sourceModeLabel ='), srcCtrl.indexOf('context.sourceMode;', srcCtrl.indexOf('const sourceModeLabel =')));
|
||||||
|
const libelles = [...bloc.matchAll(/\? '([^']+)'/g)].map((m) => m[1]).filter((l) => l !== 'Bloqué');
|
||||||
|
const nonCouverts = libelles.filter((l) => !ORIGINE_PAR_SOURCE[l]);
|
||||||
|
const okLib = libelles.length >= 6 && nonCouverts.length === 0;
|
||||||
|
if (!okLib) ko++;
|
||||||
|
console.log(`${okLib ? 'OK ' : 'KO '} les ${libelles.length} libelles de source du controleur ont une origine${okLib ? '' : ` (manquent : ${nonCouverts.join(', ') || 'table introuvable'})`}`);
|
||||||
|
|
||||||
|
const n = cas.length + cel.length + prod.length + charge.length + 1;
|
||||||
console.log(`\n${n - ko}/${n}`);
|
console.log(`\n${n - ko}/${n}`);
|
||||||
process.exit(ko ? 1 : 0);
|
process.exit(ko ? 1 : 0);
|
||||||
|
|||||||
@@ -208,6 +208,29 @@ export function productionJour(st, maintenant = new Date()) {
|
|||||||
// l'expliquer (excedent, chargeurs brides), sans decider.
|
// l'expliquer (excedent, chargeurs brides), sans decider.
|
||||||
export const SEUIL_CHARGE_W = 300; // meme seuil que les sessions de charge
|
export const SEUIL_CHARGE_W = 300; // meme seuil que les sessions de charge
|
||||||
|
|
||||||
|
// ⚠️ Correspondance EXPLICITE, libelle par libelle, de la table
|
||||||
|
// `sourceModeLabel` de src/nodes/HA_SERVICE_CALLS_OUTPUT.js. Defaut du
|
||||||
|
// 25/09 : la premiere version classait « tout ce qui n'est pas Reseau » comme
|
||||||
|
// solaire, et a publie `en_charge_solaire` de 01:25 a 06:00 pendant que la JEEP
|
||||||
|
// chargeait sur la BATTERIE en pleine nuit (« Assistance batterie nuit »). L'agent
|
||||||
|
// l'a repete fidelement. Un libelle inconnu donne `en_charge` sans origine :
|
||||||
|
// on ne devine jamais d'ou vient l'energie.
|
||||||
|
export const ORIGINE_PAR_SOURCE = {
|
||||||
|
'Solaire': 'en_charge_solaire',
|
||||||
|
'Potentiel solaire': 'en_charge_solaire',
|
||||||
|
'Probe hybride': 'en_charge_sonde',
|
||||||
|
'Assistance batterie nuit': 'en_charge_batterie',
|
||||||
|
'Réseau HC autorisé': 'en_charge_reseau',
|
||||||
|
'Réseau forcé': 'en_charge_reseau',
|
||||||
|
};
|
||||||
|
const PHRASE_ORIGINE = {
|
||||||
|
en_charge_solaire: 'sur le solaire',
|
||||||
|
en_charge_sonde: 'en essai court pour provoquer la production solaire',
|
||||||
|
en_charge_batterie: 'sur la batterie de la maison (assistance de nuit)',
|
||||||
|
en_charge_reseau: 'sur le reseau EDF',
|
||||||
|
en_charge: 'depuis une source que le controleur ne precise pas',
|
||||||
|
};
|
||||||
|
|
||||||
export function chargeSolaire(st) {
|
export function chargeSolaire(st) {
|
||||||
const txt = (id) => { const s = st.get(id)?.state; return s === undefined || s === 'unavailable' || s === 'unknown' ? null : String(s); };
|
const txt = (id) => { const s = st.get(id)?.state; return s === undefined || s === 'unavailable' || s === 'unknown' ? null : String(s); };
|
||||||
const branche = txt('binary_sensor.victron_evcs_vehicle_connected');
|
const branche = txt('binary_sensor.victron_evcs_vehicle_connected');
|
||||||
@@ -235,9 +258,10 @@ export function chargeSolaire(st) {
|
|||||||
return r('non_branchee', 'Aucun vehicule branche. Une fois branche, le controleur decidera lui-meme.');
|
return r('non_branchee', 'Aucun vehicule branche. Une fois branche, le controleur decidera lui-meme.');
|
||||||
}
|
}
|
||||||
if (puissance > SEUIL_CHARGE_W) {
|
if (puissance > SEUIL_CHARGE_W) {
|
||||||
const reseau = /^R[ée]seau/.test(source);
|
const etat = ORIGINE_PAR_SOURCE[source] || 'en_charge';
|
||||||
return r(reseau ? 'en_charge_reseau' : 'en_charge_solaire',
|
return r(etat,
|
||||||
`La voiture charge (${Math.round(puissance)} W), source decidee par le controleur : ${source}.`);
|
`La voiture charge (${Math.round(puissance)} W) ${PHRASE_ORIGINE[etat]} ` +
|
||||||
|
`(source decidee par le controleur : ${source}).`);
|
||||||
}
|
}
|
||||||
return r('en_attente',
|
return r('en_attente',
|
||||||
`Voiture branchee mais pas en charge. Le controleur dit : ${raison || 'sans raison publiee'} (source : ${source}).`);
|
`Voiture branchee mais pas en charge. Le controleur dit : ${raison || 'sans raison publiee'} (source : ${source}).`);
|
||||||
|
|||||||
Reference in New Issue
Block a user