server: replace carconnectivity with volkswagencarnet

Switches the VW API backend from carconnectivity + connector-volkswagen
to volkswagencarnet (local fork with the cariad-hybrid-auth-fix patch
applied), fixing authentication against the current CARIAD BFF endpoints.

- requirements.txt: point at local volkswagencarnet git repo via
  git+file:// URL (editable path no longer needed now the patch is
  committed there)
- we_connect.py: rewrite around the async volkswagencarnet Connection
  class; a persistent asyncio event loop held in _Session lets the rest
  of the code stay synchronous
- data_model.py: remap all extractors to volkswagencarnet Vehicle
  properties and Paths constants; output shape preserved so
  apply_procedural and the GUI client continue to work unchanged
- collect.py: swap carconnectivity error import, drop
  TemporaryAuthenticationError (no equivalent), fix vehicle.vin.value →
  vehicle.vin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 22:50:57 +02:00
co-authored by Claude Sonnet 4.6
parent 63e807bdb0
commit bc424310b5
4 changed files with 223 additions and 148 deletions
+48 -36
View File
@@ -1,52 +1,64 @@
import asyncio
import logging
import sys
try:
from carconnectivity.carconnectivity import CarConnectivity
except ImportError:
print(
"carconnectivity is not installed. Run: "
"pip install carconnectivity carconnectivity-connector-volkswagen",
file=sys.stderr,
)
sys.exit(1)
import aiohttp
from volkswagencarnet.vw_connection import Connection
from volkswagencarnet.vw_exceptions import AuthenticationError # noqa: F401 — re-exported for collect.py
log = logging.getLogger(__name__)
def connect(username: str, password: str) -> CarConnectivity:
config = {
"carConnectivity": {
"connectors": [
{
"type": "volkswagen",
"config": {
"username": username,
"password": password,
},
}
]
}
}
cc = CarConnectivity(config=config)
cc.startup()
cc.fetch_all()
return cc
class _Session:
"""Holds the asyncio event loop, aiohttp session, and Connection together."""
def __init__(
self,
loop: asyncio.AbstractEventLoop,
session: aiohttp.ClientSession,
connection: Connection,
) -> None:
self._loop = loop
self._http_session = session
self.connection = connection
def run(self, coro):
return self._loop.run_until_complete(coro)
@property
def vehicles(self):
return self.connection.vehicles
def select_vehicle(cc: CarConnectivity, vin: str | None):
def connect(username: str, password: str) -> _Session:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
async def _do():
session = aiohttp.ClientSession(headers={"Connection": "keep-alive"})
conn = Connection(session, username, password)
ok = await conn.doLogin()
if not ok:
await session.close()
raise AuthenticationError("Login failed")
return session, conn
session, conn = loop.run_until_complete(_do())
return _Session(loop, session, conn)
def select_vehicle(wc: _Session, vin: str | None):
"""Return (vehicle, error_message). error_message is None on success."""
vehicles = list(cc.get_garage().list_vehicles())
vehicles = wc.vehicles
if not vehicles:
return None, "No vehicles found in this CarConnectivity account"
return None, "No vehicles found in this account"
if vin:
vehicle = next((v for v in vehicles if v.vin.value == vin), None)
vehicle = next((v for v in vehicles if v.vin == vin), None)
if not vehicle:
available = [v.vin.value for v in vehicles]
return None, f"VIN {vin} not found. Available: {available}"
available = [v.vin for v in vehicles]
return None, f"VIN {vin} not found. Available: {available}"
return vehicle, None
return vehicles[0], None
def update(cc: CarConnectivity) -> None:
cc.fetch_all()
def update(wc: _Session) -> None:
wc.run(wc.connection.update())