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