needed_wh=0 když live_soc >= least(target,99) - charge_done_tolerance_pct (V107, default 3 p.b.). Effective target zastropovaný na 99 (clamp) → bez věčného mini-dobíjení a cyklování nabíječky. Ověřeno živě: session #6 needed_wh 1329→0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
19 KiB
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
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
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
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
# 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
# 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
# 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
# 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é autopreparing/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.
Tolerance „dost dobré" (V107): energy_needed_wh = 0 když
live_soc >= least(target, 99) − asset_vehicle.charge_done_tolerance_pct (default
3 p.b.). Effective target je zastropovaný na 99 (= clamp live_soc), takže se nehoní
poslední taper k 100 % (jinak věčné mini-dobíjení → cyklování nabíječky / Tesla
notifikace). charge_done_tolerance_pct = 0 → tvrdě na target.
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
-- 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:
- weekly — nejbližší budoucí výskyt enabled řádku
ev_weekly_requirementdo 48 h od příjezdu (deadline = dendowvdeadline_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. - forecast —
fn_ev_next_departure+fn_ev_required_soc, jen přiasset_vehicle.target_soc_forecast_enabled(chování V089 beze změny). - default —
default_target_soc_pct; deadline = příští výskytdefault_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 nebosoc_at_connect_pct(nelze spočítat Wh). Jinak vždy objekt. target_deadlinesmí být NULL (žádný tvrdý cíl) — solver_v2 hard deadline constraint aplikuje jen přienergy_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řesfn_planning_site_contextdo solver_v2: binárkaev_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) afn_plan_current_bundle.intervalsneseev1/ev2_via_bat_wpro UI.