"""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 )