Tableau « Les vehicules » : ce que chaque voiture encaisse, par periode

Demande du proprietaire : voir ce que la JEEP ou la VW encaisse dans une
session, et evaluer sur une periode (jour, semaine, mois), en assumant
l'imprecision liee a l'impossibilite d'identifier le vehicule au branchement.

Trois panneaux : les totaux par vehicule sur la periode, l'energie par periode
et par vehicule avec le meme pas adaptatif que les autres tableaux (heure /
jour / mois), et la liste des sessions.

L'incertitude est AFFICHEE plutot que masquee :
- un compteur « Etiquette douteuse » — sessions ou le selecteur a change en
  cours de route, ou n'etait pas renseigne ;
- une colonne « Doute » dans le tableau ;
- une serie « Non etiquete » dans le graphique ;
- l'heure de debranchement, qui confirme apres coup puisque les deux vehicules
  partent a des heures differentes.

Le commentaire du generateur consigne pourquoi aucune verification n'est
possible : electriquement les deux voitures sont indiscernables (3 680 contre
3 655 W a consigne identique de 20 A), et horairement la routine s'inverse le
week-end, donc une regle horaire serait fausse un week-end sur deux.

Ajout a la table : `energie_reseau_kwh`, la part venue du reseau, fondee sur le
`source_mode` du controleur — son intention — et non sur la puissance reseau
instantanee, laquelle peut alimenter la maison pendant que le soleil alimente
la voiture.

52/53 requetes verifiees. La seule en echec porte sur cette nouvelle colonne,
que la migration creera au prochain demarrage du service.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
Tallagrand Thierry
2026-09-05 03:04:06 +02:00
parent e80dbf70fb
commit c00a2107b3
4 changed files with 512 additions and 2 deletions

View File

