diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index f324811..378ed67 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -4769,6 +4769,18 @@ class ARIABridge: int(data.get("port") or 0), int(data.get("localPort") or 0)) await _send_response(writer, 200, res) + elif method == "POST" and path == "/internal/display-name": + # Anzeigename fuer einen Host-Agenten oder Satelliten. + # body: {kind: host|satellite, id, name} + try: + data = json.loads(body.decode("utf-8", "ignore")) + except Exception as exc: + await _send_response(writer, 400, {"error": f"bad json: {exc}"}) + return + res = self._display_name_set((data.get("kind") or "").strip(), + (data.get("id") or "").strip(), + data.get("name") or "") + await _send_response(writer, 200, res) elif method == "POST" and path == "/internal/satellite-name": # Brain/Diagnostic: Geraetenamen vergeben. body: {satellite, mac|ip, name} try: @@ -5156,15 +5168,37 @@ class ARIABridge: def _satellite_list(self) -> list[dict]: """Bekannte Satelliten (frisch = in den letzten 5 Min gesehen).""" now = time.time() + aliases = self._display_names("satellite") out = [] for s in self._satellites.values(): + own = s.get("location") or s["id"] + alias = aliases.get(s["id"], "") out.append({ - "id": s["id"], "location": s.get("location") or s["id"], + "id": s["id"], "location": alias or own, + "agentName": own, "alias": alias, "caps": s.get("caps") or [], "control": bool(s.get("control")), "online": (now - s.get("last_seen", 0)) < 300, }) return out + def _resolve_satellite(self, who: str) -> str: + """Anzeigename ODER echte Kennung -> echte Kennung. + + Gleicher Grund wie bei _resolve_host: der Satellit vergleicht die + Adresse gegen seine eigene ID/Location und kennt den Alias nicht. + """ + w = (who or "").strip() + if not w or w in self._satellites: + return w + wl = w.lower() + for sid, alias in self._display_names("satellite").items(): + if alias.strip().lower() == wl: + return sid + for sid, s in self._satellites.items(): + if (s.get("location") or "").strip().lower() == wl: + return sid + return w + # worker_ping kommt alle ~10s; nach 35s ohne Ping gilt ein Worker als offline. WORKER_OFFLINE_S = 35 @@ -5228,6 +5262,8 @@ class ARIABridge: Muster identisch zu _flux_generate (requestId → Future).""" if self.ws_rvs is None: return {"ok": False, "error": "RVS-Verbindung nicht aktiv"} + # Anzeigename -> echte Kennung, sonst erkennt sich der Satellit nicht. + satellite = self._resolve_satellite(satellite) request_id = str(uuid.uuid4()) loop = asyncio.get_event_loop() future: asyncio.Future = loop.create_future() @@ -5255,15 +5291,113 @@ class ARIABridge: finally: self._pending_sat.pop(request_id, None) + # ── Anzeigenamen fuer Host-Agenten ─────────────────────────────── + # + # Der Name eines Agenten kommt von ihm selbst: beim Desktop-Agenten aus + # HOST_NAME in seiner .env, beim Android-Agenten aus der App-Konfiguration. + # Umbenennen hiesse also: auf den Rechner, Datei aendern, neu starten — bzw. + # beim Fire TV neu provisionieren. + # + # Darum hier ein Alias: hostId -> Anzeigename, zentral in der Bridge. Wirkt + # sofort und fuer JEDEN schon ausgerollten Agenten, ohne ihn anzufassen. + # + # WICHTIG: der Alias wird beim Adressieren wieder aufgeloest (siehe + # _resolve_host). Sonst koennte man einen Agenten zwar umbenennen, ihn unter + # dem neuen Namen aber nicht mehr ansprechen — er vergleicht intern gegen + # seine EIGENE Kennung. + # Eine Datei fuer beides: {"host": {id: name}, "satellite": {id: name}} + _DISPLAY_NAMES_FILE = "/shared/config/display_names.json" + + def _display_names(self, kind: str) -> dict: + try: + with open(self._DISPLAY_NAMES_FILE) as f: + data = json.load(f) + sub = (data or {}).get(kind) + return sub if isinstance(sub, dict) else {} + except FileNotFoundError: + return {} + except Exception as exc: + logger.warning("[names] display_names.json unlesbar: %s", exc) + return {} + + def _display_name_set(self, kind: str, ident: str, name: str) -> dict: + ident = (ident or "").strip() + if kind not in ("host", "satellite"): + return {"ok": False, "error": "kind muss host oder satellite sein"} + if not ident: + return {"ok": False, "error": "id ist Pflicht"} + try: + with open(self._DISPLAY_NAMES_FILE) as f: + data = json.load(f) + if not isinstance(data, dict): + data = {} + except Exception: + data = {} + sub = data.get(kind) if isinstance(data.get(kind), dict) else {} + if (name or "").strip(): + sub[ident] = (name or "").strip()[:60] + else: + sub.pop(ident, None) + data[kind] = sub + try: + # Verzeichnis aus dem Dateipfad ableiten statt fest zu verdrahten — + # sonst laesst sich _DISPLAY_NAMES_FILE nicht umbiegen (Test, anderes + # Deployment) und das Schreiben scheitert stillschweigend. + os.makedirs(os.path.dirname(self._DISPLAY_NAMES_FILE) or ".", exist_ok=True) + tmp = self._DISPLAY_NAMES_FILE + ".tmp" + with open(tmp, "w") as f: + json.dump(data, f, indent=2) + os.replace(tmp, self._DISPLAY_NAMES_FILE) + except Exception as exc: + return {"ok": False, "error": str(exc)} + logger.info("[names] %s %s -> %r", kind, ident, sub.get(ident) or "(entfernt)") + return {"ok": True, "kind": kind, "id": ident, "name": sub.get(ident, "")} + + def _host_names(self) -> dict: + return self._display_names("host") + + def _resolve_host(self, who: str) -> str: + """Anzeigename ODER echte Kennung -> echte Kennung. + + Ohne das waere ein umbenannter Agent nicht mehr ansprechbar: er + vergleicht die Adresse gegen seine eigene HOST_ID/HOST_NAME und kennt + den Alias nicht. + """ + w = (who or "").strip() + if not w: + return w + if w in self._hosts: + return w + wl = w.lower() + for hid, alias in self._host_names().items(): + if alias.strip().lower() == wl: + return hid + for hid, h in self._hosts.items(): + if (h.get("name") or "").strip().lower() == wl: + return hid + return w + def _host_list(self) -> list[dict]: """Bekannte Host-Agenten (frisch = in den letzten 5 Min gesehen).""" now = time.time() - return [{ - "hostId": h["hostId"], "name": h.get("name") or h["hostId"], - "os": h.get("os") or "", "caps": h.get("caps") or [], - "control": bool(h.get("control")), - "online": (now - h.get("last_seen", 0)) < 300, - } for h in self._hosts.values()] + aliases = self._host_names() + out = [] + for h in self._hosts.values(): + hid = h["hostId"] + own = h.get("name") or hid + alias = aliases.get(hid, "") + out.append({ + "hostId": hid, + "name": alias or own, + # Eigener Name des Agenten — damit in der Oberflaeche sichtbar + # bleibt, dass da ein Alias drueberliegt. + "agentName": own, + "alias": alias, + "os": h.get("os") or "", "caps": h.get("caps") or [], + "control": bool(h.get("control")), + "online": (now - h.get("last_seen", 0)) < 300, + }) + return out _AGENT_HISTORY_DIR = "/shared/config/agent_history" _AGENT_HISTORY_MAX = 200 @@ -5317,6 +5451,8 @@ class ARIABridge: host_result. Muster identisch zu _satellite_request.""" if self.ws_rvs is None: return {"ok": False, "error": "RVS-Verbindung nicht aktiv"} + # Anzeigename -> echte Kennung, sonst erkennt sich der Agent nicht. + host = self._resolve_host(host) request_id = str(uuid.uuid4()) loop = asyncio.get_event_loop() future: asyncio.Future = loop.create_future() diff --git a/diagnostic/index.html b/diagnostic/index.html index 4ba62ab..a83dc59 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -5045,7 +5045,10 @@ '