Files
ems/db/routines/R__fn_ote_import.sql
Dusan Vojacek f0dfcefd54
All checks were successful
deploy / deploy (push) Successful in 19s
test / smoke-test (push) Successful in 5s
fix letni /zimni cas OTE
2026-04-12 21:57:37 +02:00

210 lines
7.3 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- =============================================================
-- R__fn_ote_import.sql
-- OTE CZ import parser a import funkce
-- Repeatable migration při změně funkce stačí upravit tento soubor
-- =============================================================
-- Parser: raw jsonb → 96 (nebo 92/100 při DST) cenových řádků po 15 min
CREATE OR REPLACE FUNCTION ems.fn_ote_parse_15m_price_json(
in_payload jsonb,
in_czk_per_eur numeric DEFAULT 25.000
)
RETURNS TABLE (
interval_start timestamptz,
interval_end timestamptz,
raw_price_czk_kwh numeric(10,6)
)
LANGUAGE plpgsql
AS $$
DECLARE
v_date_text text;
v_market_date date;
v_anchor timestamptz;
v_dl jsonb;
v_npts int;
BEGIN
IF in_payload IS NULL THEN
RAISE EXCEPTION 'in_payload must not be null';
END IF;
IF in_czk_per_eur IS NULL OR in_czk_per_eur <= 0 THEN
RAISE EXCEPTION 'in_czk_per_eur must be > 0, got: %', in_czk_per_eur;
END IF;
-- Datum z graph.title ve formátu "... DD.MM.YYYY"
v_date_text := substring(
in_payload #>> '{graph,title}'
FROM '([0-9]{2}\.[0-9]{2}\.[0-9]{4})'
);
IF v_date_text IS NULL THEN
RAISE EXCEPTION 'cannot parse date from graph.title: %',
in_payload #>> '{graph,title}';
END IF;
v_market_date := to_date(v_date_text, 'DD.MM.YYYY');
-- Začátek obchodního dne v Praze jako absolutní okamžik; pak +15 min v UTC.
-- Starý vzor (timestamp + interval) AT TIME ZONE při přechodu na letní čas
-- slučoval různé sloty → duplicitní interval_start a chyba ON CONFLICT.
v_anchor := v_market_date::timestamp AT TIME ZONE 'Europe/Prague';
-- OTE mění strukturu: novější data = 15min série (tooltip flash_chart_01_y_15m_*),
-- starší / jiná odpověď = jen 24 hodinových bodů s tooltip "Cena".
SELECT t.dl INTO v_dl
FROM jsonb_array_elements(in_payload #> '{data,dataLine}') AS t(dl)
WHERE dl ->> 'tooltip' = 'flash_chart_01_y_15m_price_tooltip'
AND jsonb_array_length(dl -> 'point') >= 90
LIMIT 1;
IF v_dl IS NULL THEN
SELECT t.dl INTO v_dl
FROM jsonb_array_elements(in_payload #> '{data,dataLine}') AS t(dl)
WHERE dl ->> 'tooltip' = 'flash_chart_01_y_60m_price_tooltip'
AND jsonb_array_length(dl -> 'point') >= 90
LIMIT 1;
END IF;
IF v_dl IS NULL THEN
SELECT t.dl INTO v_dl
FROM jsonb_array_elements(in_payload #> '{data,dataLine}') AS t(dl)
WHERE dl ->> 'tooltip' = 'Cena'
AND jsonb_array_length(dl -> 'point') >= 90
LIMIT 1;
END IF;
IF v_dl IS NULL THEN
SELECT t.dl INTO v_dl
FROM jsonb_array_elements(in_payload #> '{data,dataLine}') AS t(dl)
WHERE dl ->> 'tooltip' = 'Cena'
AND jsonb_array_length(dl -> 'point') BETWEEN 2 AND 28
LIMIT 1;
END IF;
IF v_dl IS NULL THEN
SELECT t.dl INTO v_dl
FROM jsonb_array_elements(in_payload #> '{data,dataLine}') AS t(dl)
WHERE dl ->> 'tooltip' = 'flash_chart_01_y_15m_price_tooltip'
AND jsonb_array_length(dl -> 'point') BETWEEN 2 AND 28
LIMIT 1;
END IF;
IF v_dl IS NULL THEN
RAISE EXCEPTION
'OTE price dataLine not found (očekáváno 15min flash_* nebo hodinová Cena); '
'dostupné tooltips: %',
(SELECT jsonb_agg(dl ->> 'tooltip' ORDER BY dl ->> 'tooltip')
FROM jsonb_array_elements(in_payload #> '{data,dataLine}') dl);
END IF;
v_npts := jsonb_array_length(v_dl -> 'point');
IF v_npts >= 90 THEN
-- x = 1..N = 15min bloky (96 běžně; 92 při zkráceném dni při přechodu na letní čas)
RETURN QUERY
SELECT s.interval_start, s.interval_end, s.raw_price_czk_kwh
FROM (
SELECT
(v_anchor + ((block_no - 1) * INTERVAL '15 minutes')) AS interval_start,
(v_anchor + (block_no * INTERVAL '15 minutes')) AS interval_end,
ROUND(
(price_eur_mwh * in_czk_per_eur / 1000.0)::numeric, 6
)::numeric(10,6) AS raw_price_czk_kwh,
block_no
FROM (
SELECT
(p ->> 'x')::int AS block_no,
(p ->> 'y')::numeric AS price_eur_mwh
FROM jsonb_array_elements(v_dl -> 'point') AS p
) pts
) AS s
ORDER BY s.block_no;
ELSIF v_npts BETWEEN 2 AND 28 THEN
-- x = 1..H = hodiny (ne nutně celý den); každou hodinovou cenu → 4× 15min slot
RETURN QUERY
SELECT s.interval_start, s.interval_end, s.raw_price_czk_kwh
FROM (
SELECT
(v_anchor + (((hour_no - 1) * 4 + qix) * INTERVAL '15 minutes'))
AS interval_start,
(v_anchor + (((hour_no - 1) * 4 + qix) * INTERVAL '15 minutes')
+ INTERVAL '15 minutes')
AS interval_end,
ROUND(
(price_eur_mwh * in_czk_per_eur / 1000.0)::numeric, 6
)::numeric(10,6) AS raw_price_czk_kwh,
(hour_no - 1) * 4 + qix + 1 AS sort_key
FROM (
SELECT (p ->> 'x')::int AS hour_no, (p ->> 'y')::numeric AS price_eur_mwh
FROM jsonb_array_elements(v_dl -> 'point') AS p
) h
CROSS JOIN (VALUES (0), (1), (2), (3)) AS q(qix)
) AS s
ORDER BY s.sort_key;
ELSE
RAISE EXCEPTION
'OTE price series: unexpected point count % (tooltip %)',
v_npts,
v_dl ->> 'tooltip';
END IF;
IF NOT FOUND THEN
RAISE EXCEPTION 'OTE price series had no points after parse';
END IF;
END;
$$;
COMMENT ON FUNCTION ems.fn_ote_parse_15m_price_json(jsonb, numeric) IS
'Parsuje raw JSON z OTE @@chart-data?time_resolution=PT15M.
Datum z graph.title (DD.MM.YYYY). Sloty: kotva půlnoci Europe/Prague jako timestamptz + násobky 15 min (bez DST duplicit).
Výběr série: flash_15m ≥90, flash_60m ≥90, Cena ≥90, Cena 228 h, flash_15m 228 h.
EUR/MWh → Kč/kWh. Typicky 96 řádků, 92 při zkráceném dni (letní čas).
Testování přímo v DB:
SELECT * FROM ems.fn_ote_parse_15m_price_json(pg_read_file(''/tmp/ote.json'')::jsonb, 25.0) LIMIT 5;';
CREATE OR REPLACE FUNCTION ems.fn_ote_import_from_json(
in_payload jsonb,
in_czk_per_eur numeric DEFAULT 25.000
)
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
v_rowcount integer;
BEGIN
INSERT INTO ems.market_interval_price (
market_source,
interval_start,
interval_end,
buy_raw_price_czk_kwh,
sell_raw_price_czk_kwh,
currency,
imported_at
)
SELECT DISTINCT ON (p.interval_start)
'OTE_CZ',
p.interval_start,
p.interval_end,
p.raw_price_czk_kwh,
p.raw_price_czk_kwh, -- spot trh: buy = sell
'CZK',
now()
FROM ems.fn_ote_parse_15m_price_json(in_payload, in_czk_per_eur) AS p
ORDER BY p.interval_start, p.interval_end DESC
ON CONFLICT (market_source, interval_start) DO UPDATE SET
interval_end = EXCLUDED.interval_end,
buy_raw_price_czk_kwh = EXCLUDED.buy_raw_price_czk_kwh,
sell_raw_price_czk_kwh = EXCLUDED.sell_raw_price_czk_kwh,
imported_at = EXCLUDED.imported_at;
GET DIAGNOSTICS v_rowcount = ROW_COUNT;
RETURN v_rowcount;
END;
$$;
COMMENT ON FUNCTION ems.fn_ote_import_from_json(jsonb, numeric) IS
'Uloží výstup fn_ote_parse_15m_price_json do ems.market_interval_price.
Python předá raw jsonb z HTTP response + kurz EUR/CZK.
Vrátí počet upsertnutých řádků (očekáváno 96).
Testování přímo v DB:
SELECT ems.fn_ote_import_from_json(
pg_read_file(''/tmp/ote.json'')::jsonb, 25.0
);';