| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660 |
- # -*- coding: utf-8 -*-
- """
- Reminders-Plugin fuer Trixy.
- Ermoeglicht das Erstellen, Auflisten und Loeschen von Erinnerungen
- per Sprache. Unterstuetzt Multi-Turn-Dialoge mit Rueckfragen
- (Was? → Wann? → VM/NM?).
- Bei Faelligkeit wird eine TTS-Meldung abgespielt.
- Nutzt den SyncStore fuer persistente Speicherung und
- automatische Synchronisation mit dem Server.
- """
- import asyncio
- from datetime import datetime, timedelta
- from pathlib import Path
- from typing import Any
- from trixy_core.plugins import TrixyPlugin
- from trixy_core.sync.models import SyncItem, _now_iso
- from trixy_core.sync.store import SyncStore
- from trixy_core.nlp.intent_registry import IntentRegistry
- from trixy_core.nlp.decorators import intent, pattern, example
- from trixy_core.nlp.handler import IntentReceivedData, IntentResult
- from trixy_core.utils.debug import pinfo, pdebug, perror, pwarn
- from plugins.reminders.time_parser import parse_german_time, ParsedTime
- DATA_TYPE = "reminders.reminder"
- class RemindersPlugin(TrixyPlugin):
- """
- Erinnerungen-Plugin mit Multi-Turn-Dialog.
- Bietet:
- - Erinnerungen erstellen (mit Rueckfragen: Was? Wann? VM/NM?)
- - Erinnerungen mit allen Details in einem Satz
- - Alle Erinnerungen auflisten
- - Heutige Erinnerungen anzeigen
- - Erinnerungen loeschen (einzeln und alle)
- - Timer-Loop prueft alle 60s auf faellige Erinnerungen
- Nutzt SyncStore fuer Persistenz und Sync.
- """
- NAME = "reminders"
- VERSION = "1.0.0"
- DESCRIPTION = "Erinnerungen per Sprachbefehl erstellen und verwalten"
- AUTHOR = "Trixy"
- def __init__(self, application: Any, plugin_path: Any, config: dict[str, Any] | None = None) -> None:
- super().__init__(application, plugin_path, config)
- self._registry = IntentRegistry.get_instance()
- self._store: SyncStore | None = None
- # Dialog-State fuer Multi-Turn (session_id → state)
- self._dialog_state: dict[str, dict[str, Any]] = {}
- # Pending delete-all Rueckfragen
- self._pending_delete_all: dict[str, bool] = {}
- # Hintergrund-Task fuer Erinnerungs-Pruefung
- self._check_task: asyncio.Task | None = None
- # Notification-Sound (PCM)
- self._notification_pcm: bytes | None = None
- # =========================================================================
- # Lifecycle
- # =========================================================================
- async def on_load(self) -> None:
- """Initialisiert das Plugin."""
- pinfo(f"RemindersPlugin: Lade Plugin v{self.VERSION}")
- # SyncStore holen
- self._store = self.application.sync_store
- if self._store is None:
- self._store = SyncStore(base_directory="./sync_data")
- pdebug("RemindersPlugin: Eigenen SyncStore erstellt (kein globaler verfuegbar)")
- # Notification-Sound vorladen
- self._load_notification_sound()
- # Hintergrund-Task starten fuer faellige Erinnerungen
- self._check_task = asyncio.create_task(self._check_reminders_loop())
- pinfo("RemindersPlugin: Geladen")
- async def on_unload(self) -> None:
- """Entlaedt das Plugin."""
- # Hintergrund-Task stoppen
- if self._check_task and not self._check_task.done():
- self._check_task.cancel()
- try:
- await self._check_task
- except asyncio.CancelledError:
- pass
- self._registry.unregister_plugin(self.NAME)
- self._dialog_state.clear()
- self._pending_delete_all.clear()
- self._notification_pcm = None
- pdebug("RemindersPlugin: Entladen")
- # =========================================================================
- # Notification-Sound
- # =========================================================================
- def _load_notification_sound(self) -> None:
- """Laedt den Notification-Sound und entfernt den WAV-Header."""
- sound_name = self.get_config_value("notification_sound", "notification.wav")
- asset_service = self.application.services.get_service("AssetService")
- if not asset_service:
- return
- audio_path: Path | None = asset_service.get_audio(sound_name)
- if not audio_path or not audio_path.exists():
- return
- try:
- raw = audio_path.read_bytes()
- self._notification_pcm = raw[44:]
- pdebug(f"RemindersPlugin: Notification-Sound geladen ({len(self._notification_pcm)} bytes)")
- except Exception as e:
- perror(f"RemindersPlugin: Notification-Sound Fehler: {e}")
- # =========================================================================
- # Hintergrund-Pruefung faelliger Erinnerungen
- # =========================================================================
- async def _check_reminders_loop(self) -> None:
- """Prueft alle 60 Sekunden ob eine Erinnerung faellig ist."""
- interval = self.get_config_value("check_interval_seconds", 60)
- while True:
- try:
- await asyncio.sleep(interval)
- await self._check_due_reminders()
- except asyncio.CancelledError:
- break
- except Exception as e:
- perror(f"RemindersPlugin: Fehler in Check-Loop: {e}")
- async def _check_due_reminders(self) -> None:
- """Prueft und benachrichtigt faellige Erinnerungen."""
- if not self._store:
- return
- now = datetime.now()
- reminders = self._store.get_all(DATA_TYPE)
- for reminder in reminders:
- if reminder.data.get("notified") or reminder.data.get("completed"):
- continue
- remind_at_str = reminder.data.get("remind_at", "")
- if not remind_at_str:
- continue
- try:
- remind_at = datetime.fromisoformat(remind_at_str)
- # Timezone-naive Vergleich
- if remind_at.tzinfo:
- remind_at = remind_at.replace(tzinfo=None)
- except (ValueError, TypeError):
- continue
- if remind_at <= now:
- await self._trigger_reminder(reminder)
- async def _trigger_reminder(self, reminder: SyncItem) -> None:
- """Benachrichtigt ueber eine faellige Erinnerung."""
- title = reminder.data.get("title", "Erinnerung")
- pinfo(f"RemindersPlugin: Erinnerung faellig: {title}")
- # Als benachrichtigt markieren
- reminder.data["notified"] = True
- reminder.data["completed"] = True
- reminder.updated_at = _now_iso()
- reminder.version += 1
- self._store.save(reminder)
- # TTS-Meldung via Event (funktioniert im Server- und Standalone-Modus)
- text = f"Erinnerung: {title}."
- await self.application.events.emit("tts_request", {
- "request_id": f"reminder-{reminder.item_id}",
- "satellite_id": reminder.source_id or "",
- "text": text,
- })
- # =========================================================================
- # Dialog-State Verwaltung
- # =========================================================================
- def _get_dialog_step(self, session_id: str) -> str:
- """Gibt den aktuellen Dialog-Schritt zurueck."""
- state = self._dialog_state.get(session_id)
- if state:
- return state.get("step", "")
- return ""
- def _set_dialog_step(self, session_id: str, step: str) -> None:
- """Setzt den Dialog-Schritt."""
- if session_id not in self._dialog_state:
- self._dialog_state[session_id] = {}
- self._dialog_state[session_id]["step"] = step
- def _set_dialog_data(self, session_id: str, key: str, value: Any) -> None:
- """Speichert einen Wert im Dialog-State."""
- if session_id not in self._dialog_state:
- self._dialog_state[session_id] = {}
- self._dialog_state[session_id][key] = value
- def _get_dialog_data(self, session_id: str, key: str, default: Any = None) -> Any:
- """Liest einen Wert aus dem Dialog-State."""
- state = self._dialog_state.get(session_id)
- if state:
- return state.get(key, default)
- return default
- def _clear_dialog(self, session_id: str) -> None:
- """Loescht den Dialog-State fuer eine Session."""
- self._dialog_state.pop(session_id, None)
- # =========================================================================
- # Intent-Handler: Erinnerung erstellen (Multi-Turn)
- # =========================================================================
- @intent("create_reminder", description="Erinnerung erstellen")
- @pattern("(erinnere|erinner) [mich] [an] {content}")
- @pattern("erinnerung {content}")
- @pattern("(richte|erstelle|erstell) [eine] erinnerung [ein|fuer] {content}")
- @example("Erinnere mich an Zahnarzt", "Erinnerung Einkaufen")
- @example("Richte eine Erinnerung ein", "Erstelle Erinnerung Muell rausbringen")
- async def handle_create_reminder(self, data: IntentReceivedData) -> IntentResult:
- """Erstellt eine Erinnerung — ggf. mit Rueckfragen."""
- session_id = data.session_id or "unknown"
- step = self._get_dialog_step(session_id)
- # Multi-Turn: Antwort auf "Wofuer?"
- if step == "ask_what":
- title = data.original_text.strip()
- if not title:
- return IntentResult.failure(
- "Kein Inhalt",
- response_text="Ich habe nichts verstanden. Wofuer ist die Erinnerung?",
- )
- self._set_dialog_data(session_id, "title", title)
- self._set_dialog_step(session_id, "ask_when")
- return IntentResult(
- success=True,
- response_text="Wann soll ich dich erinnern?",
- follow_up_intent="create_reminder",
- )
- # Multi-Turn: Antwort auf "Wann?"
- if step == "ask_when":
- time_text = data.original_text.strip()
- return self._handle_when_response(session_id, time_text, data)
- # Multi-Turn: VM/NM Klaerung
- if step == "clarify_ampm":
- return self._handle_ampm_response(session_id, data)
- # Erster Aufruf: Pruefe ob Slots gefuellt
- title = data.get_slot("content") or data.get_slot("title") or ""
- time_slot = data.get_slot("time") or data.get_slot("when") or ""
- if not title:
- # Kein Titel → Rueckfrage
- self._set_dialog_step(session_id, "ask_what")
- self._set_dialog_data(session_id, "satellite_id", data.satellite_id)
- return IntentResult(
- success=True,
- response_text="Wofuer ist die Erinnerung?",
- follow_up_intent="create_reminder",
- )
- if not time_slot:
- # Titel da, aber keine Zeit → Rueckfrage
- self._set_dialog_data(session_id, "title", title)
- self._set_dialog_data(session_id, "satellite_id", data.satellite_id)
- self._set_dialog_step(session_id, "ask_when")
- return IntentResult(
- success=True,
- response_text="Wann soll ich dich erinnern?",
- follow_up_intent="create_reminder",
- )
- # Alles da → direkt erstellen
- return self._try_create_with_time(session_id, title, time_slot, data)
- @intent("create_reminder_full", description="Erinnerung mit vollstaendigen Details")
- @pattern("(erinnere|erinner) [mich] [morgen|uebermorgen|heute] [um] {time} [uhr] [an] {content}")
- @pattern("(erinnere|erinner) [mich] in {time} [an] {content}")
- @example("Erinnere mich morgen um 14 Uhr an Zahnarzt")
- @example("Erinnere mich in 2 Stunden an Kuchen aus dem Ofen")
- async def handle_create_reminder_full(self, data: IntentReceivedData) -> IntentResult:
- """Erstellt eine Erinnerung mit allen Details in einem Satz."""
- title = data.get_slot("content") or data.get_slot("title") or ""
- time_text = data.get_slot("time") or data.get_slot("when") or ""
- if not title or not time_text:
- # Fallback: Gesamttext parsen
- return await self.handle_create_reminder(data)
- session_id = data.session_id or "unknown"
- return self._try_create_with_time(session_id, title, time_text, data)
- def _handle_when_response(self, session_id: str, time_text: str, data: IntentReceivedData) -> IntentResult:
- """Verarbeitet die Antwort auf 'Wann?'."""
- if not time_text:
- return IntentResult.failure(
- "Keine Zeitangabe",
- response_text="Ich habe die Zeit nicht verstanden. Wann soll ich dich erinnern?",
- )
- title = self._get_dialog_data(session_id, "title", "Erinnerung")
- return self._try_create_with_time(session_id, title, time_text, data)
- def _try_create_with_time(self, session_id: str, title: str, time_text: str,
- data: IntentReceivedData) -> IntentResult:
- """Versucht eine Erinnerung mit geparster Zeit zu erstellen."""
- default_time = self.get_config_value("default_time", "09:00")
- parsed = parse_german_time(time_text, default_time=default_time)
- if parsed.error:
- # Zeit nicht erkannt → nochmal fragen
- self._set_dialog_data(session_id, "title", title)
- self._set_dialog_step(session_id, "ask_when")
- return IntentResult(
- success=True,
- response_text=f"Ich habe die Zeitangabe nicht verstanden. Wann genau?",
- follow_up_intent="create_reminder",
- )
- if parsed.ambiguous:
- # VM/NM Rueckfrage
- self._set_dialog_data(session_id, "title", title)
- self._set_dialog_data(session_id, "time_text", time_text)
- self._set_dialog_data(session_id, "option_a", parsed.option_a)
- self._set_dialog_data(session_id, "option_b", parsed.option_b)
- self._set_dialog_step(session_id, "clarify_ampm")
- return IntentResult(
- success=True,
- response_text=f"Meinst du {parsed.option_a} oder {parsed.option_b}?",
- follow_up_intent="create_reminder",
- )
- # Eindeutig → Erinnerung erstellen
- return self._finalize_reminder(session_id, title, parsed.dt, data)
- def _handle_ampm_response(self, session_id: str, data: IntentReceivedData) -> IntentResult:
- """Verarbeitet die VM/NM Antwort."""
- text = data.original_text.lower().strip()
- title = self._get_dialog_data(session_id, "title", "Erinnerung")
- time_text = self._get_dialog_data(session_id, "time_text", "")
- option_a = self._get_dialog_data(session_id, "option_a", "")
- option_b = self._get_dialog_data(session_id, "option_b", "")
- # Versuche aus der Antwort die Praeferenz zu erkennen
- use_pm = False
- if any(w in text for w in ("nachmittag", "abend", "pm", "nachm", option_b.lower() if option_b else "")):
- use_pm = True
- elif any(w in text for w in ("morgen", "vormittag", "frueh", "früh", "am", "vorm", option_a.lower() if option_a else "")):
- use_pm = False
- else:
- # Versuche Zahl zu finden (z.B. "14 Uhr")
- import re
- match = re.search(r"(\d{1,2})", text)
- if match:
- hour = int(match.group(1))
- if hour >= 13:
- use_pm = True
- else:
- # Unklar → nochmal fragen
- return IntentResult(
- success=True,
- response_text=f"Bitte sage {option_a} oder {option_b}.",
- follow_up_intent="create_reminder",
- )
- # Nochmal parsen mit Kontext
- if use_pm:
- adjusted_text = time_text + " nachmittags"
- else:
- adjusted_text = time_text + " morgens"
- default_time = self.get_config_value("default_time", "09:00")
- parsed = parse_german_time(adjusted_text, default_time=default_time)
- if parsed.dt:
- return self._finalize_reminder(session_id, title, parsed.dt, data)
- # Fallback: Aus den Optionen direkt die Stunde ableiten
- import re
- match = re.search(r"(\d{1,2})", option_b if use_pm else option_a)
- if match:
- hour = int(match.group(1))
- now = datetime.now()
- dt = now.replace(hour=hour, minute=0, second=0, microsecond=0)
- if dt <= now:
- dt += timedelta(days=1)
- return self._finalize_reminder(session_id, title, dt, data)
- self._clear_dialog(session_id)
- return IntentResult.failure(
- "Zeitangabe nicht erkannt",
- response_text="Entschuldigung, ich konnte die Zeit nicht bestimmen.",
- )
- def _finalize_reminder(self, session_id: str, title: str, remind_at: datetime,
- data: IntentReceivedData) -> IntentResult:
- """Erstellt die Erinnerung und speichert sie."""
- self._clear_dialog(session_id)
- now = _now_iso()
- remind_date = remind_at.strftime("%Y-%m-%d")
- item = SyncItem(
- data_type=DATA_TYPE,
- data={
- "title": title,
- "remind_at": remind_at.isoformat(),
- "remind_date": remind_date,
- "recurring": False,
- "completed": False,
- "notified": False,
- },
- created_at=now,
- updated_at=now,
- source_id=data.satellite_id or "standalone",
- )
- self._store.save(item)
- # Benutzerfreundliche Zeitangabe
- time_str = self._format_remind_time(remind_at)
- pinfo(f"RemindersPlugin: Erinnerung erstellt: {title} um {time_str}")
- return IntentResult.success_with_response(
- f"Erinnerung fuer {title} {time_str} gespeichert."
- )
- def _format_remind_time(self, dt: datetime) -> str:
- """Formatiert ein datetime benutzerfreundlich."""
- now = datetime.now()
- today = now.date()
- target_date = dt.date()
- time_str = dt.strftime("%H:%M Uhr")
- if target_date == today:
- return f"heute um {time_str}"
- if target_date == today + timedelta(days=1):
- return f"morgen um {time_str}"
- if target_date == today + timedelta(days=2):
- return f"uebermorgen um {time_str}"
- wochentage = ["Montag", "Dienstag", "Mittwoch", "Donnerstag",
- "Freitag", "Samstag", "Sonntag"]
- diff = (target_date - today).days
- if 0 < diff <= 7:
- tag = wochentage[target_date.weekday()]
- return f"am {tag} um {time_str}"
- return f"am {dt.strftime('%d.%m.%Y')} um {time_str}"
- # =========================================================================
- # Intent-Handler: Erinnerungen auflisten
- # =========================================================================
- @intent("list_reminders", description="Alle Erinnerungen auflisten")
- @pattern("zeig [mir] [meine|alle] erinnerungen")
- @pattern("welche erinnerungen [habe ich]")
- @pattern("liste [meine|alle] erinnerungen [auf]")
- @pattern("meine erinnerungen")
- @example("Zeige meine Erinnerungen", "Welche Erinnerungen habe ich?")
- async def handle_list_reminders(self, data: IntentReceivedData) -> IntentResult:
- """Listet alle aktiven Erinnerungen auf."""
- reminders = self._get_active_reminders()
- if not reminders:
- return IntentResult.success_with_response(
- "Du hast keine Erinnerungen."
- )
- if len(reminders) == 1:
- r = reminders[0]
- title = r.data.get("title", "Unbenannt")
- remind_at = r.data.get("remind_at", "")
- time_str = self._format_remind_at(remind_at)
- return IntentResult.success_with_response(
- f"Du hast eine Erinnerung: {title} {time_str}."
- )
- lines = []
- for r in reminders[:5]:
- title = r.data.get("title", "Unbenannt")
- remind_at = r.data.get("remind_at", "")
- time_str = self._format_remind_at(remind_at)
- lines.append(f"{title} {time_str}")
- text = ", ".join(lines)
- total = len(reminders)
- if total > 5:
- return IntentResult.success_with_response(
- f"Du hast {total} Erinnerungen. Die naechsten fuenf: {text}."
- )
- return IntentResult.success_with_response(
- f"Du hast {total} Erinnerungen: {text}."
- )
- @intent("list_reminders_today", description="Heutige Erinnerungen anzeigen")
- @pattern("was steht heute an")
- @pattern("erinnerungen [fuer|von] heute")
- @pattern("heutige erinnerungen")
- @example("Was steht heute an?", "Erinnerungen fuer heute")
- async def handle_list_reminders_today(self, data: IntentReceivedData) -> IntentResult:
- """Zeigt Erinnerungen fuer heute."""
- today = datetime.now().strftime("%Y-%m-%d")
- reminders = self._get_active_reminders()
- today_reminders = [r for r in reminders if r.data.get("remind_date") == today]
- if not today_reminders:
- return IntentResult.success_with_response(
- "Fuer heute stehen keine Erinnerungen an."
- )
- if len(today_reminders) == 1:
- r = today_reminders[0]
- title = r.data.get("title", "Unbenannt")
- remind_at = r.data.get("remind_at", "")
- time_str = self._format_remind_at(remind_at)
- return IntentResult.success_with_response(
- f"Heute steht an: {title} {time_str}."
- )
- lines = []
- for r in today_reminders:
- title = r.data.get("title", "Unbenannt")
- remind_at = r.data.get("remind_at", "")
- time_str = self._format_remind_at(remind_at)
- lines.append(f"{title} {time_str}")
- return IntentResult.success_with_response(
- f"Heute stehen {len(today_reminders)} Erinnerungen an: {', '.join(lines)}."
- )
- # =========================================================================
- # Intent-Handler: Erinnerung loeschen
- # =========================================================================
- @intent("delete_reminder", description="Erinnerung loeschen")
- @pattern("(loesche|entferne|loesch|entfern) [die] erinnerung {query}")
- @pattern("erinnerung {query} (loeschen|entfernen)")
- @example("Loesche Erinnerung Zahnarzt", "Entferne die Erinnerung Einkaufen")
- async def handle_delete_reminder(self, data: IntentReceivedData) -> IntentResult:
- """Loescht eine Erinnerung (Soft-Delete)."""
- query = data.get_slot("query", "")
- if not query:
- return IntentResult.failure(
- "Kein Suchbegriff",
- response_text="Welche Erinnerung soll ich loeschen?",
- )
- query_lower = query.lower()
- reminders = self._get_active_reminders()
- matches = [
- r for r in reminders
- if query_lower in r.data.get("title", "").lower()
- ]
- if not matches:
- return IntentResult.failure(
- f"Erinnerung '{query}' nicht gefunden",
- response_text=f"Ich habe keine Erinnerung mit '{query}' gefunden.",
- )
- if len(matches) > 1:
- titles = [m.data.get("title", "Unbenannt") for m in matches]
- return IntentResult.failure(
- "Mehrere Treffer",
- response_text=f"Mehrere Erinnerungen gefunden: {', '.join(titles[:3])}. Welche soll ich loeschen?",
- )
- reminder = matches[0]
- self._store.delete(DATA_TYPE, reminder.item_id)
- title = reminder.data.get("title", "Unbenannt")
- pinfo(f"RemindersPlugin: Erinnerung geloescht: {title}")
- return IntentResult.success_with_response(f"Erinnerung '{title}' geloescht.")
- @intent("delete_all_reminders", description="Alle Erinnerungen loeschen")
- @pattern("(loesche|entferne|loesch|entfern) alle erinnerungen")
- @example("Loesche alle Erinnerungen")
- async def handle_delete_all_reminders(self, data: IntentReceivedData) -> IntentResult:
- """Startet Bulk-Delete mit Rueckfrage."""
- reminders = self._get_active_reminders()
- if not reminders:
- return IntentResult.success_with_response("Du hast keine Erinnerungen.")
- session_id = data.session_id or "unknown"
- self._pending_delete_all[session_id] = True
- return IntentResult(
- success=True,
- response_text=f"Du hast {len(reminders)} Erinnerungen. Bist du sicher? Sage Ja oder Nein.",
- follow_up_intent="confirm_delete_all_reminders",
- )
- @intent("confirm_delete_all_reminders", description="Bestaetigung Alle-Loeschen")
- async def handle_confirm_delete_all(self, data: IntentReceivedData) -> IntentResult:
- """Verarbeitet die Bestaetigung fuer Bulk-Delete."""
- session_id = data.session_id or "unknown"
- if session_id not in self._pending_delete_all:
- return IntentResult.failure(
- "Keine ausstehende Loeschung",
- response_text="Es gibt keine ausstehende Loeschanfrage.",
- )
- text = data.original_text.lower().strip()
- self._pending_delete_all.pop(session_id, None)
- if text in ("ja", "yes", "klar", "sicher", "mach", "ok", "genau"):
- reminders = self._get_active_reminders()
- count = 0
- for r in reminders:
- self._store.delete(DATA_TYPE, r.item_id)
- count += 1
- pinfo(f"RemindersPlugin: {count} Erinnerungen geloescht (Bulk)")
- return IntentResult.success_with_response(
- f"Alle {count} Erinnerungen wurden geloescht."
- )
- return IntentResult.success_with_response("Abgebrochen. Erinnerungen bleiben erhalten.")
- # =========================================================================
- # Hilfsmethoden
- # =========================================================================
- def _get_active_reminders(self) -> list[SyncItem]:
- """Gibt alle aktiven (nicht abgeschlossenen) Erinnerungen zurueck."""
- all_reminders = self._store.get_all(DATA_TYPE)
- return [r for r in all_reminders if not r.data.get("completed") and not r.data.get("notified")]
- def _format_remind_at(self, remind_at_str: str) -> str:
- """Formatiert einen ISO-Zeitstring benutzerfreundlich."""
- if not remind_at_str:
- return ""
- try:
- dt = datetime.fromisoformat(remind_at_str)
- if dt.tzinfo:
- dt = dt.replace(tzinfo=None)
- return self._format_remind_time(dt)
- except (ValueError, TypeError):
- return ""
|