Update clients, docs, and install for CarConnectivity migration
- gui_client: update _TRACE_LABELS paths to new flat snapshot structure - README: update library name, pip command, domain list, credentials template, output example, and module reference - install.sh: update credentials template and systemd service description - client.py: update docstring - data_model: Kelvin→Celsius conversion for battery temps (CarConnectivity returns Kelvin); round range_estimated_full to 1 decimal - jay_diff: fix merge when old value is not a dict; skip empty-dict branches in delete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# we_monitor
|
# 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.
|
timestamped records as JSON, and pushes live data to connected TCP clients.
|
||||||
A PyQt5 GUI client visualises the data in real time.
|
A PyQt5 GUI client visualises the data in real time.
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ server/
|
|||||||
data_model.py — domain extractors, collect_snapshot()
|
data_model.py — domain extractors, collect_snapshot()
|
||||||
network.py — TCP push server (accept loop + broadcast)
|
network.py — TCP push server (accept loop + broadcast)
|
||||||
storage_helpers.py — load / save JSON store, midnight rotation helpers
|
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
|
log_config.py — logging setup
|
||||||
utils/
|
utils/
|
||||||
jay_diff.py — JSON diff / merge library
|
jay_diff.py — JSON diff / merge library
|
||||||
@@ -25,7 +25,7 @@ requirements.txt
|
|||||||
## Requirements
|
## 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:
|
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`)
|
## 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
|
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.
|
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"
|
"password": "secret"
|
||||||
},
|
},
|
||||||
"vin": "WVWZZZE1ZMP000000",
|
"vin": "WVWZZZE1ZMP000000",
|
||||||
"domains": ["charging", "measurements", "readiness"],
|
"domains": ["charging", "electric_drive", "connectivity"],
|
||||||
"interval_s": 300,
|
"interval_s": 300,
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": 9999,
|
"port": 9999,
|
||||||
@@ -79,10 +79,10 @@ All connection and collection settings live in a single JSON file:
|
|||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
|-----|---------|-------------|
|
|-----|---------|-------------|
|
||||||
| `credentials.username` | — | WeConnect / MyVolkswagen email |
|
| `credentials.username` | — | MyVolkswagen email |
|
||||||
| `credentials.password` | — | WeConnect password |
|
| `credentials.password` | — | MyVolkswagen password |
|
||||||
| `vin` | first vehicle | Target VIN |
|
| `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 |
|
| `interval_s` | `300` | Collection interval in seconds |
|
||||||
| `host` | `"0.0.0.0"` | Push server bind address |
|
| `host` | `"0.0.0.0"` | Push server bind address |
|
||||||
| `port` | `9999` | Push server TCP port |
|
| `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 |
|
| `--log-dir DIR` | `log_dir` | Directory for rotating log files |
|
||||||
| `--max-records N` | — | Keep only the last N records per file (default: unlimited) |
|
| `--max-records N` | — | Keep only the last N records per file (default: unlimited) |
|
||||||
| `--diff` | `diff` | Enable diff-mode on the push server |
|
| `--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 |
|
| `-v` | — | Verbose / debug logging |
|
||||||
| `--list-domains` | — | Print available domains and exit |
|
| `--list-domains` | — | Print available domains and exit |
|
||||||
|
|
||||||
**Available domains:** `charging`, `climatisation`, `measurements`,
|
**Available domains:** `charging`, `climatisation`, `electric_drive`,
|
||||||
`readiness`, `parking`, `access`
|
`connectivity`, `vehicle`, `position`, `doors`
|
||||||
|
|
||||||
### Log rotation
|
### Log rotation
|
||||||
|
|
||||||
@@ -129,10 +129,12 @@ Log files are named `YYYY_MM_DD_HH_MM_SS_<VIN>.json` and contain:
|
|||||||
{
|
{
|
||||||
"ts": "2025-05-25T10:00:00+00:00",
|
"ts": "2025-05-25T10:00:00+00:00",
|
||||||
"charging": {
|
"charging": {
|
||||||
"batteryStatus": {
|
"state": "readyForCharging",
|
||||||
"currentSOC": { "value": 80, "unit": "%" },
|
"power": { "value": 11.0, "unit": "kW" }
|
||||||
"cruisingRangeElectric": { "value": 310, "unit": "km" }
|
},
|
||||||
}
|
"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`
|
### `collect.py`
|
||||||
|
|
||||||
Entry point for the collector daemon. Parses CLI / credentials-file arguments,
|
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.
|
poll loop.
|
||||||
|
|
||||||
### `server/data_model.py`
|
### `server/data_model.py`
|
||||||
|
|
||||||
`collect_snapshot(vehicle, domains) → dict`
|
`collect_snapshot(vehicle, domains) → dict`
|
||||||
Iterates over the requested domains on a `weconnect.Vehicle` and extracts every
|
Iterates over the requested domains on a CarConnectivity vehicle object and extracts
|
||||||
`Status` attribute into a flat-ish dict keyed by domain and status-object name.
|
fields into a flat dict keyed by domain.
|
||||||
Physical values are normalised to `{"value": …, "unit": …}`. Returns a record
|
Physical values are normalised to `{"value": …, "unit": …}`. Returns a record
|
||||||
with a top-level `"ts"` ISO-8601 timestamp.
|
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`
|
### `server/we_connect.py`
|
||||||
|
|
||||||
`connect(username, password) → WeConnect`
|
`connect(username, password) → CarConnectivity`
|
||||||
Authenticates and returns a connected `WeConnect` instance.
|
Authenticates and returns a connected `CarConnectivity` instance.
|
||||||
|
|
||||||
`select_vehicle(wc, vin) → (vehicle, error)`
|
`select_vehicle(wc, vin) → (vehicle, error)`
|
||||||
Returns the target vehicle (first available if `vin` is empty) or an error string.
|
Returns the target vehicle (first available if `vin` is empty) or an error string.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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
|
Connects to the collect.py TCP push server and prints each incoming
|
||||||
snapshot to stdout as pretty-printed JSON.
|
snapshot to stdout as pretty-printed JSON.
|
||||||
|
|||||||
+13
-14
@@ -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
|
# 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.
|
# cursor readout. Falls back to the last path component for unknown keys.
|
||||||
_TRACE_LABELS: dict[str, str] = {
|
_TRACE_LABELS: dict[str, str] = {
|
||||||
"charging.chargingStatus.chargePower": "charge power",
|
"charging.power": "charge power",
|
||||||
"charging.chargingStatus.remainingChargingTimeToComplete": "time to full",
|
"charging.remain": "time to full",
|
||||||
"charging.batteryStatus.currentSOC": "SOC",
|
"charging.settings.target_level": "target SOC",
|
||||||
"charging.batteryStatus.cruisingRangeElectric": "range",
|
"climatisation.remain": "clim. time",
|
||||||
"charging.chargingSettings.targetSOC": "target SOC",
|
"climatisation.settings.target_temperature": "target temp",
|
||||||
"climatisation.climatisationStatus.remainingClimatisationTime": "clim. time",
|
"climatisation.environment.temperature_outside": "outside temp",
|
||||||
"climatisation.climatisationSettings.targetTemperature": "target temp",
|
"electric_drive.battery.soc": "SOC",
|
||||||
"measurements.odometerStatus.odometer": "odometer",
|
"electric_drive.range": "range",
|
||||||
"measurements.rangeStatus.electricRange": "elec. range",
|
"electric_drive.range_full": "range (full)",
|
||||||
"measurements.rangeStatus.totalRange": "total range",
|
"electric_drive.battery.temperature_max": "batt. temp max",
|
||||||
"measurements.temperatureBatteryStatus.temperatureHvBatteryMax": "batt. temp max",
|
"electric_drive.battery.temperature_min": "batt. temp min",
|
||||||
"measurements.temperatureBatteryStatus.temperatureHvBatteryMin": "batt. temp min",
|
"electric_drive.odometer.odometer": "odometer",
|
||||||
"measurements.temperatureOutsideStatus.temperatureOutside": "outside temp",
|
"procedural.range_at_100": "range 100%",
|
||||||
"procedural.range_at_100": "range 100%"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -69,7 +69,7 @@ if [[ ! -f "$CREDS_FILE" ]]; then
|
|||||||
"password": "secret"
|
"password": "secret"
|
||||||
},
|
},
|
||||||
"vin": "",
|
"vin": "",
|
||||||
"domains": ["charging", "measurements", "readiness"],
|
"domains": ["charging", "electric_drive", "connectivity"],
|
||||||
"interval_s": 300,
|
"interval_s": 300,
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": 9999,
|
"port": 9999,
|
||||||
@@ -93,7 +93,7 @@ mkdir -p "$SYSTEMD_DIR"
|
|||||||
|
|
||||||
cat > "$SERVICE_FILE" <<EOF
|
cat > "$SERVICE_FILE" <<EOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=WeConnect vehicle data collector
|
Description=CarConnectivity vehicle data collector
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
|
|||||||
@@ -118,13 +118,13 @@ def extract_electric_drive(vehicle) -> dict | None:
|
|||||||
try:
|
try:
|
||||||
ed = vehicle.get_electric_drive()
|
ed = vehicle.get_electric_drive()
|
||||||
result["range"] = _phys(ed.range.value, "km")
|
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:
|
try:
|
||||||
bat = ed.battery
|
bat = ed.battery
|
||||||
result["battery"] = {
|
result["battery"] = {
|
||||||
"soc": _phys(ed.level.value, "%"),
|
"soc": _phys(ed.level.value, "%"),
|
||||||
"temperature_max": _phys(bat.temperature_max.value, "degC"),
|
"temperature_max": _phys(round(float(bat.temperature_max.value) - 273.15, 1), "degC"),
|
||||||
"temperature_min": _phys(bat.temperature_min.value, "degC"),
|
"temperature_min": _phys(round(float(bat.temperature_min.value) - 273.15, 1), "degC"),
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception:
|
||||||
log.debug("electric_drive/battery unavailable", exc_info=True)
|
log.debug("electric_drive/battery unavailable", exc_info=True)
|
||||||
|
|||||||
+4
-1
@@ -77,7 +77,10 @@ def jay_merge_update(_old, _new):
|
|||||||
r = jay_merge_update(_old.get(k, {}), v)
|
r = jay_merge_update(_old.get(k, {}), v)
|
||||||
_old[k] = r
|
_old[k] = r
|
||||||
else:
|
else:
|
||||||
|
if isinstance(_old, dict):
|
||||||
_old[k] = _new[k]
|
_old[k] = _new[k]
|
||||||
|
else:
|
||||||
|
_old = _new
|
||||||
return _old
|
return _old
|
||||||
|
|
||||||
|
|
||||||
@@ -96,7 +99,7 @@ def jay_merge_add(_old, _new):
|
|||||||
|
|
||||||
def jay_merge_delete(_old, _new):
|
def jay_merge_delete(_old, _new):
|
||||||
for k, v in _new.items():
|
for k, v in _new.items():
|
||||||
if isinstance(v, dict):
|
if isinstance(v, dict) and v:
|
||||||
jay_merge_delete(_old[k], _new[k])
|
jay_merge_delete(_old[k], _new[k])
|
||||||
if len(_old[k].keys()) == 0:
|
if len(_old[k].keys()) == 0:
|
||||||
del _old[k]
|
del _old[k]
|
||||||
|
|||||||
Reference in New Issue
Block a user