Fahrgestellnummer: expliziter "automatisch"-Schalter statt Nur-wenn-leer-Heuristik

- Neues Profilfeld fahrzeug.fin_automatisch (Standard: an).
- identitaet.py: bei "aus" läuft die Ableitung (samt Mehrfach-Sensor-Abgleich)
  gar nicht erst; bei "an" überschreibt der abgeleitete Wert auch einen
  vorher von Hand eingetragenen - der Schalter selbst entscheidet jetzt, statt
  aus einem leeren Feld zu raten.
- dienste.py: profil_schreiben löst die Ableitung jetzt auch aus, damit das
  Umschalten des Schalters selbst sofort wirkt.
- Panel: Schalter "automatisch" zwischen Label und Textfeld in der
  Fahrgestellnummer-Zeile, Feld wird bei "an" deaktiviert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 12:01:10 +02:00
parent c0acc836f0
commit 6b35de458d
7 changed files with 94 additions and 17 deletions
+38
View File
@@ -4444,6 +4444,44 @@ needed either - the "Fahrgestellnummer (FIN)" field in "Fahrzeug einrichten" alr
profile field, so it now simply arrives pre-filled instead of blank, and stays fully editable/correctable by
hand same as before.
**Same-day follow-up (2026.8.28.16): an explicit "automatisch" switch, per owner request** ("make a Slider
next to 'Fahrgestellnummer (FIN)' ... automatisch turns on/off the auto-grab VIN from Sensor-Database ...
and also deactivates the sanity-check if VIN matches between sensors"). The original design's "only fill an
empty field, never touch a set one" heuristic is gone - replaced by an explicit, user-visible mode:
- New profile field `fahrzeug.fin_automatisch` (bool, default `true` - added to `vorlage/fahrzeugprofil.json`
and defaulted in `profilZuCar()` via `!== false` for profiles from before this field existed).
- `identitaet.fin_ableiten_und_uebernehmen()` rewritten: returns immediately if `fin_automatisch` is `false`
- **before** even calling `fin_kandidaten_ermitteln()`, so the multi-sensor comparison/mismatch-warning
never runs either, exactly as asked. When `true`, the derived value is now treated as authoritative and
**overwrites** whatever is currently in `fin` (including a previously hand-typed value) whenever a single
clear candidate is found - this is the whole point of an explicit toggle: no more guessing from emptiness,
the human's chosen mode decides. A `fin == kandidat` check avoids a pointless rewrite/republish when
nothing actually changed.
- Panel (`vEinst()`): a `<label class="switch">` reading "automatisch" placed between the "Fahrgestellnummer
(FIN)" label and the text input, in the same `.feld` row (owner's exact layout request) - the input gets
`disabled` whenever automatic is on (`.feld input:disabled{opacity:.5;cursor:not-allowed}`, new rule) and
a title tooltip explaining why, and re-enables the moment automatic is switched off. Wired via the same
`data-*`/central-`change`-listener idiom as every other switch in this file (mirrors `data-steuerlast`).
`CAR.finAutomatisch`/`p.fahrzeug.fin_automatisch` added to both directions of the profile↔CAR mapping.
- `dienste.py`'s `profil_schreiben` handler (not just `entitaeten_schreiben`) now also calls
`identitaet.fin_ableiten_und_uebernehmen()` after writing - this is the actual mechanism that makes
flipping the switch itself take effect immediately: toggling it goes through the ordinary profile-save
path (same as every other "Fahrzeug einrichten" field), so the just-written `fin_automatisch` flag is
what the very next derivation attempt reads.
**Verified live end-to-end, all four transitions, not just read from source**: (1) automatic on, empty
underlying state → real VIN appears, log line fires; (2) toggled off → input's `disabled`/`opacity`
computed style confirmed `false`/`1` (genuinely editable, not just visually similar - the app's normal
`.feld input` background already looks grey by design, so a screenshot alone wasn't proof); (3) typed a
fake value while off, saved → persisted to disk exactly as typed, confirmed via direct file read, log
stayed silent (sanity-check correctly skipped); (4) toggled back on → immediately overwrote the fake value
back to the real derived VIN, both the on-disk file and a fresh log line (`Fahrgestellnummer automatisch
... übernommen: WUAZZZF48PA902804`) confirmed within seconds, no manual save/reload needed. Layout also
confirmed clean at both a real 1400px desktop width and the default phone-width layout - no overflow at
either. `node --check` clean, manifest bumped to `2026.8.28.16`, `audi_ha_test` restarted and confirmed
clean via log.
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
+7 -1
View File
@@ -219,8 +219,14 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None:
Die Oberfläche hält Profil, Fahrzeugdaten und Einstellungen ohnehin
als einen zusammenhängenden Stand im Speicher und schickt ihn nach
jeder Änderung komplett zurück - deshalb ein einziger Dienst statt
eines eigenen je Feld."""
eines eigenen je Feld.
Danach auch ein Versuch, die Fahrgestellnummer zu synchronisieren
(identitaet.py) - unter anderem der Weg, über den das Umschalten von
"automatisch" selbst wirkt: dieser Dienst schreibt auch das gerade
geänderte fin_automatisch-Flag, bevor die Ableitung es liest."""
await k.ablage.profil_schreiben(_objekt(aufruf.data["profil_json"]))
await identitaet.fin_ableiten_und_uebernehmen(k.hass, k.zuordnung, k.ablage)
await k.profil_veroeffentlichen()
async def entitaeten_schreiben(aufruf: ServiceCall) -> None:
@@ -134,6 +134,10 @@ function profilZuCar(p, status) {
const reifenSatz = (k) => p.reifen.saetze[k] || {};
return {
fin: p.fahrzeug.fin,
// Default true (auch für Profile von vor dieser Funktion, die das Feld
// noch nicht kennen) - deckt sich mit identitaet.py's eigenem
// fahrzeug.get("fin_automatisch", True).
finAutomatisch: p.fahrzeug.fin_automatisch !== false,
details: p.fahrzeug.details,
erstzulassung: p.fahrzeug.erstzulassung,
hu: p.fahrzeug.hauptuntersuchung_faellig,
@@ -198,6 +202,7 @@ function configCarZuProfil(vorherigesProfil) {
p.fahrzeug.kennzeichen = CONFIG.kennzeichen;
p.fahrzeug.hauptuntersuchung_faellig = CAR.hu;
p.fahrzeug.fin = CAR.fin;
p.fahrzeug.fin_automatisch = CAR.finAutomatisch;
p.fahrzeug.erstzulassung = CAR.erstzulassung;
p.fahrzeug.ausfuehrung = CONFIG.ausfuehrung;
p.einstellungen.uebersichtsbild = CONFIG.startbild;
@@ -2520,7 +2525,14 @@ function vEinst() {
<div class="tile"><span class="label">Fahrzeug einrichten</span>
${einrichtenOffen ? `
<div class="feld" style="margin-top:16px"><label for="einFin">Fahrgestellnummer (FIN)</label>
<input id="einFin" value="${esc(CAR.fin || "")}" data-einrichten="fin"></div>
<span style="display:flex;align-items:center;gap:10px;min-width:0">
<span style="display:flex;align-items:center;gap:6px;flex:0 0 auto">
<span class="label" style="margin:0;text-transform:none;letter-spacing:0;font-size:12.5px;color:var(--fg2)">automatisch</span>
<label class="switch"><input type="checkbox" data-fin-automatisch ${CAR.finAutomatisch ? "checked" : ""}><span class="track"></span></label>
</span>
<input id="einFin" value="${esc(CAR.fin || "")}" data-einrichten="fin" ${CAR.finAutomatisch ? "disabled" : ""}
title="${CAR.finAutomatisch ? "Wird automatisch aus den im Setup zugeordneten Sensoren ermittelt - \"automatisch\" ausschalten, um von Hand einzutragen." : ""}">
</span></div>
<div class="feld"><label for="einModell">Modell</label>
<select id="einModell" data-einrichten="modell">
${(FAHRZEUG_MODELLE.includes(CONFIG.fahrzeugtitel) ? FAHRZEUG_MODELLE : [CONFIG.fahrzeugtitel, ...FAHRZEUG_MODELLE])
@@ -5372,6 +5384,7 @@ function ereignisseVerdrahten() {
if (e.target.dataset.steuerzeit !== undefined) { CAR.steuer.zeitraum = e.target.value; profilSpeichern(); render(); return; }
if (e.target.dataset.steuerfaellig !== undefined) { CAR.steuer.faellig = e.target.value; profilSpeichern(); render(); return; }
if (e.target.dataset.steuerlast !== undefined) { CAR.steuer.lastschrift = e.target.checked; profilSpeichern(); render(); return; }
if (e.target.dataset.finAutomatisch !== undefined) { CAR.finAutomatisch = e.target.checked; profilSpeichern(); render(); return; }
const vteil = e.target.dataset.vteil;
if (vteil) {
const [i, feld] = vteil.split("|");
@@ -702,6 +702,10 @@ button.leaf:active { background: var(--tile-2); }
appearance: none;
}
.feld input:focus, .feld select:focus, .notiz:focus { outline: none; border-color: var(--red); }
/* z. B. Fahrgestellnummer bei aktivem "automatisch"-Schalter: der Wert kommt
aus den zugeordneten Sensoren, Handeingabe ist erst nach dem Ausschalten
wieder möglich - das Feld soll das auf den ersten Blick zeigen. */
.feld input:disabled { opacity: .5; cursor: not-allowed; }
/* Label und Eingabe untereinander statt nebeneinander, die Eingabe über die
volle Zeilenbreite - für Felder, deren Wert typischerweise länger ist als
ein Kurztext (Kommentar bei Sommer-/Winterrädern, live wie archiviert).
+29 -14
View File
@@ -9,18 +9,27 @@ Kanal-Konfiguration ebenfalls liefern. Ein eigenes Setup-Feld dafür würde vom
Nutzer verlangen, denselben Wert doppelt zu suchen und zuzuordnen, obwohl die
Information am Gerät der ohnehin schon zugeordneten Sensoren hängt.
Stattdessen wird bei jedem Start und nach jeder Setup-Änderung das
Geräte-Register der AKTUELL zugeordneten Sensoren durchsucht (nicht aller
Entitäten der Instanz - nur was Sensorzuordnung tatsächlich referenziert):
für jede zugeordnete Entity-ID wird ihr Gerät nachgeschlagen und daraus eine
FIN-Kandidatin extrahiert (Geräte-Identifier oder Seriennummer, die wie eine
echte FIN aussieht - 17 Zeichen, ohne die im VIN-Standard verbotenen
Ein Schalter "automatisch" neben dem FIN-Feld ("Fahrzeug einrichten",
`fahrzeug.fin_automatisch` im Profil, Standard an) entscheidet, ob diese
Ableitung überhaupt läuft. An: bei jedem Start, nach jeder Setup-Änderung und
nach jedem Profil-Speichern (also auch direkt beim Umschalten des Schalters
selbst) wird das Geräte-Register der AKTUELL zugeordneten Sensoren durchsucht
(nicht aller Entitäten der Instanz - nur was Sensorzuordnung tatsächlich
referenziert): für jede zugeordnete Entity-ID wird ihr Gerät nachgeschlagen und
daraus eine FIN-Kandidatin extrahiert (Geräte-Identifier oder Seriennummer, die
wie eine echte FIN aussieht - 17 Zeichen, ohne die im VIN-Standard verbotenen
Verwechslungsbuchstaben I/O/Q). Liefern mehrere Geräte je eine Kandidatin,
müssen sie übereinstimmen; weichen sie ab, wird nur gewarnt statt geraten -
dieselbe "kein Vorschlag ist besser als ein falscher" Haltung wie an anderen
Stellen dieser App (siehe z. B. entitaetIdKurz() im Panel). Ist im Profil
bereits eine FIN eingetragen, wird sie nie überschrieben - von Hand
eingetragene Werte haben Vorrang vor einer Ableitung.
Stellen dieser App (siehe z. B. entitaetIdKurz() im Panel). Solange
"automatisch" an ist, gilt der abgeleitete Wert als maßgeblich und wird bei
jedem Lauf aktualisiert (auch über einen vorherigen Wert hinweg) - genau das
ist der Zweck des Schalters, nicht ein "nur einmal beim leeren Feld" wie in
einer früheren Fassung dieses Moduls.
Aus: der Schalter deaktiviert die Ableitung vollständig, einschließlich des
Mehrfach-Sensor-Abgleichs oben - das Feld wird dann nirgends mehr angefasst
und bleibt reine Handeingabe wie jedes andere Profilfeld auch.
"""
from __future__ import annotations
@@ -93,29 +102,35 @@ def fin_kandidaten_ermitteln(hass: HomeAssistant, werte: Sensorzuordnung) -> dic
async def fin_ableiten_und_uebernehmen(
hass: HomeAssistant, zuordnung: Zuordnung, ablage: Ablage
) -> None:
"""Trägt die FIN automatisch ins Profil ein, wenn sie dort noch fehlt und
sich aus den zugeordneten Sensoren eindeutig ableiten lässt."""
"""Hält die FIN im Profil mit den zugeordneten Sensoren synchron, solange
fahrzeug.fin_automatisch nicht ausdrücklich abgeschaltet ist (Standard:
an)."""
profil = await ablage.profil_lesen()
if profil is None:
return
fahrzeug = profil.setdefault("fahrzeug", {})
if fahrzeug.get("fin"):
if not fahrzeug.get("fin_automatisch", True):
return
kandidaten = fin_kandidaten_ermitteln(hass, zuordnung.werte)
eindeutig = set(kandidaten.values())
if not eindeutig:
# Nichts gefunden - einen eventuell schon eingetragenen Wert nicht
# löschen, nur weil gerade kein Kandidat da ist (z. B. Sensoren
# kurzzeitig nicht verfügbar).
return
if len(eindeutig) > 1:
_LOGGER.warning(
"Fahrgestellnummer nicht eindeutig aus den zugeordneten Sensoren "
"ableitbar (gefunden: %s) - bitte in \"Fahrzeug einrichten\" von "
"Hand eintragen.",
"ableitbar (gefunden: %s) - bitte \"automatisch\" in \"Fahrzeug "
"einrichten\" ausschalten und von Hand eintragen.",
", ".join(sorted(eindeutig)),
)
return
fin = eindeutig.pop()
if fahrzeug.get("fin") == fin:
return
fahrzeug["fin"] = fin
await ablage.profil_schreiben(profil)
_LOGGER.info(
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.8.28.15",
"version": "2026.8.28.16",
"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"],
@@ -3,6 +3,7 @@
"fahrzeug": {
"fin": "",
"fin_automatisch": true,
"kennzeichen": "",
"titel": "RS 4 Avant competition",
"zusatz": "Avant competition",