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
|
CREATE OR REPLACE VIEW ems.vw_latest_heat_pump
|
||||||
WITH (security_invoker = false)
|
WITH (security_invoker = false)
|
||||||
AS
|
AS
|
||||||
SELECT DISTINCT ON (t.heat_pump_id)
|
SELECT
|
||||||
t.site_id,
|
hp.site_id,
|
||||||
t.heat_pump_id,
|
hp.id AS heat_pump_id,
|
||||||
hp.code AS heat_pump_code,
|
hp.code AS heat_pump_code,
|
||||||
t.measured_at,
|
t.measured_at,
|
||||||
t.outdoor_temp_c,
|
t.outdoor_temp_c,
|
||||||
@@ -81,11 +81,25 @@ SELECT DISTINCT ON (t.heat_pump_id)
|
|||||||
t.defrost_active,
|
t.defrost_active,
|
||||||
t.alarm_code,
|
t.alarm_code,
|
||||||
-- Odhadovaný COP pro aktuální venkovní teplotu
|
-- 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
|
now() - t.measured_at AS data_age
|
||||||
FROM ems.telemetry_heat_pump t
|
FROM ems.asset_heat_pump hp
|
||||||
JOIN ems.asset_heat_pump hp ON hp.id = t.heat_pump_id
|
LEFT JOIN LATERAL (
|
||||||
ORDER BY t.heat_pump_id, t.measured_at DESC;
|
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
|
COMMENT ON VIEW ems.vw_latest_heat_pump IS
|
||||||
'Nejnovější telemetrická data pro každé tepelné čerpadlo včetně odhadovaného COP.
|
'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 {
|
import {
|
||||||
getCurrentPlan,
|
getCurrentPlan,
|
||||||
getSiteForecastPv,
|
|
||||||
getSitePrices,
|
getSitePrices,
|
||||||
getForecastPvSlotsRangeCorrected,
|
getForecastPvSlotsRangeCorrected,
|
||||||
type SiteEffectivePriceRowDto,
|
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 forecastDays: ForecastDayTotal[] = []
|
||||||
const weekDates = Array.from({ length: 7 }, (_, d) => pragueAddCalendarDays(todayPrague, d))
|
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 windowFromIso = new Date(windowStart).toISOString()
|
||||||
const windowToIso = new Date(windowStart + TOTAL_SLOTS * SLOT_MS).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>>,
|
() => [] as Awaited<ReturnType<typeof getForecastPvSlotsRangeCorrected>>,
|
||||||
)
|
)
|
||||||
const correctedBySlot = new Map<string, number>()
|
const correctedBySlot = new Map<string, number>()
|
||||||
@@ -295,16 +266,14 @@ export function useDashboardData(siteId: number | null) {
|
|||||||
correctedBySlot.set(slotTimeKey(t), Number(v))
|
correctedBySlot.set(slotTimeKey(t), Number(v))
|
||||||
}
|
}
|
||||||
for (const ymd of weekDates) {
|
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
|
let kwh = 0
|
||||||
for (const [, v] of byStart) {
|
// Křivka je po 15 min, takže energie = W * 0.25h
|
||||||
kwh += ((v.a + v.b) * 0.25) / 1000
|
// 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', {
|
const label = new Date(ymd + 'T12:00:00Z').toLocaleDateString('cs-CZ', {
|
||||||
weekday: 'short',
|
weekday: 'short',
|
||||||
@@ -374,14 +343,12 @@ export function useDashboardData(siteId: number | null) {
|
|||||||
base.sell_price = pr.sell
|
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)
|
const corr = correctedBySlot.get(k)
|
||||||
if (corr != null) {
|
if (corr != null) {
|
||||||
base.pv_forecast_corrected_w = corr
|
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)
|
const pi = planBySlot.get(k)
|
||||||
|
|||||||
Reference in New Issue
Block a user