diff --git a/tests/kilometrage/run.mjs b/tests/kilometrage/run.mjs index c255e54..ec6b87d 100644 --- a/tests/kilometrage/run.mjs +++ b/tests/kilometrage/run.mjs @@ -46,21 +46,25 @@ eq('helper corrompu repart a vide', JSON.parse(ajouterReleve('pas du json', { ve return { rows: [] }; }; const journ = async () => {}; + const appels = []; + const etiq = async (b, n) => { appels.push([b.toISOString(), n]); 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 }], ['input_number.victron_evcs_capacity_jeep_kwh', { state: '8.7' }], ['input_number.victron_evcs_autonomie_jeep_km', { state: '50' }]]); - await ingererJeep(sql, lire, etat(112716, 46, '2026-10-04T11:28:39+00:00'), journ); + await ingererJeep(sql, lire, etat(112716, 46, '2026-10-04T11:28:39+00:00'), journ, etiq); 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); + 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]]); 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]); await ingererJeep(sql, lire, etat(112700, 30, '2026-10-05T18:00:00+00:00'), journ); eq('jeep : compteur en baisse refuse', base.releves.length, 2); base.branche = new Date('2026-10-06T16:00:00Z'); - await ingererJeep(sql, lire, etat(112800, 25, '2026-10-06T16:05:00+00:00', 'on', 'off'), journ); + 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]); } diff --git a/tools/historise-evcs.mjs b/tools/historise-evcs.mjs index 22df8f4..c4180f0 100644 --- a/tools/historise-evcs.mjs +++ b/tools/historise-evcs.mjs @@ -1131,6 +1131,9 @@ const HOUSEHOLD = new Set(['VW', 'JEEP']); // un canal inconnu ne doit pas valoir confirmation par defaut. `regle-puissance` // n'y est pas et n'y sera jamais — c'est une deduction, pas une parole. const CANAUX_HUMAINS = new Set(['whatsapp', 'proprietaire']); +// Sources qui CONFIRMENT un vehicule : l'humain, ou la voiture elle-meme +// (04/10/2026 : la Jeep se declare branchee via Uconnect). +const CANAUX_SURS = new Set([...CANAUX_HUMAINS, 'uconnect']); const LABELS = new Set(['VW', 'JEEP', 'MEGANE', 'INVITE', 'ZERO']); let dernierBlancVu = null; // anti-repetition du journal en essai a blanc @@ -1196,7 +1199,7 @@ async function piloterSelecteurVehicule(sql, st, label, source) { return 1; } await journaliser(sql, 'vehicule', - `Selecteur vehicule pilote par le concierge : ${actuel || 'inconnu'} -> ${label}`, + `Selecteur vehicule pilote par ${source === 'uconnect' ? 'la Jeep (Uconnect)' : 'le concierge'} : ${actuel || 'inconnu'} -> ${label}`, { origine: 'airtria', source_airtria: source, label, avant: actuel, entite: 'input_select.victron_evcs_vehicle', motif: 'la reponse du proprietaire fait foi, et le controleur lit le selecteur' }); @@ -1394,6 +1397,45 @@ async function appliquerEtiquetteConcierge(sql, client, st) { // debranchement suivant — l invariant « un cable, un vehicule ». Les 2 secondes // de tolerance sur la borne basse sont celles du 09/09 : une session s ouvre // jusqu a 62 ms AVANT que sa transition de branchement soit journalisee. +// LA JEEP SE DECLARE ELLE-MEME (04/10/2026, accord du proprietaire). Appelee par +// ingererJeep a la premiere mise a jour Uconnect qui suit un branchement de la +// borne, Jeep « branchee ». Trois gestes, une fois par branchement : +// 1. etiquette JEEP du branchement (source uconnect, confirmee) — SANS ecraser +// une etiquette deja donnee par le proprietaire (ON CONFLICT DO NOTHING) ; +// 2. selecteur sur JEEP (heure de depart, regles du controleur) ; +// 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) { + 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 }); + 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)) { + try { + const r = await fetch(`${HA_URL}/api/services/input_number/set_value`, { + method: 'POST', + headers: { Authorization: `Bearer ${HA_TOKEN}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ entity_id: 'input_number.victron_evcs_vehicle_level_pct', value: niveau }), + signal: AbortSignal.timeout(8000), + }); + if (!r.ok) throw new Error(`HTTP ${r.status}`); + await journaliser(sql, 'vehicule', `Niveau du vehicule branche pose a ${niveau} % (lu par la Jeep)`, + { origine: 'uconnect', niveau, entite: 'input_number.victron_evcs_vehicle_level_pct' }); + n++; + } catch (e) { + await journaliser(sql, 'vehicule', `Niveau JEEP NON pose (${e.message})`, { origine: 'uconnect', niveau }); + } + } + return n; +} + async function appliquerEtiquettesEnAttente(sql, client) { if (DRY) return 0; const { rows } = await client.query( @@ -1401,7 +1443,7 @@ async function appliquerEtiquettesEnAttente(sql, client) { WHERE branchement > now() - interval '48 hours' ORDER BY branchement`); let n = 0; for (const e of rows) { - const sur = CANAUX_HUMAINS.has(e.source_airtria); + const sur = CANAUX_SURS.has(e.source_airtria); const { rowCount } = await sql( `UPDATE session_charge s SET vehicule = $2, vehicule_sur = $3 WHERE s.debut >= $1::timestamptz - interval '2 seconds' @@ -2140,7 +2182,8 @@ async function cycle(client) { console.error(`releves km en echec : ${e.message}`); return 0; }) + 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).catch((e) => { + }) + await ingererJeep(sql, (q, v) => client.query(q, v), st, journaliser, + (branche, niveau) => etiqueterJeepUconnect(sql, client, st, branche, niveau)).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 c6bd21f..0db8e2d 100644 --- a/tools/kilometrage.mjs +++ b/tools/kilometrage.mjs @@ -276,7 +276,10 @@ export const JEEP = { branchee: 'binary_sensor.jeep_renegade_phev_ev_charger', }; -export async function ingererJeep(sql, lire, st, journaliser) { +// `etiqueter(branche, niveau)` (fourni par le service) : pose l'etiquette JEEP +// du branchement, le selecteur et le niveau « saisi » — appelee UNE fois par +// branchement, a la premiere mise a jour Uconnect qui le confirme. +export async function ingererJeep(sql, lire, st, journaliser, etiqueter = null) { const km = Number(st.get(JEEP.odometre)?.state); const niv = Number(st.get(JEEP.niveau)?.state); const t = Date.parse(st.get(JEEP.majA)?.state ?? ''); @@ -317,6 +320,7 @@ export async function ingererJeep(sql, lire, st, journaliser) { 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); } } }