Chauffe-eau : Grafana « Le chauffe-eau », historisation (compteur rattrape depuis le 14/08), jour faible = aussi surplus solaire reel et securite douche de 20 h

This commit is contained in:
Tallagrand Thierry
2026-10-07 07:25:41 +02:00
parent c9e0e4bb5a
commit 36c2fb713a
10 changed files with 654 additions and 12 deletions

View File

@@ -5,6 +5,20 @@
// indisponible ne cree ni energie negative ni saut.
export const COMPTEUR_CHAUFFE_EAU = 'sensor.tt_chauffe_eau_pleumeur_energie_totale';
// Energie depuis `depuisMs` (meme regle : somme des hausses ; le point juste
// avant la borne sert de reference).
export function energieDepuis(serie, depuisMs) {
const pts = (serie || []).map((e) => [Date.parse(e.last_changed), Number.parseFloat(e.state)])
.filter(([t, v]) => Number.isFinite(t) && Number.isFinite(v));
if (!pts.length) return null;
let ref = null; const apres = [];
for (const p of pts) { if (p[0] <= depuisMs) ref = p; else apres.push(p); }
const vals = (ref ? [ref, ...apres] : apres).map((p) => p[1]);
let kwh = 0;
for (let i = 1; i < vals.length; i++) if (vals[i] > vals[i - 1]) kwh += vals[i] - vals[i - 1];
return Math.round(kwh * 100) / 100;
}
export function energieSur24h(serie) {
const vals = (serie || []).map((e) => Number.parseFloat(e.state)).filter(Number.isFinite);
if (!vals.length) return null; // compteur illisible sur 24 h : on ne dit rien

View File

@@ -1971,6 +1971,112 @@ journee = dashboard(
], frm="now/d", jusqu="now/d")
# =====================================================================
# LE CHAUFFE-EAU (07/10/2026) — demande du proprietaire
# =====================================================================
# Le 06/10 il a pris 5,4 kWh de 10 h a 15 h, l'energie qui manquait a la
# batterie. Depuis le 07/10 il chauffe en HC de nuit les jours faibles
# (decision du bilan, workflow ECS). Ce tableau dit QUAND il chauffe et SUR
# QUOI : l'energie vient du compteur cumule du ballon (historique HA rattrape
# depuis le 14/08), le contexte de chaque heure des puissances moyennes de
# l'heure — EDF si le reseau fournit plus de 300 W, solaire si la production
# couvre la maison, batterie sinon. C'est une ATTRIBUTION par heure, pas un
# comptage par electron : une heure moitie soleil moitie batterie est classee
# d'un seul cote.
ECS_C = "sensor.tt_chauffe_eau_pleumeur_energie_totale"
def ecs_heures(select, filtre=""):
"""kWh du chauffe-eau par heure locale, avec le contexte de l'heure."""
def moy(cle):
return (f"coalesce((SELECT avg(valeur) FROM mesure WHERE cle = '{cle}' "
f"AND ts >= k.t AND ts < k.t + interval '1 hour'), "
f"(SELECT valeur FROM mesure WHERE cle = '{cle}' AND ts < k.t "
f"ORDER BY ts DESC LIMIT 1), 0)")
return (
"WITH h AS (SELECT generate_series(date_trunc('hour', $__timeFrom()::timestamptz),\n"
" date_trunc('hour', least(now(), $__timeTo()::timestamptz)), interval '1 hour') AS t),\n"
f"v AS (SELECT t, (SELECT valeur FROM mesure WHERE cle = '{ECS_C}' AND ts <= t\n"
" ORDER BY ts DESC LIMIT 1) AS e FROM h),\n"
"k AS (SELECT t, greatest(0, lead(e) OVER (ORDER BY t) - e) AS kwh FROM v),\n"
"c AS (SELECT k.t, k.kwh,\n"
f" {moy('sensor.victron_grid_power_l1')} AS reseau,\n"
f" {moy('sensor.victron_pv_power_system')} AS pv,\n"
f" {moy('sensor.victron_consumption_power_l1')} AS maison\n"
" FROM k WHERE k.kwh IS NOT NULL),\n"
"x AS (SELECT t, kwh, CASE WHEN reseau > 300 THEN 'EDF' WHEN pv >= maison THEN 'solaire'\n"
" ELSE 'batterie' END AS ctx FROM c)\n" + select)
ECS_PAR_SOURCE = sur # (alias de lisibilite)
COULEURS_ECS = [sur("Solaire", [{"id": "color", "value": {"mode": "fixed", "fixedColor": "yellow"}}]),
sur("Batterie", [{"id": "color", "value": {"mode": "fixed", "fixedColor": "red"}}]),
sur("EDF", [{"id": "color", "value": {"mode": "fixed", "fixedColor": "blue"}}])]
chauffe_eau = dashboard("evcs-chauffe-eau", "Le chauffe-eau", (
"Quand le chauffe-eau chauffe, et sur quoi : solaire, batterie ou EDF. "
"Les jours faibles il doit chauffer la nuit sur EDF (decision du bilan "
"apres 07:24), jamais sur la batterie."), [
stat("Maintenant", [sql("A",
"SELECT now() AS time,\n"
" (SELECT valeur FROM mesure WHERE cle = 'sensor.evcs_chauffe_eau_24h_kwh' ORDER BY ts DESC LIMIT 1) AS \"Sur 24 h\",\n"
" (SELECT valeur FROM mesure WHERE cle = 'input_number.victron_water_heater_observed_power_w' ORDER BY ts DESC LIMIT 1) AS \"Puissance\"",
fmt="table")],
PG, 0, 0, 8, 4,
desc="Energie des 24 dernieres heures (securite eau chaude : sous 1,5 kWh "
"il chauffe meme un jour faible) et puissance mesuree par le workflow "
"ECS sur ses 5 dernieres minutes (morte jusqu'au 07/10).",
overrides=[sur("Sur 24 h", [{"id": "unit", "value": "kwatth"}, {"id": "decimals", "value": 2}]),
sur("Puissance", [{"id": "unit", "value": "watt"}])]),
table("Decisions et etats", [sql("A",
"SELECT to_char(ts AT TIME ZONE 'Europe/Paris', 'DD/MM HH24:MI') AS \"Quand\", resume AS \"Quoi\"\n"
"FROM evenement WHERE genre = 'chauffe_eau' AND $__timeFilter(ts) ORDER BY ts DESC", fmt="table")],
PG, 8, 0, 16, 8,
desc="Decision quotidienne du bilan (jour faible : eau chaude en HC de "
"nuit), mise sous tension / coupure, changements de regime (pompe a "
"chaleur, resistance). Journalise depuis le 07/10."),
ts("Energie par heure, selon d'ou elle venait", [sql("A", ecs_heures(
"SELECT t AS time,\n"
" CASE WHEN ctx = 'solaire' THEN kwh END AS \"Solaire\",\n"
" CASE WHEN ctx = 'batterie' THEN kwh END AS \"Batterie\",\n"
" CASE WHEN ctx = 'EDF' THEN kwh END AS \"EDF\"\n"
"FROM x ORDER BY t"))],
PG, 0, 8, 24, 9, unit="kwatth",
desc="Une barre par heure. Contexte de l'heure : EDF si le reseau fournit "
"plus de 300 W en moyenne, solaire si la production couvre la maison, "
"batterie sinon (attribution par heure). Le rouge est ce qu'on veut "
"faire disparaitre : de l'eau chauffee sur la batterie.",
extra={"defaults": {"custom": {"drawStyle": "bars", "fillOpacity": 85, "lineWidth": 0,
"stacking": {"mode": "normal", "group": "A"},
"showPoints": "never", "spanNulls": False}},
"overrides": COULEURS_ECS}),
barchart("Par jour", [sql("A", ecs_heures(
"SELECT to_char(t AT TIME ZONE 'Europe/Paris', 'DD/MM') AS \"Jour\",\n"
" round(sum(kwh) FILTER (WHERE ctx = 'solaire')::numeric, 2) AS \"Solaire\",\n"
" round(sum(kwh) FILTER (WHERE ctx = 'batterie')::numeric, 2) AS \"Batterie\",\n"
" round(sum(kwh) FILTER (WHERE ctx = 'EDF')::numeric, 2) AS \"EDF\"\n"
"FROM x GROUP BY 1, date_trunc('day', t AT TIME ZONE 'Europe/Paris')\n"
"ORDER BY date_trunc('day', t AT TIME ZONE 'Europe/Paris')"), fmt="table")],
PG, 0, 17, 24, 8, unit="kwatth",
desc="kWh du chauffe-eau par jour et par contexte. Repere : le 06/10, 5,4 kWh "
"dont une partie sur la batterie a midi ; un jour faible reussi n'a que "
"du bleu (EDF en heures creuses).",
overrides=COULEURS_ECS),
ts("Puissance mesuree et regime", [sql("A",
"SELECT ts AS time, valeur AS \"Puissance\" FROM mesure\n"
"WHERE cle = 'input_number.victron_water_heater_observed_power_w' AND $__timeFilter(ts) ORDER BY ts")],
PG, 0, 25, 24, 8, unit="watt",
desc="Puissance moyenne sur 5 minutes calculee par le workflow ECS depuis le "
"compteur. Pompe a chaleur : puissance basse, ~5 h pour 55 °C ; "
"resistance (~1,2 kW) : de 55 a 65 °C. Disponible depuis le 07/10 "
"(avant, la mesure ne lisait jamais ses horodatages).",
extra={"defaults": {"custom": {"drawStyle": "line", "lineInterpolation": "stepAfter",
"fillOpacity": 20}}}),
], frm="now-7d")
base = pathlib.Path('deploy/historisation/grafana/dashboards')
for nom, d in [('journee.json', journee),
('nuit.json', nuit), ('solaire.json', solaire),
@@ -1978,6 +2084,7 @@ for nom, d in [('journee.json', journee),
('telephone.json', telephone),
('systeme.json', systeme),
('vehicules.json', vehicules),
('batteries.json', batteries)]:
('batteries.json', batteries),
('chauffe-eau.json', chauffe_eau)]:
(base / nom).write_text(json.dumps(d, indent=2, ensure_ascii=False) + '\n')
print(f"{nom:16s} {len(d['panels'])} panneaux")

