diff --git a/collect.py b/collect.py index 4d28476..a633c15 100644 --- a/collect.py +++ b/collect.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 """ -WeConnect vehicle data collector. +CarConnectivity vehicle data collector. -Periodically fetches selected domains from a VW WeConnect vehicle and -appends timestamped records to a JSON file. The JSON structure mirrors -the WeConnect domain hierarchy: domain → status-object → field. +Periodically fetches selected domains from a VW vehicle via CarConnectivity +and appends timestamped records to a JSON file. The JSON structure mirrors +the domain hierarchy: domain → status-object → field. Usage examples -------------- @@ -53,7 +53,7 @@ from datetime import datetime, timezone from pathlib import Path from server import log_config, network, we_connect -from weconnect.errors import AuthentificationError +from carconnectivity.errors import AuthenticationError, TemporaryAuthenticationError from server.data_model import ALL_DOMAINS, collect_snapshot, apply_procedural from server.storage_helpers import auto_out_path, load_last_24h_records, load_store, save_store from utils.jay_diff import jay_diff_full @@ -74,12 +74,12 @@ def run(args: argparse.Namespace) -> None: sys.exit(1) if args.dry: - log.info("Dry-run mode — skipping WeConnect login") + log.info("Dry-run mode — skipping CarConnectivity login") wc = None vehicle = None vin = args.vin or "UNKNOWN" else: - log.info("Connecting to WeConnect…") + log.info("Connecting via CarConnectivity…") wc = we_connect.connect(args.username, args.password) vehicle, err = we_connect.select_vehicle(wc, args.vin) if err: @@ -132,7 +132,7 @@ def run(args: argparse.Namespace) -> None: log.info("Midnight UTC rotation → %s", out) try: - wc.update() + we_connect.update(wc) snapshot = collect_snapshot(vehicle, domains) apply_procedural(snapshot) store["records"].append(snapshot) @@ -146,12 +146,8 @@ def run(args: argparse.Namespace) -> None: log.info("Record #%d saved at %s", len(store["records"]), snapshot["ts"]) except KeyboardInterrupt: raise - except AuthentificationError: - log.warning("Authentication error — re-logging in before next interval") - try: - wc.login() - except Exception: - log.exception("Re-login failed") + except (AuthenticationError, TemporaryAuthenticationError): + log.warning("Authentication error — will retry at next interval") except Exception: log.exception("Collection failed — will retry at next interval") @@ -180,12 +176,12 @@ def main() -> None: creds.add_argument( "-u", "--username", default=os.environ.get("WC_USER"), - help="WeConnect / MyVolkswagen email (overrides credentials file)", + help="MyVolkswagen email (overrides credentials file)", ) creds.add_argument( "-p", "--password", default=os.environ.get("WC_PASS"), - help="WeConnect password (overrides credentials file)", + help="MyVolkswagen password (overrides credentials file)", ) p.add_argument( @@ -239,7 +235,7 @@ def main() -> None: p.add_argument( "--dry", action="store_true", - help="Skip WeConnect login and data collection; run push server only (for GUI testing)", + help="Skip CarConnectivity login and data collection; run push server only (for GUI testing)", ) p.add_argument( "--list-domains", diff --git a/requirements.txt b/requirements.txt index f6b403b..b1d5538 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -weconnect +carconnectivity +carconnectivity-connector-volkswagen PyQt5 -pyqtgraph +pyqtgraph \ No newline at end of file diff --git a/server/data_model.py b/server/data_model.py index d2e750f..6472792 100644 --- a/server/data_model.py +++ b/server/data_model.py @@ -6,60 +6,73 @@ log = logging.getLogger(__name__) # ── value helpers ───────────────────────────────────────────────────────────── -def _str(v) -> str: - return str(v.value) if hasattr(v, "value") else str(v) +def _str(attr) -> str: + """Return the string value of a CarConnectivity attribute.""" + return str(attr) def _phys(value, unit: str) -> dict: return {"value": value, "unit": unit} +def _remaining_min(estimated_date_reached_attr) -> float | None: + """Compute remaining minutes from an estimated_date_reached DateAttribute.""" + edr = estimated_date_reached_attr.value + if edr is None: + return None + return max(0.0, (edr - datetime.now(timezone.utc)).total_seconds() / 60) + + # ── domain extractors ───────────────────────────────────────────────────────── def extract_charging(vehicle) -> dict | None: - try: - d = vehicle.domains["charging"] - except KeyError: - return None - result: dict = {} try: - cs = d["chargingStatus"] - result["chargingStatus"] = { - "chargingState": _str(cs.chargingState), - "chargePower": _phys(cs.chargePower_kW.value, "kW"), - "remainingChargingTimeToComplete": _phys(cs.remainingChargingTimeToComplete_min.value, "min"), - "chargeType": _str(cs.chargeType), + ch = vehicle.charging + status: dict = { + "chargingState": _str(ch.state), + "chargeType": _str(ch.type), } + try: + status["chargePower"] = _phys(ch.power.value, "kW") + except Exception: + pass + try: + rem = _remaining_min(ch.estimated_date_reached) + if rem is not None: + status["remainingChargingTimeToComplete"] = _phys(round(rem), "min") + except Exception: + pass + result["chargingStatus"] = status except Exception: log.debug("chargingStatus unavailable", exc_info=True) try: - bs = d["batteryStatus"] + ed = vehicle.get_electric_drive() result["batteryStatus"] = { - "currentSOC": _phys(bs.currentSOC_pct.value, "%"), - "cruisingRangeElectric": _phys(bs.cruisingRangeElectric_km.value, "km"), + "currentSOC": _phys(ed.level.value, "%"), + "cruisingRangeElectric": _phys(ed.range.value, "km"), } except Exception: log.debug("charging/batteryStatus unavailable", exc_info=True) try: - cfg = d["chargingSettings"] + cfg = vehicle.charging.settings result["chargingSettings"] = { - "targetSOC": _phys(cfg.targetSOC_pct.value, "%"), - "maxChargeCurrentAC": _str(cfg.maxChargeCurrentAC), - "autoUnlockPlugWhenCharged": _str(cfg.autoUnlockPlugWhenCharged), + "targetSOC": _phys(cfg.target_level.value, "%"), + "maxChargeCurrentAC": _phys(cfg.maximum_current.value, "A"), + "autoUnlockPlugWhenCharged": _str(cfg.auto_unlock), } except Exception: log.debug("chargingSettings unavailable", exc_info=True) try: - ps = d["plugStatus"] + conn = vehicle.charging.connector result["plugStatus"] = { - "plugConnectionState": _str(ps.plugConnectionState), - "plugLockState": _str(ps.plugLockState), - "externalPower": _str(ps.externalPower), + "plugConnectionState": _str(conn.connection_state), + "plugLockState": _str(conn.lock_state), + "externalPower": _str(conn.external_power), } except Exception: log.debug("plugStatus unavailable", exc_info=True) @@ -68,36 +81,34 @@ def extract_charging(vehicle) -> dict | None: def extract_climatisation(vehicle) -> dict | None: - try: - d = vehicle.domains["climatisation"] - except KeyError: - return None - result: dict = {} try: - cl = d["climatisationStatus"] - result["climatisationStatus"] = { - "climatisationState": _str(cl.climatisationState), - "remainingClimatisationTime": _phys(cl.remainingClimatisationTime_min.value, "min"), - } + cl = vehicle.climatization + status: dict = {"climatisationState": _str(cl.state)} + try: + rem = _remaining_min(cl.estimated_date_reached) + if rem is not None: + status["remainingClimatisationTime"] = _phys(round(rem), "min") + except Exception: + pass + result["climatisationStatus"] = status except Exception: log.debug("climatisationStatus unavailable", exc_info=True) try: - cs = d["climatisationSettings"] + cs = vehicle.climatization.settings result["climatisationSettings"] = { - "targetTemperature": _phys(cs.targetTemperature_C.value, "degC"), - "unitInCar": _str(cs.unitInCar), + "targetTemperature": _phys(cs.target_temperature.value, "degC"), } except Exception: log.debug("climatisationSettings unavailable", exc_info=True) try: - wh = d["windowHeatingStatus"] + wh = vehicle.window_heatings result["windowHeatingStatus"] = { - name: _str(win.windowHeatingState) - for name, win in wh.windows.items() + name: _str(win.heating_state) + for name, win in wh.heatings.items() } except Exception: log.debug("windowHeatingStatus unavailable", exc_info=True) @@ -106,41 +117,35 @@ def extract_climatisation(vehicle) -> dict | None: def extract_measurements(vehicle) -> dict | None: - try: - d = vehicle.domains["measurements"] - except KeyError: - return None - result: dict = {} try: - od = d["odometerStatus"] - result["odometerStatus"] = {"odometer": _phys(od.odometer.value, "km")} + result["odometerStatus"] = {"odometer": _phys(vehicle.odometer.value, "km")} except Exception: log.debug("odometerStatus unavailable", exc_info=True) try: - rs = d["rangeStatus"] + ed = vehicle.get_electric_drive() + electric_range = ed.range.value result["rangeStatus"] = { - "electricRange": _phys(rs.electricRange.value, "km"), - "totalRange": _phys(rs.totalRange_km.value, "km"), + "electricRange": _phys(electric_range, "km"), + "totalRange": _phys(electric_range, "km"), } except Exception: log.debug("rangeStatus unavailable", exc_info=True) try: - bs = d["temperatureBatteryStatus"] + bat = vehicle.get_electric_drive().battery result["temperatureBatteryStatus"] = { - "temperatureHvBatteryMax": _phys(float(bs.temperatureHvBatteryMax_K.value - 273.15), "degC"), - "temperatureHvBatteryMin": _phys(float(bs.temperatureHvBatteryMin_K.value - 273.15), "degC"), + "temperatureHvBatteryMax": _phys(bat.temperature_max.value, "degC"), + "temperatureHvBatteryMin": _phys(bat.temperature_min.value, "degC"), } except Exception: log.debug("measurements/temperatureBatteryStatus unavailable", exc_info=True) try: - temp = d["temperatureOutsideStatus"] result["temperatureOutsideStatus"] = { - "temperatureOutside": _phys(temp.temperatureOutside_C.value, "degC"), + "temperatureOutside": _phys(vehicle.outside_temperature.value, "degC"), } except Exception: log.debug("temperatureOutsideStatus unavailable", exc_info=True) @@ -149,25 +154,14 @@ def extract_measurements(vehicle) -> dict | None: def extract_readiness(vehicle) -> dict | None: - try: - d = vehicle.domains["readiness"] - except KeyError: - return None - result: dict = {} try: - rs = d["readinessStatus"] - conn = rs.connectionState - warn = rs.connectionWarning + conn_state = str(vehicle.connection_state) result["readinessStatus"] = { "connectionState": { - "isOnline": conn.isOnline.value, - "isActive": conn.isActive.value, - }, - "connectionWarning": { - "insufficientBatteryLevelWarning": warn.insufficientBatteryLevelWarning.value, - "insufficientBatteryLevelError": warn.insufficientBatteryLevelError.value, + "isOnline": conn_state == "online", + "isActive": conn_state in ("online", "reachable"), }, } except Exception: @@ -177,18 +171,13 @@ def extract_readiness(vehicle) -> dict | None: def extract_parking(vehicle) -> dict | None: - try: - d = vehicle.domains["parking"] - except KeyError: - return None - result: dict = {} try: - pos = d["parkingPosition"] + pos = vehicle.position result["parkingPosition"] = { - "lat": pos.lat.value, - "lon": pos.lon.value, + "lat": pos.latitude.value, + "lon": pos.longitude.value, } except Exception: log.debug("parkingPosition unavailable", exc_info=True) @@ -197,27 +186,22 @@ def extract_parking(vehicle) -> dict | None: def extract_access(vehicle) -> dict | None: - try: - d = vehicle.domains["access"] - except KeyError: - return None - result: dict = {} try: - acc = d["accessStatus"] + doors = vehicle.doors result["accessStatus"] = { - "overallStatus": _str(acc.overallStatus), + "overallStatus": _str(doors.lock_state), "doors": { name: { - "lockState": _str(door.lockState), - "openState": _str(door.openState), + "lockState": _str(door.lock_state), + "openState": _str(door.open_state), } - for name, door in acc.doors.items() + for name, door in doors.doors.items() }, "windows": { - name: {"openState": _str(win.openState)} - for name, win in acc.windows.items() + name: {"openState": _str(win.open_state)} + for name, win in vehicle.windows.windows.items() }, } except Exception: @@ -283,4 +267,4 @@ def apply_procedural(record: dict) -> dict: if proc: record["procedural"] = proc - return record + return record \ No newline at end of file diff --git a/server/we_connect.py b/server/we_connect.py index 61fa8f6..453e445 100644 --- a/server/we_connect.py +++ b/server/we_connect.py @@ -2,35 +2,51 @@ import logging import sys try: - from weconnect import weconnect + from carconnectivity.carconnectivity import CarConnectivity except ImportError: - print("weconnect is not installed. Run: pip install weconnect", file=sys.stderr) + print( + "carconnectivity is not installed. Run: " + "pip install carconnectivity carconnectivity-connector-volkswagen", + file=sys.stderr, + ) sys.exit(1) log = logging.getLogger(__name__) -def connect(username: str, password: str): - wc = weconnect.WeConnect( - username=username, - password=password, - updateAfterLogin=False, - loginOnInit=True, - ) - wc.login() - wc.update() - return wc +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 -def select_vehicle(wc, vin: str | None): +def select_vehicle(cc: CarConnectivity, vin: str | None): """Return (vehicle, error_message). error_message is None on success.""" - vehicles = list(wc.vehicles.values()) + vehicles = list(cc.get_garage().list_vehicles()) if not vehicles: - return None, "No vehicles found in this WeConnect account" + return None, "No vehicles found in this CarConnectivity account" if vin: vehicle = next((v for v in vehicles if v.vin.value == vin), None) if not vehicle: available = [v.vin.value for v in vehicles] return None, f"VIN {vin} not found. Available: {available}" return vehicle, None - return vehicles[0], None \ No newline at end of file + return vehicles[0], None + + +def update(cc: CarConnectivity) -> None: + cc.fetch_all() \ No newline at end of file