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
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 (`"<id-suffix> · <value> <unit>"`), 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.
---
@@ -4061,40 +4061,77 @@ function sheetMarkup() {
/* ------------------------------------------------------------ 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>`;
/* 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 = `<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>` : ""}
</div>`;
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-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%;
@@ -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"],