Tankstellenmarke als eigenes Feld, Belegfelder in der App richtig zugeordnet
Die Marke steht auf jedem Beleg, war aber nirgends gespeichert: der Parser erkennt sie seit dem 16.08.2026, faltete sie aber nur in station_name. Fuer jeden Beleg, den eine aeltere Fassung eingelesen hat, blieb dort der Betreibername stehen und die Marke war danach nicht mehr zu holen. - shell_beleg_parser: station_brand als eigenes Feld aus beiden Parserwegen, dazu marke_aus_text()/marke_aus_pdf() - sie lesen nur den Belegkopf und haengen nicht am vollstaendigen Einlesen. - belege.marken_nachtragen(): traegt die Marke einmal beim Start aus der abgelegten Belegdatei nach. Nur wo das Feld gar nicht vorkommt und die Datei liegt; ein einmal eingetragenes Feld wird nie ueberschrieben. - Panel und App: die gespeicherte Marke geht vor, geraten wird nur, wo keine da ist. Ein Teil, der fuer sich genommen eine Kopfmarke ist, taugt zudem nie als Ort. Dazu Punkt 5 der Meldung vom 04.09.2026: ein eingelesener Beleg fuellte nur Datum und Uhrzeit. Der Parser las ihn immer vollstaendig - die App fragte liter/kosten/station/ersparnis/kraftstoff ab, veroeffentlicht werden aber die Namen des Belegs (liters/fuel_total_eur/station_name/discount/fuel_type). Uebereingestimmt hat allein ts. Die Zuordnung steht jetzt als belegFormularwerte() in daten/belegentwurf.ts und wird gegen die echte Nutzlast dieses Belegs geprueft. tsc sauber, 258 Tests (die drei neuen Marken-Tests gegen den alten Stand als scheiternd nachgewiesen), vite build sauber, Panel als Modul geparst, Parser-Suite 11 Tests gegen elf echte Belege. Panel und App liefern fuer elf Faelle byteweise dasselbe. Live in audi_ha_test auf 2026.9.4.21 ohne Traceback: Uebersicht "Shell, Nuernberger Str., Ansbach", Einzelbeleg zweizeilig mit Marke, und der echte geteilte Beleg fuellt durchs Backend das ganze Formular.
This commit is contained in:
@@ -137,6 +137,7 @@ def _beleg_felder(geparst: dict, pdf_pfad: str) -> dict:
|
||||
"ts_payment": geparst.get("ts_payment"),
|
||||
"ts_tse": geparst.get("ts_tse"),
|
||||
"station_id": geparst.get("station_id"),
|
||||
"station_brand": geparst.get("station_brand"),
|
||||
"station_name": geparst.get("station_name"),
|
||||
"station_address": geparst.get("station_address"),
|
||||
"article_no": geparst.get("article_no"),
|
||||
@@ -370,3 +371,60 @@ async def tankvorgang_loeschen(k: Koordinator, tank_id: str | None) -> None:
|
||||
_LOGGER.info("Tankvorgang %s gelöscht", tank_id)
|
||||
else:
|
||||
_LOGGER.warning("Tankvorgang %s nicht gefunden", tank_id)
|
||||
|
||||
|
||||
async def marken_nachtragen(k: Koordinator) -> None:
|
||||
"""Traegt die Tankstellenmarke bei Belegen nach, die vor dem 16.08.2026
|
||||
eingelesen wurden.
|
||||
|
||||
WARUM ES DAS BRAUCHT
|
||||
--------------------
|
||||
Bis zum 16.08.2026 kannte der Parser keine Marken; `station_name` trug
|
||||
deshalb die Firmierung des Betreibers ("TC Sengül GmbH"), und die Marke
|
||||
war fuer diese Datensaetze nirgends gespeichert. Auf dem BELEG steht sie
|
||||
aber weiterhin - jeder der geprueften Altbelege beginnt mit "Shell-Station"
|
||||
bzw. "SHELL STATION". Ohne dieses Nachtragen bliebe die Einzelbeleg-Zeile
|
||||
dieser Vorgaenge dauerhaft ohne Marke (Befund des Eigentuemers,
|
||||
04.09.2026).
|
||||
|
||||
VORSICHTIG GEHALTEN
|
||||
-------------------
|
||||
* Angefasst wird nur, wo `station_brand` noch gar nicht vorkommt und die
|
||||
Belegdatei tatsaechlich liegt. Ein einmal eingetragenes Feld - auch mit
|
||||
Wert None - wird nie wieder ueberschrieben; damit ueberschreibt das
|
||||
Nachtragen keine Korrektur von Hand und laeuft nicht bei jedem Start
|
||||
erneut ueber dieselben Belege.
|
||||
* Gelesen wird nur der Belegkopf (marke_aus_pdf), nicht der ganze Beleg.
|
||||
Ein Beleg, an dem beide Parser scheitern wuerden, gibt seine Marke
|
||||
trotzdem her - und ein Lesefehler bleibt folgenlos.
|
||||
* Geschrieben wird nur, wenn sich wirklich etwas geaendert hat.
|
||||
"""
|
||||
vorgaenge = await k.ablage.tankvorgaenge_lesen()
|
||||
offen = [
|
||||
v
|
||||
for v in vorgaenge
|
||||
if "station_brand" not in v and v.get("receipt_file")
|
||||
]
|
||||
if not offen:
|
||||
return
|
||||
|
||||
def _lesen() -> dict[str, str | None]:
|
||||
gefunden: dict[str, str | None] = {}
|
||||
for vorgang in offen:
|
||||
pfad = vorgang.get("receipt_file")
|
||||
if not pfad or not os.path.exists(pfad):
|
||||
continue
|
||||
gefunden[vorgang["tank_id"]] = shell_beleg_parser.marke_aus_pdf(pfad)
|
||||
return gefunden
|
||||
|
||||
marken = await k.hass.async_add_executor_job(_lesen)
|
||||
if not marken:
|
||||
return
|
||||
|
||||
for vorgang in vorgaenge:
|
||||
if vorgang.get("tank_id") in marken:
|
||||
vorgang["station_brand"] = marken[vorgang["tank_id"]]
|
||||
await k.ablage.tankvorgaenge_schreiben(vorgaenge)
|
||||
benannt = sorted({m for m in marken.values() if m})
|
||||
zusatz = ", ".join(benannt) if benannt else "keine erkennbar"
|
||||
_LOGGER.info("Tankstellenmarke bei %d Beleg(en) nachgetragen (%s)", len(marken), zusatz)
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":"2026.9.4.20","sha256":"95213bb0998806d4b59b4d520d2dd93a4f1c7ce48991e6f77b38e9dbca8247a8","bytes":322945,"gebaut":"2026-09-04T14:28:33Z"}
|
||||
{"version":"2026.9.4.21","sha256":"3601a55ee4120c8508dd8f5018dcfbe806f10d87fd706763a0bbd97377d217d1","bytes":323037,"gebaut":"2026-09-04T14:55:41Z"}
|
||||
Binary file not shown.
@@ -1331,6 +1331,13 @@ function markeErkennen(name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* NUR DER ZWEITE WEG: seit dem 04.09.2026 speichert die Integration die Marke
|
||||
als eigenes Feld `station_brand` (shell_beleg_parser._marke, aus dem
|
||||
Belegkopf), und fuer Altbelege traegt sie belege.marken_nachtragen() aus der
|
||||
abgelegten Belegdatei nach. Wo dieses Feld etwas hergibt, gilt es - hier
|
||||
geraten wird nur, wo es fehlt: bei Vorgaengen ohne Beleg, bei von Hand
|
||||
eingetragenen Tankstellen und solange das Nachtragen nicht gelaufen ist. */
|
||||
|
||||
/* Kennzeichen einer Firmierung. Ein Teil, der so etwas enthaelt, ist der
|
||||
BETREIBER und gehoert nach Vorgabe des Eigentuemers nicht in die Anzeige. */
|
||||
const FIRMA_MUSTER = /\b(gmbh|mbh|ohg|kg|ag|e\.\s?k\.|inh\.|& co)\b/i;
|
||||
@@ -1357,13 +1364,23 @@ function strasseKuerzen(strasse) {
|
||||
Die Markenerkennung lief bis hierher nur ueber den Namen - bei den aelteren
|
||||
Belegen steht die Marke dort gar nicht, deshalb blieb in der Uebersicht die
|
||||
Firmierung stehen ("...GmbH", Nutzerbefund 04.09.2026), waehrend die
|
||||
Anschrift daneben ungenutzt blieb. Gelesen werden jetzt beide Felder. */
|
||||
function tankstelleTeile(name, adresse) {
|
||||
const teile = { marke: markeErkennen(name) || "", strasse: "", hausnummer: "", plz: "", ort: "", roh: (name || "").trim() };
|
||||
Anschrift daneben ungenutzt blieb. Gelesen werden jetzt beide Felder.
|
||||
|
||||
`marke` ist die gespeicherte Angabe des Belegs (station_brand). Sie geht
|
||||
vor, weil sie vom Belegkopf stammt und nicht aus dem Anzeigenamen geraten
|
||||
ist - genau die Luecke, durch die der Beleg vom 10.11.2025 ohne Marke
|
||||
dastand, obwohl "Shell Station" auf ihm steht. */
|
||||
function tankstelleTeile(name, adresse, marke) {
|
||||
const teile = { marke: (marke || "").trim() || markeErkennen(name) || "", strasse: "", hausnummer: "", plz: "", ort: "", roh: (name || "").trim() };
|
||||
const quellen = (adresse || "").split(",");
|
||||
for (const stueck of (name || "").split(",")) {
|
||||
const t = stueck.trim();
|
||||
if (teile.marke && t.toLowerCase() === teile.marke.toLowerCase()) continue;
|
||||
// Ein Teil, der fuer sich genommen eine Kopfmarke IST, taugt nie als Ort.
|
||||
// Frueher stand hier nur der Vergleich mit der gewaehlten Marke; seit die
|
||||
// gespeicherte Angabe vorgeht, koennen beide auseinanderfallen, und dann
|
||||
// rutschte die Marke aus dem Namen in den Ort.
|
||||
const eigen = t ? markeErkennen(t) : null;
|
||||
if (eigen && eigen.toLowerCase() === t.toLowerCase()) continue;
|
||||
quellen.push(t);
|
||||
}
|
||||
for (const stueck of quellen) {
|
||||
@@ -1395,16 +1412,16 @@ function tankstelleTeile(name, adresse) {
|
||||
(Vorgabe des Eigentuemers, 04.09.2026). Ergibt die Zerlegung nichts, bleibt
|
||||
der rohe Name stehen - ihn wegzulassen hiesse, eine bekannte Tankstelle
|
||||
"unbekannt" zu nennen. */
|
||||
function tankstelleKurz(name, adresse) {
|
||||
const t = tankstelleTeile(name, adresse);
|
||||
function tankstelleKurz(name, adresse, marke) {
|
||||
const t = tankstelleTeile(name, adresse, marke);
|
||||
return [t.marke, t.strasse, t.ort].filter(Boolean).join(", ") || t.roh || "unbekannt";
|
||||
}
|
||||
|
||||
/* Die beiden Zeilen im Einzelbeleg: oben Marke und Strasse mit Hausnummer,
|
||||
unten Postleitzahl und Ort - geschrieben wie eine Anschrift, das Komma
|
||||
trennt nur die Marke ab. `oben` ist nie leer. */
|
||||
function tankstelleZeilen(name, adresse) {
|
||||
const t = tankstelleTeile(name, adresse);
|
||||
function tankstelleZeilen(name, adresse, marke) {
|
||||
const t = tankstelleTeile(name, adresse, marke);
|
||||
const strasse = [t.strasse, t.hausnummer].filter(Boolean).join(" ");
|
||||
let oben = [t.marke, strasse].filter(Boolean).join(", ");
|
||||
let unten = [t.plz, t.ort].filter(Boolean).join(" ");
|
||||
@@ -1442,10 +1459,10 @@ function tankstelleSuchtext(name, adresse) {
|
||||
/* Ziel fuer den Kartendienst. Hier gehoeren Name UND Anschrift zusammen:
|
||||
Apple Karten und Google Maps kennen Betriebsnamen und treffen damit die
|
||||
Zapfsaeule statt des Nachbargebaeudes. */
|
||||
function tankstelleZiel(name, adresse) {
|
||||
function tankstelleZiel(name, adresse, marke) {
|
||||
const a = (adresse || "").trim();
|
||||
if (!a) return (name || "").trim();
|
||||
return [markeErkennen(name) || "", a].filter(Boolean).join(", ");
|
||||
return [(marke || "").trim() || markeErkennen(name) || "", a].filter(Boolean).join(", ");
|
||||
}
|
||||
|
||||
/* Die Kandidaten der Reihe nach nachschlagen, erster Treffer gewinnt. */
|
||||
@@ -1467,9 +1484,9 @@ function apfelgeraet() {
|
||||
Tankstellen-Verweis im Einzelbeleg will das Zweite (Vorgabe des
|
||||
Eigentuemers, 04.09.2026): erst sehen, wo sie liegt - eine Route erwartet
|
||||
dort niemand, und sie ist im Kartendienst einen Tipp entfernt. */
|
||||
function kartendienstUrl(name, adresse, pos, modus = "route") {
|
||||
function kartendienstUrl(name, adresse, pos, modus = "route", marke) {
|
||||
const beschriftung = (name || "").trim();
|
||||
const ziel = pos ? `${pos.lat},${pos.lon}` : tankstelleZiel(name, adresse);
|
||||
const ziel = pos ? `${pos.lat},${pos.lon}` : tankstelleZiel(name, adresse, marke);
|
||||
if (!ziel) return null;
|
||||
if (apfelgeraet()) {
|
||||
if (modus === "zeigen") {
|
||||
@@ -1499,7 +1516,7 @@ function tankstelleAufKarteZeigen(karte, pos, f) {
|
||||
// Dieselben zwei Zeilen wie in der Wertezeile darunter (tankstelleZeilen) -
|
||||
// die Nadel soll nicht die Firmierung des Betreibers nennen, wenn die Zeile
|
||||
// die Kopfmarke zeigt.
|
||||
const zeilen = tankstelleZeilen(f.station_name, f.station_address);
|
||||
const zeilen = tankstelleZeilen(f.station_name, f.station_address, f.station_brand);
|
||||
window.L.marker([pos.lat, pos.lon], { icon }).addTo(karte)
|
||||
.bindPopup(esc(zeilen.oben) + (zeilen.unten ? "<br>" + esc(zeilen.unten) : ""));
|
||||
// Position merken und den Knopf scharf schalten - er kann erst jetzt etwas
|
||||
@@ -4752,7 +4769,7 @@ function vFuel() {
|
||||
<span>Ø ${de(schnittpreis(fm), 2)} €/l</span></span></button>
|
||||
<div class="acc-b ${mi === 0 ? "on" : ""}" id="fm${j}${m}">`;
|
||||
fm.forEach((f) => { const preis = literpreis(f); h += swipeZeile(`fill:${f.tank_id}`, `<button class="leaf" data-go="fill:${f.tank_id}">
|
||||
<span class="k">${dedat(new Date(f.ts))}<small>${esc(tankstelleKurz(f.station_name, f.station_address))}</small></span>
|
||||
<span class="k">${dedat(new Date(f.ts))}<small>${esc(tankstelleKurz(f.station_name, f.station_address, f.station_brand))}</small></span>
|
||||
<span class="v">${f.liters != null ? de(f.liters, 2) + " l" : ""}<small>${preis != null ? de(preis, 2) + " €/l" : ""}</small></span>
|
||||
<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>`); });
|
||||
h += `</div></div>`;
|
||||
@@ -4793,8 +4810,8 @@ function belegKarte(f) {
|
||||
Geraets; ohne Name und ohne Anschrift bleibt es eine stumme Zeile, denn
|
||||
ein Verweis, der nirgendwohin fuehrt, waere schlechter als keiner. */
|
||||
function tankstellenZeile(f) {
|
||||
const { oben, unten } = tankstelleZeilen(f.station_name, f.station_address);
|
||||
const url = kartendienstUrl(f.station_name, f.station_address, null, "zeigen");
|
||||
const { oben, unten } = tankstelleZeilen(f.station_name, f.station_address, f.station_brand);
|
||||
const url = kartendienstUrl(f.station_name, f.station_address, null, "zeigen", f.station_brand);
|
||||
const inhalt =
|
||||
`<span class="tankstellenzeile-ort">${esc(oben)}${unten ? `<small>${esc(unten)}</small>` : ""}</span>` +
|
||||
(url ? `<svg class="chev" viewBox="0 0 6 10" aria-hidden="true"><polyline points="1,1 5,5 1,9"/></svg>` : "");
|
||||
|
||||
@@ -49,6 +49,7 @@ from homeassistant.helpers.storage import Store
|
||||
|
||||
from . import (
|
||||
batterie,
|
||||
belege,
|
||||
bilder,
|
||||
fahrterkennung,
|
||||
flespi,
|
||||
@@ -216,6 +217,11 @@ class Koordinator:
|
||||
# Fahrt warten. Reicht das Ortsbudget eines Laufs nicht, plant das
|
||||
# Screening selbst einen zweiten ein (screening._nachfassen_planen).
|
||||
await screening.durchfuehren(self)
|
||||
# Aus demselben Grund hier: die Tankstellenmarke ist so ein neues Feld.
|
||||
# Sie steht auf jedem Beleg, wurde aber vor dem 16.08.2026 nicht
|
||||
# gespeichert - nachgetragen wird sie aus der abgelegten Belegdatei,
|
||||
# einmal je Vorgang (siehe belege.marken_nachtragen).
|
||||
await belege.marken_nachtragen(self)
|
||||
# Weicht die Reglerstellung vom zuletzt gelesenen Geraetewert ab,
|
||||
# wird die Aenderung hier vorgemerkt - auch wenn sie waehrend eines
|
||||
# Ausfalls gesetzt wurde oder ein frueherer Auftrag nie zum Zug kam.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.9.4.20",
|
||||
"version": "2026.9.4.21",
|
||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||
"codeowners": [
|
||||
|
||||
@@ -123,6 +123,25 @@ def _marke(zeilen):
|
||||
return None
|
||||
|
||||
|
||||
def marke_aus_text(text):
|
||||
"""Die Kopfmarke eines Belegtextes - der oeffentliche Weg zu _marke().
|
||||
|
||||
Getrennt vom vollstaendigen Einlesen, weil die Marke nur die ersten Zeilen
|
||||
braucht: sie laesst sich auch aus einem Beleg holen, an dem beide Parser
|
||||
scheitern wuerden. Genau das braucht das Nachtragen in belege.py fuer
|
||||
Belege, die vor dem 16.08.2026 eingelesen wurden."""
|
||||
return _marke((text or "").splitlines())
|
||||
|
||||
|
||||
def marke_aus_pdf(pdf_pfad):
|
||||
"""Wie marke_aus_text(), aber liest das PDF selbst. None, wenn die Datei
|
||||
sich nicht lesen laesst - eine fehlende Marke ist kein Fehlerfall."""
|
||||
try:
|
||||
return marke_aus_text(_text_aus_pdf(pdf_pfad))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _ist_strasse(zeile):
|
||||
"""Straßenzeilen tragen eine Hausnummer oder ein Straßenwort - der
|
||||
Betreibername ("AUTO B. FRISCHMANN GMBH") tut beides nicht."""
|
||||
@@ -234,6 +253,12 @@ def _parsen(text):
|
||||
# Anzeigename "Marke, Straße, Ort" (siehe _tankstelle). Der Ort steht
|
||||
# auf dem Bon als "85057 Ingolstadt" - die PLZ gehört nur in die
|
||||
# ausführliche station_address darunter, nicht in den Anzeigenamen.
|
||||
# Die Marke steht seit dem 04.09.2026 ZUSAETZLICH als eigenes Feld da.
|
||||
# Sie nur in den Anzeigenamen zu falten hat sie fuer jeden Beleg
|
||||
# verloren, den ein aelterer Parser eingelesen hat - dort blieb der
|
||||
# Betreibername stehen ("TC Sengül GmbH"), und keine Oberflaeche
|
||||
# konnte die Marke nachtraeglich noch finden.
|
||||
"station_brand": _marke(text.splitlines()),
|
||||
"station_name": _tankstelle(
|
||||
_marke(text.splitlines()),
|
||||
station.group("strasse").strip(),
|
||||
@@ -325,7 +350,8 @@ def _parsen_generisch(text):
|
||||
break
|
||||
if station_address and strasse and strasse not in station_address:
|
||||
station_address = f"{strasse}, {station_address}"
|
||||
station_name = _tankstelle(_marke(zeilen), strasse, ort, ersatzname=betreiber)
|
||||
marke = _marke(zeilen)
|
||||
station_name = _tankstelle(marke, strasse, ort, ersatzname=betreiber)
|
||||
|
||||
# ---- Gesamtbetrag ------------------------------------------------------
|
||||
# Zeilenweise statt über den ganzen Text, aus zwei Gründen, die beide an
|
||||
@@ -408,6 +434,7 @@ def _parsen_generisch(text):
|
||||
"ts_payment": None,
|
||||
"ts_tse": None,
|
||||
"station_id": None,
|
||||
"station_brand": marke,
|
||||
"station_name": station_name,
|
||||
"station_address": station_address,
|
||||
"article_no": None,
|
||||
|
||||
Reference in New Issue
Block a user