diff --git a/AGENTS.md b/AGENTS.md index 5e98cbb..02e6d16 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,10 +1,13 @@ # AGENTS.md — Project state, review findings, open items, and working rules -**Last updated: 2026-08-13** (merged the `umsetzung-datametric360` branch — companion app phases -1–10 done, see `UMSETZUNGSPLAN.md`; later the same day: cleaned up remaining EU Data Act residue, -fixed oversized toggle switches, and fixed a desktop-layout audit (settings button / rings logo / -popups overflowing past the capped content column) — see `DESIGN_AUDIT_2026-08-13.md` and section -C). This file is the entry point for every new agent +**Last updated: 2026-08-16** (merged the `umsetzung-datametric360` branch — companion app phases +1–10 done, see `UMSETZUNGSPLAN.md`; 2026-08-13: cleaned up remaining EU Data Act residue, fixed +oversized toggle switches, and fixed a desktop-layout audit (settings button / rings logo / popups +overflowing past the capped content column) — see `DESIGN_AUDIT_2026-08-13.md`; 2026-08-16: fixed +five user-reported bugs — removed Türschloss/Haubenschloss checks, fixed a translucent confirm- +sheet, added a flespi-style split lat/lon location source, hardened the "Lädt …" bootstrap race, +and added a generic (station-independent) fuel-receipt parser fallback — see section C). This file +is the entry point for every new agent session: what this repo is, what is finished, what is missing, and how to work here. Detail lives in the linked documents — this file points, it does not duplicate. @@ -601,6 +604,68 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed diffing across all 5 tabs in both themes, a temporary 2.5x `transform:scale()` zoom on the live swipe-row DOM) but could not be reproduced — documented in the audit file as open, pending a screenshot from the real device. +- [x] Fix five user-reported bugs (2026-08-16): + (1) **Türschloss-/Haubenschloss-Erkennung entfernt** — user asked for these two checks to be + permanently removed (not just left blank/unmapped like the rest of the retired VAG- + integration fields). Removed `TUERSCHLOSS_SENSOREN`/`HAUBENSCHLOSS_SENSOR` from + `entitaeten.py`'s `FELDER` catalog (no longer offered in the Setup menu) and from + `einstellungen.py` (attributes deleted, not just blanked), and removed their entries from + `_sicherheitscheck()` in `frontend_veroeffentlichung.py` (no longer listed under "Geprüfte + Punkte"). `HECKKLAPPENSCHLOSS_SENSOR` (tailgate lock) was deliberately left untouched — user + named only the two door/hood locks. Also removed the now-orphaned + `input_boolean.test_entriegelt` helper and the four `test_audi_lock_*` template sensors from + `testumgebung/konfiguration.yaml` (door-open/window fixtures for `TUER_SENSOREN`/ + `FENSTER_SENSOREN` are untouched), and updated `testumgebung/README.md` accordingly. + (2) **"Doppelt zugeordnet" (and every other confirm-sheet) was see-through** — `.sheet-gruppe`/ + `.sheet-abbrechen` in `audi-dashboard.css` used `--tile-2`, which the iOS overlay's night + theme redefines as `rgba(255,255,255,.10)` (translucent, meant for tile-on-tile layering) — + the same root cause already fixed for `.setup-popup`/`.standortmenu` on 2026-08-13, just not + carried over to the generic confirm-sheet used by `bestaetigen()`/`hinweis()`. Switched both + to `var(--tile-deckend, var(--canvas))`, matching the existing fallback pattern already used + elsewhere in this file (base CSS must not go blank if the purely-additive iOS overlay is ever + reverted, since `--tile-deckend` is an overlay-only token). + (3) **FMM003 coordinates arrive as two separate sensors, not one device_tracker** — user + reported "coordinates are split into latitude and longitude". Root cause: `_standort()` in + `frontend_veroeffentlichung.py` only ever read `latitude`/`longitude` as *attributes of a + single `STANDORT_TRACKER` device_tracker entity* (HA's usual convention) — but some + integrations (confirmed live in `audi_ha_test`'s own simulated FMM003 device, which exposes + separate "Latitude coordinate value"/"Longitude coordinate value" sensors) publish them as + two independent `sensor` entities instead, which `_standort()` had no way to consume. Added + `STANDORT_LAT_SENSOR`/`STANDORT_LON_SENSOR` (new optional fields in `einstellungen.py` + + `entitaeten.py`'s Setup-menu catalog, group "standort") as a second path: `_standort()` now + tries `STANDORT_TRACKER` first (unchanged behavior when it works), and falls back to reading + the two plain sensors' states directly when the tracker is unset or has no coordinates. + (4) **App can show only "Lädt …" until a tab is clicked** — known, already-mitigated race + (see the `nachladeAnstossen` header comment, `audi-dashboard-app.js`): on a slow/cold backend + start, `render()` no-ops until `DATEN_GELADEN` flips true, and the previously observed + manual fix was a HA panel remount. Root cause of *why* the click helps was not fully + reproducible in `audi_ha_test` (this is a genuine uncertainty, flagged rather than guessed + around) — but as a safe, low-risk hardening, `go()` (the tab/menu click handler) now calls + `datenLaden(false)` proactively if `DATEN_GELADEN` is still false, so any click that happens + to occur after data has actually become available is guaranteed to pick it up immediately + instead of depending on timing. + (5) **Fuel-receipt parser only understood Shell's exact layout** — user asked for a generic + algorithm for other/unknown fuel stations: find the address, find the total ("Gesamt"/ + "Absolut"), find the liters ("Menge"/"Amount"), divide to get the paid price/liter, compare + against a printed "Preis/Liter" — a mismatch means a discount, found on the receipt as a + minus-marked amount. Implemented exactly this as `_parsen_generisch()` in + `homeassistant/data/shell_beleg_parser.py`, wired as a fallback in `main()` (Shell-specific + `_parsen()` tried first, unchanged — verified the existing 10-receipt regression suite still + passes byte-for-byte; `_parsen_generisch()` verified against a synthetic non-Shell receipt + with a known discount, all fields and the discount math correct). No separate "SmartDeal" + flag was needed — the frontend already treats any populated `discount` as SmartDeal-eligible + (gated only by the user's own "SmartDeal aktiv" switch, not by station name), so a generically + parsed Shell receipt behaves identically to one parsed via the strict path. Receipt-key + generation uses `hashlib.sha1` (not the built-in `hash()`, which is randomly salted per + process and would have broken the §7.7 duplicate-detection dedup across the parser's + per-receipt subprocess invocations). + All five deployed and verified live in `audi_ha_test` (version `1786752000`): Sicherheit- + Liste and Setup-Katalog confirmed to no longer offer Türschloss/Haubenschloss; the new + Breitengrad/Längengrad Setup fields confirmed present and correctly auto-suggesting the + container's real split lat/lon sensors; "Doppelt zugeordnet" confirmed rendering on an opaque + card by deliberately triggering it (this container's limited entity set causes a real + duplicate auto-suggestion); pyscript reload clean, no new console errors beyond the + pre-existing placeholder-image 404s. Synced to `installationspaket/`. - [ ] Fix remaining documentation drift (statistics claim, README gaps, obsolete TODO comment) — text-only changes; INSTALL.md's WLAN/TommiG1 drift and stale variable names were fixed 2026-08-12 (see section B); `DESIGN_REVIEW_2026-08-13.md` and `REVIEW_main_2026-08-13.md` diff --git a/homeassistant/data/shell_beleg_parser.py b/homeassistant/data/shell_beleg_parser.py index 3d4b251..fb03160 100644 --- a/homeassistant/data/shell_beleg_parser.py +++ b/homeassistant/data/shell_beleg_parser.py @@ -50,6 +50,7 @@ Aufbau eines Shell-eReceipts (aus dem echten Beispiel, Reihenfolge fix): """ import datetime +import hashlib import json import re import sys @@ -198,6 +199,118 @@ def _parsen(text): } +def _parsen_generisch(text): + """Stationsunabhängiger Fallback für Belege, die keinem der oben fest an + Shells Beleg-Struktur verankerten Muster entsprechen (_parsen() wirft in + dem Fall ValueError - siehe main()). Arbeitet nach demselben Verfahren, + das an einem echten Beleg einer bislang unbekannten Tankstelle von Hand + nachvollzogen wurde: Adresse suchen, Gesamtbetrag suchen ("Gesamt.../ + Absolut..."), Literangabe suchen ("Menge"/"Amount" oder eine an "l"/ + "Liter" hängende Zahl), daraus den tatsächlich bezahlten Preis/Liter + errechnen (Gesamtbetrag / Liter). Steht daneben auch ein aufgedruckter + Preis/Liter auf dem Beleg und weicht er vom errechneten ab, wurde ein + Rabatt gewährt - auf dem Beleg meist als eigener Betrag mit einem Minus + davor erkennbar, danach wird zuerst gesucht, sonst aus der Preisdifferenz + hergeleitet. Ob ein so gefundener Rabatt als "SmartDeal" behandelt wird, + entscheidet einzig station_name (Shell-Logo/-Label im Frontend, siehe + frontend_veroeffentlichung.py) - kein Sonderfall hier nötig, die Frontend- + Anzeige hängt nur an einem gesetzten discount, nicht an einem eigenen + Kennzeichen. + + Liefert deutlich weniger Felder als _parsen() (kein Bon-Kleingedrucktes + wie TSE-Nummer/Zahlungszeitstempel) - unproblematisch, denn + belegverarbeitung.py liest nur receipt_key als echtes Pflichtfeld, + alles andere per .get().""" + + def suchen(muster, flags=re.IGNORECASE | re.MULTILINE): + return re.search(muster, text, flags) + + # ---- Adresse: Name-/Straßen-/PLZ-Ort-Zeile hintereinander - wie bei + # Shell (_parsen() oben), aber ohne Ankerzeile "Obj.-Nr." danach, also + # direkt an der PLZ-Ort-Form (5 Ziffern + Text) erkannt. Nicht Pflicht. + adresse = suchen(r"([^\n]{2,60})\n([^\n]{2,60})\n(\d{5}\s+[^\n]{2,40})") + station_name = adresse.group(1).strip() if adresse else None + station_address = ( + f"{adresse.group(2).strip()}, {adresse.group(3).strip()}" if adresse else None + ) + + # ---- Gesamtbetrag ------------------------------------------------------ + gesamt = suchen(r"(?:Gesamt\w*|Absolut\w*|Endbetrag|Summe|Total)\D{0,20}?(" + _ZAHL + r")\s*(?:€|EUR)?") + if not gesamt: + raise ValueError("Kein Gesamtbetrag gefunden (weder 'Gesamt' noch 'Absolut')") + fuel_total_eur = _de_zahl(gesamt.group(1)) + + # ---- Menge in Litern ---------------------------------------------------- + menge = suchen(r"(?:Menge|Amount)\D{0,15}?(" + _ZAHL + r")\s*(?:l\b|L\b|Liter|Ltr\.?)") + if not menge: + menge = suchen(r"(" + _ZAHL + r")\s*(?:Liter|Ltr\.?|l)\b") + if not menge: + raise ValueError("Keine Literangabe gefunden (weder 'Menge'/'Amount' noch 'X l')") + liters = _de_zahl(menge.group(1)) + if not liters: + raise ValueError("Literangabe ist 0 oder unlesbar") + + berechneter_preis_l = round(fuel_total_eur / liters, 3) + + # ---- Aufgedruckter Preis/Liter - Abweichung verrät einen Rabatt ------- + aufgedruckt = suchen( + r"(?:Preis\s*/?\s*(?:je\s*)?Liter|Price\s*/?\s*L(?:iter)?|€\s*/\s*l|EUR\s*/\s*l)\D{0,10}?(" + _ZAHL + r")" + ) + list_price_per_l = _de_zahl(aufgedruckt.group(1)) if aufgedruckt else berechneter_preis_l + + discount = None + discount_per_l = None + if aufgedruckt and abs(list_price_per_l - berechneter_preis_l) > 0.005: + minus = suchen(r"-\s*(" + _ZAHL + r")\s*(?:€|EUR)") + if minus: + discount = _de_zahl(minus.group(1)) + discount_per_l = round(discount / liters, 3) + else: + discount_per_l = round(list_price_per_l - berechneter_preis_l, 3) + discount = round(discount_per_l * liters, 2) + + # ---- Zeitstempel: irgendein Datum+Uhrzeit auf dem Beleg ---------------- + zeit = suchen(r"(\d{2})\.(\d{2})\.(\d{2,4})\D{0,10}?(\d{2}):(\d{2})") + ts = _iso(zeit.group(1), zeit.group(2), zeit.group(3), zeit.group(4), zeit.group(5)) if zeit else None + + # ---- Belegnummer, sonst deterministischer Ersatzschlüssel -------------- + # Wichtig: kein eingebautes hash() (pro Prozess zufällig gesalzen, siehe + # PYTHONHASHSEED) - der Parser läuft für jeden Beleg als eigener Prozess + # (Kopfkommentar oben), ein instabiler Schlüssel würde den §7.7-Regel-3- + # Dedup-Vergleich in belegverarbeitung.py unbrauchbar machen. + beleg_nr = suchen( + r"(?:Beleg-?Nr\.?|Bon-?Nr\.?|Rechnungs-?Nr\.?|Receipt\s*No\.?|Trans(?:aktions)?-?Nr\.?)\D{0,5}?([\dA-Za-z/\-]+)" + ) + receipt_no = beleg_nr.group(1) if beleg_nr else None + schluessel_basis = "|".join( + str(teil) for teil in (receipt_no, ts, station_address, fuel_total_eur, liters) if teil + ) or text[:80] + receipt_key = "generisch_" + hashlib.sha1(schluessel_basis.encode("utf-8")).hexdigest()[:16] + + return { + "receipt_key": receipt_key, + "receipt_no": receipt_no, + "tse_beleg_nr": None, + "ts": ts, + "ts_payment": None, + "ts_tse": None, + "station_id": None, + "station_name": station_name, + "station_address": station_address, + "article_no": None, + "product_name": None, + "fuel_type": None, + "liters": liters, + "list_price_per_l": list_price_per_l, + "discount": discount, + "discount_per_l": discount_per_l, + "fuel_total_eur": fuel_total_eur, + "receipt_total_eur": fuel_total_eur, + "net_eur": None, + "vat_eur": None, + } + + def main(): if len(sys.argv) != 2: print("Aufruf: python3 shell_beleg_parser.py ", file=sys.stderr) @@ -205,10 +318,20 @@ def main(): pdf_pfad = sys.argv[1] try: text = _text_aus_pdf(pdf_pfad) - ergebnis = _parsen(text) except Exception as exc: - print(f"Beleg konnte nicht geparst werden ({type(exc).__name__}): {exc}", file=sys.stderr) + print(f"PDF konnte nicht gelesen werden ({type(exc).__name__}): {exc}", file=sys.stderr) sys.exit(1) + try: + ergebnis = _parsen(text) + except Exception: + # Kein Shell-Beleg im bekannten Format - Versuch mit dem + # stationsunabhängigen Fallback (_parsen_generisch() oben), bevor + # endgültig aufgegeben wird. + try: + ergebnis = _parsen_generisch(text) + except Exception as exc: + print(f"Beleg konnte nicht geparst werden ({type(exc).__name__}): {exc}", file=sys.stderr) + sys.exit(1) print(json.dumps(ergebnis, ensure_ascii=False)) diff --git a/homeassistant/pyscript/modules/einstellungen.py b/homeassistant/pyscript/modules/einstellungen.py index 9378306..832628d 100644 --- a/homeassistant/pyscript/modules/einstellungen.py +++ b/homeassistant/pyscript/modules/einstellungen.py @@ -65,14 +65,10 @@ TUER_SENSOREN = [] # Fenster (§4.1) - keine Quelle mehr vorhanden. FENSTER_SENSOREN = [] -# Verriegelung - keine Quelle mehr vorhanden. -TUERSCHLOSS_SENSOREN = [] - # Heckklappe und Motorhaube - keine Quelle mehr vorhanden. HECKKLAPPE_SENSOR = "" HECKKLAPPENSCHLOSS_SENSOR = "" HAUBE_SENSOR = "" -HAUBENSCHLOSS_SENSOR = "" # Vom Fahrzeug selbst gemeldete Service-Fälligkeit (ergänzt die App-eigene, # aus dem Servicebuch berechnete Prognose) - keine Quelle mehr vorhanden. @@ -87,6 +83,13 @@ INSPEKTION_STRECKE_SENSOR = "" # vom FMM003 geliefert. STANDORT_TRACKER = "device_tracker.testzone_fmm003" +# Alternative zu STANDORT_TRACKER: manche Integrationen (z. B. flespi) liefern +# Breiten-/Längengrad nicht als Attribute einer einzelnen device_tracker- +# Entity, sondern als zwei eigene sensor-Entities. _standort() nutzt diese +# beiden nur, wenn STANDORT_TRACKER leer ist oder keine Koordinaten liefert. +STANDORT_LAT_SENSOR = "" +STANDORT_LON_SENSOR = "" + # Update-Funktion (Einstellungen -> "Update suchen", siehe # updateverwaltung.py): Git-Repository, in das dieses Projekt gepflegt wird - # z. B. ein privates GitHub-Repo, genau wie für die HACS-Integration bereits diff --git a/homeassistant/pyscript/modules/entitaeten.py b/homeassistant/pyscript/modules/entitaeten.py index 5de66b7..32e3f47 100644 --- a/homeassistant/pyscript/modules/entitaeten.py +++ b/homeassistant/pyscript/modules/entitaeten.py @@ -81,10 +81,6 @@ FELDER = [ "hinweis": "\"aus\"/off = zu.", "domains": ["binary_sensor"], "device_classes": ["window"], "units": [], "liste": True, "positionen": POSITIONEN, "pflicht": False, "stichworte": ["fenster", "window"]}, - {"key": "TUERSCHLOSS_SENSOREN", "label": "Türschlösser", "gruppe": "sicherheit", - "hinweis": "\"on\" = entriegelt, \"aus\"/off = verriegelt (device_class \"lock\"-Konvention).", - "domains": ["binary_sensor"], "device_classes": ["lock"], "units": [], "liste": True, "positionen": POSITIONEN, - "pflicht": False, "stichworte": ["schloss", "lock", "verriegel", "entriegel"]}, {"key": "HECKKLAPPE_SENSOR", "label": "Heckklappe", "gruppe": "sicherheit", "hinweis": "\"aus\"/off = zu.", "domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False, @@ -97,10 +93,6 @@ FELDER = [ "hinweis": "\"aus\"/off = zu.", "domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False, "stichworte": ["haube", "hood", "bonnet", "motorhaube"]}, - {"key": "HAUBENSCHLOSS_SENSOR", "label": "Haubenschloss", "gruppe": "sicherheit", - "hinweis": "\"on\" = entriegelt, \"aus\"/off = verriegelt.", - "domains": ["binary_sensor"], "device_classes": ["lock"], "units": [], "liste": False, "pflicht": False, - "stichworte": ["haube", "hood", "schloss", "lock"]}, {"key": "NAECHSTER_OELWECHSEL_SENSOR", "label": "Nächster Ölwechsel (Datum)", "gruppe": "uebersicht", "hinweis": "Vom Fahrzeug selbst gemeldete Fälligkeit, ergänzt die App-eigene Servicebuch-Prognose.", "domains": ["sensor"], "device_classes": ["date", "timestamp"], "units": [], "liste": False, "pflicht": False, @@ -121,6 +113,14 @@ FELDER = [ "hinweis": "Braucht latitude/longitude-Attribute (HA-Konvention für device_tracker).", "domains": ["device_tracker"], "device_classes": [], "units": [], "liste": False, "pflicht": False, "stichworte": ["standort", "gps", "tracker", "position", "ortung"]}, + {"key": "STANDORT_LAT_SENSOR", "label": "GPS-Standort: Breitengrad", "gruppe": "standort", + "hinweis": "Nur nötig, falls die Integration Breiten-/Längengrad als zwei eigene Sensoren statt als Attribute einer GPS-Standort-Entity liefert (z. B. flespi).", + "domains": ["sensor"], "device_classes": [], "units": [], "liste": False, "pflicht": False, + "stichworte": ["breitengrad", "latitude", "lat"]}, + {"key": "STANDORT_LON_SENSOR", "label": "GPS-Standort: Längengrad", "gruppe": "standort", + "hinweis": "Nur nötig, falls die Integration Breiten-/Längengrad als zwei eigene Sensoren statt als Attribute einer GPS-Standort-Entity liefert (z. B. flespi).", + "domains": ["sensor"], "device_classes": [], "units": [], "liste": False, "pflicht": False, + "stichworte": ["längengrad", "longitude", "lon", "lng"]}, ] # Mehrere @state_trigger(f"...")-Dekoratoren (fahrterkennung.py, diff --git a/homeassistant/pyscript/modules/frontend_veroeffentlichung.py b/homeassistant/pyscript/modules/frontend_veroeffentlichung.py index 0160b7b..6a2d002 100644 --- a/homeassistant/pyscript/modules/frontend_veroeffentlichung.py +++ b/homeassistant/pyscript/modules/frontend_veroeffentlichung.py @@ -88,37 +88,59 @@ def _abs_zahl(wert): def _standort(): - """Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel) - liest - latitude/longitude/gps_accuracy von der in STANDORT_TRACKER hinterlegten - device_tracker-Entity (HA-Konvention). Fehlt die Entity-ID oder ist sie - (noch) nicht verfügbar, liefert diese Funktion durchgehend None statt - eines geratenen Werts - genau das zustand_oder_none()-Muster von oben, - hier als eigene Funktion, weil zusätzlich zum Zustand noch Attribute und - der Aktualisierungszeitpunkt gebraucht werden: state.get() liefert dafür - ein StateVal (str-Unterklasse) mit .last_updated als datetime-Attribut - - kein eigenständiges state.last_updated(), das gibt es in pyscript nicht.""" - entity_id = einstellungen.STANDORT_TRACKER + """Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel). Zwei + mögliche Quellen, in dieser Reihenfolge versucht: (1) STANDORT_TRACKER, + eine device_tracker-Entity mit latitude/longitude/gps_accuracy-Attributen + (HA-Konvention) - der Normalfall; (2) STANDORT_LAT_SENSOR/ + STANDORT_LON_SENSOR, zwei eigene sensor-Entities für Breiten-/Längengrad - + manche Integrationen (z. B. flespi) liefern Koordinaten so, statt als + Attribute einer einzelnen Tracker-Entity. Fehlt beides oder ist (noch) + nicht verfügbar, liefert diese Funktion durchgehend None statt eines + geratenen Werts - genau das zustand_oder_none()-Muster von oben, hier als + eigene Funktion, weil zusätzlich zum Zustand noch Attribute und der + Aktualisierungszeitpunkt gebraucht werden: state.get() liefert dafür ein + StateVal (str-Unterklasse) mit .last_updated als datetime-Attribut - kein + eigenständiges state.last_updated(), das gibt es in pyscript nicht.""" leer = {"lat": None, "lon": None, "genauigkeit_m": None, "zeit": None} - if not entity_id: - return leer - try: - wert = state.get(entity_id) - attribute = state.getattr(entity_id) - except NameError: - return leer - if not attribute or attribute.get("latitude") is None: - return leer - zeit = None - try: - zeit = wert.last_updated.isoformat() - except AttributeError: - pass - return { - "lat": _zu_zahl(attribute.get("latitude")), - "lon": _zu_zahl(attribute.get("longitude")), - "genauigkeit_m": _zu_zahl(attribute.get("gps_accuracy")), - "zeit": zeit, - } + + tracker_id = einstellungen.STANDORT_TRACKER + if tracker_id: + try: + wert = state.get(tracker_id) + attribute = state.getattr(tracker_id) + except NameError: + attribute = None + if attribute and attribute.get("latitude") is not None: + zeit = None + try: + zeit = wert.last_updated.isoformat() + except AttributeError: + pass + return { + "lat": _zu_zahl(attribute.get("latitude")), + "lon": _zu_zahl(attribute.get("longitude")), + "genauigkeit_m": _zu_zahl(attribute.get("gps_accuracy")), + "zeit": zeit, + } + + lat_id, lon_id = einstellungen.STANDORT_LAT_SENSOR, einstellungen.STANDORT_LON_SENSOR + if lat_id and lon_id: + try: + lat_wert = state.get(lat_id) + lon_wert = state.get(lon_id) + except NameError: + return leer + lat, lon = _zu_zahl(lat_wert), _zu_zahl(lon_wert) + if lat is None or lon is None: + return leer + zeit = None + try: + zeit = lat_wert.last_updated.isoformat() + except AttributeError: + pass + return {"lat": lat, "lon": lon, "genauigkeit_m": None, "zeit": zeit} + + return leer POSITIONEN = ["vorne links", "vorne rechts", "hinten links", "hinten rechts"] @@ -137,14 +159,10 @@ def _sicherheitscheck(): for pos, sensor in zip(POSITIONEN, einstellungen.FENSTER_SENSOREN): w = zustand_oder_none(sensor) eintraege.append({"label": f"Fenster {pos}", "ok": None if w is None else w == "off"}) - for pos, sensor in zip(POSITIONEN, einstellungen.TUERSCHLOSS_SENSOREN): - w = zustand_oder_none(sensor) - eintraege.append({"label": f"Türschloss {pos}", "ok": None if w is None else w == "off"}) for label, sensor in [ ("Heckklappe", einstellungen.HECKKLAPPE_SENSOR), ("Heckklappenschloss", einstellungen.HECKKLAPPENSCHLOSS_SENSOR), ("Motorhaube", einstellungen.HAUBE_SENSOR), - ("Haubenschloss", einstellungen.HAUBENSCHLOSS_SENSOR), ]: w = zustand_oder_none(sensor) eintraege.append({"label": label, "ok": None if w is None else w == "off"}) diff --git a/homeassistant/www/audi-dashboard-app.js b/homeassistant/www/audi-dashboard-app.js index a8d762c..f2bb246 100644 --- a/homeassistant/www/audi-dashboard-app.js +++ b/homeassistant/www/audi-dashboard-app.js @@ -2992,6 +2992,15 @@ function render() { }); } const go = (name, id) => { + // Bekanntes Race (siehe Kopfkommentar bei nachladeAnstossen): auf einem + // langsam startenden Backend kann die App auf "Lädt …" hängen bleiben, + // obwohl die Daten inzwischen längst da wären - der bislang einzige + // beobachtete Ausweg war ein Tabwechsel, der zufällig einen frischen + // datenLaden()-Durchlauf auslöste. Hier deshalb bei jedem Tab-/Menüklick + // aktiv nachschauen, statt nur auf den nächsten hass-Push zu warten - + // no-op, falls die Daten längst da sind (datenLaden() rendert dann + // ohnehin nichts doppelt Sichtbares). + if (!DATEN_GELADEN) datenLaden(false); // Ein unbestätigter Servicebuch-Entwurf (siehe data-sbneu) verlässt die // Anwendung nie ungespeichert - wird die Eintragsseite ohne Klick auf // "Speichern" verlassen, verwerfen wir ihn hier statt ihn liegen zu lassen diff --git a/homeassistant/www/audi-dashboard-version.json b/homeassistant/www/audi-dashboard-version.json index 5514bea..45c8b46 100644 --- a/homeassistant/www/audi-dashboard-version.json +++ b/homeassistant/www/audi-dashboard-version.json @@ -1 +1 @@ -{"version": 1786744000} +{"version": 1786752000} diff --git a/homeassistant/www/audi-dashboard.css b/homeassistant/www/audi-dashboard.css index 9557bec..02aaa62 100644 --- a/homeassistant/www/audi-dashboard.css +++ b/homeassistant/www/audi-dashboard.css @@ -853,7 +853,15 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; } @keyframes sheet-fade { from { opacity: 0 } to { opacity: 1 } } @keyframes sheet-rein { from { transform: translateY(18px); opacity: 0 } to { transform: none; opacity: 1 } } .sheet-gruppe { - background: var(--tile-2); + /* --tile-2 is translucent in the iOS overlay's night theme (rgba white, + meant for tile-on-tile layering) - a confirm sheet sits directly over + whatever page content triggered it, so a see-through card made titles + like "Doppelt zugeordnet" hard to read against the busy background + bleeding through. Same fix already applied to .setup-popup/.standortmenu + for the same reason - opaque surface token, with the --canvas fallback + this file already uses elsewhere for when the (purely additive, in + principle removable) overlay isn't loaded. */ + background: var(--tile-deckend, var(--canvas)); border-radius: 16px; overflow: hidden; display: flex; flex-direction: column; @@ -877,7 +885,7 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; } .sheet-gruppe button:active { background: var(--shade); } .sheet-gruppe button.loeschen { color: var(--bad); } .sheet-abbrechen { - background: var(--tile-2); border: none; border-radius: 16px; + background: var(--tile-deckend, var(--canvas)); border: none; border-radius: 16px; font-family: inherit; font-size: 16px; color: var(--fg); padding: 16px; min-height: 52px; cursor: pointer; } diff --git a/testumgebung/README.md b/testumgebung/README.md index 5b32af9..4a75d56 100644 --- a/testumgebung/README.md +++ b/testumgebung/README.md @@ -41,12 +41,13 @@ lassen sich die `@state_trigger`-Abläufe wirklich auslösen: | `input_number.test_reichweite` | `sensor.audi_rs_4_avant_range_primary` | Übersichtsanzeige (nur bei manueller Zuordnung, `RANGE_SENSOR` ist per Default leer) | | `input_boolean.test_tuer_vl` | Tür vorne links | „sicher abgestellt" (nur bei manueller Zuordnung) | | `input_boolean.test_fenster_vl` | Fenster vorne links | „sicher abgestellt" (nur bei manueller Zuordnung) | -| `input_boolean.test_entriegelt` | Türschloss vorne links | „sicher abgestellt" (nur bei manueller Zuordnung) | -Die Kilometerstand-/Tankfüllstand-/Reichweite- und Tür-/Fenster-/Schloss-Sensoren +Die Kilometerstand-/Tankfüllstand-/Reichweite- und Tür-/Fenster-Sensoren bilden die Entitäten der abgelösten HACS-Integration `TommiG1/HA_VAG-EU-Data-Act` nach (siehe `einstellungen.py`) — für den Standardbetrieb ohne Bedeutung, aber weiterhin nützlich, um die manuelle Zuordnung über das Setup-Menü zu testen. +(Türschloss- und Haubenschloss-Erkennung wurden 2026-08-16 ersatzlos aus der +App entfernt, siehe `AGENTS.md`.) Beispiel — Fahrt auslösen (Zündung/ACC an, dann aus): diff --git a/testumgebung/konfiguration.yaml b/testumgebung/konfiguration.yaml index 1fb523f..875bab6 100644 --- a/testumgebung/konfiguration.yaml +++ b/testumgebung/konfiguration.yaml @@ -2,8 +2,8 @@ # Bildet zwei Datenquellen nach: den FMM003-Zuendungssensor (Fahrterkennung, # aktiv per Standard in einstellungen.py) sowie die Entitaeten der # abgeloesten HACS-Integration TommiG1/HA_VAG-EU-Data-Act (Kilometerstand, -# Tankfuellstand, Tueren/Fenster/Schloesser - in einstellungen.py per Default -# leer, aber ueber das Setup-Menue weiterhin manuell zuordenbar). Ueber die +# Tankfuellstand, Tueren/Fenster - in einstellungen.py per Default leer, +# aber ueber das Setup-Menue weiterhin manuell zuordenbar). Ueber die # input_*-Helfer sind die Werte zur Laufzeit aenderbar (Fahrterkennung, # Reifenzaehler, Tankerkennung haengen an @state_trigger). @@ -61,8 +61,6 @@ input_boolean: name: Test Tuer vorne links test_fenster_vl: name: Test Fenster vorne links - test_entriegelt: - name: Test entriegelt template: - binary_sensor: @@ -132,22 +130,6 @@ template: unique_id: test_audi_win_rr state: "off" device_class: window - - name: "Audi RS 4 Avant Front Left Door Lock" - unique_id: test_audi_lock_fl - state: "{{ is_state('input_boolean.test_entriegelt', 'on') }}" - device_class: lock - - name: "Audi RS 4 Avant Front Right Door Lock" - unique_id: test_audi_lock_fr - state: "off" - device_class: lock - - name: "Audi RS 4 Avant Rear Left Door Lock" - unique_id: test_audi_lock_rl - state: "off" - device_class: lock - - name: "Audi RS 4 Avant Rear Right Door Lock" - unique_id: test_audi_lock_rr - state: "off" - device_class: lock - name: "Audi RS 4 Avant Tailgate" unique_id: test_audi_tailgate state: "off"