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:
@@ -34,6 +34,7 @@ from . import (
|
||||
csv_import,
|
||||
fahrterkennung,
|
||||
historienimport,
|
||||
identitaet,
|
||||
reifen,
|
||||
screening,
|
||||
sicherung,
|
||||
@@ -229,11 +230,18 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None:
|
||||
|
||||
Das Neubinden ist der Grund, warum der frühere Hinweis "wirkt erst
|
||||
nach einem Neustart" und der Neustart-Knopf daneben entfallen konnten:
|
||||
auch Zündung, Kilometerstand und Tankfüllstand wirken jetzt sofort."""
|
||||
auch Zündung, Kilometerstand und Tankfüllstand wirken jetzt sofort.
|
||||
|
||||
Danach auch ein erneuter Versuch, die Fahrgestellnummer aus den
|
||||
(jetzt ggf. neuen) zugeordneten Sensoren abzuleiten (identitaet.py) -
|
||||
eine leere FIN kann durch diese Zuordnung gerade erst ableitbar
|
||||
geworden sein."""
|
||||
await k.zuordnung.speichern(_objekt(aufruf.data["zuordnung_json"]))
|
||||
await k.trigger_neu_binden()
|
||||
await k.zuordnung_veroeffentlichen()
|
||||
await k.fahrzeugstatus_veroeffentlichen()
|
||||
await identitaet.fin_ableiten_und_uebernehmen(k.hass, k.zuordnung, k.ablage)
|
||||
await k.profil_veroeffentlichen()
|
||||
|
||||
async def historie_importieren(aufruf: ServiceCall) -> None:
|
||||
"""Startet den Import und kehrt sofort zurück.
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -47,7 +47,7 @@ from homeassistant.helpers.event import (
|
||||
from homeassistant.helpers.start import async_at_started
|
||||
from homeassistant.helpers.storage import Store
|
||||
|
||||
from . import batterie, fahrterkennung, reifen, screening, sicherung, tankerkennung
|
||||
from . import batterie, fahrterkennung, identitaet, reifen, screening, sicherung, tankerkennung
|
||||
from .ablage import Ablage
|
||||
from .const import (
|
||||
BUENDEL_INFO,
|
||||
@@ -149,6 +149,11 @@ class Koordinator:
|
||||
|
||||
async def _nach_start(self, _hass: HomeAssistant) -> None:
|
||||
await fahrterkennung.nach_neustart_fortsetzen(self)
|
||||
# Erst hier (nicht in starten()): das Geräte-/Entity-Register der
|
||||
# zugeordneten Sensoren ist vor async_at_started oft noch nicht
|
||||
# vollständig - siehe die Begründung von trigger_neu_binden() oben.
|
||||
await identitaet.fin_ableiten_und_uebernehmen(self.hass, self.zuordnung, self.ablage)
|
||||
await self.profil_veroeffentlichen()
|
||||
|
||||
async def beenden(self) -> None:
|
||||
for abmelden in (*self._takte, *self._beobachter):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.8.28.14",
|
||||
"version": "2026.8.28.15",
|
||||
"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"],
|
||||
|
||||
Reference in New Issue
Block a user