Memoire de mesures : PostgreSQL et InfluxDB sur le second Synology

Le proprietaire a tranche : on met les deux en service et on decidera a l usage
s il faut garder les deux. Elles ne repondent pas a la meme question.

PostgreSQL garde ce que n8n CALCULE — sorties du bilan, decisions du
controleur, prevision telle qu elle a ete publiee — parce que ce ne sont pas des
capteurs, donc qu aucune statistique Home Assistant ne les retient, et parce que
ce sont des vecteurs, du texte et des jointures. Cinq tables, une vue de confort
qui repond a la question restee sans reponse le 03/09 : ce que le bilan exigeait
contre le SOC reellement atteint, heure par heure.

InfluxDB garde ce que Home Assistant OBSERVE, a la resolution native et sur tout
le perimetre energie, y compris les entites auxquelles personne n a encore
pense. C est exactement ce qui a manque le 03/09, quand les modes des chargeurs
MPPT se sont reveles etre le bon signal alors que personne ne les enregistrait.

Les deux sur 10.10.10.22 : cette machine ne porte qu un service, quand
10.10.10.17 en porte quatre. Le volume n est pas l argument, les deux Synology
ont des teraoctets libres.

L alimentation de PostgreSQL ne passe volontairement PAS par n8n. Tout ce que
n8n calcule, il le publie deja dans des helpers Home Assistant : les lire donne
la meme information sans toucher aux trois workflows de production stabilises
hier soir ni creer d identifiant dans n8n. L horodatage retenu est celui de Home
Assistant et non celui du sondage, si bien qu une valeur inchangee porte la meme
clef primaire et n est pas reecrite : on enregistre quand la grandeur bouge, pas
quand on regarde.

Essai a blanc verifie sur l installation reelle : 15 mesures, 1 ligne de bilan,
1 decision, 2 previsions, avec les bonnes dates. Le mode blanc comptait d abord
les lignes ecrites, donc zero par construction — corrige pour compter ce qu il
ecrirait, sans quoi il ne verifiait rien.

Le branchement de Home Assistant sur InfluxDB demande un redemarrage et n est
donc PAS applique : le fichier de configuration est pret, a poser en milieu de
matinee, jamais entre 01:24 et 07:24.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
Tallagrand Thierry
2026-09-04 01:11:09 +02:00
parent b27c91a3f6
commit 230039d2a2
9 changed files with 817 additions and 2 deletions

View File

@@ -0,0 +1,70 @@
# Mémoire de mesures EVCS
Deux bases sur **10.10.10.22**, décidées le 04/09/2026 : on met les deux en
service et on décide à l'usage s'il faut garder les deux.
| | rôle | source | résolution |
|---|---|---|---|
| **PostgreSQL** | ce que n8n **calcule** — bilan, décisions, prévisions publiées | workflows n8n | cadence des workflows |
| **InfluxDB** | ce que Home Assistant **observe** — tous les états du périmètre énergie | intégration HA | chaque changement d'état |
Le raisonnement complet et le comparatif : `HANDOFF.md` §42 et suivantes.
## Pourquoi pas sur 10.10.10.17
Cette machine porte déjà Home Assistant, n8n, openclaw et open-webui. Une
mémoire hébergée par la machine dont elle documente les pannes ne survit pas à
celles-ci. Le volume n'est pas l'argument : les deux Synology ont des téraoctets
libres.
## Mise en service
### 1. Les deux bases (à faire sur 10.10.10.22)
```sh
cd /volume2/projects/EVCS/EVCS/deploy/historisation
cp historisation.env.exemple historisation.env
# renseigner les trois secrets ; pour le jeton : openssl rand -hex 32
docker compose up -d
docker compose logs -f
```
Vérifications :
```sh
docker compose exec postgres psql -U evcs -d evcs -c '\dt'
curl -s http://10.10.10.22:8086/health
```
Le schéma (`schema.sql`) est appliqué **une seule fois**, à la création de la
base. Pour le faire évoluer ensuite, écrire une migration et la passer à `psql` :
recréer le volume effacerait l'historique, ce qui est précisément ce qu'on
cherche à ne plus faire.
### 2. Brancher Home Assistant sur InfluxDB
⚠️ **Demande un redémarrage de Home Assistant**, pendant lequel le contrôleur
EVCS ne décide plus rien. **Jamais entre 01:24 et 07:24**, ni pendant une charge.
Le bon créneau est le milieu de matinée.
1. mettre le jeton dans `secrets.yaml` sous la clé `influxdb_token` ;
2. coller le bloc de `ha-influxdb.yaml` dans `configuration.yaml` ;
3. `homeassistant.check_config`, puis redémarrer.
### 3. Alimenter PostgreSQL depuis n8n
À faire après l'étape 1 — n8n a besoin que la base existe pour qu'on y crée
l'identifiant de connexion.
## Ce qu'aucune des deux ne fait
Elles ne remontent pas dans le passé. La mémoire commence le jour où on les
branche : c'est un argument pour les mettre en service vite, pas pour les
mettre en service parfaitement.
## Sauvegarde
Les deux bases vivent dans des volumes Docker nommés, donc **hors du dépôt et
hors des sauvegardes Home Assistant**. Une mémoire non sauvegardée est une
mémoire qu'on perdra une fois — voir `HANDOFF.md` §34, qui raconte la même
histoire pour Home Assistant.

