V098: týdenní požadavky EV (ev_weekly_requirement) + fn_ev_session_defaults

Tabulka ems.ev_weekly_requirement (dow 0=pondělí..6, target_soc_pct,
deadline_hour Europe/Prague, enabled; unique per vozidlo+den) se seedem
tesla-my pondělí 07:00 → 90 %. Nová ems.fn_ev_session_defaults(vehicle,
arrival) → jsonb {target_soc_pct, deadline, source}: kaskáda týdenní
požadavek (výskyt do 48 h) → forecast z ev_usage_stats
(target_soc_forecast_enabled, chování V089 beze změny) → defaulty vozidla
(deadline = příští výskyt default_deadline_hour). fn_ev_session_transition
ji volá při založení session (SQL-first, Python beze změny); comment
funkce sjednocen na styl bez parametrů.

Docs: ev-charging.md sekce Týdenní požadavky + kaskáda, CLAUDE.md seznam fn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dusan Vojacek
2026-06-12 19:14:40 +02:00
parent e41840cb7d
commit 60eda46dd7
5 changed files with 182 additions and 29 deletions

View File

@@ -161,7 +161,7 @@ Projekt je **SQL-first**: doménová logika, agregace, joiny mezi tabulkami a st
| `signal_state` | Poslední požadovaná / odeslaná / ověřená hodnota na cíli (idempotence). |
| `cutoff_switch_log` | Log přepnutí cut-off přepínačů (mikroinvertory); edge trigger, důvod a cena. |
**View / funkce (nejsou tabulky):** `vw_site_effective_price`, `vw_site_directory`, `vw_modbus_last_verified`, `vw_asset_inverter_modbus_poll`, `vw_asset_ev_charger_modbus_poll`, `vw_asset_heat_pump_modbus_poll`, `vw_latest_telemetry`, `vw_telemetry_hourly_7d`, `vw_telemetry_15m_7d` (15min agregát pro dashboard sloty; repeatable `R__071_vw_telemetry_15m_7d.sql`), `vw_audit_summary`, `vw_operating_mode`, `vw_forecast_accuracy_by_lead_time`, `vw_forecast_accuracy_daily`; `fn_effective_price`, `fn_green_bonus_revenue`, `fn_cop_estimate`, `fn_fill_audit_interval`, `fn_fill_forecast_accuracy`, `fn_delete_forecast_pv_prague_calendar_day`, `fn_pv_forecast_sync_reference_days`, `fn_set_mode`, `fn_expire_modes` (vrací řádky přepnutí pro Discord), `fn_restore_previous_mode`, `fn_update_ev_arrival_stats`, `fn_ev_expected_arrival`, `fn_update_baseline_stats`, `fn_rebuild_consumption_baseline_stats`, `fn_get_baseline_forecast`, `fn_update_market_price_stats`, `fn_update_tuv_usage_stats`, `fn_get_predicted_price`, dále read-modely: `fn_site_configuration`, `fn_site_full_status`, `fn_site_notifications_context`, `fn_plan_current_bundle`, `fn_planning_run_horizon`, `fn_planning_future_price_days`, `fn_economics_daily_month`, `fn_economics_monthly_chart`, `fn_economics_lock_day`, `fn_economics_unlock_day`, `fn_energy_flows_daily_month`, `fn_energy_flows_intervals_day`, `fn_forecast_pv_split`, `fn_ev_sessions_active`, `fn_ev_session_apply_patch`, `fn_ev_arrival_prediction_bundle`, `fn_ev_session_transition`, `fn_negative_price_predictions`, `fn_latest_ote_day_stats`, `fn_ote_day_slot_stats_prague`, `fn_ote_list_missing_days`, `fn_site_effective_prices_day_prague`, `fn_modbus_journal_list`, `fn_modbus_written_command_ids`, `fn_modbus_commands_by_ids`, `fn_inverter_modbus_caps_patch`, `fn_set_mode_with_context`, `fn_fill_audit_for_site_window`, plánování: `fn_load_planning_slots_full`, `fn_last_effective_ote`, `fn_planning_horizon_end`, `fn_planning_site_context`, `fn_pv_forecast_correction_factor`, `fn_planning_run_commit`, `fn_planning_slot_boundary_prague`, `fn_planning_interval_at_offset`, `fn_telemetry_inverter_sample`, `fn_telemetry_ev_charger_sample`, `fn_telemetry_heat_pump_sample`, `fn_battery_cycle_audit`, Deye helpery: `fn_deye_pack_system_time`, `fn_deye_clock_drift_sec`, `fn_deye_time_point_regs`, `fn_deye_tou_inactive_signature`, `fn_modbus_last_verified_map`, `fn_inverter_pv_a_max_w`, `fn_site_has_active_green_bonus_pv`.
**View / funkce (nejsou tabulky):** `vw_site_effective_price`, `vw_site_directory`, `vw_modbus_last_verified`, `vw_asset_inverter_modbus_poll`, `vw_asset_ev_charger_modbus_poll`, `vw_asset_heat_pump_modbus_poll`, `vw_latest_telemetry`, `vw_telemetry_hourly_7d`, `vw_telemetry_15m_7d` (15min agregát pro dashboard sloty; repeatable `R__071_vw_telemetry_15m_7d.sql`), `vw_audit_summary`, `vw_operating_mode`, `vw_forecast_accuracy_by_lead_time`, `vw_forecast_accuracy_daily`; `fn_effective_price`, `fn_green_bonus_revenue`, `fn_cop_estimate`, `fn_fill_audit_interval`, `fn_fill_forecast_accuracy`, `fn_delete_forecast_pv_prague_calendar_day`, `fn_pv_forecast_sync_reference_days`, `fn_set_mode`, `fn_expire_modes` (vrací řádky přepnutí pro Discord), `fn_restore_previous_mode`, `fn_update_ev_arrival_stats`, `fn_ev_expected_arrival`, `fn_update_baseline_stats`, `fn_rebuild_consumption_baseline_stats`, `fn_get_baseline_forecast`, `fn_update_market_price_stats`, `fn_update_tuv_usage_stats`, `fn_get_predicted_price`, dále read-modely: `fn_site_configuration`, `fn_site_full_status`, `fn_site_notifications_context`, `fn_plan_current_bundle`, `fn_planning_run_horizon`, `fn_planning_future_price_days`, `fn_economics_daily_month`, `fn_economics_monthly_chart`, `fn_economics_lock_day`, `fn_economics_unlock_day`, `fn_energy_flows_daily_month`, `fn_energy_flows_intervals_day`, `fn_forecast_pv_split`, `fn_ev_sessions_active`, `fn_ev_session_apply_patch`, `fn_ev_arrival_prediction_bundle`, `fn_ev_session_transition`, `fn_ev_session_defaults` (kaskáda ev_weekly_requirement → forecast → defaulty), `fn_negative_price_predictions`, `fn_latest_ote_day_stats`, `fn_ote_day_slot_stats_prague`, `fn_ote_list_missing_days`, `fn_site_effective_prices_day_prague`, `fn_modbus_journal_list`, `fn_modbus_written_command_ids`, `fn_modbus_commands_by_ids`, `fn_inverter_modbus_caps_patch`, `fn_set_mode_with_context`, `fn_fill_audit_for_site_window`, plánování: `fn_load_planning_slots_full`, `fn_last_effective_ote`, `fn_planning_horizon_end`, `fn_planning_site_context`, `fn_pv_forecast_correction_factor`, `fn_planning_run_commit`, `fn_planning_slot_boundary_prague`, `fn_planning_interval_at_offset`, `fn_telemetry_inverter_sample`, `fn_telemetry_ev_charger_sample`, `fn_telemetry_heat_pump_sample`, `fn_battery_cycle_audit`, Deye helpery: `fn_deye_pack_system_time`, `fn_deye_clock_drift_sec`, `fn_deye_time_point_regs`, `fn_deye_tou_inactive_signature`, `fn_modbus_last_verified_map`, `fn_inverter_pv_a_max_w`, `fn_site_has_active_green_bonus_pv`.
---

