456 files

This commit is contained in:
Home Assistant Version Control
2026-10-01 05:38:21 +00:00
parent 4192ecbaae
commit 5d6d845ebf
456 changed files with 64951 additions and 3828 deletions
+72 -22
View File
@@ -38,6 +38,9 @@ from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from homeassistant.loader import async_get_integration
from packaging.version import InvalidVersion, Version
@@ -539,10 +542,21 @@ class HaMcpServerOptionsFlow(OptionsFlow):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Show / apply the server options."""
if user_input is not None:
return self.async_create_entry(title="", data=self._normalize(user_input))
opts = self.config_entry.options
errors: dict[str, str] = {}
if user_input is not None:
errors = self._connect_path_override_errors(user_input)
if not errors:
return self.async_create_entry(
title="", data=self._normalize(user_input)
)
# A validation failure must re-render the values the user just entered.
# Required fields fall back to their stored/default values only when a
# direct unit-test call omits them; optional text fields treat omission
# as an intentional clear, matching Home Assistant's frontend payload.
form_values = opts if user_input is None else {**opts, **user_input}
suggested_values = opts if user_input is None else user_input
schema = vol.Schema(
{
# Authentication mode first, directly under the connect URLs
@@ -551,7 +565,7 @@ class HaMcpServerOptionsFlow(OptionsFlow):
# clients such as Google Gemini Spark and Copilot CLI.
vol.Required(
OPT_WEBHOOK_AUTH,
default=opts.get(OPT_WEBHOOK_AUTH, WEBHOOK_AUTH_NONE),
default=form_values.get(OPT_WEBHOOK_AUTH, WEBHOOK_AUTH_NONE),
): SelectSelector(
SelectSelectorConfig(
options=[
@@ -565,7 +579,7 @@ class HaMcpServerOptionsFlow(OptionsFlow):
),
vol.Required(
OPT_CHANNEL,
default=opts.get(OPT_CHANNEL, DEFAULT_CHANNEL),
default=form_values.get(OPT_CHANNEL, DEFAULT_CHANNEL),
): SelectSelector(
SelectSelectorConfig(
options=[CHANNEL_STABLE, CHANNEL_DEV],
@@ -575,15 +589,15 @@ class HaMcpServerOptionsFlow(OptionsFlow):
),
vol.Required(
OPT_AUTO_UPDATE,
default=bool(opts.get(OPT_AUTO_UPDATE, DEFAULT_AUTO_UPDATE)),
default=bool(form_values.get(OPT_AUTO_UPDATE, DEFAULT_AUTO_UPDATE)),
): bool,
vol.Required(
OPT_SERVER_PORT,
default=opts.get(OPT_SERVER_PORT, DEFAULT_SERVER_PORT),
default=form_values.get(OPT_SERVER_PORT, DEFAULT_SERVER_PORT),
): vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
vol.Required(
OPT_BIND_HOST,
default=opts.get(OPT_BIND_HOST, DEFAULT_BIND_HOST),
default=form_values.get(OPT_BIND_HOST, DEFAULT_BIND_HOST),
): SelectSelector(
# Inline labels: hassfest forbids dots in translation
# keys, so the IP-valued options cannot use strings.json
@@ -616,7 +630,9 @@ class HaMcpServerOptionsFlow(OptionsFlow):
# renders an EMPTY field — the help text says "Leave empty",
# and pre-filling DEFAULT_PIP_SPEC would show the STABLE dist
# name even on the dev channel.
description={"suggested_value": opts.get(OPT_PIP_SPEC, "")},
description={
"suggested_value": suggested_values.get(OPT_PIP_SPEC, "")
},
): str,
vol.Optional(
OPT_SERVER_URL,
@@ -626,28 +642,34 @@ class HaMcpServerOptionsFlow(OptionsFlow):
# constant as an explicit override, which would pin the
# scheme/port even after issue #1890's SSL/port-aware
# loopback derivation.
description={"suggested_value": opts.get(OPT_SERVER_URL, "")},
description={
"suggested_value": suggested_values.get(OPT_SERVER_URL, "")
},
): str,
vol.Required(
OPT_ENABLE_WEBHOOK,
default=bool(opts.get(OPT_ENABLE_WEBHOOK, True)),
default=bool(form_values.get(OPT_ENABLE_WEBHOOK, True)),
): bool,
vol.Required(
OPT_ENABLE_STARTUP_NOTIFICATION,
default=bool(opts.get(OPT_ENABLE_STARTUP_NOTIFICATION, True)),
default=bool(
form_values.get(OPT_ENABLE_STARTUP_NOTIFICATION, True)
),
): bool,
vol.Required(
OPT_ENABLE_SIDEBAR_PANEL,
default=bool(opts.get(OPT_ENABLE_SIDEBAR_PANEL, True)),
default=bool(form_values.get(OPT_ENABLE_SIDEBAR_PANEL, True)),
): bool,
vol.Required(
OPT_ENABLE_LLM_API,
default=bool(opts.get(OPT_ENABLE_LLM_API, DEFAULT_ENABLE_LLM_API)),
default=bool(
form_values.get(OPT_ENABLE_LLM_API, DEFAULT_ENABLE_LLM_API)
),
): bool,
vol.Required(
OPT_LLM_API_EXPOSURE,
default=str(
opts.get(OPT_LLM_API_EXPOSURE, DEFAULT_LLM_API_EXPOSURE)
form_values.get(OPT_LLM_API_EXPOSURE, DEFAULT_LLM_API_EXPOSURE)
),
): SelectSelector(
SelectSelectorConfig(
@@ -660,20 +682,26 @@ class HaMcpServerOptionsFlow(OptionsFlow):
# empty submit — see the OPT_PIP_SPEC note above.
vol.Optional(
OPT_EXTERNAL_URL,
description={"suggested_value": opts.get(OPT_EXTERNAL_URL, "")},
description={
"suggested_value": suggested_values.get(OPT_EXTERNAL_URL, "")
},
): str,
vol.Optional(
OPT_WEBHOOK_ID_OVERRIDE,
description={
"suggested_value": opts.get(OPT_WEBHOOK_ID_OVERRIDE, "")
"suggested_value": suggested_values.get(
OPT_WEBHOOK_ID_OVERRIDE, ""
)
},
): str,
vol.Optional(
OPT_SECRET_PATH_OVERRIDE,
description={
"suggested_value": opts.get(OPT_SECRET_PATH_OVERRIDE, "")
"suggested_value": suggested_values.get(
OPT_SECRET_PATH_OVERRIDE, ""
)
},
): str,
): TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT)),
vol.Optional(
OPT_REGENERATE_SECRETS,
default=False,
@@ -683,14 +711,18 @@ class HaMcpServerOptionsFlow(OptionsFlow):
# Empty = auto-generate/keep the current value.
vol.Optional(
OPT_OAUTH_CLIENT_ID,
description={"suggested_value": opts.get(OPT_OAUTH_CLIENT_ID, "")},
description={
"suggested_value": suggested_values.get(OPT_OAUTH_CLIENT_ID, "")
},
): str,
vol.Optional(
OPT_OAUTH_CLIENT_SECRET,
description={
"suggested_value": opts.get(OPT_OAUTH_CLIENT_SECRET, "")
"suggested_value": suggested_values.get(
OPT_OAUTH_CLIENT_SECRET, ""
)
},
): str,
): TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT)),
vol.Optional(
OPT_OAUTH_REGENERATE,
default=False,
@@ -720,6 +752,7 @@ class HaMcpServerOptionsFlow(OptionsFlow):
return self.async_show_form(
step_id="init",
data_schema=schema,
errors=errors,
description_placeholders={
"versions": versions,
"connect_url": await self._connect_url_hint(common),
@@ -729,6 +762,23 @@ class HaMcpServerOptionsFlow(OptionsFlow):
},
)
@staticmethod
def _connect_path_override_errors(
user_input: Mapping[str, Any],
) -> dict[str, str]:
"""Return field errors for values that cannot be embedded in a URL path."""
errors: dict[str, str] = {}
for key in (OPT_SECRET_PATH_OVERRIDE, OPT_WEBHOOK_ID_OVERRIDE):
# Match _normalize's long-standing paste-friendly behavior: outer
# whitespace is discarded before saving, while whitespace inside
# the value remains invalid because it breaks the resulting URL.
value = str(user_input.get(key, "") or "").strip()
if any(character in "#?%" or character.isspace() for character in value):
errors[key] = "invalid_connect_path"
elif key == OPT_WEBHOOK_ID_OVERRIDE and "/" in value:
errors[key] = "invalid_webhook_id"
return errors
@staticmethod
def _normalize(user_input: dict[str, Any]) -> dict[str, Any]:
"""Normalize the submitted options before they are persisted.