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 <noreply@anthropic.com>
This commit is contained in:
@@ -4394,6 +4394,56 @@ Verified live: `getBoundingClientRect()` on both real rendered `.v` blocks now r
|
||||
Verified: `node --check` clean at both steps, manifest bumped `2026.8.28.12` → `.13` → `.14`, `audi_ha_test`
|
||||
restarted and confirmed clean via log each time.
|
||||
|
||||
## AG. Fahrgestellnummer (FIN/VIN) automatisch aus den zugeordneten Sensoren abgeleitet (2026.8.28.15)
|
||||
|
||||
Owner: "It shall be available at the EU Data Act as well as FMM003. Read it from all sensors (check which
|
||||
sensors are used in setup) and compare the VIN if it is matching. If VIN is empty, than use just the one
|
||||
available." The FIN was, until now, a purely manual profile field (`fahrzeugprofil.json`'s `fahrzeug.fin`,
|
||||
edited via "Fahrzeug einrichten") - no sensor role for it existed at all.
|
||||
|
||||
Investigated the real data in `audi_ha_test` before designing anything, rather than assuming a dedicated VIN
|
||||
*entity* exists on either integration: neither `cupra_eu_data_act` nor the FMM003 device expose a `sensor.*`
|
||||
with "vin"/"fahrgestell"/"chassis" in its name or a matching state attribute (checked via a live
|
||||
`hass.callWS` scan of every entity on both devices). The VIN **is** present, though - on the
|
||||
`cupra_eu_data_act` device itself, in the HA **device registry**: `identifiers: [["cupra_eu_data_act",
|
||||
"WUAZZZF48PA902804"]]` and `serial_number: "WUAZZZF48PA902804"`, exactly matching this test profile's own
|
||||
manually-entered FIN. The FMM003 device in this specific test container does not carry a VIN-shaped
|
||||
identifier (its own identifier is just its configured integration name) - real hardware may differ depending
|
||||
on firmware/channel configuration (Teltonika Codec8 Extended has a dedicated VIN AVL parameter), which is
|
||||
exactly why the owner asked for "check both, don't assume one".
|
||||
|
||||
Built on that finding rather than adding a new Setup field: a new `identitaet.py` derives the FIN from the
|
||||
**device registry of whichever sensors are already mapped** in Setup, no separate FIN sensor role to pick.
|
||||
`_zugeordnete_entity_ids()` walks every field of the live `Sensorzuordnung` dataclass generically (via
|
||||
`dataclasses.fields()`, list-typed fields like `TUER_SENSOREN` included) to get every entity ID currently in
|
||||
use; for each, its owning device is resolved (entity registry → device registry) and checked for a
|
||||
VIN-shaped identifier/serial number (`_SIEHT_AUS_WIE_FIN`: 17 chars, A-Z minus I/O/Q plus digits - the real
|
||||
VIN standard's own exclusion of those three letters, chosen specifically so an unrelated device identifier
|
||||
like "FMM003 - Testintegratoin" can never false-match). Distinct candidates found across all mapped devices
|
||||
are collected into a set: zero → no-op (nothing found yet); exactly one → adopted automatically, but **only
|
||||
if the profile's own FIN is still empty** (a manually-entered value is never overwritten - matches this
|
||||
project's "human input always wins over automation" precedent, e.g. `_fahrt_screenen()`'s `edited_fields`
|
||||
protection); more than one distinct value → logged as a warning naming both, deliberately not guessed at
|
||||
(same "no suggestion beats a wrong one" stance as `entitaetIdKurz()`'s own fallback). Wired into two places
|
||||
so it re-attempts whenever it has a real chance of succeeding: once after every HA start (`koordinator.py`'s
|
||||
`_nach_start()`, alongside `fahrterkennung.nach_neustart_fortsetzen()` - deliberately not in `starten()`
|
||||
itself, for the same device/entity-registry-not-ready-yet reason `trigger_neu_binden()`'s own docstring
|
||||
already explains) and again after every Setup-menu save (`dienste.py`'s `entitaeten_schreiben` - a mapping
|
||||
change may be exactly what makes derivation newly possible). Both call sites re-publish the profile
|
||||
afterward so a freshly-derived FIN reaches the frontend immediately, not after the next 60s safety-net tick.
|
||||
|
||||
**Verified live, not just read from source** (this file's own section S lesson): backed up the test
|
||||
container's real `fahrzeugprofil.json`, blanked `fahrzeug.fin`, restarted - log showed
|
||||
`Fahrgestellnummer automatisch aus den zugeordneten Sensoren übernommen: WUAZZZF48PA902804`, the on-disk
|
||||
file and the published `sensor.audi_dashboard_profil` entity both confirmed via direct query, no stale
|
||||
frontend needed. Restored the original file byte-for-byte afterward and restarted once more to confirm a
|
||||
clean, silent (no re-derivation - the guard against overwriting an already-set FIN held) boot. `py_compile`
|
||||
clean on all three touched files (`identitaet.py` new, `koordinator.py`, `dienste.py`). No `services.yaml`
|
||||
change needed (no new service, just two existing handlers doing more internally) and no frontend change
|
||||
needed either - the "Fahrgestellnummer (FIN)" field in "Fahrzeug einrichten" already reads/writes this exact
|
||||
profile field, so it now simply arrives pre-filled instead of blank, and stays fully editable/correctable by
|
||||
hand same as before.
|
||||
|
||||
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
|
||||
|
||||
@@ -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