#!/usr/bin/env python3 """ CarConnectivity vehicle data collector. 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 -------------- # Credentials file with all settings: python -m server.main -c credentials/alex.json # Override username/password via env vars: export WC_USER=me@example.com WC_PASS=secret python -m server.main -c credentials/alex.json # Override domains on the CLI: python -m server.main -c credentials/alex.json -d charging,measurements Available domains: charging, climatisation, electric_drive, connectivity, vehicle, position, doors Credentials file format (JSON) ------------------------------- { "credentials": { "username": "me@example.com", "password": "secret" }, "vin": "WVWZZZE1ZMP123456", "domains": ["charging", "measurements", "readiness"], "interval_s": 300, "host": "0.0.0.0", "port": 9999, "log_dir": "./logs" } Push server ----------- The collector listens on host/port from the credentials file (default 0.0.0.0:9999). Each connected TCP client receives every new snapshot as a single line of JSON (newline-delimited) the moment it is collected. Connect with: nc localhost 9999 or any TCP client that reads lines. """ import argparse import json import os import sys from pathlib import Path from .collect import run from .data_model import ALL_DOMAINS def main() -> None: p = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) creds = p.add_argument_group( "credentials (also accepted via WC_USER / WC_PASS env vars or -c FILE)" ) creds.add_argument( "-c", "--credentials", metavar="FILE", help="JSON file with connection settings (see format above).", ) creds.add_argument( "-u", "--username", default=os.environ.get("WC_USER"), help="MyVolkswagen email (overrides credentials file)", ) creds.add_argument( "-p", "--password", default=os.environ.get("WC_PASS"), help="MyVolkswagen password (overrides credentials file)", ) p.add_argument( "-d", "--domains", default=None, metavar="DOMAIN[,DOMAIN…]", help=( "Comma-separated domains to collect, or 'all' (overrides credentials file). " f"Available: {', '.join(ALL_DOMAINS)}. " "Default: charging,electric_drive,connectivity" ), ) p.add_argument( "--max-records", type=int, default=None, metavar="N", help="Keep only the last N records per file (default: unlimited)", ) p.add_argument( "-i", "--interval", type=int, default=None, metavar="SECONDS", help="Collection interval in seconds (overrides credentials file, default: 300)", ) p.add_argument( "--host", default=None, help="Push server bind address (overrides credentials file, default: 0.0.0.0)", ) p.add_argument( "--port", type=int, default=None, help="Push server TCP port (overrides credentials file, default: 9999)", ) p.add_argument( "--log-dir", default=None, metavar="DIR", help="Directory for rotating log files (overrides credentials file, default: ./logs)", ) p.add_argument("-v", "--verbose", action="store_true", help="Enable debug logging") p.add_argument( "--diff", action="store_true", default=None, help="Send diffs instead of full snapshots over the push server (overrides credentials file, default: off)", ) p.add_argument( "--dry", action="store_true", help="Skip CarConnectivity login and data collection; run push server only (for GUI testing)", ) p.add_argument( "--list-domains", action="store_true", help="Print available domains and exit", ) args = p.parse_args() if args.list_domains: print("\n".join(ALL_DOMAINS)) sys.exit(0) creds_data: dict = {} if args.credentials: creds_path = Path(args.credentials) if not creds_path.exists(): p.error(f"Credentials file not found: {args.credentials}") try: creds_data = json.loads(creds_path.read_text()) except json.JSONDecodeError as exc: p.error(f"Invalid JSON in credentials file: {exc}") nested = creds_data.get("credentials", {}) if args.username is None: args.username = nested.get("username") or creds_data.get("username") if args.password is None: args.password = nested.get("password") or creds_data.get("password") args.vin = creds_data.get("vin") if args.domains is None: raw_domains = creds_data.get("domains") if isinstance(raw_domains, list): args.domains = ",".join(raw_domains) else: args.domains = raw_domains or "charging,electric_drive,connectivity" if args.interval is None: args.interval = int(creds_data.get("interval_s", 300)) if args.host is None: args.host = creds_data.get("host", "0.0.0.0") if args.port is None: args.port = int(creds_data.get("port", 9999)) if args.log_dir is None: args.log_dir = creds_data.get("log_dir", "./logs") if args.diff is None: args.diff = bool(creds_data.get("diff", False)) if not args.dry and (not args.username or not args.password): p.error( "Username and password are required. " "Provide them in the credentials file or via -u/-p / WC_USER / WC_PASS." ) run(args) if __name__ == "__main__": main()