diff --git a/tests/kilometrage/rejeu-correction.mjs b/tests/kilometrage/rejeu-correction.mjs index 7fd9e8d..94ef0ff 100644 --- a/tests/kilometrage/rejeu-correction.mjs +++ b/tests/kilometrage/rejeu-correction.mjs @@ -29,12 +29,12 @@ try { 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')); + await fn(sql, c, st('ZERO', '2026-10-04T15:10:00.000Z')); // avant la coupe reelle de 17:16 (session JEEP 16:56-17:16) 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]); + eq('session en cours a 17:10 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('etiquette du branchement = ZERO depuis 17:10', [e.label, e.applique_depuis?.toISOString()], ['ZERO', '2026-10-04T15:10:00.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. diff --git a/tests/kilometrage/run.mjs b/tests/kilometrage/run.mjs index ec6b87d..3e2cfb6 100644 --- a/tests/kilometrage/run.mjs +++ b/tests/kilometrage/run.mjs @@ -40,14 +40,19 @@ eq('helper corrompu repart a vide', JSON.parse(ajouterReleve('pas du json', { ve return { rowCount: 1 }; }; const lire = async (q, v) => { + if (q.includes('FROM releve_km') && q.includes("source = 'uconnect'")) { + const r = base.releves.filter(x => x.ts >= v[0] && x.ts <= v[1] && x.niveau !== null).sort((a, b) => b.ts - a.ts); + return { rows: r.length ? [{ ts: r[0].ts, niveau_pct: r[0].niveau }] : [] }; + } if (q.includes('FROM releve_km')) { const r = [...base.releves].sort((a, b) => b.ts - a.ts); return { rows: r.length ? [{ km: r[0].km, ts: r[0].ts }] : [] }; } + if (q.includes('FROM etiquette_branchement')) return { rows: base.etiquette ? [{ label: base.etiquette }] : [] }; if (q.includes('FROM evenement')) return { rows: [{ t: base.branche }] }; if (q.includes('FROM niveau_saisi')) return { rows: base.niveaux.filter(n => n.saisi_a >= v[0]) }; return { rows: [] }; }; const journ = async () => {}; const appels = []; - const etiq = async (b, n) => { appels.push([b.toISOString(), n]); return 1; }; + const etiq = async (b, n, m) => { appels.push([b.toISOString(), n, m]); return 1; }; const etat = (km, niv, maj, branchee = 'on', borne = 'on') => new Map([ [JEEP.odometre, { state: String(km) }], [JEEP.niveau, { state: String(niv) }], [JEEP.majA, { state: maj }], [JEEP.branchee, { state: branchee }], ['binary_sensor.victron_evcs_vehicle_connected', { state: borne }], @@ -56,7 +61,7 @@ eq('helper corrompu repart a vide', JSON.parse(ajouterReleve('pas du json', { ve eq('jeep : premier releve + niveau au branchement', [base.releves.length, base.niveaux.length, base.niveaux[0]?.niveau], [1, 1, 46]); await ingererJeep(sql, lire, etat(112716, 60, '2026-10-04T11:58:39+00:00'), journ, etiq); eq('jeep : compteur immobile et 2e maj du branchement -> rien', [base.releves.length, base.niveaux.length], [1, 1]); - eq('jeep : branchement identifie UNE fois, avec le niveau lu', appels, [['2026-10-04T11:28:00.000Z', 46]]); + eq('jeep : branchement identifie UNE fois, avec le niveau lu', appels, [['2026-10-04T11:28:00.000Z', 46, 'branchee']]); base.branche = new Date('2026-10-05T16:00:00Z'); await ingererJeep(sql, lire, etat(112760, 30, '2026-10-05T17:00:00+00:00'), journ); eq('jeep : trajet -> releve ; maj 1 h apres le branchement -> pas de niveau', [base.releves.length, base.releves[1]?.km, base.niveaux.length], [2, 112760, 1]); @@ -66,6 +71,18 @@ eq('helper corrompu repart a vide', JSON.parse(ajouterReleve('pas du json', { ve await ingererJeep(sql, lire, etat(112800, 25, '2026-10-06T16:05:00+00:00', 'on', 'off'), journ, etiq); eq('jeep : borne debranchee -> pas d identification', appels.length, 1); eq('jeep : borne debranchee -> releve oui, niveau au branchement non', [base.releves.length, base.niveaux.length], [3, 1]); + // Arrivee (05/10) : fin de trajet a 19:05, branchement a 19:06, Jeep non branchee pour elle. + base.branche = new Date('2026-10-07T17:06:15Z'); + await ingererJeep(sql, lire, etat(112850, 36, '2026-10-07T17:05:13+00:00', 'off', 'on'), journ, etiq); + eq('jeep : arrivee 1 min avant le branchement -> identifiee, niveau d arrivee', appels.at(-1), ['2026-10-07T17:06:15.000Z', 36, 'arrivee']); + await ingererJeep(sql, lire, etat(112850, 36, '2026-10-07T17:05:13+00:00', 'off', 'on'), journ, etiq); + eq('jeep : arrivee -> une seule identification', appels.length, 2); + base.branche = new Date('2026-10-08T17:30:00Z'); + await ingererJeep(sql, lire, etat(112900, 30, '2026-10-08T17:05:00+00:00', 'off', 'on'), journ, etiq); + eq('jeep : arrivee 25 min avant -> pas d identification', appels.length, 2); + base.branche = new Date('2026-10-09T17:06:00Z'); base.etiquette = 'VW'; + await ingererJeep(sql, lire, etat(112950, 40, '2026-10-09T17:05:00+00:00', 'off', 'on'), journ, etiq); + eq('jeep : proprietaire a deja dit VW -> rien', [appels.length, base.niveaux.length], [2, 2]); } if (process.argv.includes('--vues')) { diff --git a/tools/historise-evcs.mjs b/tools/historise-evcs.mjs index dc9441a..360fe14 100644 --- a/tools/historise-evcs.mjs +++ b/tools/historise-evcs.mjs @@ -1434,15 +1434,15 @@ async function appliquerEtiquetteConcierge(sql, client, st) { // 3. niveau « saisi » du vehicule = niveau lu, sauf saisie humaine depuis le // branchement - 1 h (l'automatisation HA recale la reference du compteur). // Le concierge ne doit alors plus poser sa question : il lit etiquette_branchement. -async function etiqueterJeepUconnect(sql, client, st, branche, niveau) { +async function etiqueterJeepUconnect(sql, client, st, branche, niveau, motif = 'branchee') { if (DRY) return 0; const { rowCount } = await client.query( `INSERT INTO etiquette_branchement (branchement, label, source_airtria, confirmed_at) VALUES ($1, 'JEEP', 'uconnect', $2) ON CONFLICT (branchement) DO NOTHING`, [branche, new Date().toISOString()]); if (!rowCount) return 0; // deja etiquete (le proprietaire a repondu) : on ne touche a rien - await journaliser(sql, 'vehicule', `Branchement identifie : JEEP (la voiture se declare branchee, Uconnect), niveau ${niveau} %`, - { origine: 'uconnect', label: 'JEEP', branchement: branche.toISOString(), niveau }); + await journaliser(sql, 'vehicule', `Branchement identifie : JEEP (${motif === 'arrivee' ? 'fin de trajet juste avant le branchement' : 'la voiture se declare branchee'}, Uconnect), niveau ${niveau} %`, + { origine: 'uconnect', label: 'JEEP', branchement: branche.toISOString(), niveau, preuve: motif }); let n = 1 + await piloterSelecteurVehicule(sql, st, 'JEEP', 'uconnect'); const saisieA = Number(st.get('input_datetime.victron_evcs_vehicle_level_at')?.attributes?.timestamp) * 1000; if (!(saisieA >= branche.getTime() - 3600000)) { @@ -2212,7 +2212,7 @@ async function cycle(client) { }) + await ingererNiveauSaisi(sql, st).catch((e) => { console.error(`niveau saisi en echec : ${e.message}`); return 0; }) + await ingererJeep(sql, (q, v) => client.query(q, v), st, journaliser, - (branche, niveau) => etiqueterJeepUconnect(sql, client, st, branche, niveau)).catch((e) => { + (branche, niveau, motif) => etiqueterJeepUconnect(sql, client, st, branche, niveau, motif)).catch((e) => { console.error(`jeep uconnect en echec : ${e.message}`); return 0; }), pvDiagnostic: DRY ? 0 : await publierDiagnosticPv(st).catch((e) => { diff --git a/tools/kilometrage.mjs b/tools/kilometrage.mjs index 0db8e2d..dede24c 100644 --- a/tools/kilometrage.mjs +++ b/tools/kilometrage.mjs @@ -303,24 +303,52 @@ export async function ingererJeep(sql, lire, st, journaliser, etiqueter = null) { vehicule: 'JEEP', km, niveau, ts: ts.toISOString(), source: 'uconnect' }); } } - if (niveau !== null && st.get(JEEP.branchee)?.state === 'on' - && st.get('binary_sensor.victron_evcs_vehicle_connected')?.state === 'on') { + // Deux preuves que le branchement de la borne est la JEEP : + // 1. elle se DECLARE branchee dans [branchement -5 min, +30 min] (charge demarree) ; + // 2. (05/10, accord du proprietaire) elle a FINI UN TRAJET (releve uconnect, + // compteur change) dans les 10 min AVANT le branchement. Le soir en « sans + // EDF » la charge ne demarre pas et la Jeep ne republie rien : sans cet + // indice, le 05/10 a 19:06 le concierge a du poser sa question. + // Risque assume : une autre voiture branchee dans ces 10 min serait prise pour + // la JEEP — la reponse du proprietaire corrige (sa derniere reponse fait foi). + if (st.get('binary_sensor.victron_evcs_vehicle_connected')?.state === 'on') { const { rows: b } = await lire( `SELECT max(ts) AS t FROM evenement WHERE genre = 'vehicule' AND detail->>'entite' = 'binary_sensor.victron_evcs_vehicle_connected' AND detail->>'apres' = 'on'`, []); const branche = b[0]?.t; - if (branche && ts >= new Date(branche.getTime() - 5 * 60000) && ts <= new Date(branche.getTime() + 30 * 60000)) { + let preuve = null; + if (branche && niveau !== null && st.get(JEEP.branchee)?.state === 'on' + && ts >= new Date(branche.getTime() - 5 * 60000) && ts <= new Date(branche.getTime() + 30 * 60000)) { + preuve = { ts, niveau, motif: 'branchee' }; + } else if (branche) { + const { rows: arr } = await lire( + `SELECT ts, niveau_pct FROM releve_km WHERE vehicule = 'JEEP' AND source = 'uconnect' + AND ts >= $1 AND ts <= $2 AND niveau_pct IS NOT NULL ORDER BY ts DESC LIMIT 1`, + [new Date(branche.getTime() - 10 * 60000), branche]); + if (arr.length) preuve = { ts: arr[0].ts, niveau: Number(arr[0].niveau_pct), motif: 'arrivee' }; + } + // Le proprietaire a deja nomme un AUTRE vehicule pour ce branchement : on n'y touche pas. + let autre = false; + if (preuve) { + const { rows: et } = await lire( + `SELECT label FROM etiquette_branchement WHERE branchement BETWEEN $1 AND $2 LIMIT 1`, + [new Date(branche.getTime() - 2000), new Date(branche.getTime() + 2000)]); + autre = et.length > 0 && et[0].label !== 'JEEP'; + } + if (preuve && !autre) { + const ts = preuve.ts, niveau = preuve.niveau; const { rows: deja } = await lire( - "SELECT 1 FROM niveau_saisi WHERE source = 'uconnect' AND saisi_a >= $1", [new Date(branche.getTime() - 5 * 60000)]); + "SELECT 1 FROM niveau_saisi WHERE source = 'uconnect' AND saisi_a >= $1", [new Date(branche.getTime() - 10 * 60000)]); if (!deja.length) { const r = await sql( "INSERT INTO niveau_saisi (saisi_a, niveau_pct, ref_kwh, source) VALUES ($1,$2,NULL,'uconnect') ON CONFLICT DO NOTHING", [ts, niveau]); if (r?.rowCount) { n++; - await journaliser(sql, 'vehicule', `Niveau JEEP au branchement : ${niveau} % (lu par Uconnect)`, - { niveau, ts: ts.toISOString(), branchement: branche.toISOString() }); - if (etiqueter) n += await etiqueter(branche, niveau); + await journaliser(sql, 'vehicule', `Niveau JEEP au branchement : ${niveau} % (lu par Uconnect` + + (preuve.motif === 'arrivee' ? ', fin de trajet juste avant le branchement)' : ')'), + { niveau, ts: ts.toISOString(), branchement: branche.toISOString(), preuve: preuve.motif }); + if (etiqueter) n += await etiqueter(branche, niveau, preuve.motif); } } }