Un tableau pour la journee, et le report cesse de fabriquer des kilowattheures
Il manquait le bilan qu'on cherche en premier : ce qui a ete produit, consomme, achete, et ou c'est alle. « La journee » (evcs-journee) le donne sur la fenetre now/d — le jour civil, pas les 24 dernieres heures, sinon deux journees se melangent et le total de production n'est plus comparable d'un jour a l'autre. En controlant ses chiffres, un defaut bien plus large est apparu : la voiture annoncait 6,9 kWh la ou le compteur cumulatif de la borne et la table des sessions donnent tous deux 3,6. Le report de la derniere valeur connue propageait la MOYENNE de la minute au lieu de sa DERNIERE valeur. Le 07/09 a 14:28 la borne passe de 1577 a 0 W dans la meme minute : cette minute vaut 788 W en moyenne, et le report la maintenait pendant les heures sans ecriture qui suivaient — 0,79 kWh fabriques sur la seule heure de 15 h, exactement 788 x 60 / 1000. Le defaut ne se voyait ni sur le solaire ni sur la maison, ecrits en continu. Il frappe les grandeurs intermittentes, au premier rang la borne qui passe l'essentiel de la journee a zero. Les colonnes « Voiture » de « Le systeme » et « Les vehicules » etaient donc fausses depuis leur creation, en surestimation. Corrige dans les trois constructeurs de requetes : la minute observee garde sa moyenne, juste pour l'energie consommee dans cette minute, mais le report propage la derniere valeur reellement lue. Aucun seuil, aucune peremption arbitraire. Apres correction la voiture tombe a 3,6 kWh, egal au centieme a la somme des sessions. verifier-dashboards annoncait 73/73 avec le chiffre faux : il prouve qu'une requete s'execute, pas qu'elle rend la bonne grandeur. Deuxieme fois en trois jours qu'une source independante est le seul controle qui attrape le defaut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
@@ -34,17 +34,22 @@ def sql_grandeurs(refid, colonnes, calculees=None, fmt="time_series"):
|
||||
"""
|
||||
noms = [n for _, n in colonnes]
|
||||
par_minute = ",\n".join(
|
||||
f" avg(valeur) FILTER (WHERE cle = '{c}') AS {n}" for c, n in colonnes)
|
||||
groupes = ",\n".join(f" count({n}) OVER (ORDER BY t) AS g_{n}" for n in noms)
|
||||
f" avg(valeur) FILTER (WHERE cle = '{c}') AS {n},\n"
|
||||
f" (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = '{c}'))[1]"
|
||||
f" AS d_{n}" for c, n in colonnes)
|
||||
groupes = ",\n".join(
|
||||
f" {n}, count(d_{n}) OVER (ORDER BY t) AS g_{n}" for n in noms)
|
||||
remplis = ",\n".join(
|
||||
f" first_value({n}) OVER (PARTITION BY g_{n} ORDER BY t) AS {n}" for n in noms)
|
||||
f" coalesce({n}, first_value(d_{n}) OVER (PARTITION BY g_{n} ORDER BY t))"
|
||||
f" AS {n}" for n in noms)
|
||||
sorties = ",\n".join(f' {n} AS "{n.replace("_", " ")}"' for n in noms)
|
||||
if calculees:
|
||||
sorties += ",\n" + ",\n".join(f' {e} AS "{lib}"' for e, lib in calculees)
|
||||
q = (f"WITH par_minute AS (\n"
|
||||
f" SELECT date_trunc('minute', ts) AS t,\n{par_minute}\n"
|
||||
f" FROM mesure WHERE $__timeFilter(ts) GROUP BY 1\n"
|
||||
f"),\npaliers AS (\n SELECT t, {', '.join(noms)},\n{groupes}\n FROM par_minute\n"
|
||||
f"),\npaliers AS (\n SELECT t,\n{groupes},\n"
|
||||
+ ",\n".join(f" d_{n}" for n in noms) + f"\n FROM par_minute\n"
|
||||
f"),\nreport AS (\n SELECT t,\n{remplis}\n FROM paliers\n)\n"
|
||||
f"SELECT t AS time,\n{sorties}\nFROM report ORDER BY t")
|
||||
return sql(refid, q, fmt=fmt)
|
||||
@@ -709,22 +714,26 @@ axe AS (
|
||||
releve AS (
|
||||
SELECT date_trunc('minute', ts) AS t,
|
||||
""" + ",\n".join(
|
||||
f" avg(valeur) FILTER (WHERE cle = '{c}') AS {n}" for c, n in ENERGIE_CLES) + """
|
||||
f" avg(valeur) FILTER (WHERE cle = '{c}') AS {n},\n"
|
||||
f" (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = '{c}'))[1] AS d_{n}"
|
||||
for c, n in ENERGIE_CLES) + """
|
||||
FROM mesure WHERE $__timeFilter(ts) GROUP BY 1
|
||||
),
|
||||
par_minute AS (
|
||||
SELECT axe.t, """ + ", ".join(f"releve.{n}" for _, n in ENERGIE_CLES) + """
|
||||
SELECT axe.t, """ + ", ".join(
|
||||
f"releve.{n}, releve.d_{n}" for _, n in ENERGIE_CLES) + """
|
||||
FROM axe LEFT JOIN releve USING (t)
|
||||
),
|
||||
paliers AS (
|
||||
SELECT t, """ + ", ".join(n for _, n in ENERGIE_CLES) + ",\n" + ",\n".join(
|
||||
f" count({n}) OVER (ORDER BY t) AS g_{n}" for _, n in ENERGIE_CLES) + """
|
||||
SELECT t, """ + ", ".join(
|
||||
f"{n}, d_{n}" for _, n in ENERGIE_CLES) + ",\n" + ",\n".join(
|
||||
f" count(d_{n}) OVER (ORDER BY t) AS g_{n}" for _, n in ENERGIE_CLES) + """
|
||||
FROM par_minute
|
||||
),
|
||||
report AS (
|
||||
SELECT t,
|
||||
""" + ",\n".join(
|
||||
f" first_value({n}) OVER (PARTITION BY g_{n} ORDER BY t) AS {n}"
|
||||
f" coalesce({n}, first_value(d_{n}) OVER (PARTITION BY g_{n} ORDER BY t)) AS {n}"
|
||||
for _, n in ENERGIE_CLES) + """
|
||||
FROM paliers
|
||||
)
|
||||
@@ -1197,8 +1206,173 @@ batteries = dashboard("evcs-batteries", "Les batteries", (
|
||||
|
||||
], frm="now-7d", variables=[VARIABLE_MODULE])
|
||||
|
||||
# =====================================================================
|
||||
# 8. LA JOURNEE — le bilan condense, de minuit a maintenant
|
||||
# =====================================================================
|
||||
# Les sept autres tableaux sont thematiques : on y va quand on sait deja quoi
|
||||
# chercher. Celui-ci repond a la question qu'on se pose en premier — « qu'est-ce
|
||||
# qui s'est passe aujourd'hui ». D'ou la fenetre `now/d` (le jour civil en
|
||||
# cours) et non `now-24h`, qui melangerait deux journees et rendrait le total
|
||||
# de production incomparable d'un jour a l'autre.
|
||||
#
|
||||
# Toutes les energies sont obtenues en INTEGRANT la puissance, jamais en lisant
|
||||
# les compteurs kWh du GX : ceux-ci se remettent a zero sans le signaler
|
||||
# (verifie le 04/09, -373 kWh enregistres sur une heure cote Home Assistant).
|
||||
|
||||
BILAN_CLES = [
|
||||
('sensor.victron_pv_power_system', 'pv'),
|
||||
('sensor.victron_consumption_power_l1', 'maison'),
|
||||
('sensor.victron_grid_power_l1', 'reseau'),
|
||||
('sensor.victron_evcs_ac_power', 'voiture'),
|
||||
('sensor.victron_battery_power', 'batterie'),
|
||||
]
|
||||
_bn = [n for _, n in BILAN_CLES]
|
||||
|
||||
BILAN_BASE = ("""WITH axe AS (
|
||||
SELECT generate_series(date_trunc('minute', $__timeFrom()::timestamptz),
|
||||
date_trunc('minute', $__timeTo()::timestamptz),
|
||||
interval '1 minute') AS t
|
||||
),
|
||||
releve AS (
|
||||
SELECT date_trunc('minute', ts) AS t,
|
||||
"""
|
||||
+ ",\n".join(
|
||||
f" avg(valeur) FILTER (WHERE cle = '{c}') AS {n},\n"
|
||||
f" (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = '{c}'))[1] AS d_{n}"
|
||||
for c, n in BILAN_CLES) + """
|
||||
FROM mesure WHERE $__timeFilter(ts) GROUP BY 1
|
||||
),
|
||||
par_minute AS (
|
||||
SELECT axe.t, """ + ", ".join(f"releve.{n}, releve.d_{n}" for n in _bn) + """
|
||||
FROM axe LEFT JOIN releve USING (t)
|
||||
),
|
||||
paliers AS (
|
||||
SELECT t, """ + ", ".join(f"{n}, d_{n}" for n in _bn) + ",\n"
|
||||
+ ",\n".join(f" count(d_{n}) OVER (ORDER BY t) AS g_{n}" for n in _bn) + """
|
||||
FROM par_minute
|
||||
),
|
||||
report AS (
|
||||
SELECT t,
|
||||
""" + ",\n".join(
|
||||
f" coalesce({n}, first_value(d_{n}) OVER (PARTITION BY g_{n} ORDER BY t), 0)"
|
||||
f" AS {n}" for n in _bn) + """
|
||||
FROM paliers
|
||||
)
|
||||
""")
|
||||
|
||||
# Une seule ligne, six colonnes : c'est le resume que l'on lit en premier.
|
||||
# L'autonomie se calcule sur la maison SANS la voiture, sinon une grosse charge
|
||||
# solaire du vehicule gonfle artificiellement le taux.
|
||||
BILAN_TOTAUX = BILAN_BASE + """SELECT
|
||||
round((sum(pv) / 60.0 / 1000.0)::numeric, 1) AS "Solaire produit",
|
||||
round((sum(maison) / 60.0 / 1000.0)::numeric, 1) AS "Maison consommee",
|
||||
round((sum(reseau) / 60.0 / 1000.0)::numeric, 1) AS "Reseau importe",
|
||||
round((sum(voiture) / 60.0 / 1000.0)::numeric, 1) AS "Voiture",
|
||||
round((sum(greatest(batterie, 0)) / 60.0 / 1000.0)::numeric, 1) AS "Batterie chargee",
|
||||
round((sum(greatest(-batterie, 0)) / 60.0 / 1000.0)::numeric, 1) AS "Batterie rendue",
|
||||
round((100.0 * greatest(sum(maison) - sum(reseau), 0)
|
||||
/ nullif(sum(maison), 0))::numeric, 0) AS "Autonomie"
|
||||
FROM report"""
|
||||
|
||||
BILAN_FLUX = BILAN_BASE + """SELECT t AS time,
|
||||
pv AS "Production solaire",
|
||||
maison AS "Consommation maison",
|
||||
voiture AS "Charge du vehicule",
|
||||
reseau AS "Reseau",
|
||||
batterie AS "Batterie (+ charge, - decharge)"
|
||||
FROM report ORDER BY t"""
|
||||
|
||||
journee = dashboard(
|
||||
'evcs-journee', 'La journee',
|
||||
"Le bilan de la journee en cours, de minuit a maintenant : ce qui a ete "
|
||||
"produit, ce qui a ete consomme, ce qui a ete achete, et ou c'est alle. "
|
||||
"Les energies sont integrees depuis les puissances mesurees.",
|
||||
[
|
||||
stat("Le bilan depuis minuit", [sql("A", BILAN_TOTAUX, fmt="table")],
|
||||
PG, 0, 0, 24, 5,
|
||||
desc="Sept chiffres, une ligne. L'autonomie est la part de la "
|
||||
"consommation de la maison qui n'est PAS venue du reseau. "
|
||||
"« Batterie rendue » est ce qu'elle a restitue, donc du solaire "
|
||||
"differe et non une consommation supplementaire.",
|
||||
overrides=[sur("Autonomie", [{"id": "unit", "value": "percent"},
|
||||
{"id": "thresholds", "value": seuils(
|
||||
("red", None), ("#EAB839", 60),
|
||||
("green", 85))}]),
|
||||
sur("Reseau importe", [{"id": "thresholds", "value": seuils(
|
||||
("green", None), ("#EAB839", 3),
|
||||
("orange", 8))}])],
|
||||
champs="/.*/"),
|
||||
|
||||
dict(barchart("Energie heure par heure", [sql("A", ENERGIE_SQL, fmt="table")],
|
||||
PG, 0, 5, 24, 9, "kwatth",
|
||||
"La forme de la journee : quand le solaire a couvert la maison, et quand "
|
||||
"il a fallu acheter. Le pas suit la periode choisie."),
|
||||
transformations=FILTRE_VUE),
|
||||
|
||||
ts("Les flux de la journee", [sql("A", BILAN_FLUX)],
|
||||
PG, 0, 14, 24, 10, "watt",
|
||||
desc="Les cinq puissances sur le meme axe. La batterie est signee : "
|
||||
"au-dessus de zero elle se remplit, en dessous elle porte la maison. "
|
||||
"Quand « Reseau » decolle alors que la production est forte, c'est "
|
||||
"une fermeture du contacteur qu'il faut expliquer."),
|
||||
|
||||
ts("Etat de charge, et ce que la nuit exigera", [
|
||||
sql("A", "SELECT ts AS time, valeur AS \"SOC reel\"\n"
|
||||
"FROM mesure WHERE cle = 'sensor.victron_battery_soc'\n"
|
||||
" AND $__timeFilter(ts) ORDER BY 1"),
|
||||
sql("B", "SELECT ts AS time, soc_requis_pct AS \"SOC requis par le bilan\"\n"
|
||||
"FROM bilan WHERE $__timeFilter(ts) AND statut = 'ok' ORDER BY 1")],
|
||||
PG, 0, 24, 12, 9, "percent",
|
||||
desc="Le SOC reel contre ce que le bilan energetique reclame pour "
|
||||
"traverser la nuit jusqu'au croisement production/consommation. "
|
||||
"Quand le reel passe sous la ligne en fin de journee, le reseau sera "
|
||||
"necessaire avant le matin."),
|
||||
|
||||
timeline("Reseau, source et decisions", [
|
||||
sql("A", "SELECT ts AS time, source_mode AS \"Source\"\n"
|
||||
"FROM decision WHERE $__timeFilter(ts) ORDER BY 1", fmt="table"),
|
||||
sql("B", "SELECT ts AS time, etat AS \"Etat du controleur\"\n"
|
||||
"FROM decision WHERE $__timeFilter(ts) ORDER BY 1", fmt="table"),
|
||||
sql("C", "SELECT ts AS time,\n"
|
||||
" CASE WHEN pv_censure THEN 'production bridee'\n"
|
||||
" ELSE 'production libre' END AS \"Production\"\n"
|
||||
"FROM bilan WHERE $__timeFilter(ts) ORDER BY 1", fmt="table")],
|
||||
PG, 12, 24, 12, 9,
|
||||
desc="Trois bandes qui se lisent ensemble. « Production bridee » signale "
|
||||
"que le solaire n'avait plus de preneur : l'energie manquante ce "
|
||||
"moment-la a ete refusee, pas absente."),
|
||||
|
||||
table("Les charges du jour", [sql("A",
|
||||
"SELECT to_char(debut AT TIME ZONE 'Europe/Paris', 'HH24:MI') AS \"Debut\",\n"
|
||||
" coalesce(to_char(fin AT TIME ZONE 'Europe/Paris', 'HH24:MI'), 'en cours') AS \"Fin\",\n"
|
||||
" coalesce(vehicule, '?') AS \"Vehicule\",\n"
|
||||
" round(coalesce(energie_compteur_kwh, energie_kwh)::numeric, 2) AS \"Energie\",\n"
|
||||
" round(coalesce(p_max_w, 0)::numeric) AS \"Pointe\",\n"
|
||||
" coalesce(source_mode, '?') AS \"Source\"\n"
|
||||
"FROM session_charge WHERE $__timeFilter(debut) ORDER BY debut DESC", fmt="table")],
|
||||
PG, 0, 33, 12, 8,
|
||||
desc="⚠️ L'etiquette du vehicule vient d'un selecteur manuel : elle est "
|
||||
"fausse s'il n'a pas ete positionne au branchement. Les deux "
|
||||
"voitures sont electriquement indiscernables (3 680 W contre "
|
||||
"3 655 W a consigne egale).",
|
||||
overrides=[sur("Energie", [{"id": "unit", "value": "kwatth"}]),
|
||||
sur("Pointe", [{"id": "unit", "value": "watt"}])]),
|
||||
|
||||
table("Ce qui est arrive aujourd'hui", [sql("A",
|
||||
"SELECT to_char(ts AT TIME ZONE 'Europe/Paris', 'HH24:MI:SS') AS \"Heure\",\n"
|
||||
" genre AS \"Genre\", resume AS \"Evenement\"\n"
|
||||
"FROM evenement WHERE $__timeFilter(ts) AND genre <> 'bilan'\n"
|
||||
"ORDER BY ts DESC", fmt="table")],
|
||||
PG, 12, 33, 12, 8,
|
||||
desc="Le genre `bilan` est ecarte : ses bascules de production bridee "
|
||||
"changent plusieurs fois par heure et noieraient le reste. Elles "
|
||||
"sont lisibles dans la frise ci-dessus."),
|
||||
], frm="now/d")
|
||||
|
||||
|
||||
base = pathlib.Path('deploy/historisation/grafana/dashboards')
|
||||
for nom, d in [('nuit.json', nuit), ('solaire.json', solaire),
|
||||
for nom, d in [('journee.json', journee),
|
||||
('nuit.json', nuit), ('solaire.json', solaire),
|
||||
('prevision.json', prevision),
|
||||
('telephone.json', telephone),
|
||||
('systeme.json', systeme),
|
||||
|
||||
Reference in New Issue
Block a user