Setup: Erwartungswert je Listenposition, gruen auch dort (2026.9.1.15)

Tuer- und Fenstersensoren haben vier Positionen, aber nur einen Beispielnamen
(front_left_door). Der beschreibt die erste; die anderen drei koennen ihn nie
enthalten. Folge: nur die erste Position konnte gruen werden, und im Kopf
stand ein Erwartungswert, der fuer drei von vier Zeilen nicht galt.

Neu "beispiele" je Position im Feld-Katalog und feldBeispiel(feld, idx) im
Panel. Der Erwartungswert steht jetzt an jeder Position statt im Kopf -
rechtsbuendig, in der kleinen grauen Schrift des Positionslabels. Auch die
Sortierung der Vorschlagsliste nutzt ihn, sonst stuende in allen vier Zeilen
dieselbe Entitaet oben.

Typografie: .setup-feld-hinweis ist fuer die Kopfzeile gebaut (14px, --fg,
rechtsbuendig, max-width 55%). Im 12,5px-Positionslabel wirkt das falsch herum.
Eine Regel im Kontext .setup-unterfeld-label .setup-feld-hinweis laesst Groesse
und Farbe erben; die Rechtsbuendigkeit kommt aus justify-content: space-between
am Label, wie eine Ebene darueber bei .setup-feld-kopf.

