plan explain DB function
This commit is contained in:
190
db/routines/R__fn_plan_explain_bundle.sql
Normal file
190
db/routines/R__fn_plan_explain_bundle.sql
Normal 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 15–19 (baseline, horizont, režimy, MILP export)'
|
||||||
|
],
|
||||||
|
'slot_weights_hours_from_horizon_start',
|
||||||
|
'0–36: váha 1.0; 36–72: 0.7; 72–96: 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ů.';
|
||||||
Reference in New Issue
Block a user