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
@@ -2759,7 +2759,15 @@ function entitaetKandidaten(feldKey, suchtext, aktuellerWert) {
if (e.id === aktuellerWert) return true;
if (setupNurPassend) {
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;
return true;
@@ -3517,7 +3525,13 @@ function distanzVorschlag(aktuellerKm) {
if (aktuellerKm === "" || aktuellerKm == null || !FILLS.length) return "";
const letzte = FILLS.reduce((a, b) => (new Date(b.ts) > new Date(a.ts) ? b : a));
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) {
@@ -3541,12 +3555,14 @@ function tankFelder(praefix, f) {
return `
<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>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>
<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>
<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>
<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>
@@ -4082,35 +4098,47 @@ function setupAndereIdsOhneDomain(ausser) {
return ids.map((id) => id.includes(".") ? id.split(".").slice(1).join(".") : id);
}
/* 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_").
/* Die Entity-ID ohne Domain und ohne das installationsspezifische Präfix -
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. */
Das Präfix ist NICHT das mit dem einzelnen nächsten Verwandten geteilte
(frühere Fassung) - das kürzte in der Praxis zu viel: zwei Felder
derselben Sensorquelle können selbst wieder ein gemeinsames Namens-
Teilstück haben, das nichts mit der Installation zu tun hat (z. B. teilen
sich "oil_change_due" und "oil_change_distance" das Wortstück
"oil_change_", "can_fuel_volume" teilt sich "can_" mit einem anderen
CAN-Sensor) - genau das hat der Nutzer live beobachtet: "can_fuel_volume"
wurde zu bloß "fuel_volume", "oil_change_due" zu bloß "due". Stattdessen
zählt jedes Präfix, das eine Entität mit irgendeiner ANDEREN teilt, als
eine Stimme; das Präfix mit den MEISTEN Stimmen gewinnt. Das echte
Installationspräfix teilt sich die Entität mit fast allen anderen
Sensoren ihrer Quelle (viele Stimmen), ein zufällig geteiltes
Namens-Teilstück nur mit dem einen verwandten Feld (eine Stimme) - die
Mehrheit entscheidet also richtig, ohne einen einzelnen "nächsten"
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) {
if (!entityId) return null;
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)) {
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;
}
@@ -1265,7 +1265,7 @@ button.leaf:active { background: var(--tile-2); }
.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%;
max-width: 55%;
}
.setup-unterfelder { display: flex; flex-direction: column; gap: 12px; }
.setup-unterfeld { display: flex; flex-direction: column; gap: 4px; }