fix repeatable migrations
This commit is contained in:
285
db/routines/R__063_fn_load_planning_slots_full.sql
Normal file
285
db/routines/R__063_fn_load_planning_slots_full.sql
Normal file
@@ -0,0 +1,285 @@
|
||||
-- sloty pro LP: ceny, forecast, baseline, EV připojení + masky allow_charge / allow_discharge_export
|
||||
|
||||
create or replace function ems.fn_load_planning_slots_full(
|
||||
p_site_id int,
|
||||
p_from timestamptz,
|
||||
p_to timestamptz,
|
||||
p_current_soc_wh numeric
|
||||
)
|
||||
returns table (
|
||||
slot_ord int,
|
||||
interval_start timestamptz,
|
||||
buy_price numeric,
|
||||
sell_price numeric,
|
||||
is_predicted_price boolean,
|
||||
pv_a_forecast_w int,
|
||||
pv_b_forecast_w int,
|
||||
load_baseline_w int,
|
||||
ev1_connected boolean,
|
||||
ev2_connected boolean,
|
||||
allow_charge boolean,
|
||||
allow_discharge_export boolean
|
||||
)
|
||||
language plpgsql
|
||||
stable
|
||||
as $fn$
|
||||
declare
|
||||
v_charge_buf numeric;
|
||||
v_discharge_buf numeric;
|
||||
v_usable numeric;
|
||||
v_min_soc_wh numeric;
|
||||
v_soc_max_wh numeric;
|
||||
v_energy_to_fill numeric;
|
||||
v_exportable numeric;
|
||||
v_charge_eff numeric;
|
||||
v_discharge_eff numeric;
|
||||
v_max_charge_w numeric;
|
||||
v_max_discharge_w numeric;
|
||||
v_per_slot_charge_wh numeric;
|
||||
v_per_slot_discharge_wh numeric;
|
||||
v_grid_target_wh numeric;
|
||||
v_discharge_target_wh numeric;
|
||||
v_cum numeric;
|
||||
r_slot record;
|
||||
begin
|
||||
drop table if exists _ems_plan_slot_wk;
|
||||
create temp table _ems_plan_slot_wk on commit drop as
|
||||
with slot_spine as (
|
||||
select gs as interval_start
|
||||
from generate_series(
|
||||
p_from,
|
||||
(p_to - interval '15 minutes')::timestamptz,
|
||||
interval '15 minutes'
|
||||
) as gs
|
||||
)
|
||||
select
|
||||
row_number() over (order by s.interval_start) - 1 as slot_ord,
|
||||
s.interval_start,
|
||||
coalesce(
|
||||
ep.effective_buy_price_czk_kwh,
|
||||
ems.fn_get_predicted_price(p_site_id, s.interval_start)
|
||||
) as buy_price,
|
||||
coalesce(
|
||||
ep.effective_sell_price_czk_kwh,
|
||||
ems.fn_get_predicted_price(p_site_id, s.interval_start) * 0.85
|
||||
) as sell_price,
|
||||
(ep.effective_buy_price_czk_kwh is null) as is_predicted_price,
|
||||
coalesce(fpi_a.power_w, 0)::int as pv_a_forecast_w,
|
||||
coalesce(fpi_b.power_w, 0)::int as pv_b_forecast_w,
|
||||
coalesce(
|
||||
(
|
||||
select bs.avg_power_w
|
||||
from ems.consumption_baseline_stats bs
|
||||
where bs.site_id = p_site_id
|
||||
and bs.day_of_week = extract(
|
||||
dow from s.interval_start at time zone 'Europe/Prague'
|
||||
)::int
|
||||
and bs.hour_of_day = extract(
|
||||
hour from s.interval_start at time zone 'Europe/Prague'
|
||||
)::int
|
||||
limit 1
|
||||
),
|
||||
500
|
||||
)::int as load_baseline_w,
|
||||
(coalesce(ev1.status, 'available') not in ('available', 'unavailable')) as ev1_connected,
|
||||
(coalesce(ev2.status, 'available') not in ('available', 'unavailable')) as ev2_connected,
|
||||
greatest(
|
||||
0,
|
||||
coalesce(fpi_a.power_w, 0) + coalesce(fpi_b.power_w, 0)
|
||||
- coalesce(
|
||||
(
|
||||
select bs.avg_power_w
|
||||
from ems.consumption_baseline_stats bs
|
||||
where bs.site_id = p_site_id
|
||||
and bs.day_of_week = extract(
|
||||
dow from s.interval_start at time zone 'Europe/Prague'
|
||||
)::int
|
||||
and bs.hour_of_day = extract(
|
||||
hour from s.interval_start at time zone 'Europe/Prague'
|
||||
)::int
|
||||
limit 1
|
||||
),
|
||||
500
|
||||
)
|
||||
)::int as pv_surplus_w,
|
||||
false::boolean as allow_charge,
|
||||
false::boolean as allow_discharge_export
|
||||
from slot_spine s
|
||||
left join ems.vw_site_effective_price ep
|
||||
on ep.site_id = p_site_id and ep.interval_start = s.interval_start
|
||||
left join lateral (
|
||||
select coalesce(sum(u.power_w), 0)::int as power_w
|
||||
from (
|
||||
select distinct on (apa.id)
|
||||
fpi.power_w
|
||||
from ems.asset_pv_array apa
|
||||
join ems.forecast_pv_run fpr
|
||||
on fpr.pv_array_id = apa.id
|
||||
and fpr.site_id = apa.site_id
|
||||
and fpr.status = 'ok'
|
||||
join ems.forecast_pv_interval fpi
|
||||
on fpi.run_id = fpr.id
|
||||
and fpi.pv_array_id = apa.id
|
||||
and fpi.interval_start = s.interval_start
|
||||
where apa.site_id = p_site_id
|
||||
and apa.controllable is true
|
||||
order by apa.id, fpr.created_at desc
|
||||
) u
|
||||
) fpi_a on true
|
||||
left join lateral (
|
||||
select coalesce(sum(u.power_w), 0)::int as power_w
|
||||
from (
|
||||
select distinct on (apa.id)
|
||||
fpi.power_w
|
||||
from ems.asset_pv_array apa
|
||||
join ems.forecast_pv_run fpr
|
||||
on fpr.pv_array_id = apa.id
|
||||
and fpr.site_id = apa.site_id
|
||||
and fpr.status = 'ok'
|
||||
join ems.forecast_pv_interval fpi
|
||||
on fpi.run_id = fpr.id
|
||||
and fpi.pv_array_id = apa.id
|
||||
and fpi.interval_start = s.interval_start
|
||||
where apa.site_id = p_site_id
|
||||
and apa.controllable is false
|
||||
order by apa.id, fpr.created_at desc
|
||||
) u
|
||||
) fpi_b on true
|
||||
left join lateral (
|
||||
select t.status
|
||||
from ems.telemetry_ev_charger t
|
||||
join ems.asset_ev_charger ch on ch.id = t.charger_id
|
||||
where t.site_id = p_site_id and ch.code = 'ev-charger-1'
|
||||
order by t.measured_at desc
|
||||
limit 1
|
||||
) ev1 on true
|
||||
left join lateral (
|
||||
select t.status
|
||||
from ems.telemetry_ev_charger t
|
||||
join ems.asset_ev_charger ch on ch.id = t.charger_id
|
||||
where t.site_id = p_site_id and ch.code = 'ev-charger-2'
|
||||
order by t.measured_at desc
|
||||
limit 1
|
||||
) ev2 on true;
|
||||
|
||||
if not exists (select 1 from _ems_plan_slot_wk) then
|
||||
raise exception 'No planning slots available – check market prices and horizon settings';
|
||||
end if;
|
||||
|
||||
select
|
||||
coalesce(ab.charge_slot_buffer, 0::numeric),
|
||||
coalesce(ab.discharge_slot_buffer, 0::numeric),
|
||||
ab.usable_capacity_wh::numeric,
|
||||
(ab.min_soc_percent / 100.0 * ab.usable_capacity_wh)::numeric,
|
||||
(ab.max_soc_percent / 100.0 * ab.usable_capacity_wh)::numeric,
|
||||
greatest(coalesce(ab.charge_efficiency, 1::numeric), 0.0001::numeric),
|
||||
least(
|
||||
coalesce(ai.max_battery_charge_w, ai.max_charge_power_w),
|
||||
coalesce(
|
||||
ab.bms_max_charge_w,
|
||||
case when ab.max_charge_c_rate is not null
|
||||
then (ab.max_charge_c_rate * ab.usable_capacity_wh)::bigint
|
||||
end,
|
||||
coalesce(ai.max_battery_charge_w, ai.max_charge_power_w)
|
||||
)
|
||||
)::numeric,
|
||||
least(
|
||||
coalesce(ai.max_battery_discharge_w, ai.max_discharge_power_w),
|
||||
coalesce(
|
||||
ab.bms_max_discharge_w,
|
||||
case when ab.max_discharge_c_rate is not null
|
||||
then (ab.max_discharge_c_rate * ab.usable_capacity_wh)::bigint
|
||||
end,
|
||||
coalesce(ai.max_battery_discharge_w, ai.max_discharge_power_w)
|
||||
)
|
||||
)::numeric,
|
||||
greatest(coalesce(ab.discharge_efficiency, 1::numeric), 0.0001::numeric)
|
||||
into
|
||||
v_charge_buf,
|
||||
v_discharge_buf,
|
||||
v_usable,
|
||||
v_min_soc_wh,
|
||||
v_soc_max_wh,
|
||||
v_charge_eff,
|
||||
v_max_charge_w,
|
||||
v_max_discharge_w,
|
||||
v_discharge_eff
|
||||
from ems.asset_battery ab
|
||||
join ems.asset_inverter ai on ai.id = ab.inverter_id and ai.site_id = ab.site_id
|
||||
where ab.site_id = p_site_id
|
||||
order by ab.id
|
||||
limit 1;
|
||||
|
||||
if v_usable is null then
|
||||
raise exception 'No asset_battery for site_id=%', p_site_id;
|
||||
end if;
|
||||
|
||||
v_per_slot_charge_wh := v_max_charge_w * v_charge_eff * 0.25;
|
||||
v_per_slot_discharge_wh := v_max_discharge_w * v_discharge_eff * 0.25;
|
||||
v_energy_to_fill := v_soc_max_wh - p_current_soc_wh;
|
||||
v_exportable := v_soc_max_wh - v_min_soc_wh;
|
||||
v_grid_target_wh := v_energy_to_fill * v_charge_buf;
|
||||
v_discharge_target_wh := v_exportable * v_discharge_buf;
|
||||
|
||||
-- charge mask
|
||||
if v_charge_buf <= 0 then
|
||||
update _ems_plan_slot_wk set allow_charge = true;
|
||||
elsif v_energy_to_fill <= 0 then
|
||||
update _ems_plan_slot_wk set allow_charge = false;
|
||||
else
|
||||
update _ems_plan_slot_wk set allow_charge = (pv_surplus_w > 0);
|
||||
v_cum := 0;
|
||||
for r_slot in
|
||||
select slot_ord
|
||||
from _ems_plan_slot_wk
|
||||
where pv_surplus_w <= 0
|
||||
order by buy_price, slot_ord
|
||||
loop
|
||||
exit when v_cum >= v_grid_target_wh;
|
||||
exit when v_per_slot_charge_wh <= 0;
|
||||
update _ems_plan_slot_wk set allow_charge = true where slot_ord = r_slot.slot_ord;
|
||||
v_cum := v_cum + v_per_slot_charge_wh;
|
||||
end loop;
|
||||
end if;
|
||||
|
||||
-- discharge-export mask
|
||||
if v_discharge_buf <= 0 then
|
||||
update _ems_plan_slot_wk set allow_discharge_export = true;
|
||||
elsif v_exportable <= 0 then
|
||||
update _ems_plan_slot_wk set allow_discharge_export = false;
|
||||
else
|
||||
update _ems_plan_slot_wk set allow_discharge_export = false;
|
||||
v_cum := 0;
|
||||
for r_slot in
|
||||
select slot_ord
|
||||
from _ems_plan_slot_wk
|
||||
order by sell_price desc, slot_ord desc
|
||||
loop
|
||||
exit when v_cum >= v_discharge_target_wh;
|
||||
exit when v_per_slot_discharge_wh <= 0;
|
||||
update _ems_plan_slot_wk set allow_discharge_export = true where slot_ord = r_slot.slot_ord;
|
||||
v_cum := v_cum + v_per_slot_discharge_wh;
|
||||
end loop;
|
||||
end if;
|
||||
|
||||
return query
|
||||
select
|
||||
w.slot_ord,
|
||||
w.interval_start,
|
||||
w.buy_price,
|
||||
w.sell_price,
|
||||
w.is_predicted_price,
|
||||
w.pv_a_forecast_w,
|
||||
w.pv_b_forecast_w,
|
||||
w.load_baseline_w,
|
||||
w.ev1_connected,
|
||||
w.ev2_connected,
|
||||
w.allow_charge,
|
||||
w.allow_discharge_export
|
||||
from _ems_plan_slot_wk w
|
||||
order by w.slot_ord;
|
||||
end;
|
||||
$fn$;
|
||||
|
||||
comment on function ems.fn_load_planning_slots_full(int, timestamptz, timestamptz, numeric) is
|
||||
'15min sloty s cenami, forecastem, baseline a maskami proti mikro-cyklu (charge/discharge-export).';
|
||||
Reference in New Issue
Block a user