From 7b7dc222453b464b0de4c48a70345d0cd9ab616a Mon Sep 17 00:00:00 2001 From: Claus Dethlefsen Date: Wed, 22 Apr 2026 17:56:49 +0200 Subject: [PATCH] Simplify shopping list: drop Keep, fast reset, HTML output for HA iframe --- dashboards/views/04c_madplan.yaml | 4 +- python_scripts/mealie_shopping_merge.py | 138 +++---- www/bilka_togo_checklist.html | 113 ++++++ www/bilka_togo_checklist.json | 492 ++++++------------------ 4 files changed, 280 insertions(+), 467 deletions(-) create mode 100644 www/bilka_togo_checklist.html diff --git a/dashboards/views/04c_madplan.yaml b/dashboards/views/04c_madplan.yaml index a2a92c6..8775969 100644 --- a/dashboards/views/04c_madplan.yaml +++ b/dashboards/views/04c_madplan.yaml @@ -137,7 +137,7 @@ cards: - type: markdown content: | ## Bilka ToGo - kryds-af - Tryk på knappen for at flette Mealie-indkøb med jeres Google Keep-basisliste. + Tryk på knappen for at hente ingredienser fra ugeplanen (fredag–torsdag). - type: button name: Opdater Bilka ToGo-liste nu @@ -147,5 +147,5 @@ cards: service: script.mealie_shopping_refresh - type: iframe - url: /local/bilka_togo_checklist_bilka.md + url: /local/bilka_togo_checklist.html aspect_ratio: 100% diff --git a/python_scripts/mealie_shopping_merge.py b/python_scripts/mealie_shopping_merge.py index a61a8c6..d898c9f 100644 --- a/python_scripts/mealie_shopping_merge.py +++ b/python_scripts/mealie_shopping_merge.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 -"""Build and merge shopping list for Bilka ToGo. +"""Build Mealie shopping list for the Friday-Thursday meal plan window. Flow: -1) Build Mealie shopping list from meal plan entries in the Friday-Thursday window. -2) Merge those Mealie shopping items with a local Google Keep base list. +1) Compute next Friday-Thursday window. +2) Ensure/clear the 'Bilka ToGo' Mealie shopping list. +3) Add all recipes from the meal plan window to the list. +4) Fetch the resulting shopping items and write a styled HTML file for display in HA. Output: -- /www/bilka_togo_checklist.md -- /www/bilka_togo_checklist_bilka.md -- /www/bilka_togo_checklist.json +- /www/bilka_togo_checklist.html (iframe-renderable in HA) +- /www/bilka_togo_checklist.json (machine-readable backup) """ from __future__ import annotations @@ -101,16 +102,8 @@ def classify_category(name: str) -> str: return 'andet' -def read_keep_items(keep_path: Path) -> list[str]: - if not keep_path.exists(): - return [] - items: list[str] = [] - for line in keep_path.read_text().splitlines(): - stripped = line.strip() - if not stripped or stripped.startswith('#'): - continue - items.append(stripped) - return items +def read_keep_items(_keep_path: Path) -> list[str]: # noqa: ARG001 (kept for backward compat) + return [] def friday_to_thursday_window(today: date) -> tuple[date, date]: @@ -136,11 +129,17 @@ def ensure_shopping_list(base_url: str, token: str, name: str) -> str: return created['id'] -def clear_shopping_list_items(base_url: str, token: str, shopping_list_id: str) -> None: - items = api_request(base_url, '/api/households/shopping/items?perPage=1000', token).get('items', []) or [] - for item in items: - if item.get('shoppingListId') == shopping_list_id and item.get('id'): - api_request(base_url, f"/api/households/shopping/items/{item['id']}", token, method='DELETE') +def reset_shopping_list(base_url: str, token: str, name: str, shopping_list_id: str) -> str: + """Delete the list and recreate it. Returns new list id.""" + api_request(base_url, f'/api/households/shopping/lists/{shopping_list_id}', token, method='DELETE') + created = api_request( + base_url, + '/api/households/shopping/lists', + token, + method='POST', + payload={'name': name}, + ) + return created['id'] def get_mealplan_recipe_ids(base_url: str, token: str, start_date: date, end_date: date) -> list[str]: @@ -200,9 +199,8 @@ def extract_item_name(item: dict) -> str: return (food.get('name') or '').strip() -def merge_items(mealie_items: list[dict], keep_items: list[str]) -> list[dict]: +def build_items(mealie_items: list[dict]) -> list[dict]: merged: dict[str, dict] = {} - for item in mealie_items: name = extract_item_name(item) if not name: @@ -212,30 +210,9 @@ def merge_items(mealie_items: list[dict], keep_items: list[str]) -> list[dict]: merged[norm] = { 'name': name, 'category': classify_category(name), - 'sources': set(), } - merged[norm]['sources'].add('Mealie') - - for name in keep_items: - norm = normalize_name(name) - if norm not in merged: - merged[norm] = { - 'name': name, - 'category': classify_category(name), - 'sources': set(), - } - merged[norm]['sources'].add('Google Keep') - - result = [] - for _, row in merged.items(): - result.append( - { - 'name': row['name'], - 'category': row['category'], - 'sources': sorted(row['sources']), - } - ) + result = [v for v in merged.values()] result.sort(key=lambda x: (x['category'], normalize_name(x['name']))) return result @@ -251,64 +228,61 @@ def write_outputs(root: Path, items: list[dict], start_date: date, end_date: dat for item in items: grouped[item['category']].append(item) - lines = [ - '# Bilka ToGo - Kryds-af-liste', - '', - 'Gå listen igennem derhjemme først, og bestil kun de varer du mangler.', - f'Plan-vindue: {start_date.isoformat()} til {end_date.isoformat()}', - '', - ] - + rows_html = '' for category in sorted(grouped.keys()): - lines.append(f'## {category.title()}') + rows_html += f'{category.title()}\n' for item in grouped[category]: - sources = '/'.join(item['sources']) - lines.append(f"- [ ] {item['name']} ({sources})") - lines.append('') + rows_html += f'{item["name"]}\n' - (www / 'bilka_togo_checklist.md').write_text('\n'.join(lines)) - - # Bilka-ready checklist: same grouped list, but without source metadata. - bilka_lines = [ - '# Bilka ToGo - Klar til bestilling', - '', - 'Kryds af hvad I allerede har i huset, og bestil resten.', - f'Plan-vindue: {start_date.isoformat()} til {end_date.isoformat()}', - '', - ] - - for category in sorted(grouped.keys()): - bilka_lines.append(f'## {category.title()}') - for item in grouped[category]: - bilka_lines.append(f"- [ ] {item['name']}") - bilka_lines.append('') - - (www / 'bilka_togo_checklist_bilka.md').write_text('\n'.join(bilka_lines)) + html = f""" + + + + +Bilka ToGo + + + +

