Setup-Menue: Sensorkennung per naechstem Verwandten statt globalem Praefix

Zwei weitere Korrekturrunden, beide vom Nutzer live erkannt:

- .6: paarweise Abstimmung statt fortlaufender Verengung - robuster gegen
  einen einzelnen Ausreisser, aber grundsaetzlich noch falsch.
- .7: die eigentliche Ursache war, dass diese Installation zwei etwa
  gleich grosse Sensorquellen gleichzeitig hat (9 FMM003-Rollen, 18 Rollen
  einer zweiten Integration mit "audi_rs_4_avant_"-Praefix) - ein
  EINZELNES globales "Sieger"-Praefix kann immer nur eine der beiden
  Quellen richtig kuerzen. entitaetIdKurz() sucht jetzt pro Feld den
  naechsten Verwandten unter allen zugeordneten Sensoren und kuerzt nur
  dessen gemeinsames Praefix - unabhaengig von Anzahl und Groesse der
  Quellen.

Diesmal vor dem Deploy anhand der ECHTEN sensor.audi_dashboard_entitaeten-
Zuordnung durchgerechnet statt mit erfundenen Test-IDs (das hatte den
.6-Fehler faelschlich als behoben erscheinen lassen). Version 2026.8.28.7,
live verifiziert: alle 20 Einzelwert-Felder zeigen die richtige, kurze
Kennung, keine installationsspezifischen Reste aus beiden Quellen mehr.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 10:54:16 +02:00
parent af64fc39e3
commit 4f85a179cf
3 changed files with 83 additions and 38 deletions
+40
View File
@@ -4241,6 +4241,46 @@ plus the `TANK_DISTANZ_SENSOR` fallback case above and a screenshot confirming t
--check` clean at each step, manifest bumped `2026.8.28.4` → `.5`, `audi_ha_test` restarted and confirmed
clean via log both times.
**`.5`'s own verification above was itself misleading - the owner caught the real bug immediately after
("I still see testcar_b9_fmm003_testintegratoin_ and audi_rs_4_avant_").** `.5`'s `setupGemeinsamesPraefix()`
computed ONE global prefix by intersecting every mapped sensor's ID down to their shared characters - and a
single outlier (any one field mapped to a sensor from a different source than the rest) collapses that
intersection to nothing, so EVERY field falls back to its full, unstripped ID. The live verification quoted
above only exercised four fields and happened not to include the one that broke it - a real gap in how
thoroughly that pass was checked, not a fluke in the code.
Fixed properly in `.6`: replaced the single global intersection with pairwise voting (every sensor pair's own
common prefix, the most frequently occurring one wins) - more robust against one outlier, but **still wrong
for a different reason**, found by the owner's report and confirmed by pulling the actual live
`sensor.audi_dashboard_entitaeten` data (not by re-reading the source - the only way this surfaced): this
installation genuinely has **two comparably-sized sensor sources at once** - 9 FMM003-sourced roles
(ignition, odometer, tank, battery, GPS) and 18 roles from a second integration, prefixed
`audi_rs_4_avant_` (all four doors, all four windows, tailgate/hood/lock/sunroof/light, all four service-date
fields). A single global "winner" prefix, however it's computed, can only ever strip ONE of the two families
correctly and leaves the other's IDs fully unstripped - exactly what both `.5` and `.6` did, just for
different roles each time.
**`.7` drops the idea of a single global prefix entirely.** `entitaetIdKurz()` now finds, for the entity
being displayed, its own single closest relative among every other currently-mapped sensor (the one it
shares the longest prefix with) and strips only that pairwise prefix - computed independently per field, so
it doesn't matter how many different sensor sources are mixed together or how large each cluster is. Verified
by pulling the *actual* stored `werte` mapping from `sensor.audi_dashboard_entitaeten` (not fabricated test
IDs like the `.5`/`.6` passes used) and running the real algorithm against it before deploying - this is what
caught the `.6` design flaw *before* declaring it fixed, rather than after another owner report. A pleasant
side effect: `NAECHSTER_OELWECHSEL_SENSOR`/`NAECHSTE_INSPEKTION_SENSOR` (`oil_change_due`/`inspection_due`)
and their `_STRECKE_SENSOR` counterparts (`oil_change_distance`/`inspection_distance`) now pair off with each
other specifically and reduce to `"due"`/`"distance"` - each field lands next to its true sibling, not just
"some" shared prefix.
Verified live in `audi_ha_test` at every step (`.6` then `.7`), each time reading the actual rendered Setup
popup fresh after a real restart (not a stale tab - confirmed via `performance.getEntriesByType('resource')`
showing the new `?v=` served with a nonzero transfer size each time). Final state (`.7`), all 20 single-value
fields checked in one pass: every FMM003 role shows its bare signal name
(`external_power_voltage`, `can_ambient_air_temperature`, ...), every `audi_rs_4_avant_` role shows its own
(`tailgate`, `hood`, `sunroof`, `parking_lights`, `due`, `distance`, ...), nothing shows installation noise
from either source. `node --check` clean at each step. Manifest `2026.8.28.5``.6``.7`, `audi_ha_test`
restarted and confirmed clean via log each time.
---
## Working conventions (observed — keep them)
@@ -4061,53 +4061,58 @@ 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>`;
/* 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() {
/* Gemeinsames Präfix zweier domain-freier IDs, bis zum letzten gemeinsamen
"_" - oder null, wenn sie nicht einmal ein volles Segment teilen. */
function praefixPaar(a, b) {
let i = 0;
while (i < a.length && i < b.length && a[i].toLowerCase() === b[i].toLowerCase()) i++;
const idxUnter = a.slice(0, i).lastIndexOf("_");
return idxUnter > 0 ? a.slice(0, idxUnter + 1) : null;
}
/* Alle aktuell im Setup zugeordneten Entity-IDs (domain-frei), außer der
übergebenen selbst - Vergleichsbasis für entitaetIdKurz() unten. */
function setupAndereIdsOhneDomain(ausser) {
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);
if (feld.liste) { for (const w of (wert || [])) if (w && w !== ausser) ids.push(w); }
else if (wert && wert !== ausser) 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;
return ids.map((id) => id.includes(".") ? id.split(".").slice(1).join(".") : id);
}
/* 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. */
/* Die Entity-ID ohne Domain und ohne das installationsspezifische Präfix,
das sie mit ihrem NÄCHSTEN Verwandten unter den übrigen zugeordneten
Sensoren teilt (dem, mit dem sie das längste gemeinsame Präfix hat) - die
tatsächliche, stabile Kennung des zugeordneten Sensors, unabhängig davon,
wie Geräte-Slug und Integrationsinstanz beim jeweiligen Nutzer heißen
(z. B. "testcar_b9_fmm003_testintegratoin_" oder "audi_rs_4_avant_").
Bewusst PRO ENTITÄT der beste Nachbar statt eines einzigen, für alle
Felder gemeinsamen Präfixes (frühere Fassung) - eine reale Installation
hat oft MEHRERE Sensorquellen gleichzeitig (hier: FMM003 für Fahrt-/
Tank-/Batteriewerte, eine zweite Integration für Türen/Fenster/
Service-Termine, per "Nur passende Sensoren anzeigen" beide gleichzeitig
vorgeschlagen) - keine ist objektiv "die Mehrheit". Ein einziges globales
Präfix (egal ob per fortlaufender Verengung oder per Mehrheitsentscheid)
kann immer nur EINE Quelle richtig kürzen und zeigt die Sensoren der
anderen ungekürzt - genau das hat der Nutzer live beobachtet
("audi_rs_4_avant_" gewann die Abstimmung, alle FMM003-Felder blieben
unangetastet). Mit dem nächsten Verwandten statt einem globalen Sieger
kürzt jede Rolle unabhängig von den anderen richtig, egal wie viele
verschiedene Quellen gleichzeitig im Spiel sind. Findet sich kein
Verwandter mit gemeinsamem Präfix, bleibt die volle domain-freie ID
stehen statt einer geratenen, möglicherweise falschen Kürzung. */
function entitaetIdKurz(entityId) {
if (!entityId) return null;
const ohneDomain = entityId.includes(".") ? entityId.split(".").slice(1).join(".") : entityId;
const praefix = setupGemeinsamesPraefix();
if (praefix && ohneDomain.toLowerCase().startsWith(praefix.toLowerCase())) {
return ohneDomain.slice(praefix.length);
let bestesPraefix = null, besteLaenge = 0;
for (const andere of setupAndereIdsOhneDomain(entityId)) {
const kandidat = praefixPaar(ohneDomain, andere);
if (kandidat && kandidat.length > besteLaenge) { bestesPraefix = kandidat; besteLaenge = kandidat.length; }
}
return ohneDomain;
return bestesPraefix ? ohneDomain.slice(bestesPraefix.length) : ohneDomain;
}
/* Die Art des erwarteten Werts, als kurzes Klammer-Etikett direkt neben der
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.8.28.5",
"version": "2026.8.28.7",
"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"],