From 5b952812ee105e41de9dc872aaf4c792763988cd Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 19 Sep 2026 12:09:52 +0200 Subject: [PATCH] fix(satellite): http.get liefert ganze Statusseiten + contains-Filter, kein Scan-Overhead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ARIA erreichte den Drucker per Satellit, bekam aber nur die ersten 2000 Zeichen (r.text[:2000]) — die Tintenbalken stehen weiter unten und fielen weg; ein Offset-Versuch wurde ignoriert (nicht unterstuetzt). Zudem lief vor JEDEM sat_command ein voller LAN-Scan, auch fuer http.get, das die URL direkt nutzt -> sehr lange Antwortzeiten. _do_http: - Body-Limit env-konfigurierbar (HTTP_MAX_CHARS Default 20000, harter Deckel HTTP_MAX_CHARS_HARD 200000) statt fixer 2000. - params.offset + params.max_chars zum Paginieren. - params.contains (String/Liste): nur Zeilen mit einem der Begriffe -> zieht aus einer grossen Statusseite gezielt die Tintenwerte, ohne Paging. - Antwort meldet total_chars/returned_chars/offset/truncated/filtered. - HTTP_TIMEOUT_SEC (Default 10s) statt fixer 6s. sat_command: LAN-Scan nur noch bei dial.launch oder wenn ein device-Ref mitkommt; http.get/http.post/wol nutzen die gecachte Liste -> deutlich schneller. agent.py + seed_rules.py: satellite_command-Tool + Drucker-Beispiel um contains/offset/max_chars ergaenzt, damit ARIA den Filter nutzt. Co-Authored-By: Claude Opus 4.8 --- aria-brain/agent.py | 11 +++++-- aria-brain/seed_rules.py | 9 ++++-- satellite/satellite.py | 64 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/aria-brain/agent.py b/aria-brain/agent.py index e92670d..483dc10 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -1220,8 +1220,15 @@ META_TOOLS = [ "Beispiel YouTube-Video auf Fire TV: action='dial.launch', " "device='Fire TV', params={'app':'YouTube','v':''}. Weitere " "Aktionen: 'wol' (params={'mac':'...'}) zum Aufwecken, " - "'http.get'/'http.post' (params={'url':'...'}) fuer lokale Webhooks. " - "Geht nur, wenn der Satellit Steuerung erlaubt (siehe satellite_list)." + "'http.get'/'http.post' (params={'url':'...'}) fuer lokale Webhooks UND " + "um Geraete-Statusseiten zu lesen (Drucker-Tinte, NAS ...). Bei grossen " + "Seiten NICHT blind paginieren: setze params['contains'] (String oder " + "Liste) — dann kommen nur Zeilen zurueck, die einen der Begriffe enthalten " + "(z.B. contains=['ink','toner','cyan','magenta','yellow','black','%'] fuer " + "Tinte). Zusaetzlich moeglich: params['offset'] und params['max_chars'] " + "(Default 20000). Die Antwort meldet total_chars + truncated, damit Du " + "siehst, ob noch mehr da ist. Geht nur, wenn der Satellit Steuerung " + "erlaubt (siehe satellite_list)." ), "parameters": { "type": "object", diff --git a/aria-brain/seed_rules.py b/aria-brain/seed_rules.py index e9d6ff7..28ac25e 100644 --- a/aria-brain/seed_rules.py +++ b/aria-brain/seed_rules.py @@ -493,8 +493,13 @@ SEED_RULES: List[dict] = [ "\n" "Beispiel 'Patronenstand vom Drucker zuhause': satellite_list -> Satellit im " "Heimnetz online? -> satellite_command(satellite='', action='http.get', " - "params={'url':'http:///general/status.html'}) -> Fuellstaende " - "(BK/C/M/Y) aus dem HTML vorlesen. NICHT aus dem Gedaechtnis raten.\n" + "params={'url':'http:///general/status.html', " + "'contains':['ink','toner','cyan','magenta','yellow','black','%']}) -> aus den " + "gefilterten Zeilen die Fuellstaende (BK/C/M/Y) vorlesen. Der contains-Filter " + "holt nur die relevanten Zeilen aus einer grossen Statusseite (sonst wird der " + "Body bei max_chars, Default 20000, abgeschnitten). Kommt nichts Brauchbares: " + "andere Statuspfade probieren (/general/information.html?kind=item, SNMP-UI) " + "oder offset erhoehen. NICHT aus dem Gedaechtnis raten.\n" "\n" "Beispiel 'spiel YouTube-Video auf dem Buero-Stick':\n" " satellite_command(satellite='Buero', device='Fire TV', " diff --git a/satellite/satellite.py b/satellite/satellite.py index d3d70bb..5c245da 100644 --- a/satellite/satellite.py +++ b/satellite/satellite.py @@ -121,6 +121,13 @@ SCAN_INTERVAL_SEC = int(os.environ.get("SCAN_INTERVAL_SEC", "300") or "300") DISCOVER_TIMEOUT_SEC = float(os.environ.get("DISCOVER_TIMEOUT_SEC", "6") or "6") DEVICE_CACHE_TTL_SEC = int(os.environ.get("DEVICE_CACHE_TTL_SEC", "120") or "120") +# http.get/http.post: Body-Ausschnitt. Default grosszuegig (ganze Statusseiten +# passen), mit hartem Deckel gegen Riesen-Payloads durchs RVS. offset/max_chars +# pro Request ueberschreibbar; contains-Filter zieht nur relevante Zeilen. +HTTP_TIMEOUT_SEC = float(os.environ.get("HTTP_TIMEOUT_SEC", "10") or "10") +HTTP_MAX_CHARS = int(os.environ.get("HTTP_MAX_CHARS", "20000") or "20000") +HTTP_MAX_CHARS_HARD = int(os.environ.get("HTTP_MAX_CHARS_HARD", "200000") or "200000") + HEARTBEAT_SEC = 25 # mDNS-Servicetypen, die fuer ARIA interessant sind. @@ -472,14 +479,60 @@ def _do_wol(params: dict) -> dict: def _do_http(action: str, params: dict) -> dict: + """HTTP-GET/POST vom Satelliten aus (lokale Webhooks, Geraete-Statusseiten …). + + params: + url Pflicht (http/https). + body/headers optional (POST). + offset ab welchem Zeichen der Body zurueckgegeben wird (Default 0). + max_chars wie viele Zeichen max. (Default HTTP_MAX_CHARS, hart gedeckelt). + contains String oder Liste: nur Zeilen, die (case-insensitive) einen der + Begriffe enthalten, werden zurueckgegeben. Ideal um aus einer + grossen Statusseite nur die relevanten Werte (z.B. Tinte) zu + ziehen, ohne die ganze Seite zu paginieren. + Antwort enthaelt total_chars + truncated, damit der Aufrufer weiss, ob noch + mehr da ist.""" import requests url = params.get("url") or "" if not url.startswith(("http://", "https://")): return {"ok": False, "error": "url (http/https) erforderlich."} method = "GET" if action == "http.get" else "POST" r = requests.request(method, url, data=params.get("body"), - headers=params.get("headers"), timeout=6) - return {"ok": True, "result": {"status": r.status_code, "body": r.text[:2000]}} + headers=params.get("headers"), timeout=HTTP_TIMEOUT_SEC) + text = r.text + total = len(text) + + contains = params.get("contains") + if contains: + terms = [contains] if isinstance(contains, str) else list(contains) + terms = [str(t).lower() for t in terms if str(t).strip()] + if terms: + lines = [ln for ln in text.splitlines() + if any(t in ln.lower() for t in terms)] + text = "\n".join(lines) + + try: + offset = max(0, int(params.get("offset", 0))) + except (TypeError, ValueError): + offset = 0 + try: + max_chars = int(params.get("max_chars", HTTP_MAX_CHARS)) + except (TypeError, ValueError): + max_chars = HTTP_MAX_CHARS + max_chars = max(1, min(max_chars, HTTP_MAX_CHARS_HARD)) + + body = text[offset:offset + max_chars] + returned_end = offset + len(body) + truncated = returned_end < len(text) + return {"ok": True, "result": { + "status": r.status_code, + "body": body, + "total_chars": total, # Groesse der Roh-Antwort + "filtered": bool(contains), # contains-Filter aktiv? + "offset": offset, + "returned_chars": len(body), + "truncated": truncated, # noch mehr Text nach diesem Ausschnitt? + }} # ─── Helpers ──────────────────────────────────────────────────────── @@ -597,7 +650,12 @@ class Satellite: params = payload.get("params") or {} if payload.get("device") and "device" not in params: params["device"] = payload.get("device") - devices = await self._scan() + # Geraeteliste nur scannen, wenn die Aktion sie wirklich braucht + # (dial.launch loest ein Geraet auf, oder es wurde ein device-Ref + # mitgegeben). http.get/http.post/wol arbeiten direkt mit url/mac — + # ein voller LAN-Scan davor kostete nur unnoetig viele Sekunden. + needs_devices = action == "dial.launch" or bool(params.get("device")) + devices = await self._scan() if needs_devices else self._devices result = await _control(action, params, devices) await self._send({ "type": "sat_result",