feat(ev): event-driven replan i na odjezd EV (ne jen příjezd)
All checks were successful
CI and deploy / migration-check (push) Successful in 7m17s
CI and deploy / deploy (push) Has been skipped

Odjezd auta (≠available → available) teď spouští okamžitý rolling replan
+ export, symetricky k příjezdu — místo čekání na */15 tick. Řeší stale
plán spočítaný těsně před odjezdem, který držel fantomovou EV alokaci
(~4–11 kW do už odjetého auta). Session už zavřela fn_ev_session_transition
synchronně v poll smyčce, takže replan vidí 'žádná session' a alokaci shodí.

Replan i pozorování jízdy každý ve vlastním try+conn (pád solveru ani spící
auto se navzájem neshodí). +2 regresní testy, +docs (changelog, ev-charging).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dusan Vojacek
2026-06-17 15:38:13 +02:00
parent ce30dbd4a4
commit ab8ddf1fdf
4 changed files with 129 additions and 3 deletions

View File

@@ -129,13 +129,38 @@ async def _on_ev_arrival(site_id: int, charger_code: str) -> None:
async def _on_ev_departure(site_id: int, charger_code: str) -> None:
"""Odjezd: zapsat pozorování (odometer+SoC) — auto právě jede, je vzhůru.
"""Odjezd: okamžitý replan + export, pak zapsat pozorování jízdy.
Pár odjezd→příjezd dává jízdu (km, kWh) pro ev_usage_stats. Spící/nečitelné
auto (408) = tiché přeskočení, jízda se dopočítá z příštích pozorování.
Session už zavřela fn_ev_session_transition v poll smyčce (synchronně, PŘED
tímto fire-and-forget taskem), takže replan uvidí 'žádná session' a shodí
fantomovou EV alokaci místo čekání na */15 tick — symetricky k _on_ev_arrival.
Přebytek pak rovnou exportuje, ne aby ho plán virtuálně cpal už odjetému autu.
Pozorování (odometer+SoC): pár odjezd→příjezd dává jízdu (km, kWh) pro
ev_usage_stats. Spící/nečitelné auto (408) = tiché přeskočení; je best-effort
a NESMÍ blokovat ani shodit replan výše (proto vlastní conn + try).
"""
if _BG_POOL is None:
return
# 1) Okamžitý replan + export (kritické — uvolnit přebytek z fantomové EV alokace).
try:
from services.control_exporter import export_setpoints
from services.planning_engine import run_rolling_replan
async with _BG_POOL.acquire() as conn:
await run_rolling_replan(
site_id, conn, triggered_by=f"ev_departure:{charger_code}"
)
await export_setpoints(site_id, conn)
logger.info(
"EV departure replan+export done (site=%s, charger=%s)",
site_id, charger_code,
)
except Exception:
logger.exception(
"EV departure replan failed (site=%s, charger=%s)", site_id, charger_code
)
# 2) Pozorování jízdy (jen Tesla; nezávislé na replanu výše).
try:
from app.db_json import fetch_json
from services.tesla_client import get_charge_state