diff --git a/android/src/screens/SettingsScreen.tsx b/android/src/screens/SettingsScreen.tsx index 23982f4..f4d2f32 100644 --- a/android/src/screens/SettingsScreen.tsx +++ b/android/src/screens/SettingsScreen.tsx @@ -51,6 +51,7 @@ import { TTS_SPEED_STORAGE_KEY, } from '../services/audio'; import audioService from '../services/audio'; +import gpsTrackingService from '../services/gpsTracking'; import { isVerboseLogging, setVerboseLogging } from '../services/logger'; import { isWakeReadySoundEnabled, @@ -121,6 +122,7 @@ const SettingsScreen: React.FC = () => { const [manualPort, setManualPort] = useState('8765'); const [currentMode, setCurrentMode] = useState('normal'); const [gpsEnabled, setGpsEnabled] = useState(false); + const [gpsTracking, setGpsTracking] = useState(gpsTrackingService.isActive()); const [scannerVisible, setScannerVisible] = useState(false); const [logTab, setLogTab] = useState('live'); const [logs, setLogs] = useState([]); @@ -188,6 +190,11 @@ const SettingsScreen: React.FC = () => { AsyncStorage.getItem('aria_gps_enabled').then(saved => { if (saved !== null) setGpsEnabled(saved === 'true'); }); + // gpsTrackingService status syncen + auf Aenderungen lauschen + setGpsTracking(gpsTrackingService.isActive()); + const offGps = gpsTrackingService.onChange(setGpsTracking); + // Persistierten Status wiederherstellen (war Tracking beim letzten Mal an?) + gpsTrackingService.restoreFromStorage().catch(() => {}); AsyncStorage.getItem(TTS_PREROLL_STORAGE_KEY).then(saved => { if (saved != null) { const n = parseFloat(saved); @@ -245,6 +252,10 @@ const SettingsScreen: React.FC = () => { }); // Voice-Liste vom XTTS-Server holen (via RVS) rvs.send('xtts_list_voices' as any, {}); + return () => { + // gpsTrackingService-Listener abmelden (Variable offGps oben definiert) + try { offGps(); } catch {} + }; }, []); // Speichergroesse berechnen @@ -407,6 +418,18 @@ const SettingsScreen: React.FC = () => { } } + // ARIA bittet um GPS-Tracking An/Aus (Tool request_location_tracking) + if (message.type === ('location_tracking' as any)) { + const p: any = message.payload || {}; + const on = !!p.on; + const reason = (p.reason as string) || 'ARIA'; + if (on) { + gpsTrackingService.start(reason).catch(() => {}); + } else { + gpsTrackingService.stop(reason); + } + } + // Datei-Manager: ZIP-Response (Multi-Download) if (message.type === ('file_zip_response' as any)) { const p: any = message.payload || {}; @@ -1004,6 +1027,29 @@ const SettingsScreen: React.FC = () => { thumbColor={gpsEnabled ? '#FFFFFF' : '#666680'} /> + + {/* GPS-Tracking (kontinuierlich) — fuer near()-Watcher */} + + + GPS-Tracking (kontinuierlich) + + Sendet alle ~15s deine Position an ARIA (wenn du dich {'>'}30m bewegt + hast). Nur noetig fuer GPS-basierte Trigger wie Blitzer-Warner + (near()-Conditions). ARIA kann das auch selbst an-/abschalten wenn + sie einen GPS-Watcher anlegt. Akku-Verbrauch erhoeht — bei langer + Fahrt einplanen. + + + { + if (v) gpsTrackingService.start('manuell').catch(() => {}); + else gpsTrackingService.stop('manuell'); + }} + trackColor={{ false: '#2A2A3E', true: '#FF9500' }} + thumbColor={gpsTracking ? '#FFFFFF' : '#666680'} + /> + )} diff --git a/android/src/services/gpsTracking.ts b/android/src/services/gpsTracking.ts new file mode 100644 index 0000000..240aa08 --- /dev/null +++ b/android/src/services/gpsTracking.ts @@ -0,0 +1,138 @@ +/** + * GPS-Tracking-Service. + * + * Wenn aktiv: pushed alle paar Sekunden die aktuelle Position als + * `location_update {lat, lon}` an den RVS-Server, damit Brain-Watcher + * mit `near()`-Conditions etwas zum Vergleichen haben. + * + * Default: AUS. Wird entweder vom User manuell in Settings angeschaltet + * oder von ARIA via location_tracking-RVS-Message (Brain-Tool + * `request_location_tracking`). + * + * Energie-Schutz: distanceFilter 30m, interval 15s. Echte Fahrt-Updates + * (Geschwindigkeit) kommen sauber durch, stationaer wird kaum gesendet. + */ + +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { PermissionsAndroid, Platform, ToastAndroid } from 'react-native'; +import Geolocation from '@react-native-community/geolocation'; +import rvs from './rvs'; + +type Listener = (active: boolean) => void; + +class GpsTrackingService { + private watchId: number | null = null; + private active = false; + private listeners: Set = new Set(); + // Defensive: nicht zu schnell oeffentlich togglen + private lastChangeAt = 0; + + isActive(): boolean { + return this.active; + } + + onChange(cb: Listener): () => void { + this.listeners.add(cb); + return () => { this.listeners.delete(cb); }; + } + + private notify() { + for (const cb of this.listeners) { + try { cb(this.active); } catch {} + } + } + + /** Beim App-Start: gespeicherten Zustand wiederherstellen (Default off). */ + async restoreFromStorage(): Promise { + try { + const v = await AsyncStorage.getItem('aria_gps_tracking'); + if (v === 'true') { + console.log('[gps-track] Restore: war an, starte wieder'); + this.start('Beim Start wiederhergestellt'); + } + } catch {} + } + + private async ensurePermission(): Promise { + if (Platform.OS !== 'android') return true; + try { + const granted = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, + { + title: 'GPS-Tracking', + message: 'ARIA braucht laufende Standort-Updates damit GPS-Watcher (Blitzer-Warner, near()) funktionieren.', + buttonPositive: 'Erlauben', + buttonNegative: 'Abbrechen', + }, + ); + return granted === PermissionsAndroid.RESULTS.GRANTED; + } catch (e) { + console.warn('[gps-track] Permission-Fehler:', e); + return false; + } + } + + async start(reason: string = ''): Promise { + if (this.active) return true; + const ok = await this.ensurePermission(); + if (!ok) { + ToastAndroid.show('GPS-Tracking: Berechtigung abgelehnt', ToastAndroid.LONG); + return false; + } + try { + this.watchId = Geolocation.watchPosition( + (pos) => { + const lat = pos.coords.latitude; + const lon = pos.coords.longitude; + rvs.send('location_update' as any, { lat, lon }); + }, + (err) => { + console.warn('[gps-track] watchPosition error:', err?.code, err?.message); + }, + { + enableHighAccuracy: true, + distanceFilter: 30, // erst senden wenn 30m gewandert + interval: 15000, // (Android) gewuenschte Frequenz + fastestInterval: 10000, // (Android) max Frequenz + } as any, + ); + this.active = true; + this.lastChangeAt = Date.now(); + this.notify(); + AsyncStorage.setItem('aria_gps_tracking', 'true').catch(() => {}); + ToastAndroid.show( + reason ? `GPS-Tracking aktiv (${reason})` : 'GPS-Tracking aktiv', + ToastAndroid.SHORT, + ); + console.log('[gps-track] gestartet', reason ? `(${reason})` : ''); + return true; + } catch (e: any) { + console.warn('[gps-track] start fehlgeschlagen:', e?.message); + return false; + } + } + + stop(reason: string = ''): void { + if (!this.active) return; + if (this.watchId !== null) { + try { Geolocation.clearWatch(this.watchId); } catch {} + this.watchId = null; + } + this.active = false; + this.lastChangeAt = Date.now(); + this.notify(); + AsyncStorage.setItem('aria_gps_tracking', 'false').catch(() => {}); + ToastAndroid.show( + reason ? `GPS-Tracking aus (${reason})` : 'GPS-Tracking aus', + ToastAndroid.SHORT, + ); + console.log('[gps-track] gestoppt', reason ? `(${reason})` : ''); + } + + async toggle(reason: string = ''): Promise { + if (this.active) this.stop(reason); + else await this.start(reason); + } +} + +export default new GpsTrackingService(); diff --git a/aria-brain/agent.py b/aria-brain/agent.py index c972873..0fbbb42 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -176,6 +176,27 @@ META_TOOLS = [ "parameters": {"type": "object", "properties": {}}, }, }, + { + "type": "function", + "function": { + "name": "request_location_tracking", + "description": ( + "Bittet die App, das kontinuierliche GPS-Tracking zu aktivieren oder zu " + "deaktivieren. Default ist AUS (Akku-Schutz). Nutze das wenn du einen " + "GPS-basierten Watcher anlegst (z.B. `near(...)`), sonst hat die App " + "veraltete Position und der Watcher feuert nie. Auch wieder ausschalten " + "wenn der letzte GPS-Watcher geloescht wurde." + ), + "parameters": { + "type": "object", + "properties": { + "on": {"type": "boolean", "description": "true = Tracking an, false = aus"}, + "reason": {"type": "string", "description": "Kurzer Grund (wird in App-Notification angezeigt)"}, + }, + "required": ["on"], + }, + }, + }, ] @@ -401,6 +422,15 @@ class Agent: return f"OK — Trigger '{arguments['name']}' geloescht." except ValueError as e: return f"FEHLER: {e}" + if name == "request_location_tracking": + on = bool(arguments.get("on", False)) + reason = (arguments.get("reason") or "").strip() + self._pending_events.append({ + "type": "location_tracking", + "on": on, + "reason": reason, + }) + return f"OK — Tracking-Request gesendet (on={on}). App wird in Kuerze umschalten." if name == "trigger_list": items = triggers_mod.list_triggers(active_only=False) if not items: diff --git a/aria-brain/prompts.py b/aria-brain/prompts.py index d5db6cf..2cba2b2 100644 --- a/aria-brain/prompts.py +++ b/aria-brain/prompts.py @@ -155,6 +155,16 @@ def build_triggers_section( lines.append("- **Timer** fuer einmalige Erinnerungen mit konkreter Zeit ('in 10min', 'um 14:30').") lines.append("- **Watcher** fuer 'wenn X passiert' (Disk voll, bestimmte Tageszeit, GPS-Naehe).") lines.append("- ARIA legt Trigger NUR auf Stefan-Wunsch an, nicht eigenmaechtig.") + lines.append("") + lines.append("### GPS-Watcher mit near()") + lines.append( + "Wenn du einen Watcher mit `near()` anlegst: die App sendet GPS-Position " + "nur kontinuierlich wenn Tracking AN ist (Default: AUS, Akku-Schutz). " + "Rufe dafuer `request_location_tracking(on=true, reason=\"...\")` auf " + "bevor oder gleich nach dem trigger_watcher. Sonst hat current_lat/lon " + "veraltete Werte und der Watcher feuert nie. " + "Beim Loeschen des letzten GPS-Watchers (trigger_cancel) wieder " + "`request_location_tracking(on=false)` aufrufen.") return "\n".join(lines) diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index 4f99eb4..cb66761 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -1358,6 +1358,18 @@ class ARIABridge: }) logger.info("[brain] ARIA hat einen Trigger angelegt: %s", event.get("trigger", {}).get("name")) + elif etype == "location_tracking": + # ARIA bittet die App das GPS-Tracking ein-/auszuschalten + await self._send_to_rvs({ + "type": "location_tracking", + "payload": { + "on": bool(event.get("on")), + "reason": event.get("reason") or "", + }, + "timestamp": int(asyncio.get_event_loop().time() * 1000), + }) + logger.info("[brain] location_tracking Request: on=%s (%s)", + event.get("on"), event.get("reason", "")) # _process_core_response uebernimmt alles weitere: # File-Marker extrahieren + broadcasten, NO_REPLY-Check, Chat- @@ -1914,6 +1926,17 @@ class ARIABridge: logger.warning("[rvs] file_delete_request: %s", e) return + elif msg_type == "location_update": + # Live-GPS-Update von der App (nicht an Chat gekoppelt). Wird in + # /shared/state/location.json geschrieben, damit Watcher-Trigger + # near()-Conditions auswerten koennen. + lat = payload.get("lat") + lon = payload.get("lon") or payload.get("lng") + if lat is not None and lon is not None: + self._persist_location({"lat": lat, "lon": lon}) + logger.debug("[gps] location_update: %.5f, %.5f", float(lat), float(lon)) + return + elif msg_type == "container_restart": # App-Button "Container neu" — leitet generisch an Diagnostic # weiter. Whitelist ist im Diagnostic-Server. diff --git a/rvs/server.js b/rvs/server.js index fdbac48..36b059e 100644 --- a/rvs/server.js +++ b/rvs/server.js @@ -26,6 +26,7 @@ const ALLOWED_TYPES = new Set([ "xtts_import_voice", "xtts_voice_imported", "skill_created", "trigger_created", + "location_update", "location_tracking", "chat_history_request", "chat_history_response", "chat_cleared", "file_delete_batch_request", "file_delete_batch_response", "file_zip_request", "file_zip_response",