-- ============================================================= -- 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 20–28 body (hodinovka → 4× 15min stejná EUR/MWh), (4) legacy 15m tooltip s 20–28 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 );';