View File

@@ -0,0 +1,36 @@
-- Týdenní požadavky na EV: explicitní cíl (target SoC) + deadline pro den
-- v týdnu (0 = pondělí .. 6 = neděle; čas Europe/Prague). Při příjezdu vozidla
-- je čte ems.fn_ev_session_defaults (R__099): nejbližší budoucí výskyt do 48 h
-- má přednost před forecastem z ev_usage_stats i před defaulty vozidla.
-- Ruční přepis (Discord výběry / UI → fn_ev_session_apply_patch) vždy vyhrává.
create table ems.ev_weekly_requirement (
id serial primary key,
vehicle_id int not null references ems.asset_vehicle (id),
dow int not null check (dow between 0 and 6),
target_soc_pct numeric(5, 2) not null check (target_soc_pct between 0 and 100),
deadline_hour int not null check (deadline_hour between 0 and 23),
enabled boolean not null default true,
created_at timestamptz not null default now(),
unique (vehicle_id, dow)
);
comment on table ems.ev_weekly_requirement is
'Týdenní požadavek na nabití vozidla: v den dow (0 = pondělí .. 6 = neděle) v deadline_hour (Europe/Prague) má mít vozidlo target_soc_pct. Vstup ems.fn_ev_session_defaults při zakládání ev_session (přednost před forecastem i defaulty).';
comment on column ems.ev_weekly_requirement.vehicle_id is
'Vozidlo (ems.asset_vehicle), max. 1 řádek na den v týdnu.';
comment on column ems.ev_weekly_requirement.dow is
'Den v týdnu DEADLINE: 0 = pondělí .. 6 = neděle (ISO pořadí, POZOR: ne postgres extract(dow) ani ev_usage_stats, kde 0 = neděle).';
comment on column ems.ev_weekly_requirement.target_soc_pct is
'Cílový SoC vozidla (%) v okamžiku deadline.';
comment on column ems.ev_weekly_requirement.deadline_hour is
'Hodina deadline v Europe/Prague (7 = 07:00 daného dne dow).';
comment on column ems.ev_weekly_requirement.enabled is
'false = řádek se při výběru defaultů ignoruje (požadavek dočasně vypnut bez smazání).';
-- Seed: Tesla Model Y (home-01) — pondělí 07:00 nabitá na 90 % (služebka).
insert into ems.ev_weekly_requirement (vehicle_id, dow, target_soc_pct, deadline_hour)
select av.id, 0, 90.0, 7
from ems.asset_vehicle av
join ems.site s on s.id = av.site_id
where s.code = 'home-01' and av.code = 'tesla-my';

