fix repeatable migrations

This commit is contained in:
Dusan Vojacek
2026-04-19 20:15:46 +02:00
parent 0c93f493a4
commit 22bca9cd9e
73 changed files with 22 additions and 15 deletions

View 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).';