Fahrgestellnummer (FIN) automatisch aus den zugeordneten Sensoren ableiten

Neues identitaet.py: durchsucht das Geräte-Register der aktuell im Setup
zugeordneten Sensoren (cupra_eu_data_act, FMM003, ...) nach einem
VIN-förmigen Geräte-Identifier/Seriennummer, statt ein eigenes Setup-Feld zu
verlangen. Mehrere übereinstimmende Quellen werden akzeptiert, abweichende nur
geloggt statt geraten; eine bereits von Hand eingetragene FIN wird nie
überschrieben. Läuft nach jedem Start und nach jeder Setup-Änderung
(koordinator.py/dienste.py).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-28 11:47:08 +02:00
co-authored by Claude Sonnet 5
parent 8f88ccf581
commit c0acc836f0
5 changed files with 189 additions and 3 deletions
@@ -0,0 +1,123 @@
"""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.
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
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.
"""
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:
"""Trägt die FIN automatisch ins Profil ein, wenn sie dort noch fehlt und
sich aus den zugeordneten Sensoren eindeutig ableiten lässt."""
profil = await ablage.profil_lesen()
if profil is None:
return
fahrzeug = profil.setdefault("fahrzeug", {})
if fahrzeug.get("fin"):
return
kandidaten = fin_kandidaten_ermitteln(hass, zuordnung.werte)
eindeutig = set(kandidaten.values())
if not eindeutig:
return
if len(eindeutig) > 1:
_LOGGER.warning(
"Fahrgestellnummer nicht eindeutig aus den zugeordneten Sensoren "
"ableitbar (gefunden: %s) - bitte in \"Fahrzeug einrichten\" von "
"Hand eintragen.",
", ".join(sorted(eindeutig)),
)
return
fin = eindeutig.pop()
fahrzeug["fin"] = fin
await ablage.profil_schreiben(profil)
_LOGGER.info(
"Fahrgestellnummer automatisch aus den zugeordneten Sensoren übernommen: %s", fin
)