From b1cd03be51bed9575b4d94f1d1d514eaf51cdf60 Mon Sep 17 00:00:00 2001 From: Tobi G Date: Fri, 28 Aug 2026 10:38:27 +0200 Subject: [PATCH] Wartungsplan-Schema companion-app<->Panel angeglichen; Setup-Feldkopf zeigt zugeordneten Sensor companion-app nannte Servicebuch-Feldnamen betrieb/notiz, waehrend Panel und Backend (profil["service"]["buch"]) werkstatt/kosten/arbeiten verwenden - ohne Uebersetzung dazwischen zeigten in einer Oberflaeche angelegte Eintraege in der anderen leere Werkstatt-/Kosten-Werte. werkstatt/kosten/ arbeiten als kanonisch uebernommen (aeltere, bereits etablierte Panel- Konvention); ServicebuchEintrag, das Bearbeitungsformular (inkl. neuem Kosten-Feld) und die Ansicht in Service.tsx angepasst, den Workaround-Cast im CSV-Export (DatensatzPopup.tsx) sowie die Testfixture entsprechend bereinigt. Setup-Popup (Panel, Item 11 aus der letzten Sammel-Rueckmeldung): die statische Beschreibung unter jedem Feldnamen ist jetzt die Kennung und der aktuelle Wert des tatsaechlich zugeordneten Sensors. Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 88 +++++++++++++++++-- companion-app/src/screens/DatensatzPopup.tsx | 23 ++--- companion-app/src/screens/Service.tsx | 37 +++++--- companion-app/src/tests/beispieldaten.ts | 4 +- .../frontend/audi-dashboard-app.js | 42 ++++++++- .../frontend/audi-dashboard.css | 13 ++- .../audi_dashboard/manifest.json | 2 +- 7 files changed, 166 insertions(+), 43 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8bdbbf1..503a5e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4001,15 +4001,11 @@ Archiv button and "Montiert" share one Y-center exactly, with a visible backgrou derived calculation, not read from any vehicle sensor. Owner pointed out a real FMM003 sensor, `sensor.testzone_fmm003_calculated_distance_covered_after_the_latest_refueling` (device-side, presumably more accurate/authoritative than our own subtraction), that reports this directly and "can be used instead - 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: 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). + of the calculation" - **done, see section AC below** (`TANK_DISTANZ_SENSOR`). +- **Item 9: done, both codebases** - panel in section AC, companion-app port in section AD. +- **Item 11: done, panel only (no companion-app equivalent, see section AF)** - Setup-Menü sensor role + fields now show the mapped sensor's ID suffix and live value next to the headline instead of a static + description. ## AC. Item 8 built (TANK_DISTANZ_SENSOR); Item 9 built on the panel: "Datensatz sichern/laden" (2026.8.28.3) @@ -4147,6 +4143,80 @@ data" smoke test, which covers `Einstellungen.tsx`), `vite build` succeeds. **No running companion-app session with backend auth this round, the same structural limitation noted repeatedly elsewhere in this file for companion-app-only changes. +## AE. Wartungsplan schema mismatch between panel and companion-app fixed (2026-08-28) + +The pre-existing schema mismatch flagged in section AD above (companion-app's `ServicebuchEintrag` naming +`betrieb`/`notiz` while the actual stored data — `profil["service"]["buch"]`, written by both the panel and +the backend — uses `werkstatt`/`kosten`/`arbeiten`, with no translation layer anywhere) is now fixed. +Decided `werkstatt`/`kosten`/`arbeiten` as canonical, per section AD's own reasoning: the panel's naming is +the older, already-established convention, and real installations' stored data already uses it. + +`companion-app/src/screens/Service.tsx`: `ServicebuchEintrag` renamed to `werkstatt?: string` / +`kosten?: number | null` / `arbeiten?: string`. The add/edit form's "Betrieb"/"Notiz" fields became +"Werkstatt" / a new "Kosten" (€) field / "Durchgeführte Arbeiten" (matching the panel's own three-field +layout and its "Durchgeführte Arbeiten" label exactly, not just the field name) — companion-app previously +had no way to record cost on a Wartungsplan entry at all, this closes that gap too. The read-only entry +display gained matching `Wertzeile`s (`Werkstatt`, `Kosten` — "noch offen" when unset, matching the panel's +placeholder wording — `Durchgeführte Arbeiten`), replacing the old `Betrieb`/`Notiz` rows. + +`DatensatzPopup.tsx`'s Wartungsplan CSV export (built the same day as the bug was found, section AC/AD) had +worked around the mismatch with an `as Record` cast and an explanatory comment reading the +real field names directly; both are now redundant and removed — it reads `e.werkstatt`/`e.kosten` through +the corrected type like any other field. + +`src/tests/beispieldaten.ts`'s two Wartungsplan fixture entries updated from `betrieb` to `werkstatt` +(no `kosten`/`arbeiten` needed adding — the existing tests don't assert on them). No dedicated +`Service.test.ts` exists; grepped the whole `src/` tree for other `betrieb`/`notiz` consumers before and +after — `Einstellungen.tsx`/`Uebersicht.tsx` only pass the raw `buch` array through as `ServicebuchEintrag[]` +without touching individual field names, so neither needed a change. + +Verified: `tsc --noEmit` clean, full suite green at 146/146 (no new tests needed — a schema/naming +alignment, not new behavior), `vite build` succeeds. **Not live-verified against a real backend** — same +structural limitation as section AD immediately above (no running companion-app session with backend auth +this round). The underlying stored data was never wrong (the panel and backend always used +`werkstatt`/`kosten`/`arbeiten`); this was a companion-app-only frontend read/write bug, so no data migration +is needed — an existing Wartungsplan entry will display correctly the next time companion-app is rebuilt and +deployed with this fix. + +## AF. Item 11: Setup-Menü zeigt jetzt den zugeordneten Sensor statt einer statischen Beschreibung (2026.8.28.4) + +Panel only - the Setup menu (sensor-role mapping) is an HA-panel-only concept, no companion-app equivalent +exists to port this to (companion-app consumes the backend's already-resolved state, it has no Setup UI at +all - see the parity rule's own carve-out for this exact case). + +Every field row in Setup used to show a static, developer-written description under the label ("on = Fahrt +läuft, erkennt Fahrtbeginn/-ende.", "Optional, vom CAN..."). Owner: replace it with something read from the +*actual* currently-mapped sensor instead - its technical ID (just the part after `fmm003_`, e.g. +`testintegratoin_engine_ignition_or_acc_status` instead of the full +`binary_sensor.testcar_b9_fmm003_testintegratoin_engine_ignition_or_acc_status`) and its live output value +with unit (`[on/off]`, `[%]`, `[l]`, `[km]`...), positioned next to the headline rather than stacked below it. + +New `entitaetIdKurz(entityId)` (strips the domain prefix, then finds `"fmm003_"` case-insensitively and +returns everything after it - falls back to the domain-free ID if `"fmm003_"` isn't present, for other +brands/integrations) and `entitaetWertKurz(entityId)` (current `state` + `unit_of_measurement`, `null` for +`unavailable`/`unknown`/unmapped - the exact same zustand logic `entitaetZeilenMarkup()` already used for +the search-result list, just for one entity instead of a whole candidate list). `setupErwarteterSensor()` +combines both into one string (`" · "`), used in `setupFeldzeile()`'s header in +place of the old `feld.hinweis` read. List-type fields (four positions, e.g. door/window sensors) show +nothing in the shared header - there's no single "the" sensor to reference there, each position has its own +mapping and its own combo below. + +`feld.hinweis` itself is untouched in the backend `FELDER` catalog (`einstellungen.py`) - still valid data, +simply no longer read at this one call site; not removed, since removing it wasn't asked and nothing else +depends on knowing it's now unused. + +CSS: `.setup-feld-kopf` was `flex-direction: column` (label above the old description) - changed to a row +(`justify-content: space-between`) so the new content sits beside the label as requested, with +`text-overflow: ellipsis` on the right-hand span since a long ID-suffix-plus-value string won't always fit. + +Verified live in `audi_ha_test` (not just read from source - this file's own section S lesson): four real +rows read via the shadow DOM after opening Setup for real (`ZUENDUNG_SENSOR` → +`"testintegratoin_engine_ignition_or_acc_status · off"`, `KM_SENSOR` → `"...mileage_read_from_can · 21302 +km"`, etc.) - label and the new span measured on the identical Y-coordinate (`sameRow: true`), confirming +the row layout actually changed, not just the text content. Confirmed a list field (`TUER_SENSOREN`, +"Türen") renders with no header span at all, as designed. `node --check` clean, manifest bumped to +`2026.8.28.4`, `audi_ha_test` restarted and confirmed clean via log. + --- ## Working conventions (observed — keep them) diff --git a/companion-app/src/screens/DatensatzPopup.tsx b/companion-app/src/screens/DatensatzPopup.tsx index 0d3c9dc..bc2015b 100644 --- a/companion-app/src/screens/DatensatzPopup.tsx +++ b/companion-app/src/screens/DatensatzPopup.tsx @@ -79,25 +79,16 @@ function exportieren( ) return } - // ACHTUNG: liest werkstatt/kosten, nicht ServicebuchEintrag.betrieb/notiz - - // das ist absichtlich. Der tatsächlich gespeicherte Datensatz - // (profil["service"]["buch"], vom Backend/Panel geschrieben) trägt - // werkstatt/kosten/arbeiten; ServicebuchEintrag hier im Frontend nennt - // sie betrieb/notiz und wird nirgends dazwischen übersetzt (Service.tsx - // liest fahrzeug.service["buch"] ungemappt durch) - ein - // Schema-Auseinanderlaufen zwischen den beiden Oberflächen, das beim Bau - // dieses CSV-Exports auffiel, aber ein eigenes Thema ist (siehe AGENTS.md). - // Der Export muss die echten Feldnamen lesen, sonst zeigt jede exportierte - // Zeile eine leere Werkstatt/Kosten-Spalte. csvHerunterladen( "wartungsplan", ["Datum", "km", "Art", "Werkstatt", "Kosten €"], - wartungsplan.map((e) => { - const roh = e as Record - const werkstatt = typeof roh.werkstatt === "string" ? roh.werkstatt : "" - const kosten = typeof roh.kosten === "number" ? roh.kosten : null - return [e.datum ?? "", e.km != null ? de(e.km) : "", e.art ?? "", werkstatt, kosten != null ? eur(kosten) : ""] - }), + wartungsplan.map((e) => [ + e.datum ?? "", + e.km != null ? de(e.km) : "", + e.art ?? "", + e.werkstatt ?? "", + e.kosten != null ? eur(e.kosten) : "", + ]), ) } diff --git a/companion-app/src/screens/Service.tsx b/companion-app/src/screens/Service.tsx index 2148cef..b9da4ff 100644 --- a/companion-app/src/screens/Service.tsx +++ b/companion-app/src/screens/Service.tsx @@ -21,7 +21,7 @@ import { oelMeldungsPrognose, oelwechselPrognose, } from "../daten/service" -import { datum, de, isoTag } from "../format" +import { datum, de, eur, isoTag } from "../format" import type { SeitenName } from "../navigation" import { SymbolInspektion, SymbolOelwechsel } from "../symbole" import { Leerzustand, NaviKachel, Wertzeile, Werteliste, bestaetigen } from "./bausteine" @@ -32,8 +32,9 @@ export interface ServicebuchEintrag { datum?: string km?: number art?: string - betrieb?: string - notiz?: string + werkstatt?: string + kosten?: number | null + arbeiten?: string [weitere: string]: unknown } @@ -373,18 +374,31 @@ export function Servicebuch() { onChange={(e) => setzeEntwurf((alt) => ({ ...alt, km: Number(e.target.value) }))} /> - + setzeEntwurf((alt) => ({ ...alt, betrieb: e.target.value }))} + value={entwurf.werkstatt ?? ""} + onChange={(e) => setzeEntwurf((alt) => ({ ...alt, werkstatt: e.target.value }))} /> - + setzeEntwurf((alt) => ({ ...alt, notiz: e.target.value }))} + inputMode="decimal" + value={entwurf.kosten ?? ""} + onChange={(e) => + setzeEntwurf((alt) => ({ + ...alt, + kosten: e.target.value === "" ? null : Number(e.target.value), + })) + } + /> + + + setzeEntwurf((alt) => ({ ...alt, arbeiten: e.target.value }))} />
@@ -428,8 +442,9 @@ export function Servicebuch() { label="Kilometerstand" wert={eintrag.km != null ? `${de(eintrag.km)} km` : "—"} /> - - {eintrag.notiz && } + + + {eintrag.arbeiten && } } /> diff --git a/companion-app/src/tests/beispieldaten.ts b/companion-app/src/tests/beispieldaten.ts index 18e7b08..bac4b53 100644 --- a/companion-app/src/tests/beispieldaten.ts +++ b/companion-app/src/tests/beispieldaten.ts @@ -104,8 +104,8 @@ export const beispielProfil: Profil = { vereinbart: {}, autohaus: { name: "Audi Zentrum", adresse: "Musterweg 1", tel: "089 1234", mail: "a@b.de" }, buch: [ - { datum: "2026-02-10", km: 40000, art: "Ölwechsel", betrieb: "Audi Zentrum" }, - { datum: "2025-03-05", km: 25000, art: "Inspektion", betrieb: "Audi Zentrum" }, + { datum: "2026-02-10", km: 40000, art: "Ölwechsel", werkstatt: "Audi Zentrum" }, + { datum: "2025-03-05", km: 25000, art: "Inspektion", werkstatt: "Audi Zentrum" }, ], }, technik: [ diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js index d9bd9ca..c1bd184 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js +++ b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js @@ -4061,6 +4061,42 @@ function sheetMarkup() { /* ------------------------------------------------------------ Setup-Popup */ const LUPE_SVG = ``; +/* Nur der Teil der Entity-ID nach "fmm003_" - die tatsächliche, technische + Kennung des zugeordneten Sensors, kürzer und aussagekräftiger als der + volle "binary_sensor.testcar_b9_fmm003_..."-Pfad. Ohne "fmm003_" im Namen + (z. B. eine andere Marke/Integration) bleibt der Domain-freie Rest + stehen, statt nichts anzuzeigen. */ +function entitaetIdKurz(entityId) { + if (!entityId) return null; + const ohneDomain = entityId.includes(".") ? entityId.split(".").slice(1).join(".") : entityId; + const idx = ohneDomain.toLowerCase().indexOf("fmm003_"); + return idx >= 0 ? ohneDomain.slice(idx + "fmm003_".length) : ohneDomain; +} + +/* Aktueller Wert + Einheit einer Entität, oder null ohne brauchbaren + Zustand (nicht zugeordnet, unavailable/unknown) - dieselbe Zustandslogik + wie in entitaetZeilenMarkup() unten, hier für eine einzelne Entität statt + einer ganzen Kandidatenliste. */ +function entitaetWertKurz(entityId) { + if (!entityId || !HASS.states[entityId]) return null; + const zustand = HASS.states[entityId].state; + if (zustand === "unavailable" || zustand === "unknown") return null; + const einheit = HASS.states[entityId].attributes && HASS.states[entityId].attributes.unit_of_measurement; + return `${zustand}${einheit ? " " + einheit : ""}`; +} + +/* Ersetzt den bisherigen statischen Beschreibungstext im Setup ("on = Fahrt + läuft", "Optional, vom CAN...") - Nutzerwunsch: statt einer generischen + Erklärung zeigt die Kopfzeile jetzt die Kennung UND die aktuelle Meldung + des tatsächlich zugeordneten Sensors, direkt neben der Feld-Überschrift. + Ohne Zuordnung (leer) gibt es nichts zu zeigen. */ +function setupErwarteterSensor(entityId) { + const kurz = entitaetIdKurz(entityId); + if (!kurz) return ""; + const wert = entitaetWertKurz(entityId); + return wert != null ? `${kurz} · ${wert}` : kurz; +} + function entitaetZeilenMarkup(feldId, key, suchtext, aktuellerWert) { const kandidaten = entitaetKandidaten(key, suchtext, aktuellerWert); if (!kandidaten.length) return `
Keine passende Entität gefunden
`; @@ -4096,9 +4132,13 @@ function entitaetCombo(key, idx) { } function setupFeldzeile(feld) { + // Bei einem Listenfeld (vier Positionen wie Tür-/Fenstersensoren) gibt es + // keinen EINEN zugeordneten Sensor für die Kopfzeile - jede Position hat + // ihre eigene Zuordnung, siehe die Unterfelder unten. + const erwartet = feld.liste ? "" : setupErwarteterSensor(setupZuordnung[feld.key]); const kopf = `
${esc(feld.label)} - ${esc(feld.hinweis)} + ${erwartet ? `${esc(erwartet)}` : ""}
`; if (feld.liste) { return `
${kopf} diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard.css b/custom_components/audi_dashboard/frontend/audi-dashboard.css index 66ddf74..53394a7 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard.css +++ b/custom_components/audi_dashboard/frontend/audi-dashboard.css @@ -1255,9 +1255,16 @@ button.leaf:active { background: var(--tile-2); } .setup-gruppe-titel { font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--fg3); margin-bottom: 6px; } .setup-feldzeile { padding: 12px 0; border-bottom: 1px solid var(--line); } .setup-feldzeile:last-child { border-bottom: none; } -.setup-feld-kopf { display: flex; flex-direction: column; gap: 2px; margin-bottom: 8px; } -.setup-feld-label { font-size: 14.5px; color: var(--fg); } -.setup-feld-hinweis { font-size: 12.5px; color: var(--fg2); line-height: 1.4; } +/* Nutzerwunsch: der zugeordnete Sensor (Kennung + aktueller Wert, siehe + setupErwarteterSensor() in audi-dashboard-app.js) steht NEBEN der + Feld-Überschrift, nicht mehr wie die frühere statische Beschreibung + darunter - deshalb Zeile statt Spalte. */ +.setup-feld-kopf { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 8px; } +.setup-feld-label { font-size: 14.5px; color: var(--fg); flex: 0 1 auto; } +.setup-feld-hinweis { + font-size: 12.5px; color: var(--fg2); line-height: 1.4; text-align: right; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 55%; +} .setup-unterfelder { display: flex; flex-direction: column; gap: 12px; } .setup-unterfeld { display: flex; flex-direction: column; gap: 4px; } .setup-unterfeld-label { font-size: 12px; color: var(--fg3); } diff --git a/custom_components/audi_dashboard/manifest.json b/custom_components/audi_dashboard/manifest.json index 01d5900..25ce0b4 100644 --- a/custom_components/audi_dashboard/manifest.json +++ b/custom_components/audi_dashboard/manifest.json @@ -1,7 +1,7 @@ { "domain": "audi_dashboard", "name": "Audi Dashboard", - "version": "2026.8.28.3", + "version": "2026.8.28.4", "documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md", "issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues", "codeowners": ["@paul"],