Files
EVCS/deploy/historisation/schema.sql
Tallagrand Thierry e80dbf70fb 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
2026-09-05 02:54:05 +02:00

181 lines
8.8 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
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
-- ---------------------------------------------------------------------
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"}');