Verifiziert: alle acht Positionen tragen ihren eigenen Beispielnamen,
py_compile und Panel als Modul sauber, audi_ha_test ohne Fehler gestartet.
Die Darstellung selbst habe ich nicht im Browser gesehen - das Panel liess
sich in der Testinstanz nicht ansteuern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 19:00:16 +02:00
parent 09c7461892
commit 7e2854fe8d
7 changed files with 115 additions and 19 deletions
+41 -1
View File
@@ -1,6 +1,7 @@
# AGENTS.md — Project state, review findings, open items, and working rules # AGENTS.md — Project state, review findings, open items, and working rules
**Last updated: 2026-09-01** (Rueckblick ohne Nullfahrten, Streckenwahl gemeinsam in `verlauf.py`, **Last updated: 2026-09-01** (Setup: gruene Bestaetigung und Erwartungswert je Listenposition,
`2026.9.1.15`, Abschnitt BG. Davor: Rueckblick ohne Nullfahrten, Streckenwahl gemeinsam in `verlauf.py`,
`2026.9.1.14`, Abschnitt BF. Davor: Fahrtstrecke auf 100 m: CAN als Anker, GNSS als Nachkommastelle, `2026.9.1.14`, Abschnitt BF. Davor: Fahrtstrecke auf 100 m: CAN als Anker, GNSS als Nachkommastelle,
`2026.9.1.10`, Abschnitt BE. Davor: Nachlauf-Abzug nur mit zugeordnetem `TRIP_SENSOR`, `2026.9.1.9`; `2026.9.1.10`, Abschnitt BE. Davor: Nachlauf-Abzug nur mit zugeordnetem `TRIP_SENSOR`, `2026.9.1.9`;
davor „502" beim Neustart entschärft, OTA-Bündel auf `2026.9.1.8` davor „502" beim Neustart entschärft, OTA-Bündel auf `2026.9.1.8`
@@ -6879,6 +6880,45 @@ weglaesst.
innerhalb einer einzigen langen `trip_status`-Phase. Nachweisbar bleibt es an den 13 Faellen; live innerhalb einer einzigen langen `trip_status`-Phase. Nachweisbar bleibt es an den 13 Faellen; live
zeigt es sich bei der naechsten kurzen Fahrt. zeigt es sich bei der naechsten kurzen Fahrt.
## BG. Setup: gruene Bestaetigung, Erwartungswert je Position (2026.9.1.11.15)
Zwei Wuensche des Eigentuemers vom 01.09.2026, die zusammengehoeren.
### Gruen, wenn die Zuordnung zum Beispielnamen passt
Enthaelt die zugeordnete Entitaet den Beispielnamen der Rolle, faerbt sich das Feld leicht gruen.
Dieselbe Pruefung, die `entitaetScore()` ohnehin mit 8 gewichtet - hier nur fuers Auge, damit man
eine richtig belegte Rolle sieht, ohne den langen Entity-Namen Zeichen fuer Zeichen mit dem Beispiel
daneben zu vergleichen.
Bewusst **nur ein Hinweis, keine Bedingung**: Installationen mit anderer Datenquelle haben andere
Namen und sind trotzdem richtig zugeordnet. Deshalb gruen als Bestaetigung, aber kein Rot und keine
Warnung, wenn es fehlt. Und nicht waehrend gesucht wird - dann steht die Sucheingabe im Feld, nicht
die Zuordnung. Die Farbe kommt per `color-mix` aus dem vorhandenen `--ok`-Ton und dem Feldgrund
statt aus einem eigenen Gruen, damit sie in beiden Themen ruhig bleibt.
`„Auf Standard zuruecksetzen"` ist im selben Zug entfallen - Knopf, Symbol, Zuhoerer, CSS und der
nur dafuer vorhandene Helfer `setupStandardwert()`.
### Listenfelder brauchen vier Beispiele, nicht eines
Tuer- und Fenstersensoren haben vier Positionen, aber nur **einen** Beispielnamen
(`front_left_door`). Der beschreibt die erste; die anderen drei koennen ihn nie enthalten. Folge:
nur die erste Position konnte gruen werden, und im Kopf stand ein Erwartungswert, der fuer drei von
vier Zeilen nicht galt.
Neu: `"beispiele"` je Position im Feld-Katalog, `feldBeispiel(feld, idx)` im Panel. Der
Erwartungswert steht jetzt **an jeder Position** statt im Kopf - rechtsbuendig, in der kleinen grauen
Schrift des Positionslabels. Auch die Sortierung der Vorschlagsliste nutzt ihn, sonst stuende in
allen vier Zeilen dieselbe Entitaet oben.
**Merkposten zur Typografie:** `.setup-feld-hinweis` ist fuer die Kopfzeile gebaut (14px, `--fg`,
rechtsbuendig, `max-width: 55%`). Im 12,5px-Positionslabel wirkt das falsch herum - groesser und
dunkler als die Zeile, in der es steht. Eine eigene Regel im Kontext
`.setup-unterfeld-label .setup-feld-hinweis` laesst es Groesse und Farbe erben; die Rechtsbuendigkeit
kommt aus `justify-content: space-between` am Label, wie eine Ebene darueber bei
`.setup-feld-kopf`.
## Working conventions (observed — keep them) ## Working conventions (observed — keep them)
- German is the project language: identifiers, comments, commits, UI texts. Exceptions: - German is the project language: identifiers, comments, commits, UI texts. Exceptions:
@@ -293,11 +293,26 @@ FELDER: list[dict] = [
"hinweis": "\"aus\"/off = zu. Für \"Sicher abgestellt\" wichtig.", "hinweis": "\"aus\"/off = zu. Für \"Sicher abgestellt\" wichtig.",
"domains": ["binary_sensor"], "device_classes": ["door"], "units": [], "liste": True, "positionen": POSITIONEN, "domains": ["binary_sensor"], "device_classes": ["door"], "units": [], "liste": True, "positionen": POSITIONEN,
"pflicht": False, "beispiel": "front_left_door", "pflicht": False, "beispiel": "front_left_door",
# Ein Beispielname je Position. "beispiel" allein beschreibt nur die
# erste; die anderen drei koennen "front_left_door" nie enthalten und
# standen deshalb ohne Erwartungswert da.
"beispiele": [
"front_left_door",
"front_right_door",
"rear_left_door",
"rear_right_door",
],
"stichworte": ["tür", "door"]}, "stichworte": ["tür", "door"]},
{"key": "FENSTER_SENSOREN", "label": "Fenster", "gruppe": "sicherheit", {"key": "FENSTER_SENSOREN", "label": "Fenster", "gruppe": "sicherheit",
"hinweis": "\"aus\"/off = zu.", "hinweis": "\"aus\"/off = zu.",
"domains": ["binary_sensor"], "device_classes": ["window"], "units": [], "liste": True, "positionen": POSITIONEN, "domains": ["binary_sensor"], "device_classes": ["window"], "units": [], "liste": True, "positionen": POSITIONEN,
"pflicht": False, "beispiel": "front_left_window", "pflicht": False, "beispiel": "front_left_window",
"beispiele": [
"front_left_window",
"front_right_window",
"rear_left_window",
"rear_right_window",
],
"stichworte": ["fenster", "window"]}, "stichworte": ["fenster", "window"]},
{"key": "HECKKLAPPE_SENSOR", "label": "Heckklappe", "gruppe": "sicherheit", {"key": "HECKKLAPPE_SENSOR", "label": "Heckklappe", "gruppe": "sicherheit",
"hinweis": "\"aus\"/off = zu.", "hinweis": "\"aus\"/off = zu.",
@@ -1 +1 @@
{"version":"2026.9.1.14","sha256":"f1fa3a91435bfc80f3f4245e52c488e3487fe12e5031349e1d7d20ca757d4b7d","bytes":265727,"gebaut":"2026-09-01T16:47:50Z"} {"version":"2026.9.1.15","sha256":"c7eda6e1a85b2d4295b0bd21383b66895dfb217a5a788a6ccf92acee467cad4c","bytes":265741,"gebaut":"2026-09-01T16:59:16Z"}
@@ -3044,7 +3044,7 @@ function entitaetName(id) {
Domain/Einheit/Device-Class sind Zusatzpunkte fürs Sortieren, wenn der Domain/Einheit/Device-Class sind Zusatzpunkte fürs Sortieren, wenn der
Schalter "Nur passende Sensoren" aus ist (dann filtern sie nicht hart, Schalter "Nur passende Sensoren" aus ist (dann filtern sie nicht hart,
siehe entitaetKandidaten()). */ siehe entitaetKandidaten()). */
function entitaetScore(feld, e, suchtext) { function entitaetScore(feld, e, suchtext, idx) {
let score = 0; let score = 0;
const hay = (e.id + " " + e.name).toLowerCase(); const hay = (e.id + " " + e.name).toLowerCase();
for (const wort of feld.stichworte || []) if (hay.includes(wort)) score += 5; for (const wort of feld.stichworte || []) if (hay.includes(wort)) score += 5;
@@ -3054,12 +3054,15 @@ function entitaetScore(feld, e, suchtext) {
// Der Signalname aus dem Katalog ("z. B. can_fuel_volume") ist der // Der Signalname aus dem Katalog ("z. B. can_fuel_volume") ist der
// treffsicherste Hinweis, den es gibt - deutlich mehr wert als ein // treffsicherste Hinweis, den es gibt - deutlich mehr wert als ein
// einzelnes Stichwort. // einzelnes Stichwort.
if (feld.beispiel && hay.includes(feld.beispiel.toLowerCase())) score += 8; // Bei Listenfeldern der Name der jeweiligen Position, nicht der der ersten -
// sonst stuende in allen vier Zeilen dieselbe Entitaet oben.
const beispiel = feldBeispiel(feld, idx);
if (beispiel && hay.includes(beispiel.toLowerCase())) score += 8;
if (suchtext && hay.includes(suchtext.trim().toLowerCase())) score += 10; if (suchtext && hay.includes(suchtext.trim().toLowerCase())) score += 10;
return score; return score;
} }
function entitaetKandidaten(feldKey, suchtext, aktuellerWert) { function entitaetKandidaten(feldKey, suchtext, aktuellerWert, idx) {
const feld = setupKatalog().find((f) => f.key === feldKey); const feld = setupKatalog().find((f) => f.key === feldKey);
if (!feld) return []; if (!feld) return [];
const text = (suchtext || "").trim().toLowerCase(); const text = (suchtext || "").trim().toLowerCase();
@@ -3101,7 +3104,7 @@ function entitaetKandidaten(feldKey, suchtext, aktuellerWert) {
); );
if (genau.length) liste = genau; if (genau.length) liste = genau;
} }
liste.sort((a, b) => entitaetScore(feld, b, suchtext) - entitaetScore(feld, a, suchtext)); liste.sort((a, b) => entitaetScore(feld, b, suchtext, idx) - entitaetScore(feld, a, suchtext, idx));
return liste.slice(0, 60); return liste.slice(0, 60);
} }
@@ -3192,11 +3195,34 @@ function setupSchliessen() {
anderer Datenquelle, deren Namen nicht passen und die trotzdem richtig anderer Datenquelle, deren Namen nicht passen und die trotzdem richtig
zugeordnet sind. Deshalb grün als Bestätigung, aber kein Rot und keine zugeordnet sind. Deshalb grün als Bestätigung, aber kein Rot und keine
Warnung, wenn es fehlt. */ Warnung, wenn es fehlt. */
function entitaetPasstZumBeispiel(key, wert) { function entitaetPasstZumBeispiel(key, idx, wert) {
if (!wert) return false; if (!wert) return false;
const feld = setupKatalog().find((f) => f.key === key); const beispiel = feldBeispiel(setupKatalog().find((f) => f.key === key), idx);
if (!feld || !feld.beispiel) return false; if (!beispiel) return false;
return String(wert).toLowerCase().includes(String(feld.beispiel).toLowerCase()); return String(wert).toLowerCase().includes(String(beispiel).toLowerCase());
}
/* Der Beispielname für eine bestimmte Position einer Rolle.
Listenfelder (Tür- und Fenstersensoren, vier Positionen) haben je Position
einen eigenen Namen: die zweite Position kann "front_left_door" nie
enthalten. Ohne diese Unterscheidung stand nur bei der ersten Position ein
Erwartungswert, und nur sie konnte grün werden - vom Eigentümer beanstandet
am 01.09.2026. */
function feldBeispiel(feld, idx) {
if (!feld) return null;
if (idx != null && Array.isArray(feld.beispiele) && feld.beispiele[idx]) {
return feld.beispiele[idx];
}
return feld.beispiel || null;
}
/* Der Klammertext "(z. B. )" - im Kopf einer Rolle wie an einer
Listenposition derselbe Bau, damit beide gleich aussehen. */
function beispielHinweis(name) {
return name
? `<span class="setup-feld-hinweis">(z. B. <span class="setup-feld-einheit">${esc(name)}</span>)</span>`
: "";
} }
/* Entity-IDs, die im Arbeitsstand mehr als einer Rolle zugeordnet sind. */ /* Entity-IDs, die im Arbeitsstand mehr als einer Rolle zugeordnet sind. */
@@ -4513,7 +4539,11 @@ function feldEinheitAnzeige(feld) {
} }
function entitaetZeilenMarkup(feldId, key, suchtext, aktuellerWert) { function entitaetZeilenMarkup(feldId, key, suchtext, aktuellerWert) {
const kandidaten = entitaetKandidaten(key, suchtext, aktuellerWert); // Die Position steckt in der Feld-Kennung ("KEY:idx") - sie entscheidet,
// welcher Beispielname die Vorschlagsliste anfuehrt.
const teile = String(feldId).split(":");
const idx = teile.length === 2 ? Number(teile[1]) : null;
const kandidaten = entitaetKandidaten(key, suchtext, aktuellerWert, idx);
if (!kandidaten.length) return `<div class="entitaet-leer">Keine passende Entität gefunden</div>`; if (!kandidaten.length) return `<div class="entitaet-leer">Keine passende Entität gefunden</div>`;
return kandidaten.map((e) => { return kandidaten.map((e) => {
const zustand = HASS.states[e.id] ? HASS.states[e.id].state : null; const zustand = HASS.states[e.id] ? HASS.states[e.id].state : null;
@@ -4536,7 +4566,7 @@ function entitaetCombo(key, idx) {
// Waehrend gesucht wird nicht gruen faerben - der Text im Feld ist dann // Waehrend gesucht wird nicht gruen faerben - der Text im Feld ist dann
// die Sucheingabe und nicht die Zuordnung, die Faerbung wuerde ihm // die Sucheingabe und nicht die Zuordnung, die Faerbung wuerde ihm
// widersprechen. // widersprechen.
const treffer = !offen && entitaetPasstZumBeispiel(key, wert); const treffer = !offen && entitaetPasstZumBeispiel(key, idx, wert);
return `<div class="entitaet-combo"> return `<div class="entitaet-combo">
<span class="entitaet-combo-icon">${LUPE_SVG}</span> <span class="entitaet-combo-icon">${LUPE_SVG}</span>
<input type="text" class="entitaet-combo-input${treffer ? " passend" : ""}" autocomplete="off" spellcheck="false" <input type="text" class="entitaet-combo-input${treffer ? " passend" : ""}" autocomplete="off" spellcheck="false"
@@ -4570,12 +4600,13 @@ function setupFeldzeile(feld) {
Listenfelder bekommen es ebenfalls: dort ist der Name der ersten Listenfelder bekommen es ebenfalls: dort ist der Name der ersten
Position das Muster, nach dem sich die anderen drei richten. */ Position das Muster, nach dem sich die anderen drei richten. */
const erwartet = feld.beispiel || ""; // Bei Listenfeldern steht das Beispiel NICHT im Kopf, sondern an jeder
// Position - siehe unten. Ein einzelner Name im Kopf beschriebe nur die
// erste der vier und liesse die anderen drei ohne Erwartungswert dastehen.
const erwartet = feld.liste ? "" : feld.beispiel || "";
// Der Klammertext in Label-Größe/-Farbe, der Sensorname darin in derselben // Der Klammertext in Label-Größe/-Farbe, der Sensorname darin in derselben
// kleinen/grauen Schrift wie die Einheit ("[on/off]"). // kleinen/grauen Schrift wie die Einheit ("[on/off]").
const hinweis = erwartet const hinweis = beispielHinweis(erwartet);
? `<span class="setup-feld-hinweis">(z. B. <span class="setup-feld-einheit">${esc(erwartet)}</span>)</span>`
: "";
const kopf = `<div class="setup-feld-kopf"> const kopf = `<div class="setup-feld-kopf">
<span class="setup-feld-label">${esc(feld.label)}${einheit ? ` <span class="setup-feld-einheit">[${esc(einheit)}]</span>` : ""}</span> <span class="setup-feld-label">${esc(feld.label)}${einheit ? ` <span class="setup-feld-einheit">[${esc(einheit)}]</span>` : ""}</span>
${hinweis} ${hinweis}
@@ -4585,7 +4616,7 @@ function setupFeldzeile(feld) {
<div class="setup-unterfelder"> <div class="setup-unterfelder">
${feld.positionen.map((pos, i) => ` ${feld.positionen.map((pos, i) => `
<div class="setup-unterfeld"> <div class="setup-unterfeld">
<span class="setup-unterfeld-label">${esc(pos)}</span> <span class="setup-unterfeld-label">${esc(pos)}${beispielHinweis(feldBeispiel(feld, i))}</span>
${entitaetCombo(feld.key, i)} ${entitaetCombo(feld.key, i)}
${entitaetStatusWarnung((setupZuordnung[feld.key] || [])[i])} ${entitaetStatusWarnung((setupZuordnung[feld.key] || [])[i])}
</div>`).join("")} </div>`).join("")}
@@ -1378,7 +1378,17 @@ img.carfix.klein { aspect-ratio: auto; height: auto; }
} }
.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; }
.setup-unterfeld-label { font-size: 12.5px; color: var(--fg3); } /* Positionsname links, Erwartungswert rechts - derselbe Bau wie
.setup-feld-kopf eine Ebene darueber, damit beide Zeilen dieselbe Kante
haben. Der Hinweis erbt hier die kleine graue Schrift des Labels; die
14px/--fg-Form von .setup-feld-hinweis passt nur in die Kopfzeile. */
.setup-unterfeld-label {
font-size: 12.5px; color: var(--fg3);
display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.setup-unterfeld-label .setup-feld-hinweis {
font-size: inherit; color: inherit; max-width: none; flex: 0 0 auto;
}
.entitaet-combo { position: relative; } .entitaet-combo { position: relative; }
.entitaet-combo-icon { .entitaet-combo-icon {
@@ -1,7 +1,7 @@
{ {
"domain": "audi_dashboard", "domain": "audi_dashboard",
"name": "Audi Dashboard", "name": "Audi Dashboard",
"version": "2026.9.1.14", "version": "2026.9.1.15",
"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"],