client/install.sh → install_client.sh
server/install.sh → install_server.sh
Both scripts now resolve REPO_DIR as their own directory (repo root)
and use a shared .venv at the repo root instead of per-subdirectory
virtual environments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
requirements.txt now references the upstream GitHub repo pinned to
commit c30dc37 (the last upstream commit before our fix), so the source
is portable and not machine-specific.
install.sh applies cariad-hybrid-auth-fix.patch to the installed
site-packages after pip runs, using `patch -N` so it is idempotent on
repeat installs. The patch file is checked in to the repo so install.sh
can always find it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switches the VW API backend from carconnectivity + connector-volkswagen
to volkswagencarnet (local fork with the cariad-hybrid-auth-fix patch
applied), fixing authentication against the current CARIAD BFF endpoints.
- requirements.txt: point at local volkswagencarnet git repo via
git+file:// URL (editable path no longer needed now the patch is
committed there)
- we_connect.py: rewrite around the async volkswagencarnet Connection
class; a persistent asyncio event loop held in _Session lets the rest
of the code stay synchronous
- data_model.py: remap all extractors to volkswagencarnet Vehicle
properties and Paths constants; output shape preserved so
apply_procedural and the GUI client continue to work unchanged
- collect.py: swap carconnectivity error import, drop
TemporaryAuthenticationError (no equivalent), fix vehicle.vin.value →
vehicle.vin
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add extract_windows() and register 'windows' in ALL_DOMAINS
- Remove windows extraction from extract_doors; no more double keys
- extract_windows also captures the overall windows.open_state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Don't store attributes whose .value is None; avoids spurious "None"
strings in the snapshot (lock_state, open_state were unset)
- Flatten the structure: overall lock_state/open_state now live directly
under 'doors' instead of the double-nested 'doors.doors.overallState'
- Give vehicle.windows its own try/except, independent of vehicle.doors
- Capture doors.open_state (overall open state) which was missing before
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DashboardTab.update() had no else-branch for non-dict, non-physical
values, so string fields like charging.state/type, connectivity.state,
vehicle.state and numeric fields like position.lat/lon were silently
skipped. Mirrors the existing else-branch in _add_fields().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
server/install.sh was broken after moving into server/: SCRIPT_DIR now
resolves to the server/ subdirectory, so requirements path, log dir, and
systemd WorkingDirectory all pointed at wrong locations. Introduce REPO_DIR
(parent of SCRIPT_DIR) and use it throughout.
client/install.sh creates a venv at client/.venv, installs
client/requirements.txt, and writes a client/run.sh launcher.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reflect new layout (server/, client/), separate requirements.txt files,
python -m server.main entry point, and server/install.sh location.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Move client.py and gui_client.py into client/, split root requirements.txt
into server/requirements.txt and client/requirements.txt, update install.sh
to reference server/requirements.txt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migrate we_monitor from weconnect to CarConnectivity/VW connector,
fix two jay_diff correctness bugs, and replace utils/jay_diff.py
with the shared jaydiff package from jayfield/jaypy.
jay_diff docs, tests, and data live in jayfield/jaypy; the __init__.py
files for the empty local packages are no longer needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the local copy of jay_diff and its tests/data; switch all four
import sites to `jaydiff.diff.diff_full` / `jaydiff.merge.merge_full`
from the shared Gitea package (http://192.168.22.90:3001/jayfield/jaypy).
Add the package as a VCS dependency in requirements.txt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers API reference, diff/delete-delta encoding semantics, round-trip
guarantee, wire format usage, test overview, and bug history from the
recent aliasing crash and empty-dict pruning fixes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug 1 (crash): jay_merge_delete iterated _new.items() while _old (which
aliases _new sub-dicts due to jay_diff_rev's side-effect writes) was being
mutated. Fixed by snapshotting with list(_new.items()) before iteration.
Bug 2 (wrong round-trip): When new has an empty dict {} at a location where
old had a non-empty dict, jay_merge_delete incorrectly pruned the now-empty
parent key. Root cause: jay_diff_del stored the full old sub-dict in the
delete delta for both "key absent from new" and "key present but empty in
new", making them indistinguishable at merge time.
Fix: jay_diff_del is now a standalone recursive function that stores None
for dict keys entirely absent from new (sentinel for "delete entire key")
vs a sub-dict of specific sub-keys for partial deletions. jay_merge_delete
no longer prunes empty dicts; None (or any non-dict value) in the delta is
the explicit signal to remove the parent key.
Also adds:
- tests/data/ — copy of 25 real JSON log files for integration testing
- tests/test_jay_diff_real_data.py — 2028 parametrised round-trip tests
over 675 real vehicle snapshots; exposed and confirmed both bugs above
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move the inline assertions from jay_diff.py into tests/test_jay_diff.py
as proper pytest functions, grouped by function under test. Add corner
cases for numeric/bool/None/list values, completely different structures,
empty containers, deep nesting, type changes (dict↔scalar), and
parametrised round-trip tests (diff→merge produces new).
Two bugs fixed during the exercise:
- jay_diff_rev: guard against _old being a non-dict (type change from
dict to scalar caused TypeError on item assignment)
- jay_merge_delete / jay_merge_update: already fixed in previous commit;
covered by new tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
Flatten the snapshot structure so each domain maps directly to its
fields rather than mirroring the old WeConnect status-object nesting.
New domains: electric_drive (range, battery/SOC/temps, odometer),
connectivity, vehicle, position, doors — replacing the old readiness,
parking, access, and measurements domains. Update apply_procedural
paths and collect.py defaults accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the weconnect library with carconnectivity and
carconnectivity-connector-volkswagen. The connector is configured via
a dict passed to CarConnectivity(); startup()/fetch_all() replace
login()/update(). All domain extractors in data_model.py are rewritten
for the new object model (vehicle.charging.*, vehicle.get_electric_drive(),
vehicle.climatization.*, etc.). Remaining time fields are now derived
from estimated_date_reached DateAttributes. Battery temperatures are
read directly in °C (no Kelvin conversion). Auth error handling drops
the manual re-login since the connector manages token refresh internally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The outer loop in DashboardTab.update assumed a 3-level hierarchy
(domain → object → fields). Procedural fields sit one level higher
(domain → field directly). Check _is_phys() first and render value /
unit into the correct columns instead of descending into the dict and
showing 'value' and 'unit' as separate rows.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Compute estimated full-charge range from totalRange and current SOC.
Guard against None values (_get returns None for missing keys) and
SOC = 0 to avoid TypeError / ZeroDivisionError on incomplete records.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
server/data_model.py:
- apply_procedural(record) — stub function; add computed fields to the
proc dict inside and they appear under record['procedural'] in every
record stored and broadcast.
- _get(data, *keys) — safe nested dict accessor for use inside
apply_procedural.
collect.py:
- New snapshots: apply_procedural() called after collect_snapshot(),
before appending to the store and broadcasting.
- Preloaded 24-h history: guarded apply (skip if 'procedural' already
present) applied after load_last_24h_records().
- Current-day store loaded from disk: same guarded apply after
load_store(), so legacy records sent to clients are enriched on the fly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a small dimmed timestamp label at the top of PlotTooltip.
set_values() now accepts ts_str as first argument; _on_mouse_move
formats the cursor x-position as HH:MM:SS and passes it through.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
README now covers:
- Updated diff-mode section (self-describing protocol, no _diff marker)
- Full GUI section with Connector/Dashboard/Plot tab descriptions
- Plot mouse interactions table (scroll, Shift+scroll, drag)
- Module reference for all server/, utils/, and gui_client classes
- --dry flag and all credential-file keys documented
gui_client.py module docstring updated to list tabs and mouse interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add PlotViewBox subclassing pg.ViewBox. Overrides wheelEvent to route
plain scroll to the X axis and Shift+scroll to the Y axis, both centred
on the cursor position. Passed as viewBox= to every PlotWidget so the
behaviour applies uniformly across all 8 plots.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Track a _needs_autozoom flag per plot. Set True at startup, on
reconnect (clear_data), and when a trace is added or removed. In
_redraw, when the flag is set and visible data exists, fit the Y range
to the data (with 10% margin) then clear the flag — subsequent live
updates leave the user's manual Y zoom undisturbed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add PlotTooltip — a frameless child widget that appears next to the
mouse cursor whenever it moves over a plot. It lists each active trace
with a colored bullet, short name, and the nearest y-value (unit
appended). The tooltip is repositioned on every mouse-move event and
clamped to stay within the plot widget bounds. Hidden on mouse-exit and
on clear_data (reconnect).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace pg.DateAxisItem with a custom TimeAxisItem that formats ticks
as HH:MM:SS, eliminating locale-dependent day/date prefixes on the axis.
Add a per-plot date label (right-aligned in the control bar) that shows
the date range of the visible window — single date when the view stays
within one day, "YYYY-MM-DD – YYYY-MM-DD" when it spans midnight.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When _diff contains none of the jaydiff keys (update, update_add, add,
delete) and _old is empty, return _diff directly. This lets callers pass
a raw snapshot into jay_merge_full without needing a separate code path
for the initial-state bootstrap case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Server (network.py): send every record in diff mode starting from
prev={}, including the first — fixes history drop on new client connect.
Collect (collect.py): remove payload["_diff"] = True from live broadcast.
GUI client (gui_client.py): always call jay_merge_full in diff mode;
drop the _diff key check that gated reconstruction.
CLI client (client.py): fix `args.diff or True` guard that made --diff
irrelevant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Detect fresh install vs update via systemctl is-enabled
- On update: pip install --upgrade picks up new dependency versions
- If service was active before running the script, restart it automatically
- Summary message adapts to install vs update context
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Change --diff from store_true to default=None so CLI presence is
distinguishable from the JSON default. If not given on CLI, reads
creds_data.get("diff", False). Updated install.sh template and README.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch from flat username/password keys to nested credentials block
and add interval_s, host, port, log_dir with their defaults.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CLI table now shows which credentials file key each flag overrides
- Added --diff, --dry, --log-dir, -i/--interval, --host, --port
- New "Diff mode" subsection under Push server protocol
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-i/--interval, --host, --port, --log-dir can now be passed on the
command line; each overrides the corresponding credentials file value
when given. Defaults remain unchanged (300 s, 0.0.0.0, 9999, ./logs).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jay_merge_full/jay_merge_update mutates _state nested dicts in place.
The previously emitted dict(_state) was a shallow copy sharing those
nested dicts, so background-thread merges retroactively corrupted
already-emitted snapshots: timestamps were correct (strings are
immutable) but Y values all converged to the latest record's values.
Fix: emit copy.deepcopy(_state) so each snapshot is fully independent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Server (collect.py --diff):
- History burst: first record sent full, subsequent as jay_diff_full diffs
- Live broadcast: each snapshot diffed against the previous one
- Diff payloads tagged with "_diff": true
Client (client.py --diff):
- Reconstructs full state via jay_merge_full before printing
GUI (Connector tab "Diff mode" checkbox):
- TcpReader.connect_to() accepts diff_mode; maintains _state dict
- Merges incoming diffs before emitting to Dashboard/Plot
- Checkbox state persisted to plot_settings.json
Both sides default to off; must be enabled consistently on server and client.
utils/__init__.py added so utils.jay_diff is importable as a package.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace direct _redraw() calls in PlotTab.update() with a 100 ms
single-shot QTimer. During a burst of incoming records (e.g. 500
history records on connect) only the data is accumulated; a single
redraw fires 100 ms after the last record arrives instead of once
per record (~4000 pyqtgraph redraws avoided).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When --dry is set, WeConnect login and data collection are skipped.
The push server still starts and serves preloaded 24 h history to
connecting clients — useful for GUI testing without a live vehicle.
Credentials are not required in dry-run mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Credentials file is now the single source for vin, interval_s, host,
port, log_dir; document new format with nested credentials block
- CLI reference trimmed to the remaining flags only
- Push server section: 24 h rolling window instead of same-day history
- GUI section: connection persistence, clear-on-reconnect, per-plot time
window spinner, x-axis fit when data < window size
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove --vin, --interval/-i, --output/-o, --host, --port from argparse.
All are now read from the credentials JSON (interval_s, host, port,
log_dir with sensible defaults). CLI flags -u/-p/-d still override the
file; log_dir replaces the old --output/auto-logs logic — logs_dir is
always used, no fixed output path option.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add PlotTab.clear_data() which resets _series and _units, then redraws.
Called in MainWindow._connect() before opening the socket so stale data
from a previous session never mingles with fresh history from the server.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Track the earliest timestamp across plotted traces (starting from None,
not cutoff) so setXRange uses the first real data point as the left
bound — no empty gap when data doesn't fill the full time window.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace load_today_records with load_last_24h_records which computes a
rolling 24 h cutoff, globs log files for both the current and previous
calendar day, and filters records by timestamp — so clients always
receive a full 24 h window regardless of when midnight falls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Save host/port to plot_settings.json on connect; restore on startup
- Raise MAX_PLOT_POINTS from 200 to 2880 so 24 h of data is retained
even at 30 s polling intervals
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the single global toolbar spinbox with one QSpinBox per plot,
placed between the '+' button and the trace chips. Each plot has an
independent 1–24 h window; values are persisted in plot_settings.json
under 'time_windows'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A spinbox above the plot grid sets the visible x range for all 8 plots.
Data outside the window is excluded from each redraw; x axis is fixed to
[now - window, now]. Setting is persisted in plot_settings.json.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates .venv/ with python3 -m venv if not present, then installs/
updates requirements.txt into it before setting up the systemd service.
The service ExecStart always uses the venv Python.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates ~/.config/we_monitor/credentials.json template (mode 600),
writes ~/.config/systemd/user/we_monitor.service using the project
venv Python, enables linger for boot-time start, and enables the
service. Does not start until credentials are edited.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
README covers installation, collector CLI reference, push server protocol,
GUI client usage, and output format. conversation_recap documents the full
development history: features, bugs fixed, and design decisions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Color palette taken directly from nmea_client/gui/plot_widget.py:
- Background #1a1a2e / #12121e, borders #2a2a4a/#3a3a5e,
text #aaaacc/#ddddee, connected #4CAF50, error #F44336
- Trace colors: Material palette (blue, red, green, orange, …)
- Plot widget background overridden from "white" to #12121e
- pyqtgraph foreground set to #aaaacc
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Trace selection combos show short labels (_trace_label) with full key
stored as item data; width auto-adjusts to content (min 120 px)
- Extended plot grid from 4 to 8 (4×2) wrapped in QScrollArea
- Removed reset-zoom toolbar and cursor readout area
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
InfiniteLine was included in auto-range calculations, causing the view
to rescale on every cursor move. Add it with ignoreBounds=True in both
the initial setup and after each redraw.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace static combo grid with a control bar above each plot:
add-trace dropdown + "+" button; active traces shown as colored chips with
"×" to remove (max 4 per plot)
- Save trace selection to ~/.config/we_monitor/plot_settings.json on every
change; restore automatically once matching signals arrive from the server
- Remove tiny tick-font overrides so axis labels are readable at default size
- Show received record count in the status bar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously load_today() was only called if in-memory records were empty,
which was never true once the first WeConnect poll had completed.
Now disk records are loaded unconditionally at startup and merged with
live in-memory records (dedup by ts) when a client connects.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
storage_helpers: load_today_records() scans logs/ for all files
matching today's UTC date and VIN, merges and sorts their records.
network: _accept_loop receives an optional load_today callable;
if store_ref["current"] has no records it calls load_today() to
replay the day's history from disk before adding the client.
collect: passes a load_today lambda (None when -o is used).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removed the QScrollArea wrapper and fixed figure-height calculation.
FigureCanvasQTAgg now fills the tab with an Expanding size policy so
all 8 subplots fit in the visible area at 1920x1200 without scrolling.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
create_connection(timeout=5) left a 5-second read timeout on the
socket. Any collection interval longer than 5 s caused recv() to
raise socket.timeout, which was caught as OSError and reported as
a lost connection. Switch to blocking mode with settimeout(None)
immediately after the connect handshake.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
data_model, log_config, network, storage_helpers, we_connect are now
under server/. collect.py stays in the project root and imports via
the server package.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Connector tab: host/port fields, connect/disconnect button, status
- Dashboard tab: live QTreeWidget of the full snapshot hierarchy;
physical values show value and unit in separate columns;
expand state preserved across refreshes
- Plot tab: matplotlib time-series subplots (2 columns) for every
physical {value, unit} field, scrollable, auto-layout on new keys
- TcpReader runs in a daemon thread and delivers messages via
Qt signals so the UI never blocks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The server keeps the most recent snapshot in memory (under the same
lock as the client list). When a client connects it receives that
snapshot right away, then continues to receive live updates on each
collection cycle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
auto_out_path, load_store, save_store moved out of data_model.py into
their own module. data_model.py now contains only value helpers and
domain extractors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
we_connect.py now only handles WeConnect login and vehicle selection.
All extract_*() functions, ALL_DOMAINS, and collect_snapshot() live
in data_model.py alongside the store and value helpers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Connects to collect.py's push server and prints each incoming
snapshot as pretty-printed JSON, separated by a dashed line.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each connected TCP client receives every snapshot as a newline-
delimited JSON line the moment it is collected (no polling needed).
The server binds on --host/--port (default 0.0.0.0:9999).
Client connections are accepted in a daemon thread; dead connections
are pruned on the next broadcast.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When -o is not set, the collector detects a UTC date change at the
top of each collection cycle and opens a fresh timestamped file under
logs/. Explicit -o paths are never rotated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When -o is omitted, the output file is auto-named from the run's
start timestamp and the vehicle VIN and placed under ./logs/,
which is created on demand. logs/ is gitignored.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All fields with a physical unit are now emitted as
{"value": <number>, "unit": "<unit>"} instead of encoding the unit
in the field name. Field names are renamed accordingly (e.g.
chargePower_kW → chargePower). Battery temperature field names are
also fixed to reflect the already-applied K→°C conversion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>