View File

@@ -45,23 +45,14 @@ begin
ac.id,
av.id,
now(),
-- forecast z týdenního rytmu (ev_usage_stats), fallback defaulty;
-- ruční přepis přes fn_ev_session_apply_patch vždy vyhrává.
coalesce(fc.required_soc, av.default_target_soc_pct),
coalesce(
fc.expected_departure,
case
when av.default_deadline_hour is not null then
(
(timezone('Europe/Prague', now()))::date + interval '1 day'
+ make_interval(hours => av.default_deadline_hour)
)::timestamp at time zone 'Europe/Prague'
end
)
-- kaskáda fn_ev_session_defaults: týdenní požadavek (ev_weekly_requirement)
-- → forecast (ev_usage_stats) → defaulty vozidla; ruční přepis přes
-- fn_ev_session_apply_patch vždy vyhrává.
(d.defaults ->> 'target_soc_pct')::double precision,
(d.defaults ->> 'deadline')::timestamptz
from ems.asset_ev_charger ac
left join lateral (
select v.id, v.default_target_soc_pct, v.default_deadline_hour,
v.target_soc_forecast_enabled
select v.id
from ems.asset_vehicle v
where v.default_charger_id = ac.id
and v.site_id = ac.site_id
@@ -69,15 +60,9 @@ begin
order by v.id
limit 1
) av on true
left join lateral (
select dep.expected_departure,
ems.fn_ev_required_soc(av.id, dep.expected_departure) as required_soc
from (
select ems.fn_ev_next_departure(av.id, now()) as expected_departure
) dep
where av.target_soc_forecast_enabled
and dep.expected_departure is not null
) fc on true
cross join lateral (
select ems.fn_ev_session_defaults(av.id, now()) as defaults
) d
where ac.id = p_charger_id
and ac.site_id = p_site_id
on conflict (charger_id) where session_end is null do nothing;
@@ -98,5 +83,5 @@ begin
end;
$fn$;
comment on function ems.fn_ev_session_transition(int, int, text, text, timestamptz) is
'Detekce příjezdu/odjezdu EV po změně statusu nabíječky (telemetry_collector).';
comment on function ems.fn_ev_session_transition is
'Detekce příjezdu/odjezdu EV po změně statusu nabíječky (telemetry_collector); defaulty nové session z ems.fn_ev_session_defaults.';

