HANDOFF – Lidl/HelloFresh → Grocy Tracking-System
Übergabe aus einer Cowork-Session an Claude Code. Dieses Dokument enthält den kompletten Kontext, alle technischen Fakten und den nächsten Schritt, damit nahtlos weitergearbeitet werden kann. Lies es zuerst vollständig.
1. Ziel des Projekts
Ein selbstgehostetes System (auf einem VPS mit Docker), das:
- Lidl-Plus-Einkäufe automatisch abruft – inkl. echter EAN pro Position.
- Produkte über OpenFoodFacts anreichert (Klarname, Kategorie, kcal + Makros).
- Alles in Grocy schreibt: Bestand, Preise, Nährwerte.
- Alle historischen Bons rückwirkend importiert (Preis-/Kaufhistorie).
- Später HelloFresh-Rezepte einbindet.
- Über die Grocy-API als Datenquelle für YNAB und Makro-Tracking dient.
Nutzer: Dennis. Sprache der Doku/Skripte: Deutsch. Land/Sprache Lidl: DE / de.
2. Getroffene Entscheidungen (nicht erneut hinterfragen)
- Lidl-Anbindung: eigene Integration statt der Bibliothek
lidl-plus(unmaintained seit Aug 2024, Selenium-Login kaputt). Wir nutzen die Daten-API direkt. - ~~App-Token-Weg (mit EAN) wurde bewusst gewählt~~ OBSOLET (Juni 2026, live
verifiziert): Lidl hat das strukturierte Bon-Detail mit
codeInput/EAN serverseitig abgeschafft (v2-Detail erlaubt nur noch DELETE; v3 liefertticketType: "HTML"). Es gibt KEINE EAN mehr aus der API – auch nicht über Community-Forks. Neuer Weg: HTML-Bon parsen (interne 7-stelligedata-art-idals stabiler Schlüssel) + Hybrid-Anreicherung (Entscheidung Dennis, 05.06.2026): automatische OpenFoodFacts-Namenssuche mit Lidl-Eigenmarken-Filter und Konfidenz; unsichere Treffer kommen ohne Makros rein und werden präzise, sobald Dennis das Produkt zuhause per Grocy-App scannt (echte EAN → exakter OFF-Treffer). Zusätzlich fordert Dennis die Lidl-DSGVO-Datenkopie an (App → Konto → Datenschutz) – falls dort EANs drin sind, nutzen wir sie für den historischen Import. - Historische Bons werden im „sofort verbrauchen"-Modus importiert: erzeugt Preis-/Kaufhistorie, ohne den realen Bestand mit Phantom-Artikeln zu verfälschen.
- Voller Grocy-Funktionsumfang (Bestand, MHD, Einkaufsliste) ab dem Go-live; Historie kommt zusätzlich rein.
- Credentials-Grenze: Lidl-Passwort und SMS-2FA macht Dennis selbst im Browser. Das Skript fängt nur den OAuth-Code ab. Niemals Passwort/SMS automatisiert eingeben.
- Auslieferungsstil: Runbook + fertige Configs/Skripte, Dennis führt auf dem VPS aus.
3. Phasenplan & Status
| Phase | Inhalt | Status |
|---|---|---|
| 1 | Grocy auf VPS (docker-compose, Makro-Userfields, API-Key) | ✅ fertig (05.06.2026): Grocy 4.6.0 läuft, API-Key + 4 Userfields per DB angelegt & per API verifiziert. Port nur auf 127.0.0.1 + WireGuard (192.168.3.8) gebunden. Offen: Dennis ändert das admin-Passwort |
| 2 | Eigene Lidl-Integration (OAuth + Ticket-API) | ✅ fertig & live verifiziert (05.06.2026): Login ok, refresh_token in .env, 467 Bons abrufbar. KEINE EAN mehr (s. Abschnitt 2) → HTML-Parser gebaut, 40/40 Bons rechnen auf den Cent (inkl. Gewichtsartikel, Rabatte, Pfandrückgabe) |
| 3 | Sync-Skript: Bon → OpenFoodFacts (Namenssuche) → Grocy | ✅ gebaut & an 2 echten Bons verifiziert (05.06.2026): sync.py mit import/enrich/status, --dry-run, idempotent. Produkte mit LIDL:{artId}-Barcode, sichere OFF-Treffer mit EAN+Makros, Kategorien-Mapping per Mehrheits-Score. Offen: Mapping/Schwelle (0.75) mit Dennis feinjustieren |
| 4 | Historische Bons rückwirkend importieren | ✅ fertig (05.06.2026): alle 467 Bons (Okt 2024 – Mai 2026) im „sofort verbrauchen"-Modus importiert. 1020 Produkte, 405 (40 %) automatisch sicher mit EAN+Makros, Restbestand 0, Preishistorie vollständig (z. B. Apfel grün: 77 Preispunkte) |
| 5 | HelloFresh-Anbindung | Recherche ✅ (05.06.2026, s. RECHERCHE-HELLOFRESH.md + Abschnitt 9), Umsetzung offen |
Unmittelbar nächster Schritt: HelloFresh-Sync bauen (Phase 5, Bauplan in
RECHERCHE-HELLOFRESH.md; Dennis muss dafür einmalig das JWT aus dem Web-Login
ziehen). Laufend: täglicher Cron 07:30 bucht neue Bons mit echtem Bestand; Dennis
scannt unsichere Produkte nach (sync.py status listet sie) → sync.py enrich.
4. Dateien in diesem Paket
lidl-grocy/
├── HANDOFF.md # dieses Dokument
├── README-PHASE2.md # Anleitung zur Lidl-Integration
├── RECHERCHE-HELLOFRESH.md # Phase-5-Recherche (Endpoints, Bauplan, Risiken)
├── lidl.py # eigene Lidl-Plus-Integration (login/tickets/fetch+Parser)
├── sync.py # Phase 3: Bons -> OFF-Namenssuche -> Grocy (import/enrich/status)
├── sync-state.json # lokaler Zustand (importierte Bons, artId-Mapping) – gitignored
├── .env # LIDL_REFRESH_TOKEN, GROCY_URL, GROCY_API_KEY – gitignored!
├── .venv/ # python3 -m venv, curl_cffi installiert
└── requirements.txt # curl_cffi
docker-compose.yml # (übergeordneter Ordner) Grocy, Port nur localhost+WireGuard
RUNBOOK.md # Phase-1-Anleitung (Grocy aufsetzen) + Gesamt-Roadmap
lidl-kassenbon-2026-05-30.json # Beispiel-Bon (aus Web-Session, OHNE EAN) als Referenz
Git-Repo liegt in /home/dennis/silverscale (branch main). Täglicher Sync:
Crontab-Eintrag 07:30 (sync.py import --all, neue Bons mit echtem Bestand).
5. Technische Referenz Lidl (verifiziert)
5.1 OAuth (accounts.lidl.com)
Quelle: Quellcode von lidl-plus + OIDC-Discovery, beides geprüft.
client_id=LidlPlusNativeClient- Discovery:
https://accounts.lidl.com/.well-known/openid-configuration authorization_endpoint=https://accounts.lidl.com/connect/authorizetoken_endpoint=https://accounts.lidl.com/connect/tokenscope=openid profile offline_access lpprofile lpapisredirect_uri=com.lidlplus.app://callback- PKCE: S256 unterstützt
- Token-Endpoint-Auth: HTTP-Basic-Header
base64("LidlPlusNativeClient:secret"),Content-Type: application/x-www-form-urlencoded - Grants:
authorization_code(mitcode_verifier),refresh_token - Authorize-URL bekommt zusätzlich
Country=DEundlanguage=de-DE - Der zurückkommende
codeist hex/uppercase (code=([0-9A-F]+)).
5.2 Daten-API (Bearer-Token) – Stand Juni 2026, live verifiziert
- Default-Header: NUR
Authorization: Bearer <token>+Accept-Language: de. ACHTUNG: Der gefakte HeaderApp-Version: 999.99.9triggert Akamai – der Request hängt dann bis zum Timeout (HTTP/2-Stream-Reset bzw. 0 Bytes). Keine Fake-App-Header! - Bon-Liste:
GET https://tickets.lidlplus.com/api/v2/DE/tickets?pageNumber=1&onlyFavorite=false→{ tickets:[...], totalCount, size }, weitere Seiten viapageNumber. (v2 läuft.) - Bon-Detail:
GET https://tickets.lidlplus.com/api/v3/DE/tickets/{id}(v2-GET ist tot, nur noch DELETE!) →ticketType: "HTML", Bon steckt inhtmlPrintedReceipt. KeincodeInput/EAN mehr. Struktur des HTML: jede Bon-Zeile = mehrere<span>s mit gleicherid="purchase_list_line_N"; Artikelzeilenclass="article"mitdata-art-id(7-stellig, intern),data-art-description,data-unit-price,data-art-quantity,data-tax-type. Rabatte:class="discount"(mitdata-promotion-id) sowie klassenlose Zeilen („Preisvorteil", „Aktionsrabatt") – gehören jeweils zur vorhergehenden Artikelzeile. Gewichtsartikel haben eine Folgezeile (gleiche data-Attribute, eigene Zeilen-ID) mit0,498 | kg x | 7,97 | EUR/kg. Pfandrückgaben: css_bold-Zeilen „Pfandrückgabe" mit negativem Betrag. → alles implementiert inlidl.py::parse_receipt(), validiert an 40 Bons (Summe Positionen + Rabatte + Pfandrückgaben == totalAmount, auf den Cent). - Coupons (Referenz, für späteres Auto-Aktivieren): v2
coupons.lidlplus.com/api/v2/DEliefert teils 404 (Issue #26). Funktionierender Weg ist v1:GET coupons.lidlplus.com/app/api/v1/promotionslistundPOST .../app/api/v1/promotions/{id}/activation, jeweils mit HeaderCountry: DE.
5.3 Akamai-Schutz
accounts.lidl.com blockt python-requests (HTTP 403, TLS-Fingerprinting).
Lösung: curl_cffi mit impersonate="chrome" für alle Lidl-Calls. Bereits umgesetzt.
5.4 Web- vs. App-API (wichtige Erkenntnis)
- Die Web-Bons (
www.lidl.de/mre/api/v1/tickets/{id}) liefern nurhtmlPrintedReceiptmitdata-art-id= interne 7-stellige Artikelnummer (z. B.0081315= „Apfel grün"), KEINE EAN. Eine öffentliche PLU→EAN-Datenbank existiert nicht. Deshalb gehen wir den App-Weg – dort steckt die echte EAN incodeInput. - Die Coupon-Aktivierung lief in der Cowork-Session bereits erfolgreich über die
Web-UI (
www.lidl.de/prm/promotions-list); für die Automatisierung später lieber die v1-Coupon-API oben nutzen.
6. Technische Referenz Grocy
- Image
lscr.io/linuxserver/grocy:latest, Port 9283, Volume./grocy-config:/config,PUID/PGID/TZsetzen. Default-Loginadmin/admin→ sofort ändern. - API: Header
GROCY-API-KEY: <key>. Test:GET /api/system/info. Relevante Endpoints:/api/objects/products,/api/objects/product_groups,/api/objects/quantity_units,/api/stock/products/{id}/add,/api/userfields/products/{id}. - Makro-Userfields (Entität
products, Typ Dezimalzahl) anlegen:energy_kcal_100g,protein_g_100g,carbs_g_100g,fat_g_100g. - Preis-Tracking in den Stock-Einstellungen aktivieren.
7. Technische Referenz OpenFoodFacts (für Phase 3)
- Produkt per EAN:
GET https://world.openfoodfacts.org/api/v2/product/{ean}.json - Relevante Felder:
product.product_name(ggf._de),product.categories/categories_tags,product.nutriments:energy-kcal_100g,proteins_100g,carbohydrates_100g,fat_100g. - Höflicher User-Agent setzen. Nicht-Lebensmittel (Reiniger, Spielzeug) haben oft keinen Eintrag/keine Nährwerte → ohne Makros importieren, das ist ok.
- OFF-Kategorien sind sehr granular → auf eine kleine Liste Grocy-Produktgruppen mappen (Obst, Gemüse, Molkerei, Getränke, Backwaren, Aufschnitt, Snacks, Haushalt, …). Mapping-Tabelle ist noch mit Dennis abzustimmen.
8. Phase 3 – Bauplan (noch offen, angepasst an HTML-Realität)
Sync-Skript (sync.py), Ablauf pro Bon-Position (aus lidl.py::parse_receipt()):
1. Schlüssel ist die Lidl-Artikelnummer (artId). Pfand-Artikel („Pfand 0,25")
gesondert behandeln/überspringen.
2. Grocy nach Produkt mit Barcode LIDL:{artId} suchen (so bleibt das echte
EAN-Feld frei für nachgescannte EANs).
- existiert → Bestand buchen (Menge, Preis, Kaufdatum).
- existiert nicht → OFF-Namenssuche (search.openfoodfacts.org; die alte
cgi/search.pl-API wirft oft 503!) mit Lidl-Eigenmarken-Boost (Milbona, Dulano,
Freshona, Solevita, Bon Gelati, Metzgerfrisch, Alesto, Snack Day, Combino,
Baresa, Vemondo, Pilos, Crivit, W5, Cien, Lupilu, Parkside …) + Konfidenz-Score.
Sicherer Treffer → Name/Kategorie/Makros übernehmen, EAN als zweiten Barcode
anlegen. Unsicher → Produkt nur mit Bon-Name anlegen (ohne Makros), Dennis
scannt später die echte EAN per Grocy-App → Nachanreicherung (eigener
enrich-Lauf, der Produkte mit echter EAN aber ohne Makros per OFF auffüllt).
3. Für historische Bons: Modus „sofort verbrauchen" (Bestand nicht dauerhaft erhöhen,
aber Preis-/Kaufhistorie behalten).
4. Idempotenz: bereits importierte Bon-IDs merken (z. B. Grocy-Userfield am „Einkauf"
oder lokale State-Datei), damit erneute Läufe nicht doppelt buchen.
5. Als Cron/systemd-Timer auf dem VPS täglich laufen lassen.
9. Phase 5 – HelloFresh (Recherche-Notiz)
Recherche abgeschlossen (05.06.2026) → siehe RECHERCHE-HELLOFRESH.md. Hybrid-Weg:
Delivery-Liste über interne /gw/-API (manuelles JWT), Rezept-Inhalte (Zutaten,
Mengen, Nährwerte) von den öffentlichen Rezeptseiten (__NEXT_DATA__-Blob, kein
Token nötig). Grocy-Ziel: POST /api/objects/recipes + recipes_pos.
Fragilster Teil: JWT-Ablauf. Unabhängig von der Lidl-Strecke.
10. Was NICHT mit übergeht (Cowork-spezifisch)
In der Cowork-Session wurde ein geplanter Task „lidl-plus-daily-coupons" (täglich 07:01) angelegt, der Coupons über den Browser aktiviert. Das ist Cowork-spezifisch und läuft in Claude Code nicht. Wenn Coupon-Automatisierung gewünscht ist, in Phase 2+ über die v1-Coupon-API (Abschnitt 5.2) nachbauen.
11. Offene To-dos für Claude Code (Reihenfolge)
- Repo anlegen, Dateien einsortieren,
.gitignorefür.env. - Phase 1: Grocy deployen (RUNBOOK.md), Makro-Userfields + API-Key anlegen.
- Phase 2 live:
python lidl.py login→ Token;python lidl.py fetch --all→ echten Bon mitcodeInputverifizieren. Bei Fehlern an der Server-Antwort debuggen. - Phase 3:
sync.pybauen (Abschnitt 8), an wenigen Bons testen, Kategorien-Mapping mit Dennis abstimmen. - Phase 4: alle Bons rückwirkend importieren (idempotent, „sofort verbrauchen").
- Phase 5: HelloFresh recherchieren, dann integrieren.