Simplify shopping list: drop Keep, fast reset, HTML output for HA iframe
This commit is contained in:
@@ -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')} · {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)}'
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user