@@ -950,10 +950,119 @@ systeme = dashboard("evcs-systeme", "Le systeme", (
transformations=FILTRE_VUE),
])
# =====================================================================
# 6. LES VEHICULES — ce que chaque voiture encaisse, et sur quelle periode
# =====================================================================
# ⚠️ LIMITE STRUCTURELLE, a garder en tete a chaque lecture de ce tableau.
# L'etiquette du vehicule vient d'un SELECTEUR MANUEL. Elle est fiable quand
# le proprietaire l'a positionnee au branchement, et fausse s'il l'a oubliee ou
# si un redemarrage de Home Assistant l'a annulee (constate le 04/09 a 19:44).
#
# Il n'existe aucun moyen de la verifier :
# - electriquement, les deux voitures sont indiscernables. Mesure du 05/09 a
# consigne identique de 20 A : la VW accepte 3 680 W, la JEEP 3 655 W,
# soit 0,1 A d'ecart. Meme chargeur embarque de 3,7 kW, meme plateau.
# - horairement, la routine s'inverse le week-end : en semaine la JEEP charge
# le midi et la VW la nuit, mais le samedi la JEEP charge la nuit et la VW
# en journee. Une regle horaire serait fausse un week-end sur deux.
#
# D'ou la colonne « Doute » du tableau et le compteur d'etiquettes douteuses :
# on affiche l'incertitude au lieu de la masquer.
SESSIONS_PERIODE = """WITH bornes AS (
SELECT $__timeFrom()::timestamptz AS d, $__timeTo()::timestamptz AS f
),
pas AS (
SELECT CASE WHEN f - d > interval '70 days' THEN 'month'
WHEN f - d > interval '4 days' THEN 'day'
ELSE 'hour' END AS unite,
CASE WHEN f - d > interval '70 days' THEN 'MM/YYYY'
WHEN f - d > interval '4 days' THEN 'DD/MM'
ELSE 'DD/MM HH24' END AS forme
FROM bornes
)
SELECT to_char(date_trunc(pas.unite, debut), pas.forme) AS "Periode",
round(coalesce(sum(energie_kwh) FILTER (WHERE vehicule = 'VW'), 0)::numeric, 2) AS "VW",
round(coalesce(sum(energie_kwh) FILTER (WHERE vehicule = 'JEEP'), 0)::numeric, 2) AS "JEEP",
round(coalesce(sum(energie_kwh) FILTER (WHERE vehicule IS NULL), 0)::numeric, 2) AS "Non etiquete"
FROM session_charge, pas
WHERE $__timeFilter(debut)
GROUP BY date_trunc(pas.unite, debut), pas.forme
ORDER BY date_trunc(pas.unite, debut)"""
vehicules = dashboard("evcs-vehicules", "Les vehicules", (
"Ce que chaque voiture encaisse, session par session et sur la periode "
"choisie. L'etiquette vient d'un selecteur manuel : elle peut etre fausse, "
"et rien ne permet de la verifier — les deux vehicules sont "
"electriquement indiscernables et leur routine s'inverse le week-end."),
frm="now-7d", refresh="5m", panels=[
stat("Sur la periode", [
sql("A",
"SELECT round(coalesce(sum(energie_kwh) FILTER (WHERE vehicule = 'VW'), 0)::numeric, 1) AS \"VW\",\n"
" round(coalesce(sum(energie_kwh) FILTER (WHERE vehicule = 'JEEP'), 0)::numeric, 1) AS \"JEEP\",\n"
" count(*) AS \"Sessions\",\n"
" count(*) FILTER (WHERE NOT vehicule_sur OR vehicule IS NULL) AS \"Etiquette douteuse\"\n"
"FROM session_charge WHERE $__timeFilter(debut)", fmt="table")],
PG, 0, 0, 24, 5,
"« Etiquette douteuse » compte les sessions ou le selecteur a change en "
"cours de route, ou n'etait pas renseigne. C'est la mesure honnete de "
"l'incertitude : elle ne peut pas etre levee autrement qu'en positionnant "
"le selecteur au branchement.",
taille={"valueSize": 34, "titleSize": 12},
overrides=[
sur("VW", [{"id": "unit", "value": "kwatth"},
{"id": "color", "value": {"mode": "fixed", "fixedColor": "blue"}}]),
sur("JEEP", [{"id": "unit", "value": "kwatth"},
{"id": "color", "value": {"mode": "fixed", "fixedColor": "green"}}]),
sur("Etiquette douteuse", [{"id": "thresholds", "value": seuils(
("green", None), ("orange", 1))}])]),
barchart("Energie par periode et par vehicule",
[sql("A", SESSIONS_PERIODE, fmt="table")],
PG, 0, 5, 24, 9, "kwatth",
"Le pas suit la plage choisie : l'heure jusqu'a 4 jours, le jour jusqu'a "
"70, le mois au-dela. « Non etiquete » est ce qui a ete charge sans que le "
"selecteur soit renseigne — a interpreter, pas a ignorer."),
table("Les sessions", [
sql("A",
"SELECT to_char(debut, 'Dy DD/MM HH24:MI') AS \"Debut\",\n"
" coalesce(vehicule, '?') AS \"Vehicule\",\n"
" CASE WHEN vehicule_sur THEN '' ELSE 'selecteur change' END AS \"Doute\",\n"
" round((extract(epoch FROM fin - debut) / 60)::numeric) AS \"Minutes\",\n"
" round(energie_kwh::numeric, 2) AS \"kWh\",\n"
" round(energie_reseau_kwh::numeric, 2) AS \"dont reseau\",\n"
" round(p_max_w::numeric) AS \"P max\",\n"
" consigne_max_a AS \"Consigne max\",\n"
" source_mode AS \"Source au depart\",\n"
" to_char(debranche_a, 'Dy HH24:MI') AS \"Debranche\"\n"
"FROM session_charge WHERE $__timeFilter(debut) ORDER BY debut DESC", fmt="table")],
PG, 0, 14, 24, 11,
"« Debranche » confirme le vehicule apres coup : les deux partent a des "
"heures differentes. « dont reseau » se fonde sur le mode source declare "
"par le controleur — son intention — et non sur la puissance reseau "
"instantanee, qui peut alimenter la maison pendant que le soleil alimente "
"la voiture.",
overrides=[
sur("Vehicule", [{"id": "custom.width", "value": 100},
{"id": "custom.cellOptions", "value": {"type": "color-text"}},
{"id": "mappings", "value": [{"type": "value", "options": {
"VW": {"color": "blue", "index": 0},
"JEEP": {"color": "green", "index": 1},
"?": {"color": "orange", "index": 2}}}]}]),
sur("Doute", [{"id": "custom.width", "value": 150}]),
sur("kWh", [{"id": "unit", "value": "kwatth"}]),
sur("dont reseau", [{"id": "unit", "value": "kwatth"}]),
sur("P max", [{"id": "unit", "value": "watt"}])]),
])
base = pathlib.Path('deploy/historisation/grafana/dashboards')
for nom, d in [('nuit.json', nuit), ('solaire.json', solaire),
('prevision.json', prevision),
('telephone.json', telephone),
('systeme.json', systeme)]:
('systeme.json', systeme),
('vehicules.json', vehicules)]:
(base / nom).write_text(json.dumps(d, indent=2, ensure_ascii=False) + '\n')
print(f"{nom:16s} {len(d['panels'])} panneaux")