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:
@@ -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")
|
||||
|
||||
@@ -349,6 +349,12 @@ async function migrer(client) {
|
||||
debranche_a timestamptz,
|
||||
brut jsonb
|
||||
)`);
|
||||
// Part venue du reseau. On se fie au `source_mode` du controleur — son
|
||||
// INTENTION, sans ambiguite — et non a la puissance reseau instantanee, qui
|
||||
// peut alimenter la maison pendant que le soleil alimente la voiture.
|
||||
await client.query(
|
||||
'ALTER TABLE session_charge ADD COLUMN IF NOT EXISTS energie_reseau_kwh '
|
||||
+ 'double precision NOT NULL DEFAULT 0');
|
||||
await client.query(
|
||||
'CREATE INDEX IF NOT EXISTS session_charge_debut_idx ON session_charge (debut DESC)');
|
||||
// Le role de lecture doit voir la nouvelle table. S'il n'existe pas encore,
|
||||
@@ -413,6 +419,7 @@ async function ecrireSession(sql, client, st) {
|
||||
`UPDATE session_charge SET
|
||||
fin = $2,
|
||||
energie_kwh = energie_kwh + $3 * $4 / 3600000.0,
|
||||
energie_reseau_kwh = energie_reseau_kwh + $8 * $3 * $4 / 3600000.0,
|
||||
p_max_w = GREATEST(coalesce(p_max_w, 0), $3),
|
||||
courant_max_a = GREATEST(coalesce(courant_max_a, 0), coalesce($5, 0)),
|
||||
consigne_max_a = GREATEST(coalesce(consigne_max_a, 0), coalesce($6, 0)),
|
||||
@@ -421,7 +428,8 @@ async function ecrireSession(sql, client, st) {
|
||||
WHERE debut = $1`,
|
||||
[ouverte.debut, maintenant, p, dt,
|
||||
num(g('sensor.victron_evcs_current')?.state),
|
||||
num(g('sensor.victron_evcs_set_current')?.state), vehicule]);
|
||||
num(g('sensor.victron_evcs_set_current')?.state), vehicule,
|
||||
txt(g('input_text.victron_evcs_source_mode')?.state).startsWith('Réseau') ? 1 : 0]);
|
||||
n++;
|
||||
}
|
||||
} else if (ouverte) {
|
||||
|
||||
Reference in New Issue
Block a user