6b35de458d
- 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>
139 lines
5.9 KiB
Python
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
|
|
)
|