Files
EVCS/deploy/historisation/README.md
Tallagrand Thierry a6d70321a0 evcs_lecture n est pas en lecture seule, contrairement a ce que j ecrivais
Verification des droits reels : SELECT sur toutes les tables, plus INSERT
sur `evenement` — un octroi deliberé et documente plus haut dans ce meme
README (les annotations qu aucun service ne peut deviner s ecrivent a la
main), mais que deux autres passages decrivaient comme « lecture seule ».

Je l ai moi-meme affirme a un pair qui prepare un acces pour un agent
externe. La consequence est concrete : confier ce role a un tiers lui
donnerait l ecriture sur le journal qui alimente les annotations Grafana,
le rapport de nuit et l attribution des transitions — et ses lignes y
seraient indiscernables des notres.

Pour un tiers : un role dedie, SELECT sur les seules tables necessaires.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
2026-09-13 17:21:58 +02:00

345 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Mémoire de mesures EVCS
> ⚠️ **Sur le Synology, `docker` n'est PAS dans le `PATH` d'une session SSH.**
> Il vit dans `/usr/local/bin/docker`, et DSM ne le met dans le chemin que pour
> les sessions interactives de l'interface. Toute commande lancee a distance
> doit donc l'appeler par son chemin complet, sinon elle echoue sur
> `sudo: docker: command not found` — constate le 06/09/2026.
> Depuis un terminal DSM ouvert a la main, `docker compose ...` suffit.
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
bash preparer-env.sh # génère .env avec des secrets aléatoires
/usr/local/bin/docker compose up -d
/usr/local/bin/docker compose logs -f
```
### D'où viennent les secrets
| secret | origine |
|---|---|
| `HA_TOKEN` | **existe déjà** — repris du `.env` à la racine du dépôt |
| `POSTGRES_PASSWORD` | **créé ici** — la base n'existe pas encore, ce fichier décide de son mot de passe |
| `DOCKER_INFLUXDB_INIT_PASSWORD` | **créé ici**, idem (InfluxDB exige au moins 8 caractères) |
| `DOCKER_INFLUXDB_INIT_ADMIN_TOKEN` | **créé ici** — c'est le jeton que Home Assistant utilisera pour écrire |
Aucun ne s'obtient auprès de quelqu'un, et aucun ne doit être choisi à la main :
personne ne les tape jamais — les conteneurs les lisent dans `.env`, et Home
Assistant lit le jeton dans son propre `secrets.yaml`. Une phrase inventée est
plus courte et plus devinable qu'elle n'en a l'air ; `openssl rand` ne l'est pas.
`preparer-env.sh` refuse d'écraser un `.env` existant : régénérer les mots de
passe d'une base déjà créée la rendrait inaccessible, puisqu'elle continuerait
d'attendre les anciens.
Vérifications :
```sh
/usr/local/bin/docker compose exec postgres psql -U evcs -d evcs -c '\dt'
curl -s http://10.10.10.22:8086/health
/usr/local/bin/docker compose logs historisation --tail 5
```
### Ports : 5433, et pas 5432
DSM fait tourner **son propre PostgreSQL sur 5432**, pour ses services internes.
Notre base est donc publiée sur **5433**. Ce port ne sert qu'à l'analyse depuis
un poste de travail :
```sh
psql -h 10.10.10.22 -p 5433 -U evcs -d evcs
```
Les trois conteneurs, eux, se parlent par le réseau interne du compose et par
nom de service (`postgres:5432`) — ni adresse IP en dur, ni dépendance au port
publié. Vérifier ce qui occupe un port avant de conclure :
```sh
sudo ss -lntp | grep -E '5432|5433|8086'
```
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.
## Événements : détectés par le service, annotés à la main
La table `evenement` existe parce qu'une courbe sans ses événements ne
s'interprète pas : « le rendement a changé le 12 novembre » ne veut rien dire
sans la ligne qui dit qu'un pack a été rebranché ce jour-là.
**Le service consigne seul ce qu'il peut constater** — il voit passer les états
toutes les minutes, autant qu'il en tire les conclusions plutôt que d'attendre
que quelqu'un y pense :
| genre | ce qui est consigné |
|---|---|
| `systeme` | démarrage du service, redémarrage de Home Assistant, épisode d'indisponibilité avec sa durée |
| `reseau` | contacteur EDF, session réseau de nuit, reprise nuit, verrou hystérésis |
| `vehicule` | véhicule branché ou débranché |
| `intervention` | changement de stratégie ou de véhicule sélectionné |
| `ess` | état BatteryLife, mode de l'Auto Charge Manager |
| `bilan` | réseau nécessaire, vigilance orage, production bridée |
On suit des états **discrets** dont chaque changement mérite une ligne. Les
grandeurs continues sont dans `mesure`, les décisions du contrôleur dans
`decision` : un événement qui double une autre table est du bruit. Une lecture
`unavailable` n'est pas une transition mais une absence de lecture — elle n'est
pas consignée.
Le redémarrage de Home Assistant est detecté **sans capteur d'uptime**, qui
n'existe pas sur cette installation : au redémarrage HA restaure ses helpers et
leur pose un `last_changed` neuf. Le 04/09, 352 entités partageaient la seconde
du redémarrage, dont 183 `input_number`. Le service prend donc le **mode** des
`last_changed` des `input_number` — aucune entité codée en dur, et l'estimation
se corrige d'elle-même.
**Les annotations qu'aucun service ne peut deviner** — « pack rebranché »,
« correctif déployé » — s'écrivent à la main. Pour cela le rôle d'analyse a
besoin d'un `INSERT` sur cette seule table :
```sh
sudo /usr/local/bin/docker compose exec -T postgres psql -U evcs -d evcs -c \
"GRANT INSERT ON evenement TO evcs_lecture;
GRANT USAGE, SELECT ON SEQUENCE evenement_id_seq TO evcs_lecture;"
```
`INSERT` seulement : ni `UPDATE`, ni `DELETE`. On peut ajouter une annotation,
jamais réécrire ni effacer les précédentes — y compris les siennes. C'est ce
qu'on attend d'un journal, et ça laisse les tables de mesure hors de portée.
## Accès en lecture pour l'analyse
```sh
sudo bash creer-role-lecture.sh
```
Crée le rôle `evcs_lecture` et dépose son mot de passe dans
le `.env` à la racine du dépôt. Le mot de passe est généré, jamais choisi, et
**n'est pas affiché** : il va du générateur au fichier, donc rien à recopier et
rien à exposer dans une capture.
Deux raisons de ne pas analyser avec le compte `evcs` : une mémoire dont tout
l'intérêt est de ne pas être réécrite ne se consulte pas avec un compte qui peut
écrire, et un mot de passe rangé dans un fichier lisible ne doit pas être celui
qui gouverne la base.
Le script est rejouable : si le rôle existe déjà et que son mot de passe est
dans le `.env`, il ne fait rien.
## 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.
---
## Consultation — Grafana (ajouté le 04/09/2026)
### Ce qu'il y a à consulter, et où
| Base | Où | Ce qu'elle garde | Consultable dans Grafana |
|---|---|---|---|
| **PostgreSQL `evcs`** | 10.10.10.22:5433 | ce que le système **calcule** : bilan, décisions, prévision publiée, événements | oui |
| **InfluxDB `evcs`** | 10.10.10.22:8086 | ce que HA **observe**, ~1 point/s, 113 grandeurs | oui |
| SQLite de Home Assistant | 10.10.10.17 | son historique brut (20 j) et ses statistiques long terme (186 j) | **non — volontairement** |
Le PostgreSQL du port **5432** appartient à DSM et à ses services internes.
Ce n'est pas une base du projet ; ne pas s'y connecter.
L'historique de Home Assistant reste consulté **dans Home Assistant**. L'y
brancher demanderait de lire un fichier SQLite de 4,4 Go en mode WAL, à travers
un partage réseau, pendant que HA écrit dedans : c'est la meilleure façon de
lire une base incohérente, et le risque n'est pas payé en retour puisque tout
ce qui compte pour l'avenir arrive désormais dans InfluxDB à une résolution
bien meilleure.
### Mise en service
```bash
cd /volume2/projects/EVCS/EVCS/deploy/historisation
bash preparer-grafana.sh # génère le mot de passe admin, l'affiche une fois
sudo /usr/local/bin/docker compose up -d grafana
sudo /usr/local/bin/docker compose logs -f grafana
```
⚠️ **`sudo` est obligatoire sur DSM** : l'utilisateur ordinaire n'a pas accès
au socket Docker (`permission denied while trying to connect to the Docker
daemon socket`).
⚠️ **Aucun `git pull` n'est nécessaire.** `/volume2/projects` sur le Syno et
`/mnt/projects` sur le poste de travail sont **le même dossier** (export NFS
depuis 10.10.10.22) : il n'existe qu'une seule copie du dépôt, et un fichier
écrit depuis le poste est déjà là côté Syno.
Puis **http://10.10.10.22:3001** — port 3001 et non 3000, occupé sur cette
machine par autre chose.
Mot de passe perdu :
`grep GRAFANA_ADMIN_PASSWORD deploy/historisation/.env`
### Les trois tableaux de bord
- **La nuit** — le SOC réel contre le SOC exigé par le bilan, la marge entre
les deux, la session réseau, et ce que la nuit a coûté. C'est la question qui
avait demandé une nuit d'archéologie le 03/09.
- **Le solaire** — production, maison, et surtout **l'excédent** (la grandeur
qui décide d'un démarrage depuis le 03/09), avec la censure du MPPT et le
mode déclaré par chacun des sept chargeurs.
- **Prévision contre réel** — le biais de forme de la prévision, question
ouverte qui demande des semaines de données.
- **Le système** — réplique locale de la vue générale VRM : réseau, charges,
batterie, borne, production, **détail par tracker**, les flux dans le temps
et l'énergie heure par heure. Existe parce que la console Victron est
hébergée dans le cloud et disparaît quand une coupure d'orage emporte
Internet. Tout y est lu sur le réseau local.
⚠️ Les énergies sont obtenues **en intégrant la puissance**, jamais en lisant
les compteurs kWh du GX : `gx_device_pv_energy` et les compteurs d'énergie
batterie se remettent à zéro sans le signaler, et Home Assistant enregistre
alors une variation de −373 kWh sur une heure (vérifié le 04/09). Les
compteurs `*_total_yield` des chargeurs et ceux du MultiPlus, eux, sont
monotones et sains.
- **Sur le téléphone** — une seule colonne, six panneaux, et la réponse en
premier. À épingler sur l'écran d'accueil de l'iPhone :
`http://10.10.10.22:3001/d/evcs-telephone?kiosk` (Safari → Partager → Sur
l'écran d'accueil). `?kiosk` retire les menus.
⚠️ Contrainte de conception à connaître avant d'y ajouter quoi que ce soit :
sous ~770 px de large, Grafana passe **chaque** panneau en pleine largeur et
les empile. Le nombre de panneaux est donc le nombre d'écrans à faire
défiler — d'où les chiffres regroupés dans deux panneaux « stat » plutôt que
répartis en six.
Les événements de la table `evenement` apparaissent en **marqueurs verticaux
orange sur tous les graphiques** : une courbe sans ses événements ne
s'interprète pas.
### Modifier un tableau de bord
Ils viennent du dépôt git, comme le code des nodes n8n — et pour la même
raison : une modification faite dans l'interface serait écrasée au prochain
démarrage, en silence. Le générateur est `tools/gen-grafana-dashboards.py` :
```bash
python3 tools/gen-grafana-dashboards.py # réécrit les trois JSON
node tools/verifier-dashboards.mjs # rejoue CHAQUE requête pour de vrai
```
`verifier-dashboards.mjs` exécute les requêtes SQL contre PostgreSQL et les
requêtes Flux contre InfluxDB, macros de Grafana substituées. Sans lui, une
requête cassée ne se voit qu'à l'écran, panneau par panneau. Sortie non nulle
si une seule échoue.
Grafana relit le dossier toutes les 30 s : pas besoin de redémarrer.
Pour explorer librement sans rien casser : **« Save as… »** depuis n'importe
quel tableau de bord en crée une copie personnelle et modifiable.
### Ce que Grafana ne peut pas faire
Il ne **stocke** rien : il interroge les deux bases à la demande. Le perdre ne
perd aucune mesure. Il n'écrit rien non plus — sa connexion PostgreSQL utilise
le rôle `evcs_lecture`.
⚠️ **`evcs_lecture` N'EST PLUS EN LECTURE SEULE**, et ce README l'a longtemps
écrit à tort. Il porte un `INSERT` sur `evenement` (voir plus haut, annotations
manuelles) : SELECT partout, plus INSERT sur cette seule table. Vérifiable :
```sh
psql -U evcs_lecture -d evcs -c "select table_name, privilege_type
from information_schema.table_privileges where grantee='evcs_lecture'"
```
Conséquence pratique : **ne pas confier ce rôle à un tiers** — un agent, un
service externe, un tableau de bord hébergé ailleurs. Le journal `evenement`
alimente les annotations Grafana, le rapport de nuit et l'attribution des
transitions ; une écriture venue d'ailleurs y serait indiscernable d'une nôtre.
Pour un tiers, créer un rôle dédié avec `SELECT` sur les seules tables dont il
a besoin.
### ⚠️ Ce que `--dry-run` ne peut pas vérifier
L'essai à blanc remplace l'envoi à la base par un compteur : il valide la
logique, jamais le SQL. Une erreur de typage passe donc inaperçue.
C'est arrivé le 05/09 : `energie_kwh + $3 * $4 / 3600000.0` échouait sur
`operator is not unique: unknown * unknown`, parce que node-postgres envoie ses
paramètres **sans type** et que PostgreSQL ne sait alors pas quel opérateur `*`
choisir. Aucun `UPDATE` de session n'a abouti pendant vingt minutes, et cela ne
s'est vu qu'au journal du conteneur.
**Règle : tout paramètre entrant dans un calcul porte son `::float8`.**
**Et après tout déploiement du service :**
```bash
sudo /usr/local/bin/docker compose restart historisation
sudo /usr/local/bin/docker compose logs --since 2m historisation
```
⚠️ **`--since`, pas `--tail`.** Le service ne journalise que lorsqu'il a écrit
quelque chose : `--tail=5` affiche donc des lignes qui peuvent dater de
plusieurs minutes, y compris des erreurs déjà corrigées. On a perdu dix minutes
là-dessus le 05/09 — le service était réparé, les erreurs affichées étaient
antérieures au redémarrage.
Une ligne `cycle en echec` **postérieure au redémarrage** vaut arrêt immédiat.
Une ligne `mesures=… session=…` vaut confirmation.
Et la vérification qui ne ment pas, puisqu'elle regarde le résultat et non le
journal :
```sql
SELECT to_char(fin,'HH24:MI:SS'),
round(extract(epoch FROM now()-fin)) AS "il y a (s)",
round(energie_kwh::numeric,3)
FROM session_charge ORDER BY debut DESC LIMIT 1;
```