diff --git a/db/views/R__072_z_postgrest_ems_anon_grants.sql b/db/views/R__072_z_postgrest_ems_anon_grants.sql index adf6b19..d0da621 100644 --- a/db/views/R__072_z_postgrest_ems_anon_grants.sql +++ b/db/views/R__072_z_postgrest_ems_anon_grants.sql @@ -17,6 +17,7 @@ END $$; GRANT USAGE ON SCHEMA ems TO ems_anon; GRANT SELECT ON ems.vw_site_status TO ems_anon; +GRANT SELECT ON ems.vw_telemetry_heat_pump_15m_7d TO ems_anon; GRANT SELECT ON ems.vw_site_effective_price TO ems_anon; GRANT SELECT ON ems.vw_latest_inverter TO ems_anon; GRANT SELECT ON ems.vw_latest_heat_pump TO ems_anon; diff --git a/db/views/R__101_vw_telemetry_heat_pump_15m_7d.sql b/db/views/R__101_vw_telemetry_heat_pump_15m_7d.sql new file mode 100644 index 0000000..80b0c1d --- /dev/null +++ b/db/views/R__101_vw_telemetry_heat_pump_15m_7d.sql @@ -0,0 +1,23 @@ +-- 15min agregace telemetrie TČ pro dashboard (TUV křivka v SocTuvChart). +-- Teploty: avg přes přítomné řádky je OK (idle-skip ředí řádky, ale teplota +-- není výkon — viz telemetry.md Idle-skip; nikdy z toho nepočítat energii). + +drop view if exists ems.vw_telemetry_heat_pump_15m_7d; + +create view ems.vw_telemetry_heat_pump_15m_7d as +select + time_bucket(interval '15 minutes', t.measured_at) as slot_start, + t.site_id, + round(avg(t.tuv_tank_temp_c)::numeric, 1) as avg_tuv_c, + round(avg(t.water_outlet_temp_c)::numeric, 1) as avg_water_out_c, + round(avg(t.water_inlet_temp_c)::numeric, 1) as avg_water_in_c, + max(t.operating_mode) as operating_mode, + count(*) as sample_count +from ems.telemetry_heat_pump t +where t.measured_at > now() - interval '7 days' +group by 1, 2; + +comment on view ems.vw_telemetry_heat_pump_15m_7d is + '15min agregace TČ telemetrie za 7 dní pro dashboard (TUV/voda křivky).'; + +grant select on ems.vw_telemetry_heat_pump_15m_7d to ems_anon; diff --git a/frontend/src/hooks/useDashboardData.ts b/frontend/src/hooks/useDashboardData.ts index 06fff85..f86486c 100644 --- a/frontend/src/hooks/useDashboardData.ts +++ b/frontend/src/hooks/useDashboardData.ts @@ -217,6 +217,7 @@ export function useDashboardData(siteId: number | null) { telemetry15m7d, auditHourly, modeLog, + hpRows, priceRows, ] = await Promise.all([ getCurrentPlan(siteId).catch((e: unknown) => { @@ -240,6 +241,15 @@ export function useDashboardData(siteId: number | null) { order: 'activated_at.asc', limit: '200', }), + getJson<{ slot_start: string; avg_tuv_c: number | null }[]>( + '/vw_telemetry_heat_pump_15m_7d', + { + site_id: `eq.${siteId}`, + slot_start: `gte.${new Date(windowStart).toISOString()}`, + order: 'slot_start.asc', + limit: TELEMETRY_15M_LIMIT, + }, + ).catch(() => [] as { slot_start: string; avg_tuv_c: number | null }[]), // Ceny bereme přes FastAPI range endpoint (PostgREST /rest je u vás chráněné → 401). getSitePricesSlotsRange( siteId, @@ -312,6 +322,14 @@ export function useDashboardData(siteId: number | null) { } } + const hpBySlot = new Map() + if (Array.isArray(hpRows)) { + for (const r of hpRows) { + const v = r.avg_tuv_c + if (v != null) hpBySlot.set(slotTimeKey(new Date(r.slot_start).getTime()), Number(v)) + } + } + const auditMap = new Map() if (Array.isArray(auditHourly)) { for (const r of auditHourly) { @@ -338,6 +356,11 @@ export function useDashboardData(siteId: number | null) { base.soc_actual_pct = parseNum(tel.last_soc_pct) ?? base.soc_actual_pct } + const hpTuv = hpBySlot.get(k) + if (hpTuv != null) { + base.tuv_actual_c = hpTuv + } + const aud = auditMap.get(pragueHourKey(startMs)) if (aud) { if (base.pv_power_w == null && aud.avg_pv_kw != null) {