526 lines
26 KiB
Python
526 lines
26 KiB
Python
"""Constants for the HA-MCP custom component.
|
|
|
|
The integration serves two config-entry types under one domain
|
|
(:data:`DOMAIN`), discriminated by ``entry.data[CONF_ENTRY_TYPE]``:
|
|
|
|
* ``tools`` — the privileged file / YAML services (the original component).
|
|
Pre-existing entries carry no ``entry_type`` key, so a missing value is
|
|
treated as ``tools`` (no migration needed).
|
|
* ``server`` — the in-process ha-mcp FastMCP server (issue #1527), exposed
|
|
through a Home Assistant webhook.
|
|
|
|
The two halves keep their constants in separate blocks below; the ``server``
|
|
block was folded in from the former standalone ``ha_mcp_server`` integration.
|
|
"""
|
|
|
|
import re
|
|
from datetime import timedelta
|
|
|
|
DOMAIN = "ha_mcp_tools"
|
|
|
|
# Component version, kept in lockstep with ``manifest.json``'s ``version``.
|
|
# ``ha_mcp_tools/info`` reports this so the server can display/debug the running
|
|
# component build; ``TestManifestVersionParity`` pins the two together so a
|
|
# manifest bump that forgets this constant (or vice-versa) fails in CI. The
|
|
# capability negotiation — not this version — gates each WS command (see
|
|
# ``websocket_api.CAPABILITIES``).
|
|
COMPONENT_VERSION = "1.2.3"
|
|
|
|
# Config-entry discriminator (``entry.data[CONF_ENTRY_TYPE]``). A missing value
|
|
# means "tools" so the pre-existing services entry keeps working across the
|
|
# component update with no migration.
|
|
CONF_ENTRY_TYPE = "entry_type"
|
|
ENTRY_TYPE_TOOLS = "tools"
|
|
ENTRY_TYPE_SERVER = "server"
|
|
|
|
# Titles shown for each entry in the integration tile's entry list. Public so
|
|
# __init__'s setup migration can retitle pre-#1853 tools entries still
|
|
# carrying the legacy default (a user-customized title is left alone).
|
|
TOOLS_ENTRY_TITLE = "HA-MCP File & YAML Tools"
|
|
TOOLS_ENTRY_LEGACY_TITLE = "HA MCP Tools"
|
|
MIN_EMBEDDED_HOME_ASSISTANT_VERSION = "2026.6.0"
|
|
|
|
# Allowed directories for file operations (relative to config dir).
|
|
# "blueprints" is read-only BY DEFAULT — in ALLOWED_READ_DIRS but not
|
|
# ALLOWED_WRITE_DIRS, so ha_write_file / ha_delete_file reject it (raw blueprint
|
|
# reads are safe: community YAML, no secrets — issue #1965). This is the default
|
|
# allowlist, not an absolute guarantee: an admin who adds "blueprints" as a
|
|
# custom extra directory (issue #1567, see _current_extra_dirs) grants it
|
|
# read+write, since extra_dirs are honored on the write path too. Blueprint
|
|
# writes should instead go through ha_import_blueprint (which invokes the
|
|
# blueprint/save WS command internally). Prefer ha_get_blueprint for the parsed
|
|
# body; raw read is the escape hatch for the exact on-disk text.
|
|
ALLOWED_READ_DIRS = ["www", "themes", "custom_templates", "dashboards", "blueprints"]
|
|
ALLOWED_WRITE_DIRS = ["www", "themes", "custom_templates", "dashboards"]
|
|
|
|
# NON-OVERRIDABLE deny floor for the user-configurable extra read/write
|
|
# directories (issue #1567). The custom allowlist is applied ON TOP of the
|
|
# built-in ALLOWED_*_DIRS, but a custom directory can NEVER grant access to
|
|
# these. The floor is re-checked before any allow decision on every read,
|
|
# write, list, and delete, so neither a stored entry nor an in-flight one can
|
|
# punch through it.
|
|
#
|
|
# .storage holds HA's auth database (refresh/access tokens), hashed passwords,
|
|
# and every integration's cleartext credentials (core.config_entries,
|
|
# application_credentials, cloud) — including this component's OWN caller
|
|
# token (.storage/ha_mcp_tools_auth). Letting a custom dir reach it would both
|
|
# leak secrets and hand out the key to this component's own auth gate.
|
|
DENY_PATH_SEGMENTS = frozenset({".storage"})
|
|
|
|
# secrets.yaml is reachable ONLY as the canonical config-root file, where the
|
|
# read handler masks its values. Any OTHER secrets.yaml surfaced via a custom
|
|
# dir would be returned UNMASKED (masking keys off the literal root path), so
|
|
# the floor blocks the basename everywhere except that one canonical location.
|
|
DENY_READ_BASENAMES = frozenset({"secrets.yaml"})
|
|
|
|
# HAOS sibling-volume mounts (issue #1586). These live OUTSIDE the config dir,
|
|
# so the config-relative custom-directory allowlist (issue #1567) cannot reach
|
|
# them — its normalizer rejects every absolute path. A user may instead add one
|
|
# of these fixed absolute roots — or a subdirectory of one — to the custom
|
|
# directory list; access is then enforced against the volume root exactly as a
|
|
# config-relative entry is enforced against the config dir (issue #1586).
|
|
#
|
|
# The component runs inside HA Core, so a volume is reachable only if the HA
|
|
# Core container actually mounts it (the standard HAOS/Supervised mounts are
|
|
# config/share/media/ssl/backup). An unmounted or non-existent root simply
|
|
# yields a "not found" at use time — adding it is harmless. As with the
|
|
# config-relative list, a configured volume grants BOTH read and write, and the
|
|
# non-overridable deny floor (.storage / secrets.yaml) still applies.
|
|
ALLOWED_VOLUME_ROOTS = ("/share", "/media", "/ssl", "/backup")
|
|
|
|
# Files allowed for managed YAML editing
|
|
ALLOWED_YAML_CONFIG_FILES = ["configuration.yaml"]
|
|
# Also allows <packages-folder>/*.yaml via pattern matching, where the folder is
|
|
# the one the user binds under ``homeassistant: packages:`` (default "packages",
|
|
# detected at runtime — see _detect_package_dirs), plus themes/*.yaml.
|
|
|
|
# Top-level YAML keys allowed for editing in any allowed file
|
|
# (configuration.yaml or packages/*.yaml).
|
|
# ONLY keys that have no UI/API alternative belong here.
|
|
# Keys manageable via ha_config_set_helper (input_*, counter, timer, schedule)
|
|
# are intentionally excluded. automation/script/scene live in
|
|
# PACKAGES_ONLY_YAML_KEYS below — they have storage-mode equivalents
|
|
# (ha_config_set_automation/script/scene) but are still exposed in
|
|
# packages/*.yaml for the YAML-packages workflow.
|
|
ALLOWED_YAML_KEYS = frozenset(
|
|
{
|
|
"template",
|
|
"sensor",
|
|
"binary_sensor",
|
|
"command_line",
|
|
"rest",
|
|
"knx",
|
|
"mqtt",
|
|
"shell_command",
|
|
"switch",
|
|
"light",
|
|
"fan",
|
|
"cover",
|
|
"climate",
|
|
"notify",
|
|
"group",
|
|
"utility_meter",
|
|
# recorder is YAML-only (no UI or storage-mode helper): purge_keep_days,
|
|
# include/exclude, commit_interval. Its surface is smaller than keys
|
|
# already here — it only controls what HA records and for how long, with
|
|
# no code-execution path like command_line/shell_command/rest (#1852).
|
|
"recorder",
|
|
}
|
|
)
|
|
|
|
# Top-level YAML keys allowed ONLY inside packages/*.yaml files, never in
|
|
# configuration.yaml. Storage-mode UI/API equivalents already exist
|
|
# (ha_config_set_automation/script/scene), so these are exposed here only
|
|
# for the YAML-packages workflow used by git-managed configs — where users
|
|
# expect to keep automations/scripts/scenes alongside templates and other
|
|
# YAML-defined items. Writes to configuration.yaml for these keys remain
|
|
# rejected so storage-mode and YAML-mode collections don't collide.
|
|
PACKAGES_ONLY_YAML_KEYS = frozenset(
|
|
{
|
|
"automation",
|
|
"script",
|
|
"scene",
|
|
}
|
|
)
|
|
|
|
# Post-edit action required for each YAML key.
|
|
# template, mqtt, group, automation, script, and scene have first-party
|
|
# reload services in HA core. All others require a full HA restart.
|
|
# ``TestPostActionTableContract`` pins the in-repo shape; the HA-core
|
|
# side of the contract is a write-time snapshot, not a continuous check.
|
|
YAML_KEY_POST_ACTIONS: dict[str, dict[str, str]] = {
|
|
"template": {
|
|
"post_action": "reload_available",
|
|
"reload_service": "homeassistant.reload_custom_templates",
|
|
},
|
|
"mqtt": {
|
|
"post_action": "reload_available",
|
|
"reload_service": "mqtt.reload",
|
|
},
|
|
"group": {
|
|
"post_action": "reload_available",
|
|
"reload_service": "group.reload",
|
|
},
|
|
"automation": {
|
|
"post_action": "reload_available",
|
|
"reload_service": "automation.reload",
|
|
},
|
|
"script": {
|
|
"post_action": "reload_available",
|
|
"reload_service": "script.reload",
|
|
},
|
|
"scene": {
|
|
"post_action": "reload_available",
|
|
"reload_service": "scene.reload",
|
|
},
|
|
}
|
|
# Default for keys not in YAML_KEY_POST_ACTIONS:
|
|
YAML_KEY_DEFAULT_POST_ACTION = {"post_action": "restart_required"}
|
|
|
|
# YAML-mode dashboard url_path validation (issue #1034).
|
|
# Pattern: lowercase letters/digits, hyphen-separated, must contain at least
|
|
# one hyphen (HA's lovelace dashboard rule). No leading/trailing/double hyphens.
|
|
DASHBOARD_URL_PATH_PATTERN = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)+")
|
|
|
|
# url_paths reserved by HA core dashboards/routes — must not be registered as
|
|
# YAML-mode dashboards or they will shadow / collide with built-ins.
|
|
RESERVED_DASHBOARD_URL_PATHS = frozenset(
|
|
{
|
|
"lovelace",
|
|
"overview",
|
|
"map",
|
|
"logbook",
|
|
"history",
|
|
"energy",
|
|
"developer-tools",
|
|
"config",
|
|
"profile",
|
|
"media-browser",
|
|
"todo",
|
|
"calendar",
|
|
}
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# HA-MCP Server entry (issue #1527)
|
|
#
|
|
# Folded in from the former standalone ``ha_mcp_server`` integration. The
|
|
# "server" config-entry type runs the full ha-mcp FastMCP server in-process
|
|
# inside Home Assistant (a dedicated thread with its own asyncio loop) and
|
|
# exposes it remotely through a Home Assistant webhook, exactly like the
|
|
# webhook-proxy add-on. Creating the entry starts the server; disabling or
|
|
# removing the entry stops it. Everything below is namespaced under the shared
|
|
# ``DOMAIN`` (distinct hass.data sub-keys, distinct entry unique_id).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# PyPI distribution names. Stable ships as ``ha-mcp``; the dev channel ships as
|
|
# ``ha-mcp-dev`` — published on every master push. BOTH are installed unpinned,
|
|
# so every install / reload resolves the newest build of the selected channel
|
|
# (the component auto-updates the server rather than pinning a lockstep version
|
|
# — see ``UPDATE_CHECK_INTERVAL`` and
|
|
# ``EmbeddedServerManager._async_ensure_package``). Both wheels contain the
|
|
# *same* ``ha_mcp`` import package (publish-dev.yml only renames the
|
|
# distribution), so only one may be installed at a time — see
|
|
# EmbeddedServerManager's channel-switch handling.
|
|
DIST_NAME_STABLE = "ha-mcp"
|
|
DIST_NAME_DEV = "ha-mcp-dev"
|
|
|
|
# Default pip requirement for the stable channel: the unpinned ``ha-mcp``
|
|
# distribution, so each install resolves the newest stable release. The options
|
|
# flow's advanced "pip requirement" field overrides this with any pip spec
|
|
# (e.g. a version pin or a GitHub tarball URL) for pre-release testing — an
|
|
# explicit override also disables automatic updates.
|
|
DEFAULT_PIP_SPEC = DIST_NAME_STABLE
|
|
DEV_PIP_SPEC = DIST_NAME_DEV
|
|
|
|
# Release channels (options-flow selector). ``stable`` installs the unpinned
|
|
# ``ha-mcp`` and ``dev`` installs the unpinned ``ha-mcp-dev``; both refresh to
|
|
# the newest build of that channel on every entry reload / HA restart, and the
|
|
# periodic auto-update check reloads the entry when PyPI publishes a newer one.
|
|
# An explicit OPT_PIP_SPEC override wins over both and disables auto-update.
|
|
CHANNEL_STABLE = "stable"
|
|
CHANNEL_DEV = "dev"
|
|
DEFAULT_CHANNEL = CHANNEL_STABLE
|
|
|
|
|
|
def dist_for_channel(channel: str) -> str:
|
|
"""Map a release channel to its PyPI distribution name.
|
|
|
|
The channel <-> distribution correspondence is used by the version
|
|
coordinator, the auto-update notification, and the server manager's pip
|
|
resolution — one shared mapping so a future third channel cannot be added
|
|
to some sites and missed in others (review finding on #1760).
|
|
"""
|
|
return DIST_NAME_DEV if channel == CHANNEL_DEV else DIST_NAME_STABLE
|
|
|
|
|
|
def channel_for_dist(dist: str) -> str:
|
|
"""Inverse of :func:`dist_for_channel`."""
|
|
return CHANNEL_DEV if dist == DIST_NAME_DEV else CHANNEL_STABLE
|
|
|
|
|
|
# Interval of the ServerVersionCoordinator's PyPI poll (coordinator.py). The
|
|
# poll itself ALWAYS runs — it feeds the `update` platform entity, which must
|
|
# stay populated even when automatic updates are off (issue #1760). Whether a
|
|
# newer build actually triggers a reload/reinstall is decided separately, per
|
|
# refresh, in embedded_setup.async_maybe_auto_update (gated on OPT_AUTO_UPDATE
|
|
# and on no pip-spec override). Only an explicit pip-spec override skips the
|
|
# PyPI fetch — comparing PyPI-latest against an arbitrary pip spec is
|
|
# meaningless.
|
|
UPDATE_CHECK_INTERVAL = timedelta(hours=6)
|
|
|
|
# PyPI JSON API for the latest published version of a distribution. ``{dist}``
|
|
# is DIST_NAME_STABLE or DIST_NAME_DEV depending on the selected channel.
|
|
PYPI_JSON_URL = "https://pypi.org/pypi/{dist}/json"
|
|
|
|
# The component manifest as it existed at a server release's git tag. Its
|
|
# ``version`` is the component version that SHIPPED with that server build, so
|
|
# a value newer than the running component means the release changed the
|
|
# component too — the pre-install auto-update gate in embedded_setup holds the
|
|
# server update until HACS delivers the component (issues #1783/#1785).
|
|
# Tag-timing caveat: stable ``vX.Y.Z`` tags exist before the PyPI publish
|
|
# (semantic-release pushes the tag first), but a dev ``vX.Y.Z.devN`` tag is
|
|
# only created when its draft GitHub release is published — AFTER the binary
|
|
# builds, minutes after PyPI already has the version. During that dev window
|
|
# this URL 404s and the gate deliberately fails open (the registry's
|
|
# skip-on-failure is the backstop on that channel).
|
|
COMPONENT_MANIFEST_AT_TAG_URL = (
|
|
"https://raw.githubusercontent.com/homeassistant-ai/ha-mcp/"
|
|
"v{version}/custom_components/ha_mcp_tools/manifest.json"
|
|
)
|
|
|
|
# Options-flow keys (stored in entry.options).
|
|
OPT_CHANNEL = "channel"
|
|
# Automatic server-version updates toggle (default on). When on, the channel is
|
|
# unpinned and auto-updates (force-install on reload/restart + a reload when the
|
|
# periodic check sees a newer build). When off, the server stays on the version
|
|
# currently installed: _resolve_pip_spec pins the channel's dist to that version
|
|
# — but the periodic PyPI check KEEPS running so the update entity still shows
|
|
# newer builds; its Install button is the manual path (issue #1760). Governs the
|
|
# ha-mcp server package only — component updates still come through HACS. An
|
|
# explicit OPT_PIP_SPEC override wins over both and skips the check entirely.
|
|
OPT_AUTO_UPDATE = "auto_update"
|
|
DEFAULT_AUTO_UPDATE = True
|
|
OPT_SERVER_PORT = "server_port"
|
|
OPT_BIND_HOST = "bind_host"
|
|
OPT_WEBHOOK_AUTH = "webhook_auth"
|
|
# Legacy OAuth mode (self-hosted authorization server, static client_id/secret
|
|
# for Google Gemini Spark) credential management — mirrors the
|
|
# OPT_WEBHOOK_ID_OVERRIDE / OPT_REGENERATE_SECRETS shape below. Empty override
|
|
# fields mean "keep the current value"; OPT_OAUTH_REGENERATE is one-shot.
|
|
# _override suffix distinguishes these OPTIONS keys from the DATA_OAUTH_*
|
|
# entry.data keys (which store the resolved values under the un-suffixed
|
|
# names) — mirrors OPT_WEBHOOK_ID_OVERRIDE vs DATA_WEBHOOK_ID.
|
|
OPT_OAUTH_CLIENT_ID = "oauth_client_id_override"
|
|
OPT_OAUTH_CLIENT_SECRET = "oauth_client_secret_override"
|
|
OPT_OAUTH_REGENERATE = "oauth_regenerate"
|
|
OPT_PIP_SPEC = "pip_spec"
|
|
OPT_SERVER_URL = "server_url"
|
|
# Connect-URL surface + secret management (owner request, parity with the
|
|
# webhook-proxy app's external-URL option and the add-on's secret-path
|
|
# override). All optional; empty string = automatic/keep-current.
|
|
OPT_EXTERNAL_URL = "external_url"
|
|
OPT_WEBHOOK_ID_OVERRIDE = "webhook_id_override"
|
|
OPT_SECRET_PATH_OVERRIDE = "secret_path_override"
|
|
OPT_REGENERATE_SECRETS = "regenerate_secrets"
|
|
# Local-only mode (owner request): when False, the HA webhook is never
|
|
# registered, so nothing - including Nabu Casa remote UI - can reach the
|
|
# server through Home Assistant; only the direct server port (+ the
|
|
# admin-only sidebar panel, which proxies over loopback) remains.
|
|
OPT_ENABLE_WEBHOOK = "enable_webhook"
|
|
# Conversation-agent LLM API (#1745): when False, the toolset is not
|
|
# registered as a Home Assistant LLM API, so it never appears in any
|
|
# conversation agent's "Control Home Assistant" selector. On by default —
|
|
# registering the API only makes it selectable; nothing is exposed until a
|
|
# user picks it on an agent.
|
|
OPT_ENABLE_LLM_API = "enable_llm_api"
|
|
DEFAULT_ENABLE_LLM_API = True
|
|
# Which exposure shape(s) the LLM API offers to conversation agents:
|
|
# ``tool_search`` (default) registers a compact API — pinned tools plus
|
|
# search/execute meta-tools — the shape context-limited models need; ``full``
|
|
# registers the whole exposed catalog as one API; ``both`` registers the two
|
|
# side by side so the choice is made per agent in HA's own selector.
|
|
OPT_LLM_API_EXPOSURE = "llm_api_exposure"
|
|
EXPOSURE_TOOL_SEARCH = "tool_search"
|
|
EXPOSURE_FULL = "full"
|
|
EXPOSURE_BOTH = "both"
|
|
DEFAULT_LLM_API_EXPOSURE = EXPOSURE_TOOL_SEARCH
|
|
# When False, the persistent notification created on every server bring-up is
|
|
# suppressed; the connect URLs still reach the admin-only Home Assistant log.
|
|
OPT_ENABLE_STARTUP_NOTIFICATION = "enable_startup_notification"
|
|
# When False, the admin-only "HA-MCP" sidebar settings panel is not registered;
|
|
# the server's options stay reachable on the entry's Configure screen.
|
|
OPT_ENABLE_SIDEBAR_PANEL = "enable_sidebar_panel"
|
|
|
|
# entry.data keys (persisted ids + secrets; entry.data is fine for secrets).
|
|
DATA_WEBHOOK_ID = "webhook_id"
|
|
DATA_SECRET_PATH = "secret_path"
|
|
# Legacy OAuth mode credentials, minted by embedded_entry._ensure_secrets and
|
|
# consumed by oauth_legacy.LegacyOAuthProvider. DATA_OAUTH_SIGNING_KEY is a hex
|
|
# string (entry.data must be JSON-serializable, so raw bytes aren't stored
|
|
# directly) — the provider converts it with bytes.fromhex(). The signed token
|
|
# payload carries the client_id (not the secret), so rotating the client_id
|
|
# revokes every outstanding token at the restart that rebinds the views (see
|
|
# LegacyOAuthProvider._validate_token).
|
|
# Because validation never involves the client_secret, a secret-only override
|
|
# change instead rotates the signing key, evicting outstanding tokens at the
|
|
# restart that activates the new credentials (see
|
|
# embedded_entry._ensure_legacy_oauth_secrets). Until that restart the bound
|
|
# views keep serving the OLD identity, so the startup log withholds rotated
|
|
# credentials (embedded_setup._surface_connect_urls).
|
|
DATA_OAUTH_CLIENT_ID = "oauth_client_id"
|
|
DATA_OAUTH_CLIENT_SECRET = "oauth_client_secret"
|
|
DATA_OAUTH_SIGNING_KEY = "oauth_signing_key"
|
|
DATA_SERVER_USER_ID = "server_user_id"
|
|
DATA_REFRESH_TOKEN_ID = "refresh_token_id"
|
|
DATA_ACCESS_TOKEN = "access_token"
|
|
# Last pip spec that was successfully installed. Lets a changed spec (the
|
|
# pre-release test channel) force an actual reinstall on the next start instead
|
|
# of hitting the requirements manager's is-installed shortcut.
|
|
DATA_LAST_PIP_SPEC = "last_pip_spec"
|
|
# One-shot marker set by the update entity's Install button (issue #1760):
|
|
# with auto-update off, EmbeddedServerManager._resolve_pip_spec pins the
|
|
# channel to the CURRENTLY installed version, so a bare reload would just
|
|
# reinstall the same build. This pins the next install to a specific version
|
|
# regardless of auto_update; embedded_server clears it when it CONSUMES it
|
|
# (before the install attempt) — one marker buys exactly one attempt, so a
|
|
# failing pinned version can never re-pin later reloads (review finding).
|
|
DATA_PENDING_INSTALL_VERSION = "pending_install_version"
|
|
|
|
# hass.data[DOMAIN] sub-keys for the server runtime. Distinct from the tools
|
|
# entry's sub-keys ("caller_token" / "allowed_paths") so both entry types can
|
|
# share hass.data[DOMAIN] without collision.
|
|
DATA_MANAGER = "manager"
|
|
DATA_WEBHOOK = "webhook"
|
|
DATA_BRINGUP_TASK = "bringup_task"
|
|
# Snapshot of entry.options taken at setup so the update listener reloads only
|
|
# on a genuine options change — the background bring-up persists ids/token/pip
|
|
# spec to entry.data, and those writes must not trigger a self-reload.
|
|
DATA_LAST_OPTIONS = "last_options"
|
|
# The ServerVersionCoordinator instance backing the `update` platform entity
|
|
# (issue #1760) — stored so the platform's async_setup_entry can retrieve it.
|
|
DATA_UPDATE_COORDINATOR = "update_coordinator"
|
|
# Set by async_maybe_auto_update right before it reloads the entry for an
|
|
# automatic update ({"old": <version>}): the "server updated" notification must
|
|
# only fire once the reloaded entry's bring-up actually installed and started
|
|
# the new build — the reload call returns as soon as entry SETUP finishes,
|
|
# while the pip install still runs in the background and can fail (review
|
|
# finding on #1760). Bring-up pops it: notification on success, silent drop on
|
|
# failure (the package/start repair issues cover that path).
|
|
DATA_PENDING_UPDATE_NOTIFY = "pending_update_notify"
|
|
# Unregister callback for the conversation-agent LLM API (#1745), stored by
|
|
# the bring-up success path and invoked (idempotently) by teardown.
|
|
DATA_LLM_API_UNSUB = "llm_api_unsub"
|
|
|
|
# Webhook auth modes (mirrors the webhook-proxy add-on's default posture).
|
|
WEBHOOK_AUTH_NONE = "none" # secret webhook URL is the shared secret (default)
|
|
WEBHOOK_AUTH_HA = "ha_auth" # HA-native bearer (HA core is the OAuth AS)
|
|
# Self-hosted OAuth 2.1 authorization server with a static client_id/secret,
|
|
# ported from the webhook-proxy add-on's "legacy" mode. Needed because HA
|
|
# core's /auth/authorize does not yet fetch Client ID Metadata Documents for
|
|
# cross-origin redirect_uris (home-assistant/core#176282), which is what
|
|
# Google Gemini Spark's custom connected apps require.
|
|
WEBHOOK_AUTH_LEGACY = "legacy"
|
|
|
|
# Default bind host + port. 9584 (not the add-on's 9583) so this in-process
|
|
# server and an add-on install can coexist on the same box.
|
|
DEFAULT_SERVER_PORT = 9584
|
|
# LAN-reachable by default - parity with the add-on, whose port has always
|
|
# been directly reachable with the secret path as the credential. Loopback
|
|
# is the optional hardening choice, not the default (owner decision).
|
|
DEFAULT_BIND_HOST = "0.0.0.0"
|
|
BIND_HOST_ALL = "0.0.0.0"
|
|
BIND_HOST_LOOPBACK = "127.0.0.1"
|
|
|
|
# Loopback base URL the server uses to reach HA core (REST + WS).
|
|
DEFAULT_LOOPBACK_URL = "http://127.0.0.1:8123"
|
|
|
|
# Persistent data dir for the in-process server, under the HA config dir so it
|
|
# survives restarts and is isolated from an add-on's /data. Generic ".ha_mcp"
|
|
# to match the merged integration's naming (unreleased server entry, so no
|
|
# migration from the former ".ha_mcp_server").
|
|
SERVER_CONFIG_SUBDIR = ".ha_mcp"
|
|
|
|
# Client name recorded on the provisioned long-lived access token, and the name
|
|
# of the local admin user the server logs in as. Stable so a reused token is
|
|
# recognizable in Settings -> People -> <user> -> tokens. "HA-MCP" phrasing (not
|
|
# "Home Assistant MCP Server") to avoid confusion with HA's official MCP Server
|
|
# integration.
|
|
SERVER_TOKEN_CLIENT_NAME = "HA-MCP Server"
|
|
SERVER_USER_NAME = "HA-MCP Server"
|
|
|
|
# RFC 8414 / RFC 9728 discovery documents for ha_auth mode are served under this
|
|
# namespace (mirrors the webhook-proxy add-on's /api/mcp_proxy/oauth base).
|
|
OAUTH_BASE = "/api/ha_mcp_tools/oauth"
|
|
|
|
# HACS repository full_names (``owner/repo``, the key HACS's repository registry
|
|
# uses) this component may be tracked under: the dedicated integration mirror is
|
|
# the current install path; the main ha-mcp server repo is the legacy pre-mirror
|
|
# path (see install_source_check). Shared by the legacy-source check and the
|
|
# HACS refresh nudge (hacs_nudge) so a repository rename lands in one place.
|
|
HACS_MIRROR_REPO_FULL_NAME = "homeassistant-ai/ha-mcp-integration"
|
|
HACS_LEGACY_REPO_FULL_NAME = "homeassistant-ai/ha-mcp"
|
|
|
|
# HACS "add repository" deep link for the custom component. learn_more_url for
|
|
# the legacy-HACS-source repair (install_source_check) only, whose fix really is
|
|
# re-adding the repository. The update-held and component-outdated issues point
|
|
# at UPDATE_HOLD_DOCS_URL instead — for an already-installed component this deep
|
|
# link just opens a blank "add repository" dialog.
|
|
HACS_COMPONENT_URL = (
|
|
"https://my.home-assistant.io/redirect/hacs_repository/"
|
|
"?owner=homeassistant-ai&repository=ha-mcp-integration&category=integration"
|
|
)
|
|
|
|
# Docs section explaining the automatic-update hold, linked as learn_more_url
|
|
# from the update-held and component-outdated repair issues (both resolve by
|
|
# updating an already-installed component, not by re-adding a repository). The
|
|
# anchor is the GitHub slug of the "Held server updates" heading in
|
|
# docs/in-process-server.md; hassfest forbids literal URLs inside strings.json.
|
|
UPDATE_HOLD_DOCS_URL = (
|
|
"https://github.com/homeassistant-ai/ha-mcp/blob/master/docs/"
|
|
"in-process-server.md#held-server-updates"
|
|
)
|
|
|
|
# Usage guide for the conversation-agent LLM API option (#1745). Injected into
|
|
# the options form as a description placeholder — hassfest forbids literal
|
|
# URLs inside strings.json.
|
|
LLM_API_DOCS_URL = (
|
|
"https://github.com/homeassistant-ai/ha-mcp/blob/master/docs/"
|
|
"in-process-server.md"
|
|
"#chat-with-the-toolset-from-home-assistant-conversation-agents--voice"
|
|
)
|
|
|
|
# Repair-issue ids surfaced when server bring-up fails.
|
|
ISSUE_PACKAGE_FAILED = "server_package_install_failed"
|
|
ISSUE_START_FAILED = "server_start_failed"
|
|
# Repair issue surfaced when the installed ha-mcp server requires a newer
|
|
# custom component than the one running. The server package updates
|
|
# independently of the HACS component, so the running component can lag what
|
|
# the server expects; this points the user at the HACS component update
|
|
# (non-blocking).
|
|
ISSUE_COMPONENT_OUTDATED = "component_outdated"
|
|
# Repair issue surfaced while an automatic server update is HELD because the
|
|
# newer server release also shipped a newer custom component than the one
|
|
# running (issues #1783/#1785): installing that server under the old component
|
|
# is the combination that broke starts. Held is loud (this issue + a warning
|
|
# log every check) and escapable — applying the HACS component update (which
|
|
# takes an HA restart) unblocks the next check, and the update entity's
|
|
# Install button bypasses the hold entirely.
|
|
ISSUE_UPDATE_HELD = "server_update_held"
|
|
# Repair issue surfaced when HACS is tracking the MAIN ha-mcp server repo for
|
|
# this component (the pre-mirror install path — issue #1760). That install
|
|
# keeps working (HACS downloads the repo snapshot at the release tag, which
|
|
# contains the component), but HACS shows the SERVER's version numbers and
|
|
# release notes, not the component's own; HACS has no repository-migration
|
|
# mechanism, so this only self-resolves if the user re-adds the dedicated
|
|
# mirror (homeassistant-ai/ha-mcp-integration).
|
|
ISSUE_LEGACY_HACS_SOURCE = "legacy_hacs_source"
|
|
# Repair issue surfaced when the legacy OAuth mode's root /authorize + /token
|
|
# views are out of sync with the CONFIGURED webhook_auth mode — either just
|
|
# enabled (views not yet bound with the current credentials) or just disabled
|
|
# (views still bound and serving the old identity). aiohttp can neither bind
|
|
# nor unbind an HTTP view without a full Home Assistant restart, so both
|
|
# transitions need one; see oauth_legacy.bind_legacy_views.
|
|
ISSUE_LEGACY_OAUTH_RESTART = "legacy_oauth_restart"
|