Move collect.py into server/; split main() into server/main.py

run() stays in server/collect.py with relative imports.
main() (CLI arg parsing, credential loading) moves to server/main.py.
install.sh updated to use `python -m server.main` (required for relative
imports to resolve correctly when invoked as a module).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 18:30:39 +02:00
co-authored by Claude Sonnet 4.6
parent 423e6d3027
commit 5230b304a2
3 changed files with 116 additions and 114 deletions
+1 -1
View File
@@ -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
+109
View File
@@ -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)
+5 -112
View File
@@ -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")