From 39da68cd87a6ecb422bb0bbab861d3cfe755612e Mon Sep 17 00:00:00 2001 From: Tobi G Date: Sun, 16 Aug 2026 12:46:48 +0200 Subject: [PATCH] Fix broken map rendering, split GPS cleanup, receipt parser and design backlog Leaflet's stylesheet was appended to document.head, so it never reached the panel's shadow root: .leaflet-tile{position:absolute} never applied, tiles laid out as in-flow images (~1040px inside a 190px box) and the marker pane ended up far below the visible area. That is the real cause of the long-standing "fragmented Leaflet rendering" finding and of the invisible vehicle pin - every tile request had actually succeeded. The stylesheet now goes into the shadow root and is awaited before the map is built. Also in this round: - Map tiles are always light (Google-Maps-style), no dark variant at night. - Vehicle marker uses the real CI poi-car icons (poi-car-l >=34px, poi-car-s below), with a white halo so the outline stays readable on tiles. - Removed the obsolete combined STANDORT_TRACKER field; only the split lat/lon sensors remain. - Receipt upload: widened the try block so base64/save failures surface, and the frontend call site now reports a rejected service call. - Generic receipt parser: total detection is line-based (letter-spaced headings, no more matching the SUMME-EUR column header, tax lines excluded), address heuristic handles 4-digit postcodes and single-line address blocks, and "Preis/L" matches without a spelled-out "Liter". - Design backlog: red hairline frame on list rows (delete button bled through at fractional row heights) and select fields now use the grey background box. Shell 10-receipt regression suite still passes; all changes verified live in the audi_ha_test container. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 115 ++++++++++++++++- homeassistant/INSTALL.md | 7 +- homeassistant/data/shell_beleg_parser.py | 71 +++++++++-- homeassistant/pyscript/belegverarbeitung.py | 16 ++- .../pyscript/modules/einstellungen.py | 14 +-- homeassistant/pyscript/modules/entitaeten.py | 12 +- .../modules/frontend_veroeffentlichung.py | 77 ++++-------- homeassistant/www/audi-dashboard-app.js | 116 +++++++++++++----- homeassistant/www/audi-dashboard-ios.css | 9 +- homeassistant/www/audi-dashboard-version.json | 2 +- homeassistant/www/audi-dashboard.css | 22 +++- 11 files changed, 328 insertions(+), 133 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 02e6d16..2c7da24 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,8 +6,14 @@ oversized toggle switches, and fixed a desktop-layout audit (settings button / r 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 +and added a generic (station-independent) fuel-receipt parser fallback; later the same day: removed +the now-unused combined `STANDORT_TRACKER` field entirely (flespi never provides it), closed a real +silent-failure gap in the receipt-upload error path, and replaced the vehicle's map marker with a +two-tone pin+car icon — see section C; a third round the same day found and fixed the long-standing +"fragmented Leaflet rendering" defect at its root — the Leaflet stylesheet was loaded into +`document.head` and therefore never reached the panel's shadow root — plus light-only map tiles, +the CI `poi-car` marker, the red frame on list rows, the select-field grey box, and two more +generic-receipt-parser gaps). 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. @@ -150,7 +156,9 @@ the `design/` export's RS-6 error extends into the technical sample data (73 l t FIN series 4G). Four browser-verified rendering defects: switches stretched by `.feld label{flex:1 1 auto}` (state display becomes ambiguous), double "offen" in the trip list, empty oil-change tile when the service book is empty, and fragmented Leaflet rendering in all -three maps (verify on a real device). Positives: the 300/400 font-weight rule and de-DE +three maps (✅ **fixed 2026-08-16** — root cause was Leaflet's stylesheet being loaded into +`document.head`, which never reaches the panel's shadow root; see the third-round entry in +"Open items"). Positives: the 300/400 font-weight rule and de-DE formatting hold everywhere; `design-system/` is verifiably brand-free. ### HA panel — known gaps (most also in SPECIFICATION.md §7) @@ -666,6 +674,107 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed 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/`. +- [x] Follow-up round the same day (2026-08-16), three more items: + (1) **Removed `STANDORT_TRACKER` entirely** — user confirmed the combined device_tracker + field is no longer needed (flespi only ever provides the split lat/lon sensors added earlier + that day). Deleted the field from `einstellungen.py` and `entitaeten.py`'s Setup catalog, and + simplified `_standort()` in `frontend_veroeffentlichung.py` down to the single split-sensor + path (no more two-branch fallback). `overrides_schreiben()`'s existing "only known keys + survive" behavior (see its docstring) meant no explicit migration was needed for stale + `STANDORT_TRACKER` entries already sitting in `data/entitaeten.json` — confirmed live: this + container's own leftover override file still had `STANDORT_TRACKER` from earlier testing + (plus stale `TUERSCHLOSS_SENSOREN`/`HAUBENSCHLOSS_SENSOR` from before finding (1) of the + previous entry), all silently dropped on the next Setup save, which is exactly why the GPS + tile was still showing "Kein GPS-Signal" after the user's own remap attempt earlier — that + attempt happened to save while the old combined field was still in the catalog, so nothing + ever actually persisted `STANDORT_LAT_SENSOR`/`STANDORT_LON_SENSOR`. Redid the Setup save + live (mapped both to `sensor.testcar_b9_fmm003_testintegratoin_lat/longitude_coordinate_value`) + and confirmed the Übersicht/fullscreen Standort map switched from the "no signal" placeholder + to a real rendered map at the correct coordinates (South Tyrol terrain, "Burgleralm" label + visible). + (2) **Fuel-receipt "silently does nothing", root cause found** — reproduced the exact + real upload flow (button click → real `` → synthetic `File`/`DataTransfer` + dispatch → `FileReader` → `hass.callService`) via browser instrumentation: the happy path + works completely (backend parses, publishes `pyscript.audi_dashboard_beleg_ergebnis`, frontend + fills the form) — `belegverarbeitung.py`'s existing `try/except` around `_parser_aufrufen()` + already publishes a visible `fehler` for parser failures specifically. The actual gap: + `base64.b64decode()` and `_pdf_speichern()` in `audi_dashboard_beleg_hochladen()` were + *outside* that try block — any failure there (corrupted upload, disk full, ...) raised an + unhandled exception with zero UI feedback, since `serviceRufen()`'s generic `.catch()` only + does `console.error()`. Widened the try block to cover all three steps. Also stopped using + `serviceRufen()` for this one call site specifically (`audi-dashboard-app.js`, the + `eingabe.onchange` handler) — it now calls `HASS.callService()` directly with its own + `.catch()` that sets `belegFehler` and re-renders, so a rejected service call (network, + timeout — anything that bypasses the backend's own state-publish) is visible too, not just + logged. Verified against the real 10-receipt Shell suite (still passing) both before and + after this change. + (3) **Vehicle map marker replaced with a two-tone pin+car icon** — added `CI.pinCar` (a + balloon/pin path + white badge circle + a simplified car-front glyph built from rects, not + circles, after an isolated-DOM-injection test round showed a circles-for-headlights version + read as a face/animal rather than a car) to `audi-dashboard-app.js`, replacing the single-color + `poiCar` teardrop previously used by `fahrzeugMarkerSVG()`. Updated both marker call sites' + `iconAnchor` to the new pin's actual tip position and simplified `.fahrzeug-pin` in + `audi-dashboard.css` (no more `color:var(--red)`/white-halo-filter hack, since the new icon + carries its own fixed colors). Verified the icon markup renders correctly via an isolated + DOM-injection test (bypassing Leaflet); could **not** get a final on-map screenshot in + `audi_ha_test` — the Standort map tiles themselves only ever partially load in this container + (reproduced on a fully fresh reload, unrelated to this change), consistent with a previously + documented network limitation of this specific sandbox (see the Standort-Kachel entry above: + reverse-geocoding via Nominatim had the same kind of container-specific network issue). + Structurally verified only; needs a look on a real device/network to confirm final visual + placement. + All three deployed to `audi_ha_test` (version `1786758000`) and synced to `installationspaket/`. +- [x] Third round the same day (2026-08-16) — **the "fragmented Leaflet rendering" defect is solved + at its root**, plus four smaller items: + (1) **Leaflet CSS never reached the shadow root.** `leafletLaden()` appended + `leaflet.min.css` to `document.head`, but the panel renders inside `this.shadowRoot` — and + document stylesheets do not cross a shadow boundary. Inside the panel, `.leaflet-tile + {position:absolute}` and friends therefore never applied: the tiles laid themselves out as + ordinary in-flow `` elements (two per row, ~1040 px of stacked height inside a 190 px + box), so only a thin strip of map was ever visible, and the marker pane ended up ~1009 px + below the visible area — which is exactly why the new vehicle pin appeared "missing". This + also explains the earlier misdiagnosis as a container/network limitation: every tile request + succeeded (`complete:true`, `naturalWidth:512`), the geometry was the problem. Fixed by + injecting the stylesheet into `ROOT` and awaiting it before the map is built; the script tag + stays in `document.head` (it must, `window.L` is global). The CSS injection is deliberately + re-checked on every `leafletLaden()` call rather than guarded by the `window.L` check, because + a HA panel remount produces a fresh shadow root while `window.L` is already set. Verified live: + both the Übersicht preview map and the fullscreen Standort map now fill their containers and + show the pin at the vehicle position. + (2) **Maps are now always light.** `TILES[theme]` switched to CARTO `dark_all` in Nacht mode; + replaced by a single themeless `TILE_URL` (`light_all`), matching how Google Maps & co. keep + the standard road map light regardless of app theme. + (3) **Vehicle marker now uses the real CI `poi-car` icons** (`poi-car-l.svg` / `poi-car-s.svg` + from the delivered icon set) instead of the hand-built two-tone pin from the previous round: + `CI.poiCarL` (48-grid) at ≥34 px, the existing `CI.poiCar` (24-grid) below, anchors recomputed + per variant. Since these are single-color outline forms in `currentColor`, `.fahrzeug-pin` + regained a stacked white drop-shadow halo so the outline stays readable on map tiles. + (4) **Red partial frame on Fahrten/Tanken list rows** (open since the 2026-08-13 design audit, + finding 3) — reproduced and confirmed by recoloring the delete button live. Row heights are + fractional (66.28 px), so `.swipe-content`'s edges miss the device-pixel grid and the red + `.swipe-delete` behind it bled through as a hairline around every row. Fixed by only painting + the delete button while a swipe is actually happening: `.swipe-delete{visibility:hidden}` plus + a `wischt` class set on the first movement of the gesture (and the existing `swiped` class for + the open row). Verified the full gesture still works — button appears at the first pixel of + drag and the row settles open at −84 px. + (5) **Select fields now show the grey background box** (design audit finding 1) — replaced + `.feld select{color:var(--ios-tint)}` with `background:var(--ios-fill);padding:8px 10px`, + matching `.mitEinheit input` ("Pause bis [15] Minuten"), as the user asked. Verified on + Modell / Ölwechsel-Intervall / Bildposition; free text fields stay plain, so the box now + genuinely marks "there is a choice here". + (6) **Generic receipt parser: total and station name** — a real Austrian non-Shell receipt + returned the right litres but 6,00 € and no station. Two causes, both fixed: the free-running + total pattern matched the *column heading* `SUMME-EUR` and read the next line's article number + as the amount, and bon printers letter-space headings (`G E S A M T BETRAG EUR: 92,60`), which + the keyword never matched. Total detection is now line-based — keyword checked against the + whitespace-stripped line, amount must be a real money value (`_GELD`, decimals required), tax + lines (`MWST`/`UST`/`VAT`/`STEUER`/`NETTO`) excluded, largest remaining candidate wins. The + address heuristic no longer demands three fixed lines with a 5-digit postcode (AT uses 4 + digits, and this receipt carries `SHELL TANKSTELLE, 6450 SÖLDEN` on one line): it finds the + first postcode line in the header and takes the name from the line above. `Preis/L` without a + spelled-out "Liter" now matches too. Result on that receipt: 42,13 l · 92,60 € · 2,198 €/l · + no discount · `AUTO B. FRISCHMANN GMBH`. The 10-receipt Shell regression suite still passes. + All deployed to `audi_ha_test` (version `1786765000`) and 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/INSTALL.md b/homeassistant/INSTALL.md index b9bf708..ee67b68 100644 --- a/homeassistant/INSTALL.md +++ b/homeassistant/INSTALL.md @@ -177,8 +177,11 @@ Oberfläche „unbekannt"/em-dash statt eines Werts, kein Absturz: (`RANGE_SENSOR`) — bis zu einer neuen Datenquelle unbelegt, siehe `AGENTS.md` (die frühere `TommiG1/HA_VAG-EU-Data-Act`-Integration liefert diese nicht mehr) -- Standort (`STANDORT_TRACKER`, ein `device_tracker`) und Batteriespannung - (`BATTERIE_SENSOR`) — vom FMM003, z. B. `device_tracker.` +- Standort (`STANDORT_LAT_SENSOR`/`STANDORT_LON_SENSOR`, zwei eigene + `sensor`-Entities für Breiten-/Längengrad — flespi liefert Koordinaten so, + nicht als `latitude`/`longitude`-Attribute eines `device_tracker`) und + Batteriespannung (`BATTERIE_SENSOR`) — vom FMM003, z. B. + `sensor._latitude_coordinate_value`/`..._longitude_coordinate_value` bzw. `sensor._external_power_voltage` (**nicht** `..._battery_voltage` — das ist die interne Pufferzelle des Trackers, nicht die Fahrzeugbatterie) diff --git a/homeassistant/data/shell_beleg_parser.py b/homeassistant/data/shell_beleg_parser.py index fb03160..a444dd3 100644 --- a/homeassistant/data/shell_beleg_parser.py +++ b/homeassistant/data/shell_beleg_parser.py @@ -91,6 +91,14 @@ def _iso(tag, monat, jahr, stunde, minute, sekunde="0"): _ZAHL = r"[\d.,]+" +# Echte Geldangabe: Nachkommastellen sind Pflicht, damit im stationsunabhängigen +# Parser (_parsen_generisch) keine Artikel-/Belegnummer als Betrag durchgeht. +_GELD = r"\d{1,3}(?:[ .]\d{3})*,\d{2}" +_GESAMT_WOERTER = ("GESAMT", "ABSOLUT", "ENDBETRAG", "ZUZAHLEN", "TOTAL", "SUMME") +# Steuer- und Nettozeilen tragen dieselben Schlüsselwörter ("MWST GESAMT"), +# nennen aber nie den bezahlten Rechnungsbetrag. +_STEUER_WOERTER = ("MWST", "UST", "VAT", "STEUER", "NETTO") + def _parsen(text): def suchen(muster, *, pflicht=True, flags=re.MULTILINE): @@ -225,20 +233,57 @@ def _parsen_generisch(text): 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 - ) + zeilen = [z.rstrip() for z in text.splitlines()] + + # ---- Adresse ----------------------------------------------------------- + # Die PLZ-Ort-Angabe steht nicht zuverlässig auf einer eigenen Zeile (ein + # österreichischer Beleg führt sie als "SHELL TANKSTELLE, 6450 SÖLDEN" + # zusammen mit der Filialbezeichnung) und PLZ sind vier- *oder* + # fünfstellig (AT/DE). Deshalb nicht mehr drei feste Zeilen hintereinander + # verlangen, sondern im Belegkopf die erste Zeile mit PLZ+Ort suchen; der + # Name ist die erste Textzeile darüber. + plz_zeile = None + for i, zeile in enumerate(zeilen[:12]): + if re.search(r"\b\d{4,5}\s+[A-Za-zÄÖÜäöüß]", zeile): + plz_zeile = i + break + station_name = None + station_address = None + if plz_zeile is not None: + station_address = zeilen[plz_zeile].strip() + for zeile in reversed(zeilen[:plz_zeile]): + if zeile.strip(): + station_name = zeile.strip() + break + if station_name is None: + for zeile in zeilen[:5]: + if zeile.strip(): + station_name = zeile.strip() + break # ---- Gesamtbetrag ------------------------------------------------------ - gesamt = suchen(r"(?:Gesamt\w*|Absolut\w*|Endbetrag|Summe|Total)\D{0,20}?(" + _ZAHL + r")\s*(?:€|EUR)?") - if not gesamt: + # Zeilenweise statt über den ganzen Text, aus zwei Gründen, die beide an + # einem echten Beleg aufgefallen sind: Bondrucker sperren Überschriften + # gern buchstabenweise ("G E S A M T BETRAG EUR: 92,60"), und ein frei + # laufendes Muster fand vorher die Spaltenüberschrift "SUMME-EUR" und + # las die Artikelnummer der Folgezeile als Betrag. Deshalb: Schlüsselwort + # auf der leerzeichenfreien Zeile prüfen, Betrag nur als echte Geldangabe + # (mit Nachkommastellen) akzeptieren und Steuerzeilen ausschließen. + kandidaten = [] + for zeile in zeilen: + kompakt = re.sub(r"\s+", "", zeile).upper() + if not any(wort in kompakt for wort in _GESAMT_WOERTER): + continue + if any(wort in kompakt for wort in _STEUER_WOERTER): + continue + betraege = re.findall(_GELD, zeile) + if betraege: + kandidaten.append(_de_zahl(betraege[-1])) + if not kandidaten: raise ValueError("Kein Gesamtbetrag gefunden (weder 'Gesamt' noch 'Absolut')") - fuel_total_eur = _de_zahl(gesamt.group(1)) + # Mehrere Treffer (z. B. Summenzeile und Kartenzahlungszeile): der + # Rechnungsbetrag ist der größte - Teilbeträge sind nie größer. + fuel_total_eur = max(kandidaten) # ---- Menge in Litern ---------------------------------------------------- menge = suchen(r"(?:Menge|Amount)\D{0,15}?(" + _ZAHL + r")\s*(?:l\b|L\b|Liter|Ltr\.?)") @@ -253,8 +298,10 @@ def _parsen_generisch(text): berechneter_preis_l = round(fuel_total_eur / liters, 3) # ---- Aufgedruckter Preis/Liter - Abweichung verrät einen Rabatt ------- + # "Preis/L" (ohne ausgeschriebenes "Liter") kommt auf echten Belegen vor, + # deshalb ist alles nach dem L optional. 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")" + r"(?:Preis\s*/?\s*(?:je\s*)?L(?:iter|tr\.?)?|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 diff --git a/homeassistant/pyscript/belegverarbeitung.py b/homeassistant/pyscript/belegverarbeitung.py index 0abb859..1e3d92d 100644 --- a/homeassistant/pyscript/belegverarbeitung.py +++ b/homeassistant/pyscript/belegverarbeitung.py @@ -103,14 +103,20 @@ def audi_dashboard_beleg_hochladen(pdf_base64=None, dateiname=None, tank_id=None # task.executor entfällt hier bewusst: _pdf_speichern/_parser_aufrufen sind # in dieser pyscript-Datei definiert, task.executor akzeptiert aber nur # echte externe Python-Funktionen (siehe Kopfkommentar in profil.py). - inhalt_bytes = base64.b64decode(pdf_base64) - dateiname = dateiname or f"beleg_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf" - pdf_pfad = _pdf_speichern(inhalt_bytes, dateiname) - + # + # Alles bis zum geparsten Ergebnis in einem try: eine unbehandelte + # Exception hier (kaputtes Base64, volle Festplatte, ...) wäre für die + # Oberfläche unsichtbar geblieben - serviceRufen() im Frontend loggt einen + # rejizierten Service-Aufruf nur in die Browser-Konsole, zeigt aber nichts + # an (siehe audi-dashboard-app.js). Bisher war nur der Parser-Aufruf + # selbst so abgesichert; b64decode/_pdf_speichern fehlten. try: + inhalt_bytes = base64.b64decode(pdf_base64) + dateiname = dateiname or f"beleg_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf" + pdf_pfad = _pdf_speichern(inhalt_bytes, dateiname) geparst = _parser_aufrufen(pdf_pfad) except Exception as exc: - log.error(f"audi_dashboard: Beleg konnte nicht geparst werden: {exc}") + log.error(f"audi_dashboard: Beleg konnte nicht verarbeitet werden: {exc}") frontend_veroeffentlichung.beleg_ergebnis_veroeffentlichen({"fehler": "Beleg konnte nicht gelesen werden", "tank_id": tank_id}) return diff --git a/homeassistant/pyscript/modules/einstellungen.py b/homeassistant/pyscript/modules/einstellungen.py index 832628d..ba43fc0 100644 --- a/homeassistant/pyscript/modules/einstellungen.py +++ b/homeassistant/pyscript/modules/einstellungen.py @@ -77,16 +77,10 @@ OELWECHSEL_STRECKE_SENSOR = "" NAECHSTE_INSPEKTION_SENSOR = "" INSPEKTION_STRECKE_SENSOR = "" -# Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel). Erwartet -# eine device_tracker.*-Entity mit "latitude"/"longitude"-Attributen (HA- -# Konvention, siehe _standort() in frontend_veroeffentlichung.py) - jetzt -# 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. +# Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel). Breiten-/ +# Längengrad als zwei eigene sensor-Entities (flespi liefert Koordinaten so, +# nicht als Attribute einer device_tracker-Entity - siehe _standort() in +# frontend_veroeffentlichung.py). STANDORT_LAT_SENSOR = "" STANDORT_LON_SENSOR = "" diff --git a/homeassistant/pyscript/modules/entitaeten.py b/homeassistant/pyscript/modules/entitaeten.py index 32e3f47..d38f16d 100644 --- a/homeassistant/pyscript/modules/entitaeten.py +++ b/homeassistant/pyscript/modules/entitaeten.py @@ -109,18 +109,14 @@ FELDER = [ "hinweis": "Meldet die Integration oft als negativen Countdown - die App zeigt nur den Betrag.", "domains": ["sensor"], "device_classes": ["distance"], "units": ["km", "mi"], "liste": False, "pflicht": False, "stichworte": ["inspektion", "inspection", "distance", "strecke"]}, - {"key": "STANDORT_TRACKER", "label": "GPS-Standort", "gruppe": "standort", - "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).", + "hinweis": "Breitengrad als eigener Sensor (z. B. flespi).", "domains": ["sensor"], "device_classes": [], "units": [], "liste": False, "pflicht": False, - "stichworte": ["breitengrad", "latitude", "lat"]}, + "stichworte": ["breitengrad", "latitude", "lat", "standort", "gps", "position", "ortung"]}, {"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).", + "hinweis": "Längengrad als eigener Sensor (z. B. flespi).", "domains": ["sensor"], "device_classes": [], "units": [], "liste": False, "pflicht": False, - "stichworte": ["längengrad", "longitude", "lon", "lng"]}, + "stichworte": ["längengrad", "longitude", "lon", "lng", "standort", "gps", "position", "ortung"]}, ] # Mehrere @state_trigger(f"...")-Dekoratoren (fahrterkennung.py, diff --git a/homeassistant/pyscript/modules/frontend_veroeffentlichung.py b/homeassistant/pyscript/modules/frontend_veroeffentlichung.py index 6a2d002..dd0cff5 100644 --- a/homeassistant/pyscript/modules/frontend_veroeffentlichung.py +++ b/homeassistant/pyscript/modules/frontend_veroeffentlichung.py @@ -88,59 +88,36 @@ def _abs_zahl(wert): def _standort(): - """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.""" + """Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel) - liest + STANDORT_LAT_SENSOR/STANDORT_LON_SENSOR, zwei eigene sensor-Entities für + Breiten-/Längengrad (flespi liefert Koordinaten so, nicht als Attribute + einer einzelnen device_tracker-Entity). Fehlt eine der beiden Entity-IDs + oder ist der Zustand (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 der Aktualisierungszeitpunkt gebraucht wird: + 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} - 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 + if not lat_id or not lon_id: + return leer + 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} POSITIONEN = ["vorne links", "vorne rechts", "hinten links", "hinten rechts"] diff --git a/homeassistant/www/audi-dashboard-app.js b/homeassistant/www/audi-dashboard-app.js index f2bb246..da54ea4 100644 --- a/homeassistant/www/audi-dashboard-app.js +++ b/homeassistant/www/audi-dashboard-app.js @@ -386,31 +386,57 @@ function ring(pct) { /* ------------------------------------------------------------------ Karte Leaflet weiterhin per CDN geladen (siehe Kopfkommentar). */ let MAP = null, TILE = null, LEAFLET_LADEND = null; -const TILES = { - nacht: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", - tag: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", -}; +/* Bewusst themeunabhaengig hell: Kartendienste (Google Maps & Co.) zeigen die + Standard-Strassenkarte immer im hellen Stil, weil Strassen, Beschriftungen + und Wegfarben darauf lesbar bleiben. Die frueher bei Nacht eingeblendeten + dark_all-Kacheln liessen die Karte im Panel schwarz wirken. */ +const TILE_URL = "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"; const ATTR = "© OpenStreetMap · CARTO"; /* Satellitenansicht der Standort-Vollbildkarte (Layer-Knopf) - wie die CARTO-Straßenkacheln oben ohne eigenen Schlüssel nutzbar. */ const SAT_TILE_URL = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"; const SAT_ATTR = "Tiles © Esri"; -function leafletLaden() { - if (window.L) return Promise.resolve(); - if (LEAFLET_LADEND) return LEAFLET_LADEND; - LEAFLET_LADEND = new Promise((resolve, reject) => { - const css1 = document.createElement("link"); - css1.rel = "stylesheet"; - css1.href = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css"; - document.head.appendChild(css1); - const script = document.createElement("script"); - script.src = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"; - script.onload = resolve; - script.onerror = reject; - document.head.appendChild(script); +const LEAFLET_CSS_URL = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css"; +const LEAFLET_JS_URL = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"; + +/* Das Leaflet-Stylesheet muss in den Shadow-Root, nicht in document.head: + Dokument-Styles greifen nicht in ein Shadow-DOM hinein. Lag es im Head, + fehlte drinnen u. a. .leaflet-tile{position:absolute} - die Kacheln flossen + dann als gewoehnliche Bilder untereinander (bei 8 Kacheln ueber 1000 px + hoch), von der Karte war nur ein schmaler Streifen zu sehen und der + Fahrzeugmarker landete weit unterhalb des sichtbaren Ausschnitts. Das JS + gehoert dagegen weiterhin in den Head, damit window.L global bleibt. */ +function leafletCssEinhaengen() { + if (!ROOT || ROOT.querySelector("link[data-leaflet-css]")) return Promise.resolve(); + return new Promise((fertig) => { + const css = document.createElement("link"); + css.rel = "stylesheet"; + css.href = LEAFLET_CSS_URL; + css.dataset.leafletCss = "1"; + /* Auch bei Ladefehler weitermachen statt haengen zu bleiben - dann sieht + die Karte zwar wieder falsch aus, aber die Ansicht baut sich auf. */ + css.onload = fertig; + css.onerror = fertig; + ROOT.appendChild(css); }); - return LEAFLET_LADEND; +} + +function leafletLaden() { + /* Bewusst bei jedem Aufruf: haengt HA das Panel neu ein, entsteht ein neuer + Shadow-Root ohne das Stylesheet, waehrend window.L laengst gesetzt ist. */ + const css = leafletCssEinhaengen(); + if (window.L) return css; + if (!LEAFLET_LADEND) { + LEAFLET_LADEND = new Promise((resolve, reject) => { + const script = document.createElement("script"); + script.src = LEAFLET_JS_URL; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); + } + return Promise.all([css, LEAFLET_LADEND]); } function mapBox() { return `
`; } @@ -437,7 +463,7 @@ function fakeTrack(a, b, n = 13) { } function tiles() { if (!MAP || !window.L) return; - const url = TILES[HOST.dataset.theme]; + const url = TILE_URL; if (TILE) MAP.removeLayer(TILE); TILE = window.L.tileLayer(url, { maxZoom: 19, attribution: ATTR }).addTo(MAP); } @@ -498,10 +524,15 @@ const STANDORT_PEEK_PX = 96; // sichtbarer Anteil im geschlossenen Zustand, muss function standortBekannt() { return CAR.standortLat != null && CAR.standortLon != null; } -/* Kartenmarkierung: poi-car aus dem Audi-CI-Satz (assets/icons/poi-car.svg). - Die Spitze sitzt unten, deshalb ankert Leaflet am unteren Rand. */ +/* Kartenmarkierung: poi-car aus dem CI-Satz, in den beiden gelieferten + Groessenvarianten - poi-car-l (48er-Raster, mehr Detail) ab 34 px, darunter + poi-car-s (24er-Raster). Die Nadelspitze sitzt unten, deshalb ankert Leaflet + am unteren Rand (siehe iconAnchor an den Aufrufstellen: Spitze bei y=45.25 + im 48er- bzw. y=23.3 im 24er-viewBox). */ function fahrzeugMarkerSVG(px = 34) { - return ``; + const gross = px >= 34; + const box = gross ? 48 : 24; + return ``; } /* Fahrzustand fürs Menü: "fährt" solange die aktuelle Fahrt noch offen ist @@ -576,7 +607,7 @@ function standortErfassenFallsNoetig() { } /* Reverse-Geocoding über die öffentliche Nominatim-API (OpenStreetMap) - - wie die Kartenkacheln oben (TILES) ohne eigenen Schlüssel; bei diesem + wie die Kartenkacheln oben (TILE_URL) ohne eigenen Schlüssel; bei diesem Aufrufvolumen (ein Fahrzeug, nur bei geöffneter Standortansicht) im Rahmen der Nominatim-Nutzungsbedingungen unproblematisch, genau wie das bereits dokumentierte Leaflet-über-CDN-Vorgehen (siehe AGENTS.md). Cache-Schlüssel @@ -639,14 +670,14 @@ function standortTiles() { if (SMAP_TILE) SMAP.removeLayer(SMAP_TILE); SMAP_TILE = SMAP_STIL === "satellit" ? window.L.tileLayer(SAT_TILE_URL, { maxZoom: 19, attribution: SAT_ATTR }).addTo(SMAP) - : window.L.tileLayer(TILES[HOST.dataset.theme], { maxZoom: 19, attribution: ATTR }).addTo(SMAP); + : window.L.tileLayer(TILE_URL, { maxZoom: 19, attribution: ATTR }).addTo(SMAP); } function standortMarkerZeichnen() { if (!SMAP || !window.L) return; if (FAHRZEUG_MARKER) { SMAP.removeLayer(FAHRZEUG_MARKER); FAHRZEUG_MARKER = null; } if (USER_MARKER) { SMAP.removeLayer(USER_MARKER); USER_MARKER = null; } if (standortBekannt()) { - const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `
${fahrzeugMarkerSVG(38)}
`, iconSize: [38, 38], iconAnchor: [18, 36] }); + const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `
${fahrzeugMarkerSVG(38)}
`, iconSize: [38, 38], iconAnchor: [19, 36] }); FAHRZEUG_MARKER = window.L.marker([CAR.standortLat, CAR.standortLon], { icon }).addTo(SMAP); FAHRZEUG_MARKER.on("click", () => standortMenuOeffnen()); } @@ -693,10 +724,10 @@ async function initVorschauMap() { zoomControl: false, attributionControl: false, dragging: false, scrollWheelZoom: false, doubleClickZoom: false, boxZoom: false, keyboard: false, touchZoom: false, }); - TMAP_TILE = window.L.tileLayer(TILES[HOST.dataset.theme], { maxZoom: 19, attribution: ATTR }).addTo(TMAP); + TMAP_TILE = window.L.tileLayer(TILE_URL, { maxZoom: 19, attribution: ATTR }).addTo(TMAP); if (standortBekannt()) { TMAP.setView([CAR.standortLat, CAR.standortLon], 14); - const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `
${fahrzeugMarkerSVG(32)}
`, iconSize: [32, 32], iconAnchor: [15, 30] }); + const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `
${fahrzeugMarkerSVG(32)}
`, iconSize: [32, 32], iconAnchor: [15, 31] }); window.L.marker([CAR.standortLat, CAR.standortLon], { icon }).addTo(TMAP); } else { TMAP.setView(kartenMittelpunkt(), 11); @@ -781,6 +812,9 @@ const CI = { carSearch: '', gps: '', poiCar: '', + /* poi-car-l - grosse Variante desselben CI-Symbols (48er-Raster), fuer die + Fahrzeugnadel auf der Vollbildkarte. Siehe fahrzeugMarkerSVG(). */ + poiCarL: '', selectAll: '', planRoute: '', upload: '', @@ -3103,11 +3137,9 @@ function ereignisseVerdrahten() { ROOT.getElementById("theme").addEventListener("click", () => { themeSetzen(HOST.dataset.theme === "nacht" ? "tag" : "nacht"); themeIcon(); - /* Neu zeichnen, damit auch die Leaflet-Kacheln den Stil wechseln: die - Kachel-URL wird beim Aufbau aus TILES[theme] gewaehlt und nicht - nachtraeglich aktualisiert. Ohne dies blieb die Karte bis zum naechsten - Backend-Update im alten Stil - seit der Dauerkarte auf der Uebersicht - deutlich sichtbar. */ + /* Neu zeichnen, damit theme-abhaengige, in JS gesetzte Werte (z. B. die + aus --red gelesene Routenfarbe in initMap) mitziehen. Die Kacheln selbst + sind seit TILE_URL themeunabhaengig hell und brauchen das nicht mehr. */ render(); }); // Wischgeste über Pointer Events (nicht Touch Events), damit dieselbe @@ -3153,6 +3185,9 @@ function ereignisseVerdrahten() { } e.preventDefault(); swipeWarGezogen = true; + // Loeschknopf erst waehrend der Geste einblenden (siehe .swipe-delete in + // audi-dashboard.css) - sonst blitzt er dauerhaft an den Zeilenraendern durch. + swipeAktivWrap.classList.add("wischt"); const basis = swipeAktivWrap.classList.contains("swiped") ? -SWIPE_BREITE : 0; swipeDX = Math.max(-SWIPE_BREITE - 20, Math.min(0, basis + dx)); const inhalt = swipeAktivWrap.querySelector(".swipe-content"); @@ -3163,6 +3198,7 @@ function ereignisseVerdrahten() { if (!swipeAktivWrap) return; const inhalt = swipeAktivWrap.querySelector(".swipe-content"); inhalt.style.transition = ""; inhalt.style.transform = ""; + swipeAktivWrap.classList.remove("wischt"); if (swipeGeste === "x") { if (swipeDX <= -SWIPE_BREITE / 2) { schliesseAlleSwipes(); swipeAktivWrap.classList.add("swiped"); } else { swipeAktivWrap.classList.remove("swiped"); } @@ -3510,7 +3546,21 @@ function ereignisseVerdrahten() { const leser = new FileReader(); leser.onload = () => { const base64 = leser.result.split(",")[1]; - serviceRufen("audi_dashboard_beleg_hochladen", { pdf_base64: base64, dateiname: datei.name, tank_id: tankId }); + // Kein serviceRufen() hier: dessen catch loggt nur in die Browser- + // Konsole (bei allen anderen Aufrufen ok, die reagieren auf + // datenLaden() statt auf den Rückgabewert) - ein rejizierter + // Service-Aufruf (Backend nicht erreichbar, Timeout, ...) blieb + // für den Beleg-Upload sonst komplett unsichtbar in der + // Oberfläche, obwohl der Backend-seitige Parser-Fehlerpfad selbst + // schon einen Fehler veröffentlicht (siehe belegverarbeitung.py). + HASS.callService("pyscript", "audi_dashboard_beleg_hochladen", + { pdf_base64: base64, dateiname: datei.name, tank_id: tankId } + ).catch((err) => { + console.error("audi_dashboard: audi_dashboard_beleg_hochladen", err); + belegFehler = { context: kontext, meldung: "Beleg konnte nicht hochgeladen werden" }; + if (!tankId) belegEntwurf = null; + render(); + }); }; leser.readAsDataURL(datei); }; diff --git a/homeassistant/www/audi-dashboard-ios.css b/homeassistant/www/audi-dashboard-ios.css index 6cc6e32..2802cfa 100644 --- a/homeassistant/www/audi-dashboard-ios.css +++ b/homeassistant/www/audi-dashboard-ios.css @@ -140,10 +140,11 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo padding:8px 4px;color:var(--fg2)} /* Design-Audit 2026-08-13: ein