fix(trigger): near() fired bei Auto-Vorbeifahrten verpasst — Loop schneller + event-getrieben
Stefan ist mehrmals an einem 300m-near()-Watcher (DRK Kreyenbrueck)
vorbeigefahren, kein Fire. Ursache: Background-Loop tickte alle 30s,
Auto-Durchfahrt durch 600m-Durchmesser-Radius dauert bei 50-120 km/h
nur 18-43 Sekunden — der Tick konnte komplett dazwischen liegen.
Drei Fixes (A + B aus Stefans Vorschlag):
A1. Background-Loop-Frequenz: TICK_SEC 30 → 8.
Garantiert mind. 2 Checks auch bei 120 km/h durch 300m. Loop ist
billig (paar Dateilesungen + AST-Eval), Brain merkt das nicht.
A2. near() bekommt Age-Schutz (watcher.py NEAR_MAX_AGE_SEC=300):
Wenn location_age_sec > 5 min, gilt die Position als unbekannt
und near() liefert False. Verhindert Phantom-Fires wenn Tracking
aus ist oder Mobilfunk weg war — vorher haette der letzte
bekannte Wert weiter ausgewertet werden koennen.
B. Event-getriebener Tick:
- background.py: tick_now()-Funktion + Module-Slot fuer
agent_factory damit man von ausserhalb des Lifespan-Pfads
einen Tick triggern kann
- main.py: POST /triggers/check-now Endpoint ruft tick_now()
- bridge: _persist_location feuert nach jedem Save ein fire-and-
forget POST /triggers/check-now (run_in_executor, timeout 8s,
blockt nichts wenn Brain stockt)
Damit fires near() sofort wenn die App ein location_update schickt —
Polling ist nur noch der Fallback fuer Watcher OHNE GPS-Bezug
(disk_free, hour_of_day etc.) und als Sicherheits-Tick falls
location_update mal ausfaellt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,12 @@ import watcher as watcher_mod
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TICK_SEC = 30
|
||||
# Polling-Frequenz des Background-Loops. Vorher 30s → Auto-Vorbeifahrt
|
||||
# durch einen 300m-Radius bei >50 km/h konnte zwischen zwei Ticks komplett
|
||||
# verpasst werden. Mit 8s ist auch eine 18-Sekunden-Durchfahrt (120 km/h
|
||||
# durch 300m) garantiert mind. einmal getroffen. Der Loop ist billig
|
||||
# (paar Dateilesungen + AST-Eval), das macht Brain nicht warm.
|
||||
TICK_SEC = 8
|
||||
BRIDGE_URL = os.environ.get("BRIDGE_URL", "http://aria-bridge:8090")
|
||||
|
||||
|
||||
@@ -195,8 +200,31 @@ async def _tick(agent_factory) -> None:
|
||||
logger.warning("Trigger-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
|
||||
# Lifespan-Pfad geschleust zu werden.
|
||||
_AGENT_FACTORY = None
|
||||
|
||||
|
||||
async def tick_now() -> dict:
|
||||
"""Sofortiger Trigger-Check — nicht warten auf den naechsten Loop-Tick.
|
||||
Wird genutzt wenn ein neues GPS-Update reinkommt: Bridge ruft das nach
|
||||
_persist_location, damit Watcher mit near() den frischen Wert sofort
|
||||
sehen statt bis zu TICK_SEC Sekunden zu warten."""
|
||||
if _AGENT_FACTORY is None:
|
||||
return {"ok": False, "error": "Background-Loop noch nicht gestartet"}
|
||||
try:
|
||||
await _tick(_AGENT_FACTORY)
|
||||
return {"ok": True}
|
||||
except Exception as exc:
|
||||
logger.exception("tick_now: %s", exc)
|
||||
return {"ok": False, "error": str(exc)}
|
||||
|
||||
|
||||
async def run_loop(agent_factory) -> None:
|
||||
"""Endlosschleife — wird vom main lifespan gestartet + gestoppt."""
|
||||
global _AGENT_FACTORY
|
||||
_AGENT_FACTORY = agent_factory
|
||||
logger.info("Trigger-Loop gestartet (TICK_SEC=%d)", TICK_SEC)
|
||||
while True:
|
||||
try:
|
||||
|
||||
@@ -657,6 +657,16 @@ def triggers_list(active_only: bool = False):
|
||||
return {"triggers": triggers_mod.list_triggers(active_only=active_only)}
|
||||
|
||||
|
||||
@app.post("/triggers/check-now")
|
||||
async def triggers_check_now():
|
||||
"""Sofortiger Trigger-Check, statt auf den naechsten Background-Tick
|
||||
zu warten. Wird von der Bridge nach jedem location_update gerufen
|
||||
damit GPS-Watcher (near()) den frischen Wert SOFORT sehen — bei
|
||||
Auto-Vorbeifahrt durch einen 300m-Radius hat man sonst nur ~20s
|
||||
Drinnen-Zeit, was unter TICK_SEC fallen kann."""
|
||||
return await background_mod.tick_now()
|
||||
|
||||
|
||||
@app.get("/triggers/conditions")
|
||||
def triggers_conditions():
|
||||
"""Verfuegbare Variablen + Funktionen fuer Watcher-Conditions
|
||||
|
||||
+14
-1
@@ -91,6 +91,12 @@ def _cpu_load_1min() -> float:
|
||||
|
||||
_DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
|
||||
|
||||
# Maximales GPS-Alter fuer near()-Auswertung. Wenn die App laenger nicht
|
||||
# gepushed hat (z.B. Tracking aus, Mobilfunk weg, App geschlossen), gilt
|
||||
# die Position als "unbekannt" und near() liefert False — verhindert
|
||||
# Phantom-Fires basierend auf einer wochen-alten Position.
|
||||
NEAR_MAX_AGE_SEC = 5 * 60
|
||||
|
||||
|
||||
def _gps_state() -> dict[str, Any]:
|
||||
"""Letzte bekannte Position aus /shared/state/location.json.
|
||||
@@ -177,11 +183,18 @@ def collect_variables() -> dict[str, Any]:
|
||||
# Funktion-Helper — wird vom Parser als ast.Call mit Name "near" erkannt.
|
||||
# Closure ueber die GPS-Werte, damit eval keine extra Variablen braucht.
|
||||
def _near(lat: float, lon: float, radius_m: float) -> bool:
|
||||
"""Haversine-Distanz: True wenn aktuelle Position < radius_m vom Punkt."""
|
||||
"""Haversine-Distanz: True wenn aktuelle Position < radius_m vom Punkt.
|
||||
Plus Age-Schutz: GPS-Daten aelter als NEAR_MAX_AGE_SEC werden als
|
||||
veraltet betrachtet → False. Sonst wuerde near() bei abgeschaltetem
|
||||
Tracking weiter die letzte bekannte Position nutzen und potentiell
|
||||
Phantom-Fires werfen wenn der User mal in der Naehe war."""
|
||||
cur_lat = vars_.get("current_lat")
|
||||
cur_lon = vars_.get("current_lon")
|
||||
if cur_lat is None or cur_lon is None:
|
||||
return False
|
||||
age = vars_.get("location_age_sec")
|
||||
if isinstance(age, (int, float)) and age >= 0 and age > NEAR_MAX_AGE_SEC:
|
||||
return False
|
||||
try:
|
||||
R = 6371000.0
|
||||
phi1 = math.radians(float(cur_lat))
|
||||
|
||||
Reference in New Issue
Block a user