Grafana : les deux bases se consultent, et les requetes sont verifiees
Trois tableaux de bord provisionnes depuis git, sur le modele de la regle R2
(le depot est la source de verite, l'interface ne peut pas les ecraser) :
La nuit SOC reel contre SOC exige par le bilan, et la marge entre les
deux. C'est la question qui avait demande une nuit d'archeologie
le 03/09.
Le solaire l'excedent (§36), la censure, et le mode declare par chacun des
sept chargeurs (§37) — le signal le plus direct dont on dispose.
Prevision l'ecart heure par heure, avec la bande de censure dessous : une
production sous la prevision peut vouloir dire mauvaise
prevision OU absence de preneur.
Les evenements de la table `evenement` deviennent des marqueurs verticaux sur
tous les graphiques — une courbe sans ses evenements ne s'interprete pas.
tools/verifier-dashboards.mjs rejoue CHAQUE requete pour de vrai (SQL contre
PostgreSQL, Flux contre InfluxDB, macros Grafana substituees), annotations
comprises puisqu'elles s'executent sur tous les panneaux. Sans lui une requete
cassee ne se voit qu'a l'ecran, panneau par panneau. 19/19 au deploiement.
Port 3001 et non 3000 : sonde avant d'ecrire la ligne, quelque chose ecoute
deja sur 3000 — la lecon du 5432 deja pris par le PostgreSQL de DSM.
Connexion PostgreSQL par le role evcs_lecture : un tableau de bord ne doit pas
pouvoir ecrire dans la memoire. Aucun secret duplique, aucun secret commite :
le provisionnement les reference par $VARIABLE depuis les deux .env.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
89
HANDOFF.md
89
HANDOFF.md
@@ -3438,3 +3438,92 @@ La premiere est plus propre — le role d'analyse reste strictement lecteur.
|
|||||||
|
|
||||||
---
|
---
|
||||||
*Document mis a jour le : 04/09/2026 (~07:35) — InfluxDB en service, 249 entites, memoire complete*
|
*Document mis a jour le : 04/09/2026 (~07:35) — InfluxDB en service, 249 entites, memoire complete*
|
||||||
|
|
||||||
|
## 45. Session du 04/09/2026 — Grafana : les deux bases se consultent
|
||||||
|
|
||||||
|
### Ce qu'il y a a consulter, et ce qu'il n'y a pas
|
||||||
|
Trois bases coexistent, et elles ne jouent pas le meme role :
|
||||||
|
|
||||||
|
| base | ou | contenu |
|
||||||
|
|---|---|---|
|
||||||
|
| PostgreSQL `evcs` | 10.10.10.22:**5433** | ce que le systeme CALCULE |
|
||||||
|
| InfluxDB `evcs` | 10.10.10.22:8086 | ce que HA OBSERVE, ~1 pt/s, 113 grandeurs |
|
||||||
|
| SQLite de HA | 10.10.10.17 | son historique brut (20 j) + statistiques (186 j) |
|
||||||
|
|
||||||
|
⚠️ Le PostgreSQL du port **5432** appartient a DSM. Ce n'est pas une base du
|
||||||
|
projet — la confusion « j'ai deux bases postgres » vient de la.
|
||||||
|
|
||||||
|
**La SQLite de HA n'est PAS branchee a Grafana, et c'est deliberé** : il
|
||||||
|
faudrait lire un fichier de 4,4 Go en mode WAL a travers un partage reseau
|
||||||
|
pendant que HA ecrit dedans (lecture incoherente garantie), pour un gain nul —
|
||||||
|
tout ce qui compte pour l'avenir arrive desormais dans InfluxDB a bien meilleure
|
||||||
|
resolution. L'historique de HA se consulte dans HA.
|
||||||
|
|
||||||
|
### Deploiement
|
||||||
|
Service `grafana` ajoute a `deploy/historisation/compose.yaml`, image
|
||||||
|
`grafana-oss:11.3.0`, publie sur **3001**.
|
||||||
|
⚠️ **3000 est deja occupe sur 10.10.10.22** — verifie par sonde AVANT d'ecrire
|
||||||
|
la ligne, exactement la lecon du 5432 deja pris par le PostgreSQL de DSM.
|
||||||
|
Telemetrie, verification de version et flux d'actualites desactives : cette
|
||||||
|
installation doit fonctionner pendant une coupure Internet d'orage (§32).
|
||||||
|
|
||||||
|
`bash preparer-grafana.sh` genere le mot de passe admin (genere, pas choisi),
|
||||||
|
l'affiche une fois, le laisse relisible dans le `.env` 600.
|
||||||
|
|
||||||
|
**Aucun secret n'est duplique** : le compose donne DEUX `env_file` a Grafana —
|
||||||
|
`.env` de la pile (mot de passe admin) et `../../.env` du depot
|
||||||
|
(`PG_LECTURE_PASSWORD`, `INFLUXDB_TOKEN`). Le provisionnement les reference par
|
||||||
|
`$VARIABLE`, donc les fichiers YAML de sources de donnees sont commitables tels
|
||||||
|
quels. C'etait le piege du POSTGRES_PASSWORD/PGPASSWORD tenus egaux a la main.
|
||||||
|
|
||||||
|
Connexion PostgreSQL par le role **`evcs_lecture`** : un tableau de bord ne doit
|
||||||
|
pas pouvoir ecrire dans la memoire.
|
||||||
|
|
||||||
|
### Provisionnement = git, comme les nodes n8n (regle R2)
|
||||||
|
`grafana/provisioning/` (sources + fournisseur) et `grafana/dashboards/`
|
||||||
|
(les trois JSON) sont montes en LECTURE SEULE, `allowUiUpdates: false`. Une
|
||||||
|
modification faite dans l'interface serait ecrasee au redemarrage, en silence —
|
||||||
|
meme raison que « JAMAIS editer l'UI n8n ». Le generateur rejouable est
|
||||||
|
`tools/gen-grafana-dashboards.py` ; Grafana relit le dossier toutes les 30 s.
|
||||||
|
« Save as… » reste libre pour explorer (copie personnelle dans General).
|
||||||
|
|
||||||
|
### `tools/verifier-dashboards.mjs` — l'equivalent des golden pour les tableaux
|
||||||
|
Rejoue **chaque** requete : SQL contre PostgreSQL, Flux contre InfluxDB, avec
|
||||||
|
les macros de Grafana (`$__timeFilter`, `$__timeFrom/To`, `v.timeRangeStart`,
|
||||||
|
`v.windowPeriod`) substituees comme il le fait. **Les annotations sont testees
|
||||||
|
aussi** : elles s'executent sur tous les panneaux, donc une annotation cassee
|
||||||
|
casse le tableau entier. Sortie non nulle au premier echec.
|
||||||
|
Sans lui, une requete cassee ne se voit qu'a l'ecran, panneau par panneau.
|
||||||
|
Etat au deploiement : **19/19**.
|
||||||
|
|
||||||
|
### Structure InfluxDB (relevee, a ne pas redecouvrir)
|
||||||
|
L'integration `influxdb` de HA n'ecrit PAS une mesure par entite :
|
||||||
|
- `_measurement` = **l'unite** (`W`, `A`, `V`, `%`, `kWh`, `°C`, `min`, `s`, `VA`, `Hz`, `°`)
|
||||||
|
pour tout capteur numerique ; tags `entity_id` (**sans le prefixe de domaine**),
|
||||||
|
`domain`, `installation` ; champ numerique `value`.
|
||||||
|
- `_measurement` = **l'entity_id complet** (`sensor.…`, `switch.…`) pour les
|
||||||
|
entites textuelles ; champ `state` = la chaine, `value` = 0/1 pour un switch.
|
||||||
|
|
||||||
|
Donc : `r._measurement == "W" and r.entity_id == "victron_pv_power_system" and r._field == "value"`,
|
||||||
|
et pour les modes des chargeurs `r._measurement =~ /operation_mode$/ and r._field == "state"`.
|
||||||
|
Les sept `*_operation_mode` (signal §37) et `switch.edf_general_ok_switch` sont
|
||||||
|
bien presents.
|
||||||
|
|
||||||
|
### Les trois tableaux
|
||||||
|
- **La nuit** (`evcs-nuit`) — SOC reel contre SOC exige par le bilan, **marge**
|
||||||
|
(rouge sous zero : la batterie ne passera pas), decisions en frise, puissances,
|
||||||
|
energie restant a couvrir, journal des evenements.
|
||||||
|
- **Le solaire** (`evcs-solaire`) — production / maison / voiture et surtout
|
||||||
|
**l'excedent** (§36), la censure avec son motif, **le mode declare par chacun
|
||||||
|
des sept chargeurs** (§37), et un panneau au pas natif InfluxDB pour montrer
|
||||||
|
ce que le pas de 60 s de PostgreSQL ne voit pas.
|
||||||
|
- **Prevision contre reel** (`evcs-prevision`) — l'ecart heure par heure entre
|
||||||
|
la prevision du matin meme et la production, **avec la bande de censure juste
|
||||||
|
dessous** : une production sous la prevision peut vouloir dire mauvaise
|
||||||
|
prevision OU absence de preneur, et seules les heures non bridees permettent
|
||||||
|
de juger. C'est le biais de forme identifie au §42, qui demande des semaines.
|
||||||
|
|
||||||
|
Les evenements apparaissent en marqueurs verticaux sur **tous** les graphiques.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Document mis a jour le : 04/09/2026 (~08:30) — Grafana provisionne par git, 19/19 requetes verifiees*
|
||||||
|
|||||||
@@ -169,3 +169,82 @@ 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
|
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
|
mémoire qu'on perdra une fois — voir `HANDOFF.md` §34, qui raconte la même
|
||||||
histoire pour Home Assistant.
|
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
|
||||||
|
docker compose up -d grafana
|
||||||
|
docker compose logs -f grafana
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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`, en lecture seule.
|
||||||
|
|||||||
@@ -121,7 +121,56 @@ services:
|
|||||||
- HISTORISE_INTERVAL_S=60
|
- HISTORISE_INTERVAL_S=60
|
||||||
command: ["node", "tools/historise-evcs.mjs"]
|
command: ["node", "tools/historise-evcs.mjs"]
|
||||||
|
|
||||||
|
# Consultation des DEUX bases au meme endroit. Grafana ne stocke rien de la
|
||||||
|
# mesure : il interroge PostgreSQL et InfluxDB a la demande. Le perdre ne
|
||||||
|
# perd aucune donnee — c'est une fenetre, pas un coffre.
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana-oss:11.3.0
|
||||||
|
container_name: evcs-grafana
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
# 3001 et NON 3000 : quelque chose ecoute deja sur 3000 sur cette
|
||||||
|
# machine (verifie avant d'ecrire cette ligne, comme le port 5432 deja
|
||||||
|
# pris par le PostgreSQL interne de DSM).
|
||||||
|
- "3001:3000"
|
||||||
|
env_file:
|
||||||
|
# Deux fichiers : les secrets de la pile (mot de passe admin Grafana,
|
||||||
|
# mot de passe PostgreSQL) et ceux du depot (role de lecture, jeton
|
||||||
|
# InfluxDB). Aucun secret n'est duplique — c'etait le piege du
|
||||||
|
# POSTGRES_PASSWORD/PGPASSWORD tenus egaux a la main.
|
||||||
|
- .env
|
||||||
|
- ../../.env
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Paris
|
||||||
|
- GF_SECURITY_ADMIN_USER=admin
|
||||||
|
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
|
||||||
|
# Rien ne sort de la maison : ni telemetrie, ni verification de version,
|
||||||
|
# ni recuperation de plugins. Cette installation doit fonctionner
|
||||||
|
# pendant une coupure Internet d'orage (HANDOFF §32).
|
||||||
|
- GF_ANALYTICS_REPORTING_ENABLED=false
|
||||||
|
- GF_ANALYTICS_CHECK_FOR_UPDATES=false
|
||||||
|
- GF_ANALYTICS_CHECK_FOR_PLUGIN_UPDATES=false
|
||||||
|
- GF_NEWS_NEWS_FEED_ENABLED=false
|
||||||
|
- GF_USERS_ALLOW_SIGN_UP=false
|
||||||
|
- GF_DATE_FORMATS_DEFAULT_TIMEZONE=Europe/Paris
|
||||||
|
volumes:
|
||||||
|
# Provisionnement en LECTURE SEULE : sources de donnees et tableaux de
|
||||||
|
# bord viennent du depot git, comme le code des nodes n8n (regle R2).
|
||||||
|
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||||
|
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||||
|
# Base interne de Grafana : sessions, preferences, copies de travail.
|
||||||
|
- evcs-grafana-data:/var/lib/grafana
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -q -O- http://localhost:3000/api/health || exit 1"]
|
||||||
|
interval: 1m
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
evcs-pgdata:
|
evcs-pgdata:
|
||||||
evcs-influx-data:
|
evcs-influx-data:
|
||||||
evcs-influx-config:
|
evcs-influx-config:
|
||||||
|
evcs-grafana-data:
|
||||||
|
|||||||
644
deploy/historisation/grafana/dashboards/nuit.json
Normal file
644
deploy/historisation/grafana/dashboards/nuit.json
Normal file
@@ -0,0 +1,644 @@
|
|||||||
|
{
|
||||||
|
"uid": "evcs-nuit",
|
||||||
|
"title": "La nuit",
|
||||||
|
"description": "Ce que le bilan exigeait, contre ce que la batterie a reellement fait. C'est la question qui a demande une nuit d'archeologie le 03/09 et qui tient maintenant en un coup d'oeil.",
|
||||||
|
"tags": [
|
||||||
|
"evcs"
|
||||||
|
],
|
||||||
|
"timezone": "Europe/Paris",
|
||||||
|
"editable": true,
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 1,
|
||||||
|
"refresh": "1m",
|
||||||
|
"time": {
|
||||||
|
"from": "now-24h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {
|
||||||
|
"refresh_intervals": [
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"1h"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana",
|
||||||
|
"uid": "-- Grafana --"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"type": "dashboard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Evenements",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"iconColor": "semi-dark-orange",
|
||||||
|
"hide": false,
|
||||||
|
"target": {
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT ts AS time, resume AS text, genre || ',' || coalesce(source,'?') AS tags FROM evenement WHERE $__timeFilter(ts) ORDER BY 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Couverture de la nuit",
|
||||||
|
"description": "SOC requis = ce qu'il faut MAINTENANT pour tenir jusqu'au croisement PV/conso. Il decroit avec la nuit qui se consomme. Quand la courbe reelle passe dessous, la batterie ne passera pas la nuit sans reseau.",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "time_series",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT ts AS time,\n soc_pct AS \"SOC reel\",\n soc_requis_pct AS \"SOC requis (bilan)\"\nFROM bilan\nWHERE $__timeFilter(ts)\nORDER BY 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"refId": "B",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "time_series",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT ts AS time, valeur AS \"Cible du matin\"\nFROM mesure\nWHERE cle = 'input_number.victron_evcs_night_target_soc'\n AND $__timeFilter(ts)\nORDER BY 1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 16,
|
||||||
|
"h": 9
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "percent",
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"lineWidth": 2,
|
||||||
|
"fillOpacity": 8,
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": true,
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"gradientMode": "opacity"
|
||||||
|
},
|
||||||
|
"min": 0,
|
||||||
|
"max": 100
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "SOC reel"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "green"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.lineWidth",
|
||||||
|
"value": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "SOC requis (bilan)"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "orange"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.fillOpacity",
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Cible du matin"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "purple"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.fillOpacity",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.lineStyle",
|
||||||
|
"value": {
|
||||||
|
"fill": "dash",
|
||||||
|
"dash": [
|
||||||
|
8,
|
||||||
|
6
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true,
|
||||||
|
"calcs": []
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "multi",
|
||||||
|
"sort": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Marge (reel moins requis)",
|
||||||
|
"description": "Sous zero, il manque de l'energie pour finir la nuit. La nuit du 02-03/09 est restee negative de bout en bout, et personne ne le voyait.",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "time_series",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT ts AS time, soc_pct - soc_requis_pct AS \"Marge\"\nFROM bilan\nWHERE $__timeFilter(ts) AND soc_requis_pct IS NOT NULL\nORDER BY 1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 16,
|
||||||
|
"y": 0,
|
||||||
|
"w": 8,
|
||||||
|
"h": 9
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "percent",
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"lineWidth": 2,
|
||||||
|
"fillOpacity": 25,
|
||||||
|
"showPoints": "never",
|
||||||
|
"gradientMode": "scheme",
|
||||||
|
"spanNulls": true,
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "line"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"showLegend": false
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "state-timeline",
|
||||||
|
"title": "Ce que le controleur a decide",
|
||||||
|
"description": "Une ligne par CHANGEMENT de decision, pas une toutes les 30 s. La session reseau de nuit doit s'engager UNE fois et se relacher a la fermeture de la fenetre : plusieurs bascules = un oscillateur.",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT ts AS time,\n source_mode AS \"Source\",\n etat AS \"Etat\",\n CASE WHEN session_engagee THEN 'engagee' ELSE '-' END AS \"Session reseau\",\n CASE WHEN takeover_reseau THEN 'demande' ELSE '-' END AS \"Reprise reseau\"\nFROM decision\nWHERE $__timeFilter(ts)\nORDER BY 1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 9,
|
||||||
|
"w": 24,
|
||||||
|
"h": 7
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "continuous-GrYlRd"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"lineWidth": 0,
|
||||||
|
"fillOpacity": 80,
|
||||||
|
"insertNulls": false,
|
||||||
|
"spanNulls": true
|
||||||
|
},
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"type": "value",
|
||||||
|
"options": {
|
||||||
|
"-": {
|
||||||
|
"color": "dark-blue",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
"engagee": {
|
||||||
|
"color": "orange",
|
||||||
|
"index": 1
|
||||||
|
},
|
||||||
|
"demande": {
|
||||||
|
"color": "semi-dark-orange",
|
||||||
|
"index": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"showValue": "auto",
|
||||||
|
"mergeValues": true,
|
||||||
|
"alignValue": "left",
|
||||||
|
"rowHeight": 0.85,
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": false
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Les puissances de la nuit",
|
||||||
|
"description": "Batterie positive = elle se charge. Contacteur ferme avec un import qui ne couvre pas la maison = la batterie paie quand meme : c'est le defaut du 04/09 (consigne retombee a 50 W en pleine session).",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "time_series",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT ts AS time,\n max(valeur) FILTER (WHERE cle = 'sensor.victron_grid_power_l1') AS \"Reseau\",\n max(valeur) FILTER (WHERE cle = 'sensor.victron_battery_power') AS \"Batterie\",\n max(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS \"Maison\",\n max(valeur) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power') AS \"Voiture\"\nFROM mesure\nWHERE $__timeFilter(ts)\nGROUP BY ts\nORDER BY ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 16,
|
||||||
|
"w": 12,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "watt",
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"lineWidth": 2,
|
||||||
|
"fillOpacity": 8,
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": true,
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"gradientMode": "opacity"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Reseau"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "red"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Batterie"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "green"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Maison"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.fillOpacity",
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Voiture"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "blue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true,
|
||||||
|
"calcs": []
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "multi",
|
||||||
|
"sort": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Ce qu'il reste a couvrir",
|
||||||
|
"description": "\"Manque de stockage\" non nul = meme pleine, la batterie actuelle ne passe pas. C'est le signal de dimensionnement pour l'extension prevue cet hiver, pas une erreur.",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "time_series",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT ts AS time,\n energie_a_couvrir_kwh AS \"A couvrir d'ici le croisement\",\n reseau_energie_kwh AS \"Dont reseau necessaire\",\n manque_stockage_kwh AS \"Manque de stockage\"\nFROM bilan\nWHERE $__timeFilter(ts)\nORDER BY 1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 12,
|
||||||
|
"y": 16,
|
||||||
|
"w": 12,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "kwatth",
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"lineWidth": 2,
|
||||||
|
"fillOpacity": 12,
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": true,
|
||||||
|
"gradientMode": "opacity"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true,
|
||||||
|
"calcs": []
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "multi",
|
||||||
|
"sort": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"title": "Journal des evenements",
|
||||||
|
"description": "Une courbe sans ses evenements ne s'interprete pas. Ces lignes apparaissent aussi en marqueurs verticaux sur tous les graphiques.",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT ts AS \"Quand\", genre AS \"Genre\", source AS \"Par\",\n resume AS \"Ce qui s'est passe\"\nFROM evenement\nWHERE $__timeFilter(ts)\nORDER BY ts DESC"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 24,
|
||||||
|
"w": 24,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"align": "left",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
},
|
||||||
|
"filterable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Quand"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 190
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Genre"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 140
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.cellOptions",
|
||||||
|
"value": {
|
||||||
|
"type": "color-text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mappings",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"type": "value",
|
||||||
|
"options": {
|
||||||
|
"deploiement": {
|
||||||
|
"color": "blue",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
"incident": {
|
||||||
|
"color": "red",
|
||||||
|
"index": 1
|
||||||
|
},
|
||||||
|
"intervention": {
|
||||||
|
"color": "orange",
|
||||||
|
"index": 2
|
||||||
|
},
|
||||||
|
"systeme": {
|
||||||
|
"color": "purple",
|
||||||
|
"index": 3
|
||||||
|
},
|
||||||
|
"reseau": {
|
||||||
|
"color": "yellow",
|
||||||
|
"index": 4
|
||||||
|
},
|
||||||
|
"vehicule": {
|
||||||
|
"color": "green",
|
||||||
|
"index": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Par"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 130
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"showHeader": true,
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"show": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
238
deploy/historisation/grafana/dashboards/prevision.json
Normal file
238
deploy/historisation/grafana/dashboards/prevision.json
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
{
|
||||||
|
"uid": "evcs-prevision",
|
||||||
|
"title": "Prevision contre reel",
|
||||||
|
"description": "Le total de la prevision tombe juste ; sa FORME non. Sur les trois jours verifies le 04/09, la production arrive plus tot que prevu et l'apres-midi est sur-promis d'un facteur 2 a 3 apres 16 h. Ce tableau existe pour instruire cette question sur des semaines, pas sur trois jours.",
|
||||||
|
"tags": [
|
||||||
|
"evcs"
|
||||||
|
],
|
||||||
|
"timezone": "Europe/Paris",
|
||||||
|
"editable": true,
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 1,
|
||||||
|
"refresh": "1m",
|
||||||
|
"time": {
|
||||||
|
"from": "now-24h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {
|
||||||
|
"refresh_intervals": [
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"1h"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana",
|
||||||
|
"uid": "-- Grafana --"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"type": "dashboard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Evenements",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"iconColor": "semi-dark-orange",
|
||||||
|
"hide": false,
|
||||||
|
"target": {
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT ts AS time, resume AS text, genre || ',' || coalesce(source,'?') AS tags FROM evenement WHERE $__timeFilter(ts) ORDER BY 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"type": "barchart",
|
||||||
|
"title": "Heure par heure : promis contre livre",
|
||||||
|
"description": "ATTENTION a l'interpretation : \"produit\" est une mesure CENSUREE. Moins que prevu peut vouloir dire mauvaise prevision OU absence de preneur. Lire la bande de censure juste dessous avant de conclure.",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "WITH du_matin AS (\n SELECT DISTINCT ON (jour) jour, publie_a, horaire\n FROM prevision\n WHERE publie_a::date = jour\n AND jour BETWEEN $__timeFrom()::date AND $__timeTo()::date\n ORDER BY jour, publie_a ASC\n),\nprevu AS (\n SELECT (jour + (h - 1) * interval '1 hour') AS heure,\n horaire[h] AS kwh\n FROM du_matin, generate_subscripts(horaire, 1) AS h\n),\nproduit AS (\n -- Un echantillon toutes les 60 s : chaque point vaut 1/60 h.\n SELECT date_trunc('hour', ts) AS heure,\n sum(valeur) / 60.0 / 1000.0 AS kwh\n FROM mesure\n WHERE cle = 'sensor.victron_pv_power_system' AND $__timeFilter(ts)\n GROUP BY 1\n)\nSELECT coalesce(prevu.heure, produit.heure) AS time,\n round(prevu.kwh::numeric, 2) AS \"Prevu le matin meme\",\n round(produit.kwh::numeric, 2) AS \"Reellement produit\"\nFROM prevu FULL OUTER JOIN produit USING (heure)\nWHERE coalesce(prevu.heure, produit.heure) BETWEEN $__timeFrom() AND $__timeTo()\nORDER BY 1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 24,
|
||||||
|
"h": 10
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "kwatth",
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"lineWidth": 1,
|
||||||
|
"fillOpacity": 80,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"axisBorderShow": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"orientation": "auto",
|
||||||
|
"xTickLabelRotation": -45,
|
||||||
|
"showValue": "never",
|
||||||
|
"stacking": "none",
|
||||||
|
"groupWidth": 0.7,
|
||||||
|
"barWidth": 0.9,
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "multi",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "state-timeline",
|
||||||
|
"title": "Y avait-il un preneur ?",
|
||||||
|
"description": "Seules les heures 'libre' permettent de juger la prevision. Les heures bridees ne disent rien de la ressource disponible.",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT ts AS time,\n CASE WHEN pv_censure THEN 'bridee (mesure inexploitable)'\n ELSE 'libre (mesure valide)' END AS \"Production\"\nFROM bilan WHERE $__timeFilter(ts) ORDER BY 1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 10,
|
||||||
|
"w": 24,
|
||||||
|
"h": 5
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "continuous-GrYlRd"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"lineWidth": 0,
|
||||||
|
"fillOpacity": 80,
|
||||||
|
"insertNulls": false,
|
||||||
|
"spanNulls": true
|
||||||
|
},
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"type": "value",
|
||||||
|
"options": {
|
||||||
|
"libre (mesure valide)": {
|
||||||
|
"color": "green",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
"bridee (mesure inexploitable)": {
|
||||||
|
"color": "orange",
|
||||||
|
"index": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"showValue": "auto",
|
||||||
|
"mergeValues": true,
|
||||||
|
"alignValue": "left",
|
||||||
|
"rowHeight": 0.85,
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": false
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"title": "Les previsions telles qu'elles ont ete publiees",
|
||||||
|
"description": "Sans l'horodatage de PUBLICATION, on ne peut pas mesurer le biais : comparer une prevision au reel n'a de sens que si l'on sait quand elle a ete faite. C'est la raison d'etre de cette table.",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT publie_a AS \"Publiee le\", jour AS \"Pour le jour\",\n round(total_kwh::numeric, 1) AS \"Total annonce (kWh)\",\n round((horaire[8] + horaire[9] + horaire[10] + horaire[11])::numeric, 2) AS \"07-11 h\",\n round((horaire[12] + horaire[13] + horaire[14] + horaire[15])::numeric, 2) AS \"11-15 h\",\n round((horaire[16] + horaire[17] + horaire[18] + horaire[19])::numeric, 2) AS \"15-19 h\"\nFROM prevision\nWHERE $__timeFilter(publie_a)\nORDER BY publie_a DESC, jour"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 15,
|
||||||
|
"w": 24,
|
||||||
|
"h": 9
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"align": "left",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
},
|
||||||
|
"filterable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"showHeader": true,
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"show": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
516
deploy/historisation/grafana/dashboards/solaire.json
Normal file
516
deploy/historisation/grafana/dashboards/solaire.json
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
{
|
||||||
|
"uid": "evcs-solaire",
|
||||||
|
"title": "Le solaire",
|
||||||
|
"description": "La production mesuree n'est jamais la production disponible : sans injection, le MPPT ne produit que ce que le systeme peut absorber. Ces panneaux montrent l'ecart entre les deux.",
|
||||||
|
"tags": [
|
||||||
|
"evcs"
|
||||||
|
],
|
||||||
|
"timezone": "Europe/Paris",
|
||||||
|
"editable": true,
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 1,
|
||||||
|
"refresh": "1m",
|
||||||
|
"time": {
|
||||||
|
"from": "now-24h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {
|
||||||
|
"refresh_intervals": [
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"1h"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana",
|
||||||
|
"uid": "-- Grafana --"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"type": "dashboard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Evenements",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"iconColor": "semi-dark-orange",
|
||||||
|
"hide": false,
|
||||||
|
"target": {
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT ts AS time, resume AS text, genre || ',' || coalesce(source,'?') AS tags FROM evenement WHERE $__timeFilter(ts) ORDER BY 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Production, maison, et ce qui reste",
|
||||||
|
"description": "L'excedent (production moins maison hors voiture) est la grandeur qui decide d'un demarrage depuis le 03/09. Le 03/09 a 19:44, la production valait 758 W — au-dessus du seuil — pour un excedent de MOINS 444 W : la voiture a vide la batterie.",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "time_series",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT ts AS time,\n max(valeur) FILTER (WHERE cle = 'sensor.victron_pv_power_system') AS \"Production\",\n max(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS \"Maison\",\n max(valeur) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power') AS \"Voiture\",\n max(valeur) FILTER (WHERE cle = 'sensor.victron_pv_power_system')\n - max(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS \"Excedent\"\nFROM mesure\nWHERE $__timeFilter(ts)\nGROUP BY ts\nORDER BY ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 24,
|
||||||
|
"h": 9
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "watt",
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"lineWidth": 2,
|
||||||
|
"fillOpacity": 8,
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": true,
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"gradientMode": "opacity"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Production"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "yellow"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.fillOpacity",
|
||||||
|
"value": 22
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Maison"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.fillOpacity",
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Voiture"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "blue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Excedent"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "green"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.lineWidth",
|
||||||
|
"value": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.fillOpacity",
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true,
|
||||||
|
"calcs": []
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "multi",
|
||||||
|
"sort": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "state-timeline",
|
||||||
|
"title": "Production bridee ?",
|
||||||
|
"description": "Bridee = de la ressource solaire est REFUSEE faute de preneur. Charger la voiture pendant ces heures-la ne coute rien.",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT ts AS time,\n CASE WHEN pv_censure THEN coalesce(motif_censure, 'bridee')\n ELSE 'libre' END AS \"Production\"\nFROM bilan\nWHERE $__timeFilter(ts)\nORDER BY 1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 9,
|
||||||
|
"w": 12,
|
||||||
|
"h": 6
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "continuous-GrYlRd"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"lineWidth": 0,
|
||||||
|
"fillOpacity": 80,
|
||||||
|
"insertNulls": false,
|
||||||
|
"spanNulls": true
|
||||||
|
},
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"type": "value",
|
||||||
|
"options": {
|
||||||
|
"libre": {
|
||||||
|
"color": "green",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"options": {
|
||||||
|
"pattern": ".*",
|
||||||
|
"result": {
|
||||||
|
"color": "orange",
|
||||||
|
"index": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"showValue": "auto",
|
||||||
|
"mergeValues": true,
|
||||||
|
"alignValue": "left",
|
||||||
|
"rowHeight": 0.85,
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": false
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "state-timeline",
|
||||||
|
"title": "Ce que les chargeurs declarent eux-memes",
|
||||||
|
"description": "mppt_active = ce chargeur donne tout ce qu'il peut. voltage_current_limited ou off = il se retient, faute de preneur. Le 03/09 ces sept capteurs ont bascule 24 minutes avant le drapeau du bilan — c'est le signal le plus direct dont on dispose.",
|
||||||
|
"datasource": {
|
||||||
|
"type": "influxdb",
|
||||||
|
"uid": "evcs-influx"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "influxdb",
|
||||||
|
"uid": "evcs-influx"
|
||||||
|
},
|
||||||
|
"query": "from(bucket: \"evcs\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._field == \"state\")\n |> filter(fn: (r) => r._measurement =~ /operation_mode$/)\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> map(fn: (r) => ({ r with tracker:\n if r._measurement =~ /solar_1/ then \"Sud 1\"\n else if r._measurement =~ /solar_2/ then \"Sud 2\"\n else if r._measurement =~ /solar_3/ then \"Sud 3\"\n else if r._measurement =~ /solar_4/ then \"Ouest\"\n else if r._measurement =~ /mppt1/ then \"Sud-est\"\n else \"Ensemble\" }))\n |> keep(columns: [\"_time\", \"_value\", \"tracker\"])\n |> pivot(rowKey: [\"_time\"], columnKey: [\"tracker\"], valueColumn: \"_value\")"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 12,
|
||||||
|
"y": 9,
|
||||||
|
"w": 12,
|
||||||
|
"h": 6
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "continuous-GrYlRd"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"lineWidth": 0,
|
||||||
|
"fillOpacity": 80,
|
||||||
|
"insertNulls": false,
|
||||||
|
"spanNulls": true
|
||||||
|
},
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"type": "value",
|
||||||
|
"options": {
|
||||||
|
"mppt_active": {
|
||||||
|
"color": "green",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
"voltage_current_limited": {
|
||||||
|
"color": "orange",
|
||||||
|
"index": 1
|
||||||
|
},
|
||||||
|
"off": {
|
||||||
|
"color": "dark-blue",
|
||||||
|
"index": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"showValue": "auto",
|
||||||
|
"mergeValues": true,
|
||||||
|
"alignValue": "left",
|
||||||
|
"rowHeight": 0.85,
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": false
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Batterie",
|
||||||
|
"description": "Le SOC n'est pas lineaire : 0,169 kWh par point au-dessus de 33 %, 0,097 en dessous (576 paliers mesures). Un point en bas d'echelle vaut la moitie d'un point en haut.",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "time_series",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT ts AS time,\n max(valeur) FILTER (WHERE cle = 'sensor.victron_battery_soc') AS \"SOC\"\nFROM mesure WHERE $__timeFilter(ts) GROUP BY ts ORDER BY ts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"refId": "B",
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-postgresql-datasource",
|
||||||
|
"uid": "evcs-pg"
|
||||||
|
},
|
||||||
|
"format": "time_series",
|
||||||
|
"rawQuery": true,
|
||||||
|
"editorMode": "code",
|
||||||
|
"rawSql": "SELECT ts AS time,\n max(valeur) FILTER (WHERE cle = 'sensor.victron_battery_current') AS \"Courant\"\nFROM mesure WHERE $__timeFilter(ts) GROUP BY ts ORDER BY ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 15,
|
||||||
|
"w": 12,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "percent",
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"lineWidth": 2,
|
||||||
|
"fillOpacity": 8,
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": true,
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"gradientMode": "opacity"
|
||||||
|
},
|
||||||
|
"min": 0,
|
||||||
|
"max": 100
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Courant"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "unit",
|
||||||
|
"value": "amp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.axisPlacement",
|
||||||
|
"value": "right"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "max",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "min",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "purple"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "SOC"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "green"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.lineWidth",
|
||||||
|
"value": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true,
|
||||||
|
"calcs": []
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "multi",
|
||||||
|
"sort": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Le detail que PostgreSQL ne voit pas (~1 point/s)",
|
||||||
|
"description": "InfluxDB enregistre chaque changement d'etat, pas un echantillon par minute. C'est la resolution qu'il faut pour voir un transitoire de reprise du reseau (~150 s) ou une pointe de cuisson.",
|
||||||
|
"datasource": {
|
||||||
|
"type": "influxdb",
|
||||||
|
"uid": "evcs-influx"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "influxdb",
|
||||||
|
"uid": "evcs-influx"
|
||||||
|
},
|
||||||
|
"query": "from(bucket: \"evcs\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"W\" and r._field == \"value\")\n |> filter(fn: (r) => r.entity_id == \"victron_pv_power_system\"\n or r.entity_id == \"victron_consumption_power_l1\"\n or r.entity_id == \"victron_evcs_ac_power\"\n or r.entity_id == \"victron_grid_power_l1\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"entity_id\"])\n |> pivot(rowKey: [\"_time\"], columnKey: [\"entity_id\"], valueColumn: \"_value\")"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gridPos": {
|
||||||
|
"x": 12,
|
||||||
|
"y": 15,
|
||||||
|
"w": 12,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "watt",
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"lineWidth": 1,
|
||||||
|
"fillOpacity": 5,
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true,
|
||||||
|
"calcs": []
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "multi",
|
||||||
|
"sort": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Les tableaux de bord viennent du depot git, comme le code des nodes n8n.
|
||||||
|
#
|
||||||
|
# `allowUiUpdates: false` : on ne peut pas ecraser depuis l'interface un
|
||||||
|
# tableau de bord versionne — c'est la meme regle que « JAMAIS editer le JSON
|
||||||
|
# du workflow ni l'UI n8n » (AGENTS.md R2), et pour la meme raison : sinon la
|
||||||
|
# modification est perdue au redemarrage suivant, en silence.
|
||||||
|
#
|
||||||
|
# Explorer reste libre : « Save as… » depuis n'importe quel tableau de bord
|
||||||
|
# cree une copie personnelle, modifiable, dans le dossier General.
|
||||||
|
|
||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
providers:
|
||||||
|
- name: EVCS
|
||||||
|
orgId: 1
|
||||||
|
folder: EVCS
|
||||||
|
type: file
|
||||||
|
disableDeletion: false
|
||||||
|
allowUiUpdates: false
|
||||||
|
updateIntervalSeconds: 30
|
||||||
|
options:
|
||||||
|
path: /var/lib/grafana/dashboards
|
||||||
|
foldersFromFilesStructure: false
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Sources de donnees, versionnees dans git et appliquees au demarrage.
|
||||||
|
#
|
||||||
|
# Les secrets ne sont PAS ici : `$VARIABLE` est substitue par Grafana depuis
|
||||||
|
# l'environnement du conteneur, alimente par les deux `.env` (jamais commites).
|
||||||
|
# Ce fichier peut donc etre lu par n'importe qui sans rien reveler.
|
||||||
|
|
||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
datasources:
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Ce que le systeme CALCULE : bilan, decisions, prevision publiee,
|
||||||
|
# evenements. Relationnel, donc joignable.
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
- name: EVCS PostgreSQL
|
||||||
|
uid: evcs-pg
|
||||||
|
type: grafana-postgresql-datasource
|
||||||
|
access: proxy
|
||||||
|
# Nom de service du reseau interne : ni IP en dur, ni dependance au port
|
||||||
|
# publie sur l'hote.
|
||||||
|
url: postgres:5432
|
||||||
|
database: evcs
|
||||||
|
# Role EN LECTURE SEULE (plus un INSERT restreint a la table evenement).
|
||||||
|
# Un tableau de bord ne doit pas pouvoir ecrire dans la memoire.
|
||||||
|
user: evcs_lecture
|
||||||
|
secureJsonData:
|
||||||
|
password: $PG_LECTURE_PASSWORD
|
||||||
|
jsonData:
|
||||||
|
sslmode: disable
|
||||||
|
postgresVersion: 1600
|
||||||
|
timescaledb: false
|
||||||
|
isDefault: true
|
||||||
|
editable: false
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Ce que Home Assistant OBSERVE : chaque changement d'etat, a la
|
||||||
|
# resolution native (~1 Hz), y compris les entites auxquelles
|
||||||
|
# personne n'a encore pense. C'est ce qui manquait le 03/09, quand
|
||||||
|
# les modes des chargeurs MPPT se sont reveles etre le bon signal.
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
- name: EVCS InfluxDB
|
||||||
|
uid: evcs-influx
|
||||||
|
type: influxdb
|
||||||
|
access: proxy
|
||||||
|
url: http://influxdb:8086
|
||||||
|
jsonData:
|
||||||
|
version: Flux
|
||||||
|
organization: airtria
|
||||||
|
defaultBucket: evcs
|
||||||
|
httpMode: POST
|
||||||
|
secureJsonData:
|
||||||
|
token: $INFLUXDB_TOKEN
|
||||||
|
editable: false
|
||||||
52
deploy/historisation/preparer-grafana.sh
Executable file
52
deploy/historisation/preparer-grafana.sh
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Ajoute GRAFANA_ADMIN_PASSWORD à deploy/historisation/.env s'il en manque un.
|
||||||
|
#
|
||||||
|
# Ce mot de passe n'est pas de la même nature que les autres : les secrets des
|
||||||
|
# bases ne sont jamais tapés par personne (les conteneurs les lisent), celui-ci
|
||||||
|
# sert à VOUS connecter à l'interface. Il est donc généré ici — pas choisi, une
|
||||||
|
# phrase inventée est plus devinable qu'elle n'en a l'air — puis affiché une
|
||||||
|
# fois, et il reste relisible à tout moment dans le fichier.
|
||||||
|
#
|
||||||
|
# Usage : bash preparer-grafana.sh
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "REFUS : .env absent. Lancer d'abord bash preparer-env.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q '^GRAFANA_ADMIN_PASSWORD=' .env; then
|
||||||
|
echo "GRAFANA_ADMIN_PASSWORD est déjà défini. Rien à faire."
|
||||||
|
echo
|
||||||
|
echo "Pour le relire : grep GRAFANA_ADMIN_PASSWORD deploy/historisation/.env"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
MDP="$(openssl rand -base64 18 | tr -d '/+=' | cut -c1-20)"
|
||||||
|
printf '\n# Connexion à l’interface Grafana (utilisateur : admin).\nGRAFANA_ADMIN_PASSWORD=%s\n' "$MDP" >> .env
|
||||||
|
chmod 600 .env
|
||||||
|
|
||||||
|
# Le rôle de lecture PostgreSQL et le jeton InfluxDB vivent dans le .env à la
|
||||||
|
# racine du dépôt ; le compose donne les DEUX fichiers à Grafana, donc rien à
|
||||||
|
# recopier ici. On vérifie seulement qu'ils y sont, sans jamais les afficher.
|
||||||
|
RACINE=../../.env
|
||||||
|
for V in PG_LECTURE_PASSWORD INFLUXDB_TOKEN; do
|
||||||
|
if grep -q "^$V=." "$RACINE" 2>/dev/null; then
|
||||||
|
echo " $V : présent dans $RACINE"
|
||||||
|
else
|
||||||
|
echo " $V : MANQUANT dans $RACINE — Grafana ne pourra pas lire cette base."
|
||||||
|
case "$V" in
|
||||||
|
PG_LECTURE_PASSWORD) echo " -> bash creer-role-lecture.sh" ;;
|
||||||
|
INFLUXDB_TOKEN) echo " -> bash exporter-jeton-influx.sh" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Grafana : http://10.10.10.22:3001"
|
||||||
|
echo " utilisateur : admin"
|
||||||
|
echo " mot de passe : $MDP"
|
||||||
|
echo
|
||||||
|
echo "Noté dans deploy/historisation/.env (600, jamais commité)."
|
||||||
|
echo "Relisible par : grep GRAFANA_ADMIN_PASSWORD deploy/historisation/.env"
|
||||||
435
tools/gen-grafana-dashboards.py
Normal file
435
tools/gen-grafana-dashboards.py
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Genere les tableaux de bord EVCS. Le JSON de Grafana est verbeux ; l'ecrire
|
||||||
|
a la main invite les fautes silencieuses. Ce generateur est rejouable."""
|
||||||
|
import json, pathlib
|
||||||
|
|
||||||
|
PG = {"type": "grafana-postgresql-datasource", "uid": "evcs-pg"}
|
||||||
|
IN = {"type": "influxdb", "uid": "evcs-influx"}
|
||||||
|
|
||||||
|
def sql(refid, q, fmt="time_series"):
|
||||||
|
return {"refId": refid, "datasource": PG, "format": fmt,
|
||||||
|
"rawQuery": True, "editorMode": "code", "rawSql": q}
|
||||||
|
|
||||||
|
def flux(refid, q):
|
||||||
|
return {"refId": refid, "datasource": IN, "query": q}
|
||||||
|
|
||||||
|
def ts(title, targets, ds, x, y, w, h, unit="", desc="", extra=None, legend="bottom"):
|
||||||
|
p = {"type": "timeseries", "title": title, "description": desc,
|
||||||
|
"datasource": ds, "targets": targets,
|
||||||
|
"gridPos": {"x": x, "y": y, "w": w, "h": h},
|
||||||
|
"fieldConfig": {"defaults": {
|
||||||
|
"unit": unit, "color": {"mode": "palette-classic"},
|
||||||
|
"custom": {"lineWidth": 2, "fillOpacity": 8, "showPoints": "never",
|
||||||
|
"spanNulls": True, "axisBorderShow": False,
|
||||||
|
"gradientMode": "opacity"}},
|
||||||
|
"overrides": []},
|
||||||
|
"options": {"legend": {"displayMode": "list", "placement": legend,
|
||||||
|
"showLegend": True, "calcs": []},
|
||||||
|
"tooltip": {"mode": "multi", "sort": "desc"}}}
|
||||||
|
if extra:
|
||||||
|
for k, v in extra.items():
|
||||||
|
if k == "defaults":
|
||||||
|
p["fieldConfig"]["defaults"].update(v)
|
||||||
|
elif k == "overrides":
|
||||||
|
p["fieldConfig"]["overrides"] = v
|
||||||
|
else:
|
||||||
|
p[k] = v
|
||||||
|
return p
|
||||||
|
|
||||||
|
def timeline(title, targets, ds, x, y, w, h, desc="", mappings=None):
|
||||||
|
return {"type": "state-timeline", "title": title, "description": desc,
|
||||||
|
"datasource": ds, "targets": targets,
|
||||||
|
"gridPos": {"x": x, "y": y, "w": w, "h": h},
|
||||||
|
"fieldConfig": {"defaults": {
|
||||||
|
"color": {"mode": "continuous-GrYlRd"},
|
||||||
|
"custom": {"lineWidth": 0, "fillOpacity": 80,
|
||||||
|
"insertNulls": False, "spanNulls": True},
|
||||||
|
"mappings": mappings or []}, "overrides": []},
|
||||||
|
"options": {"showValue": "auto", "mergeValues": True,
|
||||||
|
"alignValue": "left", "rowHeight": 0.85,
|
||||||
|
"legend": {"displayMode": "list", "placement": "bottom",
|
||||||
|
"showLegend": False},
|
||||||
|
"tooltip": {"mode": "single"}}}
|
||||||
|
|
||||||
|
def table(title, targets, ds, x, y, w, h, desc="", overrides=None):
|
||||||
|
return {"type": "table", "title": title, "description": desc,
|
||||||
|
"datasource": ds, "targets": targets,
|
||||||
|
"gridPos": {"x": x, "y": y, "w": w, "h": h},
|
||||||
|
"fieldConfig": {"defaults": {"custom": {"align": "left",
|
||||||
|
"cellOptions": {"type": "auto"}, "filterable": True}},
|
||||||
|
"overrides": overrides or []},
|
||||||
|
"options": {"showHeader": True, "cellHeight": "sm",
|
||||||
|
"footer": {"show": False}}}
|
||||||
|
|
||||||
|
def barchart(title, targets, ds, x, y, w, h, unit="", desc=""):
|
||||||
|
return {"type": "barchart", "title": title, "description": desc,
|
||||||
|
"datasource": ds, "targets": targets,
|
||||||
|
"gridPos": {"x": x, "y": y, "w": w, "h": h},
|
||||||
|
"fieldConfig": {"defaults": {"unit": unit,
|
||||||
|
"color": {"mode": "palette-classic"},
|
||||||
|
"custom": {"lineWidth": 1, "fillOpacity": 80,
|
||||||
|
"gradientMode": "none", "axisBorderShow": False}},
|
||||||
|
"overrides": []},
|
||||||
|
"options": {"orientation": "auto", "xTickLabelRotation": -45,
|
||||||
|
"showValue": "never", "stacking": "none",
|
||||||
|
"groupWidth": 0.7, "barWidth": 0.9,
|
||||||
|
"legend": {"displayMode": "list", "placement": "bottom",
|
||||||
|
"showLegend": True},
|
||||||
|
"tooltip": {"mode": "multi", "sort": "none"}}}
|
||||||
|
|
||||||
|
def dashboard(uid, title, desc, panels, refresh="1m", frm="now-24h"):
|
||||||
|
return {
|
||||||
|
"uid": uid, "title": title, "description": desc,
|
||||||
|
"tags": ["evcs"], "timezone": "Europe/Paris", "editable": True,
|
||||||
|
"schemaVersion": 39, "version": 1, "refresh": refresh,
|
||||||
|
"time": {"from": frm, "to": "now"},
|
||||||
|
"timepicker": {"refresh_intervals": ["30s", "1m", "5m", "15m", "1h"]},
|
||||||
|
"annotations": {"list": [
|
||||||
|
{"builtIn": 1, "datasource": {"type": "grafana", "uid": "-- Grafana --"},
|
||||||
|
"enable": True, "hide": True, "name": "Annotations & Alerts",
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)", "type": "dashboard"},
|
||||||
|
{"name": "Evenements", "datasource": PG, "enable": True,
|
||||||
|
"iconColor": "semi-dark-orange", "hide": False,
|
||||||
|
"target": {"format": "table", "rawQuery": True, "editorMode": "code",
|
||||||
|
"rawSql": "SELECT ts AS time, resume AS text, "
|
||||||
|
"genre || ',' || coalesce(source,'?') AS tags "
|
||||||
|
"FROM evenement WHERE $__timeFilter(ts) ORDER BY 1"}}]},
|
||||||
|
"panels": panels}
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 1. LA NUIT — la question a laquelle on ne savait pas repondre le 03/09
|
||||||
|
# =====================================================================
|
||||||
|
nuit = dashboard("evcs-nuit", "La nuit", (
|
||||||
|
"Ce que le bilan exigeait, contre ce que la batterie a reellement fait. "
|
||||||
|
"C'est la question qui a demande une nuit d'archeologie le 03/09 et qui "
|
||||||
|
"tient maintenant en un coup d'oeil."), [
|
||||||
|
|
||||||
|
ts("Couverture de la nuit", [
|
||||||
|
sql("A", "SELECT ts AS time,\n"
|
||||||
|
" soc_pct AS \"SOC reel\",\n"
|
||||||
|
" soc_requis_pct AS \"SOC requis (bilan)\"\n"
|
||||||
|
"FROM bilan\n"
|
||||||
|
"WHERE $__timeFilter(ts)\n"
|
||||||
|
"ORDER BY 1"),
|
||||||
|
sql("B", "SELECT ts AS time, valeur AS \"Cible du matin\"\n"
|
||||||
|
"FROM mesure\n"
|
||||||
|
"WHERE cle = 'input_number.victron_evcs_night_target_soc'\n"
|
||||||
|
" AND $__timeFilter(ts)\n"
|
||||||
|
"ORDER BY 1")],
|
||||||
|
PG, 0, 0, 16, 9, "percent",
|
||||||
|
"SOC requis = ce qu'il faut MAINTENANT pour tenir jusqu'au croisement "
|
||||||
|
"PV/conso. Il decroit avec la nuit qui se consomme. Quand la courbe reelle "
|
||||||
|
"passe dessous, la batterie ne passera pas la nuit sans reseau.",
|
||||||
|
extra={"defaults": {"min": 0, "max": 100},
|
||||||
|
"overrides": [
|
||||||
|
{"matcher": {"id": "byName", "options": "SOC reel"},
|
||||||
|
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "green"}},
|
||||||
|
{"id": "custom.lineWidth", "value": 3}]},
|
||||||
|
{"matcher": {"id": "byName", "options": "SOC requis (bilan)"},
|
||||||
|
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "orange"}},
|
||||||
|
{"id": "custom.fillOpacity", "value": 0}]},
|
||||||
|
{"matcher": {"id": "byName", "options": "Cible du matin"},
|
||||||
|
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "purple"}},
|
||||||
|
{"id": "custom.fillOpacity", "value": 0},
|
||||||
|
{"id": "custom.lineStyle",
|
||||||
|
"value": {"fill": "dash", "dash": [8, 6]}}]}]}),
|
||||||
|
|
||||||
|
ts("Marge (reel moins requis)", [
|
||||||
|
sql("A", "SELECT ts AS time, soc_pct - soc_requis_pct AS \"Marge\"\n"
|
||||||
|
"FROM bilan\n"
|
||||||
|
"WHERE $__timeFilter(ts) AND soc_requis_pct IS NOT NULL\n"
|
||||||
|
"ORDER BY 1")],
|
||||||
|
PG, 16, 0, 8, 9, "percent",
|
||||||
|
"Sous zero, il manque de l'energie pour finir la nuit. La nuit du "
|
||||||
|
"02-03/09 est restee negative de bout en bout, et personne ne le voyait.",
|
||||||
|
extra={"defaults": {
|
||||||
|
"color": {"mode": "thresholds"},
|
||||||
|
"custom": {"lineWidth": 2, "fillOpacity": 25, "showPoints": "never",
|
||||||
|
"gradientMode": "scheme", "spanNulls": True,
|
||||||
|
"thresholdsStyle": {"mode": "line"}},
|
||||||
|
"thresholds": {"mode": "absolute", "steps": [
|
||||||
|
{"color": "red", "value": None}, {"color": "green", "value": 0}]}},
|
||||||
|
"options": {"legend": {"showLegend": False},
|
||||||
|
"tooltip": {"mode": "single", "sort": "none"}}}),
|
||||||
|
|
||||||
|
timeline("Ce que le controleur a decide", [
|
||||||
|
sql("A", "SELECT ts AS time,\n"
|
||||||
|
" source_mode AS \"Source\",\n"
|
||||||
|
" etat AS \"Etat\",\n"
|
||||||
|
" CASE WHEN session_engagee THEN 'engagee' ELSE '-' END AS \"Session reseau\",\n"
|
||||||
|
" CASE WHEN takeover_reseau THEN 'demande' ELSE '-' END AS \"Reprise reseau\"\n"
|
||||||
|
"FROM decision\n"
|
||||||
|
"WHERE $__timeFilter(ts)\n"
|
||||||
|
"ORDER BY 1", fmt="table")],
|
||||||
|
PG, 0, 9, 24, 7,
|
||||||
|
"Une ligne par CHANGEMENT de decision, pas une toutes les 30 s. "
|
||||||
|
"La session reseau de nuit doit s'engager UNE fois et se relacher a la "
|
||||||
|
"fermeture de la fenetre : plusieurs bascules = un oscillateur.",
|
||||||
|
mappings=[{"type": "value", "options": {
|
||||||
|
"-": {"color": "dark-blue", "index": 0},
|
||||||
|
"engagee": {"color": "orange", "index": 1},
|
||||||
|
"demande": {"color": "semi-dark-orange", "index": 2}}}]),
|
||||||
|
|
||||||
|
ts("Les puissances de la nuit", [
|
||||||
|
sql("A", "SELECT ts AS time,\n"
|
||||||
|
" max(valeur) FILTER (WHERE cle = 'sensor.victron_grid_power_l1') AS \"Reseau\",\n"
|
||||||
|
" max(valeur) FILTER (WHERE cle = 'sensor.victron_battery_power') AS \"Batterie\",\n"
|
||||||
|
" max(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS \"Maison\",\n"
|
||||||
|
" max(valeur) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power') AS \"Voiture\"\n"
|
||||||
|
"FROM mesure\n"
|
||||||
|
"WHERE $__timeFilter(ts)\n"
|
||||||
|
"GROUP BY ts\n"
|
||||||
|
"ORDER BY ts")],
|
||||||
|
PG, 0, 16, 12, 8, "watt",
|
||||||
|
"Batterie positive = elle se charge. Contacteur ferme avec un import qui "
|
||||||
|
"ne couvre pas la maison = la batterie paie quand meme : c'est le defaut "
|
||||||
|
"du 04/09 (consigne retombee a 50 W en pleine session).",
|
||||||
|
extra={"overrides": [
|
||||||
|
{"matcher": {"id": "byName", "options": "Reseau"},
|
||||||
|
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "red"}}]},
|
||||||
|
{"matcher": {"id": "byName", "options": "Batterie"},
|
||||||
|
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "green"}}]},
|
||||||
|
{"matcher": {"id": "byName", "options": "Maison"},
|
||||||
|
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "text"}},
|
||||||
|
{"id": "custom.fillOpacity", "value": 0}]},
|
||||||
|
{"matcher": {"id": "byName", "options": "Voiture"},
|
||||||
|
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "blue"}}]}]}),
|
||||||
|
|
||||||
|
ts("Ce qu'il reste a couvrir", [
|
||||||
|
sql("A", "SELECT ts AS time,\n"
|
||||||
|
" energie_a_couvrir_kwh AS \"A couvrir d'ici le croisement\",\n"
|
||||||
|
" reseau_energie_kwh AS \"Dont reseau necessaire\",\n"
|
||||||
|
" manque_stockage_kwh AS \"Manque de stockage\"\n"
|
||||||
|
"FROM bilan\n"
|
||||||
|
"WHERE $__timeFilter(ts)\n"
|
||||||
|
"ORDER BY 1")],
|
||||||
|
PG, 12, 16, 12, 8, "kwatth",
|
||||||
|
"\"Manque de stockage\" non nul = meme pleine, la batterie actuelle ne "
|
||||||
|
"passe pas. C'est le signal de dimensionnement pour l'extension prevue "
|
||||||
|
"cet hiver, pas une erreur.",
|
||||||
|
extra={"defaults": {"custom": {"lineWidth": 2, "fillOpacity": 12,
|
||||||
|
"showPoints": "never", "spanNulls": True,
|
||||||
|
"gradientMode": "opacity"}}}),
|
||||||
|
|
||||||
|
table("Journal des evenements", [
|
||||||
|
sql("A", "SELECT ts AS \"Quand\", genre AS \"Genre\", source AS \"Par\",\n"
|
||||||
|
" resume AS \"Ce qui s'est passe\"\n"
|
||||||
|
"FROM evenement\n"
|
||||||
|
"WHERE $__timeFilter(ts)\n"
|
||||||
|
"ORDER BY ts DESC", fmt="table")],
|
||||||
|
PG, 0, 24, 24, 8,
|
||||||
|
"Une courbe sans ses evenements ne s'interprete pas. Ces lignes "
|
||||||
|
"apparaissent aussi en marqueurs verticaux sur tous les graphiques.",
|
||||||
|
overrides=[{"matcher": {"id": "byName", "options": "Quand"},
|
||||||
|
"properties": [{"id": "custom.width", "value": 190}]},
|
||||||
|
{"matcher": {"id": "byName", "options": "Genre"},
|
||||||
|
"properties": [{"id": "custom.width", "value": 140},
|
||||||
|
{"id": "custom.cellOptions",
|
||||||
|
"value": {"type": "color-text"}},
|
||||||
|
{"id": "mappings", "value": [{"type": "value", "options": {
|
||||||
|
"deploiement": {"color": "blue", "index": 0},
|
||||||
|
"incident": {"color": "red", "index": 1},
|
||||||
|
"intervention": {"color": "orange", "index": 2},
|
||||||
|
"systeme": {"color": "purple", "index": 3},
|
||||||
|
"reseau": {"color": "yellow", "index": 4},
|
||||||
|
"vehicule": {"color": "green", "index": 5}}}]}]},
|
||||||
|
{"matcher": {"id": "byName", "options": "Par"},
|
||||||
|
"properties": [{"id": "custom.width", "value": 130}]}]),
|
||||||
|
])
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 2. LE SOLAIRE — excedent, censure, et ce que les chargeurs disent
|
||||||
|
# =====================================================================
|
||||||
|
solaire = dashboard("evcs-solaire", "Le solaire", (
|
||||||
|
"La production mesuree n'est jamais la production disponible : sans "
|
||||||
|
"injection, le MPPT ne produit que ce que le systeme peut absorber. "
|
||||||
|
"Ces panneaux montrent l'ecart entre les deux."), [
|
||||||
|
|
||||||
|
ts("Production, maison, et ce qui reste", [
|
||||||
|
sql("A", "SELECT ts AS time,\n"
|
||||||
|
" max(valeur) FILTER (WHERE cle = 'sensor.victron_pv_power_system') AS \"Production\",\n"
|
||||||
|
" max(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS \"Maison\",\n"
|
||||||
|
" max(valeur) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power') AS \"Voiture\",\n"
|
||||||
|
" max(valeur) FILTER (WHERE cle = 'sensor.victron_pv_power_system')\n"
|
||||||
|
" - max(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS \"Excedent\"\n"
|
||||||
|
"FROM mesure\n"
|
||||||
|
"WHERE $__timeFilter(ts)\n"
|
||||||
|
"GROUP BY ts\n"
|
||||||
|
"ORDER BY ts")],
|
||||||
|
PG, 0, 0, 24, 9, "watt",
|
||||||
|
"L'excedent (production moins maison hors voiture) est la grandeur qui "
|
||||||
|
"decide d'un demarrage depuis le 03/09. Le 03/09 a 19:44, la production "
|
||||||
|
"valait 758 W — au-dessus du seuil — pour un excedent de MOINS 444 W : "
|
||||||
|
"la voiture a vide la batterie.",
|
||||||
|
extra={"overrides": [
|
||||||
|
{"matcher": {"id": "byName", "options": "Production"},
|
||||||
|
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "yellow"}},
|
||||||
|
{"id": "custom.fillOpacity", "value": 22}]},
|
||||||
|
{"matcher": {"id": "byName", "options": "Maison"},
|
||||||
|
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "text"}},
|
||||||
|
{"id": "custom.fillOpacity", "value": 0}]},
|
||||||
|
{"matcher": {"id": "byName", "options": "Voiture"},
|
||||||
|
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "blue"}}]},
|
||||||
|
{"matcher": {"id": "byName", "options": "Excedent"},
|
||||||
|
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "green"}},
|
||||||
|
{"id": "custom.lineWidth", "value": 3},
|
||||||
|
{"id": "custom.fillOpacity", "value": 0}]}]}),
|
||||||
|
|
||||||
|
timeline("Production bridee ?", [
|
||||||
|
sql("A", "SELECT ts AS time,\n"
|
||||||
|
" CASE WHEN pv_censure THEN coalesce(motif_censure, 'bridee')\n"
|
||||||
|
" ELSE 'libre' END AS \"Production\"\n"
|
||||||
|
"FROM bilan\n"
|
||||||
|
"WHERE $__timeFilter(ts)\n"
|
||||||
|
"ORDER BY 1", fmt="table")],
|
||||||
|
PG, 0, 9, 12, 6,
|
||||||
|
"Bridee = de la ressource solaire est REFUSEE faute de preneur. "
|
||||||
|
"Charger la voiture pendant ces heures-la ne coute rien.",
|
||||||
|
mappings=[{"type": "value", "options": {
|
||||||
|
"libre": {"color": "green", "index": 0}}},
|
||||||
|
{"type": "regex", "options": {"pattern": ".*", "result": {"color": "orange", "index": 1}}}]),
|
||||||
|
|
||||||
|
timeline("Ce que les chargeurs declarent eux-memes", [
|
||||||
|
flux("A",
|
||||||
|
'from(bucket: "evcs")\n'
|
||||||
|
' |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n'
|
||||||
|
' |> filter(fn: (r) => r._field == "state")\n'
|
||||||
|
' |> filter(fn: (r) => r._measurement =~ /operation_mode$/)\n'
|
||||||
|
' |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n'
|
||||||
|
' |> map(fn: (r) => ({ r with tracker:\n'
|
||||||
|
' if r._measurement =~ /solar_1/ then "Sud 1"\n'
|
||||||
|
' else if r._measurement =~ /solar_2/ then "Sud 2"\n'
|
||||||
|
' else if r._measurement =~ /solar_3/ then "Sud 3"\n'
|
||||||
|
' else if r._measurement =~ /solar_4/ then "Ouest"\n'
|
||||||
|
' else if r._measurement =~ /mppt1/ then "Sud-est"\n'
|
||||||
|
' else "Ensemble" }))\n'
|
||||||
|
' |> keep(columns: ["_time", "_value", "tracker"])\n'
|
||||||
|
' |> pivot(rowKey: ["_time"], columnKey: ["tracker"], valueColumn: "_value")')],
|
||||||
|
IN, 12, 9, 12, 6,
|
||||||
|
"mppt_active = ce chargeur donne tout ce qu'il peut. "
|
||||||
|
"voltage_current_limited ou off = il se retient, faute de preneur. "
|
||||||
|
"Le 03/09 ces sept capteurs ont bascule 24 minutes avant le drapeau du "
|
||||||
|
"bilan — c'est le signal le plus direct dont on dispose.",
|
||||||
|
mappings=[{"type": "value", "options": {
|
||||||
|
"mppt_active": {"color": "green", "index": 0},
|
||||||
|
"voltage_current_limited": {"color": "orange", "index": 1},
|
||||||
|
"off": {"color": "dark-blue", "index": 2}}}]),
|
||||||
|
|
||||||
|
ts("Batterie", [
|
||||||
|
sql("A", "SELECT ts AS time,\n"
|
||||||
|
" max(valeur) FILTER (WHERE cle = 'sensor.victron_battery_soc') AS \"SOC\"\n"
|
||||||
|
"FROM mesure WHERE $__timeFilter(ts) GROUP BY ts ORDER BY ts"),
|
||||||
|
sql("B", "SELECT ts AS time,\n"
|
||||||
|
" max(valeur) FILTER (WHERE cle = 'sensor.victron_battery_current') AS \"Courant\"\n"
|
||||||
|
"FROM mesure WHERE $__timeFilter(ts) GROUP BY ts ORDER BY ts")],
|
||||||
|
PG, 0, 15, 12, 8, "percent",
|
||||||
|
"Le SOC n'est pas lineaire : 0,169 kWh par point au-dessus de 33 %, "
|
||||||
|
"0,097 en dessous (576 paliers mesures). Un point en bas d'echelle vaut "
|
||||||
|
"la moitie d'un point en haut.",
|
||||||
|
extra={"defaults": {"min": 0, "max": 100},
|
||||||
|
"overrides": [
|
||||||
|
{"matcher": {"id": "byName", "options": "Courant"},
|
||||||
|
"properties": [{"id": "unit", "value": "amp"},
|
||||||
|
{"id": "custom.axisPlacement", "value": "right"},
|
||||||
|
{"id": "max", "value": None}, {"id": "min", "value": None},
|
||||||
|
{"id": "color", "value": {"mode": "fixed", "fixedColor": "purple"}}]},
|
||||||
|
{"matcher": {"id": "byName", "options": "SOC"},
|
||||||
|
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "green"}},
|
||||||
|
{"id": "custom.lineWidth", "value": 3}]}]}),
|
||||||
|
|
||||||
|
ts("Le detail que PostgreSQL ne voit pas (~1 point/s)", [
|
||||||
|
flux("A",
|
||||||
|
'from(bucket: "evcs")\n'
|
||||||
|
' |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n'
|
||||||
|
' |> filter(fn: (r) => r._measurement == "W" and r._field == "value")\n'
|
||||||
|
' |> filter(fn: (r) => r.entity_id == "victron_pv_power_system"\n'
|
||||||
|
' or r.entity_id == "victron_consumption_power_l1"\n'
|
||||||
|
' or r.entity_id == "victron_evcs_ac_power"\n'
|
||||||
|
' or r.entity_id == "victron_grid_power_l1")\n'
|
||||||
|
' |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n'
|
||||||
|
' |> keep(columns: ["_time", "_value", "entity_id"])\n'
|
||||||
|
' |> pivot(rowKey: ["_time"], columnKey: ["entity_id"], valueColumn: "_value")')],
|
||||||
|
IN, 12, 15, 12, 8, "watt",
|
||||||
|
"InfluxDB enregistre chaque changement d'etat, pas un echantillon par "
|
||||||
|
"minute. C'est la resolution qu'il faut pour voir un transitoire de "
|
||||||
|
"reprise du reseau (~150 s) ou une pointe de cuisson.",
|
||||||
|
extra={"defaults": {"custom": {"lineWidth": 1, "fillOpacity": 5,
|
||||||
|
"showPoints": "never", "spanNulls": True}}}),
|
||||||
|
])
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 3. PREVISION CONTRE REEL — le biais de forme, question ouverte
|
||||||
|
# =====================================================================
|
||||||
|
prev_sql = """WITH du_matin AS (
|
||||||
|
SELECT DISTINCT ON (jour) jour, publie_a, horaire
|
||||||
|
FROM prevision
|
||||||
|
WHERE publie_a::date = jour
|
||||||
|
AND jour BETWEEN $__timeFrom()::date AND $__timeTo()::date
|
||||||
|
ORDER BY jour, publie_a ASC
|
||||||
|
),
|
||||||
|
prevu AS (
|
||||||
|
SELECT (jour + (h - 1) * interval '1 hour') AS heure,
|
||||||
|
horaire[h] AS kwh
|
||||||
|
FROM du_matin, generate_subscripts(horaire, 1) AS h
|
||||||
|
),
|
||||||
|
produit AS (
|
||||||
|
-- Un echantillon toutes les 60 s : chaque point vaut 1/60 h.
|
||||||
|
SELECT date_trunc('hour', ts) AS heure,
|
||||||
|
sum(valeur) / 60.0 / 1000.0 AS kwh
|
||||||
|
FROM mesure
|
||||||
|
WHERE cle = 'sensor.victron_pv_power_system' AND $__timeFilter(ts)
|
||||||
|
GROUP BY 1
|
||||||
|
)
|
||||||
|
SELECT coalesce(prevu.heure, produit.heure) AS time,
|
||||||
|
round(prevu.kwh::numeric, 2) AS "Prevu le matin meme",
|
||||||
|
round(produit.kwh::numeric, 2) AS "Reellement produit"
|
||||||
|
FROM prevu FULL OUTER JOIN produit USING (heure)
|
||||||
|
WHERE coalesce(prevu.heure, produit.heure) BETWEEN $__timeFrom() AND $__timeTo()
|
||||||
|
ORDER BY 1"""
|
||||||
|
|
||||||
|
prevision = dashboard("evcs-prevision", "Prevision contre reel", (
|
||||||
|
"Le total de la prevision tombe juste ; sa FORME non. Sur les trois jours "
|
||||||
|
"verifies le 04/09, la production arrive plus tot que prevu et "
|
||||||
|
"l'apres-midi est sur-promis d'un facteur 2 a 3 apres 16 h. Ce tableau "
|
||||||
|
"existe pour instruire cette question sur des semaines, pas sur trois "
|
||||||
|
"jours."), [
|
||||||
|
|
||||||
|
barchart("Heure par heure : promis contre livre",
|
||||||
|
[sql("A", prev_sql, fmt="table")],
|
||||||
|
PG, 0, 0, 24, 10, "kwatth",
|
||||||
|
"ATTENTION a l'interpretation : \"produit\" est une mesure CENSUREE. "
|
||||||
|
"Moins que prevu peut vouloir dire mauvaise prevision OU absence de "
|
||||||
|
"preneur. Lire la bande de censure juste dessous avant de conclure."),
|
||||||
|
|
||||||
|
timeline("Y avait-il un preneur ?", [
|
||||||
|
sql("A", "SELECT ts AS time,\n"
|
||||||
|
" CASE WHEN pv_censure THEN 'bridee (mesure inexploitable)'\n"
|
||||||
|
" ELSE 'libre (mesure valide)' END AS \"Production\"\n"
|
||||||
|
"FROM bilan WHERE $__timeFilter(ts) ORDER BY 1", fmt="table")],
|
||||||
|
PG, 0, 10, 24, 5,
|
||||||
|
"Seules les heures 'libre' permettent de juger la prevision. "
|
||||||
|
"Les heures bridees ne disent rien de la ressource disponible.",
|
||||||
|
mappings=[{"type": "value", "options": {
|
||||||
|
"libre (mesure valide)": {"color": "green", "index": 0},
|
||||||
|
"bridee (mesure inexploitable)": {"color": "orange", "index": 1}}}]),
|
||||||
|
|
||||||
|
table("Les previsions telles qu'elles ont ete publiees", [
|
||||||
|
sql("A", "SELECT publie_a AS \"Publiee le\", jour AS \"Pour le jour\",\n"
|
||||||
|
" round(total_kwh::numeric, 1) AS \"Total annonce (kWh)\",\n"
|
||||||
|
" round((horaire[8] + horaire[9] + horaire[10] + horaire[11])::numeric, 2) AS \"07-11 h\",\n"
|
||||||
|
" round((horaire[12] + horaire[13] + horaire[14] + horaire[15])::numeric, 2) AS \"11-15 h\",\n"
|
||||||
|
" round((horaire[16] + horaire[17] + horaire[18] + horaire[19])::numeric, 2) AS \"15-19 h\"\n"
|
||||||
|
"FROM prevision\n"
|
||||||
|
"WHERE $__timeFilter(publie_a)\n"
|
||||||
|
"ORDER BY publie_a DESC, jour", fmt="table")],
|
||||||
|
PG, 0, 15, 24, 9,
|
||||||
|
"Sans l'horodatage de PUBLICATION, on ne peut pas mesurer le biais : "
|
||||||
|
"comparer une prevision au reel n'a de sens que si l'on sait quand elle "
|
||||||
|
"a ete faite. C'est la raison d'etre de cette table."),
|
||||||
|
])
|
||||||
|
|
||||||
|
base = pathlib.Path('deploy/historisation/grafana/dashboards')
|
||||||
|
for nom, d in [('nuit.json', nuit), ('solaire.json', solaire),
|
||||||
|
('prevision.json', prevision)]:
|
||||||
|
(base / nom).write_text(json.dumps(d, indent=2, ensure_ascii=False) + '\n')
|
||||||
|
print(f"{nom:16s} {len(d['panels'])} panneaux")
|
||||||
86
tools/verifier-dashboards.mjs
Normal file
86
tools/verifier-dashboards.mjs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Verifie que CHAQUE requete des tableaux de bord Grafana s'execute vraiment.
|
||||||
|
//
|
||||||
|
// Grafana n'annonce une requete cassee qu'a l'ecran, panneau par panneau. Ce
|
||||||
|
// controle rejoue les requetes SQL contre PostgreSQL et les requetes Flux
|
||||||
|
// contre InfluxDB, avec les macros substituees comme Grafana le fait.
|
||||||
|
//
|
||||||
|
// node tools/verifier-dashboards.mjs
|
||||||
|
//
|
||||||
|
// Sortie non nulle si une seule requete echoue.
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import pg from 'pg';
|
||||||
|
|
||||||
|
const env = Object.fromEntries(
|
||||||
|
fs.readFileSync('.env', 'utf8').split('\n').filter(l => l.includes('='))
|
||||||
|
.map(l => [l.slice(0, l.indexOf('=')).trim(), l.slice(l.indexOf('=') + 1).trim()]));
|
||||||
|
|
||||||
|
const DEPUIS = new Date(Date.now() - 24 * 3600e3).toISOString();
|
||||||
|
const JUSQUA = new Date().toISOString();
|
||||||
|
|
||||||
|
// Les macros de Grafana, telles qu'il les developpe cote serveur.
|
||||||
|
const macros = (q) => q
|
||||||
|
.replace(/\$__timeFilter\(([^)]+)\)/g, `$1 BETWEEN '${DEPUIS}' AND '${JUSQUA}'`)
|
||||||
|
.replace(/\$__timeFrom\(\)/g, `'${DEPUIS}'`)
|
||||||
|
.replace(/\$__timeTo\(\)/g, `'${JUSQUA}'`);
|
||||||
|
|
||||||
|
const fluxVars = (q) => q
|
||||||
|
.replace(/v\.timeRangeStart/g, `${DEPUIS}`)
|
||||||
|
.replace(/v\.timeRangeStop/g, `${JUSQUA}`)
|
||||||
|
.replace(/v\.windowPeriod/g, '1m');
|
||||||
|
|
||||||
|
const client = new pg.Client({
|
||||||
|
host: '10.10.10.22', port: 5433, database: 'evcs',
|
||||||
|
user: 'evcs_lecture', password: env.PG_LECTURE_PASSWORD });
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
async function flux(q) {
|
||||||
|
const r = await fetch('http://10.10.10.22:8086/api/v2/query?org=airtria', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Token ${env.INFLUXDB_TOKEN}`,
|
||||||
|
'Content-Type': 'application/vnd.flux', Accept: 'application/csv' },
|
||||||
|
body: q });
|
||||||
|
const t = await r.text();
|
||||||
|
if (!r.ok) throw new Error(t.slice(0, 300));
|
||||||
|
return t.split('\n').filter(l => l.includes(',_result,')).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dossier = 'deploy/historisation/grafana/dashboards';
|
||||||
|
let echecs = 0, total = 0;
|
||||||
|
|
||||||
|
for (const f of fs.readdirSync(dossier).filter(f => f.endsWith('.json')).sort()) {
|
||||||
|
const d = JSON.parse(fs.readFileSync(path.join(dossier, f), 'utf8'));
|
||||||
|
console.log(`\n=== ${d.title} (${f}) ===`);
|
||||||
|
|
||||||
|
// Les annotations sont des requetes comme les autres, et elles s'executent
|
||||||
|
// sur TOUS les panneaux : une annotation cassee casse tout le tableau.
|
||||||
|
const cibles = [];
|
||||||
|
for (const a of d.annotations.list) {
|
||||||
|
if (a.target?.rawSql) cibles.push({ ou: 'annotation ' + a.name, sql: a.target.rawSql });
|
||||||
|
}
|
||||||
|
for (const p of d.panels) {
|
||||||
|
for (const t of p.targets ?? []) {
|
||||||
|
if (t.rawSql) cibles.push({ ou: p.title, sql: t.rawSql });
|
||||||
|
else if (t.query) cibles.push({ ou: p.title, flux: t.query });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const c of cibles) {
|
||||||
|
total++;
|
||||||
|
try {
|
||||||
|
const n = c.sql ? (await client.query(macros(c.sql))).rows.length
|
||||||
|
: await flux(fluxVars(c.flux));
|
||||||
|
const vide = n === 0 ? ' (aucune ligne sur 24 h — normal si la mesure vient de demarrer)' : '';
|
||||||
|
console.log(` OK ${String(n).padStart(5)} lignes ${c.ou}${vide}`);
|
||||||
|
} catch (e) {
|
||||||
|
echecs++;
|
||||||
|
console.log(` ECHEC ${c.ou}\n ${String(e.message).split('\n')[0]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.end();
|
||||||
|
console.log(`\n${total - echecs}/${total} requetes valides`);
|
||||||
|
process.exit(echecs ? 1 : 0);
|
||||||
Reference in New Issue
Block a user