286 lines
9.0 KiB
PL/PgSQL
286 lines
9.0 KiB
PL/PgSQL
-- 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
|
||
volatile
|
||
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 (sloupce temp tabulky kvalifikujeme: RETURNS TABLE dělá PL proměnné stejných jmen)
|
||
if v_charge_buf <= 0 then
|
||
update _ems_plan_slot_wk wk set allow_charge = true;
|
||
elsif v_energy_to_fill <= 0 then
|
||
update _ems_plan_slot_wk wk set allow_charge = false;
|
||
else
|
||
update _ems_plan_slot_wk wk set allow_charge = (wk.pv_surplus_w > 0);
|
||
v_cum := 0;
|
||
for r_slot in
|
||
select wk.slot_ord
|
||
from _ems_plan_slot_wk wk
|
||
where wk.pv_surplus_w <= 0
|
||
order by wk.buy_price, wk.slot_ord
|
||
loop
|
||
exit when v_cum >= v_grid_target_wh;
|
||
exit when v_per_slot_charge_wh <= 0;
|
||
update _ems_plan_slot_wk wk set allow_charge = true where wk.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 wk set allow_discharge_export = true;
|
||
elsif v_exportable <= 0 then
|
||
update _ems_plan_slot_wk wk set allow_discharge_export = false;
|
||
else
|
||
update _ems_plan_slot_wk wk set allow_discharge_export = false;
|
||
v_cum := 0;
|
||
for r_slot in
|
||
select wk.slot_ord
|
||
from _ems_plan_slot_wk wk
|
||
order by wk.sell_price desc, wk.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 wk set allow_discharge_export = true where wk.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).';
|