View File

@@ -0,0 +1,124 @@
# Mémoire de mesures EVCS — à déployer sur le SECOND Synology (10.10.10.22).
#
# Deux bases, deux rôles distincts, décidés le 04/09/2026 :
#
# postgres (proposition A) — ce que n8n CALCULE : sorties du bilan,
# décisions du contrôleur, prévision telle qu'elle a été publiée.
# Relationnel, donc capable de stocker un vecteur de 24 valeurs,
# une raison en texte, un événement daté — et de les JOINDRE.
#
# influxdb (proposition B) — ce que Home Assistant OBSERVE : chaque
# changement d'état, à la résolution native, y compris les entités
# auxquelles personne n'a encore pensé. C'est ce qui a manqué le
# 03/09, quand les modes des chargeurs MPPT se sont révélés être le
# bon signal alors que personne ne les enregistrait.
#
# ⚠️ NE PAS héberger sur 10.10.10.17 : cette machine porte déjà Home Assistant,
# n8n, openclaw et open-webui. Une mémoire qui meurt avec la machine qu'elle
# documente ne sert à rien (cf. HANDOFF §32).
#
# Mise en service :
# cd /volume2/projects/EVCS/EVCS/deploy/historisation
# cp historisation.env.exemple historisation.env puis y mettre les mots de passe
# docker compose up -d
# docker compose logs -f
#
# Vérification :
# docker compose exec postgres psql -U evcs -d evcs -c '\dt'
# curl -s http://10.10.10.22:8086/health
#
# Volumes NOMMÉS et non bind-mounts : le démon Docker de DSM refuse un
# bind-mount dont le dossier hôte est absent, et une base de données sur un
# partage réseau est une mauvaise idée (verrouillage, latence, fsync).
services:
postgres:
image: postgres:16-alpine
container_name: evcs-postgres
restart: always
network_mode: bridge
ports:
# Ouvert au réseau local : n8n tourne sur 10.10.10.17 et doit écrire ici.
- "5432:5432"
env_file:
- historisation.env
environment:
- TZ=Europe/Paris
- POSTGRES_USER=evcs
- POSTGRES_DB=evcs
# POSTGRES_PASSWORD vient de historisation.env
- PGDATA=/var/lib/postgresql/data/pgdata
volumes:
- evcs-pgdata:/var/lib/postgresql/data
# Appliqué UNE SEULE FOIS, à la création de la base. Pour faire évoluer
# le schéma ensuite, écrire une migration et la passer à psql.
- ./schema.sql:/docker-entrypoint-initdb.d/10-schema.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U evcs -d evcs"]
interval: 1m
timeout: 10s
retries: 5
influxdb:
image: influxdb:2.7-alpine
container_name: evcs-influxdb
restart: always
network_mode: bridge
ports:
# Home Assistant, sur 10.10.10.17, écrit ici.
- "8086:8086"
env_file:
- historisation.env
environment:
- TZ=Europe/Paris
- DOCKER_INFLUXDB_INIT_MODE=setup
- DOCKER_INFLUXDB_INIT_ORG=airtria
- DOCKER_INFLUXDB_INIT_BUCKET=evcs
# Rétention 0 = illimitée sur le seau brut. Le sous-échantillonnage se
# fait par tâches (voir README), pas en jetant la donnée d'entrée.
- DOCKER_INFLUXDB_INIT_RETENTION=0
# DOCKER_INFLUXDB_INIT_USERNAME / _PASSWORD / _ADMIN_TOKEN : env file
volumes:
- evcs-influx-data:/var/lib/influxdb2
- evcs-influx-config:/etc/influxdb2
healthcheck:
test: ["CMD", "influx", "ping"]
interval: 1m
timeout: 10s
retries: 5
# Alimentation de PostgreSQL depuis Home Assistant. Volontairement PAS
# depuis n8n : tout ce que n8n calcule, il le publie deja dans des helpers HA
# — les lire ici donne la meme information sans toucher aux trois workflows
# de production ni creer d'identifiant dans n8n.
historisation:
image: node:22-alpine
container_name: evcs-historisation
restart: always
working_dir: /app
network_mode: bridge
depends_on:
postgres:
condition: service_healthy
volumes:
# Depot en LECTURE SEULE : ce service n'ecrit que dans la base.
- /volume2/projects/EVCS/EVCS:/app:ro
env_file:
- historisation.env
environment:
- TZ=Europe/Paris
- HA_URL=http://10.10.10.17:8123
- PGHOST=10.10.10.22
- PGPORT=5432
- PGUSER=evcs
- PGDATABASE=evcs
# PGPASSWORD doit valoir POSTGRES_PASSWORD : les deux sont dans le
# fichier d'environnement, tenus egaux a la main.
- HISTORISE_INTERVAL_S=60
command: ["node", "tools/historise-evcs.mjs"]
volumes:
evcs-pgdata:
evcs-influx-data:
evcs-influx-config:

