data_model: add extract_all() using Dashboard instruments as primary snapshot

Replaces the domain-based collect_snapshot with extract_all(), which iterates
all supported instruments via vw_dashboard.Dashboard. Each entry is stored as
{value, unit, str_state} grouped by component type. Units are validated against
a whitelist and normalised (d→days, °C→degC); unrecognised units are dropped.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 20:55:07 +02:00
co-authored by Claude Sonnet 4.6
parent f9dd96269e
commit 6e340a9582
2 changed files with 71 additions and 2 deletions
+2 -2
View File
@@ -6,7 +6,7 @@ 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 .data_model import ALL_DOMAINS, apply_procedural, extract_all
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
@@ -85,7 +85,7 @@ def run(args: argparse.Namespace) -> None:
try:
we_connect.update(wc)
snapshot = collect_snapshot(vehicle, domains)
snapshot = extract_all(vehicle)
apply_procedural(snapshot)
store["records"].append(snapshot)
save_store(out, store, args.max_records)
+69
View File
@@ -1,8 +1,10 @@
import logging
import re
from collections.abc import Callable
from datetime import datetime, timezone
from volkswagencarnet.vw_const import Paths
from volkswagencarnet.vw_dashboard import Dashboard
from volkswagencarnet.vw_utilities import find_path
log = logging.getLogger(__name__)
@@ -283,6 +285,73 @@ def extract_windows(vehicle) -> dict | None:
return result or None
_UNIT_WHITELIST: frozenset[str] = frozenset({"min", "%", "d", "kW", "°C", "km", "V", "W", "A"})
_UNIT_CONVERSIONS: dict[str, str] = {"d": "days", "°C": "degC"}
def _normalise_unit(unit: str) -> str:
if unit not in _UNIT_WHITELIST:
return ""
return _UNIT_CONVERSIONS.get(unit, unit)
def _unit_from_str_state(str_state: str, state) -> str:
"""Parse the unit suffix from a str_state like '42 %' or '21.5 degC'."""
state_repr = str(state)
if str_state.startswith(state_repr):
suffix = str_state[len(state_repr):].strip()
if suffix:
return suffix
# fallback: last whitespace-separated token if it looks like a unit
parts = str_state.rsplit(None, 1)
if len(parts) == 2 and re.fullmatch(r"[^\d\s]\S*", parts[1]):
return parts[1]
return ""
def extract_all(vehicle) -> dict | None:
"""Read every supported instrument via the Dashboard and return a flat snapshot.
Structure: snapshot[component][attr] = {"value": state, "unit": unit, "str_state": str_state}
Unit is taken from instrument.unit when available, otherwise parsed from str_state.
"""
snapshot: dict = {"ts": datetime.now(timezone.utc).isoformat()}
try:
dashboard = Dashboard(vehicle)
except Exception:
log.debug("extract_all: Dashboard setup failed", exc_info=True)
return None
for instrument in dashboard.instruments:
try:
state = instrument.state
if state is None:
continue
try:
str_state = str(instrument.str_state)
except Exception:
str_state = str(state)
if hasattr(instrument, "unit") and instrument.unit:
raw_unit = instrument.unit
else:
raw_unit = _unit_from_str_state(str_state, state)
unit = _normalise_unit(raw_unit)
component = instrument.component
attr = instrument.attr
snapshot.setdefault(component, {})[attr] = {
"value": state,
"unit": unit,
"str_state": str_state,
}
except Exception:
log.debug("extract_all: %s unavailable", instrument.attr, exc_info=True)
return snapshot or None
ALL_DOMAINS: dict[str, Callable] = {
"charging": extract_charging,
"climatisation": extract_climatisation,