Files
EVCS/deploy/historisation/schema.sql
Tallagrand Thierry 9748325356 Le compteur de la borne fait foi, pas mon integration
Le tableau annoncait 750 Wh la ou VRM affichait 8 kWh. Ce n'etait pas une
erreur de mesure mais une erreur de SOURCE : j'integrais la puissance depuis le
moment ou mon detecteur avait commence a fonctionner — 03:16 — pour une session
ouverte a 01:25.

Or la borne tient son propre compteur de session,
`sensor.evcs_hq2239dnt7p_last_session_energy`, qui vaut exactement les 8,0 kWh
de VRM et compte depuis le vrai debut. Nouvelle colonne
`energie_station_kwh` : c'est elle qui alimente desormais les totaux et le
graphique.

Mon integration est conservee en colonne « kWh integres » : l'ecart entre les
deux signale un demarrage ou un redemarrage du service en cours de session, ce
qui est une information en soi. Et elle sert de secours si la borne ne publie
rien.

Cherche au passage, sans succes : VRM affiche « Vehicule : Guest EV » mais
AUCUNE entite ne porte ce champ cote Home Assistant. « Guest EV » signifie
d'ailleurs vehicule NON identifie — Victron ne sait pas non plus qui est
branche.

50/53 : les trois requetes en echec portent sur la nouvelle colonne, que la
migration creera au prochain demarrage du service.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
2026-09-05 03:29:09 +02:00

190 lines
9.4 KiB
SQL

