job ev_presence_notify + fn_ev_presence_nudge_due (SQL-first rozhodnutí+dedup); asset_vehicle.presence_nudge_enabled default false=inertní (V110). Worktree agent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
225 lines
8.6 KiB
SQL
225 lines
8.6 KiB
SQL
-- Proaktivní notifikace "auto doma + nepíchnuté + levné/přebytek → píchni ho".
|
||
--
|
||
-- Na rozdíl od arrival nudge v telemetry_collector (jen edge příjezd) tohle běží
|
||
-- periodicky (scheduler ~20–30 min) a upozorní, i když auto bylo doma už dřív,
|
||
-- ale je výhodné ho teď píchnout (sell<=0 NEBO velmi levný buy).
|
||
--
|
||
-- SQL-first: rozhodnutí + dedup je v ems.fn_ev_presence_nudge_due(); Python jen IO/Discord.
|
||
-- DEFAULT-OFF: per vozidlo flag asset_vehicle.presence_nudge_enabled (default false) →
|
||
-- funkce nikoho nevrátí, dokud se na vozidle explicitně nezapne. Inertní pro golden gate.
|
||
|
||
-- 1) SoC z presence pozorování (zatím plní jen budoucí telemetrie; NULL = neznámé).
|
||
alter table ems.ev_presence_obs
|
||
add column if not exists battery_level_pct numeric(5, 2);
|
||
|
||
comment on column ems.ev_presence_obs.battery_level_pct is
|
||
'SoC trakční baterie vozidla v % z presence pollu (jen když je auto online a vrací charge_state). NULL = neznámé (auto spí / poloha bez SoC).';
|
||
|
||
-- 2) Per vozidlo: zapnutí proaktivní notifikace + cílový SoC práh pro nudge.
|
||
alter table ems.asset_vehicle
|
||
add column if not exists presence_nudge_enabled boolean not null default false;
|
||
|
||
alter table ems.asset_vehicle
|
||
add column if not exists presence_nudge_soc_tolerance_pct numeric(5, 2) not null default 5;
|
||
|
||
comment on column ems.asset_vehicle.presence_nudge_enabled is
|
||
'Zapne proaktivní Discord notifikaci "auto doma a nepíchnuté + levné/přebytek → píchni ho" (job ev_presence_notify). Default false = inertní.';
|
||
|
||
comment on column ems.asset_vehicle.presence_nudge_soc_tolerance_pct is
|
||
'Tolerance pod cílovým SoC: nudge se pošle jen když známé SoC < (default_target_soc_pct − tato tolerance). Pokud je SoC neznámé (NULL), prahem se neblokuje.';
|
||
|
||
-- 3) Dedup: jedno potvrzení odeslaného nudge na "epizodu" (vozidlo + klíč stavu).
|
||
-- nudge_key = observed_at začátku epizody, kdy auto JE doma a JE odpojené.
|
||
-- Dokud epizoda trvá (stejný start), klíč se nemění → on conflict do nothing tlumí
|
||
-- opakování každých 20–30 min. Po píchnutí / odjezdu epizoda končí; nový příjezd =
|
||
-- nový observed_at = nový klíč = nudge se znovu nabije.
|
||
create table if not exists ems.ev_presence_nudge_sent (
|
||
vehicle_id int not null references ems.asset_vehicle (id),
|
||
nudge_key timestamptz not null,
|
||
sent_at timestamptz not null default now(),
|
||
primary key (vehicle_id, nudge_key)
|
||
);
|
||
|
||
create index if not exists idx_ev_presence_nudge_sent_sent_at
|
||
on ems.ev_presence_nudge_sent (sent_at desc);
|
||
|
||
comment on table ems.ev_presence_nudge_sent is
|
||
'Dedup proaktivních "píchni auto" notifikací: PK vehicle_id + nudge_key (start epizody doma+odpojeno). Jeden nudge na epizodu; po píchnutí/odjezdu se klíč přirozeně změní.';
|
||
|
||
-- 4) Rozhodovací funkce: vrátí vozidla, kde je teď výhodné píchnout, a zapíše dedup.
|
||
-- Podmínky (vše musí platit):
|
||
-- - vozidlo aktivní a presence_nudge_enabled = true,
|
||
-- - poslední pozorování se známou polohou: at_home = true,
|
||
-- - charging_state značí odpojeno (Disconnected / NoPower / null po příjezdu),
|
||
-- - žádná otevřená ev_session (auto reálně není na wallboxu),
|
||
-- - SoC neznámé NEBO SoC < (target − tolerance),
|
||
-- - aktuální 15min slot: efektivní sell <= 0 NEBO efektivní buy <= práh
|
||
-- (levný buy = pod p_cheap_buy_max_czk_kwh; statický práh, žádný kód zařízení).
|
||
-- Dedup: insert do ev_presence_nudge_sent (on conflict do nothing); vrací jen řádky,
|
||
-- pro které insert skutečně proběhl (= ještě neposláno pro tuto epizodu).
|
||
create or replace function ems.fn_ev_presence_nudge_due(
|
||
p_site_id int,
|
||
p_now timestamptz default now(),
|
||
p_cheap_buy_max_czk_kwh numeric default 1.50
|
||
)
|
||
returns table (
|
||
vehicle_id int,
|
||
vehicle_name text,
|
||
site_id int,
|
||
site_code text,
|
||
at_home boolean,
|
||
battery_level_pct numeric,
|
||
target_soc_pct numeric,
|
||
charging_state text,
|
||
effective_buy_price_czk_kwh numeric,
|
||
effective_sell_price_czk_kwh numeric,
|
||
trigger_reason text,
|
||
nudge_key timestamptz
|
||
)
|
||
language sql
|
||
volatile
|
||
as $fn$
|
||
with slot as (
|
||
-- aktuální 15min slot v UTC (zarovnání po Europe/Prague hranicích řeší boundary fn)
|
||
select ems.fn_planning_slot_boundary_prague(0, p_now) as interval_start
|
||
),
|
||
veh as (
|
||
select
|
||
v.id as vehicle_id,
|
||
v.name as vehicle_name,
|
||
v.site_id,
|
||
v.default_target_soc_pct,
|
||
v.presence_nudge_soc_tolerance_pct
|
||
from ems.asset_vehicle v
|
||
where v.site_id = p_site_id
|
||
and v.active = true
|
||
and v.presence_nudge_enabled = true
|
||
),
|
||
last_obs as (
|
||
-- poslední pozorování se ZNÁMOU polohou (at_home not null) per vozidlo
|
||
select distinct on (o.vehicle_id)
|
||
o.vehicle_id,
|
||
o.observed_at,
|
||
o.at_home,
|
||
o.charging_state,
|
||
o.battery_level_pct
|
||
from ems.ev_presence_obs o
|
||
join veh on veh.vehicle_id = o.vehicle_id
|
||
where o.at_home is not null
|
||
order by o.vehicle_id, o.observed_at desc
|
||
),
|
||
episode as (
|
||
-- poslední "zlom" epizody: nejnovější pozorování, kdy auto bylo pryč nebo připojené.
|
||
select
|
||
lo.vehicle_id,
|
||
coalesce(
|
||
(
|
||
select max(o2.observed_at)
|
||
from ems.ev_presence_obs o2
|
||
where o2.vehicle_id = lo.vehicle_id
|
||
and o2.observed_at <= lo.observed_at
|
||
and (
|
||
o2.at_home is distinct from true
|
||
or (
|
||
o2.charging_state is not null
|
||
and lower(o2.charging_state) not in ('disconnected', 'nopower')
|
||
)
|
||
)
|
||
),
|
||
'-infinity'::timestamptz
|
||
) as last_break_at
|
||
from last_obs lo
|
||
),
|
||
episode_start as (
|
||
select
|
||
ep.vehicle_id,
|
||
coalesce(
|
||
(
|
||
select min(o3.observed_at)
|
||
from ems.ev_presence_obs o3
|
||
where o3.vehicle_id = ep.vehicle_id
|
||
and o3.observed_at > ep.last_break_at
|
||
and o3.at_home = true
|
||
),
|
||
-- fallback: žádný explicitní zlom v historii → ber poslední pozorování
|
||
(select observed_at from last_obs lo where lo.vehicle_id = ep.vehicle_id)
|
||
) as nudge_key
|
||
from episode ep
|
||
),
|
||
price as (
|
||
select
|
||
ep.site_id,
|
||
ep.effective_buy_price_czk_kwh,
|
||
ep.effective_sell_price_czk_kwh
|
||
from ems.vw_site_effective_price ep, slot
|
||
where ep.site_id = p_site_id
|
||
and ep.interval_start = slot.interval_start
|
||
),
|
||
due as (
|
||
select
|
||
v.vehicle_id,
|
||
v.vehicle_name,
|
||
v.site_id,
|
||
lo.at_home,
|
||
lo.battery_level_pct,
|
||
v.default_target_soc_pct as target_soc_pct,
|
||
lo.charging_state,
|
||
pr.effective_buy_price_czk_kwh,
|
||
pr.effective_sell_price_czk_kwh,
|
||
es.nudge_key,
|
||
case
|
||
when pr.effective_sell_price_czk_kwh <= 0 then 'NEG_OR_ZERO_SELL'
|
||
else 'CHEAP_BUY'
|
||
end as trigger_reason
|
||
from veh v
|
||
join last_obs lo on lo.vehicle_id = v.vehicle_id
|
||
join episode_start es on es.vehicle_id = v.vehicle_id
|
||
cross join price pr
|
||
where lo.at_home = true
|
||
and (
|
||
lo.charging_state is null
|
||
or lower(lo.charging_state) in ('disconnected', 'nopower')
|
||
)
|
||
and not exists (
|
||
select 1
|
||
from ems.ev_session sess
|
||
where sess.vehicle_id = v.vehicle_id
|
||
and sess.session_end is null
|
||
)
|
||
and (
|
||
lo.battery_level_pct is null
|
||
or lo.battery_level_pct
|
||
< (coalesce(v.default_target_soc_pct, 80) - coalesce(v.presence_nudge_soc_tolerance_pct, 5))
|
||
)
|
||
and (
|
||
pr.effective_sell_price_czk_kwh <= 0
|
||
or pr.effective_buy_price_czk_kwh <= p_cheap_buy_max_czk_kwh
|
||
)
|
||
),
|
||
ins as (
|
||
insert into ems.ev_presence_nudge_sent (vehicle_id, nudge_key)
|
||
select d.vehicle_id, d.nudge_key
|
||
from due d
|
||
on conflict (vehicle_id, nudge_key) do nothing
|
||
returning vehicle_id, nudge_key
|
||
)
|
||
select
|
||
d.vehicle_id,
|
||
d.vehicle_name,
|
||
d.site_id,
|
||
(select s.code from ems.site s where s.id = d.site_id) as site_code,
|
||
d.at_home,
|
||
d.battery_level_pct,
|
||
d.target_soc_pct,
|
||
d.charging_state,
|
||
d.effective_buy_price_czk_kwh,
|
||
d.effective_sell_price_czk_kwh,
|
||
d.trigger_reason,
|
||
d.nudge_key
|
||
from due d
|
||
join ins on ins.vehicle_id = d.vehicle_id and ins.nudge_key = d.nudge_key;
|
||
$fn$;
|
||
|
||
comment on function ems.fn_ev_presence_nudge_due is
|
||
'Proaktivní "píchni auto" notifikace: vozidla doma + odpojená + (SoC neznámé nebo < cíl−tolerance) + (efektivní sell<=0 nebo buy<=práh) v aktuálním 15min slotu. Default-off (asset_vehicle.presence_nudge_enabled). Dedup zápisem do ev_presence_nudge_sent (1 nudge na epizodu doma+odpojeno). Vrací jen nově due řádky pro Discord.';
|