Files
ems/db/routines/R__063_fn_load_planning_slots_full.sql
2026-04-19 20:15:46 +02:00

286 lines
8.9 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 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).';