EV spotřební forecast: týdenní rytmus vozidla → target SoC a deadline session
Myšlenka uživatele: pondělní služebka ~150 km (~35 kWh) chce skoro plnou, konec týdne stačí míň, víkend = levné sloty na přípravu pondělka. - V089: ev_vehicle_obs (odometer+SoC při příjezdu/ODJEZDU — auto v obou okamžicích vzhůru, žádné buzení navíc), ev_trip (km z odometru, kWh z ΔSoC; nabíjení cestou → charged_away flag), ev_usage_stats per (vozidlo, DOW); asset_vehicle: target_soc_forecast_enabled (default false), min_target_soc_pct - R__096: fn_ev_build_trips (párování), fn_update_ev_usage_stats (job 00:50), fn_ev_next_departure (příští typický odjezd, >=4 vzorky, >=3 km), fn_ev_required_soc (P80 spotřeby dne + 10 p.b., clamp [min_target, 100]) - R__016: session při příjezdu bere forecast target+deadline (za per-vozidlo flagem, fallback defaulty, ruční patch vždy vyhrává) → víkendová session s pondělním deadline = v2 solver přirozeně nabije v levných slotech - tesla_client: + vehicle_state endpoint (odometer v MÍLÍCH → km), collector: departure hook, lifespan: job 00:50 Aktivace po nasbírání dat: update asset_vehicle set target_soc_forecast_enabled=true. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
205
db/routines/R__096_fn_ev_usage.sql
Normal file
205
db/routines/R__096_fn_ev_usage.sql
Normal file
@@ -0,0 +1,205 @@
|
||||
-- EV spotřební forecast (SQL-first): vložení pozorování, párování jízd,
|
||||
-- statistiky per DOW, odvození target SoC a očekávaného odjezdu.
|
||||
|
||||
create or replace function ems.fn_ev_vehicle_obs_insert(
|
||||
p_site_id int,
|
||||
p_vehicle_id int,
|
||||
p_trigger text,
|
||||
p_odometer_km numeric,
|
||||
p_soc_pct numeric,
|
||||
p_charging_state text
|
||||
)
|
||||
returns bigint
|
||||
language sql
|
||||
as $fn$
|
||||
insert into ems.ev_vehicle_obs (
|
||||
site_id, vehicle_id, trigger, odometer_km, soc_pct, charging_state
|
||||
)
|
||||
values (
|
||||
p_site_id, p_vehicle_id, p_trigger, p_odometer_km, p_soc_pct, p_charging_state
|
||||
)
|
||||
returning id;
|
||||
$fn$;
|
||||
|
||||
-- Spáruje odjezdy s nejbližším následujícím příjezdem téhož vozidla.
|
||||
create or replace function ems.fn_ev_build_trips()
|
||||
returns int
|
||||
language plpgsql
|
||||
as $fn$
|
||||
declare
|
||||
v_count int := 0;
|
||||
r record;
|
||||
v_arr record;
|
||||
begin
|
||||
for r in
|
||||
select o.*
|
||||
from ems.ev_vehicle_obs o
|
||||
where o.trigger = 'departure'
|
||||
and o.odometer_km is not null
|
||||
and not exists (
|
||||
select 1 from ems.ev_trip t where t.departure_obs_id = o.id
|
||||
)
|
||||
order by o.vehicle_id, o.observed_at
|
||||
loop
|
||||
select a.* into v_arr
|
||||
from ems.ev_vehicle_obs a
|
||||
where a.vehicle_id = r.vehicle_id
|
||||
and a.trigger = 'arrival'
|
||||
and a.observed_at > r.observed_at
|
||||
and a.odometer_km is not null
|
||||
order by a.observed_at
|
||||
limit 1;
|
||||
|
||||
if v_arr.id is null then
|
||||
continue; -- jízda ještě neskončila
|
||||
end if;
|
||||
|
||||
insert into ems.ev_trip (
|
||||
vehicle_id, departure_obs_id, arrival_obs_id,
|
||||
departed_at, arrived_at, km, kwh_est, charged_away
|
||||
)
|
||||
select
|
||||
r.vehicle_id, r.id, v_arr.id,
|
||||
r.observed_at, v_arr.observed_at,
|
||||
greatest(0, v_arr.odometer_km - r.odometer_km),
|
||||
case
|
||||
when r.soc_pct is null or v_arr.soc_pct is null then null
|
||||
when v_arr.soc_pct > r.soc_pct then null -- nabíjeno cestou
|
||||
else round(((r.soc_pct - v_arr.soc_pct) / 100.0
|
||||
* av.battery_capacity_kwh)::numeric, 2)
|
||||
end,
|
||||
coalesce(v_arr.soc_pct > r.soc_pct, false)
|
||||
from ems.asset_vehicle av
|
||||
where av.id = r.vehicle_id
|
||||
on conflict (departure_obs_id) do nothing;
|
||||
|
||||
v_count := v_count + 1;
|
||||
end loop;
|
||||
return v_count;
|
||||
end;
|
||||
$fn$;
|
||||
|
||||
-- Přepočet týdenního rytmu z jízd za lookback okno (plný přepočet, ne EMA —
|
||||
-- rebuild-friendly; jízdy s nabíjením cestou se počítají do km, ne do kWh).
|
||||
create or replace function ems.fn_update_ev_usage_stats(
|
||||
p_lookback_days int default 60
|
||||
)
|
||||
returns int
|
||||
language plpgsql
|
||||
as $fn$
|
||||
declare
|
||||
v_built int;
|
||||
v_count int;
|
||||
begin
|
||||
v_built := ems.fn_ev_build_trips();
|
||||
|
||||
with daily as (
|
||||
select
|
||||
t.vehicle_id,
|
||||
(t.departed_at at time zone 'Europe/Prague')::date as d,
|
||||
extract(dow from t.departed_at at time zone 'Europe/Prague')::int as dow,
|
||||
sum(t.kwh_est) filter (where not t.charged_away) as day_kwh,
|
||||
sum(t.km) as day_km,
|
||||
min(extract(hour from t.departed_at at time zone 'Europe/Prague')
|
||||
+ extract(minute from t.departed_at at time zone 'Europe/Prague') / 60.0
|
||||
) as first_departure_hour
|
||||
from ems.ev_trip t
|
||||
where t.departed_at >= now() - make_interval(days => p_lookback_days)
|
||||
and t.km >= 1.0 -- přepojení kabelu bez jízdy nepočítat
|
||||
group by 1, 2, 3
|
||||
),
|
||||
agg as (
|
||||
select
|
||||
vehicle_id, dow,
|
||||
avg(day_kwh) as avg_kwh,
|
||||
stddev(day_kwh) as sd_kwh,
|
||||
avg(day_km) as avg_km,
|
||||
avg(first_departure_hour) as avg_dep,
|
||||
count(*) as samples
|
||||
from daily
|
||||
group by 1, 2
|
||||
)
|
||||
insert into ems.ev_usage_stats (
|
||||
vehicle_id, day_of_week, avg_day_kwh, stddev_day_kwh,
|
||||
avg_day_km, avg_departure_hour, sample_count, last_updated
|
||||
)
|
||||
select
|
||||
vehicle_id, dow, round(avg_kwh::numeric, 2), round(coalesce(sd_kwh, 0)::numeric, 2),
|
||||
round(avg_km::numeric, 1), round(avg_dep::numeric, 2), samples, now()
|
||||
from agg
|
||||
on conflict (vehicle_id, day_of_week) do update set
|
||||
avg_day_kwh = excluded.avg_day_kwh,
|
||||
stddev_day_kwh = excluded.stddev_day_kwh,
|
||||
avg_day_km = excluded.avg_day_km,
|
||||
avg_departure_hour = excluded.avg_departure_hour,
|
||||
sample_count = excluded.sample_count,
|
||||
last_updated = now();
|
||||
|
||||
get diagnostics v_count = row_count;
|
||||
return v_count;
|
||||
end;
|
||||
$fn$;
|
||||
|
||||
comment on function ems.fn_update_ev_usage_stats is
|
||||
'Spáruje nové jízdy (fn_ev_build_trips) a přepočte ev_usage_stats za lookback okno. Job 00:50.';
|
||||
|
||||
-- Příští očekávaný den s jízdou (>= 3 km průměru a >= 4 vzorky) v horizontu 7 dní.
|
||||
create or replace function ems.fn_ev_next_departure(
|
||||
p_vehicle_id int,
|
||||
p_from timestamptz default now()
|
||||
)
|
||||
returns timestamptz
|
||||
language sql
|
||||
stable
|
||||
as $fn$
|
||||
select min(dep)
|
||||
from (
|
||||
select (
|
||||
((p_from at time zone 'Europe/Prague')::date + offs)::timestamp
|
||||
+ make_interval(mins => (round(s.avg_departure_hour * 60))::int)
|
||||
) at time zone 'Europe/Prague' as dep
|
||||
from generate_series(0, 7) offs
|
||||
join ems.ev_usage_stats s
|
||||
on s.vehicle_id = p_vehicle_id
|
||||
and s.day_of_week = extract(
|
||||
dow from (p_from at time zone 'Europe/Prague')::date + offs
|
||||
)::int
|
||||
where s.sample_count >= 4
|
||||
and s.avg_day_km >= 3.0
|
||||
) cand
|
||||
where cand.dep > p_from + interval '30 minutes';
|
||||
$fn$;
|
||||
|
||||
comment on function ems.fn_ev_next_departure is
|
||||
'Nejbližší typický odjezd vozidla dle ev_usage_stats (DOW s >=4 vzorky a >=3 km). NULL = málo dat → volající použije default_deadline_hour.';
|
||||
|
||||
-- Target SoC pro odjezd v daný den: P80 denní spotřeby + rezerva 10 p.b.,
|
||||
-- clamp do [min_target_soc_pct, 100]. NULL = málo dat.
|
||||
create or replace function ems.fn_ev_required_soc(
|
||||
p_vehicle_id int,
|
||||
p_departure_at timestamptz
|
||||
)
|
||||
returns numeric
|
||||
language sql
|
||||
stable
|
||||
as $fn$
|
||||
select least(100.0, greatest(
|
||||
av.min_target_soc_pct,
|
||||
ceil(
|
||||
(s.avg_day_kwh + 0.8416 * s.stddev_day_kwh) -- ~P80
|
||||
/ nullif(av.battery_capacity_kwh, 0) * 100.0
|
||||
) + 10.0
|
||||
))
|
||||
from ems.asset_vehicle av
|
||||
join ems.ev_usage_stats s
|
||||
on s.vehicle_id = av.id
|
||||
and s.day_of_week = extract(
|
||||
dow from p_departure_at at time zone 'Europe/Prague'
|
||||
)::int
|
||||
where av.id = p_vehicle_id
|
||||
and s.sample_count >= 4
|
||||
and s.avg_day_kwh is not null;
|
||||
$fn$;
|
||||
|
||||
comment on function ems.fn_ev_required_soc is
|
||||
'Cílové SoC (%) pro odjezd: P80 spotřeby dne v týdnu + 10 p.b. rezerva, mez [min_target_soc_pct, 100]. NULL = málo dat → default_target_soc_pct.';
|
||||
Reference in New Issue
Block a user