TANK_DISTANZ_SENSOR + Datensatz sichern/laden mit echtem CSV-Import (Panel)
- Neuer optionaler Sensor TANK_DISTANZ_SENSOR ersetzt die eigene Kilometerstand-Subtraktion fuer die Tankvorgang-Strecke, wo zugeordnet (Live-Erkennung, manuelle Erfassung, historischer Import) - "Fahrzeugprofil" und "Daten ausgeben" zu einer Kachel zusammengelegt: "Datensatz sichern" (4 Exporte wie bisher) / "Datensatz laden" (neu: echter CSV-Import fuer Fahrten/Tankvorgaenge/Wartungsplan) - Import gleicht per ID ab (Fahrten/Tankvorgaenge) bzw. Datum+Art (Wartungsplan, hat keine eigene ID) und aktualisiert nur die in der CSV enthaltenen Spalten - alles andere am Datensatz bleibt unangetastet - Nebenbei gefunden und behoben: belege.tankvorgang_aktualisieren() ueberschrieb bisher immer alle Felder, auch mit None, wenn irgendeins geaendert wurde - fuer den CSV-Import gefaehrlich, jetzt nur noch tatsaechlich uebergebene Felder - Ebenfalls gefunden und behoben: Panel las das Import-Ergebnis per HASS.states direkt nach dem Dienstaufruf - ein Wettlauf mit dem state_changed-Push. Nutzt jetzt denselben HASS.callWS(get_states)-Weg wie der bestehende historie_importieren-Ablauf. companion-app-Portierung von "Datensatz sichern/laden" steht noch aus. Version 2026.8.28.3, live im Testcontainer verifiziert (alle drei CSV-Datensatztypen: anlegen + aktualisieren per ID/Datum+Art getestet, Testdaten danach geloescht). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user