""" LGCC Telegram Push v1.0 — Fase B PVA "Proactieve Agendabesturing" ================================================================== Content-generator voor de dagelijkse Telegram-push bij agenda-signalen. De daadwerkelijke push regelt de OpenClaw-cron (Kas); dit script print alleen de content naar stdout: TELEGRAM: → versturen toegestaan SKIP → niets versturen (stiltevenster of geen signalen of verouderd) Stiltevenster (vastgesteld): pushes nooit vóór 08:00 Europe/Amsterdam; verstuurvenster 08:00–21:30 Europe/Amsterdam (half 9 's avonds = laatste moment). Buiten dat venster, zonder signalen, of bij een verouderd alerts-bestand (andere datum dan vandaag) → SKIP. Gebruik (cron): python3 telegram_push.py [pad/naar/alerts-vandaag.json] (default: data/alerts-vandaag.json naast dit script, of $ALERTS_JSON) """ from datetime import datetime, time from zoneinfo import ZoneInfo import json import os import sys TZ_AMSTERDAM = ZoneInfo('Europe/Amsterdam') VENSTER_START = time(8, 0) # nooit pushes vóór 08:00 VENSTER_EIND = time(21, 30) # laatste verstuurmoment 21:29:59 MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.environ.get('ALERTS_DATA_DIR') or os.path.join(MODULE_DIR, 'data') DEFAULT_ALERTS_PAD = os.environ.get('ALERTS_JSON') or os.path.join(DATA_DIR, 'alerts-vandaag.json') MAX_REGELS = 6 # compact NL-bericht, max 6 regels def nu_amsterdam(): """Huidige tijd in Europe/Amsterdam (aparte functie → mockbaar in tests).""" return datetime.now(TZ_AMSTERDAM) def binnen_verstuurvenster(nu): """True als de tijd binnen 08:00–21:30 Europe/Amsterdam valt (08:00 inclusief, 21:30 exclusief: om 21:30:00 wordt niet meer verstuurd).""" t = nu.time() return VENSTER_START <= t < VENSTER_EIND def laad_alerts(pad=None): """Leest alerts-vandaag.json. Retourneert dict of None bij fouten.""" with open(pad or DEFAULT_ALERTS_PAD, encoding='utf-8') as f: return json.load(f) def bouw_bericht(datum_iso, alerts, max_regels=MAX_REGELS): """ Bouwt het compacte NL-bericht (max `max_regels` regels): regel 1 : header met datum + aantal signalen regels 2..n : één regel per signaal (maximaal), dringendste eerst laatste : evt. "(+n meer · zie dashboard)" Sortering: T-2 → T-7 → T-14 → T-20, daarna op datum. """ volgorde = {'T-2': 0, 'T-7': 1, 'T-14': 2, 'T-20': 3} gesorteerd = sorted(alerts, key=lambda a: (volgorde.get(a.get('signaaltype'), 9), a.get('datum', ''))) d = datetime.strptime(datum_iso, '%Y-%m-%d') regels = [f"🔔 LGCC-agenda {d.strftime('%d-%m')}: {len(gesorteerd)} signaal(len)"] budget = max_regels - 1 # header is regel 1 getoond = gesorteerd[:budget] for a in getoond: dag = datetime.strptime(a['datum'], '%Y-%m-%d').strftime('%d-%m') regels.append(f"• {a['signaaltype']} · {a['titel']} → {dag}") resterend = len(gesorteerd) - len(getoond) if resterend > 0: if len(regels) < max_regels: regels.append(f"(+{resterend} meer · zie dashboard)") else: # geen regel meer vrij: verwerk het in de laatste signaalregel regels[-1] = f"(+{resterend} meer signaal(len) · zie dashboard)" return '\n'.join(regels) def genereer_output(alerts_data, nu): """ Bepaalt de stdout-regel voor de cron: 'SKIP' → niet versturen 'TELEGRAM:' → content voor de push Voorwaarden om te versturen (alle drie verplicht): 1. er zijn vandaag signalen (alerts niet-leeg) 2. alerts-bestand is van vandaag (datum == nu.date()) 3. nu binnen verstuurvenster 08:00–21:30 Europe/Amsterdam """ if not isinstance(alerts_data, dict): return 'SKIP' alerts = alerts_data.get('alerts') or [] if not alerts: return 'SKIP' if alerts_data.get('datum') != nu.date().isoformat(): return 'SKIP' # verouderd bestand (gisteren) → niet pushen if not binnen_verstuurvenster(nu): return 'SKIP' # stiltevenster return 'TELEGRAM:' + bouw_bericht(alerts_data['datum'], alerts) def main(pad=None, nu=None): alerts_data = None try: alerts_data = laad_alerts(pad) except (FileNotFoundError, json.JSONDecodeError): print('SKIP') # geen/geen geldig alerts-bestand → niets pushen return print(genereer_output(alerts_data, nu if nu is not None else nu_amsterdam())) if __name__ == '__main__': main(sys.argv[1] if len(sys.argv) > 1 else None)