diff --git a/server/collect.py b/server/collect.py index d539781..664791f 100644 --- a/server/collect.py +++ b/server/collect.py @@ -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) diff --git a/server/data_model.py b/server/data_model.py index 199805f..4064670 100644 --- a/server/data_model.py +++ b/server/data_model.py @@ -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,