View File

@@ -33,7 +33,7 @@ import { creerSchemaKm, ingererReleves, ingererNiveauSaisi, ingererJeep } from '
import { texteRapport, envoyer, donneesNuit, texteDepuis } from './rapport-nuit.mjs';
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, minuitLocal, COMPTEURS_IMPORT, COMPTEUR_EDF, consoJour, COMPTEURS_CONSO, projectionSoir, previsionNuit, vrmVersCourbes, nuitMaisonPrevue } from './diagnostics-agent.mjs';
import { transitionsDansSerie } from './allers-retours.mjs';
import { energieSur24h, COMPTEUR_CHAUFFE_EAU } from './chauffe-eau.mjs';
import { energieSur24h, energieDepuis, COMPTEUR_CHAUFFE_EAU } from './chauffe-eau.mjs';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const ONCE = process.argv.includes('--once');
@@ -99,6 +99,11 @@ const MESURES = [
'sensor.jeep_renegade_phev_odometer',
'sensor.jeep_renegade_phev_hvbattery_charge',
'sensor.jeep_renegade_phev_extrapolated_battery',
// Chauffe-eau (07/10/2026) : compteur cumule, puissance observee par le
// workflow ECS (morte jusqu'au 07/10, cf. HANDOFF), energie sur 24 h.
'sensor.tt_chauffe_eau_pleumeur_energie_totale',
'input_number.victron_water_heater_observed_power_w',
'sensor.evcs_chauffe_eau_24h_kwh',
];
// ---------------------------------------------------------------------
@@ -129,6 +134,10 @@ const TRANSITIONS_SUIVIES = [
['input_select.victron_evcs_vehicle', 'intervention','Vehicule selectionne'],
['input_select.victron_evcs_strategy', 'intervention','Strategie'],
['input_select.victron_evcs_night_charge_mode', 'intervention','Charge de la voiture la nuit'],
['switch.tt_chauffe_eau_pleumeur_switch', 'chauffe_eau', 'Chauffe-eau sous tension'],
['input_boolean.evcs_balance_ecs_nuit', 'chauffe_eau', 'Chauffe-eau en HC de nuit (jour faible)'],
['input_text.evcs_balance_ecs_nuit_jour', 'chauffe_eau', 'Decision du jour pour le chauffe-eau'],
['input_text.victron_water_heater_observed_regime', 'chauffe_eau', 'Regime du chauffe-eau'],
['input_text.victron_evcs_night_budget', 'vehicule', 'Budget voiture de la nuit fige (kWh|index borne)'],
['sensor.gx_device_ess_batterylife_state', 'ess', 'Etat BatteryLife de l ESS'],
['input_text.victron_charge_mode', 'ess', 'Mode Auto Charge Manager'],
@@ -2076,12 +2085,25 @@ async function publierChauffeEau24h() {
{ headers: { Authorization: `Bearer ${HA_TOKEN}` }, signal: AbortSignal.timeout(15000) },
);
if (!r.ok) throw new Error(`historique HA ${r.status}`);
const kwh = energieSur24h((await r.json())[0]);
const serie = (await r.json())[0];
const kwh = energieSur24h(serie);
await publierCapteur('sensor.evcs_chauffe_eau_24h_kwh', kwh === null ? 'indisponible' : kwh, {
friendly_name: 'EVCS — chauffe-eau sur 24 h', unit_of_measurement: 'kWh', icon: 'mdi:water-boiler',
compteur: COMPTEUR_CHAUFFE_EAU, calcule_a: fin.toISOString(),
});
return 1;
// Depuis la fin de la derniere fenetre HC de nuit (07:24 heure de Paris) :
// securite de la douche du soir (20 h), regle « jour faible ».
const p = new Intl.DateTimeFormat('sv-SE', { timeZone: 'Europe/Paris', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }).format(fin);
const [jour, hm] = p.split(' ');
const decalage = new Date(fin.toLocaleString('en-US', { timeZone: 'Europe/Paris' })).getTime() - new Date(fin.toLocaleString('en-US', { timeZone: 'UTC' })).getTime();
let borne = Date.parse(`${jour}T07:24:00Z`) - decalage;
if (hm < '07:24') borne -= 24 * 3600e3;
const kwhJour = energieDepuis(serie, borne);
await publierCapteur('sensor.evcs_chauffe_eau_jour_kwh', kwhJour === null ? 'indisponible' : kwhJour, {
friendly_name: 'EVCS — chauffe-eau depuis 07:24', unit_of_measurement: 'kWh', icon: 'mdi:water-boiler',
depuis: new Date(borne).toISOString(), calcule_a: fin.toISOString(),
});
return 2;
}
async function publierImportJour(st) {