diff --git a/backend/services/tesla_client.py b/backend/services/tesla_client.py index 600287a..ae5dca2 100644 --- a/backend/services/tesla_client.py +++ b/backend/services/tesla_client.py @@ -47,7 +47,11 @@ def parse_charge_state(vehicle_data: dict[str, Any]) -> dict[str, Any] | None: if level is None: return None odo_miles = vs.get("odometer") + ds = resp.get("drive_state") or {} return { + "latitude": ds.get("latitude"), + "longitude": ds.get("longitude"), + "shift_state": ds.get("shift_state"), "vin": resp.get("vin"), "battery_level": int(level), "charge_limit_soc": int(cs.get("charge_limit_soc") or 0) or None, @@ -91,7 +95,18 @@ async def _get_access_token(db: asyncpg.Connection) -> Optional[str]: "refresh_token": refresh, }, ) - r.raise_for_status() + if r.status_code >= 400: + # 400 invalid_grant = token spálený rotací NEBO ~10min výpadek po + # revokaci souhlasu (Tesla docs). Neshazovat volajícího tracebackem. + body = r.text[:300] + logger.error( + "Tesla token refresh selhal (HTTP %s): %s — pokud jsi právě " + "revokoval souhlas, počkej ~10 min; jinak obnov token dle " + "docs/tesla-fleet-api.md (deploy/tesla/reauth.sh)", + r.status_code, + body, + ) + return None data = r.json() new_access = str(data["access_token"]) @@ -144,7 +159,7 @@ async def get_charge_state( r = await client.get( f"{API_BASE}/api/1/vehicles/{chosen['id']}/vehicle_data", - params={"endpoints": "charge_state;vehicle_state"}, + params={"endpoints": "charge_state;vehicle_state;location_data"}, ) if r.status_code == 408: logger.info("Tesla: vozidlo spí / nedostupné (408) — SoC nedoplněno") diff --git a/deploy/tesla/reauth.sh b/deploy/tesla/reauth.sh new file mode 100755 index 0000000..dc7e35d --- /dev/null +++ b/deploy/tesla/reauth.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Jednorázová obnova Tesla refresh tokenu (rotační past: žádné mezikroky). +# Použití NA SERVERU: +# 1) otevři authorize URL (vypíše ji tento skript bez argumentů) +# 2) ./reauth.sh — výměna → zápis do DB → ověřovací test +# Env: CID, CSEC (client id/secret). Redirect URI: https://ems.vojacek.eu/t-auth +set -euo pipefail + +REDIRECT="https://ems.vojacek.eu/t-auth" +AUTH="https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3" + +: "${CID:?export CID=''}" +: "${CSEC:?export CSEC=''}" + +if [[ $# -lt 1 ]]; then + echo "Otevři v prohlížeči (consent musí ukázat 'Vehicle location'):" + echo "$AUTH/authorize?response_type=code&client_id=$CID&redirect_uri=$REDIRECT&scope=openid%20offline_access%20vehicle_device_data%20vehicle_location&state=ems" + echo + echo "Pak: $0 (do minuty!)" + exit 0 +fi + +CODE="$1" +RESP=$(curl -s "$AUTH/token" \ + -d grant_type=authorization_code \ + --data-urlencode "client_id=$CID" \ + --data-urlencode "client_secret=$CSEC" \ + --data-urlencode "redirect_uri=$REDIRECT" \ + --data-urlencode "code=$CODE") + +RT=$(echo "$RESP" | python3 -c "import json,sys; print(json.load(sys.stdin).get('refresh_token',''))" 2>/dev/null || true) +if [[ -z "$RT" ]]; then + echo "VÝMĚNA SELHALA:"; echo "$RESP"; exit 1 +fi + +echo "refresh token OK (${#RT} znaků) → DB" +docker exec -i ems-deploy-db-1 psql -U ems_user -d ems -c \ + "update ems.tesla_token set refresh_token='$RT', access_token=null, access_expires_at=null, updated_at=now() where id=1;" + +echo "=== ověření ===" +docker exec -i ems-deploy-backend-1 python - <<'PY' +import asyncio, asyncpg, os, httpx +from services.tesla_client import _get_access_token, API_BASE +async def m(): + c = await asyncpg.connect(host=os.environ["DB_HOST"], port=int(os.environ["DB_PORT"]), + user=os.environ["DB_USER"], password=os.environ["DB_PASSWORD"], database="ems") + try: + tok = await _get_access_token(c) + if not tok: + print("REFRESH SELHAL (viz log backendu — po revokaci počkej ~10 min)"); return + h = {"Authorization": f"Bearer {tok}"} + async with httpx.AsyncClient(timeout=15, headers=h) as cl: + r = await cl.get(f"{API_BASE}/api/1/vehicles") + v = (r.json().get("response") or [None])[0] + print("vozidlo:", v and v.get("state")) + if v and v.get("state") == "online": + r2 = await cl.get(f"{API_BASE}/api/1/vehicles/{v['id']}/vehicle_data", + params={"endpoints": "location_data"}) + ds = (r2.json().get("response") or {}).get("drive_state") or {} + print("location scope:", "OK" if ds.get("latitude") is not None + else f"CHYBI (HTTP {r2.status_code})") + else: + print("auto spí — refresh OK, location doověřit po jízdě") + finally: + await c.close() +asyncio.run(m()) +PY