feat(phase-04): Wyoming Satellite integration + OpenClaw HA components
## Voice Pipeline (P3) - Replace openWakeWord daemon with Wyoming Satellite approach - Add Wyoming Satellite service on port 10700 for HA voice pipeline - Update setup.sh with cross-platform sed compatibility (macOS/Linux) - Add version field to Kokoro TTS voice info - Update launchd service loader to use Wyoming Satellite ## Home Assistant Integration (P4) - Add custom conversation agent component (openclaw_conversation) - Fix: Use IntentResponse instead of plain strings (HA API requirement) - Support both HTTP API and CLI fallback modes - Config flow for easy HA UI setup - Add OpenClaw bridge scripts (Python + Bash) - Add ha-ctl utility for HA entity control - Fix: Use context manager for token file reading - Add HA configuration examples and documentation ## Infrastructure - Add mem0 backup automation (launchd + script) - Add n8n workflow templates (morning briefing, notification router) - Add VS Code workspace configuration - Reorganize model files into categorized folders: - lmstudio-community/ - mlx-community/ - bartowski/ - mradermacher/ ## Documentation - Update PROJECT_PLAN.md with Wyoming Satellite architecture - Update TODO.md with completed Wyoming integration tasks - Add OPENCLAW_INTEGRATION.md for HA setup guide ## Testing - Verified Wyoming services running (STT:10300, TTS:10301, Satellite:10700) - Verified OpenClaw CLI accessibility - Confirmed cross-platform compatibility fixes
This commit is contained in:
114
homeai-agent/custom_components/openclaw_conversation/README.md
Normal file
114
homeai-agent/custom_components/openclaw_conversation/README.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# OpenClaw Conversation - Home Assistant Custom Component
|
||||
|
||||
A custom conversation agent for Home Assistant that routes all voice/text queries to OpenClaw for processing.
|
||||
|
||||
## Features
|
||||
|
||||
- **Direct OpenClaw Integration**: Routes all conversation requests to OpenClaw
|
||||
- **CLI-based Communication**: Uses the `openclaw` CLI command (fallback if HTTP API unavailable)
|
||||
- **Configurable**: Set host, port, agent name, and timeout via UI
|
||||
- **Voice Pipeline Compatible**: Works with Home Assistant's voice assistant pipeline
|
||||
|
||||
## Installation
|
||||
|
||||
### Method 1: Manual Copy
|
||||
|
||||
1. Copy the entire `openclaw_conversation` folder to your Home Assistant `custom_components` directory:
|
||||
```bash
|
||||
# On the HA host (if using HA OS or Container, use the File Editor add-on)
|
||||
cp -r homeai-agent/custom_components/openclaw_conversation \
|
||||
/config/custom_components/
|
||||
```
|
||||
|
||||
2. Restart Home Assistant
|
||||
|
||||
3. Go to **Settings → Devices & Services → Add Integration**
|
||||
4. Search for "OpenClaw Conversation"
|
||||
5. Configure the settings:
|
||||
- **OpenClaw Host**: `localhost` (or IP of Mac Mini)
|
||||
- **OpenClaw Port**: `8080`
|
||||
- **Agent Name**: `main` (or your configured agent)
|
||||
- **Timeout**: `30` seconds
|
||||
|
||||
### Method 2: Using HACS (if available)
|
||||
|
||||
1. Add this repository to HACS as a custom repository
|
||||
2. Install "OpenClaw Conversation"
|
||||
3. Restart Home Assistant
|
||||
|
||||
## Configuration
|
||||
|
||||
### Via UI (Recommended)
|
||||
|
||||
After installation, configure via **Settings → Devices & Services → OpenClaw Conversation → Configure**.
|
||||
|
||||
### Via YAML (Alternative)
|
||||
|
||||
Add to your `configuration.yaml`:
|
||||
|
||||
```yaml
|
||||
openclaw_conversation:
|
||||
openclaw_host: localhost
|
||||
openclaw_port: 8080
|
||||
agent_name: main
|
||||
timeout: 30
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Once configured, the OpenClaw agent will be available as a conversation agent in Home Assistant.
|
||||
|
||||
### Setting as Default Agent
|
||||
|
||||
1. Go to **Settings → Voice Assistants**
|
||||
2. Edit your voice assistant pipeline
|
||||
3. Set **Conversation Agent** to "OpenClaw Conversation"
|
||||
4. Save
|
||||
|
||||
### Testing
|
||||
|
||||
1. Open the **Assist** panel in Home Assistant
|
||||
2. Type a query like: "Turn on the reading lamp"
|
||||
3. OpenClaw will process the request and return a response
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
[Voice Input] → [HA Voice Pipeline] → [OpenClaw Conversation Agent]
|
||||
↓
|
||||
[OpenClaw CLI/API]
|
||||
↓
|
||||
[Ollama LLM + Skills]
|
||||
↓
|
||||
[HA Actions + TTS Response]
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Agent Not Responding
|
||||
|
||||
1. Check OpenClaw is running: `pgrep -f openclaw`
|
||||
2. Test CLI directly: `openclaw agent --message "Hello" --agent main`
|
||||
3. Check HA logs: **Settings → System → Logs**
|
||||
|
||||
### Connection Errors
|
||||
|
||||
1. Verify OpenClaw host/port settings
|
||||
2. Ensure OpenClaw is accessible from HA container/host
|
||||
3. Check network connectivity: `curl http://localhost:8080/status`
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `manifest.json` | Component metadata |
|
||||
| `__init__.py` | Component setup and registration |
|
||||
| `config_flow.py` | Configuration UI flow |
|
||||
| `const.py` | Constants and defaults |
|
||||
| `conversation.py` | Conversation agent implementation |
|
||||
| `strings.json` | UI translations |
|
||||
|
||||
## See Also
|
||||
|
||||
- [OpenClaw Integration Guide](../../skills/home-assistant/OPENCLAW_INTEGRATION.md)
|
||||
- [Voice Pipeline Implementation](../../../plans/ha-voice-pipeline-implementation.md)
|
||||
@@ -0,0 +1,98 @@
|
||||
"""OpenClaw Conversation integration for Home Assistant."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import (
|
||||
CONF_AGENT_NAME,
|
||||
CONF_OPENCLAW_HOST,
|
||||
CONF_OPENCLAW_PORT,
|
||||
CONF_TIMEOUT,
|
||||
DEFAULT_AGENT,
|
||||
DEFAULT_HOST,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
)
|
||||
from .conversation import OpenClawCLIAgent
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.CONVERSATION]
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPENCLAW_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_OPENCLAW_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_AGENT_NAME, default=DEFAULT_AGENT): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
|
||||
"""Set up the OpenClaw Conversation component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
conf = config[DOMAIN]
|
||||
|
||||
# Store config
|
||||
hass.data[DOMAIN] = {
|
||||
"config": conf,
|
||||
}
|
||||
|
||||
# Register the conversation agent
|
||||
agent = OpenClawCLIAgent(hass, conf)
|
||||
|
||||
# Add to conversation agent registry
|
||||
from homeassistant.components import conversation
|
||||
conversation.async_set_agent(hass, DOMAIN, agent)
|
||||
|
||||
_LOGGER.info("OpenClaw Conversation agent registered")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up OpenClaw Conversation from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
# Store entry data
|
||||
hass.data[DOMAIN][entry.entry_id] = entry.data
|
||||
|
||||
# Register the conversation agent
|
||||
agent = OpenClawCLIAgent(hass, entry.data)
|
||||
|
||||
from homeassistant.components import conversation
|
||||
conversation.async_set_agent(hass, DOMAIN, agent)
|
||||
|
||||
_LOGGER.info("OpenClaw Conversation agent registered from config entry")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
# Unregister the conversation agent
|
||||
from homeassistant.components import conversation
|
||||
conversation.async_unset_agent(hass, DOMAIN)
|
||||
|
||||
hass.data[DOMAIN].pop(entry.entry_id, None)
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Config flow for OpenClaw Conversation integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import (
|
||||
CONF_AGENT_NAME,
|
||||
CONF_OPENCLAW_HOST,
|
||||
CONF_OPENCLAW_PORT,
|
||||
CONF_TIMEOUT,
|
||||
DEFAULT_AGENT,
|
||||
DEFAULT_HOST,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPENCLAW_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_OPENCLAW_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_AGENT_NAME, default=DEFAULT_AGENT): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for OpenClaw Conversation."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
# Validate the input
|
||||
try:
|
||||
# Test connection to OpenClaw
|
||||
await self._test_openclaw_connection(
|
||||
user_input[CONF_OPENCLAW_HOST],
|
||||
user_input[CONF_OPENCLAW_PORT],
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title="OpenClaw Conversation",
|
||||
data=user_input,
|
||||
)
|
||||
except ConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def _test_openclaw_connection(self, host: str, port: int) -> None:
|
||||
"""Test if OpenClaw is reachable."""
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
# Try to connect to OpenClaw
|
||||
reader, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(host, port),
|
||||
timeout=5,
|
||||
)
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
except Exception as err:
|
||||
raise ConnectionError(f"Cannot connect to OpenClaw: {err}")
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
) -> OptionsFlow:
|
||||
"""Create the options flow."""
|
||||
return OptionsFlow(config_entry)
|
||||
|
||||
|
||||
class OptionsFlow(config_entries.OptionsFlow):
|
||||
"""Handle options flow for OpenClaw Conversation."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage the options."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
options = {
|
||||
vol.Optional(
|
||||
CONF_AGENT_NAME,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_AGENT_NAME, DEFAULT_AGENT
|
||||
),
|
||||
): cv.string,
|
||||
vol.Optional(
|
||||
CONF_TIMEOUT,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_TIMEOUT, DEFAULT_TIMEOUT
|
||||
),
|
||||
): cv.positive_int,
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(options),
|
||||
errors=errors,
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Constants for OpenClaw Conversation integration."""
|
||||
|
||||
DOMAIN = "openclaw_conversation"
|
||||
|
||||
# Configuration keys
|
||||
CONF_OPENCLAW_HOST = "openclaw_host"
|
||||
CONF_OPENCLAW_PORT = "openclaw_port"
|
||||
CONF_AGENT_NAME = "agent_name"
|
||||
CONF_TIMEOUT = "timeout"
|
||||
|
||||
# Defaults
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_PORT = 8080
|
||||
DEFAULT_AGENT = "main"
|
||||
DEFAULT_TIMEOUT = 30
|
||||
|
||||
# API endpoints
|
||||
OPENCLAW_API_PATH = "/api/agent/message"
|
||||
|
||||
# Service names
|
||||
SERVICE_PROCESS = "process"
|
||||
|
||||
# Attributes
|
||||
ATTR_MESSAGE = "message"
|
||||
ATTR_RESPONSE = "response"
|
||||
ATTR_AGENT = "agent"
|
||||
@@ -0,0 +1,222 @@
|
||||
"""Conversation agent for OpenClaw integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.conversation import (
|
||||
AbstractConversationAgent,
|
||||
ConversationInput,
|
||||
ConversationResult,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.intent import IntentResponse
|
||||
|
||||
from .const import (
|
||||
CONF_AGENT_NAME,
|
||||
CONF_OPENCLAW_HOST,
|
||||
CONF_OPENCLAW_PORT,
|
||||
CONF_TIMEOUT,
|
||||
DEFAULT_AGENT,
|
||||
DEFAULT_HOST,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
OPENCLAW_API_PATH,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Schema for configuration
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_OPENCLAW_HOST, default=DEFAULT_HOST): str,
|
||||
vol.Optional(CONF_OPENCLAW_PORT, default=DEFAULT_PORT): int,
|
||||
vol.Optional(CONF_AGENT_NAME, default=DEFAULT_AGENT): str,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): int,
|
||||
})
|
||||
|
||||
|
||||
class OpenClawAgent(AbstractConversationAgent):
|
||||
"""OpenClaw conversation agent."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None:
|
||||
"""Initialize the agent."""
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
self.host = config.get(CONF_OPENCLAW_HOST, DEFAULT_HOST)
|
||||
self.port = config.get(CONF_OPENCLAW_PORT, DEFAULT_PORT)
|
||||
self.agent_name = config.get(CONF_AGENT_NAME, DEFAULT_AGENT)
|
||||
self.timeout = config.get(CONF_TIMEOUT, DEFAULT_TIMEOUT)
|
||||
|
||||
@property
|
||||
def supported_languages(self) -> list[str]:
|
||||
"""Return a list of supported languages."""
|
||||
return ["en"] # OpenClaw primarily supports English
|
||||
|
||||
@property
|
||||
def attribution(self) -> dict[str, str] | None:
|
||||
"""Return attribution information."""
|
||||
return {
|
||||
"name": "OpenClaw",
|
||||
"url": "https://github.com/homeai/openclaw",
|
||||
}
|
||||
|
||||
async def async_process(
|
||||
self, user_input: ConversationInput
|
||||
) -> ConversationResult:
|
||||
"""Process a sentence and return a response."""
|
||||
text = user_input.text
|
||||
conversation_id = user_input.conversation_id
|
||||
|
||||
_LOGGER.debug("Processing message: %s", text)
|
||||
|
||||
try:
|
||||
response_text = await self._call_openclaw(text)
|
||||
|
||||
# Create proper IntentResponse for Home Assistant
|
||||
intent_response = IntentResponse(language=user_input.language or "en")
|
||||
intent_response.async_set_speech(response_text)
|
||||
|
||||
return ConversationResult(
|
||||
response=intent_response,
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
except Exception as err:
|
||||
_LOGGER.error("Error calling OpenClaw: %s", err)
|
||||
intent_response = IntentResponse(language=user_input.language or "en")
|
||||
intent_response.async_set_speech("I'm sorry, I encountered an error processing your request.")
|
||||
return ConversationResult(
|
||||
response=intent_response,
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
|
||||
async def _call_openclaw(self, message: str) -> str:
|
||||
"""Call OpenClaw API and return the response."""
|
||||
url = f"http://{self.host}:{self.port}{OPENCLAW_API_PATH}"
|
||||
|
||||
payload = {
|
||||
"message": message,
|
||||
"agent": self.agent_name,
|
||||
}
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(self.timeout):
|
||||
async with session.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
_LOGGER.error(
|
||||
"OpenClaw returned status %s: %s",
|
||||
response.status,
|
||||
await response.text(),
|
||||
)
|
||||
return "I'm sorry, I couldn't process that request."
|
||||
|
||||
data = await response.json()
|
||||
return data.get("response", "I didn't get a response.")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error("Timeout calling OpenClaw")
|
||||
return "I'm sorry, the request timed out."
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error("Error connecting to OpenClaw: %s", err)
|
||||
return "I'm sorry, I couldn't connect to the OpenClaw service."
|
||||
|
||||
|
||||
class OpenClawCLIAgent(AbstractConversationAgent):
|
||||
"""OpenClaw conversation agent using CLI (fallback if HTTP API unavailable)."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None:
|
||||
"""Initialize the agent."""
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
self.agent_name = config.get(CONF_AGENT_NAME, DEFAULT_AGENT)
|
||||
self.timeout = config.get(CONF_TIMEOUT, DEFAULT_TIMEOUT)
|
||||
|
||||
@property
|
||||
def supported_languages(self) -> list[str]:
|
||||
"""Return a list of supported languages."""
|
||||
return ["en"]
|
||||
|
||||
@property
|
||||
def attribution(self) -> dict[str, str] | None:
|
||||
"""Return attribution information."""
|
||||
return {
|
||||
"name": "OpenClaw",
|
||||
"url": "https://github.com/homeai/openclaw",
|
||||
}
|
||||
|
||||
async def async_process(
|
||||
self, user_input: ConversationInput
|
||||
) -> ConversationResult:
|
||||
"""Process a sentence using OpenClaw CLI."""
|
||||
text = user_input.text
|
||||
conversation_id = user_input.conversation_id
|
||||
|
||||
_LOGGER.debug("Processing message via CLI: %s", text)
|
||||
|
||||
try:
|
||||
response_text = await self._call_openclaw_cli(text)
|
||||
|
||||
# Create proper IntentResponse for Home Assistant
|
||||
intent_response = IntentResponse(language=user_input.language or "en")
|
||||
intent_response.async_set_speech(response_text)
|
||||
|
||||
return ConversationResult(
|
||||
response=intent_response,
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
except Exception as err:
|
||||
_LOGGER.error("Error calling OpenClaw CLI: %s", err)
|
||||
intent_response = IntentResponse(language=user_input.language or "en")
|
||||
intent_response.async_set_speech("I'm sorry, I encountered an error processing your request.")
|
||||
return ConversationResult(
|
||||
response=intent_response,
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
|
||||
async def _call_openclaw_cli(self, message: str) -> str:
|
||||
"""Call OpenClaw CLI and return the response."""
|
||||
import subprocess
|
||||
|
||||
cmd = [
|
||||
"openclaw",
|
||||
"agent",
|
||||
"--message", message,
|
||||
"--agent", self.agent_name,
|
||||
]
|
||||
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
proc.communicate(),
|
||||
timeout=self.timeout,
|
||||
)
|
||||
|
||||
if proc.returncode != 0:
|
||||
_LOGGER.error("OpenClaw CLI failed: %s", stderr.decode().strip())
|
||||
return "I'm sorry, I couldn't process that request."
|
||||
|
||||
return stdout.decode().strip()
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error("Timeout calling OpenClaw CLI")
|
||||
return "I'm sorry, the request timed out."
|
||||
except FileNotFoundError:
|
||||
_LOGGER.error("OpenClaw CLI not found")
|
||||
return "I'm sorry, OpenClaw is not available."
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "openclaw_conversation",
|
||||
"name": "OpenClaw Conversation",
|
||||
"codeowners": ["@homeai"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["conversation"],
|
||||
"documentation": "https://github.com/homeai/homeai-agent",
|
||||
"iot_class": "local_push",
|
||||
"requirements": [],
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"title": "OpenClaw Conversation",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "OpenClaw Conversation Setup",
|
||||
"description": "Configure the OpenClaw conversation agent.",
|
||||
"data": {
|
||||
"openclaw_host": "OpenClaw Host",
|
||||
"openclaw_port": "OpenClaw Port",
|
||||
"agent_name": "Agent Name",
|
||||
"timeout": "Timeout (seconds)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect to OpenClaw. Please check the host and port.",
|
||||
"unknown": "Unexpected error occurred."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "OpenClaw Conversation is already configured."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "OpenClaw Conversation Options",
|
||||
"data": {
|
||||
"agent_name": "Agent Name",
|
||||
"timeout": "Timeout (seconds)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user