diff --git a/README.md b/README.md index a733b3e..f19c002 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # we_monitor -Periodically polls a VW WeConnect vehicle via the `weconnect` library, stores +Periodically polls a VW vehicle via the `carconnectivity` library, stores timestamped records as JSON, and pushes live data to connected TCP clients. A PyQt5 GUI client visualises the data in real time. @@ -14,7 +14,7 @@ server/ data_model.py — domain extractors, collect_snapshot() network.py — TCP push server (accept loop + broadcast) storage_helpers.py — load / save JSON store, midnight rotation helpers - we_connect.py — WeConnect login and vehicle selection + we_connect.py — CarConnectivity login and vehicle selection log_config.py — logging setup utils/ jay_diff.py — JSON diff / merge library @@ -25,7 +25,7 @@ requirements.txt ## Requirements ``` -pip install weconnect PyQt5 pyqtgraph +pip install carconnectivity carconnectivity-connector-volkswagen PyQt5 pyqtgraph ``` Or use the installer which also sets up a systemd user service: @@ -38,7 +38,7 @@ Or use the installer which also sets up a systemd user service: ## Collector (`collect.py`) -Connects to WeConnect, polls selected domains at a configurable interval, and +Connects via CarConnectivity, polls selected domains at a configurable interval, and appends timestamped records to a rotating JSON log. Simultaneously runs a TCP push server so clients receive every new snapshot the moment it is collected. @@ -68,7 +68,7 @@ All connection and collection settings live in a single JSON file: "password": "secret" }, "vin": "WVWZZZE1ZMP000000", - "domains": ["charging", "measurements", "readiness"], + "domains": ["charging", "electric_drive", "connectivity"], "interval_s": 300, "host": "0.0.0.0", "port": 9999, @@ -79,10 +79,10 @@ All connection and collection settings live in a single JSON file: | Key | Default | Description | |-----|---------|-------------| -| `credentials.username` | — | WeConnect / MyVolkswagen email | -| `credentials.password` | — | WeConnect password | +| `credentials.username` | — | MyVolkswagen email | +| `credentials.password` | — | MyVolkswagen password | | `vin` | first vehicle | Target VIN | -| `domains` | `["charging","measurements","readiness"]` | Domains to collect (list or `"all"`) | +| `domains` | `["charging","electric_drive","connectivity"]` | Domains to collect (list or `"all"`) | | `interval_s` | `300` | Collection interval in seconds | | `host` | `"0.0.0.0"` | Push server bind address | | `port` | `9999` | Push server TCP port | @@ -104,12 +104,12 @@ CLI flags override the corresponding credentials-file value when given. | `--log-dir DIR` | `log_dir` | Directory for rotating log files | | `--max-records N` | — | Keep only the last N records per file (default: unlimited) | | `--diff` | `diff` | Enable diff-mode on the push server | -| `--dry` | — | Skip WeConnect login; run push server only (useful for GUI testing) | +| `--dry` | — | Skip CarConnectivity login; run push server only (useful for GUI testing) | | `-v` | — | Verbose / debug logging | | `--list-domains` | — | Print available domains and exit | -**Available domains:** `charging`, `climatisation`, `measurements`, -`readiness`, `parking`, `access` +**Available domains:** `charging`, `climatisation`, `electric_drive`, +`connectivity`, `vehicle`, `position`, `doors` ### Log rotation @@ -129,10 +129,12 @@ Log files are named `YYYY_MM_DD_HH_MM_SS_.json` and contain: { "ts": "2025-05-25T10:00:00+00:00", "charging": { - "batteryStatus": { - "currentSOC": { "value": 80, "unit": "%" }, - "cruisingRangeElectric": { "value": 310, "unit": "km" } - } + "state": "readyForCharging", + "power": { "value": 11.0, "unit": "kW" } + }, + "electric_drive": { + "range": { "value": 310, "unit": "km" }, + "battery": { "soc": { "value": 80, "unit": "%" } } } } ] @@ -259,14 +261,14 @@ pretty-printed JSON with a running record counter. ### `collect.py` Entry point for the collector daemon. Parses CLI / credentials-file arguments, -establishes the WeConnect session, starts the push server, and runs the main +establishes the CarConnectivity session, starts the push server, and runs the main poll loop. ### `server/data_model.py` `collect_snapshot(vehicle, domains) → dict` -Iterates over the requested domains on a `weconnect.Vehicle` and extracts every -`Status` attribute into a flat-ish dict keyed by domain and status-object name. +Iterates over the requested domains on a CarConnectivity vehicle object and extracts +fields into a flat dict keyed by domain. Physical values are normalised to `{"value": …, "unit": …}`. Returns a record with a top-level `"ts"` ISO-8601 timestamp. @@ -307,8 +309,8 @@ the last 24 hours, and returns them sorted by timestamp. ### `server/we_connect.py` -`connect(username, password) → WeConnect` -Authenticates and returns a connected `WeConnect` instance. +`connect(username, password) → CarConnectivity` +Authenticates and returns a connected `CarConnectivity` instance. `select_vehicle(wc, vin) → (vehicle, error)` Returns the target vehicle (first available if `vin` is empty) or an error string. diff --git a/client.py b/client.py index 843e344..b6fecb8 100644 --- a/client.py +++ b/client.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -WeConnect push-server test client. +CarConnectivity push-server test client. Connects to the collect.py TCP push server and prints each incoming snapshot to stdout as pretty-printed JSON. diff --git a/gui_client.py b/gui_client.py index 44217af..f950ae5 100644 --- a/gui_client.py +++ b/gui_client.py @@ -94,20 +94,19 @@ _SETTINGS_PATH = Path.home() / ".config" / "we_monitor" / "plot_settings.json" # Maps full dotted trace key → short display label used in chips, legends and # cursor readout. Falls back to the last path component for unknown keys. _TRACE_LABELS: dict[str, str] = { - "charging.chargingStatus.chargePower": "charge power", - "charging.chargingStatus.remainingChargingTimeToComplete": "time to full", - "charging.batteryStatus.currentSOC": "SOC", - "charging.batteryStatus.cruisingRangeElectric": "range", - "charging.chargingSettings.targetSOC": "target SOC", - "climatisation.climatisationStatus.remainingClimatisationTime": "clim. time", - "climatisation.climatisationSettings.targetTemperature": "target temp", - "measurements.odometerStatus.odometer": "odometer", - "measurements.rangeStatus.electricRange": "elec. range", - "measurements.rangeStatus.totalRange": "total range", - "measurements.temperatureBatteryStatus.temperatureHvBatteryMax": "batt. temp max", - "measurements.temperatureBatteryStatus.temperatureHvBatteryMin": "batt. temp min", - "measurements.temperatureOutsideStatus.temperatureOutside": "outside temp", - "procedural.range_at_100": "range 100%" + "charging.power": "charge power", + "charging.remain": "time to full", + "charging.settings.target_level": "target SOC", + "climatisation.remain": "clim. time", + "climatisation.settings.target_temperature": "target temp", + "climatisation.environment.temperature_outside": "outside temp", + "electric_drive.battery.soc": "SOC", + "electric_drive.range": "range", + "electric_drive.range_full": "range (full)", + "electric_drive.battery.temperature_max": "batt. temp max", + "electric_drive.battery.temperature_min": "batt. temp min", + "electric_drive.odometer.odometer": "odometer", + "procedural.range_at_100": "range 100%", } diff --git a/install.sh b/install.sh index 2657ece..75d7b21 100755 --- a/install.sh +++ b/install.sh @@ -69,7 +69,7 @@ if [[ ! -f "$CREDS_FILE" ]]; then "password": "secret" }, "vin": "", - "domains": ["charging", "measurements", "readiness"], + "domains": ["charging", "electric_drive", "connectivity"], "interval_s": 300, "host": "0.0.0.0", "port": 9999, @@ -93,7 +93,7 @@ mkdir -p "$SYSTEMD_DIR" cat > "$SERVICE_FILE" < dict | None: try: ed = vehicle.get_electric_drive() result["range"] = _phys(ed.range.value, "km") - result["range_full"] = _phys(ed.range_estimated_full.value, "km") + result["range_full"] = _phys(round(float(ed.range_estimated_full.value), 1), "km") try: bat = ed.battery result["battery"] = { "soc": _phys(ed.level.value, "%"), - "temperature_max": _phys(bat.temperature_max.value, "degC"), - "temperature_min": _phys(bat.temperature_min.value, "degC"), + "temperature_max": _phys(round(float(bat.temperature_max.value) - 273.15, 1), "degC"), + "temperature_min": _phys(round(float(bat.temperature_min.value) - 273.15, 1), "degC"), } except Exception: log.debug("electric_drive/battery unavailable", exc_info=True) diff --git a/utils/jay_diff.py b/utils/jay_diff.py index a678d8b..5b67555 100644 --- a/utils/jay_diff.py +++ b/utils/jay_diff.py @@ -77,7 +77,10 @@ def jay_merge_update(_old, _new): r = jay_merge_update(_old.get(k, {}), v) _old[k] = r else: - _old[k] = _new[k] + if isinstance(_old, dict): + _old[k] = _new[k] + else: + _old = _new return _old @@ -96,7 +99,7 @@ def jay_merge_add(_old, _new): def jay_merge_delete(_old, _new): for k, v in _new.items(): - if isinstance(v, dict): + if isinstance(v, dict) and v: jay_merge_delete(_old[k], _new[k]) if len(_old[k].keys()) == 0: del _old[k]