fix(satellite): http.get liefert ganze Statusseiten + contains-Filter, kein Scan-Overhead

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 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 12:09:52 +02:00
co-authored by Claude Opus 4.8
parent d0d06defb1
commit d6cca88455
3 changed files with 77 additions and 7 deletions
+61 -3
View File
@@ -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",