diff --git a/install.sh b/install.sh index 0d8fa26..f97d2c2 100755 --- a/install.sh +++ b/install.sh @@ -100,7 +100,7 @@ Wants=network-online.target [Service] Type=simple WorkingDirectory=$SCRIPT_DIR -ExecStart=$PYTHON $SCRIPT_DIR/collect.py -c $CREDS_FILE +ExecStart=$PYTHON -m server.main -c $CREDS_FILE Restart=on-failure RestartSec=60 StandardOutput=journal diff --git a/server/collect.py b/server/collect.py new file mode 100644 index 0000000..cd12d4a --- /dev/null +++ b/server/collect.py @@ -0,0 +1,109 @@ +import argparse +import logging +import sys +import time +from datetime import datetime, timezone +from pathlib import Path + +from . import log_config, network, we_connect +from .data_model import ALL_DOMAINS, collect_snapshot, apply_procedural +from .storage_helpers import auto_out_path, load_last_24h_records, load_store, save_store +from jaydiff.diff import diff_full as jay_diff_full + +from carconnectivity.errors import AuthenticationError, TemporaryAuthenticationError + +log = logging.getLogger(__name__) + + +def run(args: argparse.Namespace) -> None: + log_config.setup(args.verbose) + + raw = [d.strip() for d in args.domains.split(",")] + domains = list(ALL_DOMAINS) if raw == ["all"] else raw + unknown = [d for d in domains if d not in ALL_DOMAINS] + if unknown: + log.error("Unknown domain(s): %s. Available: %s", unknown, list(ALL_DOMAINS)) + sys.exit(1) + + if args.dry: + log.info("Dry-run mode — skipping CarConnectivity login") + wc = None + vehicle = None + vin = args.vin or "UNKNOWN" + else: + log.info("Connecting via CarConnectivity…") + wc = we_connect.connect(args.username, args.password) + vehicle, err = we_connect.select_vehicle(wc, args.vin) + if err: + log.error(err) + sys.exit(1) + vin = vehicle.vin.value + + log.info("Vehicle: %s", vin) + + logs_dir = Path(args.log_dir) + logs_dir.mkdir(parents=True, exist_ok=True) + out = auto_out_path(logs_dir, vin) + + push_clients, push_lock, push_store_ref = network.start_push_server(args.host, args.port) + push_store_ref["diff_mode"] = args.diff + push_store_ref["preloaded"] = load_last_24h_records(logs_dir, vin) + for _r in push_store_ref["preloaded"]: + if "procedural" not in _r: + apply_procedural(_r) + if push_store_ref["preloaded"]: + log.info("Pre-loaded %d record(s) from the last 24 h", len(push_store_ref["preloaded"])) + + current_day = datetime.now(timezone.utc).date() + store = load_store(out, vin, args.interval) + for _r in store["records"]: + if "procedural" not in _r: + apply_procedural(_r) + push_store_ref["current"] = store + last_broadcast: dict = {} + + if args.dry: + log.info("Push server running on %s:%d — no live collection (Ctrl-C to stop)", args.host, args.port) + else: + log.info( + "Collecting [%s] every %ds → %s (Ctrl-C to stop)", + ", ".join(domains), + args.interval, + out, + ) + + try: + while True: + if not args.dry: + today = datetime.now(timezone.utc).date() + if today != current_day: + out = auto_out_path(logs_dir, vin) + store = load_store(out, vin, args.interval) + push_store_ref["current"] = store + current_day = today + log.info("Midnight UTC rotation → %s", out) + + try: + we_connect.update(wc) + snapshot = collect_snapshot(vehicle, domains) + apply_procedural(snapshot) + store["records"].append(snapshot) + save_store(out, store, args.max_records) + if args.diff and last_broadcast: + payload = jay_diff_full(last_broadcast, snapshot, combine_upd_add=True) + else: + payload = snapshot + network.broadcast(payload, push_clients, push_lock) + last_broadcast = snapshot + log.info("Record #%d saved at %s", len(store["records"]), snapshot["ts"]) + except KeyboardInterrupt: + raise + except (AuthenticationError, TemporaryAuthenticationError): + log.warning("Authentication error — will retry at next interval") + except Exception: + log.exception("Collection failed — will retry at next interval") + + time.sleep(args.interval) + + except KeyboardInterrupt: + log.info("Stopped by user. %d records in %s", len(store["records"]), out) diff --git a/collect.py b/server/main.py similarity index 53% rename from collect.py rename to server/main.py index 00f5d5c..2760737 100644 --- a/collect.py +++ b/server/main.py @@ -9,14 +9,14 @@ the domain hierarchy: domain → status-object → field. Usage examples -------------- # Credentials file with all settings: - python collect.py -c credentials/alex.json + python -m server.main -c credentials/alex.json # Override username/password via env vars: export WC_USER=me@example.com WC_PASS=secret - python collect.py -c credentials/alex.json + python -m server.main -c credentials/alex.json # Override domains on the CLI: - python collect.py -c credentials/alex.json -d charging,measurements + python -m server.main -c credentials/alex.json -d charging,measurements Available domains: charging, climatisation, electric_drive, connectivity, vehicle, position, doors @@ -45,119 +45,13 @@ Connect with: nc localhost 9999 or any TCP client that reads lines. import argparse import json -import logging import os import sys -import time -from datetime import datetime, timezone from pathlib import Path -from server import log_config, network, we_connect -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 jaydiff.diff import diff_full as jay_diff_full +from .collect import run +from .data_model import ALL_DOMAINS -log = logging.getLogger(__name__) - - -# ── main loop ──────────────────────────────────────────────────────────────── - -def run(args: argparse.Namespace) -> None: - log_config.setup(args.verbose) - - raw = [d.strip() for d in args.domains.split(",")] - domains = list(ALL_DOMAINS) if raw == ["all"] else raw - unknown = [d for d in domains if d not in ALL_DOMAINS] - if unknown: - log.error("Unknown domain(s): %s. Available: %s", unknown, list(ALL_DOMAINS)) - sys.exit(1) - - if args.dry: - log.info("Dry-run mode — skipping CarConnectivity login") - wc = None - vehicle = None - vin = args.vin or "UNKNOWN" - else: - log.info("Connecting via CarConnectivity…") - wc = we_connect.connect(args.username, args.password) - vehicle, err = we_connect.select_vehicle(wc, args.vin) - if err: - log.error(err) - sys.exit(1) - vin = vehicle.vin.value - - log.info("Vehicle: %s", vin) - - logs_dir = Path(args.log_dir) - logs_dir.mkdir(parents=True, exist_ok=True) - out = auto_out_path(logs_dir, vin) - - push_clients, push_lock, push_store_ref = network.start_push_server(args.host, args.port) - push_store_ref["diff_mode"] = args.diff - push_store_ref["preloaded"] = load_last_24h_records(logs_dir, vin) - for _r in push_store_ref["preloaded"]: - if "procedural" not in _r: - apply_procedural(_r) - if push_store_ref["preloaded"]: - log.info("Pre-loaded %d record(s) from the last 24 h", len(push_store_ref["preloaded"])) - - current_day = datetime.now(timezone.utc).date() - store = load_store(out, vin, args.interval) - for _r in store["records"]: - if "procedural" not in _r: - apply_procedural(_r) - push_store_ref["current"] = store - last_broadcast: dict = {} - - if args.dry: - log.info("Push server running on %s:%d — no live collection (Ctrl-C to stop)", args.host, args.port) - else: - log.info( - "Collecting [%s] every %ds → %s (Ctrl-C to stop)", - ", ".join(domains), - args.interval, - out, - ) - - try: - while True: - if not args.dry: - today = datetime.now(timezone.utc).date() - if today != current_day: - out = auto_out_path(logs_dir, vin) - store = load_store(out, vin, args.interval) - push_store_ref["current"] = store - current_day = today - log.info("Midnight UTC rotation → %s", out) - - try: - we_connect.update(wc) - snapshot = collect_snapshot(vehicle, domains) - apply_procedural(snapshot) - store["records"].append(snapshot) - save_store(out, store, args.max_records) - if args.diff and last_broadcast: - payload = jay_diff_full(last_broadcast, snapshot, combine_upd_add=True) - else: - payload = snapshot - network.broadcast(payload, push_clients, push_lock) - last_broadcast = snapshot - log.info("Record #%d saved at %s", len(store["records"]), snapshot["ts"]) - except KeyboardInterrupt: - raise - except (AuthenticationError, TemporaryAuthenticationError): - log.warning("Authentication error — will retry at next interval") - except Exception: - log.exception("Collection failed — will retry at next interval") - - time.sleep(args.interval) - - except KeyboardInterrupt: - log.info("Stopped by user. %d records in %s", len(store["records"]), out) - - -# ── CLI ────────────────────────────────────────────────────────────────────── def main() -> None: p = argparse.ArgumentParser( @@ -259,7 +153,6 @@ def main() -> None: except json.JSONDecodeError as exc: p.error(f"Invalid JSON in credentials file: {exc}") - # username / password: env / CLI flag > credentials.username > credentials file root nested = creds_data.get("credentials", {}) if args.username is None: args.username = nested.get("username") or creds_data.get("username") @@ -296,4 +189,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main()