View File

@@ -0,0 +1,103 @@
-- Defaulty nové ev_session pro vozidlo: kaskáda
-- 1) ems.ev_weekly_requirement — nejbližší budoucí výskyt enabled řádku
-- do 48 h od příjezdu (deadline_hour v den dow, Europe/Prague),
-- 2) forecast z týdenního rytmu (V089: fn_ev_next_departure +
-- fn_ev_required_soc), jen při asset_vehicle.target_soc_forecast_enabled,
-- 3) defaulty vozidla (default_target_soc_pct; deadline = příští výskyt
-- default_deadline_hour v Europe/Prague).
-- Volá fn_ev_session_transition při založení session; ruční přepis přes
-- fn_ev_session_apply_patch (Discord / UI) vždy vyhrává.
create or replace function ems.fn_ev_session_defaults(
p_vehicle_id int,
p_arrival timestamptz
)
returns jsonb
language plpgsql
stable
as $fn$
declare
v_vehicle record;
v_weekly record;
v_forecast_departure timestamptz;
v_deadline timestamptz;
begin
select av.default_target_soc_pct, av.default_deadline_hour,
av.target_soc_forecast_enabled
into v_vehicle
from ems.asset_vehicle av
where av.id = p_vehicle_id;
if not found then
return jsonb_build_object(
'target_soc_pct', null, 'deadline', null, 'source', 'none'
);
end if;
-- 1) týdenní požadavek: nejbližší budoucí výskyt do 48 h (Europe/Prague)
select wr.target_soc_pct, occ.deadline
into v_weekly
from generate_series(0, 2) as offs
cross join lateral (
select ((p_arrival at time zone 'Europe/Prague')::date + offs) as d
) day
join ems.ev_weekly_requirement wr
on wr.vehicle_id = p_vehicle_id
and wr.enabled
and wr.dow = extract(isodow from day.d)::int - 1
cross join lateral (
select (day.d::timestamp + make_interval(hours => wr.deadline_hour))
at time zone 'Europe/Prague' as deadline
) occ
where occ.deadline > p_arrival
and occ.deadline <= p_arrival + interval '48 hours'
order by occ.deadline
limit 1;
if v_weekly.deadline is not null then
return jsonb_build_object(
'target_soc_pct', v_weekly.target_soc_pct,
'deadline', v_weekly.deadline,
'source', 'weekly'
);
end if;
-- 2) forecast z týdenního rytmu (chování shodné s dřívějším
-- fn_ev_session_transition: deadline = typický odjezd; target P80,
-- při málo datech default target)
if v_vehicle.target_soc_forecast_enabled then
v_forecast_departure := ems.fn_ev_next_departure(p_vehicle_id, p_arrival);
if v_forecast_departure is not null then
return jsonb_build_object(
'target_soc_pct', coalesce(
ems.fn_ev_required_soc(p_vehicle_id, v_forecast_departure),
v_vehicle.default_target_soc_pct
),
'deadline', v_forecast_departure,
'source', 'forecast'
);
end if;
end if;
-- 3) defaulty vozidla: deadline = příští výskyt default_deadline_hour
v_deadline := (
(p_arrival at time zone 'Europe/Prague')::date::timestamp
+ make_interval(hours => v_vehicle.default_deadline_hour)
) at time zone 'Europe/Prague';
if v_deadline <= p_arrival then
v_deadline := (
((p_arrival at time zone 'Europe/Prague')::date + 1)::timestamp
+ make_interval(hours => v_vehicle.default_deadline_hour)
) at time zone 'Europe/Prague';
end if;
return jsonb_build_object(
'target_soc_pct', v_vehicle.default_target_soc_pct,
'deadline', v_deadline,
'source', 'default'
);
end;
$fn$;
comment on function ems.fn_ev_session_defaults is
'Target SoC + deadline pro novou ev_session: jsonb {target_soc_pct, deadline, source}. Kaskáda ev_weekly_requirement (výskyt do 48 h, Europe/Prague) → forecast (target_soc_forecast_enabled) → defaulty vozidla (deadline = příští výskyt default_deadline_hour). p_vehicle_id null/neznámé → null hodnoty.';

