Add HTTPS/SSL support and enhance connection handling
- Implemented HTTPS/SSL support for OpenClaw gateways in `lan_https` mode. - Added configuration options for SSL certificate verification. - Improved auto-discovery for `lan_https` access mode. - Updated documentation in README and changelog for new features and fixes. - Fixed connection errors related to SSL certificate verification.
This commit is contained in:
@@ -2,6 +2,18 @@
|
||||
|
||||
All notable changes to the OpenClaw Home Assistant Integration will be documented in this file.
|
||||
|
||||
## [0.1.52] - 2026-02-23
|
||||
|
||||
### Added
|
||||
- Added HTTPS / SSL support for connecting to OpenClaw gateways running in `lan_https` mode or behind TLS reverse proxies.
|
||||
- Auto-discovery now detects `access_mode: lan_https` and connects to the internal gateway port automatically (no certificate setup needed for local addons).
|
||||
- Added `Verify SSL certificate` option in manual config for self-signed certificate environments.
|
||||
- Added `ssl_error` config flow error with actionable guidance.
|
||||
- Added comprehensive remote connection documentation to README with setup table for all access modes.
|
||||
|
||||
### Fixed
|
||||
- Fixed "400 Bad Request — plain HTTP request was sent to HTTPS port" when the addon uses `lan_https` access mode.
|
||||
|
||||
## [0.1.51] - 2026-02-23
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -38,10 +38,68 @@ OpenClaw is a Home Assistant custom integration that connects your HA instance t
|
||||
## Requirements
|
||||
|
||||
- Home Assistant Core `2025.1.0+` (declared minimum)
|
||||
- Supervisor is optional (used for auto-discovery)
|
||||
- OpenClaw Assistant addon installed and running
|
||||
- An **OpenClaw gateway** with `enable_openai_api` enabled — either:
|
||||
- The [OpenClaw Assistant addon](https://github.com/techartdev/OpenClawHomeAssistant) running on the same HA instance (auto-discovery supported), **or**
|
||||
- Any standalone [OpenClaw](https://github.com/openclaw/openclaw) installation reachable over the network (manual config)
|
||||
- Supervisor is optional (used only for addon auto-discovery)
|
||||
|
||||
The integration can auto-detect the addon when Supervisor is available. You can always configure host/port/token manually.
|
||||
> **No addon required.** If you have OpenClaw running anywhere — on a separate server, a VPS, a Docker container, or even another machine on your LAN — this integration can connect to it via the manual configuration flow.
|
||||
|
||||
---
|
||||
|
||||
## Connection modes
|
||||
|
||||
The integration supports connecting to OpenClaw in several ways:
|
||||
|
||||
### Local addon (auto-discovery)
|
||||
|
||||
If the OpenClaw Assistant addon is installed on the **same** Home Assistant instance, the integration auto-discovers it:
|
||||
- Reads token from the shared filesystem
|
||||
- Detects `access_mode` and chooses the correct port automatically
|
||||
- No manual config needed — just click **Submit** on the confirm step
|
||||
|
||||
> **`lan_https` mode**: The integration automatically connects to the internal gateway port (plain HTTP on loopback), bypassing the HTTPS proxy entirely. No certificate setup required.
|
||||
|
||||
### Remote or standalone OpenClaw instance (manual config)
|
||||
|
||||
You can connect to **any reachable OpenClaw gateway** — whether it's the HA addon on another machine, a standalone `openclaw` install on a VPS, or a Docker container on your LAN. The integration doesn't care how OpenClaw is installed; it only needs the `/v1/chat/completions` endpoint.
|
||||
|
||||
**Prerequisites on the OpenClaw instance:**
|
||||
|
||||
1. The OpenAI-compatible API must be **enabled**:
|
||||
- **Addon users**: Set `enable_openai_api: true` in addon settings
|
||||
- **Standalone users**: Set `gateway.http.endpoints.chatCompletions.enabled: true` in `openclaw.json`, or run:
|
||||
```sh
|
||||
openclaw config set gateway.http.endpoints.chatCompletions.enabled true
|
||||
```
|
||||
2. The gateway must be **network-reachable** from your HA instance (not bound to loopback only)
|
||||
3. You need the **gateway auth token**:
|
||||
```sh
|
||||
openclaw config get gateway.auth.token
|
||||
```
|
||||
|
||||
**Setup steps:**
|
||||
|
||||
1. Go to **Settings → Devices & Services → Add Integration → OpenClaw**
|
||||
2. Auto-discovery will fail (no local addon) — you'll see the **Manual Configuration** form
|
||||
3. Fill in:
|
||||
- **Gateway Host**: IP or hostname of the remote machine (e.g. `192.168.1.50`)
|
||||
- **Gateway Port**: The gateway port (default `18789`)
|
||||
- **Gateway Token**: Auth token from the remote `openclaw.json`
|
||||
- **Use SSL (HTTPS)**: Check if connecting to an HTTPS endpoint
|
||||
- **Verify SSL certificate**: Uncheck for self-signed certificates (e.g. `lan_https` mode)
|
||||
|
||||
### Common remote scenarios
|
||||
|
||||
| Remote access mode | Host | Port | Use SSL | Verify SSL | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| Standalone OpenClaw (plain HTTP on LAN) | Remote IP | 18789 | ❌ | — | Default `openclaw gateway run` config |
|
||||
| `lan_https` (addon built-in HTTPS proxy) | Remote IP | 18789 | ✅ | ❌ | Self-signed cert; disable verification |
|
||||
| Behind reverse proxy (NPM/Caddy with Let's Encrypt) | Domain or IP | 443 | ✅ | ✅ | Trusted cert from a real CA |
|
||||
| Plain HTTP addon on LAN | Remote IP | 18789 | ❌ | — | Addon `bind_mode` must be `lan` |
|
||||
| Tailscale | Tailscale IP | 18789 | ❌ | — | Encrypted tunnel; plain HTTP is fine |
|
||||
|
||||
> **Security note**: Avoid exposing plain HTTP gateways to the public internet. Use `lan_https`, a reverse proxy with TLS, or Tailscale for remote access.
|
||||
|
||||
---
|
||||
|
||||
@@ -307,6 +365,12 @@ action:
|
||||
- Verify `openclaw_message_received` is being fired in Developer Tools → Events
|
||||
- Confirm session IDs match between card and service calls
|
||||
|
||||
### "400 Bad Request — plain HTTP request was sent to HTTPS port"
|
||||
|
||||
- The gateway is running in `lan_https` mode (built-in HTTPS proxy)
|
||||
- **Local addon**: Remove and re-add the integration — auto-discovery now detects `lan_https` and uses the correct internal port automatically
|
||||
- **Remote connection**: Enable **Use SSL (HTTPS)** and disable **Verify SSL certificate** in the manual config
|
||||
|
||||
---
|
||||
|
||||
## Development notes
|
||||
|
||||
@@ -51,6 +51,7 @@ from .const import (
|
||||
CONF_GATEWAY_PORT,
|
||||
CONF_GATEWAY_TOKEN,
|
||||
CONF_USE_SSL,
|
||||
CONF_VERIFY_SSL,
|
||||
CONF_CONTEXT_MAX_CHARS,
|
||||
CONF_CONTEXT_STRATEGY,
|
||||
CONF_ENABLE_TOOL_CALLS,
|
||||
@@ -92,7 +93,7 @@ _CARD_PATH = Path(__file__).parent / "www" / _CARD_FILENAME
|
||||
# URL at which the card JS is served (registered via register_static_path)
|
||||
_CARD_STATIC_URL = f"/openclaw/{_CARD_FILENAME}"
|
||||
# Versioned URL used for Lovelace resource registration to avoid stale browser cache
|
||||
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.51"
|
||||
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.52"
|
||||
|
||||
OpenClawConfigEntry = ConfigEntry
|
||||
|
||||
@@ -130,13 +131,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: OpenClawConfigEntry) ->
|
||||
|
||||
Creates the API client, coordinator, and forwards setup to platforms.
|
||||
"""
|
||||
session = async_get_clientsession(hass)
|
||||
use_ssl = entry.data.get(CONF_USE_SSL, False)
|
||||
verify_ssl = entry.data.get(CONF_VERIFY_SSL, True)
|
||||
session = async_get_clientsession(hass, verify_ssl=verify_ssl)
|
||||
|
||||
client = OpenClawApiClient(
|
||||
host=entry.data[CONF_GATEWAY_HOST],
|
||||
port=entry.data[CONF_GATEWAY_PORT],
|
||||
token=entry.data[CONF_GATEWAY_TOKEN],
|
||||
use_ssl=entry.data.get(CONF_USE_SSL, False),
|
||||
use_ssl=use_ssl,
|
||||
verify_ssl=verify_ssl,
|
||||
session=session,
|
||||
)
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ class OpenClawApiClient:
|
||||
port: int,
|
||||
token: str,
|
||||
use_ssl: bool = False,
|
||||
verify_ssl: bool = True,
|
||||
session: aiohttp.ClientSession | None = None,
|
||||
) -> None:
|
||||
"""Initialize the API client.
|
||||
@@ -58,14 +59,19 @@ class OpenClawApiClient:
|
||||
port: Gateway port number.
|
||||
token: Authentication token from openclaw.json.
|
||||
use_ssl: Use HTTPS instead of HTTP.
|
||||
verify_ssl: Verify SSL certificates (set False for self-signed certs).
|
||||
session: Optional aiohttp session (reused from HA).
|
||||
"""
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._token = token
|
||||
self._use_ssl = use_ssl
|
||||
self._verify_ssl = verify_ssl
|
||||
self._session = session
|
||||
self._base_url = f"{'https' if use_ssl else 'http'}://{host}:{port}"
|
||||
# ssl=False disables cert verification for self-signed certs;
|
||||
# ssl=None uses default verification.
|
||||
self._ssl_param: bool | None = False if (use_ssl and not verify_ssl) else None
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
@@ -121,6 +127,7 @@ class OpenClawApiClient:
|
||||
url,
|
||||
headers=self._headers(),
|
||||
timeout=timeout,
|
||||
ssl=self._ssl_param,
|
||||
**kwargs,
|
||||
) as resp:
|
||||
if resp.status == 401:
|
||||
@@ -146,6 +153,13 @@ class OpenClawApiClient:
|
||||
)
|
||||
return await resp.json()
|
||||
|
||||
except aiohttp.ClientConnectorCertificateError as err:
|
||||
raise OpenClawConnectionError(
|
||||
f"SSL certificate verification failed for {url}. "
|
||||
f"If using self-signed certificates (e.g. lan_https mode), "
|
||||
f"disable 'Verify SSL certificate' in the integration config. "
|
||||
f"Error: {err}"
|
||||
) from err
|
||||
except (aiohttp.ClientConnectorError, aiohttp.ClientOSError, asyncio.TimeoutError) as err:
|
||||
raise OpenClawConnectionError(
|
||||
f"Cannot connect to OpenClaw gateway at {url}: {err}"
|
||||
@@ -213,6 +227,7 @@ class OpenClawApiClient:
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=STREAM_TIMEOUT,
|
||||
ssl=self._ssl_param,
|
||||
) as resp:
|
||||
if resp.status == 401:
|
||||
raise OpenClawAuthError("Authentication failed")
|
||||
@@ -274,6 +289,7 @@ class OpenClawApiClient:
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=STREAM_TIMEOUT,
|
||||
ssl=self._ssl_param,
|
||||
) as resp:
|
||||
if resp.status == 401:
|
||||
raise OpenClawAuthError("Authentication failed")
|
||||
@@ -338,6 +354,7 @@ class OpenClawApiClient:
|
||||
headers=self._headers(),
|
||||
json={"messages": [], "stream": False},
|
||||
timeout=API_TIMEOUT,
|
||||
ssl=self._ssl_param,
|
||||
) as resp:
|
||||
if resp.status in (401, 403):
|
||||
raise OpenClawAuthError(
|
||||
@@ -381,6 +398,7 @@ class OpenClawApiClient:
|
||||
async with session.get(
|
||||
self._base_url,
|
||||
timeout=API_TIMEOUT,
|
||||
ssl=self._ssl_param,
|
||||
) as resp:
|
||||
return resp.status < 500
|
||||
except (aiohttp.ClientConnectorError, aiohttp.ClientOSError, asyncio.TimeoutError) as err:
|
||||
@@ -424,6 +442,7 @@ class OpenClawApiClient:
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=STREAM_TIMEOUT,
|
||||
ssl=self._ssl_param,
|
||||
) as resp:
|
||||
if resp.status == 401:
|
||||
raise OpenClawAuthError("Authentication failed")
|
||||
|
||||
@@ -38,6 +38,7 @@ from .const import (
|
||||
CONF_GATEWAY_PORT,
|
||||
CONF_GATEWAY_TOKEN,
|
||||
CONF_USE_SSL,
|
||||
CONF_VERIFY_SSL,
|
||||
CONF_CONTEXT_MAX_CHARS,
|
||||
CONF_CONTEXT_STRATEGY,
|
||||
CONF_ENABLE_TOOL_CALLS,
|
||||
@@ -233,11 +234,30 @@ async def _async_try_discover_addon(hass: HomeAssistant) -> dict[str, Any] | Non
|
||||
if port is None:
|
||||
port = DEFAULT_GATEWAY_PORT
|
||||
|
||||
# Detect access mode — lan_https uses a built-in HTTPS reverse proxy.
|
||||
# In that mode nginx terminates TLS on the external (user-facing) port
|
||||
# and the gateway itself listens on port+1 on loopback (plain HTTP).
|
||||
# Since HA and the addon share host_network, we connect to the internal
|
||||
# port via loopback to avoid self-signed certificate issues entirely.
|
||||
access_mode = addon_options.get("access_mode", "custom")
|
||||
use_ssl = False
|
||||
verify_ssl = True
|
||||
|
||||
if access_mode == "lan_https":
|
||||
_LOGGER.info(
|
||||
"Detected access_mode=lan_https — using internal gateway port %d "
|
||||
"(external HTTPS proxy is on port %d)",
|
||||
port + 1,
|
||||
port,
|
||||
)
|
||||
port = port + 1
|
||||
|
||||
return {
|
||||
CONF_GATEWAY_HOST: DEFAULT_GATEWAY_HOST,
|
||||
CONF_GATEWAY_PORT: port,
|
||||
CONF_GATEWAY_TOKEN: token,
|
||||
CONF_USE_SSL: False,
|
||||
CONF_USE_SSL: use_ssl,
|
||||
CONF_VERIFY_SSL: verify_ssl,
|
||||
CONF_ADDON_CONFIG_PATH: str(config_dir),
|
||||
}
|
||||
|
||||
@@ -248,14 +268,16 @@ async def _async_validate_connection(
|
||||
port: int,
|
||||
token: str,
|
||||
use_ssl: bool = False,
|
||||
verify_ssl: bool = True,
|
||||
) -> bool:
|
||||
"""Validate that we can connect and authenticate to the gateway."""
|
||||
session = async_get_clientsession(hass)
|
||||
session = async_get_clientsession(hass, verify_ssl=verify_ssl)
|
||||
client = OpenClawApiClient(
|
||||
host=host,
|
||||
port=port,
|
||||
token=token,
|
||||
use_ssl=use_ssl,
|
||||
verify_ssl=verify_ssl,
|
||||
session=session,
|
||||
)
|
||||
return await client.async_check_connection()
|
||||
@@ -312,6 +334,7 @@ class OpenClawConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._discovered[CONF_GATEWAY_PORT],
|
||||
self._discovered[CONF_GATEWAY_TOKEN],
|
||||
self._discovered.get(CONF_USE_SSL, False),
|
||||
self._discovered.get(CONF_VERIFY_SSL, True),
|
||||
)
|
||||
except OpenClawAuthError:
|
||||
connected = False
|
||||
@@ -355,17 +378,23 @@ class OpenClawConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
port = user_input[CONF_GATEWAY_PORT]
|
||||
token = user_input[CONF_GATEWAY_TOKEN]
|
||||
use_ssl = user_input.get(CONF_USE_SSL, False)
|
||||
verify_ssl = user_input.get(CONF_VERIFY_SSL, True)
|
||||
|
||||
try:
|
||||
connected = await _async_validate_connection(
|
||||
self.hass, host, port, token, use_ssl
|
||||
self.hass, host, port, token, use_ssl, verify_ssl
|
||||
)
|
||||
except OpenClawAuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
connected = False
|
||||
except OpenClawConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except OpenClawConnectionError as err:
|
||||
err_msg = str(err).lower()
|
||||
if "ssl" in err_msg or "certificate" in err_msg:
|
||||
errors["base"] = "ssl_error"
|
||||
else:
|
||||
errors["base"] = "cannot_connect"
|
||||
connected = False
|
||||
_LOGGER.warning("Connection error during config check: %s", err)
|
||||
except OpenClawApiError as err:
|
||||
errors["base"] = "openai_api_disabled"
|
||||
connected = False
|
||||
@@ -379,6 +408,7 @@ class OpenClawConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_GATEWAY_PORT: port,
|
||||
CONF_GATEWAY_TOKEN: token,
|
||||
CONF_USE_SSL: use_ssl,
|
||||
CONF_VERIFY_SSL: verify_ssl,
|
||||
},
|
||||
)
|
||||
if "base" not in errors:
|
||||
@@ -396,6 +426,7 @@ class OpenClawConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
): vol.All(int, vol.Range(min=1, max=65535)),
|
||||
vol.Required(CONF_GATEWAY_TOKEN): str,
|
||||
vol.Optional(CONF_USE_SSL, default=False): bool,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): bool,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
|
||||
@@ -21,6 +21,7 @@ CONF_GATEWAY_HOST = "gateway_host"
|
||||
CONF_GATEWAY_PORT = "gateway_port"
|
||||
CONF_GATEWAY_TOKEN = "gateway_token"
|
||||
CONF_USE_SSL = "use_ssl"
|
||||
CONF_VERIFY_SSL = "verify_ssl"
|
||||
CONF_ADDON_CONFIG_PATH = "addon_config_path"
|
||||
|
||||
# Options
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"issue_tracker": "https://github.com/techartdev/OpenClawHomeAssistant/issues",
|
||||
"requirements": [],
|
||||
"version": "0.1.51",
|
||||
"version": "0.1.52",
|
||||
"dependencies": ["conversation"],
|
||||
"after_dependencies": ["hassio", "lovelace"]
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
"gateway_host": "Gateway Host",
|
||||
"gateway_port": "Gateway Port",
|
||||
"gateway_token": "Gateway Token",
|
||||
"use_ssl": "Use SSL (HTTPS)"
|
||||
"use_ssl": "Use SSL (HTTPS)",
|
||||
"verify_ssl": "Verify SSL certificate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Cannot connect to the OpenClaw gateway. Ensure the addon is running.",
|
||||
"invalid_auth": "Invalid gateway token. Check your OpenClaw configuration.",
|
||||
"openai_api_disabled": "The gateway returned an unexpected response — the OpenAI-compatible API is likely disabled. In the OpenClaw addon settings enable 'enable_openai_api', restart the addon, and try again.",
|
||||
"invalid_auth": "Invalid gateway token. Check your OpenClaw configuration.", "ssl_error": "SSL certificate verification failed. If using self-signed certificates (e.g. lan_https mode), uncheck 'Verify SSL certificate' or use automatic discovery.", "openai_api_disabled": "The gateway returned an unexpected response — the OpenAI-compatible API is likely disabled. In the OpenClaw addon settings enable 'enable_openai_api', restart the addon, and try again.",
|
||||
"unknown": "An unexpected error occurred."
|
||||
},
|
||||
"abort": {
|
||||
|
||||
@@ -17,13 +17,15 @@
|
||||
"gateway_host": "Gateway Host",
|
||||
"gateway_port": "Gateway Port",
|
||||
"gateway_token": "Gateway Token",
|
||||
"use_ssl": "Use SSL (HTTPS)"
|
||||
"use_ssl": "Use SSL (HTTPS)",
|
||||
"verify_ssl": "Verify SSL certificate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Cannot connect to the OpenClaw gateway. Ensure the addon is running.",
|
||||
"invalid_auth": "Invalid gateway token. Check your OpenClaw configuration.",
|
||||
"ssl_error": "SSL certificate verification failed. If using self-signed certificates (e.g. lan_https mode), uncheck 'Verify SSL certificate' or use automatic discovery.",
|
||||
"openai_api_disabled": "The gateway returned an unexpected response — the OpenAI-compatible API is likely disabled. In the OpenClaw addon settings enable 'enable_openai_api', restart the addon, and try again.",
|
||||
"unknown": "An unexpected error occurred."
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
(async () => {
|
||||
try {
|
||||
if (!customElements.get("openclaw-chat-card")) {
|
||||
const src = "/openclaw/openclaw-chat-card.js?v=0.1.51";
|
||||
const src = "/openclaw/openclaw-chat-card.js?v=0.1.52";
|
||||
console.info("OpenClaw loader importing", src);
|
||||
await import(src);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user