fix(db): čtecí cesty telemetrie robustní vůči řídkým řádkům (idle-skip)

- fn_fill_audit_interval: EV a TČ agregace sum(power_w)/15 místo avg přes
  přítomné řádky — avg by při řídké telemetrii nadhodnotil aktivitu části
  slotu; chybějící minuta = 0 W (idle). TČ drží NULL bez power_w (MIM-B19N).
- fn_update_tuv_usage_stats: delta TUV normalizovaná na °C/min délkou mezery
  mezi řádky (gap_min), mezery > 30 min vyloučeny; pro hustá 1min data
  numericky identické s původním LAG.
- vw_pool_pump_day_energy: komentář — on_minutes drží invariant „zapnuté
  čerpadlo se ukládá každou minutu".

Pro hustá 1min data beze změny výsledků; připravuje idle-skip zápisů
v telemetry_collector (navazující commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dusan Vojacek
2026-06-12 19:06:23 +02:00
parent e41840cb7d
commit f71bc944b4
3 changed files with 56 additions and 42 deletions

View File

@@ -66,34 +66,40 @@ LANGUAGE plpgsql
AS $$
DECLARE v_count INT;
BEGIN
INSERT INTO ems.tuv_usage_stats
-- Telemetrie TČ je idle-skip (telemetry_collector): rozestup řádků je
-- 114 min. Delta se proto normalizuje na °C/min (gap_min); hustá 1min
-- data dávají identické hodnoty jako dřív. Mezery > 30 min (výpadek
-- sběru) se z učení vylučují.
insert into ems.tuv_usage_stats
(site_id, day_of_week, hour_of_day,
avg_temp_delta_c, stddev_temp_delta,
sample_count, last_updated)
WITH deltas AS (
SELECT
with deltas as (
select
measured_at,
EXTRACT(DOW FROM measured_at AT TIME ZONE 'Europe/Prague')::INT AS dow,
EXTRACT(HOUR FROM measured_at AT TIME ZONE 'Europe/Prague')::INT AS hour,
tuv_tank_temp_c - LAG(tuv_tank_temp_c) OVER (
PARTITION BY site_id ORDER BY measured_at
) AS temp_delta_c
FROM ems.telemetry_heat_pump
WHERE site_id = p_site_id
AND measured_at >= now() - make_interval(days => p_lookback_days)
AND tuv_tank_temp_c IS NOT NULL
extract(dow from measured_at at time zone 'Europe/Prague')::int as dow,
extract(hour from measured_at at time zone 'Europe/Prague')::int as hour,
tuv_tank_temp_c - lag(tuv_tank_temp_c) over w as temp_delta_c,
extract(epoch from measured_at - lag(measured_at) over w) / 60.0 as gap_min
from ems.telemetry_heat_pump
where site_id = p_site_id
and measured_at >= now() - make_interval(days => p_lookback_days)
and tuv_tank_temp_c is not null
window w as (partition by site_id order by measured_at)
)
SELECT
select
p_site_id, dow, hour,
AVG(temp_delta_c),
STDDEV(temp_delta_c),
COUNT(*)::INT,
avg(temp_delta_c / gap_min),
stddev(temp_delta_c / gap_min),
count(*)::int,
now()
FROM deltas
WHERE temp_delta_c IS NOT NULL
AND ABS(temp_delta_c) < 5
GROUP BY dow, hour
HAVING COUNT(*) >= 4
from deltas
where temp_delta_c is not null
and gap_min > 0
and gap_min <= 30
and abs(temp_delta_c) < 5
group by dow, hour
having count(*) >= 4
ON CONFLICT (site_id, day_of_week, hour_of_day) DO UPDATE SET
avg_temp_delta_c = 0.7 * ems.tuv_usage_stats.avg_temp_delta_c
+ 0.3 * EXCLUDED.avg_temp_delta_c,
@@ -112,6 +118,8 @@ $$;
COMMENT ON FUNCTION ems.fn_update_tuv_usage_stats IS
'Aktualizuje statistiku poklesu teploty TUV zásobníku per DOW+hodina.
avg_temp_delta_c je v °C/min (delta normalizovaná délkou mezery mezi řádky —
robustní vůči idle-skip telemetrii; mezery > 30 min vyloučeny).
Záporné avg_temp_delta_c = zásobník se ochlazuje (spotřeba teplé vody).
Potřeba min. 1 měsíc telemetrie TČ. Volat denně.';

View File

@@ -150,25 +150,28 @@ BEGIN
v_grid_export_wh := ems.fn_audit_grid_export_wh_for_economics(
v_imp_before, v_exp_before, v_avg_grid_power_w);
-- Agregovat EV nabíječky (součet průměrů po charger_id)
SELECT COALESCE(SUM(avg_power), 0)::INT
INTO v_sum_ev_power_w
FROM (
SELECT AVG(power_w) AS avg_power
FROM ems.telemetry_ev_charger
WHERE site_id = p_site_id
AND measured_at >= p_interval_start
AND measured_at < v_interval_end
GROUP BY charger_id
) sub;
-- Agregovat EV nabíječky: sum(W·min) / 15 = průměrný výkon slotu.
-- Telemetrie je idle-skip (telemetry_collector): vypnutá nabíječka má jen
-- heartbeat řádky — avg přes přítomné řádky by nabíjení části slotu
-- NADHODNOTILO; chybějící minuta = 0 W (zařízení idle).
select round(coalesce(sum(power_w), 0) / 15.0)::int
into v_sum_ev_power_w
from ems.telemetry_ev_charger
where site_id = p_site_id
and measured_at >= p_interval_start
and measured_at < v_interval_end;
-- Agregovat tepelné čerpadlo
SELECT AVG(power_w)::INT
INTO v_avg_hp_power_w
FROM ems.telemetry_heat_pump
WHERE site_id = p_site_id
AND measured_at >= p_interval_start
AND measured_at < v_interval_end;
-- Agregovat tepelné čerpadlo: stejný sumový přístup (idle-skip řádky);
-- NULL pokud žádný řádek nenese power_w (MIM-B19N příkon neměří).
select case
when count(power_w) = 0 then null
else round(sum(power_w) / 15.0)::int
end
into v_avg_hp_power_w
from ems.telemetry_heat_pump
where site_id = p_site_id
and measured_at >= p_interval_start
and measured_at < v_interval_end;
-- Efektivní cena pro výpočet skutečných nákladů
v_buy_price := ems.fn_effective_buy_price(p_site_id, p_interval_start);
@@ -377,9 +380,10 @@ BEGIN
END;
$$;
COMMENT ON FUNCTION ems.fn_fill_audit_interval(INT, TIMESTAMPTZ) IS
COMMENT ON FUNCTION ems.fn_fill_audit_interval IS
'Naplní nebo aktualizuje jeden řádek v audit_interval pro danou lokalitu a 15min interval.
Agreguje průměry z telemetrie (střídač, EV,), porovná se skutečným plánem a spočítá odchylky.
Agreguje telemetrii (střídač průměry; EV a sum/15 — idle-skip zápisy, chybějící minuta = 0 W),
porovná se skutečným plánem a spočítá odchylky.
Per-minutový split pro 6 energetických veličin (import/export/batt/PV/load Wh);
grid import/export nejprve z delta Deye total counterů (reg 522-525), fallback per-minute; poté sjednocení
fn_audit_grid_*_wh_for_economics (u jednosměrného toku max s odhadem z průměrného grid_power_w).

View File

@@ -41,7 +41,9 @@ where tp.measured_at >= now() - interval '8 days'
group by 1, 2, 3;
comment on view ems.vw_pool_pump_day_energy is
'Denní kWh čerpadla (delta čítače energy_wh_total) a minuty běhu, 8 dní zpět.';
'Denní kWh čerpadla (delta čítače energy_wh_total) a minuty běhu, 8 dní zpět.
on_minutes = počet ON řádků: drží invariant idle-skip telemetrie (zapnuté
čerpadlo se ukládá každou minutu, vypnuté jen změna/heartbeat).';
grant select on ems.vw_latest_pool_pump to ems_anon;
grant select on ems.vw_pool_pump_day_energy to ems_anon;