diff --git a/AGENTS.md b/AGENTS.md index b7f63c8..1872893 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4004,21 +4004,98 @@ Archiv button and "Montiert" share one Y-center exactly, with a visible backgrou of the calculation" - not yet wired in, would mirror the `TANK_LITER_SENSOR` precedent (section Q): new optional sensor role, preferred over the calculation when mapped, calculation stays as the fallback when unmapped. Owner to confirm before this is built. -- **Item 9**: combine the "Fahrzeugprofil" tile (`data-profil="export"`/`"import"`, JSON) and "Daten ausgeben" - tile (`data-csv="fahrten"`/`"tanken"`/`"service"`, CSV export only, no import) into one area with two - buttons - "Datensatz sichern" opening a popup with 4 individual export actions (Fahrzeugprofil, - Fahrten/Tankvorgänge/Wartungsplan as CSV), "Datensatz laden" the same 4 in reverse. The pre-existing, - separate "Backup" tile (`data-backup="jetzt"`/`"export"`/`"import"`, a combined profil+fahrten+tank JSON - bundle plus the scheduled-backup mechanism) was NOT named by the owner and stays untouched. Three of the - four export actions and one of the four import actions already exist and just need moving into the new - popup; CSV **import** for Fahrten/Tankvorgänge/Wartungsplan does not exist anywhere yet and needs new - backend parsing (inverse of the existing CSV export format: `;`-delimited, de-DE number format). +- **Item 9: done on the panel, see section AC below - companion-app port still pending** (parity rule). - **Item 11**: Setup-Menü sensor role fields - replace the current description text under each field's headline (e.g. "on = Fahrt läuft", "Optional, vom CAN...") with the live sensor's current output value and unit inline next to the headline (`[on/off]`, `[%]`, `[l]`, `[km]`...), and show the matched entity's ID suffix (the part after `fmm003_...`) as "the expected sensor" - not yet investigated in the code (`entitaeten.py`'s `FELDER` catalog / the Setup popup's row-rendering in the panel). +## AC. Item 8 built (TANK_DISTANZ_SENSOR); Item 9 built on the panel: "Datensatz sichern/laden" (2026.8.28.3) + +**Item 8.** Owner: "you can use the sensor instead of our current logic. I guess it also helps for fuel +refill detection the best way." New optional `TANK_DISTANZ_SENSOR` (`einstellungen.py`, `FELDER` catalog, +group `fahrterkennung`) - the FMM003's own `calculated_distance_covered_after_the_latest_refueling`. New +shared helper `tankerkennung.distanz_seit_tankung(k, odometer_km)`: reads the sensor if mapped, else falls +back to the existing `ablage.distanz_seit_letzter_tankung()` - used at all three places a refuel's +`distance_km` gets set (`tankerkennung._automatisch_anlegen()`, `belege.tankvorgang_manuell()`'s +auto-suggestion, and `historienimport._tankvorgaenge_importieren()`, the last via `wert_bei()` on the +sensor's own fetched history). **The "helps with refill detection" half of the idea was investigated, not +built**: tried to read the sensor's recorder history in the test container to see whether it resets +synchronously with a real refuel (which would make it useful as a detection signal in its own right, not +just a value source) - the frontend's `history/period` REST call came back empty because it defaults to +significant-changes-only, unlike this project's own `verlauf.py` (`significant_changes_only=False`, +deliberately, for exactly this reason) - inconclusive, not investigated further. Confirmed live: entity +`sensor.testcar_b9_fmm003_testintegratoin_calculated_distance_covered_after_the_latest_refueling` exists in +`audi_ha_test` (reads `407` km); `py_compile` clean on all four touched backend files. + +**Item 9, panel only - companion-app not yet ported (parity rule, flagged, not silently skipped).** Merged +the "Fahrzeugprofil" tile and "Daten ausgeben" tile into one "Fahrzeugprofil" tile with two buttons - +"Datensatz sichern" (a popup with 4 instant, clientside export actions: Fahrzeugprofil as JSON, +Fahrten/Tankvorgänge/Wartungsplan as CSV - unchanged logic, just moved into the popup) and "Datensatz laden" +(the same 4 as file pickers). The separate "Backup" tile (combined profil+fahrten+tank JSON bundle plus the +scheduled-backup mechanism) was never named by the owner and is untouched. + +**The real new work is CSV import**, which didn't exist for any dataset before. Design question resolved via +`AskUserQuestion` before writing any code: re-importing an edited CSV **matches by ID and updates only the +CSV's own columns**, never a full-dataset replace - a full replace would have silently deleted every field +not in the CSV's few display columns (GPS, route, `edited_fields`, receipt links, ...) on every existing +record, even ones the owner never touched. New `csv_import.py` (own module, own moduledocstring with the +full reasoning): a `_de_zahl()` parser (`"1.234,5"` -> `1234.5`), a `_zeilen()` CSV reader (`;`-delimited, +BOM-tolerant), and one importer function per dataset, dispatched via `importieren(k, art, inhalt)`. New +service `csv_importieren(art, inhalt)` (`dienste.py`/`const.py`/`services.yaml`) - **synchronous**, not +backgrounded like `historie_importieren` (a CSV file is small, the whole point is that the frontend gets an +immediate, precise result). Result published through the existing `sensor.audi_dashboard_import_status` +entity (`k.import_status_veroeffentlichen("csv_fertig", {...})`) rather than inventing a second status +entity for what is, in HIG terms, the same kind of event. + +**Fahrten/Tankvorgänge have a real ID already (`trip_id`/`tank_id`) - now exported as a new first CSV +column.** Matched rows go through the existing, already-safe `fahrterkennung.aktualisieren()` (its +`_handfelder()` already drops `None` values, an untouched field was never at risk there) and +`belege.tankvorgang_aktualisieren()`. **The second one was not already safe** - found while designing this: +it unconditionally overwrote every one of its ~9 fields with `daten.get(x)`, `None` included, whenever any +one of them changed (a gap flagged but deliberately left alone during the earlier bug audit in this same +session, since nothing had needed the fix yet). CSV import needed it fixed for real, so it now only touches +keys actually present in `daten` (`"key" in daten`, not `daten.get(key)`) - verified this doesn't change the +existing manual-edit-form caller's behavior, since that caller already sends every key on every save +regardless of whether the value is empty. + +**Wartungsplan has no ID at all** - entries live as a plain list inside the vehicle profile +(`profil["service"]["buch"]`), addressed elsewhere in the app only by list index (`vSbuch()`/`Servicebuch()` +in both frontends already work this way). Retrofitting a real ID was judged bigger than this feature +warranted (touches delete/edit routing in both codebases). CSV import for this dataset instead matches on +**Datum+Art** as a natural key - documented in `csv_import.py`'s own docstring as a known, accepted +limitation: editing either of those two columns for an existing row makes it unmatchable and creates a +duplicate instead of updating. + +Verified end-to-end, live, in the browser against `audi_ha_test` (not just compiled/typechecked - this +file's own section S lesson) - created and then updated one record of each of the three CSV-backed types via +synthetic `File`/`DataTransfer` dispatch through the real popup/file-input flow, confirmed via the actual +published entity data each time, then deleted the test records afterward: +- Fahrten: created (`1 neu angelegt`), then updated the same `trip_id` (`1 aktualisiert`, `distance_km` + 22→60) while `odo_end`/`start_lat`/`route`/`source` all stayed byte-identical - the core guarantee, proven, + not assumed. +- Tankvorgänge: created with de-DE-formatted `"45,00"`/`"1,75"`/`"78,75"` (parsed correctly to `45`/`1.75`/ + `78.75`), then updated `station_name` by `tank_id` while `odometer_km` stayed untouched - the exact case + `belege.tankvorgang_aktualisieren()`'s fix was for. +- Wartungsplan: created, then updated by Datum+Art match (`km`/`Werkstatt`/`Kosten` changed, `buchLength` + stayed 1 - no duplicate). + +**One real bug found and fixed during this live testing, not by code review**: the first version read the +result via `HASS.states[E.importStatus]` immediately inside the service call's `.then()` - a race, since a +`state_changed` push arrives over its own websocket message and isn't guaranteed to have landed in the +frontend's local state cache by the time the service-call promise resolves. Reproduced live: the popup said +"Keine gültigen Zeilen gefunden" even though the backend had genuinely created the record (confirmed via the +entity's own fresh state). Fixed by switching to the existing `importStatusLesen()` helper (already used by +`importAusloesen()` for `historie_importieren`, for the identical reason) - it fetches fresh via +`HASS.callWS({type:"get_states"})`, bypassing the local cache race entirely. Re-tested after the fix: +correct result every time. + +Verified: `node --check` clean on the panel; companion-app untouched, `tsc --noEmit` still clean. Manifest +bumped `2026.8.28.1` → `.2` (initial popup) → `.3` (the race-condition fix - a second restart was needed +specifically to see this fix, since the panel JS is cache-busted by manifest version, not by content hash). +`audi_ha_test` restarted at each step, confirmed clean via log every time. + --- ## Working conventions (observed — keep them) diff --git a/custom_components/audi_dashboard/belege.py b/custom_components/audi_dashboard/belege.py index 6909f17..081f124 100644 --- a/custom_components/audi_dashboard/belege.py +++ b/custom_components/audi_dashboard/belege.py @@ -33,6 +33,7 @@ import os from typing import TYPE_CHECKING from . import shell_beleg_parser +from . import tankerkennung from .tankerkennung import leerer_tankvorgang from .veroeffentlichung import zustand_oder_none from .verlauf import zeit_normalisiert @@ -195,10 +196,10 @@ async def tankvorgang_manuell(k: Koordinator, daten: dict) -> None: odometer_km = km if km is not None else _km_aus_sensor(k) distanz = daten.get("distanz") if distanz is None: - # Vorschlag aus der Differenz zum letzten Tankvorgang, aber wie - # odometer_km selbst frei überschreibbar - ein übergebener Wert hat - # deshalb immer Vorrang vor der Berechnung. - distanz = await k.ablage.distanz_seit_letzter_tankung(odometer_km) + # Vorschlag aus TANK_DISTANZ_SENSOR oder, ohne ihn, der Differenz zum + # letzten Tankvorgang - aber wie odometer_km selbst frei + # überschreibbar, ein übergebener Wert hat deshalb immer Vorrang. + distanz = await tankerkennung.distanz_seit_tankung(k, odometer_km) ts_roh = daten.get("ts") ts = zeit_normalisiert(ts_roh) if ts_roh else datetime.datetime.now(datetime.UTC) @@ -236,26 +237,41 @@ def _km_aus_sensor(k: Koordinator) -> float | None: async def tankvorgang_aktualisieren(k: Koordinator, daten: dict) -> None: """Bearbeitet einen bestehenden Tankvorgang (Einzelbeleg-Seite, egal ob - ursprünglich per Beleg oder manuell angelegt).""" + ursprünglich per Beleg oder manuell angelegt). + + Nur tatsächlich in `daten` enthaltene Schlüssel werden geändert - fehlt + ein Feld ganz (statt nur leer/None zu sein), bleibt der bisherige Wert + unangetastet. Das bestehende Bearbeiten-Formular (Einzelbeleg-Seite) + schickt ohnehin immer alle Felder mit, verhält sich also unverändert; + ein schlankerer Aufrufer wie der CSV-Import (csv_import.py) kann so + gezielt nur einzelne Spalten aktualisieren, ohne den Rest des + Datensatzes (odometer_km, discount, fuel_type, receipt_key, ...) + stillschweigend auf None zu setzen.""" tank_id = daten.get("tank_id") if not tank_id: _LOGGER.warning("tankvorgang_aktualisieren ohne tank_id aufgerufen") return - liter = daten.get("liter") - kosten = daten.get("kosten") - ts_normalisiert = zeit_normalisiert(daten.get("ts")) - aenderungen = { - "ts": ts_normalisiert.isoformat() if ts_normalisiert else daten.get("ts"), - "liters": liter, - "fuel_total_eur": kosten, - "price_per_l": preis_je_liter(kosten, liter), - "odometer_km": daten.get("km"), - "discount": daten.get("ersparnis"), - "station_name": daten.get("station"), - "distance_km": daten.get("distanz"), - "fuel_type": daten.get("kraftstoff"), - } + aenderungen: dict = {} + if "ts" in daten: + ts_normalisiert = zeit_normalisiert(daten.get("ts")) + aenderungen["ts"] = ts_normalisiert.isoformat() if ts_normalisiert else daten.get("ts") + if "liter" in daten or "kosten" in daten: + liter = daten.get("liter") + kosten = daten.get("kosten") + aenderungen["liters"] = liter + aenderungen["fuel_total_eur"] = kosten + aenderungen["price_per_l"] = preis_je_liter(kosten, liter) + if "km" in daten: + aenderungen["odometer_km"] = daten.get("km") + if "ersparnis" in daten: + aenderungen["discount"] = daten.get("ersparnis") + if "station" in daten: + aenderungen["station_name"] = daten.get("station") + if "distanz" in daten: + aenderungen["distance_km"] = daten.get("distanz") + if "kraftstoff" in daten: + aenderungen["fuel_type"] = daten.get("kraftstoff") if daten.get("receipt_key"): aenderungen["receipt_key"] = daten["receipt_key"] aenderungen["receipt_file"] = daten.get("receipt_file") diff --git a/custom_components/audi_dashboard/const.py b/custom_components/audi_dashboard/const.py index 797fade..3d5d536 100644 --- a/custom_components/audi_dashboard/const.py +++ b/custom_components/audi_dashboard/const.py @@ -93,6 +93,7 @@ D_REIFEN_ARCHIV_LOESCHEN = "reifen_archiv_loeschen" D_BACKUP_JETZT = "backup_jetzt" D_BACKUP_WIEDERHERSTELLEN = "backup_wiederherstellen" D_HISTORIE_IMPORTIEREN = "historie_importieren" +D_CSV_IMPORTIEREN = "csv_importieren" D_UPDATE_PRUEFEN = "update_pruefen" D_UPDATE_INSTALLIEREN = "update_installieren" diff --git a/custom_components/audi_dashboard/csv_import.py b/custom_components/audi_dashboard/csv_import.py new file mode 100644 index 0000000..8a15923 --- /dev/null +++ b/custom_components/audi_dashboard/csv_import.py @@ -0,0 +1,179 @@ +"""CSV-Import für Fahrten, Tankvorgänge und Wartungsplan (Einstellungen -> +"Datensatz laden") - das Gegenstück zum bestehenden, rein clientseitigen +CSV-Export (beide Frontends: der "Datensatz sichern"-Popup baut die Datei +direkt im Browser aus den schon geladenen Daten, ohne Dienstaufruf). + +Format identisch zum Export: Semikolon als Trennzeichen, deutsches +Zahlenformat ("," als Dezimaltrennzeichen, optional "." als +Tausendertrennzeichen), UTF-8 mit BOM (wird beim Lesen übersprungen). + +ABGLEICH STATT VOLLSTÄNDIGEM ERSATZ (Nutzerentscheidung): eine Zeile mit +bekannter ID aktualisiert NUR die in der CSV enthaltenen Spalten am +bestehenden Datensatz - alles andere (GPS, Route, edited_fields, +Beleg-Verknüpfung, ...) bleibt unangetastet, siehe +`belege.tankvorgang_aktualisieren()`s eigene Begründung. Eine Zeile ohne +oder mit unbekannter ID legt einen neuen Datensatz an - so lässt sich auch +eine von Hand in der Tabelle ergänzte Zeile importieren. + +Wartungsplan hat, anders als Fahrten/Tankvorgänge, keine eigene ID - die +Einträge leben als einfache Liste im Fahrzeugprofil und werden bisher nur +über ihren Listenindex adressiert (siehe vSbuch()/Servicebuch() in beiden +Frontends). Der Abgleich läuft dort stattdessen über Datum+Art als +Ersatzschlüssel - eine Zeile, die eines von beidem ändert, wird deshalb +nicht wiedererkannt und legt einen neuen Eintrag an, statt den alten zu +aktualisieren. Für die kleinen, von Hand gepflegten Wartungsplan-Listen +dieses Projekts ist das ein bewusst akzeptierter Kompromiss gegenüber einer +eigens für diesen Zweck eingeführten ID. +""" + +from __future__ import annotations + +import csv +import io +import logging +from typing import TYPE_CHECKING + +from . import belege, fahrterkennung + +if TYPE_CHECKING: + from .koordinator import Koordinator + +_LOGGER = logging.getLogger(__name__) + + +def _de_zahl(text: str | None) -> float | None: + """"1.234,5" -> 1234.5. None/leer -> None (Spalte nicht gesetzt, nicht + "auf 0 setzen").""" + text = (text or "").strip() + if not text: + return None + try: + return float(text.replace(".", "").replace(",", ".")) + except ValueError: + return None + + +def _zeilen(inhalt: str) -> list[dict[str, str]]: + """CSV-Text (inkl. Kopfzeile, optional mit BOM) als Liste von + Feldname->Text-Wörterbüchern, in der Reihenfolge der Kopfzeile. Ganz + leere Zeilen (z. B. eine Leerzeile am Dateiende) werden übersprungen.""" + bereinigt = inhalt.lstrip("") + zeilen = list(csv.reader(io.StringIO(bereinigt), delimiter=";")) + if not zeilen: + return [] + kopf = zeilen[0] + return [dict(zip(kopf, zeile)) for zeile in zeilen[1:] if any(f.strip() for f in zeile)] + + +async def fahrten_importieren(k: Koordinator, inhalt: str) -> dict: + bestehende = {f["trip_id"] for f in await k.ablage.fahrten_lesen()} + aktualisiert = angelegt = uebersprungen = 0 + for zeile in _zeilen(inhalt): + trip_id = (zeile.get("ID") or "").strip() + start = zeile.get("Start") or None + ende = zeile.get("Ende") or None + art = zeile.get("Art") or None + km = _de_zahl(zeile.get("km")) + if trip_id and trip_id in bestehende: + await fahrterkennung.aktualisieren(k, { + "trip_id": trip_id, "ts_start": start, "ts_end": ende, + "art": art, "distanz": km, + }) + aktualisiert += 1 + elif start and ende: + await fahrterkennung.manuell_anlegen(k, { + "ts_start": start, "ts_end": ende, "art": art, "distanz": km, + }) + angelegt += 1 + else: + _LOGGER.warning("CSV-Zeile (Fahrten) ohne Start/Ende übersprungen: %s", zeile) + uebersprungen += 1 + return {"aktualisiert": aktualisiert, "angelegt": angelegt, "uebersprungen": uebersprungen} + + +async def tankvorgaenge_importieren(k: Koordinator, inhalt: str) -> dict: + bestehende = {t["tank_id"] for t in await k.ablage.tankvorgaenge_lesen()} + aktualisiert = angelegt = uebersprungen = 0 + for zeile in _zeilen(inhalt): + tank_id = (zeile.get("ID") or "").strip() + ts = zeile.get("Zeitpunkt") or None + station = zeile.get("Station") or None + liter = _de_zahl(zeile.get("Liter")) + kosten = _de_zahl(zeile.get("Kosten €")) + if tank_id and tank_id in bestehende: + daten: dict = {"tank_id": tank_id} + if ts is not None: + daten["ts"] = ts + if liter is not None or kosten is not None: + daten["liter"] = liter + daten["kosten"] = kosten + if station is not None: + daten["station"] = station + await belege.tankvorgang_aktualisieren(k, daten) + aktualisiert += 1 + elif ts and liter is not None and kosten is not None: + await belege.tankvorgang_manuell(k, { + "ts": ts, "liter": liter, "kosten": kosten, "station": station, + }) + angelegt += 1 + else: + _LOGGER.warning( + "CSV-Zeile (Tankvorgänge) ohne Zeitpunkt/Liter/Kosten übersprungen: %s", zeile + ) + uebersprungen += 1 + return {"aktualisiert": aktualisiert, "angelegt": angelegt, "uebersprungen": uebersprungen} + + +async def wartungsplan_importieren(k: Koordinator, inhalt: str) -> dict: + profil = await k.ablage.profil_lesen() + if profil is None: + return {"aktualisiert": 0, "angelegt": 0, "uebersprungen": 0} + buch = profil.setdefault("service", {}).setdefault("buch", []) + aktualisiert = angelegt = uebersprungen = 0 + for zeile in _zeilen(inhalt): + datum = zeile.get("Datum") or None + art = zeile.get("Art") or None + if not datum or not art: + _LOGGER.warning("CSV-Zeile (Wartungsplan) ohne Datum/Art übersprungen: %s", zeile) + uebersprungen += 1 + continue + km = _de_zahl(zeile.get("km")) + werkstatt = zeile.get("Werkstatt") or None + kosten = _de_zahl(zeile.get("Kosten €")) + treffer = next( + (e for e in buch if e.get("datum") == datum and e.get("art") == art), None + ) + if treffer is not None: + if km is not None: + treffer["km"] = km + if werkstatt is not None: + treffer["werkstatt"] = werkstatt + if kosten is not None: + treffer["kosten"] = kosten + aktualisiert += 1 + else: + buch.append({ + "datum": datum, "km": km or 0, "art": art, + "werkstatt": werkstatt or "", "kosten": kosten, "arbeiten": "", + }) + angelegt += 1 + await k.ablage.profil_schreiben(profil) + await k.profil_veroeffentlichen() + return {"aktualisiert": aktualisiert, "angelegt": angelegt, "uebersprungen": uebersprungen} + + +IMPORTIERER = { + "fahrten": fahrten_importieren, + "tanken": tankvorgaenge_importieren, + "service": wartungsplan_importieren, +} + + +async def importieren(k: Koordinator, art: str, inhalt: str) -> dict: + funktion = IMPORTIERER.get(art) + if funktion is None: + _LOGGER.warning("csv_importieren mit unbekannter Art aufgerufen: %s", art) + return {"aktualisiert": 0, "angelegt": 0, "uebersprungen": 0} + ergebnis = await funktion(k, inhalt) + _LOGGER.info("CSV-Import (%s) abgeschlossen - %s", art, ergebnis) + return ergebnis diff --git a/custom_components/audi_dashboard/dienste.py b/custom_components/audi_dashboard/dienste.py index 480f0e3..851e529 100644 --- a/custom_components/audi_dashboard/dienste.py +++ b/custom_components/audi_dashboard/dienste.py @@ -31,6 +31,7 @@ from . import ( batterie, belege, bilder, + csv_import, fahrterkennung, historienimport, reifen, @@ -46,6 +47,7 @@ from .const import ( D_BELEG_HOCHLADEN, D_BILD_HOCHLADEN, D_BILD_LOESCHEN, + D_CSV_IMPORTIEREN, D_ENTITAETEN_SCHREIBEN, D_FAHRT_AKTUALISIEREN, D_FAHRT_LOESCHEN, @@ -165,6 +167,10 @@ SCHEMATA: dict[str, vol.Schema] = { vol.Required("start"): cv.string, vol.Required("ende"): cv.string, }), + D_CSV_IMPORTIEREN: vol.Schema({ + vol.Required("art"): vol.In(["fahrten", "tanken", "service"]), + vol.Required("inhalt"): cv.string, + }), D_UPDATE_PRUEFEN: vol.Schema({}), D_UPDATE_INSTALLIEREN: vol.Schema({}), } @@ -243,6 +249,15 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None: "audi_dashboard_historienimport", ) + async def csv_importieren(aufruf: ServiceCall) -> None: + """Liest eine per "Datensatz laden" hochgeladene CSV-Datei ein (siehe + csv_import.py). Läuft synchron statt im Hintergrund wie + historie_importieren - eine einzelne CSV-Datei ist klein, der Aufruf + kehrt erst zurück, wenn das Ergebnis auch wirklich in + sensor.audi_dashboard_import_status steht, kein Abfragen nötig.""" + ergebnis = await csv_import.importieren(k, aufruf.data["art"], aufruf.data["inhalt"]) + await k.import_status_veroeffentlichen("csv_fertig", {"art": aufruf.data["art"], **ergebnis}) + async def update_pruefen(_aufruf: ServiceCall) -> None: """Fragt bei Gitea nach, ob eine neuere Fassung vorliegt - ohne etwas herunterzuladen. Läuft nur auf Tastendruck, siehe @@ -311,6 +326,7 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None: D_PROFIL_SCHREIBEN: profil_schreiben, D_ENTITAETEN_SCHREIBEN: entitaeten_schreiben, D_HISTORIE_IMPORTIEREN: historie_importieren, + D_CSV_IMPORTIEREN: csv_importieren, D_SCREENING_JETZT: _als_dienst(lambda _a: screening.durchfuehren(k)), D_FAHRT_MANUELL_ANLEGEN: _als_dienst( lambda a: fahrterkennung.manuell_anlegen(k, dict(a.data)) diff --git a/custom_components/audi_dashboard/einstellungen.py b/custom_components/audi_dashboard/einstellungen.py index ad7ce88..bf51b92 100644 --- a/custom_components/audi_dashboard/einstellungen.py +++ b/custom_components/audi_dashboard/einstellungen.py @@ -72,6 +72,16 @@ class Sensorzuordnung: # Vorbelegung für die getankte Menge (siehe tankerkennung.py). TANK_LITER_SENSOR: str = "" + # Vom Fahrzeug selbst berechnete Strecke seit der letzten Tankung + # (FMM003: "calculated distance covered after the latest refueling"). + # Optional - ist er gesetzt, übernimmt sein aktueller Wert das Feld + # distance_km eines neu erkannten Tankvorgangs anstelle der eigenen + # Kilometerstand-Subtraktion (ablage.distanz_seit_letzter_tankung()) - + # direkt vom Fahrzeug gemeldet statt selbst gerechnet, und robust gegen + # Lücken im eigenen Bestand. Ohne ihn bleibt die bisherige Berechnung + # unverändert der Rückfall. + TANK_DISTANZ_SENSOR: str = "" + # Reichweite (Übersicht). RANGE_SENSOR: str = "" @@ -163,6 +173,11 @@ FELDER: list[dict] = [ "Tankerkennung genauer und liefert eine Vorbelegung für die getankte Menge.", "domains": ["sensor"], "device_classes": ["volume"], "units": ["L", "l"], "liste": False, "pflicht": False, "stichworte": ["tankvolumen", "liter", "litres", "fuel volume", "kraftstoffvolumen"]}, + {"key": "TANK_DISTANZ_SENSOR", "label": "Strecke seit letzter Tankung", "gruppe": "fahrterkennung", + "hinweis": "Optional, vom Fahrzeug selbst berechnet - ersetzt die eigene Kilometerstand-Rechnung " + "für die Strecke eines Tankvorgangs.", + "domains": ["sensor"], "device_classes": ["distance"], "units": ["km", "mi"], "liste": False, "pflicht": False, + "stichworte": ["strecke tankung", "distance refuel", "distance since refueling", "refuelling"]}, {"key": "RANGE_SENSOR", "label": "Reichweite", "gruppe": "uebersicht", "hinweis": "Für die Übersicht - bleibt sie leer, zeigt die Oberfläche \"unbekannt\".", "domains": ["sensor"], "device_classes": ["distance"], "units": ["km", "mi"], "liste": False, "pflicht": False, diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js index f2aef41..d9bd9ca 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js +++ b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js @@ -2622,11 +2622,12 @@ function vEinst() { Alles Fahrzeugspezifische liegt in einer einzigen Datei: Stammdaten, technische Daten, - Ausstattung, Reifen, Versicherung, Steuer und Werkstatt. Für ein anderes Fahrzeug wird nur - diese Datei getauscht — die App selbst bleibt unverändert. - - + Ausstattung, Reifen, Versicherung, Steuer und Werkstatt. Fahrten, Tankvorgänge und der + Wartungsplan lassen sich einzeln als CSV sichern und bearbeitet wieder einspielen — eine + Zeile mit bekannter ID aktualisiert nur ihre eigenen Spalten, alles andere bleibt + unangetastet. + +