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:
techartdev
2026-02-23 11:49:21 +02:00
parent 8f82526bb9
commit 99aaef05c1
10 changed files with 150 additions and 17 deletions
+7 -3
View File
@@ -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,
)
+19
View File
@@ -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")
+36 -5
View File
@@ -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,
+1
View File
@@ -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
+1 -1
View File
@@ -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"]
}
+3 -3
View File
@@ -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."
},