View File

@@ -327,8 +327,9 @@ avg/stddev kWh, km, hodina prvního odjezdu.
**Použití:** `fn_ev_next_departure` (příští typický odjezd: DOW s ≥4 vzorky
a ≥3 km) + `fn_ev_required_soc` (P80 spotřeby dne + 10 p.b., clamp
[`min_target_soc_pct`, 100]) `fn_ev_session_transition` při příjezdu
(fallback defaulty; ruční patch `fn_ev_session_apply_patch` vždy vyhrává).
[`min_target_soc_pct`, 100]) — od V098 zapojeno jako 2. stupeň kaskády
`fn_ev_session_defaults` (viz níže); ruční patch `fn_ev_session_apply_patch`
vždy vyhrává.
**Aktivace per vozidlo** (po ~měsíci dat):
`update ems.asset_vehicle set target_soc_forecast_enabled = true where code = 'tesla-my';`
@@ -336,6 +337,34 @@ a ≥3 km) + `fn_ev_required_soc` (P80 spotřeby dne + 10 p.b., clamp
Tesla napojení (SoC při příjezdu → `soc_at_connect_pct`): `docs/tesla-fleet-api.md`.
Registry wallboxu: `docs/04-modules/modbus-registers-teltocharge.md`.
## Týdenní požadavky + fn_ev_session_defaults (2026-06-12)
Explicitní týdenní rytmus „v pondělí v 7:00 chci 90 %" bez čekání na
naučený forecast: tabulka **`ems.ev_weekly_requirement`** (V098) —
max 1 řádek na (vozidlo, den): `dow` (**0 = pondělí .. 6 = neděle**, ISO
pořadí — POZOR, jiné než postgres `extract(dow)` v `ev_usage_stats`),
`target_soc_pct`, `deadline_hour` (Europe/Prague), `enabled`.
Seed: tesla-my (home-01) pondělí 07:00 → 90 %.
Defaulty nové session dává **`ems.fn_ev_session_defaults(vehicle_id,
arrival)`** (R__099) → jsonb `{target_soc_pct, deadline, source}`, kaskáda:
1. **weekly** — nejbližší budoucí výskyt enabled řádku
`ev_weekly_requirement` do **48 h** od příjezdu (deadline = den `dow`
v `deadline_hour`, Europe/Prague). Páteční příjezd tedy pondělní
požadavek NEvyzvedne (>48 h) — nedělní večer už ano; dřívější nabití
na pondělí zajistí levné víkendové sloty samy (v2 + oportunismus),
explicitně jde vybrat „pondělí ráno 7:00" v Discordu.
2. **forecast**`fn_ev_next_departure` + `fn_ev_required_soc`, jen při
`asset_vehicle.target_soc_forecast_enabled` (chování V089 beze změny).
3. **default**`default_target_soc_pct`; deadline = příští výskyt
`default_deadline_hour` (Europe/Prague; dnešní, pokud je ještě před ní).
Volá ji `fn_ev_session_transition` při založení session (SQL-first; Python
nic nepřepočítává). Ruční přepis (Discord selecty / UI →
`fn_ev_session_apply_patch`) má vždy přednost — defaulty se aplikují jen
při vzniku session.
## Discord notifikace po příjezdu (2026-06-12, dev)
Po detekci příjezdu + Tesla SoC + replanu odejde na site webhook souhrn: