401 lines
22 KiB
JavaScript
401 lines
22 KiB
JavaScript
// Kilometrage et capacite des vehicules (04/10/2026, demande du proprietaire).
|
|
//
|
|
// Deux mesures distinctes, decidees avec lui :
|
|
// 1. km par kWh : entre deux RELEVES DU COMPTEUR kilometrique d'un vehicule,
|
|
// kWh consommes = kWh livres par la borne a ce vehicule entre les deux
|
|
// relevés + (niveau au 1er releve - niveau au 2nd) x capacite.
|
|
// Cela mesure surtout la conduite et la saison (chauffage, froid).
|
|
// 2. capacite estimee : a chaque branchement ou le niveau a ete SAISI et la
|
|
// charge menee jusqu'au plein, capacite = kWh livres depuis la saisie /
|
|
// (part manquante). C'est elle qui montrera l'usure de la batterie.
|
|
//
|
|
// ⚠️ La JEEP et la VW sont des HYBRIDES : batterie vide, elles roulent a
|
|
// l'essence et ces kilometres gonfleraient le rendement. Un intervalle n'est
|
|
// donc retenu que si le niveau au retour est > 0. La ZERO est tout electrique.
|
|
//
|
|
// Contrat d'entree (meme modele que l'etiquette du concierge) :
|
|
// `input_text.evcs_releve_km` = tableau JSON des derniers releves, chacun
|
|
// [vehicule, km, niveau_pct|null, epoch_s, source]
|
|
// ecrit par le concierge WhatsApp ou par le script HA du tableau EVCS Master.
|
|
// Le service les ingere chaque cycle, sans doublon (vehicule, horodatage).
|
|
|
|
export const VEHICULES_TOUT_ELECTRIQUES = ['ZERO'];
|
|
export const RELEVE_HELPER = 'input_text.evcs_releve_km';
|
|
export const NIVEAU_ENTITY = 'input_number.victron_evcs_vehicle_level_pct';
|
|
export const NIVEAU_AT_ENTITY = 'input_datetime.victron_evcs_vehicle_level_at';
|
|
export const NIVEAU_REF_ENTITY = 'input_number.victron_evcs_vehicle_level_ref_kwh';
|
|
// Etats du controleur qui disent « le vehicule n'accepte plus rien ».
|
|
export const ETATS_PLEIN = ['day_vehicle_full_idle', 'night_recovery_done_idle',
|
|
'night_session_done_idle', 'departure_done_idle'];
|
|
|
|
// Lit le helper. Renvoie { releves: [...], rejets: [motif...] }. Pure.
|
|
export function lireReleves(brut) {
|
|
const s = (brut ?? '').toString().trim();
|
|
if (!s || s === 'unknown' || s === 'unavailable') return { releves: [], rejets: [] };
|
|
let tab;
|
|
try { tab = JSON.parse(s); } catch { return { releves: [], rejets: ['JSON illisible'] }; }
|
|
if (!Array.isArray(tab)) return { releves: [], rejets: ['pas un tableau'] };
|
|
const releves = [], rejets = [];
|
|
for (const e of tab) {
|
|
if (!Array.isArray(e) || e.length < 4) { rejets.push('entree mal formee'); continue; }
|
|
const [v, km, niveau, t, source] = e;
|
|
const vehicule = (v ?? '').toString().trim().toUpperCase();
|
|
const kmN = Number(km), tN = Number(t);
|
|
const nivN = niveau === null || niveau === undefined || niveau === '' ? null : Number(niveau);
|
|
if (!vehicule) { rejets.push('vehicule absent'); continue; }
|
|
if (!Number.isInteger(kmN) || kmN <= 0 || kmN > 2000000) { rejets.push(`${vehicule} : km invalide (${km})`); continue; }
|
|
if (nivN !== null && !(Number.isInteger(nivN) && nivN >= 0 && nivN <= 100)) {
|
|
rejets.push(`${vehicule} : niveau invalide (${niveau})`); continue;
|
|
}
|
|
if (!Number.isFinite(tN) || tN < 1.7e9) { rejets.push(`${vehicule} : horodatage invalide`); continue; }
|
|
releves.push({ vehicule, km: kmN, niveau: nivN, ts: new Date(tN * 1000),
|
|
source: (source ?? '').toString() || 'inconnue' });
|
|
}
|
|
return { releves, rejets };
|
|
}
|
|
|
|
// Ajoute un releve au tableau existant en gardant les 6 derniers (le helper est
|
|
// plafonne a 255 caracteres par HA). Pure — utilisee par le concierge.
|
|
export function ajouterReleve(brut, releve) {
|
|
let tab = [];
|
|
try { const t = JSON.parse(brut || '[]'); if (Array.isArray(t)) tab = t; } catch { /* repart a vide */ }
|
|
tab.push([releve.vehicule, releve.km, releve.niveau ?? null, Math.round(releve.ts.getTime() / 1000), releve.source]);
|
|
let out = tab.slice(-6);
|
|
while (JSON.stringify(out).length > 255 && out.length > 1) out = out.slice(1);
|
|
return JSON.stringify(out);
|
|
}
|
|
|
|
export async function creerSchemaKm(client) {
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS releve_km (
|
|
vehicule text NOT NULL,
|
|
ts timestamptz NOT NULL,
|
|
km integer NOT NULL,
|
|
niveau_pct integer,
|
|
source text,
|
|
capacite_kwh double precision,
|
|
autonomie_ref_km double precision,
|
|
tout_electrique boolean NOT NULL DEFAULT false,
|
|
pose_a timestamptz NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (vehicule, ts)
|
|
)`);
|
|
// Chaque saisie du niveau au branchement (concierge ou tableau de bord),
|
|
// avec le compteur de la borne a cet instant. Le vehicule se deduit de
|
|
// l'etiquette du branchement, jamais du selecteur.
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS niveau_saisi (
|
|
saisi_a timestamptz PRIMARY KEY,
|
|
niveau_pct integer NOT NULL,
|
|
ref_kwh double precision NOT NULL,
|
|
pose_a timestamptz NOT NULL DEFAULT now()
|
|
)`);
|
|
// Saisie ANCIENNE ajoutee a la main (04/10 : VW a 5 % le 15/09) : le compteur
|
|
// de la borne n'etait pas encore historise, la reference peut manquer.
|
|
await client.query('ALTER TABLE niveau_saisi ALTER COLUMN ref_kwh DROP NOT NULL');
|
|
await client.query('ALTER TABLE niveau_saisi ADD COLUMN IF NOT EXISTS source text');
|
|
// NIVEAU A LA FIN (10/10/2026) : le proprietaire donne l'autonomie au
|
|
// debranchement (VW 22 km au branchement, 39 km au debranchement, pour 42 km
|
|
// pleine). Sans releve kilometrique, ce niveau n'avait nulle part ou aller ;
|
|
// il prime sur le « plein » du controleur, peu fiable pour la VW (§11.5).
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS niveau_fin (
|
|
saisi_a timestamptz PRIMARY KEY,
|
|
niveau_pct integer NOT NULL,
|
|
source text,
|
|
pose_a timestamptz NOT NULL DEFAULT now()
|
|
)`);
|
|
await client.query(SQL_VUE_INTERVALLES);
|
|
await client.query(SQL_VUE_CAPACITE);
|
|
}
|
|
|
|
// Un intervalle = deux releves consecutifs du meme vehicule.
|
|
export const SQL_VUE_INTERVALLES = `
|
|
DROP VIEW IF EXISTS km_intervalle;
|
|
CREATE VIEW km_intervalle AS
|
|
WITH r AS (
|
|
SELECT vehicule, ts, km, niveau_pct, capacite_kwh, autonomie_ref_km, tout_electrique,
|
|
lag(ts) OVER w AS ts0,
|
|
lag(km) OVER w AS km0,
|
|
lag(niveau_pct) OVER w AS n0
|
|
FROM releve_km WINDOW w AS (PARTITION BY vehicule ORDER BY ts)
|
|
), e AS (
|
|
SELECT r.*, km - km0 AS km_parcourus,
|
|
(SELECT coalesce(sum(coalesce(s.energie_station_kwh, s.energie_kwh)), 0)
|
|
FROM session_charge s
|
|
WHERE s.vehicule = r.vehicule AND s.debut >= r.ts0 AND s.debut < r.ts) AS kwh_charges
|
|
FROM r WHERE ts0 IS NOT NULL
|
|
), c AS (
|
|
SELECT e.*,
|
|
CASE WHEN n0 IS NOT NULL AND niveau_pct IS NOT NULL AND capacite_kwh > 0
|
|
THEN kwh_charges + (n0 - niveau_pct) / 100.0 * capacite_kwh END AS kwh_consommes
|
|
FROM e
|
|
)
|
|
SELECT vehicule, ts0 AS debut, ts AS fin, km_parcourus, kwh_charges, n0 AS niveau_debut,
|
|
niveau_pct AS niveau_fin, capacite_kwh, kwh_consommes,
|
|
CASE WHEN kwh_consommes > 0 THEN km_parcourus / kwh_consommes END AS km_par_kwh,
|
|
CASE WHEN capacite_kwh > 0 AND autonomie_ref_km > 0
|
|
THEN autonomie_ref_km / capacite_kwh END AS km_par_kwh_ref,
|
|
CASE
|
|
WHEN km_parcourus < 0 THEN 'compteur en baisse'
|
|
WHEN km_parcourus = 0 THEN 'aucun km'
|
|
WHEN n0 IS NULL OR niveau_pct IS NULL THEN 'niveau manquant'
|
|
WHEN NOT tout_electrique AND niveau_pct <= 0 THEN 'batterie vide au retour : km essence possibles'
|
|
WHEN kwh_consommes IS NULL OR kwh_consommes <= 0 THEN 'energie nulle'
|
|
ELSE 'valide' END AS statut
|
|
FROM c`;
|
|
|
|
// Capacite estimee : derniere saisie de chaque branchement termine.
|
|
// - Branchement = derniere transition 'on' au plus 1 h apres la saisie (la
|
|
// saisie vaut si elle date de branchement - 1 h ou apres).
|
|
// - Le niveau saisi est celui du BRANCHEMENT (le proprietaire le lit sur la
|
|
// voiture avant de brancher, et le dit parfois une heure apres : « c'etait
|
|
// 53 % a 13h10 ») : l'energie se compte donc du compteur de la borne au
|
|
// branchement jusqu'au debranchement.
|
|
// - Niveau de fin = 100 si le controleur a vu le vehicule plein, sinon le
|
|
// niveau d'un releve km du meme vehicule dans l'heure qui suit le
|
|
// debranchement (« debranche a 78 % »).
|
|
// - Vehicule = etiquette CONFIRMEE des sessions du branchement (celle que le
|
|
// proprietaire corrige), a defaut celle du concierge.
|
|
export const SQL_VUE_CAPACITE = `
|
|
DROP VIEW IF EXISTS capacite_estimee;
|
|
CREATE VIEW capacite_estimee AS
|
|
WITH tr AS (
|
|
SELECT ts, detail->>'apres' AS apres FROM evenement
|
|
WHERE genre = 'vehicule'
|
|
AND detail->>'entite' = 'binary_sensor.victron_evcs_vehicle_connected'
|
|
), s AS (
|
|
SELECT n.*,
|
|
(SELECT max(ts) FROM tr WHERE apres = 'on' AND tr.ts <= n.saisi_a + interval '1 hour') AS branche
|
|
FROM niveau_saisi n
|
|
), s1 AS (
|
|
SELECT DISTINCT ON (branche) * FROM s WHERE branche IS NOT NULL ORDER BY branche, saisi_a DESC
|
|
), b AS (
|
|
SELECT s1.*,
|
|
(SELECT min(ts) FROM tr WHERE apres = 'off' AND tr.ts > s1.branche) AS debranche
|
|
FROM s1 WHERE saisi_a >= branche - interval '1 hour'
|
|
), v AS (
|
|
SELECT b.*,
|
|
coalesce(
|
|
(SELECT sc.vehicule FROM session_charge sc
|
|
WHERE sc.debut >= b.branche AND sc.debut < b.debranche AND sc.vehicule_sur
|
|
GROUP BY sc.vehicule ORDER BY sum(coalesce(sc.energie_station_kwh, sc.energie_kwh)) DESC LIMIT 1),
|
|
(SELECT label FROM etiquette_branchement eb
|
|
WHERE eb.branchement BETWEEN b.branche - interval '2 seconds' AND b.branche + interval '2 seconds'
|
|
LIMIT 1)) AS vehicule
|
|
FROM b WHERE debranche IS NOT NULL
|
|
), k AS (
|
|
SELECT v.*,
|
|
-- Compteur de la borne (historise depuis le 30/09), a defaut la somme
|
|
-- des sessions du branchement (compteur de la borne par session).
|
|
coalesce(
|
|
(SELECT valeur FROM mesure m WHERE m.cle = 'sensor.evcs_hq2239dnt7p_total_energy'
|
|
AND m.ts <= v.debranche AND m.ts >= v.branche - interval '1 day' ORDER BY m.ts DESC LIMIT 1)
|
|
- coalesce((SELECT valeur FROM mesure m WHERE m.cle = 'sensor.evcs_hq2239dnt7p_total_energy'
|
|
AND m.ts <= v.branche AND m.ts >= v.branche - interval '1 day' ORDER BY m.ts DESC LIMIT 1), ref_kwh),
|
|
(SELECT sum(coalesce(sc.energie_station_kwh, sc.energie_kwh)) FROM session_charge sc
|
|
WHERE sc.debut >= v.branche AND sc.debut < v.debranche)) AS kwh_livres,
|
|
EXISTS (SELECT 1 FROM decision d WHERE d.ts > v.branche AND d.ts <= v.debranche
|
|
AND d.etat = ANY (ARRAY['${ETATS_PLEIN.join("','")}'])) AS plein,
|
|
(SELECT r.niveau_pct FROM releve_km r
|
|
WHERE r.vehicule = v.vehicule AND r.niveau_pct IS NOT NULL
|
|
AND r.ts >= v.debranche AND r.ts <= v.debranche + interval '1 hour'
|
|
ORDER BY r.ts LIMIT 1) AS niveau_releve_apres,
|
|
(SELECT nf.niveau_pct FROM niveau_fin nf
|
|
WHERE nf.saisi_a >= v.debranche - interval '15 minutes' AND nf.saisi_a <= v.debranche + interval '1 hour'
|
|
ORDER BY nf.saisi_a LIMIT 1) AS niveau_fin_saisi,
|
|
(SELECT nf.source FROM niveau_fin nf
|
|
WHERE nf.saisi_a >= v.debranche - interval '15 minutes' AND nf.saisi_a <= v.debranche + interval '1 hour'
|
|
ORDER BY nf.saisi_a LIMIT 1) AS niveau_fin_source
|
|
FROM v
|
|
), f AS (
|
|
SELECT k.*, coalesce(niveau_fin_saisi, CASE WHEN plein THEN 100 ELSE niveau_releve_apres END) AS niveau_fin FROM k
|
|
)
|
|
SELECT branche, debranche, coalesce(vehicule, '?') AS vehicule, niveau_pct, niveau_fin,
|
|
kwh_livres, plein,
|
|
-- Un des deux niveaux est deduit de l'autonomie affichee (pas une lecture).
|
|
(coalesce(source, '') ILIKE '%autonomie%' OR coalesce(niveau_fin_source, '') ILIKE '%autonomie%') AS niveau_estime,
|
|
CASE WHEN vehicule IS NOT NULL AND niveau_fin - niveau_pct >= 10 AND kwh_livres > 0
|
|
THEN kwh_livres / ((niveau_fin - niveau_pct) / 100.0) END AS capacite_kwh
|
|
FROM f`;
|
|
|
|
// Ingestion des releves du helper. Refuse un compteur qui baisse.
|
|
export async function ingererReleves(sql, lire, st, journaliser) {
|
|
const { releves, rejets } = lireReleves(st.get(RELEVE_HELPER)?.state);
|
|
let n = 0;
|
|
for (const r of releves) {
|
|
const v = r.vehicule.toLowerCase();
|
|
const cap = Number(st.get(`input_number.victron_evcs_capacity_${v}_kwh`)?.state);
|
|
const auto = Number(st.get(`input_number.victron_evcs_autonomie_${v}_km`)?.state);
|
|
const { rows: prec } = await lire(
|
|
'SELECT km FROM releve_km WHERE vehicule = $1 AND ts < $2 ORDER BY ts DESC LIMIT 1',
|
|
[r.vehicule, r.ts]);
|
|
const { rows: deja } = await lire(
|
|
'SELECT 1 FROM releve_km WHERE vehicule = $1 AND ts = $2', [r.vehicule, r.ts]);
|
|
if (deja.length) continue;
|
|
if (prec.length && r.km < Number(prec[0].km)) {
|
|
const cle = `${r.vehicule}|${r.ts.toISOString()}`;
|
|
if (!rejetsJournalises.has(cle)) {
|
|
rejetsJournalises.add(cle);
|
|
await journaliser(sql, 'vehicule', `Releve km ${r.vehicule} refuse : ${r.km} km < ${prec[0].km} km du releve precedent`,
|
|
{ vehicule: r.vehicule, km: r.km, precedent: Number(prec[0].km), source: r.source });
|
|
}
|
|
continue;
|
|
}
|
|
await sql(
|
|
`INSERT INTO releve_km (vehicule, ts, km, niveau_pct, source, capacite_kwh, autonomie_ref_km, tout_electrique)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT DO NOTHING`,
|
|
[r.vehicule, r.ts, r.km, r.niveau, r.source, cap > 0 ? cap : null, auto > 0 ? auto : null,
|
|
VEHICULES_TOUT_ELECTRIQUES.includes(r.vehicule)]);
|
|
await journaliser(sql, 'vehicule',
|
|
`Releve km ${r.vehicule} : ${r.km} km` + (r.niveau === null ? ', niveau non donne' : `, niveau ${r.niveau} %`) + ` (${r.source})`,
|
|
{ vehicule: r.vehicule, km: r.km, niveau: r.niveau, ts: r.ts.toISOString(), source: r.source });
|
|
n++;
|
|
}
|
|
for (const m of rejets) {
|
|
if (rejetsJournalises.has(m)) continue;
|
|
rejetsJournalises.add(m);
|
|
await journaliser(sql, 'vehicule', `Releve km illisible : ${m}`, { brut: (st.get(RELEVE_HELPER)?.state || '').slice(0, 255) });
|
|
}
|
|
return n;
|
|
}
|
|
const rejetsJournalises = new Set();
|
|
|
|
// Saisie du niveau : une ligne par horodatage de saisie.
|
|
export async function ingererNiveauSaisi(sql, st) {
|
|
// L'attribut `timestamp` d'un input_datetime est en secondes epoch : pas de
|
|
// fuseau a deviner (l'etat, lui, est une heure locale sans decalage).
|
|
// A la SECONDE : un redemarrage de HA restaure l'horodatage sans ses
|
|
// millisecondes, et la meme saisie revenait en double (04/10).
|
|
const at = Math.floor(Number(st.get(NIVEAU_AT_ENTITY)?.attributes?.timestamp)) * 1000;
|
|
const niveau = Number(st.get(NIVEAU_ENTITY)?.state);
|
|
const ref = Number(st.get(NIVEAU_REF_ENTITY)?.state);
|
|
if (!Number.isFinite(at) || !Number.isInteger(niveau) || !Number.isFinite(ref) || ref <= 0) return 0;
|
|
const r = await sql(
|
|
'INSERT INTO niveau_saisi (saisi_a, niveau_pct, ref_kwh) VALUES ($1,$2,$3) ON CONFLICT DO NOTHING',
|
|
[new Date(at), niveau, ref]);
|
|
await marquerNiveauxEstimes(sql);
|
|
return r?.rowCount || 0;
|
|
}
|
|
|
|
// Niveau ESTIME (12/10/2026, avec SPARK) : un niveau deduit de l'autonomie
|
|
// affichee (photo du tableau de bord VW, regle de trois sur l'autonomie pleine
|
|
// de reference) arrive dans le releve avec la source « photo_autonomie:<km>/<ref> »,
|
|
// mais aussi dans le helper du niveau, donc dans niveau_saisi SANS source.
|
|
// On recopie la source sur la saisie correspondante (meme niveau, a 20 min pres),
|
|
// dans un sens comme dans l'autre de l'ordre d'arrivee. La vue capacite_estimee
|
|
// l'expose (niveau_estime) : un niveau deduit de l'autonomie depend de la
|
|
// reference, du chauffage et de la conduite, et biaise justement l'usure.
|
|
export const SOURCE_ESTIMEE = 'photo_autonomie:';
|
|
export async function marquerNiveauxEstimes(sql) {
|
|
await sql(
|
|
`UPDATE niveau_saisi n SET source = r.source FROM releve_km r
|
|
WHERE n.source IS NULL AND r.source LIKE '${SOURCE_ESTIMEE}%' AND r.niveau_pct = n.niveau_pct
|
|
AND n.saisi_a BETWEEN r.ts - interval '20 minutes' AND r.ts + interval '20 minutes'`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// JEEP LUE DIRECTEMENT (04/10/2026) : integration HACS hass-uconnect (API
|
|
// Stellantis non officielle, compte du proprietaire, intervalle 15 min, sans
|
|
// PIN ni entites de commande). La voiture publie un etat a chaque evenement
|
|
// (arret, branchement) : `last_info_update_at` date les valeurs.
|
|
// - RELEVE : un par changement d'odometre (fin de trajet), date de la mise a
|
|
// jour, avec le niveau HV a cet instant. Le compteur immobile ne produit rien.
|
|
// - NIVEAU AU BRANCHEMENT : la premiere mise a jour dans les 30 min apres le
|
|
// branchement de la borne, si la Jeep se dit branchee -> niveau_saisi
|
|
// (source uconnect). Une saisie humaine posterieure reste prioritaire (la
|
|
// vue prend la derniere saisie du branchement).
|
|
// ---------------------------------------------------------------------
|
|
export const JEEP = {
|
|
odometre: 'sensor.jeep_renegade_phev_odometer',
|
|
niveau: 'sensor.jeep_renegade_phev_hvbattery_charge',
|
|
majA: 'sensor.jeep_renegade_phev_last_info_update_at',
|
|
branchee: 'binary_sensor.jeep_renegade_phev_ev_charger',
|
|
contact: 'binary_sensor.jeep_renegade_phev_ignition',
|
|
};
|
|
|
|
// `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 ?? '');
|
|
if (!Number.isFinite(t) || !Number.isInteger(km) || km <= 0) return 0;
|
|
const niveau = Number.isFinite(niv) && niv >= 0 && niv <= 100 ? Math.round(niv) : null;
|
|
const ts = new Date(t);
|
|
let n = 0;
|
|
const { rows: der } = await lire(
|
|
"SELECT km, ts FROM releve_km WHERE vehicule = 'JEEP' ORDER BY ts DESC LIMIT 1", []);
|
|
if (!der.length || (Number(der[0].km) !== km && ts > der[0].ts)) {
|
|
if (der.length && km < Number(der[0].km)) return 0; // compteur en baisse : on ne croit pas
|
|
const cap = Number(st.get('input_number.victron_evcs_capacity_jeep_kwh')?.state);
|
|
const auto = Number(st.get('input_number.victron_evcs_autonomie_jeep_km')?.state);
|
|
const r = await sql(
|
|
`INSERT INTO releve_km (vehicule, ts, km, niveau_pct, source, capacite_kwh, autonomie_ref_km, tout_electrique)
|
|
VALUES ('JEEP',$1,$2,$3,'uconnect',$4,$5,false) ON CONFLICT DO NOTHING`,
|
|
[ts, km, niveau, cap > 0 ? cap : null, auto > 0 ? auto : null]);
|
|
if (r?.rowCount) {
|
|
n++;
|
|
await journaliser(sql, 'vehicule', `Releve km JEEP ${km} km` + (niveau === null ? '' : `, niveau ${niveau} %`) + ' (lu par Uconnect)',
|
|
{ vehicule: 'JEEP', km, niveau, ts: ts.toISOString(), source: 'uconnect' });
|
|
}
|
|
}
|
|
// 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;
|
|
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' };
|
|
}
|
|
// Jeep vue AILLEURS apres le branchement (07/10/2026, accord du proprietaire) :
|
|
// une information Uconnect posterieure au branchement avec le contact MIS
|
|
// dit qu'elle roule — ce n'est pas elle sur la borne. (La position ne sert
|
|
// pas : la Jeep garee a la maison est a 2,2 km de zone.home, donc toujours
|
|
// « not_home ».)
|
|
if (preuve && branche && st.get(JEEP.contact)?.state === 'on' && ts > branche) preuve = null;
|
|
// 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() - 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`
|
|
+ (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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return n;
|
|
}
|