Files
ems/db/routines/R__fn_ote_import.sql
Dusan Vojacek 5919b6caf3
All checks were successful
deploy / deploy (push) Successful in 12s
test / smoke-test (push) Successful in 3s
new fix OTE
2026-04-12 21:43:25 +02:00

198 lines
6.8 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_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');
-- 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') BETWEEN 20 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 20 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 (typicky 96; 92/100 při přechodu letní/zimní čas)
RETURN QUERY
SELECT s.interval_start, s.interval_end, s.raw_price_czk_kwh
FROM (
SELECT
((v_market_date::timestamp
+ ((block_no - 1) * INTERVAL '15 minutes'))
AT TIME ZONE 'Europe/Prague') AS interval_start,
((v_market_date::timestamp
+ (block_no * INTERVAL '15 minutes'))
AT TIME ZONE 'Europe/Prague') 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;
ELSE
-- x = 1..24 = hodiny dne; každou hodinovou cenu rozvineme na 4× 15min slot
RETURN QUERY
SELECT s.interval_start, s.interval_end, s.raw_price_czk_kwh
FROM (
SELECT
((v_market_date::timestamp
+ (((hour_no - 1) * 4 + qix) * INTERVAL '15 minutes'))
AT TIME ZONE 'Europe/Prague') AS interval_start,
((v_market_date::timestamp
+ (((hour_no - 1) * 4 + qix) * INTERVAL '15 minutes')
+ INTERVAL '15 minutes')
AT TIME ZONE 'Europe/Prague') 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;
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 extrahuje z graph.title (DD.MM.YYYY).
Výběr série: (1) flash_chart_01_y_15m_price_tooltip s ≥90 body, (2) flash_chart_01_y_60m_price_tooltip s ≥90,
(3) tooltip Cena s 2028 body (hodinovka → 4× 15min stejná EUR/MWh), (4) legacy 15m tooltip s 2028 body.
EUR/MWh → Kč/kWh přes kurz. Výstup: řádky po 15 min (typicky 96).
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
'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
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
);';