fix reload pv on dashboard
This commit is contained in:
8
db/migration/V067__asset_heat_pump_site_index.sql
Normal file
8
db/migration/V067__asset_heat_pump_site_index.sql
Normal 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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user