implmentace plan guardu
This commit is contained in:
18
db/migration/V052__plan_fatal_deviation_sent.sql
Normal file
18
db/migration/V052__plan_fatal_deviation_sent.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- Jednorázové potvrzení odeslání fatálního Discord alertu plán vs. skutečnost (deduplikace po slotu).
|
||||
|
||||
create table ems.plan_fatal_deviation_sent (
|
||||
site_id int not null references ems.site (id),
|
||||
interval_start timestamptz not null,
|
||||
reason_code text not null,
|
||||
sent_at timestamptz not null default now(),
|
||||
primary key (site_id, interval_start)
|
||||
);
|
||||
|
||||
create index idx_plan_fatal_deviation_sent_sent_at
|
||||
on ems.plan_fatal_deviation_sent (sent_at desc);
|
||||
|
||||
comment on table ems.plan_fatal_deviation_sent is
|
||||
'Backend job po uzavření 15min slotu: při fatální odchylce grid plán vs. audit jednou pošle Discord a zapíše řádek (PK site_id + interval_start).';
|
||||
|
||||
comment on column ems.plan_fatal_deviation_sent.reason_code is
|
||||
'Kód z ems.fn_plan_actual_slot_guard_site (např. GRID_SIGN_MISMATCH, GRID_EXPORT_SPIKE).';
|
||||
@@ -1,6 +1,12 @@
|
||||
-- začátek aktuálního (+offset) 15min slotu v Europe/Prague jako timestamptz (UTC instants)
|
||||
-- Začátek aktuálního (+offset) 15min slotu v Europe/Prague jako timestamptz (UTC instants).
|
||||
-- Volitelné p_at (např. job po uzavření slotu); null = now().
|
||||
|
||||
create or replace function ems.fn_planning_slot_boundary_prague(p_offset_slots int default 0)
|
||||
drop function if exists ems.fn_planning_slot_boundary_prague(int);
|
||||
|
||||
create or replace function ems.fn_planning_slot_boundary_prague(
|
||||
p_offset_slots int default 0,
|
||||
p_at timestamptz default null
|
||||
)
|
||||
returns timestamptz
|
||||
language sql
|
||||
stable
|
||||
@@ -13,8 +19,10 @@ as $fn$
|
||||
)
|
||||
)::timestamp at time zone 'Europe/Prague'
|
||||
) + make_interval(mins => coalesce(p_offset_slots, 0) * 15)
|
||||
from (select now() at time zone 'Europe/Prague' as ts) loc;
|
||||
from (
|
||||
select coalesce(p_at, now()) at time zone 'Europe/Prague' as ts
|
||||
) loc;
|
||||
$fn$;
|
||||
|
||||
comment on function ems.fn_planning_slot_boundary_prague(int) is
|
||||
'Začátek 15min slotu v časové zóně site provozu (Europe/Prague floor); offset v násobcích 15 min.';
|
||||
comment on function ems.fn_planning_slot_boundary_prague(int, timestamptz) is
|
||||
'Začátek 15min slotu (Europe/Prague floor); offset v násobcích 15 min; p_at volitelně místo now().';
|
||||
|
||||
177
db/routines/R__076_fn_plan_actual_slot_guard.sql
Normal file
177
db/routines/R__076_fn_plan_actual_slot_guard.sql
Normal file
@@ -0,0 +1,177 @@
|
||||
-- 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
|
||||
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 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 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 → insert plan_fatal_deviation_sent (dedup); vrátí JSON s alerts k odeslání na 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.';
|
||||
Reference in New Issue
Block a user