fix(planner): živé EV SoC z integrálu power_w — konec phantom 11 kW oken
needed_wh i headroom z live_soc (soc_at_connect + integrál power_w), ne ze zamrzlého soc_at_connect. energy_delivered_wh se během session nikdy nezapisoval (→ needed konstantní, plánovač slepý k pokroku), counter energy_kwh (Telto reg 39) je rozbitý (17.4 kWh nabito → counter 0.18). Nový fn_ev_session_delivered_wh integruje power_w (dt cap 120 s), clamp 99 %, fallback drží staré chování bez telemetrie. Ověřeno živě: needed_wh 18750→1329, live_soc 97.9 %. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,12 +8,53 @@
|
||||
-- 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(
|
||||
@@ -24,53 +65,74 @@ 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,
|
||||
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 v.battery_capacity_kwh is null then null::jsonb
|
||||
when es.soc_at_connect_pct is null then null::jsonb
|
||||
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(es.target_soc_pct, v.default_target_soc_pct) is null then null
|
||||
else es.target_deadline
|
||||
when coalesce(c.target_soc_pct, c.default_target_soc_pct) is null then null
|
||||
else c.target_deadline
|
||||
end,
|
||||
'energy_needed_wh', case
|
||||
when es.target_deadline is null then 0::numeric
|
||||
when coalesce(es.target_soc_pct, v.default_target_soc_pct) is null then 0::numeric
|
||||
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
|
||||
else greatest(
|
||||
0,
|
||||
(coalesce(es.target_soc_pct, v.default_target_soc_pct)::numeric
|
||||
- es.soc_at_connect_pct::numeric) / 100.0
|
||||
* (v.battery_capacity_kwh * 1000)
|
||||
- coalesce(es.energy_delivered_wh, 0)::numeric
|
||||
(coalesce(c.target_soc_pct, c.default_target_soc_pct)::numeric
|
||||
- c.live_soc_pct) / 100.0
|
||||
* (c.battery_capacity_kwh * 1000)
|
||||
)
|
||||
end,
|
||||
-- headroom do 100 % od max(target, SoC při připojení): „nenabíjet" (nízký
|
||||
-- target) nesmí ZVĚTŠIT oportunistickou vrstvu; auto fyzicky bere jen
|
||||
-- energii nad svým aktuálním SoC. Při vypnutém oportunismu (value <= 0)
|
||||
-- headroom = 0 — session zůstane v plánu, ale solver ji nebude doplňovat.
|
||||
-- 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(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0) > 0 then greatest(
|
||||
when coalesce(c.opportunistic_value_czk_kwh, c.v_opp, 0) > 0 then greatest(
|
||||
0,
|
||||
(100 - greatest(
|
||||
coalesce(es.target_soc_pct, v.default_target_soc_pct, es.soc_at_connect_pct)::numeric,
|
||||
es.soc_at_connect_pct::numeric
|
||||
)) / 100.0 * (v.battery_capacity_kwh * 1000)
|
||||
(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(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0)
|
||||
coalesce(c.opportunistic_value_czk_kwh, c.v_opp, 0)
|
||||
)
|
||||
end
|
||||
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;
|
||||
from c;
|
||||
$fn$;
|
||||
|
||||
comment on function ems.fn_ev_session_planning_json is
|
||||
'EV session objekt pro LP (fn_planning_site_context). Session se NEvyřazuje při needed_wh=0 (auto nad targetem) — zůstává v plánu kvůli oportunistickému headroomu i jako známá zátěž. Null jen bez použitelných dat (kapacita / soc_at_connect). target_deadline smí být NULL (bez tvrdého cíle).';
|
||||
'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.';
|
||||
|
||||
Reference in New Issue
Block a user