198 lines
6.4 KiB
Python
198 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Merge Mealie shopping items with a local Google Keep base list.
|
|
|
|
Output:
|
|
- /www/bilka_togo_checklist.md
|
|
- /www/bilka_togo_checklist_bilka.md
|
|
- /www/bilka_togo_checklist.json
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import urllib.request
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT_CANDIDATES = [Path('/Volumes/homeassistant'), Path('/config')]
|
|
CATEGORY_RULES = {
|
|
'frugt & grønt': ['banan', 'aeble', 'æble', 'citron', 'lime', 'tomat', 'salat', 'agurk', 'gulerod', 'kartoffel', 'log', 'løg', 'hvidlog', 'hvidløg', 'broccoli', 'spidskal', 'spidskål', 'avocado', 'peberfrugt'],
|
|
'kød & fisk': ['kylling', 'oksekød', 'hakket', 'ribeye', 'bacon', 'laks', 'fisk', 'skinke', 'pølse'],
|
|
'mejeri & æg': ['mælk', 'yoghurt', 'smør', 'ost', 'feta', 'mozzarella', 'æg', 'fløde', 'creme fraiche'],
|
|
'kolonial': ['ris', 'pasta', 'mel', 'havregryn', 'olie', 'eddike', 'krydder', 'bønne', 'linse', 'dåse', 'sukker', 'salt', 'tomatpuré', 'tomatpure', 'nori', 'soja'],
|
|
'frost': ['frost', 'frossen', 'is', 'edamame'],
|
|
'husholdning': ['toiletpapir', 'køkkenrulle', 'kokkenrulle', 'sæbe', 'sæb', 'opvasketabs', 'vaskemiddel', 'affaldspose'],
|
|
}
|
|
|
|
|
|
def detect_root() -> Path:
|
|
# Prefer repository-relative root so the script works both in HA (/config)
|
|
# and when run locally from the mounted workspace (/Volumes/homeassistant).
|
|
local_root = Path(__file__).resolve().parent.parent
|
|
if (local_root / 'secrets.yaml').exists():
|
|
return local_root
|
|
for root in ROOT_CANDIDATES:
|
|
if (root / 'secrets.yaml').exists():
|
|
return root
|
|
raise RuntimeError('Could not locate Home Assistant config root')
|
|
|
|
|
|
def read_bearer_token(secrets_path: Path) -> str:
|
|
for line in secrets_path.read_text().splitlines():
|
|
if line.strip().startswith('mealie_bearer_token:'):
|
|
return line.split(':', 1)[1].strip().strip('"')
|
|
raise RuntimeError('mealie_bearer_token not found in secrets.yaml')
|
|
|
|
|
|
def api_get(url: str, token: str) -> dict:
|
|
req = urllib.request.Request(url, headers={'Authorization': token})
|
|
with urllib.request.urlopen(req, timeout=20) as resp:
|
|
raw = resp.read()
|
|
return json.loads(raw) if raw else {}
|
|
|
|
|
|
def normalize_name(value: str) -> str:
|
|
value = value.lower().strip()
|
|
value = value.replace('å', 'aa').replace('æ', 'ae').replace('ø', 'oe')
|
|
value = re.sub(r'\s+', ' ', value)
|
|
value = re.sub(r'[^a-z0-9 ]', '', value)
|
|
return value
|
|
|
|
|
|
def classify_category(name: str) -> str:
|
|
key = normalize_name(name)
|
|
for category, needles in CATEGORY_RULES.items():
|
|
for needle in needles:
|
|
if needle in key:
|
|
return category
|
|
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 fetch_mealie_items(base_url: str, token: str) -> list[dict]:
|
|
data = api_get(f'{base_url}/api/households/shopping/items?perPage=500', token)
|
|
return data.get('items', []) or []
|
|
|
|
|
|
def extract_item_name(item: dict) -> str:
|
|
display = (item.get('display') or '').strip()
|
|
if display:
|
|
return display
|
|
food = item.get('food') or {}
|
|
return (food.get('name') or '').strip()
|
|
|
|
|
|
def merge_items(mealie_items: list[dict], keep_items: list[str]) -> list[dict]:
|
|
merged: dict[str, dict] = {}
|
|
|
|
for item in mealie_items:
|
|
name = extract_item_name(item)
|
|
if not name:
|
|
continue
|
|
norm = normalize_name(name)
|
|
if norm not in merged:
|
|
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.sort(key=lambda x: (x['category'], normalize_name(x['name'])))
|
|
return result
|
|
|
|
|
|
def write_outputs(root: Path, items: list[dict]) -> None:
|
|
www = root / 'www'
|
|
www.mkdir(parents=True, exist_ok=True)
|
|
|
|
payload = {'count': len(items), 'items': items}
|
|
(www / 'bilka_togo_checklist.json').write_text(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
|
|
grouped: dict[str, list[dict]] = defaultdict(list)
|
|
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.',
|
|
'',
|
|
]
|
|
|
|
for category in sorted(grouped.keys()):
|
|
lines.append(f'## {category.title()}')
|
|
for item in grouped[category]:
|
|
sources = '/'.join(item['sources'])
|
|
lines.append(f"- [ ] {item['name']} ({sources})")
|
|
lines.append('')
|
|
|
|
(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.',
|
|
'',
|
|
]
|
|
|
|
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))
|
|
|
|
|
|
def main() -> None:
|
|
root = detect_root()
|
|
token = read_bearer_token(root / 'secrets.yaml')
|
|
keep_path = root / 'dokumenter' / 'google_keep_indkoeb.txt'
|
|
|
|
mealie_items = fetch_mealie_items('http://10.0.0.142:9925', token)
|
|
keep_items = read_keep_items(keep_path)
|
|
|
|
merged = merge_items(mealie_items, keep_items)
|
|
write_outputs(root, merged)
|
|
|
|
print(f'OK: merged {len(merged)} items')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|