354 lines
12 KiB
SQL
354 lines
12 KiB
SQL
-- Denní ekonomika za [p_ts_from, p_ts_to): jen audit_interval v okně + joiny (žádný vw_site_effective_price,
|
|
-- žádné volání fn_effective_* per řádek — musí zůstat v souladu s R__011_fn_effective_price.sql).
|
|
|
|
create or replace function ems.fn_economics_daily_for_window(
|
|
p_site_id int,
|
|
p_ts_from timestamptz,
|
|
p_ts_to timestamptz
|
|
)
|
|
returns table (
|
|
site_id int,
|
|
day_local date,
|
|
interval_count int,
|
|
import_kwh numeric,
|
|
export_kwh numeric,
|
|
pv_kwh numeric,
|
|
load_kwh numeric,
|
|
pv_self_consumption_kwh numeric,
|
|
ev_kwh numeric,
|
|
hp_kwh numeric,
|
|
grid_import_cashflow_czk numeric,
|
|
grid_export_revenue_czk numeric,
|
|
import_cost_czk numeric,
|
|
export_revenue_czk numeric,
|
|
net_cost_czk numeric,
|
|
green_bonus_czk numeric,
|
|
total_balance_czk numeric,
|
|
planned_net_cost_czk numeric,
|
|
planned_balance_czk numeric,
|
|
deviation_cost_czk numeric
|
|
)
|
|
language sql
|
|
stable
|
|
as $fn$
|
|
with slot0 as (
|
|
select
|
|
ai.site_id,
|
|
ai.interval_start,
|
|
(date_trunc('day', ai.interval_start at time zone 'Europe/Prague'))::date as day_local,
|
|
ai.actual_grid_import_wh,
|
|
ai.actual_grid_power_w,
|
|
ai.actual_grid_export_wh,
|
|
ai.green_bonus_czk,
|
|
ai.actual_pv_power_w,
|
|
ai.actual_load_power_w,
|
|
ai.actual_ev_power_w,
|
|
ai.actual_heat_pump_power_w,
|
|
smc.id as market_config_id,
|
|
smc.purchase_pricing_mode,
|
|
smc.buy_fixed_energy_nt_czk_kwh,
|
|
smc.buy_fixed_vt_surcharge_czk_kwh,
|
|
smc.buy_margin_fixed_czk,
|
|
smc.buy_margin_percent,
|
|
smc.sell_margin_fixed_czk,
|
|
smc.sell_margin_percent,
|
|
smc.system_services_czk_kwh,
|
|
smc.ote_fee_czk_kwh,
|
|
smc.hdo_code_id,
|
|
smc.tariff_id,
|
|
coalesce(dt.vat_rate, 0.21) as vat_rate,
|
|
mip.buy_raw_price_czk_kwh as buy_spot,
|
|
mip.sell_raw_price_czk_kwh as sell_spot,
|
|
pi.expected_cost_czk as planned_cost_czk
|
|
from ems.audit_interval ai
|
|
left join lateral (
|
|
select smc_inner.*
|
|
from ems.site_market_config smc_inner
|
|
where smc_inner.site_id = ai.site_id
|
|
and smc_inner.valid_from <= ai.interval_start
|
|
and (smc_inner.valid_to is null or smc_inner.valid_to > ai.interval_start)
|
|
order by smc_inner.valid_from desc
|
|
limit 1
|
|
) smc on true
|
|
left join ems.distribution_tariff dt on dt.id = smc.tariff_id
|
|
left join lateral (
|
|
select
|
|
mip_inner.buy_raw_price_czk_kwh,
|
|
mip_inner.sell_raw_price_czk_kwh
|
|
from ems.market_interval_price mip_inner
|
|
where mip_inner.interval_start = ai.interval_start
|
|
and mip_inner.market_source in ('OTE_CZ', 'OTE_CZ_DAM')
|
|
limit 1
|
|
) mip on true
|
|
left join ems.planning_interval pi
|
|
on pi.run_id = ai.planning_run_id
|
|
and pi.interval_start = ai.interval_start
|
|
where ai.site_id = p_site_id
|
|
and ai.interval_start >= p_ts_from
|
|
and ai.interval_start < p_ts_to
|
|
),
|
|
slot1 as (
|
|
select
|
|
s0.*,
|
|
case
|
|
when s0.hdo_code_id is null then false
|
|
else exists (
|
|
select 1
|
|
from ems.hdo_code_window w
|
|
where w.hdo_code_id = s0.hdo_code_id
|
|
and (
|
|
w.day_type = 'all'
|
|
or (
|
|
w.day_type = 'workday'
|
|
and extract(dow from s0.interval_start at time zone 'Europe/Prague') between 1 and 5
|
|
)
|
|
or (
|
|
w.day_type = 'weekend'
|
|
and extract(dow from s0.interval_start at time zone 'Europe/Prague') in (0, 6)
|
|
)
|
|
)
|
|
and w.rate_type = 'VT'
|
|
and (s0.interval_start at time zone 'Europe/Prague')::time >= w.window_from
|
|
and (s0.interval_start at time zone 'Europe/Prague')::time < w.window_to
|
|
)
|
|
end as is_vt
|
|
from slot0 s0
|
|
),
|
|
slot2 as (
|
|
select
|
|
s1.*,
|
|
dr.dist_rate
|
|
from slot1 s1
|
|
left join lateral (
|
|
select dtr.price_czk_kwh as dist_rate
|
|
from ems.distribution_tariff_rate dtr
|
|
where dtr.tariff_id = s1.tariff_id
|
|
and dtr.rate_type = case when s1.is_vt then 'VT'::text else 'NT'::text end
|
|
and dtr.valid_from <= s1.interval_start::date
|
|
and (dtr.valid_to is null or dtr.valid_to > s1.interval_start::date)
|
|
order by dtr.valid_from desc
|
|
limit 1
|
|
) dr on s1.tariff_id is not null
|
|
),
|
|
slot3 as (
|
|
select
|
|
s2.*,
|
|
case
|
|
when upper(trim(coalesce(s2.purchase_pricing_mode, ''))) = 'FIXED'
|
|
and s2.buy_fixed_energy_nt_czk_kwh is not null
|
|
then
|
|
s2.buy_fixed_energy_nt_czk_kwh
|
|
+ case
|
|
when s2.is_vt then coalesce(s2.buy_fixed_vt_surcharge_czk_kwh, 0)
|
|
else 0::numeric
|
|
end
|
|
when s2.buy_spot is null then null::numeric
|
|
else s2.buy_spot
|
|
end as energy_czk
|
|
from slot2 s2
|
|
),
|
|
slot4 as (
|
|
select
|
|
s3.*,
|
|
case
|
|
when s3.market_config_id is null then null::numeric
|
|
when s3.energy_czk is null then null::numeric
|
|
else
|
|
coalesce(s3.buy_margin_fixed_czk, 0)
|
|
+ (s3.energy_czk * coalesce(s3.buy_margin_percent, 0) / 100.0)
|
|
end as buy_margin_part
|
|
from slot3 s3
|
|
),
|
|
slot5 as (
|
|
select
|
|
s4.*,
|
|
case
|
|
when s4.market_config_id is null then null::numeric
|
|
when s4.energy_czk is null then null::numeric
|
|
else round(
|
|
(
|
|
s4.energy_czk
|
|
+ coalesce(s4.dist_rate, 0)
|
|
+ coalesce(s4.system_services_czk_kwh, 0)
|
|
+ coalesce(s4.ote_fee_czk_kwh, 0)
|
|
+ s4.buy_margin_part
|
|
) * (1 + coalesce(s4.vat_rate, 0.21)),
|
|
6
|
|
)
|
|
end as buy_p,
|
|
case
|
|
when s4.market_config_id is null then null::numeric
|
|
when s4.sell_spot is null then null::numeric
|
|
else round(
|
|
s4.sell_spot
|
|
+ coalesce(s4.sell_margin_fixed_czk, 0)
|
|
+ (s4.sell_spot * coalesce(s4.sell_margin_percent, 0) / 100.0),
|
|
6
|
|
)
|
|
end as sell_p
|
|
from slot4 s4
|
|
),
|
|
slots as (
|
|
select
|
|
s5.site_id,
|
|
s5.day_local,
|
|
round(
|
|
coalesce(s5.actual_grid_import_wh, greatest(s5.actual_grid_power_w, 0)::numeric / 4) / 1000,
|
|
4
|
|
) as import_kwh,
|
|
round(
|
|
coalesce(s5.actual_grid_export_wh, abs(least(s5.actual_grid_power_w, 0))::numeric / 4) / 1000,
|
|
4
|
|
) as export_kwh,
|
|
round(
|
|
coalesce(s5.actual_grid_import_wh, greatest(s5.actual_grid_power_w, 0)::numeric / 4) / 1000.0
|
|
* coalesce(s5.buy_p, 0),
|
|
4
|
|
) as grid_import_cashflow_czk,
|
|
round(
|
|
coalesce(s5.actual_grid_export_wh, abs(least(s5.actual_grid_power_w, 0))::numeric / 4) / 1000.0
|
|
* coalesce(s5.sell_p, 0),
|
|
4
|
|
) as grid_export_revenue_czk,
|
|
round(
|
|
coalesce(s5.actual_grid_import_wh, greatest(s5.actual_grid_power_w, 0)::numeric / 4) / 1000.0
|
|
* coalesce(s5.buy_p, 0)
|
|
- coalesce(s5.actual_grid_export_wh, abs(least(s5.actual_grid_power_w, 0))::numeric / 4) / 1000.0
|
|
* coalesce(s5.sell_p, 0),
|
|
4
|
|
) as dynamic_cost_czk,
|
|
s5.green_bonus_czk,
|
|
s5.planned_cost_czk,
|
|
s5.actual_pv_power_w,
|
|
s5.actual_load_power_w,
|
|
s5.actual_ev_power_w,
|
|
s5.actual_heat_pump_power_w
|
|
from slot5 s5
|
|
),
|
|
daily_agg as (
|
|
select
|
|
s.site_id,
|
|
s.day_local,
|
|
count(*)::int as interval_count,
|
|
round(sum(s.import_kwh), 3) as import_kwh,
|
|
round(sum(s.export_kwh), 3) as export_kwh,
|
|
round(sum(greatest(s.actual_pv_power_w, 0)::numeric / 4000), 3) as pv_kwh,
|
|
round(sum(greatest(s.actual_load_power_w, 0)::numeric / 4000), 3) as load_kwh,
|
|
round(sum(greatest(s.actual_ev_power_w, 0)::numeric / 4000), 3) as ev_kwh,
|
|
round(sum(greatest(s.actual_heat_pump_power_w, 0)::numeric / 4000), 3) as hp_kwh,
|
|
round(
|
|
sum(greatest(s.actual_pv_power_w, 0)::numeric / 4000) - sum(s.export_kwh),
|
|
3
|
|
) as pv_self_consumption_kwh,
|
|
round(sum(s.grid_import_cashflow_czk), 2) as grid_import_cashflow_czk,
|
|
round(sum(s.grid_export_revenue_czk), 2) as grid_export_revenue_czk,
|
|
round(sum(case when s.dynamic_cost_czk > 0 then s.dynamic_cost_czk else 0 end), 2) as import_cost_czk,
|
|
round(sum(case when s.dynamic_cost_czk < 0 then abs(s.dynamic_cost_czk) else 0 end), 2) as export_revenue_czk,
|
|
round(sum(s.dynamic_cost_czk), 2) as net_cost_czk,
|
|
round(coalesce(sum(s.green_bonus_czk), 0), 2) as green_bonus_czk,
|
|
round(-sum(s.dynamic_cost_czk) + coalesce(sum(s.green_bonus_czk), 0), 2) as total_balance_czk,
|
|
round(sum(s.planned_cost_czk), 2) as planned_net_cost_czk,
|
|
round(-coalesce(sum(s.planned_cost_czk), 0), 2) as planned_balance_czk,
|
|
round(sum(s.dynamic_cost_czk) - coalesce(sum(s.planned_cost_czk), 0), 2) as deviation_cost_czk
|
|
from slots s
|
|
group by s.site_id, s.day_local
|
|
)
|
|
select
|
|
d.site_id,
|
|
d.day_local,
|
|
d.interval_count,
|
|
d.import_kwh,
|
|
d.export_kwh,
|
|
d.pv_kwh,
|
|
d.load_kwh,
|
|
d.pv_self_consumption_kwh,
|
|
d.ev_kwh,
|
|
d.hp_kwh,
|
|
d.grid_import_cashflow_czk,
|
|
d.grid_export_revenue_czk,
|
|
d.import_cost_czk,
|
|
d.export_revenue_czk,
|
|
d.net_cost_czk,
|
|
d.green_bonus_czk,
|
|
d.total_balance_czk,
|
|
d.planned_net_cost_czk,
|
|
d.planned_balance_czk,
|
|
d.deviation_cost_czk
|
|
from daily_agg d
|
|
order by d.day_local;
|
|
$fn$;
|
|
|
|
comment on function ems.fn_economics_daily_for_window is
|
|
'Denní souhrn ekonomiky [from, to); ceny inline dle fn_effective_buy/sell (R__011), bez PL volání per slot.';
|
|
|
|
create or replace function ems.fn_economics_daily_month(
|
|
p_site_id int,
|
|
p_month_start date,
|
|
p_month_end date
|
|
)
|
|
returns jsonb
|
|
language sql
|
|
stable
|
|
as $fn$
|
|
select jsonb_build_object(
|
|
'has_green_bonus',
|
|
exists (
|
|
select 1
|
|
from ems.asset_pv_array apv
|
|
where apv.site_id = p_site_id
|
|
and apv.green_bonus_czk_kwh is not null
|
|
),
|
|
'days',
|
|
coalesce(
|
|
(
|
|
select jsonb_agg(sub.day_row order by sub.day_local)
|
|
from (
|
|
select
|
|
r.day_local,
|
|
jsonb_build_object(
|
|
'day', r.day_local,
|
|
'interval_count', r.interval_count,
|
|
'import_kwh', r.import_kwh,
|
|
'export_kwh', r.export_kwh,
|
|
'pv_kwh', r.pv_kwh,
|
|
'load_kwh', r.load_kwh,
|
|
'pv_self_consumption_kwh', r.pv_self_consumption_kwh,
|
|
'ev_kwh', r.ev_kwh,
|
|
'hp_kwh', r.hp_kwh,
|
|
'import_cost_czk',
|
|
case when l.site_id is not null then l.import_cost_czk else r.import_cost_czk end,
|
|
'export_revenue_czk',
|
|
case when l.site_id is not null then l.export_revenue_czk else r.export_revenue_czk end,
|
|
'grid_import_cashflow_czk',
|
|
coalesce(l.grid_import_cashflow_czk, r.grid_import_cashflow_czk),
|
|
'grid_export_revenue_czk',
|
|
coalesce(l.grid_export_revenue_czk, r.grid_export_revenue_czk),
|
|
'net_cost_czk',
|
|
case when l.site_id is not null then l.net_cost_czk else r.net_cost_czk end,
|
|
'green_bonus_czk',
|
|
case when l.site_id is not null then l.green_bonus_czk else r.green_bonus_czk end,
|
|
'total_balance_czk',
|
|
case when l.site_id is not null then l.total_balance_czk else r.total_balance_czk end,
|
|
'planned_balance_czk', r.planned_balance_czk,
|
|
'deviation_cost_czk', r.deviation_cost_czk,
|
|
'is_locked', (l.site_id is not null)
|
|
) as day_row
|
|
from ems.fn_economics_daily_for_window(
|
|
p_site_id,
|
|
(p_month_start::timestamp at time zone 'Europe/Prague'),
|
|
(p_month_end::timestamp at time zone 'Europe/Prague')
|
|
) r
|
|
left join ems.audit_day_lock l
|
|
on l.site_id = r.site_id
|
|
and l.day_local = r.day_local
|
|
order by r.day_local
|
|
) sub
|
|
),
|
|
'[]'::jsonb
|
|
)
|
|
);
|
|
$fn$;
|
|
|
|
comment on function ems.fn_economics_daily_month(int, date, date) is
|
|
'Měsíční denní ekonomika + lock merge jako JSON (GET /economics/daily).';
|