plan explain DB function
All checks were successful
deploy / deploy (push) Successful in 16s
test / smoke-test (push) Successful in 3s

This commit is contained in:
Dusan Vojacek
2026-04-19 13:58:08 +02:00
parent d3fd8b139a
commit 477e94f321

View File

@@ -0,0 +1,190 @@
-- 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;
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 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,
'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)'
],
'slot_weights_hours_from_horizon_start',
'036: váha 1.0; 3672: 0.7; 7296: 0.4 (účelovka LP; sloupec hours_from_plan_horizon_start u každého intervalu).'
)
);
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ů.';