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>
148 lines
6.7 KiB
SQL
148 lines
6.7 KiB
SQL
-- 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.';
|