Force PASSIVE/no-export when sell is negative or export_mode is NONE, and alert NEG_SELL_EXPORT in plan_actual_slot_guard when export still occurs. Co-authored-by: Cursor <cursoragent@cursor.com>
201 lines
6.6 KiB
SQL
201 lines
6.6 KiB
SQL
-- Kontrola plán vs. audit (síť) po uzavření slotu: pravidla v DB, dedup insert, výstup pro Discord z Pythonu.
|
|
|
|
create or replace function ems.fn_plan_actual_slot_guard_site(
|
|
p_site_id int,
|
|
p_now timestamptz default now()
|
|
)
|
|
returns jsonb
|
|
language sql
|
|
volatile
|
|
as $fn$
|
|
with v_code as (
|
|
select s.code as site_code
|
|
from ems.site s
|
|
where s.id = p_site_id
|
|
),
|
|
slots as (
|
|
select distinct unnest(array[
|
|
ems.fn_planning_slot_boundary_prague(-1, p_now),
|
|
ems.fn_planning_slot_boundary_prague(-2, p_now)
|
|
]) as interval_start
|
|
),
|
|
base as (
|
|
select
|
|
s.interval_start,
|
|
ai.actual_grid_power_w,
|
|
ai.deviation_grid_w,
|
|
pi.grid_setpoint_w as plan_grid_w,
|
|
pi.effective_sell_price as plan_sell_czk
|
|
from slots s
|
|
inner join ems.audit_interval ai
|
|
on ai.site_id = p_site_id
|
|
and ai.interval_start = s.interval_start
|
|
left join ems.planning_interval pi
|
|
on pi.run_id = ai.planning_run_id
|
|
and pi.interval_start = ai.interval_start
|
|
),
|
|
cls as (
|
|
select
|
|
b.interval_start,
|
|
b.plan_grid_w,
|
|
coalesce(b.actual_grid_power_w, 0) as actual_grid_w,
|
|
b.deviation_grid_w,
|
|
case
|
|
when b.plan_grid_w is null or b.deviation_grid_w is null then null::text
|
|
when coalesce(
|
|
b.plan_sell_czk,
|
|
ems.fn_effective_sell_price(p_site_id, b.interval_start)
|
|
) < 0
|
|
and coalesce(b.actual_grid_power_w, 0) < -4000
|
|
then 'NEG_SELL_EXPORT'
|
|
when b.plan_grid_w < -2000 and coalesce(b.actual_grid_power_w, 0) > 2500
|
|
then 'GRID_IMPORT_VS_EXPORT_PLAN'
|
|
when b.plan_grid_w <> 0
|
|
and coalesce(b.actual_grid_power_w, 0) <> 0
|
|
and (b.plan_grid_w > 0)
|
|
<> (coalesce(b.actual_grid_power_w, 0) > 0)
|
|
and least(
|
|
abs(b.plan_grid_w),
|
|
abs(coalesce(b.actual_grid_power_w, 0))
|
|
) >= 400
|
|
then 'GRID_SIGN_MISMATCH'
|
|
when b.plan_grid_w > -1000 and coalesce(b.actual_grid_power_w, 0) < -4000
|
|
then 'GRID_EXPORT_SPIKE'
|
|
when abs(b.deviation_grid_w) >= 10000 and abs(b.plan_grid_w) <= 2500
|
|
then 'GRID_LARGE_DEVIATION'
|
|
else null::text
|
|
end as reason_code,
|
|
case
|
|
when b.plan_grid_w is null or b.deviation_grid_w is null then null::text
|
|
when coalesce(
|
|
b.plan_sell_czk,
|
|
ems.fn_effective_sell_price(p_site_id, b.interval_start)
|
|
) < 0
|
|
and coalesce(b.actual_grid_power_w, 0) < -4000
|
|
then format(
|
|
'záporná vykupní %s Kč/kWh, skutečnost síť %s W (vývoz nad práh 4 kW)',
|
|
round(
|
|
coalesce(
|
|
b.plan_sell_czk,
|
|
ems.fn_effective_sell_price(p_site_id, b.interval_start)
|
|
)::numeric,
|
|
4
|
|
),
|
|
coalesce(b.actual_grid_power_w, 0)
|
|
)
|
|
when b.plan_grid_w < -2000 and coalesce(b.actual_grid_power_w, 0) > 2500
|
|
then format(
|
|
'plán síť %s W vs skutečnost %s W (plán vývoz, skutečnost silný odběr)',
|
|
b.plan_grid_w,
|
|
coalesce(b.actual_grid_power_w, 0)
|
|
)
|
|
when b.plan_grid_w <> 0
|
|
and coalesce(b.actual_grid_power_w, 0) <> 0
|
|
and (b.plan_grid_w > 0)
|
|
<> (coalesce(b.actual_grid_power_w, 0) > 0)
|
|
and least(
|
|
abs(b.plan_grid_w),
|
|
abs(coalesce(b.actual_grid_power_w, 0))
|
|
) >= 400
|
|
then format(
|
|
'plán síť %s W vs skutečnost %s W (opačný směr import/export)',
|
|
b.plan_grid_w,
|
|
coalesce(b.actual_grid_power_w, 0)
|
|
)
|
|
when b.plan_grid_w > -1000 and coalesce(b.actual_grid_power_w, 0) < -4000
|
|
then format(
|
|
'plán síť %s W vs skutečnost %s W (neočekávaný silný vývoz)',
|
|
b.plan_grid_w,
|
|
coalesce(b.actual_grid_power_w, 0)
|
|
)
|
|
when abs(b.deviation_grid_w) >= 10000 and abs(b.plan_grid_w) <= 2500
|
|
then format(
|
|
'odchylka výkonu sítě %s W (plán %s W, skutečnost %s W)',
|
|
b.deviation_grid_w,
|
|
b.plan_grid_w,
|
|
coalesce(b.actual_grid_power_w, 0)
|
|
)
|
|
else null::text
|
|
end as detail_cs
|
|
from base b
|
|
),
|
|
ins as (
|
|
insert into ems.plan_fatal_deviation_sent (site_id, interval_start, reason_code)
|
|
select
|
|
p_site_id,
|
|
c.interval_start,
|
|
c.reason_code
|
|
from cls c
|
|
where c.reason_code is not null
|
|
on conflict (site_id, interval_start) do nothing
|
|
returning interval_start, reason_code
|
|
),
|
|
notified as (
|
|
select
|
|
c.interval_start,
|
|
c.reason_code,
|
|
c.detail_cs,
|
|
c.plan_grid_w,
|
|
c.actual_grid_w,
|
|
c.deviation_grid_w
|
|
from cls c
|
|
inner join ins i
|
|
on i.interval_start = c.interval_start
|
|
and i.reason_code = c.reason_code
|
|
)
|
|
select
|
|
case
|
|
when not exists (select 1 from v_code) then
|
|
jsonb_build_object('error', 'unknown_site', 'site_id', p_site_id)
|
|
else
|
|
jsonb_build_object(
|
|
'site_id', p_site_id,
|
|
'site_code', (select vc.site_code from v_code vc),
|
|
'alerts', coalesce(
|
|
(
|
|
select coalesce(
|
|
jsonb_agg(
|
|
jsonb_build_object(
|
|
'interval_start', n.interval_start,
|
|
'reason_code', n.reason_code,
|
|
'detail', n.detail_cs,
|
|
'plan_grid_w', n.plan_grid_w,
|
|
'actual_grid_w', n.actual_grid_w,
|
|
'deviation_grid_w', n.deviation_grid_w,
|
|
'notify', true
|
|
)
|
|
order by n.interval_start
|
|
),
|
|
'[]'::jsonb
|
|
)
|
|
from notified n
|
|
),
|
|
'[]'::jsonb
|
|
)
|
|
)
|
|
end;
|
|
$fn$;
|
|
|
|
comment on function ems.fn_plan_actual_slot_guard_site(int, timestamptz) is
|
|
'Poslední 2 uzavřené 15min sloty: fatální odchylka síť plán vs. audit (včetně NEG_SELL_EXPORT při sell<0 a vývozu >4 kW) → insert plan_fatal_deviation_sent (dedup); JSON alerts pro Discord.';
|
|
|
|
create or replace function ems.fn_plan_actual_slot_guard_all_active(
|
|
p_now timestamptz default now()
|
|
)
|
|
returns jsonb
|
|
language sql
|
|
volatile
|
|
as $fn$
|
|
select coalesce(
|
|
jsonb_agg(
|
|
ems.fn_plan_actual_slot_guard_site((elem->>'id')::int, p_now)
|
|
order by (elem->>'id')::int
|
|
),
|
|
'[]'::jsonb
|
|
)
|
|
from jsonb_array_elements(ems.fn_vw_site_directory_active()) as t(elem);
|
|
$fn$;
|
|
|
|
comment on function ems.fn_plan_actual_slot_guard_all_active(timestamptz) is
|
|
'Projde aktivní lokality (fn_vw_site_directory_active) a zavolá fn_plan_actual_slot_guard_site; pole výsledků pro scheduler.';
|