Files
ems/docs/04-modules/ev-charging.md
Dusan Vojacek 8ffe5460f1 fix(planner): živé EV SoC z integrálu power_w — konec phantom 11 kW oken
needed_wh i headroom z live_soc (soc_at_connect + integrál power_w), ne ze
zamrzlého soc_at_connect. energy_delivered_wh se během session nikdy nezapisoval
(→ needed konstantní, plánovač slepý k pokroku), counter energy_kwh (Telto reg 39)
je rozbitý (17.4 kWh nabito → counter 0.18). Nový fn_ev_session_delivered_wh
integruje power_w (dt cap 120 s), clamp 99 %, fallback drží staré chování bez
telemetrie. Ověřeno živě: needed_wh 18750→1329, live_soc 97.9 %.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 20:33:08 +02:00

440 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
### Živé SoC během session (needed_wh, fix 2026-06-14)
`fn_ev_session_planning_json` (R__038) počítá `energy_needed_wh` i `headroom_wh` z
**živého SoC** = `soc_at_connect + dodaná_energie/kapacita`, clamp 99 % (finální taper
ignorujeme) — ne ze zamrzlého `soc_at_connect`. Dodaná energie je **time-weighted integrál
`power_w`** (`ems.fn_ev_session_delivered_wh`, dt cap 120 s), NE counter `energy_kwh`:
ten je na TeltoCharge (Telto reg 39) **rozbitý** — neakumuluje (ověřeno: 17.4 kWh nabito,
counter 0.18). Bez toho byl `energy_delivered_wh` trvale 0 → needed_wh konstantní →
plánovač slepý k pokroku → phantom 11 kW okna i u plného auta. Funguje pro Teslu i Zoe
(power-based, bez API). Pozn.: reg 39 rozbitý ⇒ i EV audit/ekonomika z něj jede naslepo.
---
## 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` (023). Č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 1718h“) 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 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).
### Session se NEvyřazuje při needed_wh=0 (fix 2026-06-13)
Dřív `fn_planning_site_context` vracela `ev_sessions[e] = null`, když
`needed_wh = 0` (auto už nad targetem) **a** oportunismus byl vypnutý/headroom
nulový — a navíc úplně, když `target_deadline is null`. Druhá past byla v
Pythonu: `_ev_session_from_json` zahazovala session bez deadline. Důsledek
incidentu: aktivní plán měl `ev_sessions:0`, ač session běžela; **plánovač
neviděl ~6 kW zátěž auta** a špatně rozvrhl baterii (zbytečný večerní import).
Oprava (R__038 `ems.fn_ev_session_planning_json` + `db_io._ev_session_from_json`):
- Session se vyřadí (`null`) **jen** bez tvrdých dat — neznámá kapacita vozidla
nebo `soc_at_connect_pct` (nelze spočítat Wh). Jinak vždy objekt.
- **`target_deadline` smí být NULL** (žádný tvrdý cíl) — solver_v2 hard
deadline constraint aplikuje jen při `energy_needed_wh > 0`; oportunistická
vrstva běží i bez deadline. Auto nad targetem nebo bez cíle tak zůstává v
plánu jako známá zátěž i s headroomem k případnému levnému doplnění.
- `energy_needed_wh` = 0 bez deadline / cíle; headroom a opportunistic_value
beze změny (coalesce session → vozidlo).
### 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é 400900 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.