feat(trigger): entered_near + left_near — drei Modi fuer near()-Watcher
Stefan: bei aktuellen near()-Watcher gibt's nur "solange drin". Reale Szenarien wollen aber differenzieren: - VORWARNUNG vor Ziel (Blitzer-Warner 2 km vorher) → entered_near mit grossem r - ANKUNFT exakt am Ziel → entered_near mit kleinem r - VERLASSEN (Parkplatz, hast du was vergessen) → left_near - KONTINUIERLICH-DRIN (bin noch in der Naehe?) → near (Default, throttled) Zwei neue Funktionen in der Condition-Whitelist: - entered_near(lat, lon, r): True NUR im Moment des Uebergangs draussen → innen. Fires einmal pro Eintritt. - left_near(lat, lon, r): True NUR im Moment des Uebergangs innen → draussen. Fires einmal pro Austritt. State-Tracking: - pro Trigger pro near-Aufruf wird der letzte Auswertungs-Wert (true/ false) im Watcher-Manifest gespeichert (Field "near_states", Key "lat.6,lon.6,radius"). Background-Loop liest's vor dem Eval, gibt's per collect_variables(prev_near_states=...) in die Closure, schreibt nach dem Eval die neuen Werte zurueck — UNABHAENGIG ob gefeuert wurde, sonst greift die Uebergangs-Erkennung nicht. Background _tick: - Aufteilung in Watcher-Pass (mit prev_near_states pro Trigger) und Timer-Pass (ohne State, gemeinsame vars). Bisher war collect_variables einmal pro Tick — jetzt einmal pro Watcher. Disk-Stats sind teuer aber unter 30 Watchern unkritisch; bei mehr koennen wir cachen. ARIA-Tool-Description erweitert (trigger_watcher): erklaert die drei Modi mit Use-Cases und empfohlenen Throttle-Werten (kurz fuer entered/ left, lang fuer near). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+39
-18
@@ -164,7 +164,12 @@ async def _fire(trigger: dict, agent_factory) -> None:
|
||||
|
||||
|
||||
async def _tick(agent_factory) -> None:
|
||||
"""Ein Pruefdurchlauf. Geht ueber alle Triggers, feuert was zu feuern ist."""
|
||||
"""Ein Pruefdurchlauf. Geht ueber alle Triggers, feuert was zu feuern ist.
|
||||
|
||||
near()-State-Tracking: entered_near/left_near brauchen die Information
|
||||
ob ein near()-Aufruf beim letzten Tick true war (Uebergang erkennen).
|
||||
Wir halten das pro Trigger als near_states-Dict im Manifest und
|
||||
aktualisieren es nach jedem Eval — auch wenn nicht gefeuert wird."""
|
||||
try:
|
||||
all_triggers = triggers_mod.list_triggers(active_only=True)
|
||||
except Exception as e:
|
||||
@@ -173,32 +178,48 @@ async def _tick(agent_factory) -> None:
|
||||
if not all_triggers:
|
||||
return
|
||||
now = datetime.now(timezone.utc)
|
||||
# Variablen einmal pro Tick sammeln (nicht pro Trigger — Disk-Stat ist teuer)
|
||||
try:
|
||||
vars_ = watcher_mod.collect_variables()
|
||||
except Exception as e:
|
||||
logger.warning("collect_variables: %s", e)
|
||||
vars_ = {}
|
||||
|
||||
# Watcher: last_checked_at jetzt updaten (auch wenn nicht gefeuert wird,
|
||||
# damit der Check-Interval respektiert wird)
|
||||
for t in all_triggers:
|
||||
if t.get("type") == "watcher":
|
||||
try:
|
||||
t["last_checked_at"] = _now_iso()
|
||||
triggers_mod.write(t["name"], t)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for trigger in all_triggers:
|
||||
if trigger.get("type") != "watcher":
|
||||
continue
|
||||
try:
|
||||
if _should_fire(trigger, vars_, now):
|
||||
# Variablen pro Trigger sammeln — wegen prev_near_states-Closure
|
||||
prev = trigger.get("near_states") or {}
|
||||
vars_ = watcher_mod.collect_variables(prev_near_states=prev)
|
||||
|
||||
# Condition evaluieren via _should_fire (intern ruft watcher.evaluate)
|
||||
fired = _should_fire(trigger, vars_, now)
|
||||
|
||||
# State immer updaten, egal ob gefeuert wurde — sonst greift
|
||||
# entered_near/left_near nicht
|
||||
new_states = vars_.get("_new_near_states") or {}
|
||||
trigger["near_states"] = new_states
|
||||
trigger["last_checked_at"] = _now_iso()
|
||||
try:
|
||||
triggers_mod.write(trigger["name"], trigger)
|
||||
except Exception as e:
|
||||
logger.warning("trigger.write %s: %s", trigger.get("name"), e)
|
||||
|
||||
if fired:
|
||||
# Feuern als eigener Task — wenn ARIA langsam antwortet,
|
||||
# darf der naechste Tick nicht blockieren
|
||||
asyncio.create_task(_fire(trigger, agent_factory))
|
||||
except Exception as e:
|
||||
logger.warning("Trigger-Check %s: %s", trigger.get("name"), e)
|
||||
|
||||
# Timer (one-shot) — separat ohne near-State
|
||||
timer_vars = None
|
||||
for trigger in all_triggers:
|
||||
if trigger.get("type") != "timer":
|
||||
continue
|
||||
try:
|
||||
if timer_vars is None:
|
||||
timer_vars = watcher_mod.collect_variables()
|
||||
if _should_fire(trigger, timer_vars, now):
|
||||
asyncio.create_task(_fire(trigger, agent_factory))
|
||||
except Exception as e:
|
||||
logger.warning("Timer-Check %s: %s", trigger.get("name"), e)
|
||||
|
||||
|
||||
# Module-Level-Slot fuer die agent_factory damit on-demand-Ticks (von
|
||||
# z.B. POST /triggers/check-now) Zugang haben ohne durch den ganzen
|
||||
|
||||
Reference in New Issue
Block a user