Table des sessions de charge : etiqueter pendant, pas deviner apres
L'etiquette du vehicule n'existe QUE pendant la session. `input_select.victron_evcs_vehicle` est un reglage manuel, il change, et rien ne conservait sa valeur au moment ou la charge avait lieu — une session d'il y a trois semaines etait anonyme pour toujours, et toute analyse d'usage impossible. Nouvelle table `session_charge` : debut, fin, vehicule tel que le selecteur l'indiquait PENDANT la session, un drapeau disant s'il a change en cours de route, energie accumulee, puissance et courant maximum, mode source, et l'heure de debranchement — qui confirme apres coup, les deux vehicules partant a des heures differentes. Le service applique lui-meme la migration au demarrage : `schema.sql` n'est joue qu'a la creation de la base, une table ajoutee plus tard n'y arriverait jamais. `CREATE TABLE IF NOT EXISTS` rend l'operation rejouable. On ne DEVINE rien, et le commentaire du schema dit pourquoi : - electriquement, la VW accepte 3 680 W et la JEEP 3 655 W a consigne identique de 20 A — 0,1 A d'ecart, indiscernables ; - horairement, la routine s'inverse le week-end (la femme du proprietaire travaille samedi et dimanche matin), donc « midi = JEEP, nuit = VW » serait faux un week-end sur deux. Essai a blanc : la session en cours serait correctement inscrite (JEEP, 3396 W, Reseau force, profil night_hc). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
@@ -120,6 +120,37 @@ CREATE TABLE evenement (
|
||||
);
|
||||
CREATE INDEX evenement_ts_idx ON evenement (ts DESC);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 6. SESSION_CHARGE — une ligne par charge de vehicule
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Raison d'etre : l'etiquette du vehicule n'existe QUE pendant la session.
|
||||
-- `input_select.victron_evcs_vehicle` est un reglage manuel, il change, et
|
||||
-- rien ne conserve sa valeur au moment ou la charge a eu lieu. Sans cet
|
||||
-- enregistrement, une session d'il y a trois semaines est anonyme pour
|
||||
-- toujours — et toute analyse d'usage devient impossible.
|
||||
--
|
||||
-- ⚠️ On ne DEVINE pas le vehicule. Mesure du 05/09/2026 : a consigne identique
|
||||
-- de 20 A, la VW accepte 3 680 W et la JEEP 3 655 W, soit 0,1 A d'ecart. Ils
|
||||
-- sont electriquement indiscernables. L'heure ne dit rien non plus : en semaine
|
||||
-- la JEEP charge le midi et la VW la nuit, le week-end c'est l'inverse.
|
||||
CREATE TABLE session_charge (
|
||||
debut timestamptz PRIMARY KEY,
|
||||
fin timestamptz NOT NULL,
|
||||
terminee boolean NOT NULL DEFAULT false,
|
||||
vehicule text, -- valeur du selecteur PENDANT la session
|
||||
vehicule_sur boolean NOT NULL DEFAULT true, -- le selecteur n'a pas change
|
||||
energie_kwh double precision NOT NULL DEFAULT 0,
|
||||
p_max_w double precision,
|
||||
courant_max_a double precision,
|
||||
consigne_max_a double precision,
|
||||
source_mode text,
|
||||
debranche_a timestamptz, -- confirme le vehicule apres coup
|
||||
brut jsonb
|
||||
);
|
||||
COMMENT ON COLUMN session_charge.debranche_a IS
|
||||
'Heure de debranchement : les deux vehicules partent a des heures differentes, ce qui confirme l''etiquette apres coup.';
|
||||
CREATE INDEX session_charge_debut_idx ON session_charge (debut DESC);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Index de lecture
|
||||
-- ---------------------------------------------------------------------
|
||||
|
||||
@@ -101,9 +101,16 @@ Deux véhicules alternent : **VW** (hybride, départ ~08:50) et **JEEP**
|
||||
| **VW** | son fils, parti vers 9 h, rentre vers 19 h | **le soir et la nuit** |
|
||||
|
||||
Exemples donnes : lundi midi la JEEP · mardi midi la JEEP puis la VW la nuit ·
|
||||
mercredi midi la JEEP. La regularite souffre des exceptions — un samedi, la
|
||||
JEEP peut charger la nuit — donc elle vaut comme **forte presomption, jamais
|
||||
comme certitude**.
|
||||
mercredi midi la JEEP.
|
||||
|
||||
**LE WEEK-END, LA ROUTINE S'INVERSE.** Sa femme travaille le samedi matin et le
|
||||
dimanche matin, retour a midi : la **JEEP** charge donc le midi ET la nuit du
|
||||
samedi au dimanche. La **VW** ne bouge pas du week-end — sauf sortie plongee —
|
||||
et se charge **en journee**, pendant que la JEEP est au travail.
|
||||
|
||||
⚠️ **Aucune regle horaire ne permet donc d'identifier le vehicule.** Une
|
||||
heuristique « midi = JEEP, nuit = VW » serait fausse un week-end sur deux. Ne
|
||||
pas en ecrire une.
|
||||
|
||||
⚠️ **Les deux vehicules sont ELECTRIQUEMENT INDISCERNABLES.** Mesure le 05/09
|
||||
sur deux sessions a consigne identique de 20 A : la VW accepte 3 680 W et la
|
||||
|
||||
@@ -328,6 +328,130 @@ async function detecterEvenements(sql, st) {
|
||||
return n;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Le service fait evoluer son propre schema. `schema.sql` n'est applique qu'a
|
||||
// la CREATION de la base : une table ajoutee plus tard n'y arriverait jamais.
|
||||
// `IF NOT EXISTS` rend l'operation rejouable a chaque demarrage.
|
||||
// ---------------------------------------------------------------------
|
||||
async function migrer(client) {
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS session_charge (
|
||||
debut timestamptz PRIMARY KEY,
|
||||
fin timestamptz NOT NULL,
|
||||
terminee boolean NOT NULL DEFAULT false,
|
||||
vehicule text,
|
||||
vehicule_sur boolean NOT NULL DEFAULT true,
|
||||
energie_kwh double precision NOT NULL DEFAULT 0,
|
||||
p_max_w double precision,
|
||||
courant_max_a double precision,
|
||||
consigne_max_a double precision,
|
||||
source_mode text,
|
||||
debranche_a timestamptz,
|
||||
brut jsonb
|
||||
)`);
|
||||
await client.query(
|
||||
'CREATE INDEX IF NOT EXISTS session_charge_debut_idx ON session_charge (debut DESC)');
|
||||
// Le role de lecture doit voir la nouvelle table. S'il n'existe pas encore,
|
||||
// on ne fait pas echouer le demarrage pour autant.
|
||||
await client.query(`DO $$ BEGIN
|
||||
GRANT SELECT ON session_charge TO evcs_lecture;
|
||||
EXCEPTION WHEN undefined_object THEN NULL; END $$`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Sessions de charge.
|
||||
//
|
||||
// Pourquoi une table plutot qu'un calcul a la demande : l'etiquette du
|
||||
// vehicule n'existe QUE pendant la session. `input_select.victron_evcs_vehicle`
|
||||
// est un reglage manuel, il change, et rien ne conserve sa valeur au moment ou
|
||||
// la charge a eu lieu. Sans cet enregistrement, une session d'il y a trois
|
||||
// semaines est anonyme pour toujours.
|
||||
//
|
||||
// ⚠️ On n'essaie PAS de deviner le vehicule. Mesure du 05/09 : a consigne
|
||||
// identique de 20 A, la VW accepte 3 680 W et la JEEP 3 655 W — 0,1 A d'ecart.
|
||||
// Ils sont electriquement indiscernables. Et l'heure ne dit rien non plus : en
|
||||
// semaine la JEEP charge le midi et la VW la nuit, mais le week-end c'est
|
||||
// l'inverse. On enregistre ce que le selecteur disait, et on note s'il a change
|
||||
// en cours de route.
|
||||
// ---------------------------------------------------------------------
|
||||
const SEUIL_CHARGE_W = 300; // au-dela, la voiture prend vraiment du courant
|
||||
const FIN_APRES_MIN = 10; // sous le seuil pendant ce temps : session close
|
||||
|
||||
async function ecrireSession(sql, client, st) {
|
||||
// En blanc la base n'est pas connectee : on lit du vide plutot que d'echouer.
|
||||
// La logique se deroule alors comme si aucune session n'etait ouverte — ce
|
||||
// qui suffit a verifier le chemin d'ouverture, pas celui de cloture.
|
||||
const lire = DRY ? async () => ({ rows: [], rowCount: 0 }) : (q, v) => client.query(q, v);
|
||||
const g = (id) => st.get(id);
|
||||
const p = num(g('sensor.victron_evcs_ac_power')?.state) ?? 0;
|
||||
const connecte = txt(g('binary_sensor.victron_evcs_vehicle_connected')?.state) === 'on';
|
||||
const vehicule = txt(g('input_select.victron_evcs_vehicle')?.state) || null;
|
||||
const maintenant = new Date();
|
||||
let n = 0;
|
||||
|
||||
const { rows } = await lire(
|
||||
'SELECT * FROM session_charge WHERE NOT terminee ORDER BY debut DESC LIMIT 1');
|
||||
const ouverte = rows[0] ?? null;
|
||||
|
||||
if (p > SEUIL_CHARGE_W) {
|
||||
if (!ouverte) {
|
||||
await sql(
|
||||
`INSERT INTO session_charge (debut, fin, vehicule, energie_kwh, p_max_w,
|
||||
courant_max_a, consigne_max_a, source_mode, brut)
|
||||
VALUES ($1,$1,$2,0,$3,$4,$5,$6,$7) ON CONFLICT (debut) DO NOTHING`,
|
||||
[maintenant, vehicule, p,
|
||||
num(g('sensor.victron_evcs_current')?.state),
|
||||
num(g('sensor.victron_evcs_set_current')?.state),
|
||||
txt(g('input_text.victron_evcs_source_mode')?.state) || null,
|
||||
JSON.stringify({ profil: txt(g('input_text.victron_evcs_operating_mode')?.state) })]);
|
||||
n++;
|
||||
} else {
|
||||
// Le pas de temps est plafonne : un arret du service ne doit pas
|
||||
// extrapoler la derniere puissance connue sur plusieurs heures.
|
||||
const dt = Math.min((maintenant - new Date(ouverte.fin)) / 1000, 300);
|
||||
await sql(
|
||||
`UPDATE session_charge SET
|
||||
fin = $2,
|
||||
energie_kwh = energie_kwh + $3 * $4 / 3600000.0,
|
||||
p_max_w = GREATEST(coalesce(p_max_w, 0), $3),
|
||||
courant_max_a = GREATEST(coalesce(courant_max_a, 0), coalesce($5, 0)),
|
||||
consigne_max_a = GREATEST(coalesce(consigne_max_a, 0), coalesce($6, 0)),
|
||||
vehicule = coalesce($7, vehicule),
|
||||
vehicule_sur = vehicule_sur AND ($7 IS NULL OR vehicule IS NULL OR vehicule = $7)
|
||||
WHERE debut = $1`,
|
||||
[ouverte.debut, maintenant, p, dt,
|
||||
num(g('sensor.victron_evcs_current')?.state),
|
||||
num(g('sensor.victron_evcs_set_current')?.state), vehicule]);
|
||||
n++;
|
||||
}
|
||||
} else if (ouverte) {
|
||||
const silence = (maintenant - new Date(ouverte.fin)) / 60000;
|
||||
if (silence >= FIN_APRES_MIN || !connecte) {
|
||||
await sql('UPDATE session_charge SET terminee = true WHERE debut = $1', [ouverte.debut]);
|
||||
await journaliser(sql, 'vehicule',
|
||||
`Session de charge terminee : ${ouverte.vehicule ?? 'vehicule inconnu'}, ` +
|
||||
`${Number(ouverte.energie_kwh).toFixed(2)} kWh`,
|
||||
{ debut: ouverte.debut, vehicule: ouverte.vehicule, kwh: ouverte.energie_kwh });
|
||||
n += 2;
|
||||
}
|
||||
}
|
||||
|
||||
// L'heure de debranchement, notee sur la derniere session. Elle confirme le
|
||||
// vehicule apres coup : les deux partent a des heures differentes.
|
||||
if (!connecte) {
|
||||
const { rowCount } = await lire(
|
||||
`SELECT 1 FROM session_charge
|
||||
WHERE debranche_a IS NULL AND fin > now() - interval '18 hours' LIMIT 1`);
|
||||
if (rowCount) {
|
||||
await sql(
|
||||
`UPDATE session_charge SET debranche_a = $1
|
||||
WHERE debranche_a IS NULL AND fin > now() - interval '18 hours'`, [maintenant]);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
async function cycle(client) {
|
||||
// En blanc, on compte ce qu'on ECRIRAIT. Un essai a blanc qui renvoie
|
||||
@@ -347,14 +471,15 @@ async function cycle(client) {
|
||||
bilan: await ecrireBilan(sql, st),
|
||||
decision: await ecrireDecision(sql, st),
|
||||
prevision: await ecrirePrevision(sql, st),
|
||||
session: await ecrireSession(sql, client, st),
|
||||
evenement: await detecterEvenements(sql, st),
|
||||
};
|
||||
const total = r.mesures + r.bilan + r.decision + r.prevision + r.evenement;
|
||||
const total = r.mesures + r.bilan + r.decision + r.prevision + r.evenement + r.session;
|
||||
if (total > 0 || ONCE) {
|
||||
console.log(
|
||||
`${new Date().toLocaleString('fr-FR', { timeZone: 'Europe/Paris' })} ` +
|
||||
`${DRY ? '[BLANC] ' : ''}mesures=${r.mesures} bilan=${r.bilan} ` +
|
||||
`decision=${r.decision} prevision=${r.prevision} evenement=${r.evenement}`
|
||||
`decision=${r.decision} prevision=${r.prevision} session=${r.session} evenement=${r.evenement}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -367,7 +492,10 @@ const client = new pg.Client({
|
||||
password: process.env.PGPASSWORD,
|
||||
});
|
||||
|
||||
if (!DRY) await client.connect();
|
||||
if (!DRY) {
|
||||
await client.connect();
|
||||
await migrer(client); // schema a jour avant le premier cycle
|
||||
}
|
||||
console.log(
|
||||
`Historisation EVCS — HA=${HA_URL} base=${DRY ? '(blanc)' : client.host + ':' + client.port} ` +
|
||||
`intervalle=${INTERVAL_S}s`
|
||||
|
||||
Reference in New Issue
Block a user