feat: OpenClaw HTTP bridge, HA conversation agent fixes, voice pipeline tooling
- Add openclaw-http-bridge.py: HTTP server translating POST requests to OpenClaw CLI calls - Add launchd plist for HTTP bridge (port 8081, auto-start) - Add install-to-docker-ha.sh: deploy custom component to Docker HA via SSH - Add package-for-ha.sh: create distributable tarball of custom component - Add test-services.sh: comprehensive voice pipeline service checker Fixes from code review: - Use OpenClawAgent (HTTP) in async_setup_entry instead of OpenClawCLIAgent (CLI agent fails inside Docker HA where openclaw binary doesn't exist) - Update all port references from 8080 to 8081 (HTTP bridge port) - Remove overly permissive CORS headers from HTTP bridge - Fix zombie process leak: kill child process on CLI timeout - Remove unused subprocess import in conversation.py - Add version field to Kokoro TTS Wyoming info - Update TODO.md with voice pipeline progress
This commit is contained in:
141
homeai-agent/openclaw-http-bridge.py
Normal file
141
homeai-agent/openclaw-http-bridge.py
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OpenClaw HTTP Bridge
|
||||
|
||||
A simple HTTP server that translates HTTP POST requests to OpenClaw CLI calls.
|
||||
This allows Home Assistant (running in Docker on a different machine) to
|
||||
communicate with OpenClaw via HTTP.
|
||||
|
||||
Usage:
|
||||
python3 openclaw-http-bridge.py [--port 8081]
|
||||
|
||||
Endpoints:
|
||||
POST /api/agent/message
|
||||
{
|
||||
"message": "Your message here",
|
||||
"agent": "main"
|
||||
}
|
||||
|
||||
Returns:
|
||||
{
|
||||
"response": "OpenClaw response text"
|
||||
}
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
class OpenClawBridgeHandler(BaseHTTPRequestHandler):
|
||||
"""HTTP request handler for OpenClaw bridge."""
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""Log requests to stderr."""
|
||||
print(f"[OpenClaw Bridge] {self.address_string()} - {format % args}")
|
||||
|
||||
def _send_json_response(self, status_code: int, data: dict):
|
||||
"""Send a JSON response."""
|
||||
self.send_response(status_code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data).encode())
|
||||
|
||||
def do_POST(self):
|
||||
"""Handle POST requests."""
|
||||
parsed_path = urlparse(self.path)
|
||||
|
||||
# Only handle the agent message endpoint
|
||||
if parsed_path.path != "/api/agent/message":
|
||||
self._send_json_response(404, {"error": "Not found"})
|
||||
return
|
||||
|
||||
# Read request body
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
if content_length == 0:
|
||||
self._send_json_response(400, {"error": "Empty request body"})
|
||||
return
|
||||
|
||||
try:
|
||||
body = self.rfile.read(content_length).decode()
|
||||
data = json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
self._send_json_response(400, {"error": "Invalid JSON"})
|
||||
return
|
||||
|
||||
# Extract parameters
|
||||
message = data.get("message", "").strip()
|
||||
agent = data.get("agent", "main")
|
||||
|
||||
if not message:
|
||||
self._send_json_response(400, {"error": "Message is required"})
|
||||
return
|
||||
|
||||
# Call OpenClaw CLI (use full path for launchd compatibility)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["/opt/homebrew/bin/openclaw", "agent", "--message", message, "--agent", agent],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
check=True
|
||||
)
|
||||
response_text = result.stdout.strip()
|
||||
self._send_json_response(200, {"response": response_text})
|
||||
except subprocess.TimeoutExpired:
|
||||
self._send_json_response(504, {"error": "OpenClaw command timed out"})
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = e.stderr.strip() if e.stderr else "OpenClaw command failed"
|
||||
self._send_json_response(500, {"error": error_msg})
|
||||
except FileNotFoundError:
|
||||
self._send_json_response(500, {"error": "OpenClaw CLI not found"})
|
||||
except Exception as e:
|
||||
self._send_json_response(500, {"error": str(e)})
|
||||
|
||||
def do_GET(self):
|
||||
"""Handle GET requests (health check)."""
|
||||
parsed_path = urlparse(self.path)
|
||||
|
||||
if parsed_path.path == "/status" or parsed_path.path == "/":
|
||||
self._send_json_response(200, {
|
||||
"status": "ok",
|
||||
"service": "OpenClaw HTTP Bridge",
|
||||
"version": "1.0.0"
|
||||
})
|
||||
else:
|
||||
self._send_json_response(404, {"error": "Not found"})
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the HTTP bridge server."""
|
||||
parser = argparse.ArgumentParser(description="OpenClaw HTTP Bridge")
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=8081,
|
||||
help="Port to listen on (default: 8081)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default="0.0.0.0",
|
||||
help="Host to bind to (default: 0.0.0.0)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
server = HTTPServer((args.host, args.port), OpenClawBridgeHandler)
|
||||
print(f"OpenClaw HTTP Bridge running on http://{args.host}:{args.port}")
|
||||
print(f"Endpoint: POST http://{args.host}:{args.port}/api/agent/message")
|
||||
print("Press Ctrl+C to stop")
|
||||
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down...")
|
||||
server.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user