Files
audi-app/custom_components/audi_dashboard/identitaet.py
T
tobias 6b35de458d 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>
2026-08-28 12:01:10 +02:00

139 lines
5.9 KiB
Python

"""Fahrgestellnummer (FIN/VIN) automatisch aus den zugeordneten Sensoren ableiten.
Die FIN ist ein reines Profilfeld (fahrzeugprofil.json, "Fahrzeug einrichten"),
ohne eigene Sensor-Zuordnung im Setup-Menü - obwohl mindestens eine der ohnehin
schon zugeordneten Integrationen sie meist bereits mitbringt: die
cupra_eu_data_act-Integration trägt sie als Geräte-Identifier/Seriennummer
(nicht als eigene Entität), der Teltonika FMM003 kann sie je nach Firmware-/
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.
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). 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
import logging
import re
from dataclasses import fields
from typing import TYPE_CHECKING
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import entity_registry as er
if TYPE_CHECKING:
from .ablage import Ablage
from .einstellungen import Sensorzuordnung
from .zuordnung import Zuordnung
_LOGGER = logging.getLogger(__name__)
# 17 Zeichen, Großbuchstaben ohne I/O/Q (im VIN-Standard wegen Verwechslungs-
# gefahr mit 1/0 verboten) und Ziffern - erkennt eine FIN, ohne andere
# Geräte-Identifier (z. B. "FMM003 - Testintegratoin") fälschlich zu matchen.
_SIEHT_AUS_WIE_FIN = re.compile(r"^[A-HJ-NPR-Z0-9]{17}$")
def _zugeordnete_entity_ids(werte: Sensorzuordnung) -> list[str]:
"""Alle aktuell zugeordneten Entity-IDs, Listenfelder (Türen/Fenster ...)
eingeschlossen - dieselbe Quelle, aus der auch das Panel seine
Vergleichsbasis baut (setupAndereIdsOhneDomain() in audi-dashboard-app.js),
hier nur serverseitig."""
ids: list[str] = []
for feld_info in fields(werte):
wert = getattr(werte, feld_info.name)
if isinstance(wert, list):
ids.extend(eintrag for eintrag in wert if eintrag)
elif wert:
ids.append(wert)
return ids
def _fin_kandidat_fuer_geraet(geraet: dr.DeviceEntry) -> str | None:
werte = [geraet.serial_number] + [kennung for _domain, kennung in geraet.identifiers]
for wert in werte:
if wert and _SIEHT_AUS_WIE_FIN.match(wert.upper()):
return wert.upper()
return None
def fin_kandidaten_ermitteln(hass: HomeAssistant, werte: Sensorzuordnung) -> dict[str, str]:
"""{Geräte-ID: FIN-Kandidatin} für jedes Gerät hinter einer aktuell
zugeordneten Entität, dessen Geräte-Identifier/Seriennummer wie eine
echte FIN aussieht."""
ent_reg = er.async_get(hass)
dev_reg = dr.async_get(hass)
gefunden: dict[str, str] = {}
for entity_id in _zugeordnete_entity_ids(werte):
eintrag = ent_reg.async_get(entity_id)
if not eintrag or not eintrag.device_id:
continue
geraet = dev_reg.async_get(eintrag.device_id)
if not geraet:
continue
kandidat = _fin_kandidat_fuer_geraet(geraet)
if kandidat:
gefunden[geraet.id] = kandidat
return gefunden
async def fin_ableiten_und_uebernehmen(
hass: HomeAssistant, zuordnung: Zuordnung, ablage: Ablage
) -> None:
"""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 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 \"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(
"Fahrgestellnummer automatisch aus den zugeordneten Sensoren übernommen: %s", fin
)