# 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]) → `fn_ev_session_transition` při příjezdu
(fallback defaulty; 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`.

## 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 − max(target, soc_at_connect)) % kapacity** (fix paradoxu
„nižší target → větší headroom": auto fyzicky bere jen energii nad svým
aktuálním SoC). **Hodnota kWh:** `coalesce(ev_session.opportunistic_value_czk_kwh,
asset_vehicle.opportunistic_value_czk_kwh)` — V099 přidal per-session override
(NULL = zdědit z vozidla, 0 = vypnout pro session ⇒ headroom_wh = 0; patch
klíčem `opportunistic_value_czk_kwh` ve `fn_ev_session_apply_patch`, validace ≥ 0).
Default vozidla 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) —
a **bez session je EV == 0** (stop-session nevypíná jen tvrdý cíl, ale i
oportunismus). Session zůstává v plánu i po dosažení targetu, dokud má headroom;
**oportunistická vrstva není omezená deadline** (auto bývá doma dál, odjezd
řeší rolling replan — rozhodnutí 2026-06-12).

### Min. výkon wallboxu a účtování via-bat (2026-06-12, dev)

- **`asset_ev_charger.min_power_w`** (1380 W = 6 A IEC 61851) jde přes
  `fn_planning_site_context` do solver_v2: binárka `ev_on[e][t]`,
  `setpoint ∈ {0} ∪ [min_power_w, max]` — žádné nevykonatelné 400–900 W.
- **Tvrdý cíl** sčítá jen sloty **před** deadline (slot začínající v deadline
  už nepatří „do deadline" — oprava off-by-one).
- **`ev_direct ≤ gi + PV`** (fyzikální split; via_bat kryje vybíjení baterie).
- **Reporting:** kWh do EV z baterie (via_bat) neplatí slotový buy; solver_v2
  je oceňuje oportunitní cenou v `planning_interval.battery_arbitrage_czk`
  (min sell exportního slotu téhož pražského dne, jinak terminal value) a
  `fn_plan_current_bundle.intervals` nese `ev1/ev2_via_bat_w` pro UI.
