Setup-Menü: Sensorkennung per Mehrheitsentscheid statt nächstem Nachbarn; Ellipsis entfernt; Filter/Tanken-Fixes

- entitaetIdKurz(): Präfix per Stimmenmehrheit über alle zugeordneten Sensoren statt
  des einzelnen nächsten Nachbarn - verhindert Über-Kürzung durch zufällig geteilte
  Namensteile zwischen verwandten Feldern (can_fuel_volume->fuel_volume,
  oil_change_due->due waren falsch).
- .setup-feld-hinweis: Ellipsis-Kürzung entfernt, voller Sensorname sichtbar.
- entitaetKandidaten(): "Nur passende Sensoren anzeigen" filtert jetzt auch nach
  device_class für Datum/Timestamp-Felder, nicht nur nach Domain/Einheit.
- tankFelder(): Kilometerstand/Getankte Liter in .mitEinheit gewrappt (wie alle
  anderen km/l-Felder im Rest der App).
- distanzVorschlag(): negative Vorschläge (fehlerhafte km-Basis eines vorherigen
  Tankvorgangs) werden nicht mehr angezeigt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 11:15:12 +02:00
parent 4f85a179cf
commit d1cc742a59
4 changed files with 124 additions and 29 deletions
+67
View File
@@ -4272,6 +4272,49 @@ and their `_STRECKE_SENSOR` counterparts (`oil_change_distance`/`inspection_dist
other specifically and reduce to `"due"`/`"distance"` - each field lands next to its true sibling, not just other specifically and reduce to `"due"`/`"distance"` - each field lands next to its true sibling, not just
"some" shared prefix. "some" shared prefix.
**Follow-up (`.9`), two more owner-reported items on the same session:**
1. **"Nur passende Sensoren anzeigen" only hard-filtered by domain/unit, not by the "Datum" output shown
next to a field's headline** (`feldEinheitAnzeige()`). Fields with `device_classes: ["date",
"timestamp"]` and no `units` (the two Ölwechsel/Inspektion date roles) fell through both the domain
check (only restricts to `sensor` domain, which is nearly every entity) and the unit check (empty
`units` array skips it entirely) - so with the filter ON, every `sensor.*` entity in the whole instance
was offered, not just date/timestamp ones. `entitaetKandidaten()` now mirrors `feldEinheitAnzeige()`'s
own decision exactly: when a field has no `units` but its `device_classes` includes `date`/`timestamp`,
a candidate must itself have `device_class` `date` or `timestamp` to pass the hard filter. Verified live
by reading the actual rendered candidate list for "Nächster Ölwechsel (Datum)" via the shadow DOM and
cross-checking each of the 14 results' real `device_class` against `hass.states` - all 14 were genuine
`date`/`timestamp` sensors, zero false positives.
2. **`Neuer Tankvorgang` (`tankFelder()`): "Kilometerstand" and "Getankte Liter" were the only two fields
in the form without the `.mitEinheit` wrapper** every other km/€/l field in the app already uses (see
e.g. the Wartungsplan "Kilometerstand"/"Kosten" fields, `vSbuch()`) - so they rendered at the browser's
default, much wider intrinsic input size instead of the app's usual fixed compact width, and without a
unit suffix. Wrapped both in `.mitEinheit` (`km`/`l`), matching the existing convention instead of
introducing new CSS.
3. **Real bug: "Gefahrene Distanz" showed `-98` when opening a new Tankvorgang.** `distanzVorschlag()`
subtracts the last recorded fill's `odometer_km` from the live odometer with no floor - live-inspected
the actual data in `audi_ha_test` and found the most recent stored fill (`f-c7b0e818451e`, `source:
"auto"`, 2026-08-27T17:37) carries `odometer_km: 21400`, while the vehicle's current live odometer
(`sensor.audi_dashboard_fahrzeugstatus`'s `km`) reads `21302` - lower than that fill's own recorded
basis, so the subtraction went negative (`21302 - 21400 = -98`, exactly the reported number). Left the
underlying record itself untouched - unclear whether it's a genuine real-vehicle detection or leftover
test-data noise, and either way it's the owner's own trip/tank history, not something to delete
unilaterally. Fixed the display symptom instead: `distanzVorschlag()` now returns `""` (no suggestion)
rather than a negative number - a negative "distance driven" is never meaningful regardless of why the
underlying data disagrees. **Worth the owner's own look**: that stray fill's `odometer_km` (21400)
exceeding the current live reading (21302) suggests either a bad detection or a since-corrected odometer
somewhere in this test container's history - if it recurs on the real instance, worth checking there too.
4. Also handled the owner's report "there is still the Text with the round collums in the Sensor Page"
(referring to the `.7`/`.8` ellipsis fix) - re-verified directly against the live served CSS and by
scanning every text node and placeholder in the rendered Setup popup's DOM: the ellipsis truncation is
genuinely gone server-side, nothing found. Most likely a stale cached browser tab on the owner's side
(this project has hit that exact class of issue repeatedly - see section N) rather than a remaining code
defect; flagged to the owner rather than guessed around further.
Verified: `node --check` clean, manifest bumped to `2026.8.28.9`, `audi_ha_test` restarted and confirmed
clean via log; all three fixes re-verified live in the browser (Tanken form screenshot showing `0` instead
of `-98` and the shrunk `Kilometerstand`/`Getankte Liter` fields; Setup candidate-list device_class check
above).
Verified live in `audi_ha_test` at every step (`.6` then `.7`), each time reading the actual rendered Setup 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')` 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 showing the new `?v=` served with a nonzero transfer size each time). Final state (`.7`), all 20 single-value
@@ -4281,6 +4324,30 @@ fields checked in one pass: every FMM003 role shows its bare signal name
from either source. `node --check` clean at each step. Manifest `2026.8.28.5``.6``.7`, `audi_ha_test` 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. restarted and confirmed clean via log each time.
**`.7`'s "due"/"distance" reduction was itself wrong - owner caught it (`.8`): "Tankvolumen only shows
'fuel_volume' but the Sensor is 'can_fuel_volume'... nächster Ölwechsel is 'due' but shall be
'oil_change_due'."** `.7`'s per-entity nearest-single-relative algorithm still over-stripped: two fields from
the SAME sensor source can themselves share a namepart that has nothing to do with the installation prefix
(`oil_change_due`/`oil_change_distance` share `"oil_change_"`; `can_fuel_volume` shares `"can_"` with another
CAN-sourced sensor) - the nearest relative for a given entity is exactly the one most likely to share extra,
misleading structure with it, so picking the single nearest relative's prefix was the wrong metric all along,
not just an edge case. Replaced with a majority vote: every OTHER mapped entity casts one "vote" for whatever
prefix it shares with the entity being displayed (via the existing `praefixPaar()`), and the prefix with the
most votes wins (ties favor the shorter/safer one). The true installation prefix wins because an entity
shares it with nearly every other sensor from the same source (many votes); an incidental shared namepart
like `"oil_change_"` is shared with only the one sibling field (one vote) - so the majority is structurally
biased toward the correct, install-level prefix regardless of how many simultaneous sensor sources exist.
Also removed the CSS ellipsis truncation on the sensor-identity hint (`.setup-feld-hinweis` in
`audi-dashboard.css` had `white-space:nowrap;overflow:hidden;text-overflow:ellipsis`) per the owner's
explicit "remove the round collum (...)" - full identifiers like `can_fuel_volume` now show in full
(wrapping within the existing `max-width:55%` instead of truncating). Verified live in `audi_ha_test` at
`.8`: every field re-checked against the real Setup data - `can_fuel_volume`, `oil_change_due`/
`oil_change_distance`, `inspection_due`/`inspection_distance` all now show their full, correct suffix; the
FMM003 cluster (`external_power_voltage`, `can_ambient_air_temperature`, ...) and the `audi_rs_4_avant_`
cluster (`total_vehicle_mileage_read_from_can`, `front_left_door`, ...) both still strip correctly - the
majority-vote fix didn't regress the two-simultaneous-sources case `.7` was built to solve. `node --check`
clean, manifest bumped to `2026.8.28.8`, `audi_ha_test` restarted and confirmed clean via log.
--- ---
## Working conventions (observed — keep them) ## Working conventions (observed — keep them)
@@ -2759,7 +2759,15 @@ function entitaetKandidaten(feldKey, suchtext, aktuellerWert) {
if (e.id === aktuellerWert) return true; if (e.id === aktuellerWert) return true;
if (setupNurPassend) { if (setupNurPassend) {
if (feld.domains && feld.domains.length && !feld.domains.includes(e.domain)) return false; if (feld.domains && feld.domains.length && !feld.domains.includes(e.domain)) return false;
if (feld.units && feld.units.length && !feld.units.includes(e.unit)) return false; if (feld.units && feld.units.length) {
if (!feld.units.includes(e.unit)) return false;
} else if (feld.device_classes && (feld.device_classes.includes("date") || feld.device_classes.includes("timestamp"))) {
// Kein Einheiten-Feld, aber "Datum" ist der angezeigte Output
// (feldEinheitAnzeige()) - dieselbe Bedingung hier gespiegelt, damit
// "Nur passende Sensoren anzeigen" wirklich nur Sensoren mit
// passendem Output zeigt, nicht jeden beliebigen sensor.*.
if (e.deviceClass !== "date" && e.deviceClass !== "timestamp") return false;
}
} }
if (text && !(e.id.toLowerCase().includes(text) || e.name.toLowerCase().includes(text))) return false; if (text && !(e.id.toLowerCase().includes(text) || e.name.toLowerCase().includes(text))) return false;
return true; return true;
@@ -3517,7 +3525,13 @@ function distanzVorschlag(aktuellerKm) {
if (aktuellerKm === "" || aktuellerKm == null || !FILLS.length) return ""; if (aktuellerKm === "" || aktuellerKm == null || !FILLS.length) return "";
const letzte = FILLS.reduce((a, b) => (new Date(b.ts) > new Date(a.ts) ? b : a)); const letzte = FILLS.reduce((a, b) => (new Date(b.ts) > new Date(a.ts) ? b : a));
if (letzte.odometer_km == null) return ""; if (letzte.odometer_km == null) return "";
return Math.round((aktuellerKm - letzte.odometer_km) * 10) / 10; const distanz = Math.round((aktuellerKm - letzte.odometer_km) * 10) / 10;
// Ein negativer Wert ist nie eine gefahrene Distanz - kann bei einer
// fehlerhaft/verzögert erfassten Kilometerstand-Basis des letzten
// Tankvorgangs vorkommen (z. B. ein "auto" erkannter Vorgang, dessen
// km-Basis über einen späteren, korrigierten/niedrigeren Kilometerstand
// hinausragt). Lieber kein Vorschlag als ein irreführender negativer.
return distanz < 0 ? "" : distanz;
} }
function tankFelder(praefix, f) { function tankFelder(praefix, f) {
@@ -3541,12 +3555,14 @@ function tankFelder(praefix, f) {
return ` return `
<div class="feld"><label>Datum</label><input type="date" id="${praefix}Datum" value="${datum}"></div> <div class="feld"><label>Datum</label><input type="date" id="${praefix}Datum" value="${datum}"></div>
<div class="feld"><label>Uhrzeit</label><input type="time" id="${praefix}Zeit" value="${zeit}"></div> <div class="feld"><label>Uhrzeit</label><input type="time" id="${praefix}Zeit" value="${zeit}"></div>
<div class="feld"><label>Kilometerstand</label><input type="number" id="${praefix}Km" value="${km}"></div> <div class="feld"><label>Kilometerstand</label>
<span class="mitEinheit"><input type="number" id="${praefix}Km" value="${km}"><span>km</span></span></div>
<div class="feld"><label>Gefahrene Distanz</label> <div class="feld"><label>Gefahrene Distanz</label>
<span class="mitEinheit"><input type="number" step="1" id="${praefix}Distanz" value="${distanz}" placeholder="0"><span>km</span></span></div> <span class="mitEinheit"><input type="number" step="1" id="${praefix}Distanz" value="${distanz}" placeholder="0"><span>km</span></span></div>
<div class="feld"><label>Kraftstoff</label> <div class="feld"><label>Kraftstoff</label>
<select id="${praefix}Kraftstoff">${KRAFTSTOFFSORTEN.map((k) => `<option ${k === kraftstoff ? "selected" : ""}>${k}</option>`).join("")}</select></div> <select id="${praefix}Kraftstoff">${KRAFTSTOFFSORTEN.map((k) => `<option ${k === kraftstoff ? "selected" : ""}>${k}</option>`).join("")}</select></div>
<div class="feld"><label>Getankte Liter</label><input type="number" step="0.01" id="${praefix}Liter" data-tankpreisfeld="${praefix}" value="${liter}" placeholder="0,00"></div> <div class="feld"><label>Getankte Liter</label>
<span class="mitEinheit"><input type="number" step="0.01" id="${praefix}Liter" data-tankpreisfeld="${praefix}" value="${liter}" placeholder="0,00"><span>l</span></span></div>
<div class="feld"><label>Kosten (real, nach Rabatt)</label> <div class="feld"><label>Kosten (real, nach Rabatt)</label>
<span class="mitEinheit"><input type="number" step="0.01" id="${praefix}Kosten" data-tankpreisfeld="${praefix}" value="${kosten}" placeholder="0,00"><span></span></span></div> <span class="mitEinheit"><input type="number" step="0.01" id="${praefix}Kosten" data-tankpreisfeld="${praefix}" value="${kosten}" placeholder="0,00"><span></span></span></div>
${CAR.smartdeal.aktiv ? `<div class="feld"><label>SmartDeal-Ersparnis</label> ${CAR.smartdeal.aktiv ? `<div class="feld"><label>SmartDeal-Ersparnis</label>
@@ -4082,35 +4098,47 @@ function setupAndereIdsOhneDomain(ausser) {
return ids.map((id) => id.includes(".") ? id.split(".").slice(1).join(".") : id); return ids.map((id) => id.includes(".") ? id.split(".").slice(1).join(".") : id);
} }
/* Die Entity-ID ohne Domain und ohne das installationsspezifische Präfix, /* Die Entity-ID ohne Domain und ohne das installationsspezifische Präfix -
das sie mit ihrem NÄCHSTEN Verwandten unter den übrigen zugeordneten die tatsächliche, stabile Kennung des zugeordneten Sensors, unabhängig
Sensoren teilt (dem, mit dem sie das längste gemeinsame Präfix hat) - die davon, wie Geräte-Slug und Integrationsinstanz beim jeweiligen Nutzer
tatsächliche, stabile Kennung des zugeordneten Sensors, unabhängig davon, heißen (z. B. "testcar_b9_fmm003_testintegratoin_" oder
wie Geräte-Slug und Integrationsinstanz beim jeweiligen Nutzer heißen "audi_rs_4_avant_").
(z. B. "testcar_b9_fmm003_testintegratoin_" oder "audi_rs_4_avant_").
Bewusst PRO ENTITÄT der beste Nachbar statt eines einzigen, für alle Das Präfix ist NICHT das mit dem einzelnen nächsten Verwandten geteilte
Felder gemeinsamen Präfixes (frühere Fassung) - eine reale Installation (frühere Fassung) - das kürzte in der Praxis zu viel: zwei Felder
hat oft MEHRERE Sensorquellen gleichzeitig (hier: FMM003 für Fahrt-/ derselben Sensorquelle können selbst wieder ein gemeinsames Namens-
Tank-/Batteriewerte, eine zweite Integration für Türen/Fenster/ Teilstück haben, das nichts mit der Installation zu tun hat (z. B. teilen
Service-Termine, per "Nur passende Sensoren anzeigen" beide gleichzeitig sich "oil_change_due" und "oil_change_distance" das Wortstück
vorgeschlagen) - keine ist objektiv "die Mehrheit". Ein einziges globales "oil_change_", "can_fuel_volume" teilt sich "can_" mit einem anderen
Präfix (egal ob per fortlaufender Verengung oder per Mehrheitsentscheid) CAN-Sensor) - genau das hat der Nutzer live beobachtet: "can_fuel_volume"
kann immer nur EINE Quelle richtig kürzen und zeigt die Sensoren der wurde zu bloß "fuel_volume", "oil_change_due" zu bloß "due". Stattdessen
anderen ungekürzt - genau das hat der Nutzer live beobachtet zählt jedes Präfix, das eine Entität mit irgendeiner ANDEREN teilt, als
("audi_rs_4_avant_" gewann die Abstimmung, alle FMM003-Felder blieben eine Stimme; das Präfix mit den MEISTEN Stimmen gewinnt. Das echte
unangetastet). Mit dem nächsten Verwandten statt einem globalen Sieger Installationspräfix teilt sich die Entität mit fast allen anderen
kürzt jede Rolle unabhängig von den anderen richtig, egal wie viele Sensoren ihrer Quelle (viele Stimmen), ein zufällig geteiltes
verschiedene Quellen gleichzeitig im Spiel sind. Findet sich kein Namens-Teilstück nur mit dem einen verwandten Feld (eine Stimme) - die
Verwandter mit gemeinsamem Präfix, bleibt die volle domain-freie ID Mehrheit entscheidet also richtig, ohne einen einzelnen "nächsten"
stehen statt einer geratenen, möglicherweise falschen Kürzung. */ Nachbarn zu bevorzugen. Bei Gleichstand gewinnt das kürzere Präfix
(vorsichtiger - weniger Kürzung im Zweifel). Mehrere gleichzeitige
Sensorquellen (z. B. FMM003 und eine zweite, "audi_rs_4_avant_"-
präfixte Integration) bleiben dabei weiterhin unabhängig voneinander
korrekt gekürzt, da die Stimmen pro Entität und nicht global gezählt
werden. 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) { 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;
let bestesPraefix = null, besteLaenge = 0; const stimmen = new Map();
for (const andere of setupAndereIdsOhneDomain(entityId)) { for (const andere of setupAndereIdsOhneDomain(entityId)) {
const kandidat = praefixPaar(ohneDomain, andere); const kandidat = praefixPaar(ohneDomain, andere);
if (kandidat && kandidat.length > besteLaenge) { bestesPraefix = kandidat; besteLaenge = kandidat.length; } if (!kandidat) continue;
stimmen.set(kandidat, (stimmen.get(kandidat) || 0) + 1);
}
let bestesPraefix = null, besteStimmen = 0;
for (const [kandidat, anzahl] of stimmen) {
const besser = anzahl > besteStimmen || (anzahl === besteStimmen && bestesPraefix && kandidat.length < bestesPraefix.length);
if (besser) { bestesPraefix = kandidat; besteStimmen = anzahl; }
} }
return bestesPraefix ? ohneDomain.slice(bestesPraefix.length) : ohneDomain; return bestesPraefix ? ohneDomain.slice(bestesPraefix.length) : ohneDomain;
} }
@@ -1265,7 +1265,7 @@ button.leaf:active { background: var(--tile-2); }
.setup-feld-einheit { color: var(--fg3); font-size: 12.5px; } .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%; max-width: 55%;
} }
.setup-unterfelder { display: flex; flex-direction: column; gap: 12px; } .setup-unterfelder { display: flex; flex-direction: column; gap: 12px; }
.setup-unterfeld { display: flex; flex-direction: column; gap: 4px; } .setup-unterfeld { display: flex; flex-direction: column; gap: 4px; }
@@ -1,7 +1,7 @@
{ {
"domain": "audi_dashboard", "domain": "audi_dashboard",
"name": "Audi Dashboard", "name": "Audi Dashboard",
"version": "2026.8.28.7", "version": "2026.8.28.9",
"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"],