-- 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.';