Tesla Fleet API: čtení SoC po příjezdu k wallboxu
- services/tesla_client.py: access token s cache + ROTACE refresh tokenu do ems.tesla_token (env jen seed — Tesla refresh token je jednorázový), vehicles → vehicle_data?endpoints=charge_state, 408 (spící auto) = tiché přeskočení, výběr vozidla dle VIN / jediného na účtu (VIN se auto-naučí) - hook _patch_session_from_tesla v _on_ev_arrival: PŘED replanem doplní soc_at_connect_pct (+ target z charge_limit_soc) do otevřené session přes fn_ev_session_apply_patch (rozšířena o soc_at_connect_pct) — energii si odvodí fn_planning_site_context (SQL-first); selhání neblokuje replan - V086: asset_vehicle.vin, api_type='tesla' pro tesla-my (Model Y, home-01), singleton ems.tesla_token; R__095: fn_tesla_token_get/upsert, fn_tesla_arrival_context, fn_vehicle_set_vin - config: TESLA_CLIENT_ID/SECRET/REFRESH_TOKEN (prázdné = vypnuto) - testy parserů; plná sada beze změny Aktivace: env do /opt/ems-deploy/.env + recreate backendu (docs/tesla-fleet-api.md §Stav). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,14 @@ async def _on_ev_arrival(site_id: int, charger_code: str) -> None:
|
||||
from services.planning_engine import run_rolling_replan
|
||||
|
||||
async with _BG_POOL.acquire() as conn:
|
||||
# Tesla: skutečné SoC do session PŘED replanem (selhání nesmí blokovat plán)
|
||||
try:
|
||||
await _patch_session_from_tesla(site_id, charger_code, conn)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Tesla SoC fetch failed (site=%s, %s) — replan jede na defaultech",
|
||||
site_id, charger_code,
|
||||
)
|
||||
await run_rolling_replan(
|
||||
site_id, conn, triggered_by=f"ev_arrival:{charger_code}"
|
||||
)
|
||||
@@ -46,6 +54,61 @@ async def _on_ev_arrival(site_id: int, charger_code: str) -> None:
|
||||
"EV arrival replan failed (site=%s, charger=%s)", site_id, charger_code
|
||||
)
|
||||
|
||||
|
||||
async def _patch_session_from_tesla(
|
||||
site_id: int, charger_code: str, conn: asyncpg.Connection
|
||||
) -> None:
|
||||
"""Po příjezdu: pro vozidlo s api_type='tesla' doplnit reálné SoC do session.
|
||||
|
||||
Energie se NEpočítá tady — soc_at_connect_pct + target_soc_pct si přebere
|
||||
fn_planning_site_context (SQL-first). VIN se při prvním úspěchu naučí.
|
||||
"""
|
||||
from app.db_json import fetch_json
|
||||
from services.tesla_client import get_charge_state
|
||||
|
||||
ctx = await fetch_json(
|
||||
conn,
|
||||
"select ems.fn_tesla_arrival_context($1::int, $2::text)",
|
||||
site_id,
|
||||
charger_code,
|
||||
)
|
||||
if not isinstance(ctx, dict) or ctx.get("api_type") != "tesla":
|
||||
return
|
||||
session_id = ctx.get("session_id")
|
||||
if session_id is None:
|
||||
logger.warning("Tesla hook: chybí otevřená session (%s)", charger_code)
|
||||
return
|
||||
|
||||
state = await get_charge_state(conn, ctx.get("vin"))
|
||||
if state is None:
|
||||
return
|
||||
|
||||
if not ctx.get("vin") and state.get("vin"):
|
||||
await conn.execute(
|
||||
"select ems.fn_vehicle_set_vin($1::int, $2::text)",
|
||||
int(ctx["vehicle_id"]),
|
||||
str(state["vin"]),
|
||||
)
|
||||
|
||||
patch: dict = {"soc_at_connect_pct": state["battery_level"]}
|
||||
if state.get("charge_limit_soc"):
|
||||
patch["target_soc_pct"] = state["charge_limit_soc"]
|
||||
import json as _json
|
||||
await fetch_json(
|
||||
conn,
|
||||
"select ems.fn_ev_session_apply_patch($1::int, $2::int, $3::jsonb)",
|
||||
site_id,
|
||||
int(session_id),
|
||||
_json.dumps(patch),
|
||||
)
|
||||
logger.info(
|
||||
"Tesla SoC -> session %s: level=%s%%, limit=%s%% (%s)",
|
||||
session_id,
|
||||
state["battery_level"],
|
||||
state.get("charge_limit_soc"),
|
||||
charger_code,
|
||||
)
|
||||
|
||||
# Deye SUN – holding registry (decimal adresa = přímo pro read_holding_registers)
|
||||
DEYE_REG_RUN_STATE = 500
|
||||
DEYE_REG_BATT_CHARGE_TODAY = 514
|
||||
|
||||
Reference in New Issue
Block a user