diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ae4754..824800f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 98ddad1..b51eea3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/custom_components/openclaw/__init__.py b/custom_components/openclaw/__init__.py index 88bf02b..7963865 100644 --- a/custom_components/openclaw/__init__.py +++ b/custom_components/openclaw/__init__.py @@ -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, ) diff --git a/custom_components/openclaw/api.py b/custom_components/openclaw/api.py index d9198fb..6ede829 100644 --- a/custom_components/openclaw/api.py +++ b/custom_components/openclaw/api.py @@ -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") diff --git a/custom_components/openclaw/config_flow.py b/custom_components/openclaw/config_flow.py index d5ca526..ecd6351 100644 --- a/custom_components/openclaw/config_flow.py +++ b/custom_components/openclaw/config_flow.py @@ -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, diff --git a/custom_components/openclaw/const.py b/custom_components/openclaw/const.py index 24150e0..cb8da65 100644 --- a/custom_components/openclaw/const.py +++ b/custom_components/openclaw/const.py @@ -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 diff --git a/custom_components/openclaw/manifest.json b/custom_components/openclaw/manifest.json index 43cfd8a..5f7189f 100644 --- a/custom_components/openclaw/manifest.json +++ b/custom_components/openclaw/manifest.json @@ -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"] } diff --git a/custom_components/openclaw/strings.json b/custom_components/openclaw/strings.json index 9c4175c..a09321d 100644 --- a/custom_components/openclaw/strings.json +++ b/custom_components/openclaw/strings.json @@ -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": { diff --git a/custom_components/openclaw/translations/en.json b/custom_components/openclaw/translations/en.json index 9c4175c..4a51054 100644 --- a/custom_components/openclaw/translations/en.json +++ b/custom_components/openclaw/translations/en.json @@ -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." }, diff --git a/www/openclaw-chat-card.js b/www/openclaw-chat-card.js index 91de999..6dd9f29 100644 --- a/www/openclaw-chat-card.js +++ b/www/openclaw-chat-card.js @@ -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); }