' : '') +
devHtml +
'';
}).join('');
diff --git a/diagnostic/server.js b/diagnostic/server.js
index 9a2b098..92b0d9f 100644
--- a/diagnostic/server.js
+++ b/diagnostic/server.js
@@ -481,7 +481,7 @@ const satellites = new Map(); // id → {id, location, caps, control, last_seen}
function satelliteList() {
const now = Date.now();
return Array.from(satellites.values()).map(s => ({
- id: s.id, location: s.location, caps: s.caps, control: s.control,
+ id: s.id, location: s.location, caps: s.caps, control: s.control, net: s.net || null,
online: (now - (s.last_seen || 0)) < 300000,
}));
}
@@ -924,7 +924,8 @@ function connectRVS(forcePlain) {
if (p.id) {
satellites.set(p.id, {
id: p.id, location: p.location || p.id,
- caps: p.caps || [], control: !!p.control, last_seen: Date.now(),
+ caps: p.caps || [], control: !!p.control, net: p.net || null,
+ last_seen: Date.now(),
});
broadcastSatellites();
}
diff --git a/satellite/README.md b/satellite/README.md
index 026310c..df5f8ef 100644
--- a/satellite/README.md
+++ b/satellite/README.md
@@ -10,21 +10,44 @@ Ferienwohnung …). Er verbindet sich als RVS-Client in Stefans Raum und gibt AR
- **Hände (Steuerung):** **DIAL-App-Launch** (z.B. YouTube-Video auf dem Fire TV),
**Wake-on-LAN**, generisches **HTTP**. Nur wenn freigeschaltet (siehe Sicherheit).
-## Deploy
+## ⚠️ Wichtig: der Satellit MUSS im echten Ziel-LAN laufen
+Discovery (mDNS/SSDP-Multicast + ARP) funktioniert **nur**, wenn der Prozess
+tatsächlich im selben LAN wie die Geräte hängt — z.B. `192.168.177.0/24`, wo der
+Fire TV steht.
+
+**Docker Desktop (Mac/Windows) geht NICHT.** Dort ist `network_mode: host` das Netz
+der Docker-Linux-VM (NAT, `192.168.65.x` / `172.x`), **nicht** dein echtes LAN.
+Der Satellit sieht dann nur Docker-Container statt der echten Geräte. (Der Satellit
+erkennt das selbst und meldet eine ⚠-Warnung im Diagnostic + Log.)
+
+Richtig deployen — zwei Wege:
+
+**A) Linux-Box im Ziel-LAN mit Docker Engine** (empfohlen, z.B. Raspberry Pi / NUC im Büro):
```bash
cd satellite
-cp .env.example .env # RVS-Zugang + SATELLITE_LOCATION eintragen
+cp .env.example .env # RVS-Zugang + SATELLITE_LOCATION
docker compose up -d --build
-docker compose logs -f # "sat_hello gesendet" + "[scan] N Geraete"
+docker compose logs -f # "Netz: primary_ip=192.168.177.x" + "[scan] N Geraete"
+```
+`network_mode: host` (schon gesetzt) gibt hier echtes LAN + Multicast.
+
+**B) Nativ als Python-Prozess** (für Mac/Windows-Test oder ohne Docker) — läuft direkt
+auf einer Maschine im Ziel-LAN:
+```bash
+cd satellite
+pip install -r requirements.txt
+export RVS_HOST=... RVS_TOKEN=... SATELLITE_LOCATION="Wohnung" CONTROL_ENABLED=true
+python satellite.py
```
`RVS_HOST/PORT/TLS/TOKEN` **identisch** zum Haupt-Stack (gleicher Raum, damit ARIA
den Satelliten erreicht). `SATELLITE_LOCATION` ist der Name, über den ARIA das Netz
anspricht („Büro").
-> **`network_mode: host` ist Pflicht** (schon in der compose gesetzt): nur so sieht
-> der Container die mDNS/SSDP-Broadcasts und die Geräte-IPs des LAN.
+**Kontrolle:** im Log/Diagnostic muss `primary_ip` im Ziel-LAN liegen
+(`192.168.177.x`) — steht da `192.168.65.x` oder `172.x`, sitzt der Satellit im
+falschen (Docker-)Netz.
## So nutzt ARIA es
diff --git a/satellite/docker-compose.yml b/satellite/docker-compose.yml
index aaea78c..b04da7d 100644
--- a/satellite/docker-compose.yml
+++ b/satellite/docker-compose.yml
@@ -9,6 +9,11 @@
# sieht er die mDNS/SSDP-Broadcasts + Geraete-IPs des LAN nicht (Docker-Bridge
# wuerde das isolieren). Damit ist er zugleich als RVS-Client raus ins Internet
# verbunden. Keine Ports zu veroeffentlichen — er ist reiner Client.
+#
+# ⚠ NUR auf LINUX Docker Engine, und der Host muss physisch im Ziel-LAN haengen!
+# Docker Desktop (Mac/Windows) gibt hier NUR das Docker-VM-Netz (192.168.65.x /
+# 172.x), NICHT dein echtes LAN → Discovery findet dann nur Docker-Container.
+# Fuer Mac/Windows: satellite.py nativ starten (siehe README, Weg B).
services:
satellite:
build: .
diff --git a/satellite/satellite.py b/satellite/satellite.py
index 690dc0e..de9955e 100644
--- a/satellite/satellite.py
+++ b/satellite/satellite.py
@@ -102,6 +102,52 @@ if CONTROL_ENABLED:
CAPABILITIES += CONTROL_ALLOWLIST
+# ─── Netz-Kontext / Selbstdiagnose ──────────────────────────────────
+
+def _in_docker_bridge(ip: str) -> bool:
+ # Docker-Default-Bridge-Range 172.16.0.0/12
+ try:
+ a, b = ip.split(".")[:2]
+ return a == "172" and 16 <= int(b) <= 31
+ except Exception:
+ return False
+
+
+def _net_context() -> dict:
+ """Ermittelt in welchem Netz der Satellit LAeUFT — und warnt, wenn das ein
+ Docker-/NAT-Netz ist (dann erreicht Discovery das echte LAN nicht)."""
+ ips: list[str] = []
+ primary = ""
+ try:
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ s.settimeout(1)
+ s.connect(("8.8.8.8", 80))
+ primary = s.getsockname()[0]
+ s.close()
+ except Exception:
+ pass
+ try:
+ for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
+ ip = info[4][0]
+ if ip and not ip.startswith("127.") and ip not in ips:
+ ips.append(ip)
+ except Exception:
+ pass
+ if primary and primary not in ips:
+ ips.insert(0, primary)
+ p = primary or (ips[0] if ips else "")
+ warning = ""
+ if p.startswith("192.168.65.") or _in_docker_bridge(p):
+ warning = (f"Satellit laeuft in einem Docker-/NAT-Netz ({p}), NICHT im echten LAN. "
+ "mDNS/SSDP erreichen die realen Geraete so nicht. Auf Docker Desktop "
+ "(Mac/Windows) geht LAN-Discovery nicht — den Satelliten NATIV (python "
+ "satellite.py) oder auf einem Linux-Host im Ziel-LAN betreiben.")
+ return {"primary_ip": p, "ips": ips, "warning": warning}
+
+
+NET = _net_context()
+
+
# ─── Discovery ──────────────────────────────────────────────────────
def _discover_mdns(timeout: float) -> list[dict]:
@@ -467,6 +513,7 @@ class Satellite:
"location": SATELLITE_LOCATION,
"caps": CAPABILITIES,
"control": CONTROL_ENABLED,
+ "net": NET,
},
"timestamp": int(time.time() * 1000),
})
@@ -496,7 +543,8 @@ class Satellite:
await self._send({
"type": "sat_devices",
"payload": {"requestId": req_id, "satellite": SATELLITE_ID,
- "location": SATELLITE_LOCATION, "devices": devices},
+ "location": SATELLITE_LOCATION, "devices": devices,
+ "net": NET},
"timestamp": int(time.time() * 1000),
})
@@ -566,6 +614,9 @@ class Satellite:
def main() -> None:
logger.info("ARIA Satellit startet — id=%s location=%s control=%s",
SATELLITE_ID, SATELLITE_LOCATION, CONTROL_ENABLED)
+ logger.info("Netz: primary_ip=%s alle=%s", NET.get("primary_ip"), NET.get("ips"))
+ if NET.get("warning"):
+ logger.warning("⚠ %s", NET["warning"])
try:
asyncio.run(Satellite().run())
except KeyboardInterrupt: