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