Commit Graph
81 Commits
Author SHA1 Message Date
jensandClaude Sonnet 4.6 8ff9463e76 server: install volkswagencarnet from GitHub + apply auth patch
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>
2026-05-28 23:03:44 +02:00
jensandClaude Sonnet 4.6 bc424310b5 server: replace carconnectivity with volkswagencarnet
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>
2026-05-28 22:50:57 +02:00
jensandClaude Sonnet 4.6 63e807bdb0 data_model: split windows out of doors into own domain
- 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>
2026-05-27 21:53:26 +02:00
jensandClaude Sonnet 4.6 4dcf2ee6aa data_model: fix extract_doors — filter None values, fix structure
- 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>
2026-05-27 21:40:47 +02:00
jensandClaude Sonnet 4.6 0b68bcdef8 server/main.py: use absolute imports for collect and data_model
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:28:40 +02:00
jensandClaude Sonnet 4.6 26904d9afc gui_client: fix dashboard dropping plain-value fields
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>
2026-05-27 20:27:52 +02:00
jensandClaude Sonnet 4.6 99b34f0b5e gitignore: add .venv/ and client/run.sh
run.sh is generated by install.sh with machine-specific absolute paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 19:00:23 +02:00
jensandClaude Sonnet 4.6 53004e5a2d Update README: we_monitor_gui system command after client install
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 18:57:15 +02:00
jensandClaude Sonnet 4.6 49bc272461 client/install.sh: install we_monitor_gui command to ~/.local/bin
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 18:55:14 +02:00
jensandClaude Sonnet 4.6 b5c7e66c94 Update README: add client/install.sh and run.sh to layout and Client section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 18:53:09 +02:00
jensandClaude Sonnet 4.6 ff6a43746a Add client/install.sh; fix server/install.sh paths after move
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>
2026-05-27 18:48:34 +02:00
jensandClaude Sonnet 4.6 5b1de48b75 Update README for server/client split
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>
2026-05-27 18:35:03 +02:00
jensandClaude Sonnet 4.6 ca459c5ce9 Move install.sh to server/; clarify it is server-only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 18:33:41 +02:00
jensandClaude Sonnet 4.6 5230b304a2 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>
2026-05-27 18:30:39 +02:00
jensandClaude Sonnet 4.6 423e6d3027 Split into server/ and client/ with separate requirements.txt
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>
2026-05-27 18:26:19 +02:00
jensandClaude Sonnet 4.6 cc2c52a827 Expand abbreviated trace labels for clarity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 16:30:58 +02:00
jens eea4bdc3ba Merge branch 'lib_carconnectivity'
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.
2026-05-27 14:01:51 +02:00
jensandClaude Sonnet 4.6 205e6fd5f3 Remove README.jaydiff.md and now-empty utils/ and tests/ packages
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>
2026-05-27 14:00:32 +02:00
jensandClaude Sonnet 4.6 2f6556116e Replace utils/jay_diff.py with jaydiff package from jayfield/jaypy
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>
2026-05-27 13:56:45 +02:00
jensandClaude Sonnet 4.6 12e4620c41 Add README.jaydiff.md — jay_diff library documentation
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>
2026-05-27 12:29:51 +02:00
jensandClaude Sonnet 4.6 c8de234423 jay_diff: fix two correctness bugs + add real-data round-trip tests
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>
2026-05-27 12:25:35 +02:00
jensandClaude Sonnet 4.6 5ef7f13118 tests: add test suite for jay_diff; fix two bugs found
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>
2026-05-27 11:54:57 +02:00
jensandClaude Sonnet 4.6 4492692786 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>
2026-05-27 11:43:41 +02:00
jensandClaude Sonnet 4.6 bf33737654 data_model: redesign domain extractors for CarConnectivity object model
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>
2026-05-27 09:01:15 +02:00
jensandClaude Sonnet 4.6 f050de7001 collect: migrate from weconnect to CarConnectivity + VW connector
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>
2026-05-27 08:02:34 +02:00
jensandClaude Sonnet 4.6 ef7b99e7e8 collect: re-login on AuthentificationError after weconnect exhausts its own retry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 20:29:08 +02:00
jensandClaude Sonnet 4.6 cd8367e5d0 Dashboard: fix display of flat phys fields (procedural domain)
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>
2026-05-26 17:52:32 +02:00
jensandClaude Sonnet 4.6 cdc01aaa76 data_model: implement range_at_100 procedural field
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>
2026-05-26 17:49:12 +02:00
jensandClaude Sonnet 4.6 2756c6a49b Add procedural field post-processing to snapshots
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>
2026-05-26 17:35:10 +02:00
jensandClaude Sonnet 4.6 e6c9f7d687 Plot tooltip: show cursor timestamp (HH:MM:SS) above trace values
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>
2026-05-26 17:05:13 +02:00
jensandClaude Sonnet 4.6 55719010da Docs: full README rewrite + gui_client.py module docstring update
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>
2026-05-26 17:01:42 +02:00
jensandClaude Sonnet 4.6 d0e4ec374c Plot tab: axis-selective wheel zoom (X default, Shift+wheel for Y)
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>
2026-05-26 16:56:02 +02:00
jensandClaude Sonnet 4.6 ad3cbf4859 Plot tab: auto-zoom Y axis on first draw after connect / trace change
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>
2026-05-26 16:50:14 +02:00
jensandClaude Sonnet 4.6 849473acc0 Plot tab: floating tooltip showing trace values at crosshair position
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>
2026-05-26 16:42:32 +02:00
jensandClaude Sonnet 4.6 5e2aa3f17d Plot tab: HH:MM:SS x-axis ticks, date label in control bar
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>
2026-05-26 16:36:57 +02:00
jensandClaude Sonnet 4.6 620d22d2e4 jay_merge_full: handle non-jaydiff input (full snapshot passthrough)
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>
2026-05-26 16:16:33 +02:00
jensandClaude Sonnet 4.6 ca5b284e9a Remove _diff marker: use self-describing jay_diff protocol
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>
2026-05-26 16:08:33 +02:00
jensandClaude Sonnet 4.6 a4ce702ee4 install.sh: support update mode; auto-restart if service was active
- 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>
2026-05-26 15:50:06 +02:00
jensandClaude Sonnet 4.6 5944b0a7a4 collect: add diff to credentials file; --diff overrides it
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>
2026-05-26 15:09:54 +02:00
jensandClaude Sonnet 4.6 c33236020d install.sh: update credentials template to current format
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>
2026-05-26 14:58:11 +02:00
jensandClaude Sonnet 4.6 9951ef69e4 README: update CLI reference and add diff mode section
- 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>
2026-05-26 14:56:53 +02:00
jensandClaude Sonnet 4.6 d53202822e collect: expose interval, host, port, log-dir as optional CLI overrides
-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>
2026-05-26 14:55:30 +02:00
jensandClaude Sonnet 4.6 2adebf82ca gui_client: fix diff mode display corruption via deepcopy on emit
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>
2026-05-26 14:45:09 +02:00
jensandClaude Sonnet 4.6 56ed4ac265 Add opt-in diff mode for network transmission (--diff / Diff mode checkbox)
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>
2026-05-26 14:34:34 +02:00
jensandClaude Sonnet 4.6 d4cbe110d6 gui_client: debounce plot redraws to speed up initial record import
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>
2026-05-26 14:07:50 +02:00
jensandClaude Sonnet 4.6 bb1e087867 collect: add --dry flag to run push server without WeConnect
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>
2026-05-26 13:59:58 +02:00
jensandClaude Sonnet 4.6 19399ca73f README: update for credentials-file config and recent GUI changes
- 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>
2026-05-26 13:54:17 +02:00
jensandClaude Sonnet 4.6 bedf5ed646 collect: move vin, interval, host, port, log_dir to credentials file
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>
2026-05-26 13:52:06 +02:00
jensandClaude Sonnet 4.6 89810ce95b gui_client: clear plot data on connect/reconnect
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>
2026-05-26 13:36:26 +02:00
jensandClaude Sonnet 4.6 efc21763a3 gui_client: fit x-axis to actual data when less than window size
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>
2026-05-26 13:31:40 +02:00