fix
This commit is contained in:
@@ -75,6 +75,47 @@ async def get_site_prices(
|
|||||||
return [r for r in rows if isinstance(r, dict)]
|
return [r for r in rows if isinstance(r, dict)]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{site_id}/prices/slots")
|
||||||
|
async def get_site_prices_slots_range(
|
||||||
|
site_id: int,
|
||||||
|
db: Annotated[asyncpg.Pool, Depends(get_pg_pool)],
|
||||||
|
from_ts: datetime = Query(
|
||||||
|
...,
|
||||||
|
alias="from",
|
||||||
|
description="Začátek okna [from, to), typicky UTC zaokrouhlené na 15 min",
|
||||||
|
),
|
||||||
|
to_ts: datetime = Query(
|
||||||
|
...,
|
||||||
|
alias="to",
|
||||||
|
description="Konec polouzavřeného intervalu (max. 14 dní za from)",
|
||||||
|
),
|
||||||
|
) -> dict[str, list[dict[str, Any]]]:
|
||||||
|
if to_ts <= from_ts:
|
||||||
|
raise HTTPException(status_code=422, detail="'to' must be after 'from'")
|
||||||
|
if to_ts - from_ts > timedelta(days=14):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=422,
|
||||||
|
detail="Span between 'from' and 'to' must be at most 14 days",
|
||||||
|
)
|
||||||
|
async with db.acquire() as conn:
|
||||||
|
site_ok = await conn.fetchval(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM ems.site WHERE id = $1)", site_id
|
||||||
|
)
|
||||||
|
if not site_ok:
|
||||||
|
raise HTTPException(status_code=404, detail="Site not found")
|
||||||
|
raw = await fetch_json(
|
||||||
|
conn,
|
||||||
|
"select ems.fn_site_effective_prices_slots_range($1::int, $2::timestamptz, $3::timestamptz)",
|
||||||
|
site_id,
|
||||||
|
from_ts,
|
||||||
|
to_ts,
|
||||||
|
)
|
||||||
|
rows = raw if isinstance(raw, list) else []
|
||||||
|
if not isinstance(rows, list):
|
||||||
|
rows = []
|
||||||
|
return {"slots": [r for r in rows if isinstance(r, dict)]}
|
||||||
|
|
||||||
|
|
||||||
class PricesImportResponse(BaseModel):
|
class PricesImportResponse(BaseModel):
|
||||||
slots_imported: int
|
slots_imported: int
|
||||||
date: str
|
date: str
|
||||||
|
|||||||
28
db/routines/R__067_fn_site_effective_prices_slots_range.sql
Normal file
28
db/routines/R__067_fn_site_effective_prices_slots_range.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
create or replace function ems.fn_site_effective_prices_slots_range(
|
||||||
|
p_site_id int,
|
||||||
|
p_from timestamptz,
|
||||||
|
p_to timestamptz
|
||||||
|
)
|
||||||
|
returns jsonb
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
as $fn$
|
||||||
|
select coalesce(
|
||||||
|
jsonb_agg(u.j order by u.interval_start),
|
||||||
|
'[]'::jsonb
|
||||||
|
)
|
||||||
|
from (
|
||||||
|
select
|
||||||
|
v.interval_start,
|
||||||
|
to_jsonb(v) as j
|
||||||
|
from ems.vw_site_effective_price v
|
||||||
|
where v.site_id = p_site_id
|
||||||
|
and v.interval_start >= p_from
|
||||||
|
and v.interval_end < p_to
|
||||||
|
order by v.interval_start
|
||||||
|
) u;
|
||||||
|
$fn$;
|
||||||
|
|
||||||
|
comment on function ems.fn_site_effective_prices_slots_range(int, timestamptz, timestamptz) is
|
||||||
|
'Efektivní ceny pro [p_from, p_to) jako pole JSON řádků view (range pro UI bez PostgREST auth).';
|
||||||
|
|
||||||
@@ -270,6 +270,18 @@ export async function getSitePrices(siteId: number, date: string): Promise<SiteE
|
|||||||
return Array.isArray(data) ? data : []
|
return Array.isArray(data) ? data : []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSitePricesSlotsRange(
|
||||||
|
siteId: number,
|
||||||
|
fromIso: string,
|
||||||
|
toIso: string,
|
||||||
|
): Promise<SiteEffectivePriceRowDto[]> {
|
||||||
|
const { data } = await client.get<{ slots?: SiteEffectivePriceRowDto[] }>(
|
||||||
|
`/sites/${siteId}/prices/slots`,
|
||||||
|
{ params: { from: fromIso, to: toIso }, timeout: 60_000 },
|
||||||
|
)
|
||||||
|
return Array.isArray(data?.slots) ? data.slots : []
|
||||||
|
}
|
||||||
|
|
||||||
export type ForecastPvIntervalRow = {
|
export type ForecastPvIntervalRow = {
|
||||||
interval_start: string
|
interval_start: string
|
||||||
power_w?: number | string | null
|
power_w?: number | string | null
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||||||
import {
|
import {
|
||||||
getCurrentPlan,
|
getCurrentPlan,
|
||||||
getForecastPvSlotsRangeCorrected,
|
getForecastPvSlotsRangeCorrected,
|
||||||
|
getSitePricesSlotsRange,
|
||||||
} from '../api/backend'
|
} from '../api/backend'
|
||||||
import { getJson } from '../api/postgrest'
|
import { getJson } from '../api/postgrest'
|
||||||
import {
|
import {
|
||||||
@@ -20,7 +21,6 @@ import type {
|
|||||||
HeatPumpLatestRow,
|
HeatPumpLatestRow,
|
||||||
ModeLogRecentRow,
|
ModeLogRecentRow,
|
||||||
SiteStatusRow,
|
SiteStatusRow,
|
||||||
SiteEffectivePriceRow,
|
|
||||||
Telemetry15m7dRow,
|
Telemetry15m7dRow,
|
||||||
} from '../types/ems'
|
} from '../types/ems'
|
||||||
import type { PlanningIntervalDto } from '../types/plan'
|
import type { PlanningIntervalDto } from '../types/plan'
|
||||||
@@ -220,15 +220,12 @@ export function useDashboardData(siteId: number | null) {
|
|||||||
limit: '200',
|
limit: '200',
|
||||||
}),
|
}),
|
||||||
getJson<HeatPumpLatestRow[]>('/vw_latest_heat_pump', { site_id: `eq.${siteId}` }),
|
getJson<HeatPumpLatestRow[]>('/vw_latest_heat_pump', { site_id: `eq.${siteId}` }),
|
||||||
// Ceny bereme v jednom range dotazu přes PostgREST view (místo N× /api/v1/sites/{id}/prices?date=...).
|
// Ceny bereme přes FastAPI range endpoint (PostgREST /rest je u vás chráněné → 401).
|
||||||
// Okno: [windowStart, windowStart + TOTAL_SLOTS) pro mapování na sloty.
|
getSitePricesSlotsRange(
|
||||||
getJson<SiteEffectivePriceRow[]>('/vw_site_effective_price', {
|
siteId,
|
||||||
site_id: `eq.${siteId}`,
|
new Date(windowStart).toISOString(),
|
||||||
interval_start: `gte.${new Date(windowStart).toISOString()}`,
|
new Date(windowStart + TOTAL_SLOTS * SLOT_MS).toISOString(),
|
||||||
interval_end: `lt.${new Date(windowStart + TOTAL_SLOTS * SLOT_MS).toISOString()}`,
|
),
|
||||||
order: 'interval_start.asc',
|
|
||||||
limit: String(TOTAL_SLOTS + 32),
|
|
||||||
}),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const status = Array.isArray(statusArr) && statusArr[0] ? statusArr[0]! : null
|
const status = Array.isArray(statusArr) && statusArr[0] ? statusArr[0]! : null
|
||||||
@@ -242,7 +239,7 @@ export function useDashboardData(siteId: number | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const priceBySlot = new Map<string, { buy: number | null; sell: number | null }>()
|
const priceBySlot = new Map<string, { buy: number | null; sell: number | null }>()
|
||||||
const flatPrices: SiteEffectivePriceRow[] = Array.isArray(priceRows) ? priceRows : []
|
const flatPrices = Array.isArray(priceRows) ? priceRows : []
|
||||||
for (const r of flatPrices) {
|
for (const r of flatPrices) {
|
||||||
const k = slotTimeKey(new Date(r.interval_start).getTime())
|
const k = slotTimeKey(new Date(r.interval_start).getTime())
|
||||||
priceBySlot.set(k, {
|
priceBySlot.set(k, {
|
||||||
|
|||||||
Reference in New Issue
Block a user