fn_pool_schedule_slot: nejlevnější souvislé okno denního runtime budgetu (fn_pool_daily_runtime_min) z vw_site_effective_price + dump-load při sell<=0. fn_pool_control_tick: každých 15 min spočte stav a zařadí POOL_PUMP_ON (jen když existuje signal_route → bezpečné před aktivací). lifespan job pool_control. Shelly přes signal_service, žádné Modbus. Bazál odečet (R__003) se tím stává správným (řízená+plánovaná zátěž). Aktivace provozně: daily_runtime_min=480, schedulable, signal_route. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
128 lines
4.6 KiB
PL/PgSQL
128 lines
4.6 KiB
PL/PgSQL
-- Řízení bazénového čerpadla (Phase 1, bez solveru): denní runtime budget z
|
||
-- fn_pool_daily_runtime_min (teplotní nebo statický fallback) rozvržený do
|
||
-- NEJLEVNĚJŠÍHO souvislého okna dne (efektivní nákupní cena), + dump-load overlay
|
||
-- (záporná/nulová výkupní cena → absorbuj přebytek místo exportu se ztrátou).
|
||
-- Výstup řídí Shelly relé přes signál POOL_PUMP_ON (fn_signal_enqueue_bool →
|
||
-- signal_service). Žádné Modbus. Bazál (R__003) bazén odečítá → s tímto řízením
|
||
-- se odečet stává správným (řízená + plánovaná zátěž).
|
||
|
||
-- Rozhodnutí ON/OFF pro daný 15min slot.
|
||
create or replace function ems.fn_pool_schedule_slot(
|
||
p_pump_id int,
|
||
p_slot_start timestamptz
|
||
)
|
||
returns boolean
|
||
language sql
|
||
stable
|
||
as $fn$
|
||
with cfg as (
|
||
select pp.id, pp.site_id, pp.schedulable,
|
||
greatest(0, coalesce(
|
||
(ems.fn_pool_daily_runtime_min(pp.id) ->> 'runtime_min')::int, 0
|
||
)) as runtime_min
|
||
from ems.asset_pool_pump pp
|
||
where pp.id = p_pump_id
|
||
),
|
||
win as (
|
||
select c.site_id, ceil(c.runtime_min::numeric / 15.0)::int as w
|
||
from cfg c
|
||
),
|
||
-- sloty kalendářního dne slotu (Europe/Prague) s efektivní cenou
|
||
day_slots as (
|
||
select ep.interval_start,
|
||
ep.effective_buy_price_czk_kwh as buy,
|
||
ep.effective_sell_price_czk_kwh as sell,
|
||
row_number() over (order by ep.interval_start) as rn
|
||
from ems.vw_site_effective_price ep
|
||
join cfg c on c.site_id = ep.site_id
|
||
where (ep.interval_start at time zone 'Europe/Prague')::date
|
||
= (p_slot_start at time zone 'Europe/Prague')::date
|
||
),
|
||
-- nejlevnější souvislé okno délky w slotů (self-join, ~96×w řádků = triviální)
|
||
best as (
|
||
select s1.rn as start_rn
|
||
from day_slots s1
|
||
join day_slots s2
|
||
on s2.rn >= s1.rn and s2.rn < s1.rn + (select w from win)
|
||
where (select w from win) > 0
|
||
group by s1.rn
|
||
having count(*) = (select w from win)
|
||
order by sum(s2.buy) asc, s1.rn asc
|
||
limit 1
|
||
)
|
||
select coalesce((select schedulable from cfg), false)
|
||
and coalesce(
|
||
-- v nejlevnějším souvislém okně budgetu
|
||
exists (
|
||
select 1 from day_slots ds
|
||
cross join best b
|
||
where ds.interval_start = p_slot_start
|
||
and ds.rn >= b.start_rn
|
||
and ds.rn < b.start_rn + (select w from win)
|
||
)
|
||
-- NEBO dump-load: záporná/nulová výkupní cena ⇒ raději zkonzumuj než exportuj se ztrátou
|
||
or exists (
|
||
select 1 from day_slots ds
|
||
where ds.interval_start = p_slot_start and ds.sell <= 0
|
||
),
|
||
false
|
||
);
|
||
$fn$;
|
||
|
||
comment on function ems.fn_pool_schedule_slot is
|
||
'Pool ON/OFF pro 15min slot (Phase 1, bez solveru): nejlevnější souvislé okno délky = daily runtime budget (fn_pool_daily_runtime_min) z vw_site_effective_price, NEBO dump-load při sell<=0. false když pump není schedulable / není cena pro den.';
|
||
|
||
-- Control tick: pro každý aktivní řiditelný bazén spočti stav slotu a zařaď signál
|
||
-- POOL_PUMP_ON (idempotentně). Volá control smyčka každých 15 min (hranice slotu).
|
||
-- Enqueue jen když existuje signal_route (jinak bezpečně nic — route se seeduje provozně).
|
||
create or replace function ems.fn_pool_control_tick(
|
||
p_now timestamptz default now()
|
||
)
|
||
returns table(
|
||
pump_id int,
|
||
site_id int,
|
||
desired_on boolean,
|
||
runtime_min int,
|
||
has_route boolean,
|
||
enqueued int
|
||
)
|
||
language plpgsql
|
||
as $fn$
|
||
declare
|
||
v_slot timestamptz;
|
||
r record;
|
||
v_on boolean;
|
||
v_route boolean;
|
||
begin
|
||
v_slot := date_bin(interval '15 minutes', p_now, timestamptz '1970-01-01T00:00:00Z');
|
||
for r in
|
||
select pp.id as pid, pp.site_id as sid,
|
||
greatest(0, coalesce(
|
||
(ems.fn_pool_daily_runtime_min(pp.id) ->> 'runtime_min')::int, 0
|
||
)) as rt
|
||
from ems.asset_pool_pump pp
|
||
join ems.site s on s.id = pp.site_id
|
||
where s.active = true and pp.schedulable = true
|
||
loop
|
||
v_on := coalesce(ems.fn_pool_schedule_slot(r.pid, v_slot), false);
|
||
v_route := exists (
|
||
select 1 from ems.signal_route sr
|
||
where sr.site_id = r.sid and sr.signal_code = 'POOL_PUMP_ON'
|
||
);
|
||
pump_id := r.pid;
|
||
site_id := r.sid;
|
||
desired_on := v_on;
|
||
runtime_min := r.rt;
|
||
has_route := v_route;
|
||
enqueued := case when v_route
|
||
then ems.fn_signal_enqueue_bool(r.sid, 'POOL_PUMP_ON', v_on)
|
||
else 0
|
||
end;
|
||
return next;
|
||
end loop;
|
||
end;
|
||
$fn$;
|
||
|
||
comment on function ems.fn_pool_control_tick is
|
||
'Control tick bazénu (každých 15 min): pro aktivní řiditelné pumpy spočte fn_pool_schedule_slot a zařadí POOL_PUMP_ON (jen když existuje signal_route). Shelly relé pak řídí signal_service. Bez Modbus.';
|