Initial commit: Waste Collection API

This commit is contained in:
2026-04-05 07:29:36 +00:00
commit 109b682a88
17 changed files with 1016 additions and 0 deletions

0
app/core/__init__.py Normal file
View File

42
app/core/notifier.py Normal file
View File

@@ -0,0 +1,42 @@
"""
Send notification via Telegram.
Requires bot token and chat_id configured in environment or .env file.
"""
import os
import logging
from pathlib import Path
import httpx
from dotenv import load_dotenv
log = logging.getLogger(__name__)
load_dotenv()
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "8638072533:AAG3fcm0Sq_JBSSA1UXLspgRzn-tdIJ9N_8")
TELEGRAM_CHAT_ID = int(os.getenv("TELEGRAM_CHAT_ID", "1320170074"))
def send_telegram(text: str, chat_id: int = TELEGRAM_CHAT_ID) -> bool:
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
try:
resp = httpx.post(url, json={"chat_id": chat_id, "text": text}, timeout=10)
resp.raise_for_status()
log.info(f"Telegram sent to {chat_id}: {text[:50]}")
return True
except Exception as e:
log.error(f"Telegram failed: {e}")
return False
def format_collection_reminder(collection: dict, source_id: str) -> str:
date = collection.get("date", "?")
wtype = collection.get("type", "?")
days = collection.get("daysUntil", "?")
icon = collection.get("icon", "🗑️")
return (
f"{icon} *Waste Collection Reminder*\n"
f"📅 {date} (in {days} day{'s' if days != 1 else ''})\n"
f"🗑️ {wtype}\n"
f"🔔 from: {source_id}"
)

118
app/core/scheduler.py Normal file
View File

@@ -0,0 +1,118 @@
import uuid
from datetime import datetime, timedelta
from pathlib import Path
import json
import logging
log = logging.getLogger(__name__)
NOTIFS_FILE = Path(__file__).resolve().parent.parent.parent / "data" / "notifications.json"
# ─── Source registry ───────────────────────────────────────────
SOURCE_DEFS = {
"ics": {
"name": "Generic ICS / iCal URL",
"params": [
{"name": "url", "type": "string", "required": True, "example": "https://example.com/kalender.ics"},
],
},
"abfall_io": {
"name": "Abfall.IO (Germany)",
"params": [
{"name": "key", "type": "string", "required": True, "example": "8215c62763967916979e0e8566b6172e"},
{"name": "f_id_kommune", "type": "integer", "required": True, "example": 2999},
{"name": "f_id_strasse", "type": "integer", "required": True, "example": 1087},
],
},
"awm_muenchen_de": {
"name": "AWM München (Germany)",
"params": [
{"name": "street", "type": "string", "required": True, "example": "Bahnstr."},
{"name": "house_number", "type": "string", "required": True, "example": "11"},
],
},
"stuttgart_de": {
"name": "Abfall Stuttgart (Germany)",
"params": [
{"name": "street", "type": "string", "required": True, "example": "Im Steinengarten"},
{"name": "streetnr", "type": "integer", "required": True, "example": 7},
],
},
}
class NotificationScheduler:
def __init__(self):
self._notifications: dict[str, dict] = {}
self._load()
def _load(self):
if NOTIFS_FILE.exists():
try:
data = json.loads(NOTIFS_FILE.read_text())
self._notifications = {n["id"]: n for n in data.get("notifications", [])}
log.info(f"Loaded {len(self._notifications)} notifications from {NOTIFS_FILE}")
except Exception as e:
log.warning(f"Failed to load notifications: {e}")
def _save(self):
NOTIFS_FILE.parent.mkdir(parents=True, exist_ok=True)
NOTIFS_FILE.write_text(
json.dumps({"notifications": list(self._notifications.values())}, indent=2)
)
def add(
self,
source_id: str,
collection: dict,
notify_at_days_before: int,
channels: list[str],
chat_id: int,
) -> dict:
nid = f"notif_{uuid.uuid4().hex[:8]}"
entry = {
"id": nid,
"source_id": source_id,
"collection_type": collection.get("type", "Unknown"),
"collection_date": collection.get("date", ""),
"notify_at_days_before": notify_at_days_before,
"channels": channels,
"chat_id": chat_id,
"fired": False,
}
self._notifications[nid] = entry
self._save()
return entry
def list_all(self) -> list[dict]:
return list(self._notifications.values())
def delete(self, nid: str) -> bool:
if nid in self._notifications:
del self._notifications[nid]
self._save()
return True
return False
def due_today(self) -> list[dict]:
today = datetime.now().date().isoformat()
due = []
for n in self._notifications.values():
if n.get("fired"):
continue
coll_date = n.get("collection_date", "")
days_before = n.get("notify_at_days_before", 1)
# check if today is notify_at_days_before days before collection
try:
coll = datetime.fromisoformat(coll_date).date()
notify_date = coll - timedelta(days=days_before)
if notify_date.isoformat() == today:
due.append(n)
except Exception:
pass
return due
def mark_fired(self, nid: str):
if nid in self._notifications:
self._notifications[nid]["fired"] = True
self._save()