fix reload pv on dashboard
Some checks failed
CI and deploy / migration-check (push) Failing after 15s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-04-27 18:39:13 +02:00
parent 16fc6a065e
commit e4d4fee24d
3 changed files with 45 additions and 56 deletions

View File

@@ -0,0 +1,8 @@
-- =============================================================
-- V067__asset_heat_pump_site_index.sql
-- Zrychlení filtrování asset_heat_pump podle site_id (PostgREST).
-- =============================================================
create index if not exists idx_asset_heat_pump_site
on ems.asset_heat_pump (site_id);

View File

@@ -67,9 +67,9 @@ COMMENT ON VIEW ems.vw_latest_ev_charger IS
CREATE OR REPLACE VIEW ems.vw_latest_heat_pump
WITH (security_invoker = false)
AS
SELECT DISTINCT ON (t.heat_pump_id)
t.site_id,
t.heat_pump_id,
SELECT
hp.site_id,
hp.id AS heat_pump_id,
hp.code AS heat_pump_code,
t.measured_at,
t.outdoor_temp_c,
@@ -81,11 +81,25 @@ SELECT DISTINCT ON (t.heat_pump_id)
t.defrost_active,
t.alarm_code,
-- Odhadovaný COP pro aktuální venkovní teplotu
ems.fn_cop_estimate(t.heat_pump_id, t.outdoor_temp_c) AS cop_estimated,
ems.fn_cop_estimate(hp.id, t.outdoor_temp_c) AS cop_estimated,
now() - t.measured_at AS data_age
FROM ems.telemetry_heat_pump t
JOIN ems.asset_heat_pump hp ON hp.id = t.heat_pump_id
ORDER BY t.heat_pump_id, t.measured_at DESC;
FROM ems.asset_heat_pump hp
LEFT JOIN LATERAL (
SELECT
thp.measured_at,
thp.outdoor_temp_c,
thp.tuv_tank_temp_c,
thp.water_outlet_temp_c,
thp.power_w,
thp.operating_mode,
thp.cop_actual,
thp.defrost_active,
thp.alarm_code
FROM ems.telemetry_heat_pump thp
WHERE thp.heat_pump_id = hp.id
ORDER BY thp.measured_at DESC
LIMIT 1
) t ON true;
COMMENT ON VIEW ems.vw_latest_heat_pump IS
'Nejnovější telemetrická data pro každé tepelné čerpadlo včetně odhadovaného COP.

View File

@@ -3,7 +3,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
getCurrentPlan,
getSiteForecastPv,
getSitePrices,
getForecastPvSlotsRangeCorrected,
type SiteEffectivePriceRowDto,
@@ -245,45 +244,17 @@ export function useDashboardData(siteId: number | null) {
})
}
const forecastBySlot = new Map<string, { a: number; b: number }>()
const forecastDays: ForecastDayTotal[] = []
const weekDates = Array.from({ length: 7 }, (_, d) => pragueAddCalendarDays(todayPrague, d))
const forecastFetchDates = [...new Set([...dates, ...weekDates])].sort()
const forecastResults = await Promise.all(
forecastFetchDates.map((ymd) =>
getSiteForecastPv(siteId, ymd)
.then((fc) => ({ ymd, fc }))
.catch(() => ({ ymd, fc: null as Awaited<ReturnType<typeof getSiteForecastPv>> | null })),
),
)
const forecastByYmd = new Map(forecastResults.map((r) => [r.ymd, r.fc]))
const addForecastToByStart = (
fc: NonNullable<Awaited<ReturnType<typeof getSiteForecastPv>>>,
byStart: Map<string, { a: number; b: number }>,
) => {
for (const x of fc.pv_a ?? []) {
const t = new Date(x.interval_start).getTime()
const p = Number(x.power_w ?? 0)
const cur = byStart.get(slotTimeKey(t)) ?? { a: 0, b: 0 }
cur.a += p
byStart.set(slotTimeKey(t), cur)
}
for (const x of fc.pv_b ?? []) {
const t = new Date(x.interval_start).getTime()
const p = Number(x.power_w ?? 0)
const cur = byStart.get(slotTimeKey(t)) ?? { a: 0, b: 0 }
cur.b += p
byStart.set(slotTimeKey(t), cur)
}
}
for (const { fc } of forecastResults) {
if (!fc) continue
addForecastToByStart(fc, forecastBySlot)
}
// Forecast pro dashboard bereme pouze z range endpointu (jedno volání místo N× /forecast/pv per den).
// Rozsah musí pokrýt (a) okno slotů pro tabulku a (b) 7denní sumáře.
const windowFromIso = new Date(windowStart).toISOString()
const windowToIso = new Date(windowStart + TOTAL_SLOTS * SLOT_MS).toISOString()
const correctedSlots = await getForecastPvSlotsRangeCorrected(siteId, windowFromIso, windowToIso).catch(
const weekToIso = new Date(floorSlotUtcMs(Date.now()) + 7 * 24 * 60 * 60 * 1000).toISOString()
const forecastToIso = weekToIso > windowToIso ? weekToIso : windowToIso
const correctedSlots = await getForecastPvSlotsRangeCorrected(siteId, windowFromIso, forecastToIso).catch(
() => [] as Awaited<ReturnType<typeof getForecastPvSlotsRangeCorrected>>,
)
const correctedBySlot = new Map<string, number>()
@@ -295,16 +266,14 @@ export function useDashboardData(siteId: number | null) {
correctedBySlot.set(slotTimeKey(t), Number(v))
}
for (const ymd of weekDates) {
const fc = forecastByYmd.get(ymd) ?? null
if (!fc) {
forecastDays.push({ date: ymd, label: ymd, kwh: 0 })
continue
}
const byStart = new Map<string, { a: number; b: number }>()
addForecastToByStart(fc, byStart)
let kwh = 0
for (const [, v] of byStart) {
kwh += ((v.a + v.b) * 0.25) / 1000
// Křivka je po 15 min, takže energie = W * 0.25h
// Použijeme correctedBySlot v časovém okně daného prague dne.
const dayStart = new Date(ymd + 'T00:00:00').getTime()
const dayEnd = new Date(pragueAddCalendarDays(ymd, 1) + 'T00:00:00').getTime()
for (let t = dayStart; t < dayEnd; t += SLOT_MS) {
const w = correctedBySlot.get(slotTimeKey(t)) ?? 0
kwh += (w * 0.25) / 1000
}
const label = new Date(ymd + 'T12:00:00Z').toLocaleDateString('cs-CZ', {
weekday: 'short',
@@ -374,14 +343,12 @@ export function useDashboardData(siteId: number | null) {
base.sell_price = pr.sell
}
const fc = forecastBySlot.get(k)
if (fc) {
base.pv_a_forecast_w = fc.a
base.pv_b_forecast_w = fc.b
}
const corr = correctedBySlot.get(k)
if (corr != null) {
base.pv_forecast_corrected_w = corr
// Dashboard neřeší přesné rozdělení po polích; pro UI rozpad použijeme stabilní poměr.
base.pv_a_forecast_w = Math.round(corr * 0.6)
base.pv_b_forecast_w = Math.round(corr * 0.4)
}
const pi = planBySlot.get(k)