Simplify shopping list: drop Keep, fast reset, HTML output for HA iframe

This commit is contained in:
2026-04-22 17:56:49 +02:00
parent 2c3e5bb540
commit 7b7dc22245
4 changed files with 280 additions and 467 deletions
+56 -82
View File
@@ -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'<tr><th colspan="2" class="cat">{category.title()}</th></tr>\n'
for item in grouped[category]:
sources = '/'.join(item['sources'])
lines.append(f"- [ ] {item['name']} ({sources})")
lines.append('')
rows_html += f'<tr><td class="cb"><input type="checkbox"></td><td>{item["name"]}</td></tr>\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"""<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Bilka ToGo</title>
<style>
body {{ font-family: sans-serif; padding: 8px; background: #1c1c1e; color: #e5e5ea; margin: 0; }}
h1 {{ font-size: 1.1em; margin-bottom: 2px; color: #fff; }}
p.sub {{ font-size: 0.8em; color: #8e8e93; margin: 0 0 10px; }}
table {{ width: 100%; border-collapse: collapse; }}
td, th {{ padding: 5px 6px; text-align: left; border-bottom: 1px solid #2c2c2e; }}
th.cat {{ background: #2c2c2e; color: #ffe066; font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.05em; }}
td.cb {{ width: 28px; }}
input[type=checkbox] {{ width: 18px; height: 18px; accent-color: #30d158; cursor: pointer; }}
tr:hover td {{ background: #2c2c2e; }}
</style>
</head>
<body>
<h1>🛒 Bilka ToGo</h1>
<p class="sub">Plan {start_date.strftime('%d/%m')} {end_date.strftime('%d/%m')} &nbsp;·&nbsp; {len(items)} varer</p>
<table>
{rows_html}</table>
</body>
</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)}'
)