Setup-Menue: Sensorkennung ohne installationsspezifisches Praefix, Einheit statt Live-Wert

Korrektur zur letzten Aenderung, vom Nutzer live erkannt:
- "fmm003_" war hart einprogrammiert und liess trotzdem noch
  installationsspezifisches Rauschen stehen (Geraete-Slug, Instanzname der
  Integration). Jetzt wird das laengste gemeinsame Praefix ueber alle
  aktuell zugeordneten Sensoren berechnet (setupGemeinsamesPraefix()) -
  nichts mehr hartkodiert. Faellt bei einer einzelnen Entitaet aus
  anderer Quelle korrekt auf die volle ID zurueck, statt falsch zu kuerzen
  (live bestaetigt: TANK_DISTANZ_SENSOR zeigte den vollen Namen, weil er
  auf eine andere Integration zeigte).
- Der Live-Wert des Sensors gehoert nicht mehr neben die ID - stattdessen
  zeigt das Label selbst jetzt die ERWARTETE Art des Werts an
  ("Tankfuellstand [%]", "12V-Batteriespannung [V]"), statisch aus dem
  FELDER-Katalog abgeleitet, unabhaengig von einer aktuellen Zuordnung.

Version 2026.8.28.5, live im Testcontainer verifiziert (acht echte
Setup-Zeilen gegen die Nutzerbeispiele geprueft).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 10:45:45 +02:00
parent b1cd03be51
commit af64fc39e3
4 changed files with 132 additions and 66 deletions
+52 -28
View File
@@ -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 is needed — an existing Wartungsplan entry will display correctly the next time companion-app is rebuilt and
deployed with this fix. 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 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 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). 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 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 läuft, erkennt Fahrtbeginn/-ende.", "Optional, vom CAN..."). Owner: replace it with the *actual*
*actual* currently-mapped sensor instead - its technical ID (just the part after `fmm003_`, e.g. currently-mapped sensor's technical ID (just the part after the installation-specific noise) shown next to
`testintegratoin_engine_ignition_or_acc_status` instead of the full the headline, plus a separate indicator of the KIND of value each field expects.
`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 **First version (`.4`) got two things wrong, both caught live by the owner before being accepted:**
returns everything after it - falls back to the domain-free ID if `"fmm003_"` isn't present, for other 1. Hardcoded searching for the literal substring `"fmm003_"` and keeping everything after it - the owner
brands/integrations) and `entitaetWertKurz(entityId)` (current `state` + `unit_of_measurement`, `null` for pointed out this still leaves *other* installation-specific noise in the result (`"testintegratoin_..."` -
`unavailable`/`unknown`/unmapped - the exact same zustand logic `entitaetZeilenMarkup()` already used for the flespi channel's own instance name, not part of the sensor's real, stable identity; a different
the search-result list, just for one entity instead of a whole candidate list). `setupErwarteterSensor()` installation would have a different instance name here, making a hardcoded strip target wrong for anyone
combines both into one string (`"<id-suffix> · <value> <unit>"`), used in `setupFeldzeile()`'s header in but this one test container).
place of the old `feld.hinweis` read. List-type fields (four positions, e.g. door/window sensors) show 2. Combined the ID-suffix with the sensor's *current live reading* into one string next to the headline. The
nothing in the shared header - there's no single "the" sensor to reference there, each position has its own owner: show only the expected sensor identity there - and separately, "next to the Sensor Headline",
mapping and its own combo below. 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, **Fixed in `.5`:**
simply no longer read at this one call site; not removed, since removing it wasn't asked and nothing else - `entitaetIdKurz(entityId)` no longer searches for a hardcoded string. New `setupGemeinsamesPraefix()`
depends on knowing it's now unused. 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 `feld.hinweis` itself is untouched in the backend `FELDER` catalog - still valid data, simply not read by
(`justify-content: space-between`) so the new content sits beside the label as requested, with this UI anymore; not removed, since nothing else depends on knowing it's unused and removing it wasn't asked.
`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 CSS: `.setup-feld-kopf` changed from a column (label above the old description) to a row (space-between) so
rows read via the shadow DOM after opening Setup for real (`ZUENDUNG_SENSOR` the ID-suffix hint sits beside the label; new `.setup-feld-einheit` (a slightly dimmer `--fg3`) for the
`"testintegratoin_engine_ignition_or_acc_status · off"`, `KM_SENSOR` → `"...mileage_read_from_can · 21302 bracketed unit label appended to the headline text itself.
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`, Verified live in `audi_ha_test` both times (not just read from source - this file's own section S lesson).
"Türen") renders with no header span at all, as designed. `node --check` clean, manifest bumped to First pass (`.4`): confirmed the row layout change itself (label and hint measured on the identical
`2026.8.28.4`, `audi_ha_test` restarted and confirmed clean via log. 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.
--- ---
@@ -4061,40 +4061,77 @@ function sheetMarkup() {
/* ------------------------------------------------------------ Setup-Popup */ /* ------------------------------------------------------------ Setup-Popup */
const LUPE_SVG = `<svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="8.5" cy="8.5" r="6"/><path d="m17 17-4.3-4.3"/></svg>`; const LUPE_SVG = `<svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="8.5" cy="8.5" r="6"/><path d="m17 17-4.3-4.3"/></svg>`;
/* Nur der Teil der Entity-ID nach "fmm003_" - die tatsächliche, technische /* Das gemeinsame Präfix aller aktuell zugeordneten Sensoren (domain-frei),
Kennung des zugeordneten Sensors, kürzer und aussagekräftiger als der bis zum letzten gemeinsamen "_" - z. B. "testcar_b9_fmm003_testintegratoin_".
volle "binary_sensor.testcar_b9_fmm003_..."-Pfad. Ohne "fmm003_" im Namen NICHT hart auf "fmm003_" verdrahtet: sowohl der Geräte-Slug
(z. B. eine andere Marke/Integration) bleibt der Domain-freie Rest ("testcar_b9") als auch der Instanzname der Integration
stehen, statt nichts anzuzeigen. */ ("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) { function entitaetIdKurz(entityId) {
if (!entityId) return null; if (!entityId) return null;
const ohneDomain = entityId.includes(".") ? entityId.split(".").slice(1).join(".") : entityId; const ohneDomain = entityId.includes(".") ? entityId.split(".").slice(1).join(".") : entityId;
const idx = ohneDomain.toLowerCase().indexOf("fmm003_"); const praefix = setupGemeinsamesPraefix();
return idx >= 0 ? ohneDomain.slice(idx + "fmm003_".length) : ohneDomain; if (praefix && ohneDomain.toLowerCase().startsWith(praefix.toLowerCase())) {
return ohneDomain.slice(praefix.length);
}
return ohneDomain;
} }
/* Aktueller Wert + Einheit einer Entität, oder null ohne brauchbaren /* Die Art des erwarteten Werts, als kurzes Klammer-Etikett direkt neben der
Zustand (nicht zugeordnet, unavailable/unknown) - dieselbe Zustandslogik Feld-Überschrift ("Tankfüllstand [%]", "12V-Batteriespannung [V]") - eine
wie in entitaetZeilenMarkup() unten, hier für eine einzelne Entität statt feste Eigenschaft der FELD-ROLLE selbst (aus dem FELDER-Katalog,
einer ganzen Kandidatenliste. */ einstellungen.py), unabhängig davon, ob und welcher Sensor gerade
function entitaetWertKurz(entityId) { zugeordnet ist. Bewusst NICHT der Live-Wert eines Sensors - der gehört
if (!entityId || !HASS.states[entityId]) return null; nirgends mehr hin, siehe entitaetIdKurz()'s eigenen Kommentar zur
const zustand = HASS.states[entityId].state; Kopfzeile. binary_sensor liefert immer den Rohzustand "on"/"off"
if (zustand === "unavailable" || zustand === "unknown") return null; (unabhängig von device_class - die beeinflusst nur, wie ANDERE
const einheit = HASS.states[entityId].attributes && HASS.states[entityId].attributes.unit_of_measurement; HA-Oberflächen ihn anzeigen, nicht den Rohwert, den diese App liest).
return `${zustand}${einheit ? " " + einheit : ""}`; 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. */
/* Ersetzt den bisherigen statischen Beschreibungstext im Setup ("on = Fahrt function feldEinheitAnzeige(feld) {
läuft", "Optional, vom CAN...") - Nutzerwunsch: statt einer generischen if (feld.domains && feld.domains.includes("binary_sensor")) return "on/off";
Erklärung zeigt die Kopfzeile jetzt die Kennung UND die aktuelle Meldung if (feld.domains && feld.domains.includes("button")) return null;
des tatsächlich zugeordneten Sensors, direkt neben der Feld-Überschrift. if (feld.units && feld.units.length) {
Ohne Zuordnung (leer) gibt es nichts zu zeigen. */ return feld.units.find((u) => u === u.toLowerCase() && u !== u.toUpperCase()) || feld.units[0];
function setupErwarteterSensor(entityId) { }
const kurz = entitaetIdKurz(entityId); if (feld.device_classes && (feld.device_classes.includes("date") || feld.device_classes.includes("timestamp"))) {
if (!kurz) return ""; return "Datum";
const wert = entitaetWertKurz(entityId); }
return wert != null ? `${kurz} · ${wert}` : kurz; return null;
} }
function entitaetZeilenMarkup(feldId, key, suchtext, aktuellerWert) { function entitaetZeilenMarkup(feldId, key, suchtext, aktuellerWert) {
@@ -4133,11 +4170,14 @@ function entitaetCombo(key, idx) {
function setupFeldzeile(feld) { function setupFeldzeile(feld) {
// Bei einem Listenfeld (vier Positionen wie Tür-/Fenstersensoren) gibt es // Bei einem Listenfeld (vier Positionen wie Tür-/Fenstersensoren) gibt es
// keinen EINEN zugeordneten Sensor für die Kopfzeile - jede Position hat // keinen EINEN zugeordneten Sensor, dessen Kennung sich anzeigen ließe -
// ihre eigene Zuordnung, siehe die Unterfelder unten. // jede Position hat ihre eigene Zuordnung, siehe die Unterfelder unten.
const erwartet = feld.liste ? "" : setupErwarteterSensor(setupZuordnung[feld.key]); // 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 = `<div class="setup-feld-kopf"> const kopf = `<div class="setup-feld-kopf">
<span class="setup-feld-label">${esc(feld.label)}</span> <span class="setup-feld-label">${esc(feld.label)}${einheit ? ` <span class="setup-feld-einheit">[${esc(einheit)}]</span>` : ""}</span>
${erwartet ? `<span class="setup-feld-hinweis">${esc(erwartet)}</span>` : ""} ${erwartet ? `<span class="setup-feld-hinweis">${esc(erwartet)}</span>` : ""}
</div>`; </div>`;
if (feld.liste) { if (feld.liste) {
@@ -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-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 { padding: 12px 0; border-bottom: 1px solid var(--line); }
.setup-feldzeile:last-child { border-bottom: none; } .setup-feldzeile:last-child { border-bottom: none; }
/* Nutzerwunsch: der zugeordnete Sensor (Kennung + aktueller Wert, siehe /* Nutzerwunsch: die Kennung des zugeordneten Sensors (entitaetIdKurz() in
setupErwarteterSensor() in audi-dashboard-app.js) steht NEBEN der audi-dashboard-app.js) steht NEBEN der Feld-Überschrift, nicht mehr wie
Feld-Überschrift, nicht mehr wie die frühere statische Beschreibung die frühere statische Beschreibung darunter - deshalb Zeile statt Spalte.
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-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-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 { .setup-feld-hinweis {
font-size: 12.5px; color: var(--fg2); line-height: 1.4; text-align: right; 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%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 55%;
@@ -1,7 +1,7 @@
{ {
"domain": "audi_dashboard", "domain": "audi_dashboard",
"name": "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", "documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues", "issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": ["@paul"], "codeowners": ["@paul"],