diff --git a/diagnostic/index.html b/diagnostic/index.html
index 7505ca2..341d69e 100644
--- a/diagnostic/index.html
+++ b/diagnostic/index.html
@@ -1616,8 +1616,17 @@
Aus dem Gitea-Release. Der Satellit lädt die Datei selbst (einmal) und
installiert sie auf dem Gerät.
+
+
+ Spart das Abtippen auf dem Fernseher: Host und Token gehen direkt vom
+ Server an den Agenten — QR-Scan geht am Fire TV ja nicht.
+
+
@@ -4742,8 +4751,26 @@
'(Haken bei „Immer erlauben"). Ich warte bis zu 90 Sekunden darauf und\n' +
'installiere dann automatisch weiter. Kein zweiter Klick nötig.\n\n' +
'Danach: APK laden + installieren, zusammen 1–3 Minuten.';
+ const params = { ip: apkTarget.ip, url };
+ // Server ergaenzt Host/Port/Token — steht bewusst NICHT im Browser-Formular.
+ if (document.getElementById('apk-provision').checked) {
+ params.provision = true;
+ params.name = apkTarget.name;
+ }
send({ action: 'sat_command', satellite: apkTarget.satId, cmd: 'adb.install',
- requestId: apkRequestId, params: { ip: apkTarget.ip, url } });
+ requestId: apkRequestId, params });
+ }
+
+ /** Prüft, ob der Agent auf dem Gerät installiert ist und läuft. */
+ function runApkStatus() {
+ if (!apkTarget) return;
+ apkRequestId = 'apk_' + Date.now() + '_' + Math.floor(Math.random() * 100000);
+ document.getElementById('apk-status').innerHTML = 'prüfe …';
+ const log = document.getElementById('apk-log');
+ log.style.display = 'block';
+ log.textContent = 'Frage das Gerät über ADB ab …';
+ send({ action: 'sat_command', satellite: apkTarget.satId, cmd: 'adb.status',
+ requestId: apkRequestId, params: { ip: apkTarget.ip } });
}
function onSatResult(p) {
if (!apkRequestId || !p || p.requestId !== apkRequestId) return;
@@ -4753,7 +4780,19 @@
if (p.ok) {
const r = p.result || {};
status.innerHTML = '✓ erfolgreich';
- log.textContent = r.message || JSON.stringify(r, null, 2);
+ let txt = r.message || JSON.stringify(r, null, 2);
+ if (typeof r.installed !== 'undefined') { // adb.status
+ txt = (r.running ? '🟢 Agent läuft' : r.installed ? '🟡 installiert, läuft nicht' : '🔴 nicht installiert') +
+ (r.version ? '\nVersion: ' + r.version : '') +
+ '\nPaket: ' + (r.package || '') + '\nGerät: ' + (r.target || '');
+ if (!r.running && r.installed) {
+ txt += '\n\nTipp: Auf dem Gerät die App öffnen und „Speichern & Verbinden" —' +
+ '\noder hier „Gleich einrichten" nutzen, dann startet sie von selbst.';
+ }
+ } else if (r.provisioned === false) {
+ status.innerHTML = '⚠ installiert, Einrichtung offen';
+ }
+ log.textContent = txt;
} else {
status.innerHTML = '✗ fehlgeschlagen';
log.textContent = p.error || 'Unbekannter Fehler';
diff --git a/diagnostic/server.js b/diagnostic/server.js
index 6e91f1f..f842af8 100644
--- a/diagnostic/server.js
+++ b/diagnostic/server.js
@@ -2819,13 +2819,24 @@ wss.on("connection", (ws) => {
} else if (msg.action === "sat_command") {
// Browser loest eine Satelliten-Aktion aus (z.B. APK auf Fire TV ausrollen).
// Die Antwort (sat_result) kommt ueber die rvsWs zurueck.
+ const params = msg.params && typeof msg.params === "object" ? msg.params : {};
+ // Fern-Einrichtung: der RVS-Zugang wird SERVERSEITIG ergaenzt — so muss
+ // niemand Host/Token im Browser eintippen (und das Token steht nicht im
+ // Browser-Formular). Gilt fuer adb.provision und adb.install mit Setup.
+ if (params.provision === true) {
+ delete params.provision;
+ params.rvs_host = RVS_HOST;
+ params.rvs_port = Number(RVS_PORT) || 443;
+ params.rvs_tls = RVS_TLS === "true";
+ params.rvs_token = RVS_TOKEN;
+ }
sendToRVS_raw({
type: "sat_command",
payload: {
satellite: String(msg.satellite || ""),
device: String(msg.device || ""),
action: String(msg.cmd || ""),
- params: msg.params && typeof msg.params === "object" ? msg.params : {},
+ params,
requestId: String(msg.requestId || ""),
},
timestamp: Date.now(),
diff --git a/host-agent/android/README.md b/host-agent/android/README.md
index ebaa934..64b9adc 100644
--- a/host-agent/android/README.md
+++ b/host-agent/android/README.md
@@ -87,6 +87,25 @@ Statt die APK von Hand aufs Gerät zu schaufeln, kann ein **Satellit** sie per A
und einen **📦**-Button.
3. 📦 → APK-URL (aus dem Gitea-Release) → **Jetzt ausrollen**. Beim ersten Mal
erscheint auf dem TV „USB-Debugging zulassen?" → bestätigen (Haken „Immer erlauben").
+ Der Rollout **wartet darauf** und macht dann automatisch weiter.
+
+**Einrichtung ohne Tippen:** Am Fire TV gibt es keine Kamera (kein QR-Scan) und
+Host/Token mit der Fernbedienung einzugeben ist eine Qual. Deshalb ist im Rollout-
+Dialog **„Gleich einrichten"** vorausgewählt: die Diagnostic kennt den RVS-Zugang
+selbst und gibt ihn dem Agenten über dieselbe ADB-Verbindung mit
+(`adb.provision` → `am start` mit Intent-Extras). Der Agent speichert die Daten,
+startet den Dienst und ist **sofort online** — null Eingaben am Fernseher.
+Das Token geht dabei **serverseitig** raus, es steht nie im Browser-Formular.
+
+Mit **„Status prüfen"** (`adb.status`) siehst Du jederzeit, ob der Agent auf dem
+Gerät installiert ist, welche Version läuft und ob der Dienst aktiv ist.
+
+> **Sicherheitshinweis:** Die Einrichtung per Intent geht an die (exportierte)
+> Haupt-Activity. Wer ADB-Zugriff auf das Gerät hat, kann den Agenten damit
+> umkonfigurieren — wer ADB hat, kontrolliert das Gerät ohnehin. Die App zeigt
+> beim Empfang einen Hinweis („Von ARIA eingerichtet"), damit es nicht unbemerkt
+> passiert. Auf einem dedizierten Stick ist das unkritisch; schalte ADB-Debugging
+> nach dem Ausrollen ab, wenn Du ganz sicher gehen willst.
ARIA kann dasselbe: `satellite_command` mit `action='adb.install'`,
`params={'ip':'…','url':'…'}`. Der Satellit lädt die APK **einmal** und kann sie
diff --git a/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/MainActivity.kt b/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/MainActivity.kt
index 6bacec4..1fab72e 100644
--- a/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/MainActivity.kt
+++ b/host-agent/android/app/src/main/java/de/hackersoft/ariaagent/MainActivity.kt
@@ -122,7 +122,10 @@ class MainActivity : AppCompatActivity() {
}
}
- loadIntoUi(AgentConfig.load(this))
+ // Fern-Einrichtung (z.B. Fire TV, wo QR-Scan und Tippen wegfallen): der
+ // Satellit startet die App per ADB mit den Zugangsdaten als Extras.
+ // Hat das geklappt, ist die UI schon befuellt — sonst normal laden.
+ if (!applyProvisioning(intent)) loadIntoUi(AgentConfig.load(this))
// Fire TV: kein Touchscreen — der QR-Scan ist dort sinnlos (keine Kamera),
// und der erste Fokus muss gesetzt sein, damit die Fernbedienung greift.
@@ -209,6 +212,48 @@ class MainActivity : AppCompatActivity() {
statusView.text = "Status: ${AgentService.status} $dot"
}
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ if (intent != null && applyProvisioning(intent)) return
+ }
+
+ /**
+ * Fern-Einrichtung per Intent-Extras (vom Satelliten via ADB gestartet).
+ *
+ * Auf einem Fire TV gibt es keine Kamera (kein QR) und Tippen mit der
+ * Fernbedienung ist muehsam — deshalb kann der Satellit die Zugangsdaten
+ * direkt mitgeben:
+ * adb shell am start -n de.hackersoft.ariaagent/.MainActivity \
+ * -a de.hackersoft.ariaagent.PROVISION \
+ * -e host rvs.example.com --ei port 443 --ez tls true -e token XYZ
+ *
+ * Gibt true zurueck, wenn eingerichtet wurde (dann startet der Dienst gleich).
+ */
+ private fun applyProvisioning(intent: Intent): Boolean {
+ val host = intent.getStringExtra("host")?.trim().orEmpty()
+ val token = intent.getStringExtra("token")?.trim().orEmpty()
+ if (host.isEmpty() || token.isEmpty()) return false
+
+ val old = AgentConfig.load(this)
+ val cfg = AgentConfig(
+ host = host,
+ port = intent.getIntExtra("port", old.port.takeIf { it > 0 } ?: 443),
+ tls = intent.getBooleanExtra("tls", old.tls),
+ token = token,
+ name = intent.getStringExtra("name")?.trim().takeUnless { it.isNullOrEmpty() } ?: old.name,
+ controlEnabled = intent.getBooleanExtra("control", true),
+ replyMode = intent.getStringExtra("reply_mode")?.trim()
+ .takeUnless { it.isNullOrEmpty() } ?: old.replyMode,
+ )
+ cfg.save(this)
+ AgentService.start(this)
+ // Sichtbar machen, dass die Konfiguration von aussen kam (Transparenz).
+ toast("Von ARIA eingerichtet: ${cfg.host} — Agent gestartet")
+ loadIntoUi(cfg)
+ refreshStatus()
+ return true
+ }
+
/** Sprech-Taste: startet/stoppt die Aufnahme im laufenden Dienst. */
private fun talkToAria() {
if (!AgentConfig.load(this).isValid()) { toast("Erst verbinden"); return }
diff --git a/satellite/.env.example b/satellite/.env.example
index 24ca4d3..d0f5bf4 100644
--- a/satellite/.env.example
+++ b/satellite/.env.example
@@ -45,9 +45,12 @@ CONTROL_ENABLED=true
# adb.connect ADB-Verbindung aufbauen (params: ip)
# adb.devices verbundene ADB-Geraete auflisten
# adb.shell Shell-Kommando auf dem Android-Geraet (params: ip, cmd)
+# adb.provision Agent FERN einrichten: RVS-Zugang uebergeben, statt ihn mit der
+# Fernbedienung abzutippen (params: ip, rvs_host, rvs_token, ...)
+# adb.status laeuft/installiert der Agent auf dem Geraet? (params: ip)
# ACHTUNG: Diese Liste ueberschreibt den Code-Default. Wird eine neue Aktion
# ergaenzt, MUSS sie hier nachgetragen werden — sonst lehnt der Satellit sie ab.
-CONTROL_ALLOWLIST=dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer,snmp.ports,snmp.info,fritzbox.info,fritzbox.hosts,ssh.exec,adb.devices,adb.connect,adb.install,adb.shell
+CONTROL_ALLOWLIST=dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer,snmp.ports,snmp.info,fritzbox.info,fritzbox.hosts,ssh.exec,adb.devices,adb.connect,adb.install,adb.shell,adb.provision,adb.status
# ─── ADB / Android-Rollout (optional) ──────────────────────────────
# Fire TV Sticks & Android-Geraete im LAN: APK ausrollen ohne Datei-Kopieren.
diff --git a/satellite/satellite.py b/satellite/satellite.py
index d81e448..66e1c6b 100644
--- a/satellite/satellite.py
+++ b/satellite/satellite.py
@@ -135,7 +135,7 @@ CONTROL_ALLOWLIST = [
os.environ.get("CONTROL_ALLOWLIST",
"dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer,"
"snmp.ports,snmp.info,fritzbox.info,fritzbox.hosts,ssh.exec,"
- "adb.devices,adb.connect,adb.install,adb.shell").split(",")
+ "adb.devices,adb.connect,adb.install,adb.shell,adb.provision,adb.status").split(",")
if a.strip()
]
@@ -633,6 +633,10 @@ async def _control(action: str, params: dict, devices: list[dict]) -> dict:
return await loop.run_in_executor(None, _do_adb_install, params)
if action == "adb.shell":
return await loop.run_in_executor(None, _do_adb_shell, params)
+ if action == "adb.provision":
+ return await loop.run_in_executor(None, _do_adb_provision, params)
+ if action == "adb.status":
+ return await loop.run_in_executor(None, _do_adb_status, params)
return {"ok": False, "error": f"Aktion '{action}' nicht implementiert."}
except Exception as exc:
return {"ok": False, "error": f"{action} fehlgeschlagen: {exc}"}
@@ -1222,6 +1226,110 @@ def _do_adb_connect(params: dict) -> dict:
return {"ok": True, "result": {"target": target, "state": state, "message": msg}}
+AGENT_PACKAGE = os.environ.get("AGENT_PACKAGE", "de.hackersoft.ariaagent")
+
+
+def _adb_app_installed(target: str, package: str) -> bool:
+ rc, out, _ = _adb_run(["-s", target, "shell", "pm", "list", "packages", package],
+ timeout=30)
+ return rc == 0 and f"package:{package}" in out
+
+
+def _adb_app_running(target: str, package: str) -> bool:
+ """Laeuft der Vordergrund-Dienst des Agents? (Fire TV / Android)."""
+ rc, out, _ = _adb_run(["-s", target, "shell", "dumpsys", "activity", "services",
+ package], timeout=30)
+ return rc == 0 and "ServiceRecord" in out
+
+
+def _do_adb_status(params: dict) -> dict:
+ """Ist der ARIA-Agent auf dem Geraet installiert und laeuft er?"""
+ target = _adb_target(params)
+ if not target:
+ return {"ok": False, "error": "ip/host erforderlich."}
+ package = (params.get("package") or AGENT_PACKAGE).strip()
+ ok, msg = _adb_connect(target)
+ if not ok:
+ return {"ok": False, "error": msg}
+ installed = _adb_app_installed(target, package)
+ running = _adb_app_running(target, package) if installed else False
+ ver = ""
+ if installed:
+ rc, out, _ = _adb_run(["-s", target, "shell", "dumpsys", "package", package],
+ timeout=30)
+ m = re.search(r"versionName=(\S+)", out or "")
+ ver = m.group(1) if m else ""
+ return {"ok": True, "result": {
+ "target": target, "package": package, "installed": installed,
+ "running": running, "version": ver,
+ "message": ("Agent laeuft" if running else
+ "installiert, laeuft aber nicht" if installed else
+ "nicht installiert")}}
+
+
+def _do_adb_provision(params: dict) -> dict:
+ """Richtet den ARIA-Agent auf dem Geraet FERN ein (RVS-Zugang uebergeben).
+
+ Auf einem Fire TV gibt es keine Kamera (kein QR-Scan) und Tippen mit der
+ Fernbedienung ist muehsam. Der Satellit startet die App deshalb per ADB
+ direkt mit den Zugangsdaten als Intent-Extras.
+ params: {ip, rvs_host, rvs_token, rvs_port?, rvs_tls?, name?, control?,
+ reply_mode?, package?}
+ """
+ target = _adb_target(params)
+ if not target:
+ return {"ok": False, "error": "ip/host erforderlich."}
+ rvs_host = (params.get("rvs_host") or "").strip()
+ rvs_token = (params.get("rvs_token") or "").strip()
+ if not rvs_host or not rvs_token:
+ return {"ok": False, "error": "rvs_host und rvs_token erforderlich."}
+ package = (params.get("package") or AGENT_PACKAGE).strip()
+
+ ok, msg = _adb_connect(target)
+ state = _adb_state(target)
+ wait = float(params.get("auth_wait") or ADB_AUTH_WAIT_SEC)
+ if (not ok or state != "device") and wait > 0:
+ authed, state = _adb_wait_authorized(target, wait)
+ if not authed:
+ return {"ok": False, "error":
+ "Nicht autorisiert — 'USB-Debugging zulassen?' auf dem "
+ f"Bildschirm bestaetigen. (Zustand: '{state or '?'}')"}
+ elif not ok:
+ return {"ok": False, "error": msg}
+
+ if not _adb_app_installed(target, package):
+ return {"ok": False, "error": f"'{package}' ist nicht installiert — "
+ "erst adb.install ausfuehren."}
+
+ args = ["-s", target, "shell", "am", "start",
+ "-n", f"{package}/.MainActivity",
+ "-a", f"{package}.PROVISION",
+ "-e", "host", rvs_host,
+ "-e", "token", rvs_token,
+ "--ei", "port", str(int(params.get("rvs_port") or 443)),
+ "--ez", "tls", "true" if _as_bool(params.get("rvs_tls"), True) else "false",
+ "--ez", "control", "true" if _as_bool(params.get("control"), True) else "false"]
+ if params.get("name"):
+ args += ["-e", "name", str(params["name"])]
+ if params.get("reply_mode"):
+ args += ["-e", "reply_mode", str(params["reply_mode"])]
+
+ rc, out, err = _adb_run(args, timeout=60)
+ if rc != 0 or "Error" in f"{out} {err}":
+ return {"ok": False, "error": (f"{out} {err}".strip() or f"exit {rc}")[:400]}
+ return {"ok": True, "result": {
+ "target": target, "package": package,
+ "message": f"Agent eingerichtet (RVS {rvs_host}) und gestartet"}}
+
+
+def _as_bool(v, default: bool) -> bool:
+ if v is None:
+ return default
+ if isinstance(v, bool):
+ return v
+ return str(v).strip().lower() in ("1", "true", "yes", "on", "ja")
+
+
def _do_adb_shell(params: dict) -> dict:
"""Shell-Kommando auf dem Android-Geraet (Diagnose, z.B. 'pm list packages')."""
target = _adb_target(params)
@@ -1343,8 +1451,19 @@ def _do_adb_install(params: dict) -> dict:
txt = f"{out} {err2}".strip()
size_mb = round(os.path.getsize(apk) / 1024 / 1024, 1)
if rc == 0 and "success" in txt.lower():
- return {"ok": True, "result": {"target": target, "apk_mb": size_mb,
- "message": f"APK installiert ({size_mb} MB)"}}
+ res = {"target": target, "apk_mb": size_mb,
+ "message": f"APK installiert ({size_mb} MB)"}
+ # Direkt einrichten, wenn die RVS-Daten mitkamen — auf einem Fire TV
+ # spart das der Fernbedienung das Abtippen von Host und Token.
+ if params.get("rvs_host") and params.get("rvs_token"):
+ prov = _do_adb_provision(params)
+ res["provisioned"] = bool(prov.get("ok"))
+ if prov.get("ok"):
+ res["message"] += " und eingerichtet — der Agent sollte gleich online sein"
+ else:
+ res["message"] += (f". Einrichtung fehlgeschlagen: "
+ f"{prov.get('error', '?')}")
+ return {"ok": True, "result": res}
low = txt.lower()
if "signatures do not match" in low or "update_incompatible" in low:
return {"ok": False, "error": "Signatur passt nicht zur installierten Version — "