Files
ems/db/routines/R__038_fn_ev_session_planning_json.sql
Dusan Vojacek a9a6a88a88 fix(planner): EV tolerance 'dost dobré' — konec honění posledních % do 100 %
needed_wh=0 když live_soc >= least(target,99) - charge_done_tolerance_pct (V107,
default 3 p.b.). Effective target zastropovaný na 99 (clamp) → bez věčného
mini-dobíjení a cyklování nabíječky. Ověřeno živě: session #6 needed_wh 1329→0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:23:38 +02:00

148 lines
6.7 KiB
SQL
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.
-- jeden EV session objekt pro LP (fn_planning_site_context).
-- Vrací jsonb objekt session na daném wallboxu, nebo null::jsonb pokud session
-- není nebo nemá použitelná data (kapacita vozidla, SoC při připojení).
--
-- KLÍČOVÝ ROZDÍL oproti dřívější inline logice (bug 2026-06-13): session se
-- NEVYŘAZUJE jen proto, že needed_wh = 0 (auto už nad targetem). Plánovač pak
-- neviděl ~6 kW zátěž auta a špatně rozvrhl baterii. Session zůstává v plánu,
-- dokud má oportunistický headroom (cena rozhodne, jestli se nabíjí) — měkký
-- cíl řeší solver dekompozicí Σ == needed unmet + opp.
--
-- ŽIVÉ SoC (fix 2026-06-14, phantom okna): needed_wh i headroom se počítají z
-- ŽIVÉHO SoC = soc_at_connect + integrovaná dodaná energie (fn_ev_session_delivered_wh),
-- ne ze zamrzlého soc_at_connect. Dřív se odečítalo es.energy_delivered_wh, JENŽE
-- ten sloupec se během session NIKDY nezapisoval (trvale 0) → needed_wh konstantní
-- → plánovač slepý k pokroku nabíjení → 11 kW phantom okna i u plného auta.
-- NEpoužíváme energy_kwh counter (Telto reg 39 na TeltoCharge neakumuluje —
-- ověřeno: 17.4 kWh nabito, counter stál na 0.18 kWh), proto integrál power_w.
-- live_soc clamp 99 % (finální taper k 100 % ignorujeme). Fallback na
-- energy_delivered_wh drží staré fixtures bez telemetrie identické.
--
-- Vyřazení (null) jen když chybí tvrdá data:
-- - žádná otevřená session na wallboxu, nebo
-- - neznámá kapacita vozidla / SoC při připojení (nelze spočítat Wh).
-- target_deadline SMÍ být NULL (žádný tvrdý cíl) — solver to zvládá
-- (deadline constraint se aplikuje jen při needed_wh > 0).
-- Dodaná energie do auta za session = time-weighted integrál power_w z
-- telemetry_ev_charger (1min). dt cap 120 s ať výpadek telemetrie nezkresluje.
-- Wh (AC, bez korekce na AC→DC ztráty — mírně optimistické = méně phantom,
-- žádoucí směr). Vrací 0 bez telemetrie (drží staré chování).
drop function if exists ems.fn_ev_session_delivered_wh;
create or replace function ems.fn_ev_session_delivered_wh(
p_charger_id int,
p_since timestamptz
)
returns numeric
language sql
stable
as $fn$
select coalesce(sum(
power_w * least(coalesce(dt, 60), 120)
) / 3600.0, 0)::numeric
from (
select power_w,
extract(epoch from (
measured_at - lag(measured_at) over (order by measured_at)
)) as dt
from ems.telemetry_ev_charger
where charger_id = p_charger_id
and measured_at >= p_since
) q;
$fn$;
comment on function ems.fn_ev_session_delivered_wh is
'Dodaná energie do EV za session (Wh, AC) = time-weighted integrál power_w z telemetry_ev_charger (dt cap 120 s). NEpoužívá energy_kwh counter (Telto reg 39 neakumuluje). Vstup živého SoC ve fn_ev_session_planning_json. 0 bez telemetrie.';
drop function if exists ems.fn_ev_session_planning_json;
create or replace function ems.fn_ev_session_planning_json(
p_site_id int,
p_charger_code text
)
returns jsonb
language sql
stable
as $fn$
with s as (
select
es.soc_at_connect_pct,
es.target_soc_pct,
es.target_deadline,
es.energy_delivered_wh,
es.opportunistic_value_czk_kwh,
v.battery_capacity_kwh,
v.default_target_soc_pct,
v.opportunistic_value_czk_kwh as v_opp,
coalesce(v.charge_done_tolerance_pct, 3.0) as charge_done_tolerance_pct,
ems.fn_ev_session_delivered_wh(es.charger_id, es.session_start) as live_delivered_wh
from ems.ev_session es
join ems.asset_ev_charger ch on ch.id = es.charger_id
left join ems.asset_vehicle v on v.id = es.vehicle_id
where es.site_id = p_site_id
and es.session_end is null
and ch.code = p_charger_code
limit 1
),
c as (
select s.*,
-- živé SoC: SoC při připojení + integrovaná dodaná energie, clamp 99 %.
-- coalesce(live, energy_delivered_wh, 0): bez telemetrie = staré chování.
least(99.0, s.soc_at_connect_pct::numeric
+ coalesce(s.live_delivered_wh, s.energy_delivered_wh, 0)::numeric
/ (s.battery_capacity_kwh * 1000) * 100.0) as live_soc_pct
from s
)
select case
when c.battery_capacity_kwh is null then null::jsonb
when c.soc_at_connect_pct is null then null::jsonb
else jsonb_build_object(
-- tvrdý cíl: jen pokud je nastaven deadline I cílový SoC (jinak null →
-- solver hard constraint vynechá, energy_needed_wh = 0).
'target_deadline', case
when coalesce(c.target_soc_pct, c.default_target_soc_pct) is null then null
else c.target_deadline
end,
-- effective target zastropovaný na 99 (clamp live_soc) → bez věčného
-- mini-dobíjení u plného auta. „Dost dobré" tolerance: needed=0 když je
-- live_soc ve vzdálenosti tolerance od targetu (nehonit poslední taper →
-- žádné zbytečné start/stop nabíječky). 0 = tvrdě na target.
'energy_needed_wh', case
when c.target_deadline is null then 0::numeric
when coalesce(c.target_soc_pct, c.default_target_soc_pct) is null then 0::numeric
when c.live_soc_pct >=
least(coalesce(c.target_soc_pct, c.default_target_soc_pct)::numeric, 99)
- c.charge_done_tolerance_pct
then 0::numeric
else greatest(
0,
(least(coalesce(c.target_soc_pct, c.default_target_soc_pct)::numeric, 99)
- c.live_soc_pct) / 100.0
* (c.battery_capacity_kwh * 1000)
)
end,
-- headroom do 99 % od max(target, ŽIVÉ SoC): „nenabíjet" (nízký target)
-- nesmí ZVĚTŠIT oportunistickou vrstvu; auto fyzicky bere jen energii nad
-- aktuálním SoC. Plné auto (live_soc → 99) → headroom 0. Při vypnutém
-- oportunismu (value <= 0) headroom = 0.
'headroom_wh', case
when coalesce(c.opportunistic_value_czk_kwh, c.v_opp, 0) > 0 then greatest(
0,
(99 - greatest(
coalesce(c.target_soc_pct, c.default_target_soc_pct, c.live_soc_pct)::numeric,
c.live_soc_pct
)) / 100.0 * (c.battery_capacity_kwh * 1000)
)
else 0
end,
'opportunistic_value_czk_kwh',
coalesce(c.opportunistic_value_czk_kwh, c.v_opp, 0)
)
end
from c;
$fn$;
comment on function ems.fn_ev_session_planning_json is
'EV session objekt pro LP (fn_planning_site_context). needed_wh i headroom z ŽIVÉHO SoC = soc_at_connect + fn_ev_session_delivered_wh (integrál power_w), clamp 99 % — ne ze zamrzlého soc_at_connect (energy_delivered_wh se nikdy nezapisoval → phantom 11 kW okna). Session se NEvyřazuje při needed_wh=0 (zůstává jako známá zátěž + oportunistický headroom). Null jen bez použitelných dat (kapacita / soc_at_connect). target_deadline smí být NULL.';