Files
ems/db/routines/R__034_fn_plan_explain_bundle.sql
Dusan Vojacek 8bef1c6da6
Some checks failed
CI and deploy / migration-check (push) Failing after 13s
CI and deploy / deploy (push) Has been skipped
prepsani s opusem dle planu
2026-05-24 22:44:21 +02:00

236 lines
7.7 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.
-- Jednorázový JSON snapshot pro vysvětlení plánu (typicky nejbližších N hodin).
-- Volání: SELECT ems.fn_plan_explain_bundle(<site_id>, 6);
CREATE OR REPLACE FUNCTION ems.fn_plan_explain_bundle(
p_site_id INT,
p_hours NUMERIC DEFAULT 6
)
RETURNS JSONB
LANGUAGE plpgsql
STABLE
AS $$
DECLARE
v_hours NUMERIC;
v_slot TIMESTAMPTZ;
v_win_end TIMESTAMPTZ;
v_site_ok BOOLEAN;
v_run RECORD;
v_meta JSONB;
v_ivals JSONB;
v_mode JSONB;
v_batt JSONB;
v_grid JSONB;
v_hp JSONB;
v_ev JSONB;
v_fc JSONB;
v_ov JSONB;
v_econ JSONB;
BEGIN
IF p_site_id IS NULL THEN
RETURN jsonb_build_object('error', 'site_id_required');
END IF;
v_hours := GREATEST(0.25::NUMERIC, LEAST(COALESCE(NULLIF(p_hours, 0), 6), 96));
SELECT EXISTS(SELECT 1 FROM ems.site s WHERE s.id = p_site_id) INTO v_site_ok;
IF NOT v_site_ok THEN
RETURN jsonb_build_object(
'error', 'unknown_site_id',
'site_id', p_site_id
);
END IF;
v_slot := to_timestamp(floor(extract(epoch FROM now()) / 900) * 900)::TIMESTAMPTZ;
v_win_end := v_slot + (v_hours * INTERVAL '1 hour');
v_meta := jsonb_build_object(
'generated_at', now(),
'current_slot_start_utc', v_slot,
'window_end_utc', v_win_end,
'hours_requested', v_hours,
'slot_minutes', 15,
'note',
'interval_start je vždy UTC. Provozní čas viz ems.site.timezone (typicky Europe/Prague).'
);
SELECT * INTO v_run
FROM ems.planning_run pr
WHERE pr.site_id = p_site_id
AND pr.status = 'active'
ORDER BY pr.created_at DESC
LIMIT 1;
IF NOT FOUND THEN
RETURN jsonb_build_object(
'error', 'no_active_plan',
'site_id', p_site_id,
'meta', v_meta
);
END IF;
SELECT COALESCE(
jsonb_agg(
to_jsonb(t) || jsonb_build_object(
'hours_from_plan_horizon_start',
round(
(EXTRACT(EPOCH FROM (t.interval_start - v_run.horizon_start)) / 3600.0)::NUMERIC,
4
)
)
ORDER BY t.interval_start
),
'[]'::JSONB
)
INTO v_ivals
FROM (
SELECT pi.*
FROM ems.planning_interval pi
WHERE pi.run_id = v_run.id
AND pi.interval_start >= v_slot
AND pi.interval_start < v_win_end
) t;
select jsonb_build_object(
'window_start_utc', v_slot,
'window_end_utc', v_win_end,
'total_import_kwh', coalesce(sum(
case when pi.grid_setpoint_w > 0
then pi.grid_setpoint_w * 0.25 / 1000.0 else 0 end
), 0),
'total_export_kwh', coalesce(sum(
case when pi.grid_setpoint_w < 0
then -pi.grid_setpoint_w * 0.25 / 1000.0 else 0 end
), 0),
'total_buy_cost_czk', coalesce(sum(
case when pi.grid_setpoint_w > 0
then pi.grid_setpoint_w * pi.effective_buy_price * 0.25 / 1000.0
else 0 end
), 0),
'total_sell_revenue_czk', coalesce(sum(
case when pi.grid_setpoint_w < 0
then -pi.grid_setpoint_w * pi.effective_sell_price * 0.25 / 1000.0
else 0 end
), 0),
'total_cashflow_czk', coalesce(sum(pi.cashflow_czk), 0),
'total_battery_arbitrage_czk', coalesce(sum(pi.battery_arbitrage_czk), 0),
'total_penalty_czk', coalesce(sum(pi.penalty_czk), 0),
'total_green_bonus_czk', coalesce(sum(pi.green_bonus_czk), 0),
'net_economic_czk',
coalesce(-sum(pi.cashflow_czk), 0)
+ coalesce(sum(pi.battery_arbitrage_czk), 0)
- coalesce(sum(pi.penalty_czk), 0)
+ coalesce(sum(pi.green_bonus_czk), 0),
'neg_sell_export_slots', count(*) filter (
where pi.effective_sell_price < 0 and pi.grid_setpoint_w < -500
),
'first_grid_charge_slot_utc', min(pi.interval_start) filter (
where pi.grid_setpoint_w > 500
)
)
into v_econ
from ems.planning_interval pi
where pi.run_id = v_run.id
and pi.interval_start >= v_slot
and pi.interval_start < v_win_end;
SELECT to_jsonb(m.*) || jsonb_build_object('mode_name', d.name)
INTO v_mode
FROM ems.site_operating_mode m
LEFT JOIN ems.operating_mode_def d ON d.code = m.mode_code
WHERE m.site_id = p_site_id;
SELECT COALESCE(jsonb_agg(to_jsonb(b) ORDER BY b.id), '[]'::JSONB)
INTO v_batt
FROM ems.asset_battery b
WHERE b.site_id = p_site_id;
SELECT to_jsonb(g.*)
INTO v_grid
FROM ems.site_grid_connection g
WHERE g.site_id = p_site_id;
SELECT COALESCE(jsonb_agg(to_jsonb(h) ORDER BY h.id), '[]'::JSONB)
INTO v_hp
FROM ems.asset_heat_pump h
WHERE h.site_id = p_site_id;
SELECT COALESCE(jsonb_agg(to_jsonb(e) ORDER BY e.id), '[]'::JSONB)
INTO v_ev
FROM ems.ev_session e
WHERE e.site_id = p_site_id
AND e.session_end IS NULL;
SELECT COALESCE(
jsonb_agg(to_jsonb(f) ORDER BY f.logged_at DESC),
'[]'::JSONB
)
INTO v_fc
FROM (
SELECT l.*
FROM ems.forecast_correction_log l
WHERE l.site_id = p_site_id
ORDER BY l.logged_at DESC
LIMIT 5
) f;
SELECT COALESCE(
jsonb_agg(to_jsonb(o) ORDER BY o.valid_from DESC),
'[]'::JSONB
)
INTO v_ov
FROM (
SELECT o.*
FROM ems.site_override o
WHERE o.site_id = p_site_id
AND o.valid_from < v_win_end
AND (o.valid_to IS NULL OR o.valid_to > v_slot)
ORDER BY o.valid_from DESC
LIMIT 20
) o;
RETURN jsonb_build_object(
'meta', v_meta,
'site', (
SELECT jsonb_build_object(
'id', s.id,
'code', s.code,
'name', s.name,
'timezone', s.timezone,
'active', s.active
)
FROM ems.site s
WHERE s.id = p_site_id
),
'active_planning_run', (
SELECT to_jsonb(pr.*)
FROM ems.planning_run pr
WHERE pr.id = v_run.id
),
'intervals_next_window', v_ivals,
'operating_mode', v_mode,
'asset_battery', v_batt,
'site_grid_connection', v_grid,
'asset_heat_pump', v_hp,
'ev_sessions_open', v_ev,
'forecast_correction_log_recent', v_fc,
'site_overrides_active_in_window', v_ov,
'economics_summary', v_econ,
'ai_readme', jsonb_build_object(
'purpose',
'Data stačí k vysvětlení „proč plán v dalších hodinách vypadá takto“: ceny v řádcích intervalů, vstupy (baseline, PV), výstupy (bat/grid/EV/TČ), režim a síťové limity.',
'code_refs',
ARRAY[
'backend/services/planning_engine.py (solve_dispatch, run_type rolling/daily)',
'backend/services/control_exporter.py (mapování na Deye)',
'CLAUDE.md bod 1519 (baseline, horizont, režimy, MILP export)'
],
'horizon_and_objective',
'Dynamický horizont: ems.fn_planning_horizon_end (OTE + strop/min v SQL). Účelovka LP bez vzdálenostních vah; terminal SoC shadow price (avg buy × 0,9 / 1000 × soc_end). Sloupec hours_from_plan_horizon_start u intervalů jen pro čtení.'
)
);
END;
$$;
COMMENT ON FUNCTION ems.fn_plan_explain_bundle(INT, NUMERIC) IS
'JSONB balík: aktivní planning_run, planning_interval v okně od aktuálního 15min slotu, režim, baterie/síť/TČ, otevřené EV session, poslední forecast_correction_log, platné site_override. Pro AI / diagnostiku bez opakovaného skládání dotazů.';