Fuenf gemeldete Bugs behoben: Haubenschloss/Tuerschloesser entfernt, Bestaetigungsdialog-Transparenz, geteilte FMM003-Koordinaten, Laedt-Haenger, generischer Beleg-Parser
Entfernt Haubenschloss-/Tuerschloss-Erkennung dauerhaft aus Setup-Katalog und
Sicherheitscheck (auf ausdruecklichen Wunsch, nicht nur leer/unzuordenbar wie
der Rest der abgeloesten VAG-Integration). Behebt eine durchscheinende
Bestaetigungs-Sheet ("Doppelt zugeordnet" u.a.) - derselbe --tile-2-
Transparenz-Fehler, der fuer das Setup-Popup schon behoben war, hier
nachgezogen. Ergaenzt einen zweiten GPS-Pfad (STANDORT_LAT_SENSOR/
STANDORT_LON_SENSOR) fuer Integrationen wie flespi, die Breiten-/Laengengrad
als zwei eigene Sensoren statt als device_tracker-Attribute liefern. Haertet
den bekannten "Laedt ..."-Haenger beim App-Start zusaetzlich ab: Tab-Klicks
pruefen jetzt aktiv nach, ob Daten inzwischen da sind. Ergaenzt
shell_beleg_parser.py um einen stationsunabhaengigen Fallback fuer
Tankbelege unbekannter Formate (Adresse/Gesamtbetrag/Menge/Rabatt-Herleitung
wie vom Nutzer vorgegeben) - die bestehende Shell-Erkennung bleibt
unveraendert und weiterhin durch die zehn echten Testbelege abgedeckt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,13 @@
|
|||||||
# AGENTS.md — Project state, review findings, open items, and working rules
|
# AGENTS.md — Project state, review findings, open items, and working rules
|
||||||
|
|
||||||
**Last updated: 2026-08-13** (merged the `umsetzung-datametric360` branch — companion app phases
|
**Last updated: 2026-08-16** (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,
|
1–10 done, see `UMSETZUNGSPLAN.md`; 2026-08-13: cleaned up remaining EU Data Act residue, fixed
|
||||||
fixed oversized toggle switches, and fixed a desktop-layout audit (settings button / rings logo /
|
oversized toggle switches, and fixed a desktop-layout audit (settings button / rings logo / popups
|
||||||
popups overflowing past the capped content column) — see `DESIGN_AUDIT_2026-08-13.md` and section
|
overflowing past the capped content column) — see `DESIGN_AUDIT_2026-08-13.md`; 2026-08-16: fixed
|
||||||
C). This file is the entry point for every new agent
|
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
|
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.
|
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
|
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,
|
live swipe-row DOM) but could not be reproduced — documented in the audit file as open,
|
||||||
pending a screenshot from the real device.
|
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) —
|
- [ ] 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
|
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`
|
2026-08-12 (see section B); `DESIGN_REVIEW_2026-08-13.md` and `REVIEW_main_2026-08-13.md`
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ Aufbau eines Shell-eReceipts (aus dem echten Beispiel, Reihenfolge fix):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import sys
|
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():
|
def main():
|
||||||
if len(sys.argv) != 2:
|
if len(sys.argv) != 2:
|
||||||
print("Aufruf: python3 shell_beleg_parser.py <pfad-zur-pdf>", file=sys.stderr)
|
print("Aufruf: python3 shell_beleg_parser.py <pfad-zur-pdf>", file=sys.stderr)
|
||||||
@@ -205,10 +318,20 @@ def main():
|
|||||||
pdf_pfad = sys.argv[1]
|
pdf_pfad = sys.argv[1]
|
||||||
try:
|
try:
|
||||||
text = _text_aus_pdf(pdf_pfad)
|
text = _text_aus_pdf(pdf_pfad)
|
||||||
ergebnis = _parsen(text)
|
|
||||||
except Exception as exc:
|
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)
|
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))
|
print(json.dumps(ergebnis, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -65,14 +65,10 @@ TUER_SENSOREN = []
|
|||||||
# Fenster (§4.1) - keine Quelle mehr vorhanden.
|
# Fenster (§4.1) - keine Quelle mehr vorhanden.
|
||||||
FENSTER_SENSOREN = []
|
FENSTER_SENSOREN = []
|
||||||
|
|
||||||
# Verriegelung - keine Quelle mehr vorhanden.
|
|
||||||
TUERSCHLOSS_SENSOREN = []
|
|
||||||
|
|
||||||
# Heckklappe und Motorhaube - keine Quelle mehr vorhanden.
|
# Heckklappe und Motorhaube - keine Quelle mehr vorhanden.
|
||||||
HECKKLAPPE_SENSOR = ""
|
HECKKLAPPE_SENSOR = ""
|
||||||
HECKKLAPPENSCHLOSS_SENSOR = ""
|
HECKKLAPPENSCHLOSS_SENSOR = ""
|
||||||
HAUBE_SENSOR = ""
|
HAUBE_SENSOR = ""
|
||||||
HAUBENSCHLOSS_SENSOR = ""
|
|
||||||
|
|
||||||
# Vom Fahrzeug selbst gemeldete Service-Fälligkeit (ergänzt die App-eigene,
|
# Vom Fahrzeug selbst gemeldete Service-Fälligkeit (ergänzt die App-eigene,
|
||||||
# aus dem Servicebuch berechnete Prognose) - keine Quelle mehr vorhanden.
|
# aus dem Servicebuch berechnete Prognose) - keine Quelle mehr vorhanden.
|
||||||
@@ -87,6 +83,13 @@ INSPEKTION_STRECKE_SENSOR = ""
|
|||||||
# vom FMM003 geliefert.
|
# vom FMM003 geliefert.
|
||||||
STANDORT_TRACKER = "device_tracker.testzone_fmm003"
|
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
|
# Update-Funktion (Einstellungen -> "Update suchen", siehe
|
||||||
# updateverwaltung.py): Git-Repository, in das dieses Projekt gepflegt wird -
|
# updateverwaltung.py): Git-Repository, in das dieses Projekt gepflegt wird -
|
||||||
# z. B. ein privates GitHub-Repo, genau wie für die HACS-Integration bereits
|
# z. B. ein privates GitHub-Repo, genau wie für die HACS-Integration bereits
|
||||||
|
|||||||
@@ -81,10 +81,6 @@ FELDER = [
|
|||||||
"hinweis": "\"aus\"/off = zu.",
|
"hinweis": "\"aus\"/off = zu.",
|
||||||
"domains": ["binary_sensor"], "device_classes": ["window"], "units": [], "liste": True, "positionen": POSITIONEN,
|
"domains": ["binary_sensor"], "device_classes": ["window"], "units": [], "liste": True, "positionen": POSITIONEN,
|
||||||
"pflicht": False, "stichworte": ["fenster", "window"]},
|
"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",
|
{"key": "HECKKLAPPE_SENSOR", "label": "Heckklappe", "gruppe": "sicherheit",
|
||||||
"hinweis": "\"aus\"/off = zu.",
|
"hinweis": "\"aus\"/off = zu.",
|
||||||
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
|
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
|
||||||
@@ -97,10 +93,6 @@ FELDER = [
|
|||||||
"hinweis": "\"aus\"/off = zu.",
|
"hinweis": "\"aus\"/off = zu.",
|
||||||
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
|
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
|
||||||
"stichworte": ["haube", "hood", "bonnet", "motorhaube"]},
|
"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",
|
{"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.",
|
"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,
|
"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).",
|
"hinweis": "Braucht latitude/longitude-Attribute (HA-Konvention für device_tracker).",
|
||||||
"domains": ["device_tracker"], "device_classes": [], "units": [], "liste": False, "pflicht": False,
|
"domains": ["device_tracker"], "device_classes": [], "units": [], "liste": False, "pflicht": False,
|
||||||
"stichworte": ["standort", "gps", "tracker", "position", "ortung"]},
|
"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,
|
# Mehrere @state_trigger(f"...")-Dekoratoren (fahrterkennung.py,
|
||||||
|
|||||||
@@ -88,37 +88,59 @@ def _abs_zahl(wert):
|
|||||||
|
|
||||||
|
|
||||||
def _standort():
|
def _standort():
|
||||||
"""Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel) - liest
|
"""Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel). Zwei
|
||||||
latitude/longitude/gps_accuracy von der in STANDORT_TRACKER hinterlegten
|
mögliche Quellen, in dieser Reihenfolge versucht: (1) STANDORT_TRACKER,
|
||||||
device_tracker-Entity (HA-Konvention). Fehlt die Entity-ID oder ist sie
|
eine device_tracker-Entity mit latitude/longitude/gps_accuracy-Attributen
|
||||||
(noch) nicht verfügbar, liefert diese Funktion durchgehend None statt
|
(HA-Konvention) - der Normalfall; (2) STANDORT_LAT_SENSOR/
|
||||||
eines geratenen Werts - genau das zustand_oder_none()-Muster von oben,
|
STANDORT_LON_SENSOR, zwei eigene sensor-Entities für Breiten-/Längengrad -
|
||||||
hier als eigene Funktion, weil zusätzlich zum Zustand noch Attribute und
|
manche Integrationen (z. B. flespi) liefern Koordinaten so, statt als
|
||||||
der Aktualisierungszeitpunkt gebraucht werden: state.get() liefert dafür
|
Attribute einer einzelnen Tracker-Entity. Fehlt beides oder ist (noch)
|
||||||
ein StateVal (str-Unterklasse) mit .last_updated als datetime-Attribut -
|
nicht verfügbar, liefert diese Funktion durchgehend None statt eines
|
||||||
kein eigenständiges state.last_updated(), das gibt es in pyscript nicht."""
|
geratenen Werts - genau das zustand_oder_none()-Muster von oben, hier als
|
||||||
entity_id = einstellungen.STANDORT_TRACKER
|
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}
|
leer = {"lat": None, "lon": None, "genauigkeit_m": None, "zeit": None}
|
||||||
if not entity_id:
|
|
||||||
return leer
|
tracker_id = einstellungen.STANDORT_TRACKER
|
||||||
try:
|
if tracker_id:
|
||||||
wert = state.get(entity_id)
|
try:
|
||||||
attribute = state.getattr(entity_id)
|
wert = state.get(tracker_id)
|
||||||
except NameError:
|
attribute = state.getattr(tracker_id)
|
||||||
return leer
|
except NameError:
|
||||||
if not attribute or attribute.get("latitude") is None:
|
attribute = None
|
||||||
return leer
|
if attribute and attribute.get("latitude") is not None:
|
||||||
zeit = None
|
zeit = None
|
||||||
try:
|
try:
|
||||||
zeit = wert.last_updated.isoformat()
|
zeit = wert.last_updated.isoformat()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
return {
|
return {
|
||||||
"lat": _zu_zahl(attribute.get("latitude")),
|
"lat": _zu_zahl(attribute.get("latitude")),
|
||||||
"lon": _zu_zahl(attribute.get("longitude")),
|
"lon": _zu_zahl(attribute.get("longitude")),
|
||||||
"genauigkeit_m": _zu_zahl(attribute.get("gps_accuracy")),
|
"genauigkeit_m": _zu_zahl(attribute.get("gps_accuracy")),
|
||||||
"zeit": zeit,
|
"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"]
|
POSITIONEN = ["vorne links", "vorne rechts", "hinten links", "hinten rechts"]
|
||||||
@@ -137,14 +159,10 @@ def _sicherheitscheck():
|
|||||||
for pos, sensor in zip(POSITIONEN, einstellungen.FENSTER_SENSOREN):
|
for pos, sensor in zip(POSITIONEN, einstellungen.FENSTER_SENSOREN):
|
||||||
w = zustand_oder_none(sensor)
|
w = zustand_oder_none(sensor)
|
||||||
eintraege.append({"label": f"Fenster {pos}", "ok": None if w is None else w == "off"})
|
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 [
|
for label, sensor in [
|
||||||
("Heckklappe", einstellungen.HECKKLAPPE_SENSOR),
|
("Heckklappe", einstellungen.HECKKLAPPE_SENSOR),
|
||||||
("Heckklappenschloss", einstellungen.HECKKLAPPENSCHLOSS_SENSOR),
|
("Heckklappenschloss", einstellungen.HECKKLAPPENSCHLOSS_SENSOR),
|
||||||
("Motorhaube", einstellungen.HAUBE_SENSOR),
|
("Motorhaube", einstellungen.HAUBE_SENSOR),
|
||||||
("Haubenschloss", einstellungen.HAUBENSCHLOSS_SENSOR),
|
|
||||||
]:
|
]:
|
||||||
w = zustand_oder_none(sensor)
|
w = zustand_oder_none(sensor)
|
||||||
eintraege.append({"label": label, "ok": None if w is None else w == "off"})
|
eintraege.append({"label": label, "ok": None if w is None else w == "off"})
|
||||||
|
|||||||
@@ -2992,6 +2992,15 @@ function render() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const go = (name, id) => {
|
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
|
// Ein unbestätigter Servicebuch-Entwurf (siehe data-sbneu) verlässt die
|
||||||
// Anwendung nie ungespeichert - wird die Eintragsseite ohne Klick auf
|
// Anwendung nie ungespeichert - wird die Eintragsseite ohne Klick auf
|
||||||
// "Speichern" verlassen, verwerfen wir ihn hier statt ihn liegen zu lassen
|
// "Speichern" verlassen, verwerfen wir ihn hier statt ihn liegen zu lassen
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version": 1786744000}
|
{"version": 1786752000}
|
||||||
|
|||||||
@@ -853,7 +853,15 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
|
|||||||
@keyframes sheet-fade { from { opacity: 0 } to { opacity: 1 } }
|
@keyframes sheet-fade { from { opacity: 0 } to { opacity: 1 } }
|
||||||
@keyframes sheet-rein { from { transform: translateY(18px); opacity: 0 } to { transform: none; opacity: 1 } }
|
@keyframes sheet-rein { from { transform: translateY(18px); opacity: 0 } to { transform: none; opacity: 1 } }
|
||||||
.sheet-gruppe {
|
.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;
|
border-radius: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex; flex-direction: column;
|
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:active { background: var(--shade); }
|
||||||
.sheet-gruppe button.loeschen { color: var(--bad); }
|
.sheet-gruppe button.loeschen { color: var(--bad); }
|
||||||
.sheet-abbrechen {
|
.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);
|
font-family: inherit; font-size: 16px; color: var(--fg);
|
||||||
padding: 16px; min-height: 52px; cursor: pointer;
|
padding: 16px; min-height: 52px; cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_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_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_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`
|
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
|
nach (siehe `einstellungen.py`) — für den Standardbetrieb ohne Bedeutung, aber
|
||||||
weiterhin nützlich, um die manuelle Zuordnung über das Setup-Menü zu testen.
|
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):
|
Beispiel — Fahrt auslösen (Zündung/ACC an, dann aus):
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# Bildet zwei Datenquellen nach: den FMM003-Zuendungssensor (Fahrterkennung,
|
# Bildet zwei Datenquellen nach: den FMM003-Zuendungssensor (Fahrterkennung,
|
||||||
# aktiv per Standard in einstellungen.py) sowie die Entitaeten der
|
# aktiv per Standard in einstellungen.py) sowie die Entitaeten der
|
||||||
# abgeloesten HACS-Integration TommiG1/HA_VAG-EU-Data-Act (Kilometerstand,
|
# abgeloesten HACS-Integration TommiG1/HA_VAG-EU-Data-Act (Kilometerstand,
|
||||||
# Tankfuellstand, Tueren/Fenster/Schloesser - in einstellungen.py per Default
|
# Tankfuellstand, Tueren/Fenster - in einstellungen.py per Default leer,
|
||||||
# leer, aber ueber das Setup-Menue weiterhin manuell zuordenbar). Ueber die
|
# aber ueber das Setup-Menue weiterhin manuell zuordenbar). Ueber die
|
||||||
# input_*-Helfer sind die Werte zur Laufzeit aenderbar (Fahrterkennung,
|
# input_*-Helfer sind die Werte zur Laufzeit aenderbar (Fahrterkennung,
|
||||||
# Reifenzaehler, Tankerkennung haengen an @state_trigger).
|
# Reifenzaehler, Tankerkennung haengen an @state_trigger).
|
||||||
|
|
||||||
@@ -61,8 +61,6 @@ input_boolean:
|
|||||||
name: Test Tuer vorne links
|
name: Test Tuer vorne links
|
||||||
test_fenster_vl:
|
test_fenster_vl:
|
||||||
name: Test Fenster vorne links
|
name: Test Fenster vorne links
|
||||||
test_entriegelt:
|
|
||||||
name: Test entriegelt
|
|
||||||
|
|
||||||
template:
|
template:
|
||||||
- binary_sensor:
|
- binary_sensor:
|
||||||
@@ -132,22 +130,6 @@ template:
|
|||||||
unique_id: test_audi_win_rr
|
unique_id: test_audi_win_rr
|
||||||
state: "off"
|
state: "off"
|
||||||
device_class: window
|
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"
|
- name: "Audi RS 4 Avant Tailgate"
|
||||||
unique_id: test_audi_tailgate
|
unique_id: test_audi_tailgate
|
||||||
state: "off"
|
state: "off"
|
||||||
|
|||||||
Reference in New Issue
Block a user