Etiquettes : la derniere reponse humaine fait foi (correction de permutation), rejeu du 04/10
This commit is contained in:
26
HANDOFF.md
26
HANDOFF.md
@@ -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
|
||||
`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*
|
||||
|
||||
47
tests/kilometrage/rejeu-correction.mjs
Normal file
47
tests/kilometrage/rejeu-correction.mjs
Normal 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);
|
||||
@@ -599,6 +599,9 @@ async function migrer(client) {
|
||||
pose_a timestamptz NOT NULL DEFAULT now(),
|
||||
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(
|
||||
'CREATE INDEX IF NOT EXISTS session_charge_debut_idx ON session_charge (debut DESC)');
|
||||
|
||||
@@ -1323,11 +1326,35 @@ async function appliquerEtiquetteConcierge(sql, client, st) {
|
||||
WHERE x.value = ANY($1::text[]))`,
|
||||
[[...cibles]]);
|
||||
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 (!CANAUX_HUMAINS.has(source)) {
|
||||
return refuser(
|
||||
`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 detail = {
|
||||
@@ -1361,12 +1388,13 @@ async function appliquerEtiquetteConcierge(sql, client, st) {
|
||||
const branchement = bornes[0]?.t || null;
|
||||
if (branchement) {
|
||||
await sql(
|
||||
`INSERT INTO etiquette_branchement (branchement, label, source_airtria, confirmed_at)
|
||||
VALUES ($1,$2,$3,$4)
|
||||
`INSERT INTO etiquette_branchement (branchement, label, source_airtria, confirmed_at, applique_depuis)
|
||||
VALUES ($1,$2,$3,$4,$5)
|
||||
ON CONFLICT (branchement) DO UPDATE
|
||||
SET label = EXCLUDED.label, source_airtria = EXCLUDED.source_airtria,
|
||||
confirmed_at = EXCLUDED.confirmed_at, pose_a = now()`,
|
||||
[branchement, label, source, confirmedAt || null]);
|
||||
confirmed_at = EXCLUDED.confirmed_at, pose_a = now(),
|
||||
applique_depuis = EXCLUDED.applique_depuis`,
|
||||
[branchement, label, source, confirmedAt || null, correctionDepuis]);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (DRY) return 0;
|
||||
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`);
|
||||
let n = 0;
|
||||
for (const e of rows) {
|
||||
@@ -1452,8 +1480,9 @@ async function appliquerEtiquettesEnAttente(sql, client) {
|
||||
WHERE genre = 'vehicule'
|
||||
AND detail->>'entite' = 'binary_sensor.victron_evcs_vehicle_connected'
|
||||
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)`,
|
||||
[e.branchement, e.label, sur]);
|
||||
[e.branchement, e.label, sur, e.applique_depuis]);
|
||||
if (rowCount) {
|
||||
await journaliser(sql, 'vehicule',
|
||||
`Etiquette ${e.label} posee sur ${rowCount} session(s) ouverte(s) depuis le branchement`,
|
||||
|
||||
Reference in New Issue
Block a user