View File

@@ -0,0 +1,53 @@
# Bloc à ajouter à /mnt/ARCH3/docker/HA/configuration.yaml pour brancher
# Home Assistant sur InfluxDB (proposition B).
#
# ⚠️ NE PAS APPLIQUER N'IMPORTE QUAND. L'intégration influxdb n'est pas
# rechargeable : elle demande un REDÉMARRAGE de Home Assistant, pendant lequel
# le contrôleur EVCS ne décide plus rien. À faire hors des fenêtres qui
# comptent — donc jamais entre 01:24 et 07:24, ni pendant une charge en cours.
# Le bon créneau est le milieu de matinée, une fois la session réseau de nuit
# terminée et avant que le solaire ne devienne intéressant.
#
# Le jeton est celui de historisation.env (DOCKER_INFLUXDB_INIT_ADMIN_TOKEN).
# Le mettre dans secrets.yaml, PAS ici : configuration.yaml est lisible par
# tout ce qui monte le partage.
# # dans secrets.yaml
# influxdb_token: "…"
influxdb:
api_version: 2
ssl: false
host: 10.10.10.22
port: 8086
token: !secret influxdb_token
organization: airtria
bucket: evcs
# Pourquoi une liste plutôt que « tout » : la maison publie ~1,14 M états par
# jour, dont l'essentiel n'a aucun rapport avec l'énergie. On garde large sur
# le périmètre énergie — c'est justement là qu'on ne sait pas d'avance ce
# dont on aura besoin, comme l'ont montré les modes MPPT le 03/09 — et on
# laisse le reste de côté.
include:
entity_globs:
- sensor.victron_*
- sensor.mppt_*
- sensor.airtria_*
- sensor.gx_device_*
- sensor.sun_solar_elevation
- binary_sensor.victron_*
- switch.edf_*
- switch.gx_device_*
- number.gx_device_*
- input_boolean.victron_evcs_*
- input_boolean.evcs_*
- input_number.victron_evcs_*
- input_number.evcs_*
- input_select.victron_evcs_*
- input_text.victron_evcs_*
- input_text.evcs_*
exclude:
entities:
# Une clé d'API n'a rien à faire dans une base de séries temporelles.
- input_text.evcs_balance_forecast_api_key
tags:
installation: airtria

View File

@@ -0,0 +1,17 @@
# Copier en historisation.env et renseigner. Ce fichier-ci est un exemple
# versionné ; historisation.env, lui, est ignoré par git (secrets).
# --- PostgreSQL (proposition A) ---
POSTGRES_PASSWORD=a_remplacer
# Doit valoir EXACTEMENT le meme mot de passe : c'est celui qu'utilise le
# service d'historisation pour se connecter.
PGPASSWORD=a_remplacer
# Le jeton HA du service d'historisation (meme que celui du depot).
HA_TOKEN=a_remplacer
# --- InfluxDB (proposition B) ---
DOCKER_INFLUXDB_INIT_USERNAME=evcs
DOCKER_INFLUXDB_INIT_PASSWORD=a_remplacer_16_caracteres_minimum
# Ce jeton est celui que Home Assistant utilisera pour écrire.
# Le générer plutôt que l'inventer : openssl rand -hex 32
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=a_remplacer

View File

@@ -0,0 +1,149 @@
-- =====================================================================
-- 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);
-- ---------------------------------------------------------------------
-- 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"}');