Files
ems/db/routines/R__101_fn_pool_control.sql
Dusan Vojacek f70111f44b feat(pool): řízení bazénu Phase 1 — nejlevnější okno + dump-load (bez solveru)
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>
2026-06-14 22:00:49 +02:00

128 lines
4.6 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.
-- Ří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.';