Kilometrage : Jeep lue par Uconnect (releve a chaque trajet, niveau au branchement), saisie a la seconde
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
// node tests/kilometrage/run.mjs --vues -> vues SQL, dans une transaction
|
// node tests/kilometrage/run.mjs --vues -> vues SQL, dans une transaction
|
||||||
// ANNULEE (a lancer dans le
|
// ANNULEE (a lancer dans le
|
||||||
// conteneur d'historisation)
|
// conteneur d'historisation)
|
||||||
import { lireReleves, ajouterReleve, creerSchemaKm } from '../../tools/kilometrage.mjs';
|
import { lireReleves, ajouterReleve, creerSchemaKm, ingererJeep, JEEP } from '../../tools/kilometrage.mjs';
|
||||||
|
|
||||||
let ok = 0, ko = 0;
|
let ok = 0, ko = 0;
|
||||||
const eq = (nom, a, b) => {
|
const eq = (nom, a, b) => {
|
||||||
@@ -31,6 +31,39 @@ eq('255 car max', b.length <= 255, true);
|
|||||||
eq('dernier garde', JSON.parse(b).at(-1)[1], 112703);
|
eq('dernier garde', JSON.parse(b).at(-1)[1], 112703);
|
||||||
eq('helper corrompu repart a vide', JSON.parse(ajouterReleve('pas du json', { vehicule: 'VW', km: 5, niveau: null, ts: new Date(T * 1000), source: 'ha' })).length, 1);
|
eq('helper corrompu repart a vide', JSON.parse(ajouterReleve('pas du json', { vehicule: 'VW', km: 5, niveau: null, ts: new Date(T * 1000), source: 'ha' })).length, 1);
|
||||||
|
|
||||||
|
// --- ingererJeep : base simulee en memoire (04/10/2026)
|
||||||
|
{
|
||||||
|
const base = { releves: [], niveaux: [], branche: new Date('2026-10-04T11:28:00Z') };
|
||||||
|
const sql = async (q, v) => {
|
||||||
|
if (q.includes('INSERT INTO releve_km')) { base.releves.push({ ts: v[0], km: v[1], niveau: v[2] }); return { rowCount: 1 }; }
|
||||||
|
if (q.includes('INSERT INTO niveau_saisi')) { base.niveaux.push({ saisi_a: v[0], niveau: v[1] }); return { rowCount: 1 }; }
|
||||||
|
return { rowCount: 1 };
|
||||||
|
};
|
||||||
|
const lire = async (q, v) => {
|
||||||
|
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 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 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);
|
||||||
|
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);
|
||||||
|
eq('jeep : compteur immobile et 2e maj du branchement -> rien', [base.releves.length, base.niveaux.length], [1, 1]);
|
||||||
|
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);
|
||||||
|
eq('jeep : borne debranchee -> releve oui, niveau au branchement non', [base.releves.length, base.niveaux.length], [3, 1]);
|
||||||
|
}
|
||||||
|
|
||||||
if (process.argv.includes('--vues')) {
|
if (process.argv.includes('--vues')) {
|
||||||
const pg = (await import('pg')).default;
|
const pg = (await import('pg')).default;
|
||||||
const c = new pg.Client(); await c.connect();
|
const c = new pg.Client(); await c.connect();
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import fs from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import pg from 'pg';
|
import pg from 'pg';
|
||||||
import { creerSchemaKm, ingererReleves, ingererNiveauSaisi } from './kilometrage.mjs';
|
import { creerSchemaKm, ingererReleves, ingererNiveauSaisi, ingererJeep } from './kilometrage.mjs';
|
||||||
import { texteRapport, envoyer, donneesNuit, texteDepuis } from './rapport-nuit.mjs';
|
import { texteRapport, envoyer, donneesNuit, texteDepuis } from './rapport-nuit.mjs';
|
||||||
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, minuitLocal, COMPTEURS_IMPORT, COMPTEUR_EDF, consoJour, COMPTEURS_CONSO, projectionSoir, previsionNuit, vrmVersCourbes, nuitMaisonPrevue } from './diagnostics-agent.mjs';
|
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, minuitLocal, COMPTEURS_IMPORT, COMPTEUR_EDF, consoJour, COMPTEURS_CONSO, projectionSoir, previsionNuit, vrmVersCourbes, nuitMaisonPrevue } from './diagnostics-agent.mjs';
|
||||||
|
|
||||||
@@ -93,6 +93,10 @@ const MESURES = [
|
|||||||
// compteur de la borne, pour tracer « donne depuis 01:24 » contre le budget.
|
// compteur de la borne, pour tracer « donne depuis 01:24 » contre le budget.
|
||||||
'input_number.evcs_balance_night_vehicle_budget_kwh',
|
'input_number.evcs_balance_night_vehicle_budget_kwh',
|
||||||
'sensor.evcs_hq2239dnt7p_total_energy',
|
'sensor.evcs_hq2239dnt7p_total_energy',
|
||||||
|
// Jeep lue par Uconnect (04/10/2026)
|
||||||
|
'sensor.jeep_renegade_phev_odometer',
|
||||||
|
'sensor.jeep_renegade_phev_hvbattery_charge',
|
||||||
|
'sensor.jeep_renegade_phev_extrapolated_battery',
|
||||||
];
|
];
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
@@ -2136,6 +2140,8 @@ async function cycle(client) {
|
|||||||
console.error(`releves km en echec : ${e.message}`); return 0;
|
console.error(`releves km en echec : ${e.message}`); return 0;
|
||||||
}) + await ingererNiveauSaisi(sql, st).catch((e) => {
|
}) + await ingererNiveauSaisi(sql, st).catch((e) => {
|
||||||
console.error(`niveau saisi en echec : ${e.message}`); return 0;
|
console.error(`niveau saisi en echec : ${e.message}`); return 0;
|
||||||
|
}) + await ingererJeep(sql, (q, v) => client.query(q, v), st, journaliser).catch((e) => {
|
||||||
|
console.error(`jeep uconnect en echec : ${e.message}`); return 0;
|
||||||
}),
|
}),
|
||||||
pvDiagnostic: DRY ? 0 : await publierDiagnosticPv(st).catch((e) => {
|
pvDiagnostic: DRY ? 0 : await publierDiagnosticPv(st).catch((e) => {
|
||||||
console.error(`diagnostic PV en echec : ${e.message}`); return 0;
|
console.error(`diagnostic PV en echec : ${e.message}`); return 0;
|
||||||
|
|||||||
@@ -245,7 +245,9 @@ const rejetsJournalises = new Set();
|
|||||||
export async function ingererNiveauSaisi(sql, st) {
|
export async function ingererNiveauSaisi(sql, st) {
|
||||||
// L'attribut `timestamp` d'un input_datetime est en secondes epoch : pas de
|
// 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).
|
// fuseau a deviner (l'etat, lui, est une heure locale sans decalage).
|
||||||
const at = Number(st.get(NIVEAU_AT_ENTITY)?.attributes?.timestamp) * 1000;
|
// 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 niveau = Number(st.get(NIVEAU_ENTITY)?.state);
|
||||||
const ref = Number(st.get(NIVEAU_REF_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;
|
if (!Number.isFinite(at) || !Number.isInteger(niveau) || !Number.isFinite(ref) || ref <= 0) return 0;
|
||||||
@@ -254,3 +256,70 @@ export async function ingererNiveauSaisi(sql, st) {
|
|||||||
[new Date(at), niveau, ref]);
|
[new Date(at), niveau, ref]);
|
||||||
return r?.rowCount || 0;
|
return r?.rowCount || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// 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',
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function ingererJeep(sql, lire, st, journaliser) {
|
||||||
|
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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (niveau !== null && st.get(JEEP.branchee)?.state === 'on'
|
||||||
|
&& 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)) {
|
||||||
|
const { rows: deja } = await lire(
|
||||||
|
"SELECT 1 FROM niveau_saisi WHERE source = 'uconnect' AND saisi_a >= $1", [new Date(branche.getTime() - 5 * 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() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user