-- =====================================================================
-- Mémoire de mesures EVCS — schéma PostgreSQL (proposition A)
-- Appliqué automatiquement à la création de la base par le compose.
-- =====================================================================
--
-- Ce que cette base garde, et que ni l'historique de Home Assistant ni les
-- statistiques long terme ne savent garder :
--
-- 1. ce que n8n CALCULE — SOC requis, heure de croisement, décisions et
-- leurs motifs. Ce ne sont pas des capteurs, donc aucune statistique.
-- 2. des VECTEURS — une prévision est 24 valeurs pour une journée.
-- 3. du TEXTE — un motif de censure, une raison de décision.
-- 4. des JOINTURES — « la prévision publiée à 04:00 contre la
-- production réelle de 13 h » est ici une requête, ailleurs un exercice.
--
-- Ce qu'elle ne cherche PAS à faire : le flux brut de tous les états. C'est le
-- rôle d'InfluxDB, à côté (voir compose.yaml).
--
-- Convention : tout horodatage est en timestamptz, écrit en UTC, lu en
-- Europe/Paris. Ne jamais stocker d'heure locale nue — le passage à l'heure
-- d'hiver du 25/10 rendrait deux heures indiscernables.
SET timezone = 'Europe/Paris';
-- ---------------------------------------------------------------------
-- 1. MESURES — séries échantillonnées, format long
-- ---------------------------------------------------------------------
-- Format long (une ligne par couple clé/instant) et non large : les grandeurs
-- suivies changent au fil des sessions, et une table large impose une
-- migration à chaque ajout. L'index couvre l'accès réel : « telle clé, sur
-- telle plage de temps ».
CREATE TABLE mesure (
ts timestamptz NOT NULL,
cle text NOT NULL,
valeur double precision NOT NULL,
PRIMARY KEY (cle, ts)
);
COMMENT ON TABLE mesure IS
'Grandeurs numériques échantillonnées par n8n. Cadence = celle du workflow.';
-- ---------------------------------------------------------------------
-- 2. BILAN — une ligne par exécution du bilan énergétique
-- ---------------------------------------------------------------------
CREATE TABLE bilan (
ts timestamptz PRIMARY KEY,
statut text,
soc_pct double precision,
energie_a_couvrir_kwh double precision,
soc_requis_pct double precision,
soc_requis_prudent_pct double precision,
heure_croisement text,
reseau_necessaire boolean,
reseau_energie_kwh double precision,
manque_stockage_kwh double precision,
pv_censure boolean,
motif_censure text,
orage_niveau text,
orage_reserve_kwh double precision,
-- Le reste du bloc tel que publié, pour ne pas avoir à migrer la table
-- chaque fois que le bilan gagne un champ.
brut jsonb
);
COMMENT ON COLUMN bilan.soc_requis_pct IS
'NON écrêté au-delà de 100 : une valeur supérieure est le signal de sous-dimensionnement du stockage (HANDOFF §26).';
-- ---------------------------------------------------------------------
-- 3. DECISION — une ligne par changement de décision du contrôleur
-- ---------------------------------------------------------------------
-- Pas une ligne toutes les 30 s : seulement quand la signature change. Sinon
-- 2 880 lignes par jour dont 2 800 identiques, et la table devient illisible.
CREATE TABLE decision (
ts timestamptz PRIMARY KEY,
profil text,
source_mode text,
etat text,
decision text,
charge_desiree boolean,
courant_a double precision,
soc_pct double precision,
cible_matin_pct double precision,
takeover_reseau boolean,
session_engagee boolean,
raison text,
brut jsonb
);
-- ---------------------------------------------------------------------
-- 4. PREVISION — une ligne par publication, avec le vecteur horaire
-- ---------------------------------------------------------------------
-- `publie_a` est l'instant de publication, `jour` le jour prévu. Les deux sont
-- nécessaires : comparer une prévision à la réalité n'a de sens que si l'on
-- sait QUAND elle a été faite. La clé primaire les porte donc toutes les deux —
-- plusieurs prévisions du même jour, faites à des heures différentes, doivent
-- coexister.
CREATE TABLE prevision (
publie_a timestamptz NOT NULL,
jour date NOT NULL,
source text NOT NULL DEFAULT 'forecast.solar',
total_kwh double precision,
horaire double precision[] NOT NULL, -- 24 valeurs, index 0 = 00 h
PRIMARY KEY (publie_a, jour)
);
COMMENT ON TABLE prevision IS
'Prévision TELLE QU''ELLE A ÉTÉ PUBLIÉE. Sans cet horodatage de publication, on ne peut pas mesurer le biais de la prévision.';
-- ---------------------------------------------------------------------
-- 5. EVENEMENT — ce qu''on veut pouvoir retrouver un an après
-- ---------------------------------------------------------------------
-- Déploiements, incidents, interventions manuelles, changements de matériel.
-- Une courbe sans ses événements ne s'interprète pas : « le rendement a changé
-- le 12 novembre » n'a de sens qu'avec la ligne qui dit qu'un pack a été
-- rebranché ce jour-là.
CREATE TABLE evenement (
ts timestamptz NOT NULL DEFAULT now(),
genre text NOT NULL, -- deploiement | incident | intervention | materiel
source text, -- qui l'a écrit
resume text NOT NULL,
detail jsonb,
id bigserial PRIMARY KEY
);
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
-- Le compteur de la BORNE, qui fait foi : il compte depuis le vrai debut
-- de la session, la ou l'integration ci-dessous ne couvre que ce que le
-- service a vu. Le 05/09 : 8,0 kWh cote borne, 0,75 cote integration,
-- le detecteur n'ayant demarre qu'a 03:16 pour une session ouverte a 01:25.
energie_station_kwh double precision,
energie_kwh double precision NOT NULL DEFAULT 0,
-- Part venue du reseau : on se fie au `source_mode` du controleur, son
-- INTENTION, et non a la puissance reseau instantanee — laquelle peut
-- alimenter la maison pendant que le soleil alimente la voiture.
energie_reseau_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
-- ---------------------------------------------------------------------
CREATE INDEX mesure_ts_idx ON mesure (ts DESC);
CREATE INDEX bilan_ts_idx ON bilan (ts DESC);
CREATE INDEX decision_ts_idx ON decision (ts DESC);
CREATE INDEX prevision_jour_idx ON prevision (jour, publie_a DESC);
-- ---------------------------------------------------------------------
-- Vue de confort : la question qui a bloqué l'analyse du 03/09
-- ---------------------------------------------------------------------
-- « Ce que le bilan exigeait, contre le SOC réellement atteint, heure par
-- heure. » Trois lignes de SQL ici ; impossible sans cette base.
CREATE VIEW bilan_contre_reel AS
SELECT date_trunc('hour', ts) AS heure,
round(avg(soc_requis_pct)::numeric, 1) AS soc_requis,
round(avg(soc_pct)::numeric, 1) AS soc_reel,
round(avg(soc_pct - soc_requis_pct)::numeric, 1) AS marge,
round(avg(energie_a_couvrir_kwh)::numeric, 2) AS a_couvrir_kwh
FROM bilan
GROUP BY 1
ORDER BY 1 DESC;
INSERT INTO evenement (genre, source, resume, detail) VALUES
('deploiement', 'claude',
'Création de la mémoire de mesures : PostgreSQL sur 10.10.10.22',
'{"motif": "historique HA limité à ~20 j et sans valeurs calculées", "handoff": "§42"}');