diff --git a/HANDOFF.md b/HANDOFF.md index 2499939..ef19671 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -645,5 +645,15 @@ actifs), tous agents : timer systemd `openclaw-session-rotate.timer` (quotidien archives (0/89 au 25/09). ⚠️ Une mise a jour majeure de DSM a deja efface une config systeme (sudo NOPASSWD, §75) : reverifier le timer apres chaque MAJ DSM. +Grafana « Le solaire » : panneau **Production du jour par chargeur** (yield_today +des trois chargeurs + Total, trace seulement si les trois publient). Le suiveur +V1 manquait a InfluxDB : `sensor.v1_*` absent de `influxdb.include` de HA — +ajoute le 25/09 (sauvegarde `configuration.yaml.bak-20260925-v1`, check_config +valide), **effectif au prochain redemarrage de HA, que le proprio fait lui-meme** +(apres : verifier le selecteur vehicule, §47). Le suiveur est aussi ajoute aux +panneaux des modes (etiquette « Suiveur », sinon il tombait dans « Ensemble ») et +du detail par tracker, dont la description attribuait a tort son absence a un +defaut de publication. + --- -*Document mis a jour le : 25/09/2026 — bilan de nuit pour l'agent, retention 30 j des journaux OpenClaw* +*Document mis a jour le : 25/09/2026 — bilan de nuit pour l'agent, retention des journaux, production par chargeur dans Grafana* diff --git a/deploy/historisation/grafana/dashboards/solaire.json b/deploy/historisation/grafana/dashboards/solaire.json index 96e366d..ce3abb8 100644 --- a/deploy/historisation/grafana/dashboards/solaire.json +++ b/deploy/historisation/grafana/dashboards/solaire.json @@ -285,7 +285,7 @@ "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\")" + "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 =~ /^v1_/ then \"Suiveur\"\n else 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": { @@ -342,6 +342,92 @@ } } }, + { + "type": "timeseries", + "title": "Production du jour par chargeur", + "description": "Le rendement du jour que chaque chargeur declare lui-meme (yield_today), remis a zero chaque nuit. C'est la production REELLE (MANUEL §0), la meme que sensor.evcs_production_jour_kwh et que l'agent vocal. Le Total n'apparait que si les trois chargeurs publient : un chargeur muet donne un total partiel, qu'on ne trace pas. Les trois ne se remettent pas a zero a la meme minute (vers 00:40). Le suiveur n'est exporte que depuis le 25/09.", + "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 == \"kWh\" and r._field == \"value\")\n |> filter(fn: (r) => r.entity_id == \"airtria_mppt1_yield_today\"\n or r.entity_id == \"mppt_450_200_hq2449yr9pt_yield_today\"\n or r.entity_id == \"v1_yield_today\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> map(fn: (r) => ({ r with chargeur:\n if r.entity_id =~ /^airtria/ then \"MPPT 250/70 sud-est\"\n else if r.entity_id =~ /^mppt_450/ then \"RS 450/200 sud + ouest\"\n else \"150/35 suiveur\" }))\n |> group()\n |> keep(columns: [\"_time\", \"_value\", \"chargeur\"])\n |> pivot(rowKey: [\"_time\"], columnKey: [\"chargeur\"], valueColumn: \"_value\")\n |> sort(columns: [\"_time\"])" + }, + { + "refId": "B", + "datasource": { + "type": "influxdb", + "uid": "evcs-influx" + }, + "query": "from(bucket: \"evcs\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"kWh\" and r._field == \"value\")\n |> filter(fn: (r) => r.entity_id == \"airtria_mppt1_yield_today\"\n or r.entity_id == \"mppt_450_200_hq2449yr9pt_yield_today\"\n or r.entity_id == \"v1_yield_today\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> map(fn: (r) => ({ r with chargeur:\n if r.entity_id =~ /^airtria/ then \"MPPT 250/70 sud-est\"\n else if r.entity_id =~ /^mppt_450/ then \"RS 450/200 sud + ouest\"\n else \"150/35 suiveur\" }))\n |> group()\n |> keep(columns: [\"_time\", \"_value\", \"chargeur\"])\n |> pivot(rowKey: [\"_time\"], columnKey: [\"chargeur\"], valueColumn: \"_value\")\n |> sort(columns: [\"_time\"])\n |> fill(usePrevious: true, column: \"MPPT 250/70 sud-est\")\n |> fill(usePrevious: true, column: \"RS 450/200 sud + ouest\")\n |> fill(usePrevious: true, column: \"150/35 suiveur\")\n |> filter(fn: (r) => exists r[\"MPPT 250/70 sud-est\"] and exists r[\"RS 450/200 sud + ouest\"]\n and exists r[\"150/35 suiveur\"])\n |> map(fn: (r) => ({ _time: r._time, Total: r[\"MPPT 250/70 sud-est\"]\n + r[\"RS 450/200 sud + ouest\"] + r[\"150/35 suiveur\"] }))" + } + ], + "gridPos": { + "x": 0, + "y": 23, + "w": 24, + "h": 8 + }, + "fieldConfig": { + "defaults": { + "unit": "kwatth", + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 8, + "showPoints": "never", + "spanNulls": true, + "axisBorderShow": false, + "gradientMode": "opacity" + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "yellow" + } + }, + { + "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": "timeseries", "title": "Batterie", diff --git a/deploy/historisation/grafana/dashboards/systeme.json b/deploy/historisation/grafana/dashboards/systeme.json index 6b2a937..1f9bd86 100644 --- a/deploy/historisation/grafana/dashboards/systeme.json +++ b/deploy/historisation/grafana/dashboards/systeme.json @@ -796,7 +796,7 @@ { "type": "table", "title": "Detail par tracker", - "description": "Les 52 panneaux en quatre plans : sud-est 40 degres, trois chaines sud 35 degres, ouest 45 degres. Le suiveur de trois panneaux ne publie pas de mode de fonctionnement et n'apparait donc pas ici.", + "description": "Les 52 panneaux en quatre plans : sud-est 40 degres, trois chaines sud 35 degres, ouest 45 degres, et le suiveur de trois panneaux. Le suiveur n'est exporte vers InfluxDB que depuis le 25/09 (il manquait a la liste d'export de Home Assistant, et non faute de publier).", "datasource": { "type": "influxdb", "uid": "evcs-influx" @@ -808,7 +808,7 @@ "type": "influxdb", "uid": "evcs-influx" }, - "query": "import \"strings\"\n\nbase = from(bucket: \"evcs\")\n |> range(start: -30m)\n |> filter(fn: (r) => r._field == \"value\")\n |> filter(fn: (r) => r._measurement == \"V\" or r._measurement == \"A\" or r._measurement == \"W\")\n |> last()\n\nrs = base\n |> filter(fn: (r) => strings.containsStr(v: r.entity_id, substr: \"pv_tracker_solar_\"))\n |> map(fn: (r) => ({ r with tracker:\n if strings.containsStr(v: r.entity_id, substr: \"solar_1\") then \"Sud 1 (10 pan.)\"\n else if strings.containsStr(v: r.entity_id, substr: \"solar_2\") then \"Sud 2 (10 pan.)\"\n else if strings.containsStr(v: r.entity_id, substr: \"solar_3\") then \"Sud 3 (10 pan.)\"\n else \"Ouest (9 pan.)\" }))\n\nse = base\n |> filter(fn: (r) => r.entity_id == \"airtria_mppt1_pv_bus_voltage\"\n or r.entity_id == \"airtria_mppt1_pv_current\"\n or r.entity_id == \"airtria_mppt1_pv_yield_power\")\n |> map(fn: (r) => ({ r with tracker: \"Sud-est (10 pan.)\" }))\n\nunion(tables: [rs, se])\n |> keep(columns: [\"tracker\", \"_measurement\", \"_value\"])\n |> pivot(rowKey: [\"tracker\"], columnKey: [\"_measurement\"], valueColumn: \"_value\")\n |> sort(columns: [\"tracker\"])" + "query": "import \"strings\"\n\nbase = from(bucket: \"evcs\")\n |> range(start: -30m)\n |> filter(fn: (r) => r._field == \"value\")\n |> filter(fn: (r) => r._measurement == \"V\" or r._measurement == \"A\" or r._measurement == \"W\")\n |> last()\n\nrs = base\n |> filter(fn: (r) => strings.containsStr(v: r.entity_id, substr: \"pv_tracker_solar_\"))\n |> map(fn: (r) => ({ r with tracker:\n if strings.containsStr(v: r.entity_id, substr: \"solar_1\") then \"Sud 1 (10 pan.)\"\n else if strings.containsStr(v: r.entity_id, substr: \"solar_2\") then \"Sud 2 (10 pan.)\"\n else if strings.containsStr(v: r.entity_id, substr: \"solar_3\") then \"Sud 3 (10 pan.)\"\n else \"Ouest (9 pan.)\" }))\n\nse = base\n |> filter(fn: (r) => r.entity_id == \"airtria_mppt1_pv_bus_voltage\"\n or r.entity_id == \"airtria_mppt1_pv_current\"\n or r.entity_id == \"airtria_mppt1_pv_yield_power\")\n |> map(fn: (r) => ({ r with tracker: \"Sud-est (10 pan.)\" }))\n\nsv = base\n |> filter(fn: (r) => r.entity_id == \"v1_pv_bus_voltage\"\n or r.entity_id == \"v1_pv_current\"\n or r.entity_id == \"v1_pv_yield_power\")\n |> map(fn: (r) => ({ r with tracker: \"Suiveur (3 pan.)\" }))\n\nunion(tables: [rs, se, sv])\n |> keep(columns: [\"tracker\", \"_measurement\", \"_value\"])\n |> pivot(rowKey: [\"tracker\"], columnKey: [\"_measurement\"], valueColumn: \"_value\")\n |> sort(columns: [\"tracker\"])" } ], "gridPos": { diff --git a/ha-configuration.yaml b/ha-configuration.yaml index 7be414f..18f0e12 100755 --- a/ha-configuration.yaml +++ b/ha-configuration.yaml @@ -34,6 +34,9 @@ influxdb: - sensor.evcs_* - sensor.mppt_* - sensor.airtria_* + # Le suiveur (MPPT 150/35, 3 panneaux) : absent jusqu'au 25/09, donc ni + # son rendement journalier ni son mode dans les tableaux Grafana. + - sensor.v1_* - sensor.gx_device_* - sensor.sun_solar_elevation - binary_sensor.victron_* diff --git a/tools/gen-grafana-dashboards.py b/tools/gen-grafana-dashboards.py index 4f0d1e0..6fd953e 100644 --- a/tools/gen-grafana-dashboards.py +++ b/tools/gen-grafana-dashboards.py @@ -341,6 +341,22 @@ nuit = dashboard("evcs-nuit", "La nuit", ( # ===================================================================== # 2. LE SOLAIRE — excedent, censure, et ce que les chargeurs disent # ===================================================================== +# Rendement journalier declare par chaque chargeur (MANUEL §0 : la production +# reelle du jour est leur SOMME, jamais un sous-tracker du 450/200 en plus). +RENDEMENTS_FLUX = ( + 'from(bucket: "evcs")\n' + ' |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n' + ' |> filter(fn: (r) => r._measurement == "kWh" and r._field == "value")\n' + ' |> filter(fn: (r) => r.entity_id == "airtria_mppt1_yield_today"\n' + ' or r.entity_id == "mppt_450_200_hq2449yr9pt_yield_today"\n' + ' or r.entity_id == "v1_yield_today")\n' + ' |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n' + ' |> map(fn: (r) => ({ r with chargeur:\n' + ' if r.entity_id =~ /^airtria/ then "MPPT 250/70 sud-est"\n' + ' else if r.entity_id =~ /^mppt_450/ then "RS 450/200 sud + ouest"\n' + ' else "150/35 suiveur" }))\n' + ' |> group()\n') + 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. " @@ -393,7 +409,8 @@ solaire = dashboard("evcs-solaire", "Le solaire", ( ' |> 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' + ' if r._measurement =~ /^v1_/ then "Suiveur"\n' + ' else 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' @@ -411,6 +428,35 @@ solaire = dashboard("evcs-solaire", "Le solaire", ( "voltage_current_limited": {"color": "orange", "index": 1}, "off": {"color": "dark-blue", "index": 2}}}]), + ts("Production du jour par chargeur", [ + flux("A", RENDEMENTS_FLUX + ' |> keep(columns: ["_time", "_value", "chargeur"])\n' + ' |> pivot(rowKey: ["_time"], columnKey: ["chargeur"], valueColumn: "_value")\n' + ' |> sort(columns: ["_time"])'), + flux("B", RENDEMENTS_FLUX + + ' |> keep(columns: ["_time", "_value", "chargeur"])\n' + ' |> pivot(rowKey: ["_time"], columnKey: ["chargeur"], valueColumn: "_value")\n' + ' |> sort(columns: ["_time"])\n' + ' |> fill(usePrevious: true, column: "MPPT 250/70 sud-est")\n' + ' |> fill(usePrevious: true, column: "RS 450/200 sud + ouest")\n' + ' |> fill(usePrevious: true, column: "150/35 suiveur")\n' + ' |> filter(fn: (r) => exists r["MPPT 250/70 sud-est"] and exists r["RS 450/200 sud + ouest"]\n' + ' and exists r["150/35 suiveur"])\n' + ' |> map(fn: (r) => ({ _time: r._time, Total: r["MPPT 250/70 sud-est"]\n' + ' + r["RS 450/200 sud + ouest"] + r["150/35 suiveur"] }))')], + IN, 0, 23, 24, 8, "kwatth", + "Le rendement du jour que chaque chargeur declare lui-meme (yield_today), " + "remis a zero chaque nuit. C'est la production REELLE (MANUEL §0), la meme " + "que sensor.evcs_production_jour_kwh et que l'agent vocal. Le Total " + "n'apparait que si les trois chargeurs publient : un chargeur muet donne " + "un total partiel, qu'on ne trace pas. Les trois ne se remettent pas a " + "zero a la meme minute (vers 00:40). Le suiveur n'est exporte que depuis " + "le 25/09.", + extra={"overrides": [ + {"matcher": {"id": "byName", "options": "Total"}, + "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "yellow"}}, + {"id": "custom.lineWidth", "value": 3}, + {"id": "custom.fillOpacity", "value": 0}]}]}), + ts("Batterie", [ sql("A", "SELECT date_trunc('minute', ts) AS time, avg(valeur) AS \"SOC\"\n" "FROM mesure WHERE cle = 'sensor.victron_battery_soc'\n" @@ -852,7 +898,13 @@ se = base or r.entity_id == "airtria_mppt1_pv_yield_power") |> map(fn: (r) => ({ r with tracker: "Sud-est (10 pan.)" })) -union(tables: [rs, se]) +sv = base + |> filter(fn: (r) => r.entity_id == "v1_pv_bus_voltage" + or r.entity_id == "v1_pv_current" + or r.entity_id == "v1_pv_yield_power") + |> map(fn: (r) => ({ r with tracker: "Suiveur (3 pan.)" })) + +union(tables: [rs, se, sv]) |> keep(columns: ["tracker", "_measurement", "_value"]) |> pivot(rowKey: ["tracker"], columnKey: ["_measurement"], valueColumn: "_value") |> sort(columns: ["tracker"])''' @@ -968,8 +1020,9 @@ systeme = dashboard("evcs-systeme", "Le systeme", ( dict(table("Detail par tracker", [flux("A", TRACKERS_FLUX)], IN, 5, 7, 13, 8, "Les 52 panneaux en quatre plans : sud-est 40 degres, trois chaines sud " - "35 degres, ouest 45 degres. Le suiveur de trois panneaux ne publie pas " - "de mode de fonctionnement et n'apparait donc pas ici.", + "35 degres, ouest 45 degres, et le suiveur de trois panneaux. Le suiveur " + "n'est exporte vers InfluxDB que depuis le 25/09 (il manquait a la liste " + "d'export de Home Assistant, et non faute de publier).", overrides=[sur("tracker", [{"id": "displayName", "value": "Tracker"}, {"id": "custom.width", "value": 200}]), sur("V", [{"id": "displayName", "value": "Tension"},