# Modul: EV Nabíjení ## Přehled vozidel na home-01 | Vozidlo | Nabíječka | Max výkon | Řízení | API | |---|---|---|---|---| | Tesla | ev-charger-1 (Teltonika 22kW) | 22 kW | WB proud limit + Tesla API | Zatím nerozhodnuto (Tessie nebo přímé) | | Renault Zoe | ev-charger-2 (Teltonika 22kW) | 22 kW (Zoe max 22kW) | WB proud limit (Zoe respektuje) | Žádné – Zoe jako fixní zátěž při připojení | --- ## Klíčové principy ### 1. Přímé FVE nabíjení preferováno před průchodem přes baterii Energie která jde FVE → baterie → EV má round-trip ztráty: ``` η_round_trip = η_charge × η_discharge ≈ 0.95 × 0.95 ≈ 0.90 ``` Přímé napájení FVE → EV (nebo síť → EV) je ~10 % efektivnější. Solver to vidí přes vyšší efektivní cenu energie procházející baterií (degradation_cost + round-trip loss). ### 2. Deadline charging Každé vozidlo může mít nastaven: - **cílový SoC** (%) - **deadline** (do kdy musí být dosažen) Solver garantuje dosažení SoC do deadline jako hard constraint. Ekonomická optimalizace probíhá v rámci tohoto omezení. ### 3. Zoe – řízení přes WB proud limit Zoe respektuje maximální proud nastavený na WB (Teltonika Modbus). Solver nastaví `current_limit_a` pro daný slot. Zoe vždy nabíjí pokud je připojena a proud > 6A. Scheduler v Zoe se nepoužívá – WB proud limit je jediný řídicí prvek. ### 4. Tesla – WB + volitelně Tesla API V první fázi stejný přístup jako Zoe – proud limit přes WB. Tesla API (Tessie nebo přímé) přidáme ve fázi 2 pro: - čtení aktuálního SoC bez dotazování WB - čtení stavu připojení - případné spuštění/zastavení nabíjení přímo v autě --- ## DB rozšíření – EV session a deadline ### Tabulka `ems.ev_session` ```sql CREATE TABLE ems.ev_session ( id SERIAL PRIMARY KEY, site_id INT NOT NULL REFERENCES ems.site(id), charger_id INT NOT NULL REFERENCES ems.asset_ev_charger(id), vehicle_id INT REFERENCES ems.asset_vehicle(id), session_start TIMESTAMPTZ NOT NULL DEFAULT now(), session_end TIMESTAMPTZ, -- Stav při připojení soc_at_connect_pct NUMERIC(5,2), -- Deadline požadavek (nastavuje uživatel nebo API) target_soc_pct NUMERIC(5,2), target_deadline TIMESTAMPTZ, -- Výsledek soc_at_disconnect_pct NUMERIC(5,2), energy_delivered_kwh NUMERIC(10,3), cost_czk NUMERIC(10,4) ); ``` ### Tabulka `ems.asset_vehicle` ```sql CREATE TABLE ems.asset_vehicle ( id SERIAL PRIMARY KEY, site_id INT NOT NULL REFERENCES ems.site(id), code TEXT NOT NULL, name TEXT, make TEXT, -- 'Tesla', 'Renault' model TEXT, -- 'Model Y', 'Zoe' battery_capacity_kwh NUMERIC(6,2), -- Tesla ~58, Zoe ~22 max_charge_power_w INT, -- max přijímaný výkon vozidla default_charger_id INT REFERENCES ems.asset_ev_charger(id), api_type TEXT, -- 'tesla', 'none' api_reference TEXT, -- odkaz na credentials v env default_target_soc_pct NUMERIC(5,2) DEFAULT 80, default_deadline_hour INT DEFAULT 7 -- 7:00 ráno jako výchozí deadline ); ``` --- ## Solver rozšíření – EV s round-trip a deadline ### Nové proměnné pro každý slot t a každé EV e ```python ev_direct[e][t] # W – přímé napájení EV z FVE nebo sítě (bez průchodu baterií) ev_via_bat[e][t] # W – napájení EV přes baterii (vyšší efektivní cena) # Celkový výkon EV (co jde do auta) ev_charge[e][t] = ev_direct[e][t] + ev_via_bat[e][t] # Co ev_via_bat stojí energeticky navíc: # ev_via_bat musí být "nakoupeno" z baterie s round-trip ztrátou # solver to vidí přes účelovou funkci – viz níže ``` ### Energetická bilance rozšířená o přímé EV ```python # Zdroje = Spotřeba pv_a_net[t] + pv_b[t] + grid_import[t] + batt_discharge[t] == load_baseline[t] + Σ_e ev_direct[e][t] # přímá spotřeba EV + Σ_e ev_via_bat[e][t] # EV přes baterii (z discharge) + heat_pump[t] + batt_charge[t] + grid_export[t] # Vazba: ev_via_bat[e][t] musí pokrýt batt_discharge[t] # (solver to vyřeší sám – discharge jde buď do ev_via_bat nebo do load) ``` ### Účelová funkce – efektivní cena EV přes baterii ```python # Nabíjení přes baterii je dražší o round-trip ztrátu a degradaci: EV_VIA_BAT_COST_FACTOR = 1.0 / (charge_efficiency * discharge_efficiency) # ≈ 1.0 / (0.95 * 0.95) ≈ 1.108 # V objective function: + ev_via_bat[e][t] * buy_price[t] * EV_VIA_BAT_COST_FACTOR * H / 1000 + ev_direct[e][t] * buy_price[t] * H / 1000 # přímé – bez navýšení # Solver přirozeně preferuje přímé nabíjení kde je to možné ``` ### Deadline constraint ```python # Pro každé EV e s nastaveným deadline: if ev_session[e].target_deadline is not None: # Kolik energie ještě potřebujeme dodat energy_needed_wh = ( (ev_session[e].target_soc_pct - ev_session[e].current_soc_pct) / 100.0 * vehicle[e].battery_capacity_kwh * 1000 ) # Deadline slot index t_deadline = slot_index_for(ev_session[e].target_deadline) # Hard constraint: součet dodané energie do deadline musí být >= potřebná prob += pulp.lpSum( ev_charge[e][t] * H # Wh za 15min slot for t in range(t_deadline + 1) if ev_connected[e][t] # jen sloty kdy je auto připojeno ) >= energy_needed_wh # Zoe má tvrdší deadline (menší baterie, kritičtější) # Tesla může mít měkčí deadline nebo vyšší flexibility okno ``` ### Připojení EV – vstupní podmínka ```python # ev_connected[e][t] = True/False # Pokud auto není připojeno → ev_charge[e][t] = 0 for t in range(T): if not ev_connected[e][t]: prob += ev_charge[e][t] == 0 prob += ev_direct[e][t] == 0 prob += ev_via_bat[e][t] == 0 ``` --- ## Jak solver rozhoduje (příklady) ### Přebytek FVE přes poledne, Zoe připojena, baterie poloprázdná ``` Solver volí: ev_direct[zoe][t] = max(min(surplus_w, zoe_max_w), 0) ← přímé z FVE batt_charge[t] = zbývající surplus ← do baterie až pak Protože přímé nabíjení Zoe je levnější než FVE → baterie → Zoe. ``` ### Noc, Zoe má deadline 7:00 s SoC 20% (potřeba 30 kWh) ``` Solver: - Rozloží nabíjení do nejlevnějších nočních slotů - Garantuje dodání 30 kWh do 7:00 (hard constraint) - Pokud jsou sloty se zápornou cenou → nabíjí naplno v těch slotech - Vyhýbá se nabíjení přes baterii pokud není přebytek ``` ### Tesla připojena, SoC 70%, deadline není nastaven ``` Solver: - Tesla je "oportunistická" – nabíjí jen při přebytku FVE nebo levné ceně - Bez deadline = měkká optimalizace, ne hard constraint - Nastavit default_target_soc = 80% s default_deadline = zítra 7:00 (konfigurovatelné v asset_vehicle) ``` --- ## Zjištění stavu připojení ### Teltonika WB (oba vozy) Modbus registr stavu konektoru (status): - `available` = žádné auto - `preparing` / `charging` = auto připojeno Polling každou minutu z `telemetry_ev_charger.status`. ### Tesla API (fáze 2) Přes Tessie nebo přímé Tesla API: - SoC baterie auta - Stav připojení (plugged_in) - Nabíjecí stav (charging / stopped) Uložit do `ev_session` při připojení/odpojení. ### Renault Zoe Žádné API. Stav připojení čteme výhradně z WB Modbus (`status != 'available'`). SoC Zoe neznáme přesně – použijeme energii dodanou v session (kumulativní kWh z WB). --- ## Statistika příjezdů ### Tabulka `ems.ev_arrival_stats` Agregace podle `site_id`, `charger_id`, `day_of_week` (0 = neděle … 6 = sobota) a `arrival_hour` (0–23). Čas příjezdu se počítá v **Europe/Prague**. Unikátní klíč `(site_id, charger_id, day_of_week, arrival_hour)`; sloupec `sample_count` roste s každým zaznamenaným příjezdem. Účel: po několika týdnech dat odhadnout typickou hodinu připojení vozidla na danou wallbox — pro notifikace („obvykle přijíždíš kolem 17–18h“) a později jako měkký vstup do plánovače. ### `ems.fn_update_ev_arrival_stats(site_id, charger_id, vehicle_id, arrived_at)` Inkrementuje statistiku pro příslušný bucket (INSERT nebo `ON CONFLICT` +1). Volá se při **detekci nového příjezdu** v `telemetry_collector`: přechod telemetrie z `available` na stav připojení (`preparing`, `charging`, …). ### `ems.fn_ev_expected_arrival(site_id, charger_id, for_date)` Vrátí až **3 řádky**: nejčastější hodiny příjezdu pro den v týdnu odpovídající kalendářnímu datu `for_date` (typicky „zítřek“ v časové zóně lokality z backendu). Filtr `sample_count >= 2`; `confidence_pct` = podíl dané hodiny na součtu vzorků pro stejný `day_of_week` u té nabíječky. ### API `GET /api/v1/sites/{site_id}/ev/arrival-prediction` vrátí pro každou nabíječku (klíč = `asset_ev_charger.code`) pole `tomorrow` s `{ hour, confidence_pct, samples }`. Pokud je na site méně než **5** záznamů v `ev_session` celkem, odpověď má `insufficient_data: true` (predikce se může vracet prázdné nebo řídké). ### Provozní poznámka Historie v `ev_arrival_stats` se **nemaže** — jde o dlouhodobou agregaci. Po **4+ týdnech** reálných příjezdů má smysl UI notifikace a experimentální zapojení do solveru (soft constraint). --- ## Seed data – vozidla home-01 ```sql -- V006__vehicles.sql INSERT INTO ems.asset_vehicle (site_id, code, name, make, model, battery_capacity_kwh, max_charge_power_w, default_charger_id, api_type, default_target_soc_pct, default_deadline_hour) SELECT s.id, 'tesla-my', 'Tesla Model Y', 'Tesla', 'Model Y', 58.0, 11000, -- Tesla Model Y AC max ~11kW ch.id, 'none', -- Tesla API fáze 2 80, 7 FROM ems.site s JOIN ems.asset_ev_charger ch ON ch.site_id = s.id AND ch.code = 'ev-charger-1' WHERE s.code = 'home-01'; INSERT INTO ems.asset_vehicle (site_id, code, name, make, model, battery_capacity_kwh, max_charge_power_w, default_charger_id, api_type, default_target_soc_pct, default_deadline_hour) SELECT s.id, 'zoe-r135', 'Renault Zoe R135', 'Renault', 'Zoe R135', 22.0, 22000, -- Zoe max 22kW AC ch.id, 'none', 90, 7 -- Zoe: vyšší target SoC (menší baterie, kritičtější) FROM ems.site s JOIN ems.asset_ev_charger ch ON ch.site_id = s.id AND ch.code = 'ev-charger-2' WHERE s.code = 'home-01'; ``` --- ## Otevřené body - [ ] Tesla API: Tessie vs přímé API – rozhodnout ve fázi 2 - [ ] Ověřit Zoe max nabíjecí výkon (7.4 kW nebo méně dle podmínek) - [ ] Ověřit round-trip efficiency na reálných datech po prvních týdnech provozu - [ ] UI pro nastavení deadline a target SoC uživatelem (před odjezdem) - [ ] Notifikace pokud deadline nelze splnit (nedostatek kapacity WB nebo energie) - [ ] Zoe SoC estimace z kumulativní energie session – přesnost ověřit --- ## EV spotřební forecast — týdenní rytmus vozidla (2026-06-12) Cíl: target SoC a deadline session z reálného užívání místo fixních defaultů (pondělí služebka ~150 km → skoro plná; konec týdne míň; víkendová session s pondělním deadline → v2 nabije v levných víkendových slotech — vyplyne z cen). **Sběr (bez buzení auta):** při příjezdu/odjezdu (auto vzhůru) Tesla API → `ems.ev_vehicle_obs` (odometer km — API vrací MÍLE, převod v `tesla_client`; SoC). Páry odjezd→příjezd → `ems.ev_trip` (km z odometru, kWh z ΔSoC × kapacita; nabíjení cestou → `charged_away`, kWh se nepočítá). Job 00:50 `fn_update_ev_usage_stats` → `ems.ev_usage_stats` per (vozidlo, DOW): 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]) — 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';` 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: stav baterie auta → cíl (+kWh), deadline, plánovaná nabíjecí okna s ø cenou (`_notify_ev_arrival_plan` v telemetry_collector). Interaktivní fáze B (tlačítka „odjíždím za 2 h" → patch session + replan): `docs/discord-ev-interaction.md`. ## Měkký cíl — oportunistické nabíjení nad target (2026-06-12, dev) Tvrdý cíl (deadline) = „bez tohohle neodjedu"; měkký cíl = „klidně doplň do 100 %, když je energie skoro zadarmo". Implementace: dekompozice Σ(EV energie) == needed − unmet + opp; `opp ∈ [0, headroom]` (headroom = (100 − target) % kapacity, jen když `asset_vehicle. opportunistic_value_czk_kwh > 0`; default 1 Kč/kWh, 0 = vypnuto). Hodnota = ušetřené BUDOUCÍ nabíjení (auto neumí zpět — žádný noční prodej), proto nízká → uplatní se při záporných cenách / plné domácí baterce (lepší než curtail), běžné ceny ji nezaplatí. Víkendový vzor „pátek nemusím do plna, víkend doplní zadarmo" z toho plyne sám. Dekompozice zároveň stropuje celkovou energii do auta (dřív při buy<0 chyběl strop). Session zůstává v plánu i po dosažení targetu, dokud má headroom.