Merge branch 'worktree-agent-a53f3277d55fecfcb' into dev
All checks were successful
CI and deploy / migration-check (push) Successful in 19s
CI and deploy / deploy (push) Successful in 1m14s

This commit is contained in:
Dusan Vojacek
2026-06-12 19:40:50 +02:00
12 changed files with 425 additions and 57 deletions

View File

@@ -0,0 +1,16 @@
-- Per-session override oportunistického EV nabíjení (V094 zavedl hodnotu
-- na asset_vehicle). NULL = zdědit z vozidla; 0 = oportunismus pro tuto
-- session vypnut („nenabíjet nad target"); > 0 = vlastní ocenění kWh.
-- Efektivní hodnota se skládá v ems.fn_planning_site_context
-- (coalesce(session, vehicle)); patch přes ems.fn_ev_session_apply_patch.
alter table ems.ev_session
add column if not exists opportunistic_value_czk_kwh numeric(6, 3) null;
comment on column ems.ev_session.opportunistic_value_czk_kwh is
'Per-session override hodnoty kWh nabité NAD target (Kč/kWh). NULL = zdědit z asset_vehicle.opportunistic_value_czk_kwh; 0 = oportunistické nabíjení pro tuto session vypnuto (headroom_wh = 0 v plánovacím kontextu).';
-- v2 reporting: battery_arbitrage_czk nese oportunitní hodnotu kWh z baterie
-- do EV (via_bat × oportunitní cena), ne konstantní 0 / v1 marži exportu.
comment on column ems.planning_interval.battery_arbitrage_czk is
'Ekonomika baterie mimo slotový cashflow (Kč). v1: marže exportu baterie ge_bat × (sell acquisition) × h. v2: oportunitní cena EV energie z baterie — ev_via_bat × (nejnižší sell exportního slotu téhož pražského dne, jinak terminal value); slotový buy pro tyto kWh neplatí.';

View File

@@ -11,10 +11,19 @@ declare
begin
if not (p_patch ? 'target_soc_pct')
and not (p_patch ? 'target_deadline')
and not (p_patch ? 'soc_at_connect_pct') then
and not (p_patch ? 'soc_at_connect_pct')
and not (p_patch ? 'opportunistic_value_czk_kwh') then
return jsonb_build_object('success', false, 'error', 'no_fields');
end if;
if (p_patch ? 'opportunistic_value_czk_kwh')
and jsonb_typeof(p_patch->'opportunistic_value_czk_kwh') <> 'null'
and (p_patch->>'opportunistic_value_czk_kwh')::numeric < 0 then
return jsonb_build_object(
'success', false, 'error', 'opportunistic_value_negative'
);
end if;
update ems.ev_session es
set
target_soc_pct = case
@@ -44,6 +53,16 @@ begin
else (p_patch->>'target_deadline')::timestamptz
end
else es.target_deadline
end,
-- NULL = zdědit z asset_vehicle; 0 = oportunismus pro session vypnut
opportunistic_value_czk_kwh = case
when p_patch ? 'opportunistic_value_czk_kwh' then
case
when p_patch->'opportunistic_value_czk_kwh' is null
or jsonb_typeof(p_patch->'opportunistic_value_czk_kwh') = 'null' then null
else (p_patch->>'opportunistic_value_czk_kwh')::numeric
end
else es.opportunistic_value_czk_kwh
end
where es.id = p_session_id
and es.site_id = p_site_id
@@ -57,5 +76,5 @@ begin
end;
$fn$;
comment on function ems.fn_ev_session_apply_patch(int, int, jsonb) is
'PATCH EV session jen klíče přítomné v JSON (ISO string pro deadline; soc_at_connect_pct z Tesla API).';
comment on function ems.fn_ev_session_apply_patch is
'PATCH EV session jen klíče přítomné v JSON (ISO string pro deadline; soc_at_connect_pct z Tesla API; opportunistic_value_czk_kwh >= 0, NULL = zdědit z vozidla, 0 = vypnout).';

View File

@@ -85,6 +85,8 @@ begin
'deye_gen_cutoff_enabled', pi.deye_gen_cutoff_enabled,
'ev1_setpoint_w', pi.ev1_setpoint_w,
'ev2_setpoint_w', pi.ev2_setpoint_w,
'ev1_via_bat_w', pi.ev1_via_bat_w,
'ev2_via_bat_w', pi.ev2_via_bat_w,
'heat_pump_enabled', pi.heat_pump_enabled,
'pv_a_curtailed_w', pi.pv_a_curtailed_w,
'expected_cost_czk', pi.expected_cost_czk,
@@ -123,6 +125,8 @@ begin
'deye_gen_cutoff_enabled', null,
'ev1_setpoint_w', null,
'ev2_setpoint_w', null,
'ev1_via_bat_w', null,
'ev2_via_bat_w', null,
'heat_pump_enabled', null,
'pv_a_curtailed_w', null,
'expected_cost_czk', null,
@@ -248,5 +252,5 @@ begin
end;
$fn$;
comment on function ems.fn_plan_current_bundle(int) is
'Aktivní planning_run + intervaly + souhrn (GET /plan/current). PV za horizont plánu z canonical forecast; delta profil z cache.';
comment on function ems.fn_plan_current_bundle is
'Aktivní planning_run + intervaly + souhrn (GET /plan/current). PV za horizont plánu z canonical forecast; delta profil z cache; intervals nesou ev1/ev2_via_bat_w (EV energie z baterie — UI nemá cenit slotovým buy).';

View File

@@ -165,6 +165,7 @@ begin
jsonb_agg(
jsonb_build_object(
'max_charge_power_w', v.max_charge_power_w,
'min_power_w', coalesce(ch.min_power_w, 0),
'battery_capacity_kwh', v.battery_capacity_kwh,
'default_target_soc_pct', v.default_target_soc_pct
)
@@ -193,8 +194,11 @@ begin
- coalesce(es.energy_delivered_wh, 0)::numeric
) <= 0
and (
coalesce(v.opportunistic_value_czk_kwh, 0) <= 0
or (100 - coalesce(es.target_soc_pct, v.default_target_soc_pct)) <= 0
coalesce(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0) <= 0
or (100 - greatest(
coalesce(es.target_soc_pct, v.default_target_soc_pct)::numeric,
es.soc_at_connect_pct::numeric
)) <= 0
) then null::jsonb
else jsonb_build_object(
'target_deadline', es.target_deadline,
@@ -205,15 +209,20 @@ begin
* (v.battery_capacity_kwh * 1000)
- coalesce(es.energy_delivered_wh, 0)::numeric
),
-- headroom od max(target, SoC při připojení): „nenabíjet" (nízký
-- target) nesmí paradoxně ZVĚTŠIT oportunistickou vrstvu; auto může
-- fyzicky vzít jen energii nad svým aktuálním SoC.
'headroom_wh', case
when coalesce(v.opportunistic_value_czk_kwh, 0) > 0 then greatest(
when coalesce(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0) > 0 then greatest(
0,
(100 - coalesce(es.target_soc_pct, v.default_target_soc_pct))::numeric
/ 100.0 * (v.battery_capacity_kwh * 1000)
(100 - greatest(
coalesce(es.target_soc_pct, v.default_target_soc_pct)::numeric,
es.soc_at_connect_pct::numeric
)) / 100.0 * (v.battery_capacity_kwh * 1000)
)
else 0
end,
'opportunistic_value_czk_kwh', coalesce(v.opportunistic_value_czk_kwh, 0)
'opportunistic_value_czk_kwh', coalesce(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0)
)
end
from ems.ev_session es
@@ -238,8 +247,11 @@ begin
- coalesce(es.energy_delivered_wh, 0)::numeric
) <= 0
and (
coalesce(v.opportunistic_value_czk_kwh, 0) <= 0
or (100 - coalesce(es.target_soc_pct, v.default_target_soc_pct)) <= 0
coalesce(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0) <= 0
or (100 - greatest(
coalesce(es.target_soc_pct, v.default_target_soc_pct)::numeric,
es.soc_at_connect_pct::numeric
)) <= 0
) then null::jsonb
else jsonb_build_object(
'target_deadline', es.target_deadline,
@@ -250,15 +262,20 @@ begin
* (v.battery_capacity_kwh * 1000)
- coalesce(es.energy_delivered_wh, 0)::numeric
),
-- headroom od max(target, SoC při připojení): „nenabíjet" (nízký
-- target) nesmí paradoxně ZVĚTŠIT oportunistickou vrstvu; auto může
-- fyzicky vzít jen energii nad svým aktuálním SoC.
'headroom_wh', case
when coalesce(v.opportunistic_value_czk_kwh, 0) > 0 then greatest(
when coalesce(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0) > 0 then greatest(
0,
(100 - coalesce(es.target_soc_pct, v.default_target_soc_pct))::numeric
/ 100.0 * (v.battery_capacity_kwh * 1000)
(100 - greatest(
coalesce(es.target_soc_pct, v.default_target_soc_pct)::numeric,
es.soc_at_connect_pct::numeric
)) / 100.0 * (v.battery_capacity_kwh * 1000)
)
else 0
end,
'opportunistic_value_czk_kwh', coalesce(v.opportunistic_value_czk_kwh, 0)
'opportunistic_value_czk_kwh', coalesce(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0)
)
end
from ems.ev_session es
@@ -333,5 +350,5 @@ begin
end;
$fn$;
comment on function ems.fn_planning_site_context(int) is
'Kontext pro planning_engine / LP (bez samotného solveru).';
comment on function ems.fn_planning_site_context is
'Kontext pro planning_engine / LP (bez samotného solveru). EV session: opportunistic_value = coalesce(session, vehicle); headroom_wh od max(target, soc_at_connect), 0 při vypnutém oportunismu; vehicles nesou min_power_w wallboxu.';