Etiquettes : la derniere reponse humaine fait foi (correction de permutation), rejeu du 04/10

This commit is contained in:
Tallagrand Thierry
2026-10-05 14:01:43 +02:00
parent 13698c4d82
commit 8f526bdcd6
3 changed files with 110 additions and 10 deletions

View File

@@ -1738,5 +1738,29 @@ charge en ce moment ». Laisse volontairement : jours d'apres-midi VW/ZERO « un
tous les jours pour le matin, ne pas risquer un depart quotidien). Sauvegardes tous les jours pour le matin, ne pas risquer un depart quotidien). Sauvegardes
`evcs_master.yaml.bak-20261004-{onglets,retouches,edf-session}`. `evcs_master.yaml.bak-20261004-{onglets,retouches,edf-session}`.
### 05/10 — Premiere identification JEEP reussie, et la correction humaine qui etait refusee
- **Identification Uconnect en reel (05/10)** : JEEP branchee 13:29:38, la voiture publie 13:31:34
(branchee, 25 %), HA relit 13:44:09, mon service identifie a 13:44:10 (etiquette uconnect,
niveau 25 %), le concierge envoie la ligne d'info a 13:50:01 sans question (SPARK, journal du
cron). Delai ~20 min (cloud 15 min + cron */10). ⚠️ Le 04/10 a 20:56 elle n'avait PAS joue :
la Jeep ne publie que si sa charge DEMARRE ; le soir en « sans EDF » la question du concierge
reste normale (elle a fonctionne, reponse a 21:35). Les releves km se font aussi pendant les
trajets (112 740 / 743 / 749 / 763 km le 05/10).
- **Defaut trouve sur la capture du proprietaire** : 04/10 16:55, JEEP notee a 16:59 ; 17:16 « Non
maintenant c'est la zero » (permutation rapide, aucune transition vue par la borne) ; le concierge
repond « ✅ Noté : ZERO » mais mon service REFUSE (« le branchement porte deja l'etiquette JEEP,
incompatible avec ZERO ») -> base restee JEEP, confirmation FAUSSE. J'avais affirme a SPARK que la
correction humaine primait toujours : faux dans ce cas, rectifie aupres de lui.
- **Correctif (a), accord du proprietaire** (`appliquerEtiquetteConcierge`) : contradiction + source
HUMAINE -> la derniere reponse fait foi : sessions terminees avant la correction inchangees,
session en cours et suivantes au nouveau label ; `etiquette_branchement.applique_depuis` (nouvelle
colonne) borne aussi `appliquerEtiquettesEnAttente` ; evenement « Etiquette corrigee par le
proprietaire : X -> Y a partir de HH:MM ». Contradiction non humaine (regle de puissance) toujours
refusee. Rejeu du cas reel en transaction annulee `tests/kilometrage/rejeu-correction.mjs` 6/6
(2/6 sur l'ancien code). Le « Noté » du concierge redevient vrai sans changement chez SPARK.
- **Session du 04/10 16:56-17:39 (2,4 kWh) coupee au prorata du temps a 17:16** (proprietaire) :
JEEP 1,07 kWh / ZERO 1,33 kWh, trace dans `brut` et evenement ; etiquette du branchement ZERO
depuis 17:16.
--- ---
*Document mis a jour le : 04/10/2026 (~16:30) — EVCS Master en quatre onglets ; Jeep lue par Uconnect et identifiee seule, capacite JEEP 10 kWh, concierge variante (a) ; releve par photo (SPARK), hass-uconnect en attente ; ecart semaine/week-end mesure (pas de profil week-end la nuit) ; nuit du budget : budget borne au plancher prudent, sans EDF = voiture bloquee meme EDF ferme (batterie seule) ; Energie : sauts corriges ; tableau Energie complete (Sud/Ouest, batterie par integration, borne ; aucun redemarrage HA) ; kilometrage des vehicules (km/kWh, capacite, releves de reference, agent cable 15/15) ; a verifier le 04/10 au matin : nuit du budget JEEP ; concierge : vehicule non confirme demande, heure de saisie ; Grafana vehicules en tableau ; Grafana vehicules : ZERO et Invite ajoutes, session 16:46 reetiquetee ZERO ; prevision SOC 10 h ~35 % ; concierge fiable : niveau note par le script, une ligne, dedoublonnage, simulation par drapeau ; agent : message incompris = question, bourse sur demande explicite (banc final 6/6) ; a verifier : nuit du samedi 03 au dimanche 04/10 (budget < besoin JEEP), cron orange-cloture lundi 05/10 apres 17:40, comparaison VRM / Forecast.Solar lundi 05/10* *Document mis a jour le : 05/10/2026 (~14:30) — identification JEEP reussie en reel, correction humaine qui prime (refus supprime), session du 04/10 coupee ; EVCS Master en quatre onglets ; Jeep lue par Uconnect et identifiee seule, capacite JEEP 10 kWh, concierge variante (a) ; releve par photo (SPARK), hass-uconnect en attente ; ecart semaine/week-end mesure (pas de profil week-end la nuit) ; nuit du budget : budget borne au plancher prudent, sans EDF = voiture bloquee meme EDF ferme (batterie seule) ; Energie : sauts corriges ; tableau Energie complete (Sud/Ouest, batterie par integration, borne ; aucun redemarrage HA) ; kilometrage des vehicules (km/kWh, capacite, releves de reference, agent cable 15/15) ; a verifier le 04/10 au matin : nuit du budget JEEP ; concierge : vehicule non confirme demande, heure de saisie ; Grafana vehicules en tableau ; Grafana vehicules : ZERO et Invite ajoutes, session 16:46 reetiquetee ZERO ; prevision SOC 10 h ~35 % ; concierge fiable : niveau note par le script, une ligne, dedoublonnage, simulation par drapeau ; agent : message incompris = question, bourse sur demande explicite (banc final 6/6) ; a verifier : nuit du samedi 03 au dimanche 04/10 (budget < besoin JEEP), cron orange-cloture lundi 05/10 apres 17:40, comparaison VRM / Forecast.Solar lundi 05/10*

View File

@@ -0,0 +1,47 @@
// Rejoue la correction humaine du 04/10 (JEEP -> ZERO a 17:16) sur la vraie
// base, dans une transaction ANNULEE. A lancer dans le conteneur d'historisation.
import fs from 'node:fs';
import pg from 'pg';
const src = fs.readFileSync(new URL('../../tools/historise-evcs.mjs', import.meta.url), 'utf8');
const debut = src.indexOf('async function appliquerEtiquetteConcierge(sql, client, st) {');
let i = src.indexOf('{', debut), prof = 0, fin = i;
for (; fin < src.length; fin++) { if (src[fin] === '{') prof++; else if (src[fin] === '}' && --prof === 0) break; }
const corps = src.slice(debut, fin + 1);
const c = new pg.Client(); await c.connect();
let ok = 0, ko = 0; const eq = (n, a, b) => { if (JSON.stringify(a) === JSON.stringify(b)) ok++; else { ko++; console.log('ECHEC', n, JSON.stringify(a), '!=', JSON.stringify(b)); } };
try {
await c.query('BEGIN');
await c.query('ALTER TABLE etiquette_branchement ADD COLUMN IF NOT EXISTS applique_depuis timestamptz');
const journal = [];
const deps = {
txt: (x) => (x ?? '').toString().trim(), LABELS: new Set(['VW', 'JEEP', 'MEGANE', 'INVITE', 'ZERO']),
HOUSEHOLD: new Set(['VW', 'JEEP']), CANAUX_HUMAINS: new Set(['whatsapp', 'proprietaire']), DRY: false,
journaliser: async (sql, g, r, d) => { journal.push(r); await sql(`INSERT INTO evenement (genre, source, resume, detail) VALUES ($1,'test',$2,$3)`, [g, r, JSON.stringify(d || {})]); },
piloterSelecteurVehicule: async () => 0,
};
const fn = new Function(...Object.keys(deps), 'let dernierBlancVu = null; return (' + corps.replace('async function appliquerEtiquetteConcierge', 'async function') + ');')(...Object.values(deps));
const sql = (q, p) => c.query(q, p);
const st = (label, conf) => new Map([
['input_text.airtria_vehicle_label', { state: JSON.stringify({ branchement: '2026-10-04 16:55:45.120519+02', label, confirmed_at: conf, source: 'whatsapp' }) }],
['input_boolean.airtria_concierge_enabled', { state: 'on' }]]);
// Une session terminee AVANT la correction, pour verifier qu'elle garde JEEP.
await c.query(`INSERT INTO session_charge (debut, fin, terminee, vehicule, vehicule_sur, energie_kwh, energie_station_kwh)
VALUES ('2026-10-04T14:56:00Z','2026-10-04T14:56:30Z',true,'JEEP',true,0.05,0.1)`);
await c.query(`INSERT INTO evenement (genre, source, resume, detail) VALUES ('vehicule','test','avant',$1)`,
[JSON.stringify({ origine: 'airtria', label: 'JEEP', sessions: ['2026-10-04T14:56:00.000Z', '2026-10-04T14:56:55.447Z'] })]);
await fn(sql, c, st('ZERO', '2026-10-04T15:16:50.000Z'));
const s = (await c.query(`SELECT debut, vehicule, vehicule_sur FROM session_charge WHERE debut BETWEEN '2026-10-04T14:50Z' AND '2026-10-04T16:00Z' ORDER BY debut`)).rows;
eq('session terminee avant la correction garde JEEP', s[0].vehicule, 'JEEP');
eq('session en cours a 17:16 passe ZERO, confirmee', [s[1].vehicule, s[1].vehicule_sur], ['ZERO', true]);
const e = (await c.query(`SELECT label, applique_depuis FROM etiquette_branchement WHERE branchement = '2026-10-04T14:55:45.120Z'`)).rows[0];
eq('etiquette du branchement = ZERO depuis 17:16:50', [e.label, e.applique_depuis?.toISOString()], ['ZERO', '2026-10-04T15:16:50.000Z']);
eq('journal de correction', journal.some(r => r.startsWith('Etiquette corrigee par le proprietaire : JEEP -> ZERO')), true);
eq('pas de refus', journal.some(r => r.includes('REFUSEE')), false);
// Une contradiction NON humaine reste refusee.
journal.length = 0;
const st2 = new Map([['input_text.airtria_vehicle_label', { state: JSON.stringify({ branchement: '2026-10-04 16:55:45.120519+02', label: 'MEGANE', confirmed_at: '2026-10-04T15:30:00.000Z', source: 'regle-puissance' }) }],
['input_boolean.airtria_concierge_enabled', { state: 'on' }]]);
await fn(sql, c, st2);
eq('contradiction par la regle de puissance : refusee', journal.some(r => r.includes('REFUSEE')), true);
} finally { await c.query('ROLLBACK'); await c.end(); }
console.log(`${ok}/${ok + ko}`); process.exit(ko ? 1 : 0);

View File

@@ -599,6 +599,9 @@ async function migrer(client) {
pose_a timestamptz NOT NULL DEFAULT now(), pose_a timestamptz NOT NULL DEFAULT now(),
sessions_vues integer NOT NULL DEFAULT 0 sessions_vues integer NOT NULL DEFAULT 0
)`); )`);
// Correction humaine (05/10) : l'etiquette ne vaut que pour les sessions non
// terminees a cet instant (NULL = tout le branchement).
await client.query('ALTER TABLE etiquette_branchement ADD COLUMN IF NOT EXISTS applique_depuis timestamptz');
await client.query( await client.query(
'CREATE INDEX IF NOT EXISTS session_charge_debut_idx ON session_charge (debut DESC)'); 'CREATE INDEX IF NOT EXISTS session_charge_debut_idx ON session_charge (debut DESC)');
@@ -1323,10 +1326,34 @@ async function appliquerEtiquetteConcierge(sql, client, st) {
WHERE x.value = ANY($1::text[]))`, WHERE x.value = ANY($1::text[]))`,
[[...cibles]]); [[...cibles]]);
const contradiction = deja.map((r) => r.label).filter((l) => l && l !== label); const contradiction = deja.map((r) => r.label).filter((l) => l && l !== label);
// CORRECTION HUMAINE (05/10/2026, decision du proprietaire) : sa DERNIERE
// reponse fait foi. Cas reel du 04/10 : JEEP notee a 16:59, puis « Non
// maintenant c'est la zero » a 17:16 — permutation rapide que la borne n'a
// pas vue. L'ancien refus laissait JEEP en base pendant que le concierge
// repondait « Noté : ZERO ». Desormais : les sessions TERMINEES avant la
// correction gardent leur etiquette (c'etait le bon vehicule a ce moment-la),
// la session en cours et les suivantes prennent la nouvelle. Une
// contradiction NON humaine (regle de puissance) reste refusee.
let correctionDepuis = null;
if (contradiction.length) { if (contradiction.length) {
return refuser( if (!CANAUX_HUMAINS.has(source)) {
`le branchement porte deja l etiquette ${contradiction.join('/')}, incompatible avec ${label}`, return refuser(
{ sessions: [...cibles], deja: contradiction }); `le branchement porte deja l etiquette ${contradiction.join('/')}, incompatible avec ${label}`,
{ sessions: [...cibles], deja: contradiction });
}
correctionDepuis = Number.isFinite(Date.parse(confirmedAt)) ? new Date(confirmedAt) : new Date();
const { rows: encore } = await lire(
`SELECT debut FROM session_charge WHERE debut = ANY($1::timestamptz[])
AND (fin IS NULL OR fin > $2 OR NOT terminee)`, [[...cibles], correctionDepuis]);
const gardees = cibles.size - encore.length;
cibles.clear();
for (const r of encore) cibles.add(enIso(r.debut));
await journaliser(sql, 'vehicule',
`Etiquette corrigee par le proprietaire : ${contradiction.join('/')} -> ${label} a partir de `
+ `${correctionDepuis.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Paris' })}`
+ (gardees ? ` (${gardees} session${gardees > 1 ? 's' : ''} terminee${gardees > 1 ? 's' : ''} avant : inchangee${gardees > 1 ? 's' : ''})` : ''),
{ origine: 'correction', source_airtria: source, label, avant: contradiction,
confirmed_at: confirmedAt, depuis: correctionDepuis.toISOString(), gardees });
} }
const sur = CANAUX_HUMAINS.has(source); const sur = CANAUX_HUMAINS.has(source);
@@ -1361,12 +1388,13 @@ async function appliquerEtiquetteConcierge(sql, client, st) {
const branchement = bornes[0]?.t || null; const branchement = bornes[0]?.t || null;
if (branchement) { if (branchement) {
await sql( await sql(
`INSERT INTO etiquette_branchement (branchement, label, source_airtria, confirmed_at) `INSERT INTO etiquette_branchement (branchement, label, source_airtria, confirmed_at, applique_depuis)
VALUES ($1,$2,$3,$4) VALUES ($1,$2,$3,$4,$5)
ON CONFLICT (branchement) DO UPDATE ON CONFLICT (branchement) DO UPDATE
SET label = EXCLUDED.label, source_airtria = EXCLUDED.source_airtria, SET label = EXCLUDED.label, source_airtria = EXCLUDED.source_airtria,
confirmed_at = EXCLUDED.confirmed_at, pose_a = now()`, confirmed_at = EXCLUDED.confirmed_at, pose_a = now(),
[branchement, label, source, confirmedAt || null]); applique_depuis = EXCLUDED.applique_depuis`,
[branchement, label, source, confirmedAt || null, correctionDepuis]);
} }
// Le controleur lit le selecteur : la reponse doit y arriver, pas seulement // Le controleur lit le selecteur : la reponse doit y arriver, pas seulement
@@ -1439,7 +1467,7 @@ async function etiqueterJeepUconnect(sql, client, st, branche, niveau) {
async function appliquerEtiquettesEnAttente(sql, client) { async function appliquerEtiquettesEnAttente(sql, client) {
if (DRY) return 0; if (DRY) return 0;
const { rows } = await client.query( const { rows } = await client.query(
`SELECT branchement, label, source_airtria FROM etiquette_branchement `SELECT branchement, label, source_airtria, applique_depuis FROM etiquette_branchement
WHERE branchement > now() - interval '48 hours' ORDER BY branchement`); WHERE branchement > now() - interval '48 hours' ORDER BY branchement`);
let n = 0; let n = 0;
for (const e of rows) { for (const e of rows) {
@@ -1452,8 +1480,9 @@ async function appliquerEtiquettesEnAttente(sql, client) {
WHERE genre = 'vehicule' WHERE genre = 'vehicule'
AND detail->>'entite' = 'binary_sensor.victron_evcs_vehicle_connected' AND detail->>'entite' = 'binary_sensor.victron_evcs_vehicle_connected'
AND detail->>'apres' = 'off' AND ts > $1::timestamptz), 'infinity'::timestamptz) AND detail->>'apres' = 'off' AND ts > $1::timestamptz), 'infinity'::timestamptz)
AND ($4::timestamptz IS NULL OR s.fin IS NULL OR s.fin > $4::timestamptz OR NOT s.terminee)
AND (s.vehicule IS DISTINCT FROM $2 OR s.vehicule_sur IS DISTINCT FROM $3)`, AND (s.vehicule IS DISTINCT FROM $2 OR s.vehicule_sur IS DISTINCT FROM $3)`,
[e.branchement, e.label, sur]); [e.branchement, e.label, sur, e.applique_depuis]);
if (rowCount) { if (rowCount) {
await journaliser(sql, 'vehicule', await journaliser(sql, 'vehicule',
`Etiquette ${e.label} posee sur ${rowCount} session(s) ouverte(s) depuis le branchement`, `Etiquette ${e.label} posee sur ${rowCount} session(s) ouverte(s) depuis le branchement`,