diff --git a/AGENTS.md b/AGENTS.md index 503a5e2..d6c81ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4178,44 +4178,68 @@ this round). The underlying stored data was never wrong (the panel and backend a 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) +## AF. Item 11: Setup-Menü zeigt jetzt den zugeordneten Sensor statt einer statischen Beschreibung (2026.8.28.4/.5) 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. +läuft, erkennt Fahrtbeginn/-ende.", "Optional, vom CAN..."). Owner: replace it with the *actual* +currently-mapped sensor's technical ID (just the part after the installation-specific noise) shown next to +the headline, plus a separate indicator of the KIND of value each field expects. -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. +**First version (`.4`) got two things wrong, both caught live by the owner before being accepted:** +1. Hardcoded searching for the literal substring `"fmm003_"` and keeping everything after it - the owner + pointed out this still leaves *other* installation-specific noise in the result (`"testintegratoin_..."` - + the flespi channel's own instance name, not part of the sensor's real, stable identity; a different + installation would have a different instance name here, making a hardcoded strip target wrong for anyone + but this one test container). +2. Combined the ID-suffix with the sensor's *current live reading* into one string next to the headline. The + owner: show only the expected sensor identity there - and separately, "next to the Sensor Headline", + show the *kind* of value the field expects (`Tankfüllstand [%]`, `Tankvolumen [l]`, `12V-Batteriespannung + [V]`), not a live reading. -`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. +**Fixed in `.5`:** +- `entitaetIdKurz(entityId)` no longer searches for a hardcoded string. New `setupGemeinsamesPraefix()` + computes the longest character-for-character common prefix (case-insensitive, trimmed back to the last + shared `_`) across every entity ID currently assigned across the *entire* Setup catalog (domain-stripped), + requiring at least two mapped entities to compare. In this real installation that correctly resolves to + `"testcar_b9_fmm003_testintegratoin_"` - device slug, integration name, and instance name all stripped in + one step, none of it hardcoded. `entitaetIdKurz()` strips that computed prefix only when a given entity's + ID actually starts with it; otherwise it shows the full domain-stripped ID rather than an incorrect partial + strip. **This fallback path fired for real, unprompted, during live verification**: `TANK_DISTANZ_SENSOR` + (added the same day, section AC) had been auto-suggested to `sensor.audi_rs_4_avant_monthly_mileage` - a + genuinely different source (not FMM003) sharing no prefix with the rest - and correctly rendered the full, + unstripped ID instead of a wrong truncation. Confirms the graceful-degradation design works, not just the + common case. +- The live-value read (`entitaetWertKurz()`/`setupErwarteterSensor()`) is gone entirely - removed, not just + unused. In its place, new `feldEinheitAnzeige(feld)` derives a **static** "kind of value" label straight + from the field's own `FELDER` catalog entry (`einstellungen.py`) - `"on/off"` for every `binary_sensor` + role (HA's binary_sensor domain always reports the raw state as on/off regardless of `device_class` - that + only changes how *other* HA frontends translate it for display, not the raw value this app reads), the + first unit in `units` preferring a lowercase-distinct variant when more than one is listed (picks `"l"` + over `"L"` for `TANK_LITER_SENSOR`, `"km"` over `"mi"` elsewhere), `"Datum"` for `date`/`timestamp` + `device_classes`, nothing for a `button` role or a field with neither (GPS coordinates). This label is + appended to the field's own headline text (`"Tankfüllstand [%]"`) and is independent of whether anything is + currently mapped - unlike the ID-suffix hint, which needs a live mapping to have anything to show. -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. +`feld.hinweis` itself is untouched in the backend `FELDER` catalog - still valid data, simply not read by +this UI anymore; not removed, since nothing else depends on knowing it's unused and removing it wasn't asked. -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. +CSS: `.setup-feld-kopf` changed from a column (label above the old description) to a row (space-between) so +the ID-suffix hint sits beside the label; new `.setup-feld-einheit` (a slightly dimmer `--fg3`) for the +bracketed unit label appended to the headline text itself. + +Verified live in `audi_ha_test` both times (not just read from source - this file's own section S lesson). +First pass (`.4`): confirmed the row layout change itself (label and hint measured on the identical +Y-coordinate) before the owner's correction arrived. Second pass (`.5`), after the fix, read eight real Setup +rows via the shadow DOM in one pass and matched every one against the owner's own examples exactly: +`"Zündung/ACC-Status [on/off]"` / `"testcar_b9_fmm003_testintegratoin_engine_ignition_or_acc_status"`, +`"Tankfüllstand [%]"`, `"Tankvolumen (Liter) [l]"`, `"12V-Batteriespannung [V]"`, `"Außentemperatur [°C]"` - +plus the `TANK_DISTANZ_SENSOR` fallback case above and a screenshot confirming the visual layout. `node +--check` clean at each step, manifest bumped `2026.8.28.4` → `.5`, `audi_ha_test` restarted and confirmed +clean via log both times. --- diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js index c1bd184..17eb331 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js +++ b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js @@ -4061,40 +4061,77 @@ 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. */ +/* Das gemeinsame Präfix aller aktuell zugeordneten Sensoren (domain-frei), + bis zum letzten gemeinsamen "_" - z. B. "testcar_b9_fmm003_testintegratoin_". + NICHT hart auf "fmm003_" verdrahtet: sowohl der Geräte-Slug + ("testcar_b9") als auch der Instanzname der Integration + ("testintegratoin") sind installationsspezifisch, vom Nutzer beim + Einrichten seines eigenen flespi-Kanals selbst vergeben - bei ihm heißt + das anders. Nur was WIRKLICH von allen zugeordneten Sensoren geteilt + wird, ist reines Rauschen; alles danach ist die eigentliche, stabile + Signalbezeichnung. Mit weniger als zwei zugeordneten Sensoren (nichts zum + Vergleichen) oder wenn Sensoren aus zwei erkennbar verschiedenen Quellen + kommen (kein gemeinsames Präfix), liefert das null - dann zeigt + entitaetIdKurz() lieber die volle, ungekürzte ID als eine geratene, + möglicherweise falsche Kürzung. */ +function setupGemeinsamesPraefix() { + const ids = []; + for (const feld of setupKatalog()) { + const wert = setupZuordnung[feld.key]; + if (feld.liste) { for (const w of (wert || [])) if (w) ids.push(w); } + else if (wert) ids.push(wert); + } + const ohneDomain = ids.map((id) => id.includes(".") ? id.split(".").slice(1).join(".") : id); + if (ohneDomain.length < 2) return null; + let praefix = ohneDomain[0]; + for (const s of ohneDomain.slice(1)) { + let i = 0; + while (i < praefix.length && i < s.length && praefix[i].toLowerCase() === s[i].toLowerCase()) i++; + praefix = praefix.slice(0, i); + if (!praefix) return null; + } + const idxUnter = praefix.lastIndexOf("_"); + return idxUnter > 0 ? praefix.slice(0, idxUnter + 1) : null; +} + +/* Die Entity-ID ohne Domain und ohne das gemeinsame, installationsspezifische + Präfix (siehe setupGemeinsamesPraefix()) - die tatsächliche, stabile + Kennung des zugeordneten Sensors, unabhängig davon, wie Geräte-Slug und + Integrationsinstanz beim jeweiligen Nutzer heißen. Passt das gemeinsame + Präfix nicht auf DIESE Entität (z. B. eine einzelne Entität aus einer + anderen Quelle als der Rest), bleibt die volle domain-freie ID stehen. */ 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; + const praefix = setupGemeinsamesPraefix(); + if (praefix && ohneDomain.toLowerCase().startsWith(praefix.toLowerCase())) { + return ohneDomain.slice(praefix.length); + } + return 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; +/* Die Art des erwarteten Werts, als kurzes Klammer-Etikett direkt neben der + Feld-Überschrift ("Tankfüllstand [%]", "12V-Batteriespannung [V]") - eine + feste Eigenschaft der FELD-ROLLE selbst (aus dem FELDER-Katalog, + einstellungen.py), unabhängig davon, ob und welcher Sensor gerade + zugeordnet ist. Bewusst NICHT der Live-Wert eines Sensors - der gehört + nirgends mehr hin, siehe entitaetIdKurz()'s eigenen Kommentar zur + Kopfzeile. binary_sensor liefert immer den Rohzustand "on"/"off" + (unabhängig von device_class - die beeinflusst nur, wie ANDERE + HA-Oberflächen ihn anzeigen, nicht den Rohwert, den diese App liest). + Bei mehreren möglichen Einheiten (z. B. "L"/"l") die klein geschriebene + Variante bevorzugt, sonst die erste genannte. Ohne passenden Fall (Knopf, + GPS-Koordinate) kein Etikett. */ +function feldEinheitAnzeige(feld) { + if (feld.domains && feld.domains.includes("binary_sensor")) return "on/off"; + if (feld.domains && feld.domains.includes("button")) return null; + if (feld.units && feld.units.length) { + return feld.units.find((u) => u === u.toLowerCase() && u !== u.toUpperCase()) || feld.units[0]; + } + if (feld.device_classes && (feld.device_classes.includes("date") || feld.device_classes.includes("timestamp"))) { + return "Datum"; + } + return null; } function entitaetZeilenMarkup(feldId, key, suchtext, aktuellerWert) { @@ -4133,11 +4170,14 @@ 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]); + // keinen EINEN zugeordneten Sensor, dessen Kennung sich anzeigen ließe - + // jede Position hat ihre eigene Zuordnung, siehe die Unterfelder unten. + // Das Einheiten-Etikett neben dem Label gilt dagegen für die ganze Rolle + // und wird auch dort gezeigt. + const einheit = feldEinheitAnzeige(feld); + const erwartet = feld.liste ? "" : entitaetIdKurz(setupZuordnung[feld.key]); const kopf = `
- ${esc(feld.label)} + ${esc(feld.label)}${einheit ? ` [${esc(einheit)}]` : ""} ${erwartet ? `${esc(erwartet)}` : ""}
`; if (feld.liste) { diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard.css b/custom_components/audi_dashboard/frontend/audi-dashboard.css index 53394a7..7d84182 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard.css +++ b/custom_components/audi_dashboard/frontend/audi-dashboard.css @@ -1255,12 +1255,14 @@ 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; } -/* 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. */ +/* Nutzerwunsch: die Kennung des zugeordneten Sensors (entitaetIdKurz() in + audi-dashboard-app.js) steht NEBEN der Feld-Überschrift, nicht mehr wie + die frühere statische Beschreibung darunter - deshalb Zeile statt Spalte. + Die erwartete Einheit (feldEinheitAnzeige()) hängt direkt am Label selbst + ("Tankfüllstand [%]"), losgelöst davon, ob/welcher Sensor zugeordnet ist. */ .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-einheit { color: var(--fg3); font-size: 12.5px; } .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%; diff --git a/custom_components/audi_dashboard/manifest.json b/custom_components/audi_dashboard/manifest.json index 25ce0b4..6f42c3f 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.4", + "version": "2026.8.28.5", "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"],