🛒 Bilka ToGo

+

Plan {start_date.strftime('%d/%m')} – {end_date.strftime('%d/%m')}  ·  {len(items)} varer

+ +{rows_html}
+ + +""" + (www / 'bilka_togo_checklist.html').write_text(html) def main() -> None: root = detect_root() token = read_bearer_token(root / 'secrets.yaml') - keep_path = root / 'dokumenter' / 'google_keep_indkoeb.txt' start_date, end_date = friday_to_thursday_window(date.today()) shopping_list_id = ensure_shopping_list(MEALIE_BASE_URL, token, TARGET_SHOPPING_LIST_NAME) - clear_shopping_list_items(MEALIE_BASE_URL, token, shopping_list_id) + shopping_list_id = reset_shopping_list(MEALIE_BASE_URL, token, TARGET_SHOPPING_LIST_NAME, shopping_list_id) recipe_ids = get_mealplan_recipe_ids(MEALIE_BASE_URL, token, start_date, end_date) added_recipes = add_recipes_to_shopping_list(MEALIE_BASE_URL, token, shopping_list_id, recipe_ids) mealie_items = fetch_mealie_items(MEALIE_BASE_URL, token, shopping_list_id=shopping_list_id) - keep_items = read_keep_items(keep_path) - - merged = merge_items(mealie_items, keep_items) - write_outputs(root, merged, start_date, end_date) + items = build_items(mealie_items) + write_outputs(root, items, start_date, end_date) print( 'OK: ' f'window={start_date.isoformat()}..{end_date.isoformat()} ' - f'recipes_added={added_recipes} merged_items={len(merged)}' + f'recipes_added={added_recipes} items={len(items)}' ) diff --git a/www/bilka_togo_checklist.html b/www/bilka_togo_checklist.html new file mode 100644 index 0000000..f33baa0 --- /dev/null +++ b/www/bilka_togo_checklist.html @@ -0,0 +1,113 @@ + + + + + +Bilka ToGo + + + +

🛒 Bilka ToGo

+

Plan 24/04 – 30/04  ·  82 varer

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Andet
0,50 squash
0,50 tsk mediumstærk karry
0,50 tsk tørret rosmarin
1 dl cremefraiche 38%
1 dl grøntsagsbouillon
1 dl pickles
1 dl rødvin, eller grøntsagsboullion
1 dl tør hvidvin
1 fed hvidløg, presset
1 gulerødder, i tern
1 håndfuld persille
1 kg kartofler
1 knivspids muskatnød, fintrevet
1 knivspids sød paprika
1 løg, i tern
1 rødløg
1 spsk dijon sennep
1 spsk hampefrø
1 spsk honning
1 spsk majsstivelse
1 spsk smør, til stegning
1 spsk solsikkekerner
1 squash, groftrevet
1 tsk tørret timian
10 g smør, til stegning
100 g parmesan, fintrevet
12 tarteletter
2 æg
2 dl hønsebouillon
2 hønsebryst
2 laurbærblade
2 spsk grov sennep
2 spsk rosiner
2 spsk smør
2 tsk tørret oregano
200 g aspargessnitter
200 g lasagneplader
25 g smør, til stegning
3 dl grøntsagsbouillon
3 dl mælk
3 gulerødder, groftrevet
300 g torskefilet
4 dl mælk
4 fed hvidløg, fintrevet
4 gulerødder, groftrevet
40 g smør
5 stængler bladselleri, groftrevet
Frost
1 dl piskefløde
2 spsk mayonnaise
2,50 dl piskefløde
Frugt & Grønt
0,50 citron, saft herfra
0,50 øko citron
15 g koncentreret tomatpuré
2 æble, groftrevet
50 g koncentreret tomatpuré
75 g soltørrede tomater i olie, finthakket
800 g hakkede tomater på dåse
Kolonial
1 håndfuld frisk basilikum
1 håndfuld frisk dild
1 spsk olivenolie
1 spsk olivenolie, til stegning
1 tsk olivenolie
2 spsk hvedemel
2 spsk olivenolie
25 g hvedemel
3 dl basmati ris, kogt efter anvisning på emballagen
30 g hvedemel
40 g hvedemel
400 g pasta
flagesalt
salt og friskkværnet peber
Kød & Fisk
0,50 dl frisk estragon, finthakket
1 løg, finthakket
2 løg, finthakket
300 g laks, uden skind
4 fed hvidløg, finthakket
4 kyllingebryst
400 g hakket oksekød
600 g kyllingebryst
75 g bacon, i skiver
Mejeri & Æg
125 g frisk mozzarella
400 g haricots verts, fra frost
+ + diff --git a/www/bilka_togo_checklist.json b/www/bilka_togo_checklist.json index 56bd606..3ece9f8 100644 --- a/www/bilka_togo_checklist.json +++ b/www/bilka_togo_checklist.json @@ -1,607 +1,333 @@ { - "count": 86, + "count": 82, "items": [ { "name": "0,50 squash", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "0,50 tsk mediumstærk karry", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "0,50 tsk tørret rosmarin", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 dl cremefraiche 38%", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 dl grøntsagsbouillon", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 dl pickles", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 dl rødvin, eller grøntsagsboullion", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 dl tør hvidvin", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 fed hvidløg, presset", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { - "name": "1 gulerødder, Groftrevet", - "category": "andet", - "sources": [ - "Mealie" - ] + "name": "1 gulerødder, i tern", + "category": "andet" + }, + { + "name": "1 håndfuld persille", + "category": "andet" }, { "name": "1 kg kartofler", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 knivspids muskatnød, fintrevet", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 knivspids sød paprika", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" + }, + { + "name": "1 løg, i tern", + "category": "andet" }, { "name": "1 rødløg", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 spsk dijon sennep", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 spsk hampefrø", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 spsk honning", - "category": "andet", - "sources": [ - "Mealie" - ] - }, - { - "name": "1 spsk ingefær, fintrevet", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 spsk majsstivelse", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 spsk smør, til stegning", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 spsk solsikkekerner", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 squash, groftrevet", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 tsk tørret timian", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "10 g smør, til stegning", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "100 g parmesan, fintrevet", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" + }, + { + "name": "12 tarteletter", + "category": "andet" }, { "name": "2 æg", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "2 dl hønsebouillon", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" + }, + { + "name": "2 hønsebryst", + "category": "andet" + }, + { + "name": "2 laurbærblade", + "category": "andet" }, { "name": "2 spsk grov sennep", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "2 spsk rosiner", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "2 spsk smør", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "2 tsk tørret oregano", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" + }, + { + "name": "200 g aspargessnitter", + "category": "andet" }, { "name": "200 g lasagneplader", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "25 g smør, til stegning", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" + }, + { + "name": "3 dl grøntsagsbouillon", + "category": "andet" }, { "name": "3 dl mælk", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "3 gulerødder, groftrevet", - "category": "andet", - "sources": [ - "Mealie" - ] - }, - { - "name": "30 forårsrulleplader", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "300 g torskefilet", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { - "name": "35 g glasnudler", - "category": "andet", - "sources": [ - "Mealie" - ] + "name": "4 dl mælk", + "category": "andet" }, { "name": "4 fed hvidløg, fintrevet", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "4 gulerødder, groftrevet", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" + }, + { + "name": "40 g smør", + "category": "andet" }, { "name": "5 stængler bladselleri, groftrevet", - "category": "andet", - "sources": [ - "Mealie" - ] - }, - { - "name": "vand til pensling", - "category": "andet", - "sources": [ - "Mealie" - ] + "category": "andet" }, { "name": "1 dl piskefløde", - "category": "frost", - "sources": [ - "Mealie" - ] - }, - { - "name": "1 spsk fishsauce", - "category": "frost", - "sources": [ - "Mealie" - ] + "category": "frost" }, { "name": "2 spsk mayonnaise", - "category": "frost", - "sources": [ - "Mealie" - ] + "category": "frost" }, { "name": "2,50 dl piskefløde", - "category": "frost", - "sources": [ - "Mealie" - ] + "category": "frost" }, { "name": "0,50 citron, saft herfra", - "category": "frugt & grønt", - "sources": [ - "Mealie" - ] + "category": "frugt & grønt" }, { "name": "0,50 øko citron", - "category": "frugt & grønt", - "sources": [ - "Mealie" - ] + "category": "frugt & grønt" }, { "name": "15 g koncentreret tomatpuré", - "category": "frugt & grønt", - "sources": [ - "Mealie" - ] + "category": "frugt & grønt" }, { "name": "2 æble, groftrevet", - "category": "frugt & grønt", - "sources": [ - "Mealie" - ] + "category": "frugt & grønt" }, { "name": "50 g koncentreret tomatpuré", - "category": "frugt & grønt", - "sources": [ - "Mealie" - ] + "category": "frugt & grønt" }, { "name": "75 g soltørrede tomater i olie, finthakket", - "category": "frugt & grønt", - "sources": [ - "Mealie" - ] + "category": "frugt & grønt" }, { "name": "800 g hakkede tomater på dåse", - "category": "frugt & grønt", - "sources": [ - "Mealie" - ] + "category": "frugt & grønt" }, { "name": "1 håndfuld frisk basilikum", - "category": "kolonial", - "sources": [ - "Mealie" - ] + "category": "kolonial" }, { "name": "1 håndfuld frisk dild", - "category": "kolonial", - "sources": [ - "Mealie" - ] - }, - { - "name": "1 liter fritureolie", - "category": "kolonial", - "sources": [ - "Mealie" - ] + "category": "kolonial" }, { "name": "1 spsk olivenolie", - "category": "kolonial", - "sources": [ - "Mealie" - ] + "category": "kolonial" }, { "name": "1 spsk olivenolie, til stegning", - "category": "kolonial", - "sources": [ - "Mealie" - ] - }, - { - "name": "1 spsk soja", - "category": "kolonial", - "sources": [ - "Mealie" - ] + "category": "kolonial" }, { "name": "1 tsk olivenolie", - "category": "kolonial", - "sources": [ - "Mealie" - ] - }, - { - "name": "1 tsk sesamolie, eller anden olie til stegning", - "category": "kolonial", - "sources": [ - "Mealie" - ] - }, - { - "name": "1 tsk sukker", - "category": "kolonial", - "sources": [ - "Mealie" - ] + "category": "kolonial" }, { "name": "2 spsk hvedemel", - "category": "kolonial", - "sources": [ - "Mealie" - ] + "category": "kolonial" }, { "name": "2 spsk olivenolie", - "category": "kolonial", - "sources": [ - "Mealie" - ] + "category": "kolonial" }, { "name": "25 g hvedemel", - "category": "kolonial", - "sources": [ - "Mealie" - ] + "category": "kolonial" }, { "name": "3 dl basmati ris, kogt efter anvisning på emballagen", - "category": "kolonial", - "sources": [ - "Mealie" - ] + "category": "kolonial" }, { "name": "30 g hvedemel", - "category": "kolonial", - "sources": [ - "Mealie" - ] + "category": "kolonial" }, { "name": "40 g hvedemel", - "category": "kolonial", - "sources": [ - "Mealie" - ] + "category": "kolonial" }, { "name": "400 g pasta", - "category": "kolonial", - "sources": [ - "Mealie" - ] + "category": "kolonial" }, { "name": "flagesalt", - "category": "kolonial", - "sources": [ - "Mealie" - ] + "category": "kolonial" }, { "name": "salt og friskkværnet peber", - "category": "kolonial", - "sources": [ - "Mealie" - ] + "category": "kolonial" }, { "name": "0,50 dl frisk estragon, finthakket", - "category": "kød & fisk", - "sources": [ - "Mealie" - ] + "category": "kød & fisk" }, { "name": "1 løg, finthakket", - "category": "kød & fisk", - "sources": [ - "Mealie" - ] - }, - { - "name": "1 tsk rød chili, finthakket", - "category": "kød & fisk", - "sources": [ - "Mealie" - ] + "category": "kød & fisk" }, { "name": "2 løg, finthakket", - "category": "kød & fisk", - "sources": [ - "Mealie" - ] - }, - { - "name": "250 g champignon, finthakket", - "category": "kød & fisk", - "sources": [ - "Mealie" - ] - }, - { - "name": "3 fed hvidløg, finthakket", - "category": "kød & fisk", - "sources": [ - "Mealie" - ] + "category": "kød & fisk" }, { "name": "300 g laks, uden skind", - "category": "kød & fisk", - "sources": [ - "Mealie" - ] + "category": "kød & fisk" }, { "name": "4 fed hvidløg, finthakket", - "category": "kød & fisk", - "sources": [ - "Mealie" - ] + "category": "kød & fisk" }, { "name": "4 kyllingebryst", - "category": "kød & fisk", - "sources": [ - "Mealie" - ] + "category": "kød & fisk" }, { "name": "400 g hakket oksekød", - "category": "kød & fisk", - "sources": [ - "Mealie" - ] - }, - { - "name": "400 g hakket svinekød", - "category": "kød & fisk", - "sources": [ - "Mealie" - ] + "category": "kød & fisk" }, { "name": "600 g kyllingebryst", - "category": "kød & fisk", - "sources": [ - "Mealie" - ] + "category": "kød & fisk" }, { "name": "75 g bacon, i skiver", - "category": "kød & fisk", - "sources": [ - "Mealie" - ] + "category": "kød & fisk" }, { "name": "125 g frisk mozzarella", - "category": "mejeri & æg", - "sources": [ - "Mealie" - ] + "category": "mejeri & æg" }, { "name": "400 g haricots verts, fra frost", - "category": "mejeri & æg", - "sources": [ - "Mealie" - ] + "category": "mejeri & æg" } ] } \ No newline at end of file