429 files
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
{"pid": 71, "version": 1, "ha_version": "2026.8.3", "start_ts": 1787934175.6087143}
|
||||
{"pid": 72, "version": 1, "ha_version": "2026.8.3", "start_ts": 1788057668.905601}
|
||||
@@ -315,7 +315,7 @@
|
||||
"type": "module"
|
||||
},
|
||||
{
|
||||
"id": "7bde44beb2c244d683f5ab784e5fc567",
|
||||
"id": "34c7ccbaa8694227b4708c04071c3d46",
|
||||
"url": "/climate_scheduler/static/climate-scheduler-card.js?v=1.15.1",
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
@@ -3557,7 +3557,10 @@ async def _async_setup_tools_entry(hass: HomeAssistant, entry: ConfigEntry) -> b
|
||||
# Register the in-process ha_mcp_tools/* WebSocket commands (info + search)
|
||||
# the server calls behind a capability gate. Idempotent, and independent of
|
||||
# the caller-token gate above — HA core authenticates the WS connection and
|
||||
# @require_admin gates each command (see websocket_api.py).
|
||||
# @require_admin gates each command (see websocket_api.py). The server
|
||||
# (embedded) entry registers the same surface (#2289), so a server-only
|
||||
# install has it as well; on a dual-entry install both entries register it
|
||||
# and the second registration simply overwrites the first.
|
||||
async_register_commands(hass)
|
||||
|
||||
# Entry-level finalization: pick up the #1853 rename on existing installs and
|
||||
@@ -3617,13 +3620,19 @@ async def _async_unload_tools_entry(hass: HomeAssistant, entry: ConfigEntry) ->
|
||||
hass.services.async_remove(DOMAIN, SERVICE_GET_CALLER_TOKEN)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_GET_ALLOWED_PATHS)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_SET_ALLOWED_PATHS)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_GET_EXTRA_YAML_KEYS)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_SET_EXTRA_YAML_KEYS)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_LIST_LEGACY_BACKUPS)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_READ_LEGACY_BACKUP)
|
||||
|
||||
# Drop the cached token + allowlist from hass.data so a subsequent
|
||||
# setup_entry re-reads from storage (covers the reload-after-rotate path).
|
||||
# Drop the cached token + allowlist + extra YAML keys from hass.data so a
|
||||
# subsequent setup_entry re-reads from storage (covers the
|
||||
# reload-after-rotate path). The WS command surface is deliberately NOT
|
||||
# unregistered here — it is shared with the server entry and HA offers no
|
||||
# unregister (#2292); see ``websocket_api.async_register_commands``.
|
||||
domain_data = hass.data.get(DOMAIN)
|
||||
if isinstance(domain_data, dict):
|
||||
domain_data.pop(_HASS_DATA_TOKEN_KEY, None)
|
||||
domain_data.pop(_HASS_DATA_ALLOWED_PATHS_KEY, None)
|
||||
domain_data.pop(_HASS_DATA_EXTRA_YAML_KEYS_KEY, None)
|
||||
return True
|
||||
|
||||
@@ -25,7 +25,7 @@ DOMAIN = "ha_mcp_tools"
|
||||
# in CI. The
|
||||
# capability negotiation — not this version — gates each WS command (see
|
||||
# ``websocket_api.CAPABILITIES``).
|
||||
COMPONENT_VERSION = "1.3.2"
|
||||
COMPONENT_VERSION = "2.1.0"
|
||||
|
||||
# Config-entry discriminator (``entry.data[CONF_ENTRY_TYPE]``). A missing value
|
||||
# means "tools" so the pre-existing services entry keeps working across the
|
||||
@@ -422,6 +422,10 @@ DATA_SECRET_PATH = "secret_path"
|
||||
DATA_OAUTH_CLIENT_ID = "oauth_client_id"
|
||||
DATA_OAUTH_CLIENT_SECRET = "oauth_client_secret"
|
||||
DATA_OAUTH_SIGNING_KEY = "oauth_signing_key"
|
||||
# Hex-encoded per-entry HMAC key signing stateless DCR client_ids (RFC 7591
|
||||
# compat endpoint). Generated at setup when absent, so entries created before
|
||||
# 2.0.0 gain one on their first reload after upgrade.
|
||||
DATA_DCR_SIGNING_KEY = "dcr_signing_key"
|
||||
DATA_SERVER_USER_ID = "server_user_id"
|
||||
DATA_REFRESH_TOKEN_ID = "refresh_token_id"
|
||||
DATA_ACCESS_TOKEN = "access_token"
|
||||
|
||||
@@ -42,16 +42,19 @@ from .const import (
|
||||
OPT_SECRET_PATH_OVERRIDE,
|
||||
OPT_WEBHOOK_AUTH,
|
||||
OPT_WEBHOOK_ID_OVERRIDE,
|
||||
WEBHOOK_AUTH_HA,
|
||||
WEBHOOK_AUTH_LEGACY,
|
||||
WEBHOOK_AUTH_NONE,
|
||||
)
|
||||
|
||||
# NOTE: embedded_setup / coordinator (and their embedded_server / mcp_webhook
|
||||
# chain) are imported lazily inside the entry lifecycle functions below, not at
|
||||
# module top level. They pull in aiohttp and several homeassistant.* submodules
|
||||
# (auth, requirements, util.package, components.http/webhook) that the
|
||||
# entry-point wiring here never touches directly, so a top-level import would
|
||||
# make importing this package require that whole stack — breaking hermetic unit
|
||||
# tests that stub only the modules they use.
|
||||
# chain), plus websocket_api, are imported lazily inside the entry lifecycle
|
||||
# functions below, not at module top level. They pull in aiohttp, yaml and
|
||||
# several homeassistant.* submodules (auth, requirements, util.package,
|
||||
# components.http/webhook, components.websocket_api, half a dozen helpers) that
|
||||
# the entry-point wiring here never touches directly, so a top-level import
|
||||
# would make importing this package require that whole stack — breaking
|
||||
# hermetic unit tests that stub only the modules they use.
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -65,18 +68,33 @@ async def async_setup_server_entry(hass: HomeAssistant, entry: ConfigEntry) -> b
|
||||
startup. It runs as a config-entry background task — automatically cancelled
|
||||
on unload. The secret webhook id and secret path are generated first, before
|
||||
the update listener is registered, so those ``entry.data`` writes never
|
||||
trigger a mid-setup reload.
|
||||
trigger a mid-setup reload. The ``ha_mcp_tools/*`` WebSocket command surface
|
||||
is registered up front, before any of that.
|
||||
"""
|
||||
# Imported lazily (see the import note) so the aiohttp / auth / requirements
|
||||
# chain is pulled in only when an entry is actually set up.
|
||||
from .coordinator import ServerVersionCoordinator
|
||||
from .embedded_setup import async_bring_up_server, async_maybe_auto_update
|
||||
from .ui_panel import async_register_ui_panel
|
||||
from .websocket_api import async_register_commands
|
||||
|
||||
# The ha_mcp_tools/* WS commands are entry-agnostic read/search machinery
|
||||
# over HA-core state (entity components, storage collections, lovelace,
|
||||
# registries) — nothing in them reads the tools entry's hass.data, and HA
|
||||
# core authenticates the connection while @require_admin gates each command.
|
||||
# So the server entry registers them too (#2289): a server-entry-only
|
||||
# install used to have no ha_mcp_tools/* commands at all, silently dropping
|
||||
# every ha_search to the legacy path. Only the filesystem/YAML HA *services*
|
||||
# stay tools-entry-only. Registered first, before anything that can fail or
|
||||
# await, so a broken bring-up later never costs the install this surface;
|
||||
# registration is idempotent, so a dual-entry install re-registering is
|
||||
# harmless.
|
||||
async_register_commands(hass)
|
||||
|
||||
_ensure_secrets(hass, entry)
|
||||
# Bind the legacy OAuth root views synchronously here — before the slow
|
||||
# background bring-up below — so they are live at boot (see the helper).
|
||||
_prebind_legacy_oauth_views(hass, entry)
|
||||
# Bind every applicable OAuth route synchronously here — before the slow
|
||||
# background bring-up below — so it is live at boot (see the helper).
|
||||
_prebind_oauth_views(hass, entry)
|
||||
|
||||
# Admin-only "Open Web UI" sidebar panel + proxy. Registered while the entry
|
||||
# exists (its proxy returns 503 until the server is actually running), so the
|
||||
@@ -190,30 +208,44 @@ async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> Non
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
def _prebind_legacy_oauth_views(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Register the legacy OAuth root ``/authorize`` + ``/token`` views during
|
||||
entry setup, before the background bring-up's (slow) package install.
|
||||
def _prebind_oauth_views(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Register OAuth routes before the background bring-up's slow install.
|
||||
|
||||
An aiohttp route is only ever live if it is registered before Home Assistant
|
||||
freezes its HTTP app at the end of startup. Binding these views from the
|
||||
background bring-up task races HA reaching RUNNING: on a slow-install boot
|
||||
the routes would register AFTER the freeze — never live until a restart —
|
||||
and ``bind_legacy_views`` would see ``hass.is_running`` True and file a
|
||||
restart repair that the restart cannot clear. Binding here, while the entry
|
||||
is still setting up (``hass.is_running`` is False at boot), mirrors the
|
||||
webhook-proxy add-on, which binds in its own ``async_setup_entry``. The
|
||||
bring-up's ``async_register_webhook`` then reuses this already-bound provider.
|
||||
the advertised metadata, scoped authorize/token, and DCR routes would never
|
||||
become live until a restart. Bind the shared metadata/scoped pair for every
|
||||
remote mode, DCR for none/ha_auth, and legacy's root aliases when credentials
|
||||
are available. The later webhook registration reuses these bound views and
|
||||
supplies the live per-request providers through ``hass.data``.
|
||||
|
||||
Only relevant when legacy is the configured mode and the webhook endpoint is
|
||||
enabled (legacy OAuth guards that endpoint; with no webhook there is nothing
|
||||
to protect). A route-ownership conflict with the webhook-proxy add-on is
|
||||
swallowed here — the bring-up re-encounters it and files the user-facing
|
||||
start-failed repair.
|
||||
A legacy root-route ownership conflict with the webhook-proxy add-on is
|
||||
swallowed here; the scoped pair remains live and background bring-up selects
|
||||
its scoped-only provider.
|
||||
"""
|
||||
if str(entry.options.get(OPT_WEBHOOK_AUTH, "")) != WEBHOOK_AUTH_LEGACY:
|
||||
return
|
||||
if not bool(entry.options.get(OPT_ENABLE_WEBHOOK, True)):
|
||||
return
|
||||
auth_mode = str(entry.options.get(OPT_WEBHOOK_AUTH, ""))
|
||||
if auth_mode not in (
|
||||
WEBHOOK_AUTH_NONE,
|
||||
WEBHOOK_AUTH_HA,
|
||||
WEBHOOK_AUTH_LEGACY,
|
||||
):
|
||||
return
|
||||
|
||||
from .mcp_webhook import _register_metadata_views
|
||||
from .oauth_autoapprove import bind_autoapprove_views
|
||||
|
||||
_register_metadata_views(hass)
|
||||
bind_autoapprove_views(hass)
|
||||
|
||||
if auth_mode != WEBHOOK_AUTH_LEGACY:
|
||||
from .oauth_dcr import bind_dcr_view
|
||||
|
||||
bind_dcr_view(hass)
|
||||
return
|
||||
|
||||
client_id = entry.data.get(DATA_OAUTH_CLIENT_ID)
|
||||
client_secret = entry.data.get(DATA_OAUTH_CLIENT_SECRET)
|
||||
signing_key = entry.data.get(DATA_OAUTH_SIGNING_KEY)
|
||||
@@ -221,16 +253,9 @@ def _prebind_legacy_oauth_views(hass: HomeAssistant, entry: ConfigEntry) -> None
|
||||
# _ensure_secrets mints these whenever legacy mode is configured; a gap
|
||||
# means a partial config — let the bring-up path surface it.
|
||||
return
|
||||
from .mcp_webhook import _register_metadata_views
|
||||
from .oauth_legacy import LegacyOAuthRouteConflict, bind_legacy_views
|
||||
|
||||
with suppress(LegacyOAuthRouteConflict):
|
||||
# Register the RFC 8414/9728 discovery views alongside the root
|
||||
# /authorize + /token views, both at setup time, so the discovery
|
||||
# doc's resource_metadata URL resolves at boot for RFC-compliant
|
||||
# clients — not just the root views. Both are idempotent, so the
|
||||
# bring-up's async_register_webhook reuses them.
|
||||
_register_metadata_views(hass)
|
||||
bind_legacy_views(hass, client_id, client_secret, signing_key)
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,15 @@ from .const import (
|
||||
SERVER_USER_NAME,
|
||||
dist_for_channel,
|
||||
)
|
||||
from .dependency_diagnostics import (
|
||||
DependencyViolation,
|
||||
PinningIntegration,
|
||||
audit_dependency_graph,
|
||||
describe_dependency_failure,
|
||||
find_pinning_integrations,
|
||||
requirement_forces_conflict,
|
||||
root_import_failure,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
@@ -217,6 +226,67 @@ class EmbeddedServerError(Exception):
|
||||
self.kind = kind
|
||||
|
||||
|
||||
def _worker_startup_failure(exc: BaseException) -> EmbeddedServerError:
|
||||
"""Return the bring-up error to surface for a worker-thread crash ``exc``.
|
||||
|
||||
An :class:`EmbeddedServerError` composed inside the worker — the dependency
|
||||
diagnosis, the SystemExit unwrap, the sentinel-connection refusal — is
|
||||
surfaced verbatim: it already carries both its message and its failure
|
||||
``kind``, and re-wrapping it is what left the caller logging
|
||||
"failed to start: HA-MCP in-process server failed to start: ...".
|
||||
"""
|
||||
if isinstance(exc, EmbeddedServerError):
|
||||
return exc
|
||||
failure = EmbeddedServerError(f"the worker thread crashed: {exc}")
|
||||
# Chained here rather than with ``raise ... from``: the caller raises this
|
||||
# object as-is, and the branch above must not chain an error to itself.
|
||||
failure.__cause__ = exc
|
||||
return failure
|
||||
|
||||
|
||||
def _install_log_filters_if_available() -> None:
|
||||
"""Attach the shared MCP SDK/fastmcp log-noise filters, if this ha-mcp has them.
|
||||
|
||||
Mirrors the ``register_browser_landing`` guard just above ``_serve``'s call
|
||||
site: the installed server version is user-controlled (channel choice,
|
||||
pip-spec override), so an older ha-mcp without ``ha_mcp.log_filters`` must
|
||||
keep serving -- the filters are simply absent there, as they are today.
|
||||
|
||||
Only a ``ModuleNotFoundError`` for exactly ``ha_mcp.log_filters`` is that
|
||||
"older ha-mcp" case and is swallowed silently. Anything else -- a version
|
||||
mismatch in a third-party dependency this import touches (fastmcp,
|
||||
pydantic), which ``_purge_ha_mcp_modules`` deliberately does NOT
|
||||
reinstall per config entry, so a stale one left over from a previous
|
||||
install can break this import -- is logged instead of silently doing
|
||||
nothing: log cosmetics must never block startup, but they also must
|
||||
never fail invisibly.
|
||||
"""
|
||||
try:
|
||||
from ha_mcp.log_filters import install_sdk_log_filters
|
||||
except ModuleNotFoundError as err:
|
||||
if err.name != "ha_mcp.log_filters":
|
||||
_LOGGER.warning(
|
||||
"Could not install MCP SDK log-noise filters (missing "
|
||||
"dependency %s); continuing without them: %s",
|
||||
err.name,
|
||||
err,
|
||||
)
|
||||
return
|
||||
except ImportError as err:
|
||||
_LOGGER.warning(
|
||||
"Could not install MCP SDK log-noise filters; continuing without them: %s",
|
||||
err,
|
||||
)
|
||||
return
|
||||
try:
|
||||
install_sdk_log_filters()
|
||||
except Exception as err:
|
||||
_LOGGER.warning(
|
||||
"Could not install MCP SDK log-noise filters; continuing without them: %s",
|
||||
err,
|
||||
)
|
||||
|
||||
|
||||
class EmbeddedServerManager:
|
||||
"""Manage the lifecycle of the in-process ha-mcp server for one config entry."""
|
||||
|
||||
@@ -264,6 +334,10 @@ class EmbeddedServerManager:
|
||||
self._pip_spec: str = self._resolve_pip_spec()
|
||||
self._secret_path: str = str(entry.data.get(DATA_SECRET_PATH, ""))
|
||||
self._config_dir: str = hass.config.path(SERVER_CONFIG_SUBDIR)
|
||||
# Home Assistant's own configuration directory, captured here on the
|
||||
# event loop: the dependency diagnosis scans custom_components/ from
|
||||
# the WORKER thread, which must never touch hass.
|
||||
self._hass_config_dir: str = hass.config.config_dir
|
||||
|
||||
# Worker-thread state. ``_loop`` and ``_stop_event`` are created in the
|
||||
# thread before its loop runs, so a stop request can always reach them.
|
||||
@@ -334,6 +408,7 @@ class EmbeddedServerManager:
|
||||
ready_version = await self._async_ensure_package(
|
||||
defer_mutations=_prune_and_check_importing_workers()
|
||||
)
|
||||
await self._async_warn_on_dependency_conflicts()
|
||||
access_token = await self._async_provision_token()
|
||||
await self._hass.async_add_executor_job(self._prepare_config_dir)
|
||||
|
||||
@@ -643,6 +718,14 @@ class EmbeddedServerManager:
|
||||
|
||||
async def _async_ensure_package(
|
||||
self, *, defer_mutations: bool = False
|
||||
) -> str | None:
|
||||
"""Use the externally managed package or ensure a managed install."""
|
||||
if self._hass.config.skip_pip:
|
||||
return await self._async_externally_managed_package_version()
|
||||
return await self._async_ensure_managed_package(defer_mutations=defer_mutations)
|
||||
|
||||
async def _async_ensure_managed_package(
|
||||
self, *, defer_mutations: bool = False
|
||||
) -> str | None:
|
||||
"""Ensure ``ha-mcp`` is importable, installing the pip spec if needed.
|
||||
|
||||
@@ -813,6 +896,85 @@ class EmbeddedServerManager:
|
||||
self._store_installed_spec()
|
||||
return version
|
||||
|
||||
async def _async_externally_managed_package_version(self) -> str:
|
||||
"""Validate and return the package supplied outside Home Assistant.
|
||||
|
||||
skip_pip means the surrounding system owns this interpreter. This
|
||||
path therefore performs metadata/importability reads only: it never
|
||||
calls Home Assistant's requirements manager, UV, or the config-entry
|
||||
marker writers used by manual and automatic installs.
|
||||
"""
|
||||
importable_version: str | None = await self._hass.async_add_executor_job(
|
||||
_installed_ha_mcp_version
|
||||
)
|
||||
stable_version: str | None = await self._hass.async_add_executor_job(
|
||||
_installed_dist_version, DIST_NAME_STABLE
|
||||
)
|
||||
dev_version: str | None = await self._hass.async_add_executor_job(
|
||||
_installed_dist_version, DIST_NAME_DEV
|
||||
)
|
||||
target_dist = dist_for_channel(self._channel)
|
||||
target_version = (
|
||||
stable_version if target_dist == DIST_NAME_STABLE else dev_version
|
||||
)
|
||||
other_dist = (
|
||||
DIST_NAME_DEV if target_dist == DIST_NAME_STABLE else DIST_NAME_STABLE
|
||||
)
|
||||
other_version = (
|
||||
dev_version if target_dist == DIST_NAME_STABLE else stable_version
|
||||
)
|
||||
|
||||
if importable_version is None:
|
||||
raise EmbeddedServerError(
|
||||
"Home Assistant was started with skip_pip enabled, so HA-MCP "
|
||||
"will not install the externally managed server package. Use "
|
||||
f"the system package manager to install {target_dist} "
|
||||
f"{MIN_EMBEDDED_SERVER_VERSION} or newer, then reload this "
|
||||
"integration.",
|
||||
kind="package",
|
||||
)
|
||||
|
||||
if stable_version is not None and dev_version is not None:
|
||||
raise EmbeddedServerError(
|
||||
f"Both {DIST_NAME_STABLE} {stable_version} and "
|
||||
f"{DIST_NAME_DEV} {dev_version} are installed while skip_pip "
|
||||
"is enabled. They share the ha_mcp import package, so HA-MCP "
|
||||
"cannot safely select one without modifying the environment. "
|
||||
"Use the system package manager to leave exactly one installed, "
|
||||
"then reload this integration.",
|
||||
kind="package",
|
||||
)
|
||||
|
||||
if target_version is None:
|
||||
raise EmbeddedServerError(
|
||||
f"The configured {self._channel} channel expects {target_dist}, "
|
||||
f"but only {other_dist} {other_version} is installed while "
|
||||
"skip_pip is enabled. Use the system package manager to install "
|
||||
f"{target_dist} {MIN_EMBEDDED_SERVER_VERSION} or newer, or "
|
||||
f"change the HA-MCP release channel to match {other_dist}, then "
|
||||
"reload this integration.",
|
||||
kind="package",
|
||||
)
|
||||
|
||||
if not _is_compatible_embedded_version(target_version):
|
||||
raise EmbeddedServerError(
|
||||
f"The externally managed {target_dist} {target_version} is "
|
||||
"incompatible while skip_pip is enabled; this in-process "
|
||||
f"component requires {MIN_EMBEDDED_SERVER_VERSION} or newer. "
|
||||
"Upgrade it with the system package manager, then reload this "
|
||||
"integration.",
|
||||
kind="package",
|
||||
)
|
||||
|
||||
_LOGGER.info(
|
||||
"HA-MCP externally managed %s package ready (version %s; "
|
||||
"skip_pip enabled, channel %s)",
|
||||
target_dist,
|
||||
target_version,
|
||||
self._channel,
|
||||
)
|
||||
return target_version
|
||||
|
||||
async def _async_remove_legacy_target(
|
||||
self, target_dist: str, installed_version: str | None
|
||||
) -> None:
|
||||
@@ -1208,6 +1370,154 @@ class EmbeddedServerManager:
|
||||
"""Create the server's persistent data directory (blocking)."""
|
||||
os.makedirs(self._config_dir, exist_ok=True)
|
||||
|
||||
# -- dependency diagnostics --------------------------------------------
|
||||
|
||||
async def _async_warn_on_dependency_conflicts(self) -> None:
|
||||
"""Log the dependency diagnosis when the installed graph is already broken.
|
||||
|
||||
Runs between the install step and the worker's first ``ha_mcp`` import,
|
||||
so a conflict a third-party integration reintroduces at every startup
|
||||
(issue #2239: a manifest pinning ``mcp==1.14.1`` drags the shared
|
||||
package below fastmcp's floor) is named in the log even when the import
|
||||
that follows still succeeds — it does whenever a good copy is already
|
||||
cached in ``sys.modules`` from before the downgrade.
|
||||
|
||||
Advisory only: no repair issue, no failure, and a diagnosis that itself
|
||||
fails is logged at debug and dropped. Bring-up continues either way.
|
||||
"""
|
||||
try:
|
||||
report = await self._hass.async_add_executor_job(
|
||||
self._dependency_conflict_report
|
||||
)
|
||||
except Exception:
|
||||
_LOGGER.debug("HA-MCP dependency pre-flight audit failed", exc_info=True)
|
||||
return
|
||||
if report is not None:
|
||||
_LOGGER.warning(
|
||||
"HA-MCP in-process server: the installed dependency tree is "
|
||||
"inconsistent. %s",
|
||||
report,
|
||||
)
|
||||
|
||||
def _dependency_conflict_report(self) -> str | None:
|
||||
"""Describe the installed graph's violations, or None when it is sound.
|
||||
|
||||
Blocking (metadata + manifest reads): call from an executor job or the
|
||||
worker thread, never from the event loop.
|
||||
"""
|
||||
violations = audit_dependency_graph(self._audit_dist_name())
|
||||
if not violations:
|
||||
return None
|
||||
return describe_dependency_failure(
|
||||
None, violations, self._pinning_integrations(violations)
|
||||
)
|
||||
|
||||
def _dependency_failure(self, err: BaseException) -> EmbeddedServerError | None:
|
||||
"""Re-diagnose an import crash as a dependency conflict (blocking).
|
||||
|
||||
Returns None when ``err``'s chain holds no ``ImportError`` — every
|
||||
other crash keeps its own error untouched. The chain, not the outermost
|
||||
exception, decides: fastmcp catches the real import failure and
|
||||
re-raises its generic "FastMCP server support is not installed" hint,
|
||||
which names neither the package nor the version that moved.
|
||||
|
||||
Best-effort by construction: the diagnosis must never replace the
|
||||
failure it explains, so any error inside it degrades to the root
|
||||
``ImportError`` text — still strictly better than the outer hint.
|
||||
"""
|
||||
root = root_import_failure(err)
|
||||
if not isinstance(root, ImportError):
|
||||
return None
|
||||
try:
|
||||
violations = audit_dependency_graph(self._audit_dist_name())
|
||||
detail = describe_dependency_failure(
|
||||
root, violations, self._pinning_integrations(violations)
|
||||
)
|
||||
except Exception:
|
||||
_LOGGER.warning(
|
||||
"HA-MCP dependency diagnostics failed; reporting the import "
|
||||
"error itself",
|
||||
exc_info=True,
|
||||
)
|
||||
detail = f"The server package failed to import: {root}"
|
||||
return EmbeddedServerError(detail, kind="package")
|
||||
|
||||
def _audit_dist_name(self) -> str:
|
||||
"""Distribution whose requirement graph the audit walks (blocking).
|
||||
|
||||
A pip-spec OVERRIDE names its root explicitly — the literal
|
||||
distribution its requirement installs (:func:`_override_dist_name`:
|
||||
a pin on ``ha-mcp``, or any ``name @ url`` form, names that
|
||||
distribution whatever the channel selector says), or ``ha-mcp`` for
|
||||
a bare URL, since a repository tarball installs under that name on
|
||||
every channel. An explicit root
|
||||
is returned even with its metadata missing: auditing it then
|
||||
reports that root as the violation — the true story when its
|
||||
install failed — where any fallback would walk a stale graph
|
||||
instead. Override installs skip the conflicting-channel removal, so
|
||||
stale metadata for the unselected distribution can coexist with the
|
||||
one actually running.
|
||||
|
||||
Without an override the channel's distribution is the root, falling
|
||||
back to the other channel's when only that one has metadata —
|
||||
auditing a root that is not installed reports the root itself
|
||||
instead of the real conflict.
|
||||
"""
|
||||
if self._pip_spec_override:
|
||||
named = _override_dist_name(self._pip_spec)
|
||||
if named is None and _spec_names_no_distribution(self._pip_spec):
|
||||
# A bare URL names nothing: prefer stable (a repository
|
||||
# tarball installs as ha-mcp on every channel) but accept
|
||||
# whichever known distribution actually has metadata — a
|
||||
# dev wheel URL installs ha-mcp-dev, and auditing the
|
||||
# absent stable would report a phantom missing root on
|
||||
# every healthy bring-up (Codex on #2245).
|
||||
named = next(
|
||||
(
|
||||
dist
|
||||
for dist in (DIST_NAME_STABLE, DIST_NAME_DEV)
|
||||
if _dist_installed(dist)
|
||||
),
|
||||
DIST_NAME_STABLE,
|
||||
)
|
||||
if named is not None:
|
||||
return named
|
||||
preferred = dist_for_channel(self._channel)
|
||||
if _dist_installed(preferred):
|
||||
return preferred
|
||||
other = DIST_NAME_STABLE if preferred == DIST_NAME_DEV else DIST_NAME_DEV
|
||||
return other if _dist_installed(other) else preferred
|
||||
|
||||
def _pinning_integrations(
|
||||
self, violations: list[DependencyViolation]
|
||||
) -> list[PinningIntegration]:
|
||||
"""Find the custom integrations forcing each violation (blocking).
|
||||
|
||||
The manifest scan matches by package name; the
|
||||
:func:`requirement_forces_conflict` filter then drops integrations
|
||||
whose requirement is compatible with what the violated specifier
|
||||
needs — naming those would send the user to uninstall an innocent
|
||||
integration. This component's own domain is excluded outright: it
|
||||
declares the server requirement legitimately and is never the pin
|
||||
to remove.
|
||||
"""
|
||||
pinners: list[PinningIntegration] = []
|
||||
by_package: dict[str, list[DependencyViolation]] = {}
|
||||
for violation in violations:
|
||||
by_package.setdefault(violation.package, []).append(violation)
|
||||
for package, package_violations in by_package.items():
|
||||
pinners.extend(
|
||||
pinner
|
||||
for pinner in find_pinning_integrations(
|
||||
self._hass_config_dir, package, exclude_domains=(DOMAIN,)
|
||||
)
|
||||
if any(
|
||||
requirement_forces_conflict(pinner.requirement, violation)
|
||||
for violation in package_violations
|
||||
)
|
||||
)
|
||||
return pinners
|
||||
|
||||
# -- worker thread -----------------------------------------------------
|
||||
|
||||
def _note_startup_phase(self, phase: str) -> None:
|
||||
@@ -1266,8 +1576,12 @@ class EmbeddedServerManager:
|
||||
detail,
|
||||
)
|
||||
except Exception as err:
|
||||
self._thread_exc = err
|
||||
_LOGGER.exception("HA-MCP in-process server thread crashed")
|
||||
# Published LAST, after the (blocking) diagnosis: the readiness
|
||||
# poll surfaces _thread_exc the moment it is set, so assigning the
|
||||
# raw error first would race the enriched one out of the repair
|
||||
# issue.
|
||||
self._thread_exc = self._dependency_failure(err) or err
|
||||
finally:
|
||||
with _IMPORTING_WORKERS_LOCK:
|
||||
_IMPORTING_WORKERS.discard(threading.current_thread())
|
||||
@@ -1398,6 +1712,12 @@ class EmbeddedServerManager:
|
||||
else:
|
||||
register_browser_landing(server.mcp, self._secret_path)
|
||||
|
||||
# Parity with the CLI HTTP runner: demote the MCP SDK/fastmcp log
|
||||
# noise (routine stateless teardown, benign tool-validation
|
||||
# tracebacks, disconnect-caused "session crashed" tracebacks) that
|
||||
# every other HTTP launcher already filters.
|
||||
_install_log_filters_if_available()
|
||||
|
||||
# Own the uvicorn server instead of calling mcp.run_async(): cancelling
|
||||
# run_async's task does NOT release the listening socket in-process
|
||||
# (live-found: the next bring-up failed with EADDRINUSE and uvicorn's
|
||||
@@ -1518,9 +1838,7 @@ class EmbeddedServerManager:
|
||||
last_signature = self._progress_signature()
|
||||
while True:
|
||||
if self._thread_exc is not None:
|
||||
raise EmbeddedServerError(
|
||||
f"HA-MCP in-process server failed to start: {self._thread_exc}"
|
||||
) from self._thread_exc
|
||||
raise _worker_startup_failure(self._thread_exc)
|
||||
if self._thread is not None and not self._thread.is_alive():
|
||||
raise EmbeddedServerError(
|
||||
"HA-MCP in-process server thread exited during startup."
|
||||
@@ -2147,6 +2465,38 @@ def _pin_moves_off_installed(spec: str, installed_version: str) -> bool:
|
||||
return not requirement.specifier.contains(installed_version, prereleases=True)
|
||||
|
||||
|
||||
def _override_dist_name(spec: str) -> str | None:
|
||||
"""Distribution name a pip-spec override installs, or None for a bare URL.
|
||||
|
||||
Unlike :meth:`EmbeddedServerManager._replaced_dist_name` — which maps
|
||||
onto the two known channel distributions for the replaced-source
|
||||
checks — the audit needs the LITERAL name: an override may install an
|
||||
arbitrary distribution (``acme-ha-mcp @ file:///pkg.whl``), and that
|
||||
distribution's graph is the one the worker imports (CodeRabbit on
|
||||
#2245).
|
||||
"""
|
||||
try:
|
||||
return Requirement(spec).name
|
||||
except InvalidRequirement:
|
||||
return None
|
||||
|
||||
|
||||
def _spec_names_no_distribution(spec: str) -> bool:
|
||||
"""True when ``spec`` does not parse as a PEP 508 requirement at all.
|
||||
|
||||
A bare URL (or any unparseable spec) names no distribution of its own;
|
||||
the same conservative shape test :func:`_scoped_install_flags` routes
|
||||
on. A ``name @ url`` form parses and is therefore False here — its name
|
||||
is already what :meth:`EmbeddedServerManager._replaced_dist_name`
|
||||
reports.
|
||||
"""
|
||||
try:
|
||||
Requirement(spec)
|
||||
except InvalidRequirement:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _spec_is_url_requirement(spec: str) -> bool:
|
||||
"""True when ``spec`` installs from a URL rather than an index.
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import secrets
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING
|
||||
from urllib.parse import urlparse
|
||||
@@ -32,6 +33,7 @@ from .const import (
|
||||
CHANNEL_DEV,
|
||||
COMPONENT_MANIFEST_AT_TAG_URL,
|
||||
DATA_BRINGUP_TASK,
|
||||
DATA_DCR_SIGNING_KEY,
|
||||
DATA_MANAGER,
|
||||
DATA_OAUTH_CLIENT_ID,
|
||||
DATA_OAUTH_CLIENT_SECRET,
|
||||
@@ -123,6 +125,16 @@ async def async_bring_up_server(hass: HomeAssistant, entry: ConfigEntry) -> None
|
||||
# Always set up the loopback forwarding config — the sidebar settings
|
||||
# panel proxies through it (#1803); the option gates only the public
|
||||
# webhook endpoint. oauth_* args are ignored unless auth_mode is legacy.
|
||||
if not entry.data.get(DATA_DCR_SIGNING_KEY):
|
||||
# Upgrade path: entries created before 2.0.0 have no DCR key.
|
||||
# entry.data must stay JSON-serializable, so store hex.
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data={
|
||||
**entry.data,
|
||||
DATA_DCR_SIGNING_KEY: secrets.token_bytes(32).hex(),
|
||||
},
|
||||
)
|
||||
oauth_restart_needed = await async_register_webhook(
|
||||
hass,
|
||||
entry,
|
||||
@@ -133,6 +145,7 @@ async def async_bring_up_server(hass: HomeAssistant, entry: ConfigEntry) -> None
|
||||
oauth_client_id=oauth_client_id,
|
||||
oauth_client_secret=oauth_client_secret,
|
||||
oauth_signing_key=entry.data.get(DATA_OAUTH_SIGNING_KEY),
|
||||
dcr_signing_key=entry.data.get(DATA_DCR_SIGNING_KEY),
|
||||
)
|
||||
_async_update_legacy_oauth_issue(hass, oauth_restart_needed)
|
||||
if not webhook_enabled:
|
||||
@@ -494,7 +507,9 @@ def _surface_connect_urls(
|
||||
"(Settings - Devices & Services - HA-MCP Custom Component - "
|
||||
"HA-MCP Server - Configure) and in the Home Assistant log - both "
|
||||
"administrator-only, because the URL is the credential.\n\n"
|
||||
f"{auth_note}\n"
|
||||
f"{auth_note}\n\n"
|
||||
"To disable this notification, uncheck the startup notification box "
|
||||
"on that same configuration screen.\n"
|
||||
)
|
||||
persistent_notification.async_create(
|
||||
hass,
|
||||
@@ -519,7 +534,7 @@ def _async_update_legacy_oauth_issue(hass: HomeAssistant, restart_needed: bool)
|
||||
hass,
|
||||
DOMAIN,
|
||||
ISSUE_LEGACY_OAUTH_RESTART,
|
||||
is_fixable=False,
|
||||
is_fixable=True,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=ISSUE_LEGACY_OAUTH_RESTART,
|
||||
)
|
||||
@@ -593,6 +608,10 @@ async def async_maybe_auto_update(
|
||||
logged at debug and skipped; the next refresh retries. Genuine bugs
|
||||
propagate per the repo's no-silent-failure convention.
|
||||
"""
|
||||
if hass.config.skip_pip:
|
||||
# The system package manager owns ha-mcp; never reload to mutate it.
|
||||
return
|
||||
|
||||
if not bool(entry.options.get(OPT_AUTO_UPDATE, DEFAULT_AUTO_UPDATE)):
|
||||
# Auto-update turned off: stay on the currently-installed version.
|
||||
return
|
||||
|
||||
@@ -26,7 +26,7 @@ change would otherwise warn forever about an advisory nicety.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
@@ -113,14 +113,15 @@ async def async_nudge_hacs_refresh(hass: HomeAssistant, target_version: str) ->
|
||||
|
||||
|
||||
async def _async_force_hacs_repo_refresh(hass: HomeAssistant) -> bool:
|
||||
"""Run HACS's "Update information" force-refresh for this component's repo.
|
||||
"""Run HACS's "Update information" force-refresh for this component's repos.
|
||||
|
||||
Returns True when a tracked repository was found and its refresh completed,
|
||||
False when there is nothing to refresh (no HACS, or no INSTALLED repository
|
||||
under either candidate name). Reaches into HACS internals —
|
||||
the top-level lookups are ``getattr``-guarded so a wholly different HACS
|
||||
shape returns False cleanly; anything deeper that changes shape raises and is
|
||||
swallowed by :func:`async_nudge_hacs_refresh`.
|
||||
Returns True when at least one INSTALLED tracked repository completed its
|
||||
refresh, False when there was nothing to refresh (no HACS, or no installed
|
||||
repository under either candidate name) or every candidate's refresh
|
||||
failed. Reaches into HACS internals — the top-level lookups are
|
||||
``getattr``-guarded so a wholly different HACS shape returns False cleanly;
|
||||
anything deeper that changes shape raises and is swallowed by
|
||||
:func:`async_nudge_hacs_refresh`.
|
||||
"""
|
||||
hacs = hass.data.get("hacs")
|
||||
if hacs is None:
|
||||
@@ -133,7 +134,7 @@ async def _async_force_hacs_repo_refresh(hass: HomeAssistant) -> bool:
|
||||
if get_by_full_name is None:
|
||||
return False
|
||||
|
||||
repository = None
|
||||
installed_candidates: list[Any] = []
|
||||
for full_name in _CANDIDATE_REPO_FULL_NAMES:
|
||||
candidate = get_by_full_name(full_name)
|
||||
if candidate is None:
|
||||
@@ -146,20 +147,41 @@ async def _async_force_hacs_repo_refresh(hass: HomeAssistant) -> bool:
|
||||
# only an installed candidate counts (review finding).
|
||||
if not getattr(getattr(candidate, "data", None), "installed", False):
|
||||
continue
|
||||
repository = candidate
|
||||
break
|
||||
if repository is None:
|
||||
installed_candidates.append(candidate)
|
||||
if not installed_candidates:
|
||||
return False
|
||||
|
||||
# The repository's "Update information" menu action: re-fetch its release
|
||||
# data ignoring cached state, then push the fresh data to HACS's own update
|
||||
# entity so Home Assistant advertises the component update immediately.
|
||||
await repository.update_repository(ignore_issues=True, force=True)
|
||||
# The refresh is complete at this point; the listener push below only
|
||||
# re-publishes the fresh data to HACS's update entity sooner. Guarded
|
||||
# separately so a HACS shape change here cannot void the completed
|
||||
# refresh's throttle and re-run the network fetch every pass (review
|
||||
# finding).
|
||||
# Every installed candidate, not just the first. Two entries read
|
||||
# installed when the mirror was downloaded without removing the legacy
|
||||
# record — HACS blocks adding the same repository twice but not two
|
||||
# repositories sharing a domain — and each carries its own update entity,
|
||||
# so refreshing only the first left the other stale.
|
||||
refreshed_any = False
|
||||
for repository in installed_candidates:
|
||||
# The repository's "Update information" menu action: re-fetch its
|
||||
# release data ignoring cached state.
|
||||
try:
|
||||
await repository.update_repository(ignore_issues=True, force=True)
|
||||
except Exception:
|
||||
# One candidate's failure must not skip the other's refresh.
|
||||
_LOGGER.debug(
|
||||
"HA-MCP: HACS refresh failed for one candidate repository",
|
||||
exc_info=True,
|
||||
)
|
||||
continue
|
||||
refreshed_any = True
|
||||
_poke_hacs_update_entity(hacs, repository)
|
||||
return refreshed_any
|
||||
|
||||
|
||||
def _poke_hacs_update_entity(hacs: Any, repository: Any) -> None:
|
||||
"""Push the freshly fetched data to HACS's own update entity.
|
||||
|
||||
The refresh is already complete when this runs; the push only makes Home
|
||||
Assistant advertise the component update sooner. Guarded separately so a
|
||||
HACS shape change here cannot void the completed refresh's throttle and
|
||||
re-run the network fetch every pass (review finding).
|
||||
"""
|
||||
try:
|
||||
coordinators = getattr(hacs, "coordinators", None) or {}
|
||||
category = getattr(getattr(repository, "data", None), "category", None)
|
||||
@@ -171,4 +193,3 @@ async def _async_force_hacs_repo_refresh(hass: HomeAssistant) -> bool:
|
||||
"HA-MCP: HACS listener push after the repository refresh failed",
|
||||
exc_info=True,
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -4,7 +4,7 @@ Before the dedicated HACS mirror (``homeassistant-ai/ha-mcp-integration``)
|
||||
existed, the README told users to add the MAIN ``ha-mcp`` server repository as
|
||||
a HACS custom repository. Those installs still work — HACS downloads the repo
|
||||
snapshot at the release tag, which contains this component — but HACS shows
|
||||
the SERVER's version numbers (``7.x``) and the server/add-on release notes in
|
||||
the SERVER's version numbers (``the server's``) and the server/add-on release notes in
|
||||
the update dialog, as if the component were the server itself. HACS has no
|
||||
repository-migration mechanism, so this population stays confused forever
|
||||
unless the component itself detects the legacy source and points them at the
|
||||
|
||||
@@ -49,17 +49,16 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import importlib
|
||||
import logging
|
||||
from collections.abc import AsyncIterator, Iterable
|
||||
from contextlib import asynccontextmanager
|
||||
from collections.abc import AsyncIterator, Callable, Iterable
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
from functools import cache
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import llm
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from voluptuous_openapi import convert_to_voluptuous
|
||||
|
||||
from .const import (
|
||||
DATA_LLM_API_UNSUB,
|
||||
@@ -72,6 +71,7 @@ from .const import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import httpx
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.util.json import JsonObjectType
|
||||
from mcp import types as mcp_types
|
||||
@@ -128,6 +128,25 @@ _SEARCH_TOOL_NAME = "ha_search_tools"
|
||||
_CALL_TOOL_NAME = "ha_call_tool"
|
||||
_SEARCH_RESULT_LIMIT = 8
|
||||
|
||||
|
||||
@cache
|
||||
def _schema_converter() -> Callable[[Any], Any]:
|
||||
"""Resolve the Core-provided schema converter once, off the event loop."""
|
||||
try:
|
||||
legacy = importlib.import_module("voluptuous_openapi")
|
||||
except ModuleNotFoundError as err:
|
||||
if err.name != "voluptuous_openapi":
|
||||
raise
|
||||
probatio = importlib.import_module("probatio")
|
||||
return cast(Callable[[Any], Any], probatio.from_openapi)
|
||||
return cast(Callable[[Any], Any], legacy.convert_to_voluptuous)
|
||||
|
||||
|
||||
def convert_to_voluptuous(schema: Any) -> vol.Schema:
|
||||
"""Convert an OpenAPI schema on stable and Probatio-based HA Core."""
|
||||
return cast(vol.Schema, _schema_converter()(schema))
|
||||
|
||||
|
||||
# Used when the server's initialize result carries no instructions (it always
|
||||
# should — ha-mcp ships server-level instructions — but never render an empty
|
||||
# prompt if a build does not).
|
||||
@@ -199,74 +218,152 @@ def _is_transport_failure(err: BaseException) -> bool:
|
||||
|
||||
|
||||
def _import_mcp_sdk() -> None:
|
||||
"""Import the mcp client SDK modules (blocking; run on the executor).
|
||||
"""Import lazy LLM dependencies (blocking; run on the executor).
|
||||
|
||||
Raises ImportError when the SDK is not importable — the caller decides
|
||||
whether that skips registration (SDK missing entirely) or surfaces as a
|
||||
conversation error.
|
||||
Raises ImportError when the MCP SDK or Core's schema converter is not
|
||||
importable — the caller decides whether that skips registration or
|
||||
surfaces as a conversation error.
|
||||
"""
|
||||
importlib.import_module("mcp.client.session")
|
||||
importlib.import_module("mcp.client.streamable_http")
|
||||
_schema_converter()
|
||||
|
||||
|
||||
async def async_probe_mcp_sdk(hass: HomeAssistant) -> bool:
|
||||
"""Return True when the mcp client SDK imports (first import off-loop)."""
|
||||
"""Return True when lazy LLM dependencies import (first import off-loop)."""
|
||||
try:
|
||||
await hass.async_add_executor_job(_import_mcp_sdk)
|
||||
except ImportError as err:
|
||||
_LOGGER.warning(
|
||||
"The installed server package provides no importable 'mcp' client "
|
||||
"SDK (%s); the conversation-agent LLM API will not be available",
|
||||
"A required LLM dependency is not importable (%s); the "
|
||||
"conversation-agent LLM API will not be available",
|
||||
err,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _loopback_httpx_client_factory(
|
||||
headers: dict[str, str] | None = None,
|
||||
timeout: httpx.Timeout | None = None,
|
||||
auth: httpx.Auth | None = None,
|
||||
) -> httpx.AsyncClient:
|
||||
"""``httpx_client_factory`` for the pre-rename SDK's ``streamablehttp_client``.
|
||||
|
||||
That deprecated entry point takes no ``http_client`` — it always builds
|
||||
its own via this factory — but the factory itself IS overridable, so the
|
||||
same ``verify=False`` / ``trust_env=False`` posture as the canonical path
|
||||
in :func:`_mcp_session` still applies: this fallback is loopback-only
|
||||
too, so a real SSL context is pure waste and this call must never be
|
||||
diverted through an environment proxy (which would also leak the URL's
|
||||
embedded ``secret_path`` to it).
|
||||
|
||||
``timeout`` mirrors ``create_mcp_http_client`` (the factory this
|
||||
substitutes for): a ``None`` here means "no timeout was supplied", not
|
||||
"disable timeouts" — the real ``streamablehttp_client`` caller always
|
||||
passes an explicit ``httpx.Timeout``, but a bare ``None`` reaching
|
||||
``httpx.AsyncClient`` directly disables every timeout outright (review
|
||||
finding), which is the wrong zero-argument default for a fallback aimed
|
||||
at unknown old environments.
|
||||
|
||||
The substitute value is inlined rather than imported from
|
||||
``mcp.shared._httpx_utils``: this factory only ever runs on SDKs old
|
||||
enough to lack ``streamable_http_client`` (the canonical name this
|
||||
module tries first), and ``MCP_DEFAULT_TIMEOUT`` /
|
||||
``MCP_DEFAULT_SSE_READ_TIMEOUT`` don't exist before mcp 1.24 either — an
|
||||
import would raise on every SDK version this fallback actually serves
|
||||
(round-2 review finding: the first attempt at this fix broke the exact
|
||||
path it was meant to harden). ``create_mcp_http_client``'s own ``None``
|
||||
default on those older SDKs is this same flat ``httpx.Timeout(30.0)``,
|
||||
with no separate read timeout (the ``read=300`` shape is itself a 1.24+
|
||||
addition) — matched here rather than guessed at.
|
||||
"""
|
||||
import httpx
|
||||
|
||||
if timeout is None:
|
||||
timeout = httpx.Timeout(30.0)
|
||||
|
||||
return httpx.AsyncClient(
|
||||
headers=headers, timeout=timeout, auth=auth, verify=False, trust_env=False
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _mcp_session(
|
||||
url: str,
|
||||
http_client: Any = None,
|
||||
) -> AsyncIterator[tuple[ClientSession, mcp_types.InitializeResult]]:
|
||||
"""Open an initialized MCP session against the loopback server.
|
||||
|
||||
Imports resolve from ``sys.modules`` — :func:`async_probe_mcp_sdk` did the
|
||||
real (blocking) import on the executor before the API was registered.
|
||||
|
||||
``http_client`` is Home Assistant's shared httpx client
|
||||
(``helpers.httpx_client.get_async_client``). Passing it is what keeps
|
||||
this loop-safe: without it the SDK constructs its own httpx client per
|
||||
session, whose SSL setup loads the CA bundle SYNCHRONOUSLY inside HA's
|
||||
event loop (live-found — HA's blocking-call monitor flagged this exact
|
||||
line). HA's shared client is built against the process-cached SSL
|
||||
context, and the SDK does not close caller-owned clients (HA core's mcp
|
||||
integration relies on the same contract).
|
||||
Builds a throwaway httpx client scoped to this one session rather than
|
||||
reusing Home Assistant's shared one (``helpers.httpx_client.
|
||||
get_async_client`` — the prior approach): the SDK applies NO timeout of
|
||||
its own when a caller-provided client is passed, so whatever timeout
|
||||
THAT client happens to carry becomes the real wire-level ceiling. HA's
|
||||
shared client is built with no explicit ``timeout=``, so it silently
|
||||
carries httpx's own hardcoded 5-second default — capping every tool call
|
||||
at 5 seconds of read-idle no matter how generous
|
||||
``_CALL_TOOL_TIMEOUT_SECONDS`` / ``_LIST_TOOLS_TIMEOUT_SECONDS`` looked
|
||||
(live-found investigating a ~60s Assist-pipeline hang: a real tool doing
|
||||
real work never got anywhere near its own asyncio budget).
|
||||
|
||||
``verify=False`` is not a security relaxation: ``url`` is always
|
||||
``http://127.0.0.1:<port>...`` (see ``async_register_llm_api``) — a
|
||||
plain-HTTP loopback call that never negotiates TLS — so building a real
|
||||
SSL context would be pure waste. It also keeps this loop-safe the same
|
||||
way the shared client did: an SSL context built with ``verify=True``
|
||||
loads the system CA bundle SYNCHRONOUSLY (live-found — HA's
|
||||
blocking-call monitor flagged this exact line when the SDK built its own
|
||||
default client), and skipping verification skips that load entirely.
|
||||
|
||||
``trust_env=False`` for the same "this is loopback, not the network"
|
||||
reason: httpx defaults to reading ``HTTP_PROXY``/``NO_PROXY`` from the
|
||||
environment, and ``127.0.0.1`` is not exempt unless ``NO_PROXY``
|
||||
explicitly lists it. Under an ``HTTP_PROXY`` that doesn't, this call
|
||||
would leave the loopback listener entirely and go out through the
|
||||
configured proxy instead — which also hands the proxy ``url``'s
|
||||
embedded ``secret_path`` (the private endpoint credential). Disabling
|
||||
env trust removes both failure modes; nothing here should ever consult
|
||||
a proxy.
|
||||
|
||||
The client is entered on the exit stack so it closes with the rest of
|
||||
the session.
|
||||
"""
|
||||
from mcp.client.session import ClientSession
|
||||
|
||||
try:
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
async with AsyncExitStack() as stack:
|
||||
try:
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
except ImportError:
|
||||
# Pre-rename SDK (an older ha-mcp resolved by a pip-spec override
|
||||
# pins an older fastmcp/mcp): same call shape, deprecated name,
|
||||
# and no http_client kwarg — but it does accept a factory for the
|
||||
# client it builds internally, so _loopback_httpx_client_factory
|
||||
# keeps this fallback on the same verify=False/trust_env=False
|
||||
# posture as the canonical path below.
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
|
||||
transport = (
|
||||
streamable_http_client(url=url, http_client=http_client)
|
||||
if http_client is not None
|
||||
else streamable_http_client(url=url)
|
||||
transport = streamablehttp_client(
|
||||
url=url, httpx_client_factory=_loopback_httpx_client_factory
|
||||
)
|
||||
else:
|
||||
import httpx
|
||||
|
||||
http_client = await stack.enter_async_context(
|
||||
httpx.AsyncClient(
|
||||
verify=False,
|
||||
trust_env=False,
|
||||
timeout=httpx.Timeout(_CALL_TOOL_TIMEOUT_SECONDS),
|
||||
)
|
||||
)
|
||||
transport = streamable_http_client(url=url, http_client=http_client)
|
||||
|
||||
read_stream, write_stream, _ = await stack.enter_async_context(transport)
|
||||
session = await stack.enter_async_context(
|
||||
ClientSession(read_stream, write_stream)
|
||||
)
|
||||
except ImportError:
|
||||
# Pre-rename SDK (an older ha-mcp resolved by a pip-spec override
|
||||
# pins an older fastmcp/mcp): same call shape, deprecated name, but
|
||||
# no http_client kwarg — it builds its own client, so on those old
|
||||
# SDKs the blocking-SSL-setup warning is the accepted cost.
|
||||
from mcp.client.streamable_http import (
|
||||
streamablehttp_client,
|
||||
)
|
||||
|
||||
transport = streamablehttp_client(url=url)
|
||||
|
||||
async with (
|
||||
transport as (read_stream, write_stream, _),
|
||||
ClientSession(read_stream, write_stream) as session,
|
||||
):
|
||||
init_result = await session.initialize()
|
||||
yield session, init_result
|
||||
|
||||
@@ -350,7 +447,7 @@ async def _forward_tool_call(
|
||||
try:
|
||||
async with (
|
||||
asyncio.timeout(_CALL_TOOL_TIMEOUT_SECONDS),
|
||||
_mcp_session(server_url, get_async_client(hass)) as (session, _init),
|
||||
_mcp_session(server_url) as (session, _init),
|
||||
):
|
||||
result = await session.call_tool(name, arguments)
|
||||
except _transport_errors() as err:
|
||||
@@ -496,7 +593,7 @@ class HaMcpLlmApi(llm.API):
|
||||
try:
|
||||
async with (
|
||||
asyncio.timeout(_LIST_TOOLS_TIMEOUT_SECONDS),
|
||||
_mcp_session(self.server_url, get_async_client(self.hass)) as (
|
||||
_mcp_session(self.server_url) as (
|
||||
session,
|
||||
init_result,
|
||||
),
|
||||
@@ -538,9 +635,7 @@ class HaMcpLlmApi(llm.API):
|
||||
def _convert_parameters(self, tool: Any) -> vol.Schema | None:
|
||||
"""Convert one tool's JSON schema, or None (logged) when it fails."""
|
||||
try:
|
||||
# cast: voluptuous_openapi is an untyped (ignored) import, so the
|
||||
# call returns Any; its documented return type is vol.Schema.
|
||||
return cast(vol.Schema, convert_to_voluptuous(tool.inputSchema))
|
||||
return convert_to_voluptuous(tool.inputSchema)
|
||||
except Exception:
|
||||
# One unconvertible schema must not take down the whole
|
||||
# toolset for the conversation — skip that tool, loudly.
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"requirements": [
|
||||
"ruamel.yaml>=0.18.0"
|
||||
],
|
||||
"version": "1.3.2"
|
||||
"version": "2.1.0"
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ Three auth postures, chosen in the options flow:
|
||||
* ``ha_auth`` — Home Assistant core is the OAuth authorization server. This
|
||||
module serves the RFC 8414 / RFC 9728 discovery documents (so claude.ai /
|
||||
ChatGPT can sign in with the user's HA account) and validates inbound bearer
|
||||
tokens via ``hass.auth``. There is no bespoke authorization-server code here —
|
||||
every protocol step is HA core's own ``/auth/*``.
|
||||
tokens via ``hass.auth``. The component-scoped authorize/token views redirect
|
||||
and forward into core's own ``/auth/*`` endpoints; core remains the authority.
|
||||
* ``legacy`` — this module (via :mod:`oauth_legacy`) is its own OAuth 2.1
|
||||
authorization server with a static client_id/secret, for MCP clients (Google
|
||||
Gemini Spark) that need a credential to paste rather than an HA sign-in.
|
||||
@@ -59,16 +59,18 @@ from .const import (
|
||||
)
|
||||
from .oauth_autoapprove import (
|
||||
CFG_AUTOAPPROVE_PROVIDER,
|
||||
CFG_CIMD_SESSION,
|
||||
AutoApproveProvider,
|
||||
bind_autoapprove_views,
|
||||
)
|
||||
from .oauth_dcr import CFG_DCR_SIGNING_KEY, bind_dcr_view
|
||||
from .oauth_legacy import (
|
||||
AUTHORIZE_PATH,
|
||||
OAUTH_ROUTE_OWNER_KEY,
|
||||
TOKEN_PATH,
|
||||
LegacyOAuthProvider,
|
||||
LegacyOAuthRouteConflict,
|
||||
bind_legacy_views,
|
||||
build_unbound_legacy_provider,
|
||||
clear_scoped_legacy_credentials,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -110,6 +112,11 @@ _ALLOWED_CONTENT_TYPES = ("application/json", "text/event-stream", "text/plain")
|
||||
# long-lived streams fails a new request in 30 s instead of hanging it forever.
|
||||
_CLIENT_TIMEOUT = aiohttp.ClientTimeout(connect=30, sock_connect=10, sock_read=300)
|
||||
|
||||
# Anonymous CIMD lookups get a separate, deliberately small connection pool.
|
||||
# The relay session may hold long-lived SSE connections; public metadata fetches
|
||||
# must never consume that authenticated forwarding capacity.
|
||||
_CIMD_CONNECTOR_LIMIT = 4
|
||||
|
||||
# TOP-LEVEL hass.data flag recording that the ha_auth discovery views are bound
|
||||
# for this HA session. Deliberately NOT under DOMAIN so it survives
|
||||
# async_unload_entry's teardown — aiohttp cannot unregister an HTTP view until HA
|
||||
@@ -140,19 +147,31 @@ def _build_base_url(request: web.Request) -> str:
|
||||
|
||||
|
||||
def _authorization_server_document(base: str) -> dict[str, Any]:
|
||||
"""RFC 8414 authorization-server metadata pointing at HA core's OAuth.
|
||||
"""RFC 8414 metadata for ha_auth mode — component-owned endpoints only.
|
||||
|
||||
Advertises HA core's own ``/auth/authorize`` + ``/auth/token`` as a public
|
||||
client (``token_endpoint_auth_methods_supported: ["none"]``) and
|
||||
``client_id_metadata_document_supported`` so clients present a URL-shaped
|
||||
``client_id`` (CIMD) that HA core's long-standing IndieAuth handling accepts —
|
||||
the user never pastes a credential. No ``registration_endpoint``: HA offers no
|
||||
dynamic client registration; CIMD replaces it.
|
||||
HA core is still the authorization server (the scoped /authorize 302s into
|
||||
core's /auth/authorize and /token forwards server-side — see
|
||||
oauth_autoapprove's ha_auth branches), but every URL a client can CACHE is
|
||||
ours: a later auth-mode switch re-dispatches per request instead of
|
||||
stranding the client on core paths we can never retract (#2188's
|
||||
stickiness). ``registration_endpoint`` serves DCR-fallback brokers;
|
||||
both CIMD-selection flags stay pinned (see
|
||||
test_as_documents_pin_the_claude_cimd_selection_contract).
|
||||
|
||||
``revocation_endpoint`` is ours for a second reason (#2248): the refresh
|
||||
token the client holds is a signed envelope, and core's own
|
||||
``/auth/revoke`` answers 200 without revoking anything for a value it
|
||||
cannot recognise. Only ha_auth mints those, so only this document
|
||||
advertises it. The endpoint takes no client authentication, matching
|
||||
``token_endpoint_auth_methods_supported``.
|
||||
"""
|
||||
return {
|
||||
"issuer": f"{base}{OAUTH_BASE}",
|
||||
"authorization_endpoint": f"{base}/auth/authorize",
|
||||
"token_endpoint": f"{base}/auth/token",
|
||||
"authorization_endpoint": f"{base}{OAUTH_BASE}/authorize",
|
||||
"token_endpoint": f"{base}{OAUTH_BASE}/token",
|
||||
"registration_endpoint": f"{base}{OAUTH_BASE}/register",
|
||||
"revocation_endpoint": f"{base}{OAUTH_BASE}/revoke",
|
||||
"revocation_endpoint_auth_methods_supported": ["none"],
|
||||
"response_types_supported": ["code"],
|
||||
"grant_types_supported": ["authorization_code", "refresh_token"],
|
||||
"code_challenge_methods_supported": ["S256"],
|
||||
@@ -316,15 +335,18 @@ def issuer_for_request(request: web.Request) -> str:
|
||||
|
||||
|
||||
def _legacy_authorization_server_document(base: str) -> dict[str, Any]:
|
||||
"""RFC 8414 authorization-server metadata for legacy mode's own root
|
||||
``/authorize`` + ``/token`` views (see :mod:`oauth_legacy`)."""
|
||||
"""RFC 8414 metadata for legacy mode's component-scoped endpoints.
|
||||
|
||||
The root ``/authorize`` + ``/token`` views remain bound as aliases for
|
||||
metadata-ignoring clients (see :mod:`oauth_legacy`).
|
||||
"""
|
||||
return {
|
||||
"issuer": oauth_issuer(base),
|
||||
# RFC 9207 §3: authorization responses carry ``iss`` (oauth_legacy's
|
||||
# redirects); omission reads as "not supported" to discovery clients.
|
||||
"authorization_response_iss_parameter_supported": True,
|
||||
"authorization_endpoint": f"{base}{AUTHORIZE_PATH}",
|
||||
"token_endpoint": f"{base}{TOKEN_PATH}",
|
||||
"authorization_endpoint": f"{base}{OAUTH_BASE}/authorize",
|
||||
"token_endpoint": f"{base}{OAUTH_BASE}/token",
|
||||
"response_types_supported": ["code"],
|
||||
"grant_types_supported": ["authorization_code", "refresh_token"],
|
||||
"code_challenge_methods_supported": ["S256"],
|
||||
@@ -356,6 +378,7 @@ def _none_mode_authorization_server_document(base: str) -> dict[str, Any]:
|
||||
"authorization_response_iss_parameter_supported": True,
|
||||
"authorization_endpoint": f"{base}{OAUTH_BASE}/authorize",
|
||||
"token_endpoint": f"{base}{OAUTH_BASE}/token",
|
||||
"registration_endpoint": f"{base}{OAUTH_BASE}/register",
|
||||
"response_types_supported": ["code"],
|
||||
"grant_types_supported": ["authorization_code"],
|
||||
"code_challenge_methods_supported": ["S256"],
|
||||
@@ -400,8 +423,8 @@ class _ProtectedResourceMetadataView(HomeAssistantView):
|
||||
class _AuthorizationServerMetadataView(HomeAssistantView):
|
||||
"""RFC 8414 Authorization Server Metadata.
|
||||
|
||||
Mode-aware: ha_auth points at HA core's own ``/auth/*``; legacy points at
|
||||
this module's root ``/authorize``/``/token`` views.
|
||||
Every mode advertises the component-scoped authorize/token pair; the bound
|
||||
views dispatch each request according to the currently active mode.
|
||||
"""
|
||||
|
||||
requires_auth = False
|
||||
@@ -668,6 +691,103 @@ async def _async_handle_webhook(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _bind_ha_auth_surface(
|
||||
hass: HomeAssistant,
|
||||
cfg: dict[str, Any],
|
||||
webhook_id: str,
|
||||
dcr_signing_key: str | None,
|
||||
) -> None:
|
||||
"""Bind the ha_auth surface (fail-closed) and mark cfg's live providers."""
|
||||
provider = ResourceServer(hass, webhook_id)
|
||||
_register_metadata_views(hass)
|
||||
bind_autoapprove_views(hass)
|
||||
bind_dcr_view(hass)
|
||||
cfg["resource_server"] = provider
|
||||
if dcr_signing_key:
|
||||
cfg[CFG_DCR_SIGNING_KEY] = bytes.fromhex(dcr_signing_key)
|
||||
|
||||
|
||||
def _bind_legacy_surface(
|
||||
hass: HomeAssistant,
|
||||
cfg: dict[str, Any],
|
||||
oauth_client_id: str | None,
|
||||
oauth_client_secret: str | None,
|
||||
oauth_signing_key: str | None,
|
||||
) -> bool:
|
||||
"""Bind the legacy surface (fail-closed); return oauth_restart_needed.
|
||||
|
||||
A root-route conflict with the Webhook Proxy add-on is survivable since the
|
||||
unified scoped endpoints carry legacy's advertised URLs — downgrade it to a
|
||||
warning and serve scoped-only via an unbound provider.
|
||||
"""
|
||||
if not (oauth_client_id and oauth_client_secret and oauth_signing_key):
|
||||
raise ValueError(
|
||||
"legacy webhook auth mode requires oauth_client_id, "
|
||||
"oauth_client_secret, and oauth_signing_key"
|
||||
)
|
||||
_register_metadata_views(hass)
|
||||
bind_autoapprove_views(hass)
|
||||
oauth_restart_needed = False
|
||||
try:
|
||||
oauth_provider, oauth_restart_needed = bind_legacy_views(
|
||||
hass, oauth_client_id, oauth_client_secret, oauth_signing_key
|
||||
)
|
||||
except LegacyOAuthRouteConflict:
|
||||
# Log the owner read directly from hass.data, NOT the exception object:
|
||||
# the exception flowed through code holding the client secret, and
|
||||
# CodeQL's taint tracking flags logging it as clear-text sensitive-data
|
||||
# (#2213 gate). The owner string is what the message needs anyway.
|
||||
_LOGGER.warning(
|
||||
"HA-MCP: the Webhook Proxy add-on ('%s') owns the root "
|
||||
"/authorize and /token routes; legacy OAuth will serve "
|
||||
"only on %s/authorize + %s/token (metadata-honoring "
|
||||
"clients are unaffected; metadata-ignoring clients that "
|
||||
"guess root paths reach the add-on instead).",
|
||||
hass.data.get(OAUTH_ROUTE_OWNER_KEY),
|
||||
OAUTH_BASE,
|
||||
OAUTH_BASE,
|
||||
)
|
||||
oauth_provider = build_unbound_legacy_provider(
|
||||
hass, oauth_client_id, oauth_client_secret, oauth_signing_key
|
||||
)
|
||||
cfg["oauth_provider"] = oauth_provider
|
||||
return oauth_restart_needed
|
||||
|
||||
|
||||
def _bind_none_surface(
|
||||
hass: HomeAssistant, cfg: dict[str, Any], dcr_signing_key: str | None
|
||||
) -> None:
|
||||
"""Bind the none-mode auto-approve surface — FAILS OPEN.
|
||||
|
||||
The secret webhook URL is the credential; this discovery surface is an
|
||||
enhancement layered on a webhook that otherwise always forwards, so a
|
||||
failure here must NOT tear down the unauthenticated endpoint (issue #1978)
|
||||
— it only means claude.ai's rare OAuth-discovery fallback goes unassisted.
|
||||
Both view bundles bind at most once per HA session; per-request resolvers
|
||||
gate them on cfg, so a none<->ha_auth switch needs no restart (#1969).
|
||||
The DCR key parses before the provider is assigned, so ANY failure —
|
||||
including a corrupt key — leaves the whole surface inactive (plain proxy)
|
||||
rather than half-enabled.
|
||||
"""
|
||||
try:
|
||||
_register_metadata_views(hass)
|
||||
bind_autoapprove_views(hass)
|
||||
bind_dcr_view(hass)
|
||||
# Key BEFORE provider (#2213 review): a bad key raises here and leaves
|
||||
# BOTH unset — plain proxy — instead of a half-enabled surface whose
|
||||
# advertised /register 404s while /authorize auto-approves.
|
||||
if dcr_signing_key:
|
||||
cfg[CFG_DCR_SIGNING_KEY] = bytes.fromhex(dcr_signing_key)
|
||||
cfg[CFG_AUTOAPPROVE_PROVIDER] = AutoApproveProvider()
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"MCP webhook: failed to set up none-mode auto-approve "
|
||||
"discovery; continuing as a plain unauthenticated proxy "
|
||||
"(the webhook still forwards — only claude.ai's rare "
|
||||
"OAuth-discovery fallback is unassisted)."
|
||||
)
|
||||
|
||||
|
||||
async def async_register_webhook(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
@@ -679,6 +799,7 @@ async def async_register_webhook(
|
||||
oauth_client_id: str | None = None,
|
||||
oauth_client_secret: str | None = None,
|
||||
oauth_signing_key: str | None = None,
|
||||
dcr_signing_key: str | None = None,
|
||||
) -> bool:
|
||||
"""Register the ingress webhook (and, for ha_auth/legacy, the OAuth surface).
|
||||
|
||||
@@ -710,6 +831,10 @@ async def async_register_webhook(
|
||||
raise ValueError(f"Unknown webhook auth mode: {auth_mode!r}")
|
||||
|
||||
webhook_id: str = entry.data[DATA_WEBHOOK_ID]
|
||||
# The scoped-only provider is held in this registration's cfg and stops
|
||||
# serving on every reload/unload. Its top-level credential fingerprint must
|
||||
# follow that lifetime rather than surviving a switch away from legacy.
|
||||
clear_scoped_legacy_credentials(hass)
|
||||
# Reload-safe and off-means-off: clear any leftover registration from a
|
||||
# crashed unload before (re)registering — or before storing a local-only
|
||||
# config (async_unregister is a no-op pop when nothing is registered).
|
||||
@@ -717,15 +842,28 @@ async def async_register_webhook(
|
||||
async_unregister(hass, webhook_id)
|
||||
target_url = f"http://127.0.0.1:{port}{secret_path}"
|
||||
session = aiohttp.ClientSession(timeout=_CLIENT_TIMEOUT)
|
||||
cimd_session: aiohttp.ClientSession | None = None
|
||||
if register_endpoint and auth_mode == WEBHOOK_AUTH_HA:
|
||||
try:
|
||||
cimd_session = aiohttp.ClientSession(
|
||||
connector=aiohttp.TCPConnector(limit=_CIMD_CONNECTOR_LIMIT)
|
||||
)
|
||||
except Exception:
|
||||
# Creating the isolated pool is part of fail-closed ha_auth setup.
|
||||
# Do not leak the already-open relay session if it fails.
|
||||
await session.close()
|
||||
raise
|
||||
|
||||
cfg: dict[str, Any] = {
|
||||
"webhook_id": webhook_id,
|
||||
"target_url": target_url,
|
||||
"session": session,
|
||||
CFG_CIMD_SESSION: cimd_session,
|
||||
"auth_mode": auth_mode,
|
||||
"resource_server": None,
|
||||
"oauth_provider": None,
|
||||
CFG_AUTOAPPROVE_PROVIDER: None,
|
||||
CFG_DCR_SIGNING_KEY: None,
|
||||
}
|
||||
|
||||
oauth_restart_needed = False
|
||||
@@ -740,60 +878,15 @@ async def async_register_webhook(
|
||||
allowed_methods=["POST", "GET"],
|
||||
)
|
||||
if auth_mode == WEBHOOK_AUTH_HA:
|
||||
provider = ResourceServer(hass, webhook_id)
|
||||
_register_metadata_views(hass)
|
||||
cfg["resource_server"] = provider
|
||||
_bind_ha_auth_surface(hass, cfg, webhook_id, dcr_signing_key)
|
||||
elif auth_mode == WEBHOOK_AUTH_LEGACY:
|
||||
if not (oauth_client_id and oauth_client_secret and oauth_signing_key):
|
||||
raise ValueError(
|
||||
"legacy webhook auth mode requires oauth_client_id, "
|
||||
"oauth_client_secret, and oauth_signing_key"
|
||||
)
|
||||
_register_metadata_views(hass)
|
||||
try:
|
||||
oauth_provider, oauth_restart_needed = bind_legacy_views(
|
||||
hass, oauth_client_id, oauth_client_secret, oauth_signing_key
|
||||
)
|
||||
except LegacyOAuthRouteConflict as err:
|
||||
raise ValueError(
|
||||
"The Webhook Proxy add-on (or its dev flavor) already "
|
||||
f"owns the root /authorize and /token routes ({err}). "
|
||||
"Stop that add-on and restart Home Assistant, then "
|
||||
"enable legacy mode again."
|
||||
) from err
|
||||
cfg["oauth_provider"] = oauth_provider
|
||||
oauth_restart_needed = _bind_legacy_surface(
|
||||
hass, cfg, oauth_client_id, oauth_client_secret, oauth_signing_key
|
||||
)
|
||||
else:
|
||||
# WEBHOOK_AUTH_NONE (the only remaining mode — unknown modes
|
||||
# already raised above). The secret webhook URL is the
|
||||
# credential, but we still serve our own corrected discovery +
|
||||
# an invisible auto-approve authorization server so claude.ai's
|
||||
# intermittent OAuth discovery resolves against us instead of HA
|
||||
# core's broken origin-root document, and completes with no HA
|
||||
# login (issue #1969). Both view bundles bind at most once per
|
||||
# HA session; the per-request resolvers gate them on this cfg,
|
||||
# so a none<->ha_auth switch needs no restart.
|
||||
#
|
||||
# Fails OPEN, unlike ha_auth/legacy (issue #1978): none mode is
|
||||
# intentionally unauthenticated, and this discovery is an
|
||||
# enhancement layered on a webhook that (already registered
|
||||
# above) otherwise always forwards. A failure here must NOT fall
|
||||
# through to the outer teardown and take down a webhook the user
|
||||
# configured to need no auth — it only means claude.ai's rare
|
||||
# OAuth-discovery fallback goes unassisted. Mirrors the add-on's
|
||||
# _setup_none_autoapprove. Provider is assigned last, so a
|
||||
# partial bind leaves none-autoapprove inactive (plain proxy)
|
||||
# rather than half-enabled.
|
||||
try:
|
||||
_register_metadata_views(hass)
|
||||
bind_autoapprove_views(hass)
|
||||
cfg[CFG_AUTOAPPROVE_PROVIDER] = AutoApproveProvider()
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"MCP webhook: failed to set up none-mode auto-approve "
|
||||
"discovery; continuing as a plain unauthenticated proxy "
|
||||
"(the webhook still forwards — only claude.ai's rare "
|
||||
"OAuth-discovery fallback is unassisted)."
|
||||
)
|
||||
# already raised above).
|
||||
_bind_none_surface(hass, cfg, dcr_signing_key)
|
||||
except Exception:
|
||||
# Never leave a live endpoint (or a leaked session) behind a failed
|
||||
# auth-setup path. suppress: the ORIGINAL error must be what
|
||||
@@ -802,6 +895,10 @@ async def async_register_webhook(
|
||||
async_unregister(hass, webhook_id)
|
||||
with suppress(Exception):
|
||||
await session.close()
|
||||
if cimd_session is not None:
|
||||
with suppress(Exception):
|
||||
await cimd_session.close()
|
||||
clear_scoped_legacy_credentials(hass)
|
||||
raise
|
||||
|
||||
# A PRIOR registration this HA session may still own the legacy root views
|
||||
@@ -826,6 +923,7 @@ async def async_unregister_webhook(hass: HomeAssistant) -> None:
|
||||
restarts); they 404 while their mode is not live (see ``active_auth_mode``
|
||||
/ ``LegacyOAuthProvider.is_active``).
|
||||
"""
|
||||
clear_scoped_legacy_credentials(hass)
|
||||
domain_data = hass.data.get(DOMAIN)
|
||||
if not isinstance(domain_data, dict):
|
||||
return
|
||||
@@ -838,3 +936,6 @@ async def async_unregister_webhook(hass: HomeAssistant) -> None:
|
||||
session = cfg.get("session")
|
||||
if session is not None:
|
||||
await session.close()
|
||||
cimd_session = cfg.get(CFG_CIMD_SESSION)
|
||||
if cimd_session is not None:
|
||||
await cimd_session.close()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""None-mode auto-approve OAuth authorization server (issue #1969).
|
||||
"""Unified scoped OAuth authorization endpoints (issue #1969).
|
||||
|
||||
In ``none`` webhook auth mode the secret webhook URL *is* the credential, so no
|
||||
bearer is required and the forwarder always returns 200. But claude.ai's
|
||||
@@ -11,11 +11,11 @@ through to Home Assistant *core*'s own origin-root
|
||||
``registration_endpoint``. claude.ai then can neither use CIMD nor do dynamic
|
||||
client registration and shows "Automatic client registration isn't supported…".
|
||||
|
||||
This module is the none-mode fix's authorization-server half: a pair of
|
||||
path-scoped ``OAUTH_BASE`` endpoints that complete OAuth *invisibly* — no login,
|
||||
no consent — so a connector that does run discovery resolves against our own
|
||||
corrected documents (served by :mod:`mcp_webhook`) instead of HA core's broken
|
||||
root doc, and connects with zero HA login:
|
||||
This module owns three path-scoped ``OAUTH_BASE`` endpoints. In none mode the
|
||||
authorize/token pair completes OAuth *invisibly* — no login, no consent — so a
|
||||
connector that does run discovery resolves against our own corrected documents
|
||||
(served by :mod:`mcp_webhook`) instead of HA core's broken root doc, and
|
||||
connects with zero HA login:
|
||||
|
||||
* ``GET {OAUTH_BASE}/authorize`` issues a PKCE-bound one-time code and
|
||||
immediately 302-redirects back to the client with ``?code=…&state=…`` — no
|
||||
@@ -24,31 +24,34 @@ root doc, and connects with zero HA login:
|
||||
``client_secret``) for an opaque access token. The token is *cosmetic* — none
|
||||
mode ignores bearers entirely — but is a real random string so a spec-strict
|
||||
client is satisfied.
|
||||
* ``POST {OAUTH_BASE}/revoke`` fronts RFC 7009 revocation for ha_auth mode, the
|
||||
only mode that hands the client a signed refresh envelope core cannot redeem
|
||||
(#2248); it 404s in the other two.
|
||||
|
||||
Both views are gated per request off ``hass.data`` (they 404 unless none mode is
|
||||
the live webhook auth mode), mirroring the discovery views, so a
|
||||
``none``\\ ↔\\ ``ha_auth`` switch needs no restart. The PKCE code store and the
|
||||
redirect-URI floor are reused from :mod:`oauth_legacy` rather than copied.
|
||||
The views dispatch per request from ``hass.data`` to the live legacy, ha_auth,
|
||||
or none-mode provider (and 404 when no remote OAuth mode is live), mirroring the
|
||||
discovery views so mode switches need no restart. The none-mode PKCE code store
|
||||
and redirect-URI floor are reused from :mod:`oauth_legacy` rather than copied.
|
||||
|
||||
**Open-redirect defence.** ``/authorize`` 302-redirects to a caller-supplied
|
||||
``redirect_uri`` on the Home Assistant origin, so an unvalidated target would be
|
||||
an open redirector. On top of :func:`oauth_legacy._is_valid_redirect_uri`'s
|
||||
scheme/host/port floor, the redirect must EXACTLY match a known MCP callback
|
||||
(:data:`_AUTOAPPROVE_REDIRECT_ALLOWLIST`). Anything else is a hard 400 (no
|
||||
redirect). A "same origin as the client_id" rule was deliberately NOT used: the
|
||||
client_id is fully attacker-controlled, so ``client_id == redirect_uri origin``
|
||||
still lets an attacker bounce a victim to any site of their choosing (a real
|
||||
open redirect on a public HA origin). Properly honouring an arbitrary CIMD
|
||||
client would require fetching the attacker-supplied client_id URL — an SSRF
|
||||
vector — so the allowlist is both the safe and the simple choice. Add a client's
|
||||
callback here to support it.
|
||||
**Open-redirect policy.** In none mode THE SECRET WEBHOOK URL IS THE MAIN AND
|
||||
ONLY FORM OF SECURITY. The OAuth surface exists purely for client compatibility;
|
||||
its tokens grant nothing. ``/authorize`` therefore serves every provider and
|
||||
302-redirects to any spec-valid ``redirect_uri``; malformed targets still hard
|
||||
400 under :func:`oauth_legacy._is_valid_redirect_uri`. This makes the Home
|
||||
Assistant origin usable as a crafted-link redirector, an accepted risk in the
|
||||
secret-URL trust model. An exact-match callback allowlist shipped in PR #1976
|
||||
in July 2026; it was retired on 2026-08-14 by maintainer decision to serve every
|
||||
provider.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import secrets
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
@@ -60,10 +63,14 @@ from .oauth_legacy import (
|
||||
PKCECodeStore,
|
||||
_is_valid_redirect_uri,
|
||||
_issuer_for,
|
||||
read_form,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
from multidict import MultiDict
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# cfg (hass.data[DOMAIN][DATA_WEBHOOK]) key holding the live AutoApproveProvider.
|
||||
@@ -72,22 +79,24 @@ if TYPE_CHECKING:
|
||||
# mode (mirrors the "resource_server"/"oauth_provider" presence keys).
|
||||
CFG_AUTOAPPROVE_PROVIDER = "autoapprove_provider"
|
||||
|
||||
# TOP-LEVEL hass.data flag recording that the two auto-approve views are bound
|
||||
# for this HA session. Not under DOMAIN so it survives async_unload_entry's
|
||||
# Dedicated aiohttp session for anonymous CIMD fetches in ha_auth mode.
|
||||
# Keeping it separate from the relay session prevents slow public metadata
|
||||
# endpoints from consuming the pool used by authenticated MCP forwarding.
|
||||
CFG_CIMD_SESSION = "cimd_session"
|
||||
|
||||
# TOP-LEVEL hass.data flag recording that the three unified scoped views are
|
||||
# bound for this HA session. Not under DOMAIN so it survives async_unload_entry's
|
||||
# teardown — aiohttp cannot unregister a bound view until HA restarts, so the
|
||||
# views (and this ownership flag) must outlive the config entry (mirrors
|
||||
# mcp_webhook._OAUTH_VIEWS_REGISTERED_KEY).
|
||||
_AUTOAPPROVE_VIEWS_REGISTERED_KEY = "ha_mcp_tools_oauth_autoapprove_views_registered"
|
||||
|
||||
# Known MCP OAuth callback URLs always accepted as a redirect target even when
|
||||
# the client_id is not a same-origin URL — claude.ai's connector onboarding
|
||||
# posts its authorization code here. Exact-match only (never a prefix test, so
|
||||
# ``https://claude.ai/api/mcp/auth_callback.evil.example`` cannot slip through).
|
||||
_AUTOAPPROVE_REDIRECT_ALLOWLIST = frozenset(
|
||||
{
|
||||
"https://claude.ai/api/mcp/auth_callback",
|
||||
}
|
||||
)
|
||||
|
||||
# RFC 7009 §2.2.1: on a 503 from a revocation endpoint "the client must assume
|
||||
# the token still exists and may retry after a reasonable delay", and the server
|
||||
# MAY name that delay. Seconds, as a short transient — a client left to guess may
|
||||
# simply give up, and an abandoned revocation leaves the session live (#2248).
|
||||
_REVOKE_RETRY_AFTER = "5"
|
||||
|
||||
|
||||
def _json_not_found() -> web.Response:
|
||||
@@ -105,21 +114,6 @@ def _json_error(
|
||||
return web.json_response(body, status=status, headers=_TOKEN_RESPONSE_HEADERS)
|
||||
|
||||
|
||||
def _is_valid_autoapprove_redirect(redirect_uri: str) -> bool:
|
||||
"""Open-redirect gate for the auto-approve ``/authorize`` view.
|
||||
|
||||
Exact-match allowlist only, on top of
|
||||
:func:`oauth_legacy._is_valid_redirect_uri`'s scheme/host/port floor. The
|
||||
``client_id`` is NOT consulted: it is attacker-controlled, so validating the
|
||||
redirect against it (even "same origin") does not constrain the redirect
|
||||
target to a trusted host. See the module docstring.
|
||||
"""
|
||||
return (
|
||||
_is_valid_redirect_uri(redirect_uri)
|
||||
and redirect_uri in _AUTOAPPROVE_REDIRECT_ALLOWLIST
|
||||
)
|
||||
|
||||
|
||||
def _redirect_with(redirect_uri: str, **params: str) -> web.Response:
|
||||
"""302 to ``redirect_uri`` with ``params`` merged into its query string."""
|
||||
# yarl ships with aiohttp and handles existing-query merging + encoding
|
||||
@@ -162,6 +156,15 @@ class AutoApproveProvider:
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def _webhook_cfg(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
"""The live webhook cfg dict, or None when the entry is not set up."""
|
||||
domain_data = hass.data.get(DOMAIN)
|
||||
if not isinstance(domain_data, dict):
|
||||
return None
|
||||
cfg = domain_data.get(DATA_WEBHOOK)
|
||||
return cfg if isinstance(cfg, dict) else None
|
||||
|
||||
|
||||
def _active_autoapprove_provider(hass: HomeAssistant) -> AutoApproveProvider | None:
|
||||
"""The live none-mode auto-approve provider, or None when it is not live.
|
||||
|
||||
@@ -169,23 +172,44 @@ def _active_autoapprove_provider(hass: HomeAssistant) -> AutoApproveProvider | N
|
||||
bound views serve only while none-autoapprove is the active mode and 404
|
||||
otherwise — mirrors ``mcp_webhook._active_webhook_id``'s per-request gating.
|
||||
"""
|
||||
domain_data = hass.data.get(DOMAIN)
|
||||
if not isinstance(domain_data, dict):
|
||||
return None
|
||||
cfg = domain_data.get(DATA_WEBHOOK)
|
||||
if not isinstance(cfg, dict):
|
||||
cfg = _webhook_cfg(hass)
|
||||
if cfg is None:
|
||||
return None
|
||||
provider = cfg.get(CFG_AUTOAPPROVE_PROVIDER)
|
||||
return provider if isinstance(provider, AutoApproveProvider) else None
|
||||
|
||||
|
||||
class AutoApproveAuthorizeView(HomeAssistantView):
|
||||
"""None-mode auto-approve ``/authorize`` — issues a code, 302s, no UI.
|
||||
def _validate_autoapprove_authorize(params: Any) -> web.Response | None:
|
||||
"""Validate the none-mode /authorize query; a 400 Response, or None if OK.
|
||||
|
||||
Validates ``response_type=code``, PKCE S256, and the redirect_uri
|
||||
open-redirect gate, then issues a PKCE-bound one-time code and redirects
|
||||
straight back to the client. No login page and no consent screen render, so
|
||||
claude.ai's OAuth flow completes invisibly (issue #1969).
|
||||
Maintainer decision 2026-08-14 (supersedes the #1969-era exact-match
|
||||
allowlist): none mode's ONLY credential is the secret webhook URL, so the
|
||||
auto-approve flow completes invisibly for ANY spec-valid redirect — the
|
||||
token it yields is cosmetic and grants nothing. The HA origin being usable
|
||||
as a crafted-link redirector via this anonymous endpoint is an accepted
|
||||
trade within that trust model. The spec floor (_is_valid_redirect_uri:
|
||||
https or RFC 8252 loopback, valid port, no fragment) still hard-400s
|
||||
malformed targets without redirecting.
|
||||
"""
|
||||
if params.get("response_type", "") != "code":
|
||||
return _json_error("unsupported_response_type", 400)
|
||||
if params.get("code_challenge_method", "") != "S256":
|
||||
return _json_error("invalid_request", 400, "code_challenge_method must be S256")
|
||||
if not _PKCE_CHALLENGE_RE.fullmatch(params.get("code_challenge", "")):
|
||||
return _json_error(
|
||||
"invalid_request", 400, "invalid code_challenge (43-char base64url)"
|
||||
)
|
||||
if not _is_valid_redirect_uri(params.get("redirect_uri", "")):
|
||||
return _json_error("invalid_request", 400, "invalid redirect_uri")
|
||||
return None
|
||||
|
||||
|
||||
class AutoApproveAuthorizeView(HomeAssistantView):
|
||||
"""Unified scoped ``/authorize`` dispatcher for every remote auth mode.
|
||||
|
||||
Legacy mode serves the shared consent flow, ha_auth redirects into core,
|
||||
and none mode validates PKCE plus the redirect gate before issuing a code
|
||||
and redirecting invisibly (issue #1969).
|
||||
|
||||
ACCEPTED RISK (issue #1978): this endpoint is anonymous by design — none
|
||||
mode requires zero HA login — so it consults neither the webhook id nor a
|
||||
@@ -210,34 +234,29 @@ class AutoApproveAuthorizeView(HomeAssistantView):
|
||||
self._hass = hass
|
||||
|
||||
async def get(self, request: web.Request) -> web.Response:
|
||||
"""Auto-approve the authorization request or reject with a 400/404."""
|
||||
provider = _active_autoapprove_provider(self._hass)
|
||||
if provider is None:
|
||||
"""Dispatch the authorization request to the active mode."""
|
||||
cfg = _webhook_cfg(self._hass)
|
||||
if cfg is None:
|
||||
return _json_not_found()
|
||||
legacy_provider = cfg.get("oauth_provider")
|
||||
if legacy_provider is not None:
|
||||
from .oauth_legacy import handle_legacy_authorize_get
|
||||
|
||||
return await handle_legacy_authorize_get(legacy_provider, request)
|
||||
if cfg.get("resource_server") is not None:
|
||||
return await self._ha_auth_authorize(cfg, request)
|
||||
provider = cfg.get(CFG_AUTOAPPROVE_PROVIDER)
|
||||
if not isinstance(provider, AutoApproveProvider):
|
||||
return _json_not_found()
|
||||
|
||||
params = request.query
|
||||
response_type = params.get("response_type", "")
|
||||
redirect_uri = params.get("redirect_uri", "")
|
||||
state = params.get("state", "")
|
||||
code_challenge = params.get("code_challenge", "")
|
||||
code_challenge_method = params.get("code_challenge_method", "")
|
||||
|
||||
if response_type != "code":
|
||||
return _json_error("unsupported_response_type", 400)
|
||||
if code_challenge_method != "S256":
|
||||
return _json_error(
|
||||
"invalid_request", 400, "code_challenge_method must be S256"
|
||||
)
|
||||
if not _PKCE_CHALLENGE_RE.match(code_challenge):
|
||||
return _json_error(
|
||||
"invalid_request", 400, "invalid code_challenge (43-char base64url)"
|
||||
)
|
||||
# SECURITY: an unvalidated redirect_uri would be an open redirector on
|
||||
# the HA origin. Reject in-place (never redirect) unless it exactly
|
||||
# matches a known MCP callback (client_id is attacker-controlled and is
|
||||
# deliberately not consulted — see module docstring).
|
||||
if not _is_valid_autoapprove_redirect(redirect_uri):
|
||||
return _json_error("invalid_request", 400, "invalid redirect_uri")
|
||||
err = _validate_autoapprove_authorize(params)
|
||||
if err is not None:
|
||||
return err
|
||||
|
||||
# RFC 9207: every authorization response — success or error — names the
|
||||
# issuer that produced it, so a client registered with several
|
||||
@@ -256,14 +275,282 @@ class AutoApproveAuthorizeView(HomeAssistantView):
|
||||
redirect_params["state"] = state
|
||||
return _redirect_with(redirect_uri, **redirect_params)
|
||||
|
||||
async def _ha_auth_authorize(
|
||||
self, cfg: dict[str, Any], request: web.Request
|
||||
) -> web.Response:
|
||||
"""302 the browser into core's /auth/authorize (ha_auth indirection).
|
||||
|
||||
The user logs in on core's own page exactly as before; only the URL the
|
||||
client learned is ours. client_id is upgraded via CIMD/DCR validation
|
||||
when possible, else passed through untouched (core stays the authority).
|
||||
"""
|
||||
from multidict import MultiDict
|
||||
|
||||
from .oauth_dcr import CFG_DCR_SIGNING_KEY
|
||||
from .oauth_ha_auth import resolve_forward_client_id
|
||||
|
||||
# MultiDict copy: repeated OAuth params (e.g. RFC 8707 ``resource``)
|
||||
# must survive the forward — a plain dict() collapses them.
|
||||
params = MultiDict(request.query)
|
||||
client_id = params.get("client_id", "")
|
||||
redirect_uri = params.get("redirect_uri", "")
|
||||
forward_id = await resolve_forward_client_id(
|
||||
cfg.get(CFG_CIMD_SESSION),
|
||||
cfg.get(CFG_DCR_SIGNING_KEY),
|
||||
client_id,
|
||||
redirect_uri,
|
||||
)
|
||||
if forward_id != client_id:
|
||||
params.popall("client_id", None)
|
||||
params["client_id"] = forward_id
|
||||
import yarl
|
||||
|
||||
# Keep the browser hop relative, matching the token leg. Browsers cannot
|
||||
# be made to send X-Forwarded-Host, so this is consistency rather than a
|
||||
# vulnerability fix.
|
||||
target = yarl.URL("/auth/authorize").with_query(params)
|
||||
return web.Response(status=302, headers={"Location": str(target)})
|
||||
|
||||
async def post(self, request: web.Request) -> web.Response:
|
||||
"""Handle a legacy-mode consent submission on the scoped route."""
|
||||
cfg = _webhook_cfg(self._hass)
|
||||
if cfg is None:
|
||||
return _json_not_found()
|
||||
legacy_provider = cfg.get("oauth_provider")
|
||||
if legacy_provider is None:
|
||||
return _json_not_found()
|
||||
from .oauth_legacy import handle_legacy_authorize_post
|
||||
|
||||
return await handle_legacy_authorize_post(legacy_provider, request)
|
||||
|
||||
|
||||
def _revoke_rewrite(dcr_key: bytes | None, form: MultiDict) -> bool:
|
||||
"""Swap an envelope back for core's own token on a revocation (#2248).
|
||||
|
||||
Core takes a revocation two ways — ``action=revoke&token=…`` on
|
||||
``/auth/token`` (the IndieAuth 6.3.5 shape, which core marks deprecated in
|
||||
favour of the view below but keeps for backwards compat) and the RFC 7009
|
||||
``/auth/revoke`` view — and BOTH answer 200 even for a token they have
|
||||
never seen, so a client revoking the envelope we handed it would get a
|
||||
success it did not receive. The single place that knows how a revocation
|
||||
token is unwrapped, shared by the token view's ``action=revoke`` branch and
|
||||
:class:`AutoApproveRevokeView`. RFC 7009 authorizes the BEARER of the token
|
||||
rather than a client identity, so the presenter binding is deliberately
|
||||
skipped here. Returns whether the request must now be proxied (core has to
|
||||
see its own token).
|
||||
|
||||
BOTH a verified envelope and a prefixed-but-unverifiable one are proxied
|
||||
(#2249 review by Patch76). The unverifiable case is the ordinary one: every
|
||||
envelope minted before the DCR signing key rotated — which is what
|
||||
removing and re-adding the integration does — fails its MAC, and treating
|
||||
it as "not ours" would 307 it to core, whose revoke endpoint answers 200
|
||||
for any token it cannot resolve. The client would be told the session was
|
||||
revoked while core's grant stayed live for its full 90 days. Forwarding a
|
||||
body we did not verify is sound HERE and nowhere else: possession is the
|
||||
only authorization a revocation needs, and core's endpoint is anonymous
|
||||
and idempotent, so it grants a forger nothing they could not do by POSTing
|
||||
to core directly. The refresh path keeps answering an INVALID envelope
|
||||
locally and never forwards it.
|
||||
"""
|
||||
from .oauth_ha_auth import core_token_for_revocation
|
||||
|
||||
if dcr_key is None:
|
||||
return False
|
||||
core_refresh_token = core_token_for_revocation(dcr_key, str(form.get("token", "")))
|
||||
if core_refresh_token is None:
|
||||
return False
|
||||
form.popall("token", None)
|
||||
form["token"] = core_refresh_token
|
||||
return True
|
||||
|
||||
|
||||
def _envelope_identity(
|
||||
dcr_key: bytes | None, form: MultiDict, client_id: str
|
||||
) -> tuple[str, bool] | web.Response | None:
|
||||
"""Read a refresh grant's identity out of its signed envelope (#2248).
|
||||
|
||||
None when the presented token carries no envelope (``ABSENT``) or no DCR
|
||||
key is configured — the caller then falls through to the pre-envelope
|
||||
derivation. A verified envelope rewrites ``form``'s ``refresh_token`` to
|
||||
core's own token and forces the proxy leg, because a 307 would hand core a
|
||||
value it cannot redeem. An ``INVALID`` one is answered locally for the same
|
||||
reason: core cannot redeem it either, and relaying it would only feed its
|
||||
failed-login accounting.
|
||||
"""
|
||||
from .oauth_ha_auth import EnvelopeState, unwrap_refresh_token
|
||||
|
||||
if dcr_key is None:
|
||||
return None
|
||||
envelope = unwrap_refresh_token(
|
||||
dcr_key, str(form.get("refresh_token", "")), client_id
|
||||
)
|
||||
if isinstance(envelope, tuple):
|
||||
core_refresh_token, forward_id = envelope
|
||||
form.popall("refresh_token", None)
|
||||
form["refresh_token"] = core_refresh_token
|
||||
return forward_id, True
|
||||
if envelope is EnvelopeState.INVALID:
|
||||
_LOGGER.warning(
|
||||
"ha_auth refresh: signed envelope failed verification for "
|
||||
"client_id %s — the DCR signing key may have changed, or the "
|
||||
"token was replayed under another client_id or tampered with",
|
||||
client_id,
|
||||
)
|
||||
return _json_error(
|
||||
"invalid_grant",
|
||||
400,
|
||||
"re-authorize: this refresh token could not be verified against "
|
||||
"this server's current signing key",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
async def _pre_envelope_refresh_identity(
|
||||
cfg: dict[str, Any], dcr_key: bytes | None, client_id: str
|
||||
) -> tuple[str, bool] | web.Response:
|
||||
"""Re-derive the identity of a redirect-less PRE-envelope refresh (#2248).
|
||||
|
||||
Only ``EnvelopeState.ABSENT`` tokens reach here — minted before the
|
||||
envelope shipped, or presented while no DCR key is configured.
|
||||
"""
|
||||
from .oauth_ha_auth import RefreshDisposition, translated_client_id_for_refresh
|
||||
|
||||
translated = await translated_client_id_for_refresh(
|
||||
cfg.get(CFG_CIMD_SESSION),
|
||||
dcr_key,
|
||||
client_id,
|
||||
)
|
||||
if translated is RefreshDisposition.UNREPRODUCIBLE:
|
||||
# The token was bound to an origin nothing here can name, so core
|
||||
# would reject it; answering locally keeps a guaranteed failure out
|
||||
# of core's failed-login accounting. Registration still advertises
|
||||
# refresh_token for these clients — one re-authorize mints an
|
||||
# envelope-carrying token that refreshes from then on.
|
||||
return _json_error(
|
||||
"invalid_grant",
|
||||
400,
|
||||
"re-authorize once: this refresh token predates the signed "
|
||||
"identity envelope, and its client's registration names no single "
|
||||
"reproducible web origin to re-derive it from",
|
||||
)
|
||||
if translated is RefreshDisposition.PASSTHROUGH:
|
||||
return client_id, False
|
||||
return translated, False
|
||||
|
||||
|
||||
async def _code_leg_forces_proxy(
|
||||
cfg: dict[str, Any], dcr_key: bytes | None, client_id: str
|
||||
) -> bool:
|
||||
"""Whether an UNTRANSLATED code exchange must still be proxied (#2248).
|
||||
|
||||
The authorize leg's same-origin fast path returns the client_id without
|
||||
fetching its CIMD document, which is right for that leg — but it means a
|
||||
hybrid identity (redirects across two web origins, one of them same-origin
|
||||
with the client_id) reaches core untranslated and gets core's RAW refresh
|
||||
token back. Its redirect-less refresh then lands in
|
||||
``translated_client_id_for_refresh``, which DOES fetch, sees the split
|
||||
origins and answers UNREPRODUCIBLE — a local invalid_grant forever, under
|
||||
a message promising that re-authorizing fixes it.
|
||||
|
||||
So the code leg pays the one CIMD fetch the fast path skipped: an
|
||||
unreproducible identity is proxied instead of 307'd, its ``refresh_token``
|
||||
comes back wrapped with forward id == client_id, and the refresh leg
|
||||
proxies that same pair to core, which accepts it. A failed fetch or a
|
||||
reproducible identity degrades to PASSTHROUGH and the 307, exactly as
|
||||
before. Without a DCR key there is nothing to sign the envelope with, so
|
||||
proxying would buy nothing.
|
||||
"""
|
||||
from .oauth_ha_auth import RefreshDisposition, translated_client_id_for_refresh
|
||||
|
||||
if dcr_key is None:
|
||||
return False
|
||||
try:
|
||||
if urlparse(client_id).scheme != "https":
|
||||
return False
|
||||
except ValueError:
|
||||
# Malformed client_id: core stays the authority (the authorize leg
|
||||
# makes the same call on an anonymous view).
|
||||
return False
|
||||
disposition = await translated_client_id_for_refresh(
|
||||
cfg.get(CFG_CIMD_SESSION),
|
||||
dcr_key,
|
||||
client_id,
|
||||
)
|
||||
return disposition is RefreshDisposition.UNREPRODUCIBLE
|
||||
|
||||
|
||||
def _unavailable(description: str, *, revocation: bool) -> web.Response:
|
||||
"""A 503 for a failed forward, carrying ``Retry-After`` on a revocation.
|
||||
|
||||
RFC 7009 §2.2.1 gives that status a specific meaning on a revocation
|
||||
endpoint — the client must assume the token still exists and may retry
|
||||
after a delay the server MAY name — and a client should not get a
|
||||
different answer for spelling the same revocation as ``action=revoke`` on
|
||||
``/token`` rather than posting it to the scoped ``/revoke`` view
|
||||
(#2249 review by Patch76). Plain token failures stay bare: RFC 6749 gives
|
||||
them no such retry contract.
|
||||
"""
|
||||
response = _json_error("temporarily_unavailable", 503, description)
|
||||
if revocation:
|
||||
response.headers["Retry-After"] = _REVOKE_RETRY_AFTER
|
||||
return response
|
||||
|
||||
|
||||
async def _forward_to_core(
|
||||
hass: HomeAssistant, cfg: dict[str, Any], path: str, form: MultiDict
|
||||
) -> tuple[int, bytes, str] | web.Response:
|
||||
"""POST ``form`` to core's ``path`` server-side; its raw answer or a 503.
|
||||
|
||||
Shared by the token and revocation legs (#2248): both forward a rewritten
|
||||
credential-bearing form to core over the relay session and map the same
|
||||
transport failures, and only the token leg rewrites what comes back.
|
||||
|
||||
Whether this is a revocation is read from the request itself rather than
|
||||
passed in, so both surfaces — the scoped ``/revoke`` view and ``/token``
|
||||
with ``action=revoke`` — get the same 503, and neither call site has to
|
||||
remember to say so.
|
||||
"""
|
||||
from .oauth_ha_auth import core_token_base_url
|
||||
|
||||
revocation = path == "/auth/revoke" or form.get("action") == "revoke"
|
||||
session = cfg.get("session")
|
||||
if session is None:
|
||||
_LOGGER.warning(
|
||||
"ha_auth %s forward: the entry has no relay session "
|
||||
"(half-initialised setup); answering 503",
|
||||
path,
|
||||
)
|
||||
return _unavailable("token forwarding is not available", revocation=revocation)
|
||||
base = core_token_base_url(hass)
|
||||
try:
|
||||
async with session.post(
|
||||
f"{base}{path}",
|
||||
data=form,
|
||||
timeout=aiohttp.ClientTimeout(total=25),
|
||||
) as resp:
|
||||
return (
|
||||
resp.status,
|
||||
await resp.read(),
|
||||
resp.content_type or "application/json",
|
||||
)
|
||||
except (TimeoutError, aiohttp.ClientError) as err:
|
||||
_LOGGER.warning(
|
||||
"ha_auth %s forward to %s failed: %s",
|
||||
path,
|
||||
base,
|
||||
type(err).__name__,
|
||||
)
|
||||
return _unavailable(
|
||||
"core did not answer the token request", revocation=revocation
|
||||
)
|
||||
|
||||
|
||||
class AutoApproveTokenView(HomeAssistantView):
|
||||
"""None-mode auto-approve ``/token`` — PKCE code → opaque access token.
|
||||
"""Unified scoped ``/token`` dispatcher for every remote auth mode.
|
||||
|
||||
Public client (no ``client_secret``): the PKCE code_verifier is the only
|
||||
proof required. The returned access token is cosmetic (none mode ignores
|
||||
bearers), but real and opaque. Only the ``authorization_code`` grant is
|
||||
supported — none mode has no refresh cycle.
|
||||
Legacy mode uses the shared credentialed token handlers, ha_auth forwards
|
||||
into core, and none mode exchanges a PKCE code as a public client for a
|
||||
cosmetic opaque token (none mode ignores bearers and has no refresh cycle).
|
||||
"""
|
||||
|
||||
requires_auth = False
|
||||
@@ -276,12 +563,25 @@ class AutoApproveTokenView(HomeAssistantView):
|
||||
self._hass = hass
|
||||
|
||||
async def post(self, request: web.Request) -> web.Response:
|
||||
"""Exchange a PKCE authorization code for an opaque access token."""
|
||||
provider = _active_autoapprove_provider(self._hass)
|
||||
if provider is None:
|
||||
"""Dispatch the token request to the active mode."""
|
||||
cfg = _webhook_cfg(self._hass)
|
||||
if cfg is None:
|
||||
return _json_not_found()
|
||||
legacy_provider = cfg.get("oauth_provider")
|
||||
if legacy_provider is not None:
|
||||
from .oauth_legacy import handle_legacy_token_post
|
||||
|
||||
return await handle_legacy_token_post(legacy_provider, request)
|
||||
if cfg.get("resource_server") is not None:
|
||||
return await self._ha_auth_token(cfg, request)
|
||||
provider = cfg.get(CFG_AUTOAPPROVE_PROVIDER)
|
||||
if not isinstance(provider, AutoApproveProvider):
|
||||
return _json_not_found()
|
||||
|
||||
form: dict[str, Any] = dict(await request.post())
|
||||
raw_form = await read_form(request)
|
||||
if raw_form is None:
|
||||
return _json_error("invalid_request", 400)
|
||||
form: dict[str, Any] = dict(raw_form)
|
||||
if form.get("grant_type", "") != "authorization_code":
|
||||
return _json_error("unsupported_grant_type", 400)
|
||||
|
||||
@@ -302,25 +602,264 @@ class AutoApproveTokenView(HomeAssistantView):
|
||||
headers=_TOKEN_RESPONSE_HEADERS,
|
||||
)
|
||||
|
||||
async def _ha_auth_token(
|
||||
self, cfg: dict[str, Any], request: web.Request
|
||||
) -> web.Response:
|
||||
"""Route the token exchange to core: 307 by default, proxy if translating.
|
||||
|
||||
Untranslated identities are 307-redirected to core's own /auth/token so
|
||||
core sees the client's real address (its wrong-login notifications, ban
|
||||
counters, trusted_networks refresh validation, and last_used_ip all key
|
||||
on request.remote — #2213 review). Only translated identities (the body
|
||||
must be rewritten) are forwarded server-side; the translation matches
|
||||
the authorize leg, and every forwarded 200 that is a grant response
|
||||
comes back with its ``refresh_token`` wrapped in the signed envelope
|
||||
that makes the next refresh resolvable (#2248).
|
||||
"""
|
||||
from multidict import MultiDict
|
||||
|
||||
from .oauth_dcr import CFG_DCR_SIGNING_KEY
|
||||
|
||||
raw_form = await read_form(request)
|
||||
if raw_form is None:
|
||||
return _json_error("invalid_request", 400)
|
||||
# str()-coerce every value: request.post() also yields bytes and
|
||||
# FileField on a multipart body, and those reach the outgoing
|
||||
# session.post(data=form) serializer, which raises TypeError — an
|
||||
# anonymous 500 (#2219 codex review). Repeated keys are preserved.
|
||||
form: MultiDict = MultiDict(
|
||||
(key, str(value)) for key, value in raw_form.items()
|
||||
)
|
||||
dcr_key = cfg.get(CFG_DCR_SIGNING_KEY)
|
||||
client_id = str(form.get("client_id", ""))
|
||||
resolved = await self._ha_auth_forward_identity(cfg, form, client_id, dcr_key)
|
||||
if isinstance(resolved, web.Response):
|
||||
return resolved
|
||||
forward_id, proxy_required = resolved
|
||||
if forward_id == client_id and not proxy_required:
|
||||
# No body rewrite needed, so don't proxy: 307 the client into
|
||||
# core's own /auth/token on the same public origin it just used.
|
||||
# Core then observes the CLIENT's address, which it uses for more
|
||||
# than logging (#2213 review by Patch76): process_wrong_login
|
||||
# notifications and login_attempts_threshold ban counters on
|
||||
# failed exchanges, trusted_networks refresh-token validation,
|
||||
# and the profile's last_used_ip. 307 rather than 308: both
|
||||
# preserve method+body, but a 308 is cacheable by default and
|
||||
# could teach the client a core URL that outlives a later
|
||||
# auth-mode switch — the exact stickiness this PR removes.
|
||||
# RELATIVE Location (#2213 review round 2): an absolute target
|
||||
# would be derived from unvalidated forwarded headers, turning a
|
||||
# header a peer controls into the URL the client POSTS the grant
|
||||
# to. A relative reference resolves against the origin the client
|
||||
# actually used and keeps header derivation out of the credential
|
||||
# path entirely (RFC 9110 permits relative Location).
|
||||
return web.Response(
|
||||
status=307,
|
||||
headers={
|
||||
"Location": "/auth/token",
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
)
|
||||
# The body must be rewritten, so the exchange is forwarded server-side:
|
||||
# a translated identity (cross-origin CIMD / DCR blob), an unwrapped
|
||||
# envelope, a revocation whose token was one, or a code leg whose
|
||||
# untranslated identity could not otherwise refresh (#2248). Core
|
||||
# records this server's address for these rare clients — accepted
|
||||
# residual, noted in the PR.
|
||||
form.popall("client_id", None)
|
||||
form["client_id"] = forward_id
|
||||
return await self._proxy_token_to_core(
|
||||
cfg, form, forward_id, client_id, dcr_key
|
||||
)
|
||||
|
||||
async def _ha_auth_forward_identity(
|
||||
self,
|
||||
cfg: dict[str, Any],
|
||||
form: MultiDict,
|
||||
client_id: str,
|
||||
dcr_key: bytes | None,
|
||||
) -> tuple[str, bool] | web.Response:
|
||||
"""Resolve the client_id to present to core, plus whether proxying is forced.
|
||||
|
||||
Returns a ready ``web.Response`` instead when the grant must be
|
||||
answered locally. Mutates ``form`` wherever the wire value is one of
|
||||
ours and core must receive its own: the envelope in a refresh grant,
|
||||
and the envelope in a revocation's ``token``.
|
||||
|
||||
Envelope first (#2248). A refresh token we wrapped names the client_id
|
||||
core bound it to, so the identity is READ rather than re-derived, and
|
||||
the exchange must be proxied — a 307 would hand core an envelope it
|
||||
cannot redeem. Anything else keeps the pre-#2248 behavior: a refresh
|
||||
carrying a redirect_uri translates from that redirect exactly like the
|
||||
authorize leg, a redirect-less refresh re-derives from the registered
|
||||
list, and a verified registration with no reproducible origin is
|
||||
answered locally rather than 307'd into a guaranteed core failure.
|
||||
"""
|
||||
from .oauth_ha_auth import resolve_forward_client_id
|
||||
|
||||
grant_type = str(form.get("grant_type", ""))
|
||||
redirect_uri = str(form.get("redirect_uri", ""))
|
||||
if form.get("action") == "revoke":
|
||||
# RFC 7009 revocation carries no grant_type; the only rewrite it
|
||||
# needs is the envelope swap, and everything else 307s as before.
|
||||
return client_id, _revoke_rewrite(dcr_key, form)
|
||||
if grant_type == "refresh_token":
|
||||
envelope = _envelope_identity(dcr_key, form, client_id)
|
||||
if envelope is not None:
|
||||
return envelope
|
||||
if not client_id:
|
||||
return client_id, False
|
||||
if grant_type == "refresh_token" and not redirect_uri:
|
||||
return await _pre_envelope_refresh_identity(cfg, dcr_key, client_id)
|
||||
# Authorization-code exchanges — and refreshes that DO carry a
|
||||
# redirect_uri — use the presented redirect, exactly like the authorize
|
||||
# leg. With no redirect_uri, validation leaves the client_id untouched
|
||||
# for core to reject authoritatively.
|
||||
forward_id = await resolve_forward_client_id(
|
||||
cfg.get(CFG_CIMD_SESSION),
|
||||
dcr_key,
|
||||
client_id,
|
||||
redirect_uri,
|
||||
)
|
||||
if grant_type == "authorization_code" and forward_id == client_id:
|
||||
return client_id, await _code_leg_forces_proxy(cfg, dcr_key, client_id)
|
||||
return forward_id, False
|
||||
|
||||
async def _proxy_token_to_core(
|
||||
self,
|
||||
cfg: dict[str, Any],
|
||||
form: MultiDict,
|
||||
forward_id: str,
|
||||
client_id: str,
|
||||
dcr_key: bytes | None,
|
||||
) -> web.Response:
|
||||
"""POST the rewritten token form to core and relay its response.
|
||||
|
||||
A 200 has its ``refresh_token`` wrapped before it leaves (#2248) so the
|
||||
client's next refresh carries the identity core bound this grant to.
|
||||
Every other status — and a body with nothing to wrap — is relayed
|
||||
byte-for-byte.
|
||||
"""
|
||||
from .oauth_ha_auth import rewrite_token_response_body
|
||||
|
||||
forwarded = await _forward_to_core(self._hass, cfg, "/auth/token", form)
|
||||
if isinstance(forwarded, web.Response):
|
||||
return forwarded
|
||||
status, body, content_type = forwarded
|
||||
# Revocation answers 200 with an EMPTY body, so there is nothing to
|
||||
# rewrite and a warning would be pure noise.
|
||||
if status == 200 and dcr_key is not None and form.get("action") != "revoke":
|
||||
body = rewrite_token_response_body(dcr_key, body, forward_id, client_id)
|
||||
return web.Response(
|
||||
status=status,
|
||||
body=body,
|
||||
content_type=content_type,
|
||||
headers=_TOKEN_RESPONSE_HEADERS,
|
||||
)
|
||||
|
||||
|
||||
class AutoApproveRevokeView(HomeAssistantView):
|
||||
"""Scoped RFC 7009 ``/revoke`` endpoint, served in ha_auth mode only (#2248).
|
||||
|
||||
Core's own ``POST /auth/revoke`` answers 200 for a token it has never seen
|
||||
(RFC 7009 §2.2, which core's ``RevokeTokenView`` cites verbatim), so a
|
||||
client that posts the signed envelope we handed it straight to core gets a
|
||||
silent no-op and keeps a live session. Fronting revocation here is what
|
||||
lets the envelope be swapped for core's own token first. Legacy and none
|
||||
mode never mint an envelope, so this route 404s there — the envelope is the
|
||||
whole reason it exists.
|
||||
|
||||
ANONYMOUS BY DESIGN, like core's own revocation view (``requires_auth =
|
||||
False``, ``cors_allowed = True``, mirrored here): RFC 7009 authorizes the
|
||||
BEARER of the token, not a client identity. That grants no new reach. A
|
||||
token carrying no ``hamcp-rt-`` prefix never causes an outbound request at
|
||||
all — it 307s and this server makes no call. A prefixed one IS forwarded
|
||||
even when its MAC does not verify, which is what keeps revocation working
|
||||
across a signing-key rotation (#2249 review) and hands a forger nothing:
|
||||
core's revoke endpoint is anonymous and idempotent and answers 200 to
|
||||
whatever they could already POST to it directly. See
|
||||
:func:`_revoke_rewrite`.
|
||||
"""
|
||||
|
||||
requires_auth = False
|
||||
cors_allowed = True
|
||||
url = f"{OAUTH_BASE}/revoke"
|
||||
name = "ha_mcp_tools:oauth:autoapprove-revoke"
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Bind the view to the HA instance; liveness is resolved per request."""
|
||||
self._hass = hass
|
||||
|
||||
async def post(self, request: web.Request) -> web.Response:
|
||||
"""Unwrap an envelope and forward, or 307 the revocation into core."""
|
||||
cfg = _webhook_cfg(self._hass)
|
||||
if cfg is None or cfg.get("resource_server") is None:
|
||||
return _json_not_found()
|
||||
from multidict import MultiDict
|
||||
|
||||
from .oauth_dcr import CFG_DCR_SIGNING_KEY
|
||||
|
||||
raw_form = await read_form(request)
|
||||
if raw_form is None:
|
||||
return _json_error("invalid_request", 400)
|
||||
# str()-coerced MultiDict for the same reason the token view builds
|
||||
# one: bytes/FileField values from a multipart body would raise a
|
||||
# TypeError inside the outgoing serializer (#2219).
|
||||
form: MultiDict = MultiDict(
|
||||
(key, str(value)) for key, value in raw_form.items()
|
||||
)
|
||||
if not _revoke_rewrite(cfg.get(CFG_DCR_SIGNING_KEY), form):
|
||||
# No core token to recover from the body, so there is nothing
|
||||
# to rewrite: 307 the client into core's own /auth/revoke, which
|
||||
# then observes the CLIENT's address. Relative Location for the
|
||||
# token view's reason — an absolute one would derive the
|
||||
# credential target from unvalidated forwarded headers (#2213
|
||||
# review round 2).
|
||||
return web.Response(
|
||||
status=307,
|
||||
headers={
|
||||
"Location": "/auth/revoke",
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
)
|
||||
forwarded = await _forward_to_core(self._hass, cfg, "/auth/revoke", form)
|
||||
if isinstance(forwarded, web.Response):
|
||||
# The helper's only Response is the 503, and it already carries
|
||||
# the RFC 7009 §2.2.1 Retry-After for every revocation, whichever
|
||||
# surface it arrived on.
|
||||
return forwarded
|
||||
status, body, content_type = forwarded
|
||||
# Relayed as-is: core answers a revocation with an empty 200, so there
|
||||
# is never a refresh_token to wrap on the way back.
|
||||
return web.Response(
|
||||
status=status,
|
||||
body=body,
|
||||
content_type=content_type,
|
||||
headers=_TOKEN_RESPONSE_HEADERS,
|
||||
)
|
||||
|
||||
|
||||
def bind_autoapprove_views(hass: HomeAssistant) -> None:
|
||||
"""Bind the two auto-approve views at most once per HA session.
|
||||
"""Bind the three unified OAuth views at most once per HA session.
|
||||
|
||||
aiohttp cannot unregister a bound view, so a reload / re-enable / mode
|
||||
switch must reuse the already-bound views — they resolve the active
|
||||
provider from ``hass.data`` per request (see
|
||||
:func:`_active_autoapprove_provider`), so they serve only while
|
||||
none-autoapprove is live and 404 otherwise. The guard flag lives at a
|
||||
top-level ``hass.data`` key that survives config-entry teardown (mirrors
|
||||
mode/provider from ``hass.data`` per request (see :func:`_webhook_cfg`), so
|
||||
the same paths dispatch to legacy, ha_auth, or none-autoapprove without
|
||||
rebinding. The guard flag lives at a top-level ``hass.data`` key that
|
||||
survives config-entry teardown (mirrors
|
||||
:func:`mcp_webhook._register_metadata_views`).
|
||||
"""
|
||||
if hass.data.get(_AUTOAPPROVE_VIEWS_REGISTERED_KEY):
|
||||
return
|
||||
# Set the flag only AFTER both views register (issue #1978): see
|
||||
# Set the flag only AFTER ALL THREE views register (issue #1978): see
|
||||
# mcp_webhook._register_metadata_views. Marking the bundle bound before
|
||||
# /token registers would let a later none-mode setup assign the provider and
|
||||
# advertise OAuth with an unbound /token — a 404 on the token exchange. The
|
||||
# flag must mean the full bundle succeeded; a partial bind leaves it unset.
|
||||
# /token or /revoke registers would let a later setup assign its mode
|
||||
# provider and advertise OAuth with an unbound endpoint the discovery
|
||||
# document names — a 404 on the token exchange or on revocation (#2248).
|
||||
# The flag must mean the full bundle succeeded; a partial bind leaves it
|
||||
# unset.
|
||||
hass.http.register_view(AutoApproveAuthorizeView(hass))
|
||||
hass.http.register_view(AutoApproveTokenView(hass))
|
||||
hass.http.register_view(AutoApproveRevokeView(hass))
|
||||
hass.data[_AUTOAPPROVE_VIEWS_REGISTERED_KEY] = True
|
||||
|
||||
@@ -40,11 +40,12 @@ import secrets
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from html import escape
|
||||
from typing import TYPE_CHECKING, TypedDict
|
||||
from typing import TYPE_CHECKING, Any, TypedDict
|
||||
from urllib.parse import unquote_plus, urlparse
|
||||
|
||||
from aiohttp import web
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from multidict import MultiDictProxy
|
||||
|
||||
from .const import WEBHOOK_AUTH_LEGACY
|
||||
|
||||
@@ -86,8 +87,8 @@ PKCE_VERIFIER_MIN = 43
|
||||
PKCE_VERIFIER_MAX = 128
|
||||
# SHA-256 → 32 bytes → 43 base64url chars (no padding).
|
||||
PKCE_S256_CHALLENGE_LEN = 43
|
||||
_PKCE_VERIFIER_RE = re.compile(r"^[A-Za-z0-9._~-]+$")
|
||||
_PKCE_CHALLENGE_RE = re.compile(r"^[A-Za-z0-9_-]{43}$")
|
||||
_PKCE_VERIFIER_RE = re.compile(r"[A-Za-z0-9._~-]+")
|
||||
_PKCE_CHALLENGE_RE = re.compile(r"[A-Za-z0-9_-]{43}")
|
||||
|
||||
# Pending-code dict cap. An attacker spamming /authorize with valid params
|
||||
# could grow the dict between the prune passes that run on each issuance.
|
||||
@@ -138,6 +139,13 @@ _LEGACY_PENDING_RESTART_KEY = "ha_mcp_tools_oauth_legacy_pending_restart"
|
||||
_DOMAIN = "ha_mcp_tools"
|
||||
|
||||
|
||||
# TOP-LEVEL hass.data key holding the fingerprint of the credentials the
|
||||
# SCOPED-ONLY legacy provider serves (root routes owned by another integration
|
||||
# this session — see build_unbound_legacy_provider). Separate from
|
||||
# OAUTH_ROUTE_KEY_FINGERPRINT, which records what the bound ROOT views serve.
|
||||
_LEGACY_SCOPED_FP_KEY = "ha_mcp_tools_oauth_legacy_scoped_fingerprint"
|
||||
|
||||
|
||||
class LegacyOAuthRouteConflict(RuntimeError):
|
||||
"""Raised when another integration already owns the root OAuth routes."""
|
||||
|
||||
@@ -193,14 +201,6 @@ def bind_legacy_views(
|
||||
|
||||
owner = hass.data.get(OAUTH_ROUTE_OWNER_KEY)
|
||||
if owner is not None and owner != _DOMAIN:
|
||||
_LOGGER.error(
|
||||
"HA-MCP: cannot enable legacy OAuth mode -- the Webhook Proxy "
|
||||
"add-on ('%s') already owns the root /authorize and /token routes "
|
||||
"in this Home Assistant instance, and Home Assistant cannot "
|
||||
"release them until it restarts. Stop that add-on and restart "
|
||||
"Home Assistant, then enable legacy mode again.",
|
||||
owner,
|
||||
)
|
||||
raise LegacyOAuthRouteConflict(owner)
|
||||
|
||||
bound_provider = hass.data.get(_LEGACY_PROVIDER_KEY)
|
||||
@@ -242,30 +242,67 @@ def bind_legacy_views(
|
||||
return provider, pending_restart
|
||||
|
||||
|
||||
def build_unbound_legacy_provider(
|
||||
hass: HomeAssistant,
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
signing_key: bytes | str,
|
||||
) -> LegacyOAuthProvider:
|
||||
"""Build a legacy provider for the scoped endpoints only.
|
||||
|
||||
Root routes are owned by another integration this session. Nothing is
|
||||
bound, so no restart bookkeeping applies.
|
||||
"""
|
||||
key_bytes = _normalize_signing_key(signing_key)
|
||||
# The scoped views serve THIS provider from cfg immediately (no restart
|
||||
# semantics apply) — record its identity so legacy_credentials_active
|
||||
# reports these working credentials as live (#2213 review).
|
||||
hass.data[_LEGACY_SCOPED_FP_KEY] = _oauth_route_fingerprint(
|
||||
client_id, client_secret, key_bytes
|
||||
)
|
||||
return LegacyOAuthProvider(
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
signing_key=key_bytes,
|
||||
active_mode_getter=lambda: _live_auth_mode(hass),
|
||||
)
|
||||
|
||||
|
||||
def clear_scoped_legacy_credentials(hass: HomeAssistant) -> None:
|
||||
"""Forget credentials when no scoped-only legacy provider is live."""
|
||||
hass.data.pop(_LEGACY_SCOPED_FP_KEY, None)
|
||||
|
||||
|
||||
def legacy_credentials_active(
|
||||
hass: HomeAssistant,
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
signing_key: bytes | str,
|
||||
) -> bool:
|
||||
"""Whether the bound root views currently serve exactly these credentials.
|
||||
"""Whether a live legacy surface currently serves exactly these credentials.
|
||||
|
||||
False while a credential rotation is pending a restart (the bound provider
|
||||
keeps the previous identity until then — see :func:`bind_legacy_views`),
|
||||
when another integration owns the routes, or when legacy OAuth was never
|
||||
bound this session. Callers use this to withhold rotated credentials from
|
||||
surfaces a still-valid old-identity token can read — the admin startup log
|
||||
in particular, which is reachable through the server's own log tools
|
||||
(review finding on #1880).
|
||||
keeps the previous identity until then — see :func:`bind_legacy_views`) or
|
||||
when legacy OAuth was never bound this session. When another integration
|
||||
owns the root routes, the scoped endpoints may still serve via the unbound
|
||||
provider — True then iff the scoped provider's fingerprint matches these
|
||||
credentials (#2213 review). Callers use this to withhold rotated
|
||||
credentials from surfaces a still-valid old-identity token can read — the
|
||||
admin startup log in particular, which is reachable through the server's
|
||||
own log tools (review finding on #1880).
|
||||
"""
|
||||
if hass.data.get(OAUTH_ROUTE_OWNER_KEY) != _DOMAIN:
|
||||
return False
|
||||
bound = hass.data.get(OAUTH_ROUTE_KEY_FINGERPRINT)
|
||||
if not isinstance(bound, str):
|
||||
return False
|
||||
current = _oauth_route_fingerprint(
|
||||
client_id, client_secret, _normalize_signing_key(signing_key)
|
||||
)
|
||||
if hass.data.get(OAUTH_ROUTE_OWNER_KEY) != _DOMAIN:
|
||||
# Root routes owned elsewhere (webhook-proxy add-on). The scoped
|
||||
# endpoints may still serve these credentials via the unbound provider
|
||||
# (#2213 review) — surface them when its fingerprint matches.
|
||||
scoped = hass.data.get(_LEGACY_SCOPED_FP_KEY)
|
||||
return isinstance(scoped, str) and hmac.compare_digest(scoped, current)
|
||||
bound = hass.data.get(OAUTH_ROUTE_KEY_FINGERPRINT)
|
||||
if not isinstance(bound, str):
|
||||
return False
|
||||
return hmac.compare_digest(bound, current)
|
||||
|
||||
|
||||
@@ -330,6 +367,12 @@ def _is_loopback_host(hostname: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# RFC 3986 §3.2 authority charset (unreserved / pct-encoded / sub-delims /
|
||||
# ':' '@' and IPv6 brackets). Anything outside it (a backslash, a space, raw
|
||||
# unicode) is an illegal authority that downstream URL builders reject.
|
||||
_AUTHORITY_CHARS_RE = re.compile(r"[A-Za-z0-9._~%!$&'()*+,;=:@\[\]-]*")
|
||||
|
||||
|
||||
def _is_valid_redirect_uri(redirect_uri: str) -> bool:
|
||||
"""Spec-floor validation for OAuth redirect_uri: an https:// URL — or an
|
||||
http:// loopback URL (RFC 8252 §7.3, for native/CLI clients) — with a
|
||||
@@ -349,6 +392,13 @@ def _is_valid_redirect_uri(redirect_uri: str) -> bool:
|
||||
return False
|
||||
if not parsed.hostname:
|
||||
return False
|
||||
if not _AUTHORITY_CHARS_RE.fullmatch(parsed.netloc):
|
||||
# Same contract as the .port access above: urlparse and yarl split
|
||||
# authorities differently (a backslash before '@', a zero-width
|
||||
# character in the host), so an RFC 3986-illegal authority must fail
|
||||
# HERE rather than escape as a 500 out of _redirect_with
|
||||
# (#2219 codex review).
|
||||
return False
|
||||
if parsed.scheme == "http":
|
||||
# Plain http only for loopback callbacks (native-client flow).
|
||||
if not _is_loopback_host(parsed.hostname):
|
||||
@@ -460,7 +510,7 @@ class PKCECodeStore:
|
||||
# rejected explicitly rather than silently hashing junk.
|
||||
if not (PKCE_VERIFIER_MIN <= len(code_verifier) <= PKCE_VERIFIER_MAX):
|
||||
return False
|
||||
if not _PKCE_VERIFIER_RE.match(code_verifier):
|
||||
if not _PKCE_VERIFIER_RE.fullmatch(code_verifier):
|
||||
return False
|
||||
entry = self._codes.pop(code, None)
|
||||
if entry is None:
|
||||
@@ -606,6 +656,37 @@ class LegacyOAuthProvider:
|
||||
"""Verify PKCE S256 + one-shot consume a code (see PKCECodeStore)."""
|
||||
return self._code_store.consume_code(code, redirect_uri, code_verifier)
|
||||
|
||||
def validate_authorize_params(
|
||||
self,
|
||||
*,
|
||||
response_type: str,
|
||||
client_id: str,
|
||||
redirect_uri: str,
|
||||
code_challenge: str,
|
||||
code_challenge_method: str,
|
||||
) -> web.Response | None:
|
||||
"""Return a 400 web.Response if any /authorize param is invalid, or
|
||||
None if all checks pass. Centralized so GET and POST share identical
|
||||
validation — the POST path explicitly re-validates the hidden form
|
||||
fields rather than trusting them."""
|
||||
if response_type != "code":
|
||||
return _text_error(400, "unsupported_response_type")
|
||||
if code_challenge_method != "S256":
|
||||
return _text_error(400, "invalid code_challenge_method (S256 required)")
|
||||
if not _PKCE_CHALLENGE_RE.fullmatch(code_challenge):
|
||||
return _text_error(
|
||||
400, "invalid code_challenge (must be 43-char base64url)"
|
||||
)
|
||||
if client_id != self.client_id:
|
||||
return _text_error(400, "invalid client_id", restart_hint=True)
|
||||
if not _is_valid_redirect_uri(redirect_uri):
|
||||
return _text_error(
|
||||
400,
|
||||
"redirect_uri must be an https:// URL (or an http:// loopback "
|
||||
"URL) with a valid host and port",
|
||||
)
|
||||
return None
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Client authentication
|
||||
# -----------------------------------------------------------------
|
||||
@@ -621,58 +702,49 @@ class LegacyOAuthProvider:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Views (root /authorize + /token)
|
||||
# Shared handlers + views (root /authorize + /token)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class AuthorizeView(HomeAssistantView):
|
||||
"""OAuth /authorize endpoint with a minimal consent page."""
|
||||
def _redirect_with_params(redirect_uri: str, **params: str) -> web.Response:
|
||||
# yarl ships with aiohttp and handles existing-query-string merging
|
||||
# plus parameter encoding correctly — safer than hand-rolling.
|
||||
import yarl
|
||||
|
||||
requires_auth = False
|
||||
url = AUTHORIZE_PATH
|
||||
name = "ha_mcp_tools:oauth:authorize"
|
||||
url = yarl.URL(redirect_uri).update_query(params)
|
||||
return web.Response(status=302, headers={"Location": str(url)})
|
||||
|
||||
def __init__(self, provider: LegacyOAuthProvider) -> None:
|
||||
self._provider = provider
|
||||
|
||||
@staticmethod
|
||||
def _redirect_with(redirect_uri: str, **params: str) -> web.Response:
|
||||
# yarl ships with aiohttp and handles existing-query-string merging
|
||||
# plus parameter encoding correctly — safer than hand-rolling.
|
||||
import yarl
|
||||
async def handle_legacy_authorize_get(
|
||||
provider: LegacyOAuthProvider, request: web.Request
|
||||
) -> web.Response:
|
||||
"""Serve the legacy consent page (shared by the root and scoped routes).
|
||||
|
||||
url = yarl.URL(redirect_uri).update_query(params)
|
||||
return web.Response(status=302, headers={"Location": str(url)})
|
||||
The form posts back to ``request.path`` so the scoped
|
||||
``{OAUTH_BASE}/authorize`` route and the legacy root ``/authorize`` route
|
||||
each round-trip through themselves.
|
||||
"""
|
||||
params = request.query
|
||||
client_id = params.get("client_id", "")
|
||||
redirect_uri = params.get("redirect_uri", "")
|
||||
state = params.get("state", "")
|
||||
code_challenge = params.get("code_challenge", "")
|
||||
code_challenge_method = params.get("code_challenge_method", "")
|
||||
response_type = params.get("response_type", "")
|
||||
|
||||
async def get(self, request: web.Request) -> web.Response:
|
||||
if not self._provider.is_active():
|
||||
# Serve ONLY while legacy is the live mode. Both ha_auth/none (HA
|
||||
# core or the secret URL is the authority) and a not-yet-live
|
||||
# entry mean this root view must not serve — HA can't rebind or
|
||||
# drop it without a restart, so a mode switch away leaves it
|
||||
# bound. Refuse it.
|
||||
return _text_error(404, "not found")
|
||||
params = request.query
|
||||
client_id = params.get("client_id", "")
|
||||
redirect_uri = params.get("redirect_uri", "")
|
||||
state = params.get("state", "")
|
||||
code_challenge = params.get("code_challenge", "")
|
||||
code_challenge_method = params.get("code_challenge_method", "")
|
||||
response_type = params.get("response_type", "")
|
||||
err = provider.validate_authorize_params(
|
||||
response_type=response_type,
|
||||
client_id=client_id,
|
||||
redirect_uri=redirect_uri,
|
||||
code_challenge=code_challenge,
|
||||
code_challenge_method=code_challenge_method,
|
||||
)
|
||||
if err is not None:
|
||||
return err
|
||||
|
||||
err = self._validate_authorize_params(
|
||||
response_type=response_type,
|
||||
client_id=client_id,
|
||||
redirect_uri=redirect_uri,
|
||||
code_challenge=code_challenge,
|
||||
code_challenge_method=code_challenge_method,
|
||||
)
|
||||
if err is not None:
|
||||
return err
|
||||
|
||||
# Render minimal consent page. Showing the redirect_uri lets the user
|
||||
# verify the flow goes back to a domain they recognize.
|
||||
html = f"""<!DOCTYPE html>
|
||||
# Render minimal consent page. Showing the redirect_uri lets the user
|
||||
# verify the flow goes back to a domain they recognize.
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@@ -690,7 +762,7 @@ class AuthorizeView(HomeAssistantView):
|
||||
<p>An MCP client is requesting access to your Home Assistant MCP server.</p>
|
||||
<p>It will redirect to:<br><code>{escape(redirect_uri)}</code></p>
|
||||
<p>Only allow this if you started this connection yourself.</p>
|
||||
<form method="POST" action="{AUTHORIZE_PATH}">
|
||||
<form method="POST" action="{escape(request.path)}">
|
||||
<input type="hidden" name="client_id" value="{escape(client_id)}">
|
||||
<input type="hidden" name="redirect_uri" value="{escape(redirect_uri)}">
|
||||
<input type="hidden" name="state" value="{escape(state)}">
|
||||
@@ -700,83 +772,197 @@ class AuthorizeView(HomeAssistantView):
|
||||
</form>
|
||||
</body>
|
||||
</html>"""
|
||||
return web.Response(text=html, content_type="text/html")
|
||||
return web.Response(text=html, content_type="text/html")
|
||||
|
||||
|
||||
async def handle_legacy_authorize_post(
|
||||
provider: LegacyOAuthProvider, request: web.Request
|
||||
) -> web.Response:
|
||||
"""Consume the consent form and redirect with a code (shared by both routes)."""
|
||||
data = await read_form(request)
|
||||
if data is None:
|
||||
return _text_error(400, "Invalid form submission")
|
||||
action = str(data.get("action", ""))
|
||||
client_id = str(data.get("client_id", ""))
|
||||
redirect_uri = str(data.get("redirect_uri", ""))
|
||||
state = str(data.get("state", ""))
|
||||
code_challenge = str(data.get("code_challenge", ""))
|
||||
|
||||
# Re-validate everything from the form — never trust hidden fields.
|
||||
# response_type/method aren't carried on the POST so we hard-code the
|
||||
# spec values here; the validator still applies all the same rules to
|
||||
# the user-influenceable fields.
|
||||
err = provider.validate_authorize_params(
|
||||
response_type="code",
|
||||
client_id=client_id,
|
||||
redirect_uri=redirect_uri,
|
||||
code_challenge=code_challenge,
|
||||
code_challenge_method="S256",
|
||||
)
|
||||
if err is not None:
|
||||
return err
|
||||
|
||||
# RFC 9207: every authorization response — success or error — names the
|
||||
# issuer that produced it, so a client registered with several
|
||||
# authorization servers cannot be fed a response minted by another one.
|
||||
iss = _issuer_for(request)
|
||||
|
||||
if action == "deny":
|
||||
return _redirect_with_params(
|
||||
redirect_uri, error="access_denied", state=state, iss=iss
|
||||
)
|
||||
if action != "approve":
|
||||
return _text_error(400, "invalid action")
|
||||
|
||||
code = provider.issue_code(redirect_uri, code_challenge)
|
||||
if code is None:
|
||||
# Pending-code store at cap → signal back per RFC 6749 §4.1.2.1
|
||||
# instead of silently failing.
|
||||
return _redirect_with_params(
|
||||
redirect_uri, error="temporarily_unavailable", state=state, iss=iss
|
||||
)
|
||||
return _redirect_with_params(redirect_uri, code=code, state=state, iss=iss)
|
||||
|
||||
|
||||
async def read_form(request: web.Request) -> MultiDictProxy[Any] | None:
|
||||
"""Parse a form body, or None when the request body is undecodable.
|
||||
|
||||
aiohttp raises LookupError — NOT ValueError — when the Content-Type names
|
||||
an unknown charset (``application/x-www-form-urlencoded; charset=nope``),
|
||||
and every caller here is an ANONYMOUS view, so an unguarded parse turns
|
||||
one header into a 500 with a traceback (#2219 review round 3).
|
||||
"""
|
||||
try:
|
||||
return await request.post()
|
||||
except (ValueError, LookupError):
|
||||
return None
|
||||
|
||||
|
||||
def _extract_client_creds(
|
||||
request: web.Request, form: dict
|
||||
) -> list[tuple[str | None, str | None]]:
|
||||
"""Candidate client_id/secret pairs from Basic auth OR the form body.
|
||||
|
||||
RFC 6749 §2.3.1: client_secret_basic values are form-urlencoded before
|
||||
base64, so the decoded candidate applies unquote_PLUS ("+" means space in
|
||||
that encoding). Many clients skip the encoding step, though, and custom
|
||||
credential overrides may contain a literal "+" or "%XX" that decoding
|
||||
would mangle (#2218 review, mirrored from the proxy) — so the raw split
|
||||
is offered as a second candidate and either may authenticate. Both are
|
||||
no-ops for the generated credentials (URL-safe alphabets).
|
||||
"""
|
||||
header = request.headers.get("Authorization", "")
|
||||
if header.lower().startswith("basic "):
|
||||
try:
|
||||
decoded = base64.b64decode(header[6:].strip(), validate=True).decode(
|
||||
"utf-8"
|
||||
)
|
||||
except (ValueError, UnicodeDecodeError, binascii.Error):
|
||||
return []
|
||||
if ":" not in decoded:
|
||||
return []
|
||||
cid, _, sec = decoded.partition(":")
|
||||
candidates: list[tuple[str | None, str | None]] = [
|
||||
(unquote_plus(cid), unquote_plus(sec))
|
||||
]
|
||||
if (cid, sec) != candidates[0]:
|
||||
candidates.append((cid, sec))
|
||||
return candidates
|
||||
# str()-wrapped like every sibling site: request.post() yields
|
||||
# str | bytes | FileField, and the non-str shapes are truthy, so they
|
||||
# would slip past authenticate_client's emptiness guard and raise
|
||||
# AttributeError on .encode() (#2219 review round 3).
|
||||
return [(str(form.get("client_id", "")), str(form.get("client_secret", "")))]
|
||||
|
||||
|
||||
async def _handle_authorization_code(
|
||||
provider: LegacyOAuthProvider, form: dict
|
||||
) -> web.Response:
|
||||
code = str(form.get("code", ""))
|
||||
redirect_uri = str(form.get("redirect_uri", ""))
|
||||
code_verifier = str(form.get("code_verifier", ""))
|
||||
if not (code and redirect_uri and code_verifier):
|
||||
return _json_error("invalid_request", 400)
|
||||
if not provider.consume_code(code, redirect_uri, code_verifier):
|
||||
return _json_error("invalid_grant", 400)
|
||||
return web.json_response(
|
||||
{
|
||||
"access_token": provider.issue_access_token(),
|
||||
"token_type": "Bearer",
|
||||
"expires_in": ACCESS_TOKEN_TTL,
|
||||
"refresh_token": provider.issue_refresh_token(),
|
||||
},
|
||||
headers=_TOKEN_RESPONSE_HEADERS,
|
||||
)
|
||||
|
||||
|
||||
async def _handle_refresh(provider: LegacyOAuthProvider, form: dict) -> web.Response:
|
||||
refresh = str(form.get("refresh_token", ""))
|
||||
if not refresh or not provider.validate_refresh_token(refresh):
|
||||
return _json_error("invalid_grant", 400)
|
||||
return web.json_response(
|
||||
{
|
||||
"access_token": provider.issue_access_token(),
|
||||
"token_type": "Bearer",
|
||||
"expires_in": ACCESS_TOKEN_TTL,
|
||||
"refresh_token": provider.issue_refresh_token(),
|
||||
},
|
||||
headers=_TOKEN_RESPONSE_HEADERS,
|
||||
)
|
||||
|
||||
|
||||
async def handle_legacy_token_post(
|
||||
provider: LegacyOAuthProvider, request: web.Request
|
||||
) -> web.Response:
|
||||
"""Token endpoint body (shared by the root and scoped routes)."""
|
||||
raw_form = await read_form(request)
|
||||
if raw_form is None:
|
||||
return _json_error("invalid_request", 400)
|
||||
form = dict(raw_form)
|
||||
candidates = _extract_client_creds(request, form)
|
||||
if not any(
|
||||
provider.authenticate_client(client_id, client_secret)
|
||||
for client_id, client_secret in candidates
|
||||
):
|
||||
return _json_error(
|
||||
"invalid_client",
|
||||
401,
|
||||
headers={"WWW-Authenticate": 'Basic realm="HA-MCP OAuth"'},
|
||||
restart_hint=True,
|
||||
)
|
||||
|
||||
grant_type = form.get("grant_type", "")
|
||||
if grant_type == "authorization_code":
|
||||
return await _handle_authorization_code(provider, form)
|
||||
if grant_type == "refresh_token":
|
||||
return await _handle_refresh(provider, form)
|
||||
return _json_error("unsupported_grant_type", 400)
|
||||
|
||||
|
||||
class AuthorizeView(HomeAssistantView):
|
||||
"""OAuth /authorize endpoint with a minimal consent page."""
|
||||
|
||||
requires_auth = False
|
||||
url = AUTHORIZE_PATH
|
||||
name = "ha_mcp_tools:oauth:authorize"
|
||||
|
||||
def __init__(self, provider: LegacyOAuthProvider) -> None:
|
||||
self._provider = provider
|
||||
|
||||
async def get(self, request: web.Request) -> web.Response:
|
||||
if not self._provider.is_active():
|
||||
# Serve ONLY while legacy is the live mode. Both ha_auth/none (HA
|
||||
# core or the secret URL is the authority) and a not-yet-live
|
||||
# entry mean this root view must not serve — HA can't rebind or
|
||||
# drop it without a restart, so a mode switch away leaves it
|
||||
# bound. Refuse it.
|
||||
return _text_error(404, "not found")
|
||||
return await handle_legacy_authorize_get(self._provider, request)
|
||||
|
||||
async def post(self, request: web.Request) -> web.Response:
|
||||
if not self._provider.is_active():
|
||||
return _text_error(404, "not found")
|
||||
data = await request.post()
|
||||
action = str(data.get("action", ""))
|
||||
client_id = str(data.get("client_id", ""))
|
||||
redirect_uri = str(data.get("redirect_uri", ""))
|
||||
state = str(data.get("state", ""))
|
||||
code_challenge = str(data.get("code_challenge", ""))
|
||||
|
||||
# Re-validate everything from the form — never trust hidden fields.
|
||||
# response_type/method aren't carried on the POST so we hard-code the
|
||||
# spec values here; the validator still applies all the same rules to
|
||||
# the user-influenceable fields.
|
||||
err = self._validate_authorize_params(
|
||||
response_type="code",
|
||||
client_id=client_id,
|
||||
redirect_uri=redirect_uri,
|
||||
code_challenge=code_challenge,
|
||||
code_challenge_method="S256",
|
||||
)
|
||||
if err is not None:
|
||||
return err
|
||||
|
||||
# RFC 9207: every authorization response — success or error — names the
|
||||
# issuer that produced it, so a client registered with several
|
||||
# authorization servers cannot be fed a response minted by another one.
|
||||
iss = _issuer_for(request)
|
||||
|
||||
if action == "deny":
|
||||
return self._redirect_with(
|
||||
redirect_uri, error="access_denied", state=state, iss=iss
|
||||
)
|
||||
if action != "approve":
|
||||
return _text_error(400, "invalid action")
|
||||
|
||||
code = self._provider.issue_code(redirect_uri, code_challenge)
|
||||
if code is None:
|
||||
# Pending-code store at cap → signal back per RFC 6749 §4.1.2.1
|
||||
# instead of silently failing.
|
||||
return self._redirect_with(
|
||||
redirect_uri, error="temporarily_unavailable", state=state, iss=iss
|
||||
)
|
||||
return self._redirect_with(redirect_uri, code=code, state=state, iss=iss)
|
||||
|
||||
def _validate_authorize_params(
|
||||
self,
|
||||
*,
|
||||
response_type: str,
|
||||
client_id: str,
|
||||
redirect_uri: str,
|
||||
code_challenge: str,
|
||||
code_challenge_method: str,
|
||||
) -> web.Response | None:
|
||||
"""Return a 400 web.Response if any /authorize param is invalid, or
|
||||
None if all checks pass. Centralized so GET and POST share identical
|
||||
validation — the POST path explicitly re-validates the hidden form
|
||||
fields rather than trusting them."""
|
||||
if response_type != "code":
|
||||
return _text_error(400, "unsupported_response_type")
|
||||
if code_challenge_method != "S256":
|
||||
return _text_error(400, "invalid code_challenge_method (S256 required)")
|
||||
if not _PKCE_CHALLENGE_RE.match(code_challenge):
|
||||
return _text_error(
|
||||
400, "invalid code_challenge (must be 43-char base64url)"
|
||||
)
|
||||
if client_id != self._provider.client_id:
|
||||
return _text_error(400, "invalid client_id", restart_hint=True)
|
||||
if not _is_valid_redirect_uri(redirect_uri):
|
||||
return _text_error(
|
||||
400,
|
||||
"redirect_uri must be an https:// URL (or an http:// loopback "
|
||||
"URL) with a valid host and port",
|
||||
)
|
||||
return None
|
||||
return await handle_legacy_authorize_post(self._provider, request)
|
||||
|
||||
|
||||
class TokenView(HomeAssistantView):
|
||||
@@ -790,81 +976,7 @@ class TokenView(HomeAssistantView):
|
||||
def __init__(self, provider: LegacyOAuthProvider) -> None:
|
||||
self._provider = provider
|
||||
|
||||
@staticmethod
|
||||
def _extract_client_creds(
|
||||
request: web.Request, form: dict
|
||||
) -> tuple[str | None, str | None]:
|
||||
"""Pull client_id/secret from Basic auth header OR form body."""
|
||||
header = request.headers.get("Authorization", "")
|
||||
if header.lower().startswith("basic "):
|
||||
try:
|
||||
decoded = base64.b64decode(header[6:].strip(), validate=True).decode(
|
||||
"utf-8"
|
||||
)
|
||||
except (ValueError, UnicodeDecodeError, binascii.Error):
|
||||
return None, None
|
||||
if ":" in decoded:
|
||||
cid, _, sec = decoded.partition(":")
|
||||
# RFC 6749 §2.3.1: client_secret_basic values are
|
||||
# application/x-www-form-urlencoded before base64, so decode
|
||||
# them back with unquote_PLUS ("+" means space in that
|
||||
# encoding — plain unquote would leave it literal). A no-op for
|
||||
# the generated credentials (URL-safe alphabets, nothing to
|
||||
# decode) but required for custom overrides containing reserved
|
||||
# characters — matches the form-body path below, which aiohttp
|
||||
# also form-decodes ("+" → space).
|
||||
return unquote_plus(cid), unquote_plus(sec)
|
||||
return None, None
|
||||
return form.get("client_id"), form.get("client_secret")
|
||||
|
||||
async def post(self, request: web.Request) -> web.Response:
|
||||
if not self._provider.is_active():
|
||||
return _json_not_found()
|
||||
form = dict(await request.post())
|
||||
client_id, client_secret = self._extract_client_creds(request, form)
|
||||
if not self._provider.authenticate_client(client_id, client_secret):
|
||||
return _json_error(
|
||||
"invalid_client",
|
||||
401,
|
||||
headers={"WWW-Authenticate": 'Basic realm="HA-MCP OAuth"'},
|
||||
restart_hint=True,
|
||||
)
|
||||
|
||||
grant_type = form.get("grant_type", "")
|
||||
if grant_type == "authorization_code":
|
||||
return await self._handle_authorization_code(form)
|
||||
if grant_type == "refresh_token":
|
||||
return await self._handle_refresh(form)
|
||||
return _json_error("unsupported_grant_type", 400)
|
||||
|
||||
async def _handle_authorization_code(self, form: dict) -> web.Response:
|
||||
code = str(form.get("code", ""))
|
||||
redirect_uri = str(form.get("redirect_uri", ""))
|
||||
code_verifier = str(form.get("code_verifier", ""))
|
||||
if not (code and redirect_uri and code_verifier):
|
||||
return _json_error("invalid_request", 400)
|
||||
if not self._provider.consume_code(code, redirect_uri, code_verifier):
|
||||
return _json_error("invalid_grant", 400)
|
||||
return web.json_response(
|
||||
{
|
||||
"access_token": self._provider.issue_access_token(),
|
||||
"token_type": "Bearer",
|
||||
"expires_in": ACCESS_TOKEN_TTL,
|
||||
"refresh_token": self._provider.issue_refresh_token(),
|
||||
},
|
||||
headers=_TOKEN_RESPONSE_HEADERS,
|
||||
)
|
||||
|
||||
async def _handle_refresh(self, form: dict) -> web.Response:
|
||||
refresh = str(form.get("refresh_token", ""))
|
||||
if not refresh or not self._provider.validate_refresh_token(refresh):
|
||||
return _json_error("invalid_grant", 400)
|
||||
return web.json_response(
|
||||
{
|
||||
"access_token": self._provider.issue_access_token(),
|
||||
"token_type": "Bearer",
|
||||
"expires_in": ACCESS_TOKEN_TTL,
|
||||
"refresh_token": self._provider.issue_refresh_token(),
|
||||
},
|
||||
headers=_TOKEN_RESPONSE_HEADERS,
|
||||
)
|
||||
return await handle_legacy_token_post(self._provider, request)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "HA-MCP Custom Component",
|
||||
"description": "Choose what to add. **HA-MCP Server** runs the full ha-mcp server inside Home Assistant and exposes it through a Home Assistant webhook - this is the install most people want. It is a standalone server and a complete replacement for every other install method (add-on, Docker, uvx/PyPI, stdio); if you run it, do not also run one of those. **HA-MCP File & YAML Tools** adds the privileged file and YAML editing services, needed only if you turn on ha-mcp's opt-in file/YAML tools. If your ha-mcp server already runs elsewhere (add-on, Docker, uvx), you do not need the server entry - add only this File & YAML entry, and only if you use those tools. It works with every server type and can be added later at any time.",
|
||||
"description": "Choose what to add. **HA-MCP Server** runs the full ha-mcp server inside Home Assistant and exposes it through a Home Assistant webhook - this is the install most people want. It is a standalone server and a complete replacement for every other install method: App (add-on), Docker, uvx/PyPI, stdio; if you run it, do not also run one of those. **HA-MCP File & YAML Tools** has two roles. If your ha-mcp server runs outside Home Assistant - app, Docker, uvx/PyPI, stdio - this entry also gives that server the in-process component capabilities it cannot reach from outside (fast in-process search, config entry metadata, registry-backed reads), so it is worth adding to every external install, and it gates the privileged file and YAML editing services used by ha-mcp's opt-in file/YAML tools. If you add the HA-MCP Server entry instead, that entry already provides those shared capabilities, so this second entry is then needed only as the gate for the opt-in file/YAML tools. It works with every server type and can be added later at any time.",
|
||||
"menu_options": {
|
||||
"server": "HA-MCP Server (recommended)",
|
||||
"tools": "HA-MCP File & YAML Tools (optional)"
|
||||
@@ -11,16 +11,16 @@
|
||||
},
|
||||
"tools": {
|
||||
"title": "HA-MCP File & YAML Tools",
|
||||
"description": "Sets up the privileged file and YAML configuration services. Only needed if you enable ha-mcp's opt-in file/YAML editing tools (feature flags, off by default) - this applies to every server type, including the in-process HA-MCP Server. You can add or remove this entry at any time."
|
||||
"description": "Sets up the privileged file and YAML configuration services, which stay gated behind ha-mcp's opt-in file/YAML editing tools (feature flags, off by default) - this applies to every server type, including the in-process HA-MCP Server. If your ha-mcp server runs outside Home Assistant, this entry also gives it the in-process component capabilities it cannot reach from outside (fast in-process search, config entry metadata, registry-backed reads), which are worth having even with the file/YAML tools off. You can add or remove this entry at any time."
|
||||
},
|
||||
"server": {
|
||||
"title": "HA-MCP Server",
|
||||
"description": "This runs the full ha-mcp server inside Home Assistant and exposes it remotely through a Home Assistant webhook (reachable via Nabu Casa or any reverse proxy). It is a standalone server and a complete replacement for the add-on, Docker, uvx/PyPI, and stdio install methods - do not run it alongside another ha-mcp server. Select **Submit** to start it; you can change the port, binding, and authentication afterward in the integration options."
|
||||
"description": "This runs the full ha-mcp server inside Home Assistant and exposes it remotely through a Home Assistant webhook (reachable via Nabu Casa or any reverse proxy). It is a standalone server and a complete replacement for the app (add-on), Docker, uvx/PyPI, and stdio install methods - do not run it alongside another ha-mcp server. Select **Submit** to start it; you can change the port, binding, and authentication afterward in the integration options."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This entry is already set up.",
|
||||
"unsupported_home_assistant": "The in-process HA-MCP Server requires Home Assistant {required} or newer, but this instance is running {installed}. You can still use this component's HA-MCP File & YAML Tools entry with an external ha-mcp server running as an add-on or Docker container."
|
||||
"unsupported_home_assistant": "The in-process HA-MCP Server requires Home Assistant {required} or newer, but this instance is running {installed}. You can still use this component's HA-MCP File & YAML Tools entry with an external ha-mcp server running as an app (add-on) or Docker container."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -29,11 +29,11 @@
|
||||
"title": "HA-MCP File & YAML Tools",
|
||||
"description": "Configure the privileged file and YAML editing services used by ha-mcp's opt-in file/YAML tools. These settings also appear in the ha-mcp server's own settings UI and apply live, with no restart. A non-overridable deny floor still blocks sensitive paths (e.g. .storage) and trust-boundary keys (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Extra file directories",
|
||||
"allowed_dirs": "Extra file paths",
|
||||
"extra_yaml_keys": "Extra YAML write keys"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Directories relative to the config directory, each granted read and write (e.g. pyscript). Traversal and out-of-config entries are dropped.",
|
||||
"allowed_dirs": "Paths relative to the config directory, including exact filenames such as sensor.yaml, or paths under /share, /media, /ssl, and /backup. Each entry is granted read and write for that path and anything below it; enter every path as a separate list item. Traversal and other disallowed paths are dropped.",
|
||||
"extra_yaml_keys": "Top-level keys ha_config_set_yaml may write in addition to the built-in ones, for YAML-first integrations on this install (e.g. alert2). Trust-boundary keys are dropped."
|
||||
}
|
||||
},
|
||||
@@ -64,9 +64,9 @@
|
||||
"data_description": {
|
||||
"channel": "Stable installs the latest stable release; Development installs the newest development build. When automatic updates are on, a reload or restart, plus a periodic check, install the newest build of the selected channel. A developer package override below takes precedence and disables automatic updates.",
|
||||
"auto_update": "When on, the newest release of the selected channel is installed automatically - on a reload or restart, and via a periodic check. When off, the server stays on the version currently installed until you turn this back on. This controls the ha-mcp server package only; updates to the HA-MCP Custom Component itself still come through HACS.",
|
||||
"server_port": "The port this server listens on. The ha-mcp add-on uses 9583, so this defaults to 9584 to let both run side by side - if you don't run the add-on, any free port works.",
|
||||
"bind_host": "Who can connect to the MCP server port directly. The default matches the add-on: reachable on your local network, with the secret path as the credential. Choose loopback to allow only connections from the Home Assistant machine itself - the webhook URL and the sidebar panel work either way.",
|
||||
"webhook_auth": "How MCP clients prove themselves at the webhook URL. With the secret URL, the link itself is the credential. With Home Assistant sign-in, clients such as claude.ai log in with a Home Assistant administrator account (OAuth). With legacy OAuth, this integration issues its own Client ID and Secret to paste into clients that require one, such as Google Gemini Spark - it needs a Home Assistant restart to turn on or off.",
|
||||
"server_port": "The port this server listens on. The ha-mcp app (add-on) uses 9583, so this defaults to 9584 to let both run side by side - if you don't run the app (add-on), any free port works.",
|
||||
"bind_host": "Who can connect to the MCP server port directly. The default matches the app (add-on): reachable on your local network, with the secret path as the credential. Choose loopback to allow only connections from the Home Assistant machine itself - the webhook URL and the sidebar panel work either way.",
|
||||
"webhook_auth": "How MCP clients prove themselves at the webhook URL. With the secret URL, the link itself is the only credential - OAuth still runs for clients that use it, but every sign-in is approved automatically: no login and no Client ID or Secret needed. With Home Assistant sign-in, clients such as claude.ai log in with a Home Assistant administrator account (OAuth). With legacy OAuth, this integration runs its own sign-in service and issues a Client ID and Secret to paste into clients that require one - a Home Assistant restart may be needed to fully turn it on or off.",
|
||||
"oauth_client_id_override": "Replaces the auto-generated legacy OAuth Client ID. Leave empty to keep the current one. Only used while Authentication mode is set to legacy OAuth.",
|
||||
"oauth_client_secret_override": "Replaces the auto-generated legacy OAuth Client Secret. Leave empty to keep the current one. Only used while Authentication mode is set to legacy OAuth.",
|
||||
"oauth_regenerate": "One-time action: mints a fresh Client ID and Secret for legacy OAuth mode. It takes effect only after the Home Assistant restart the repair prompt asks for - until you restart, the previous Client ID and Secret keep working and the new ones do not. Also clears the two override fields above.",
|
||||
@@ -104,11 +104,18 @@
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Component installed from the legacy repository",
|
||||
"description": "HACS is tracking the main ha-mcp server repository for this component, so HACS shows the server's version numbers (7.x) and the server's release notes here instead of the component's own (1.x). Updates keep working, but stay mislabeled this way. To fix: remove this repository from HACS (your integration settings and config entries are kept), add homeassistant-ai/ha-mcp-integration as a custom repository, reinstall the component from it, and restart Home Assistant."
|
||||
"description": "HACS is tracking the main ha-mcp server repository for this component, so HACS shows the server's version numbers and release notes here instead of the component's own (2.x). Updates keep working, but stay mislabeled this way. To fix: remove this repository from HACS (your integration settings and config entries are kept), add homeassistant-ai/ha-mcp-integration as a custom repository, reinstall the component from it, and restart Home Assistant."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Restart Home Assistant to apply the legacy OAuth change",
|
||||
"description": "The legacy OAuth authentication mode registers its own /authorize and /token web endpoints, which Home Assistant can only bind or release when it fully restarts - whether you just turned this mode on, turned it off, or changed its Client ID/Secret. Restart Home Assistant (Settings - System - Restart) to apply the change; until then the previous behavior stays in effect."
|
||||
"title": "Restart Home Assistant to apply the legacy OAuth settings",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Restart Home Assistant?",
|
||||
"description": "Submitting this form will restart Home Assistant and apply the legacy OAuth settings.\n\nClick **Submit** to restart now."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
@@ -122,7 +129,7 @@
|
||||
"options": {
|
||||
"none": "Secret webhook URL (default)",
|
||||
"ha_auth": "Sign in with Home Assistant (OAuth)",
|
||||
"legacy": "Legacy OAuth (Client ID/Secret, for Google Gemini Spark)"
|
||||
"legacy": "Legacy OAuth (this integration issues a Client ID/Secret to paste)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Vlastní komponenta HA-MCP",
|
||||
"description": "Vyberte, co chcete přidat. **HA-MCP Server** spustí kompletní server ha-mcp uvnitř Home Assistanta a zpřístupní ho prostřednictvím webhooku Home Assistanta – toto je instalace, kterou většina uživatelů chce. Jedná se o samostatný server a kompletní náhradu za všechny ostatní způsoby instalace: Aplikace (doplněk), Docker, uvx/PyPI, stdio; pokud jej spouštíte, nespouštějte současně žádnou z nich. **HA-MCP File & YAML Tools** má dvě role. Pokud váš server ha-mcp běží mimo Home Assistanta – aplikace, Docker, uvx/PyPI, stdio – tato položka tomuto serveru poskytuje také schopnosti komponenty běžící v procesu, které nemůže dosáhnout zvenčí (rychlé vyhledávání v procesu, metadata položky konfigurace, čtení podpořená registry), takže stojí za to ji přidat ke každé externí instalaci, a slouží jako brána pro privilegované služby úprav souborů a YAML, které používají volitelné nástroje pro soubory/YAML v ha-mcp. Pokud místo toho přidáte položku HA-MCP Server, tato položka již tyto sdílené schopnosti poskytuje, takže tato druhá položka je pak potřeba pouze jako brána pro volitelné nástroje pro soubory/YAML. Funguje se všemi typy serverů a lze ji přidat kdykoliv později.",
|
||||
"menu_options": {
|
||||
"server": "Server HA-MCP (doporučeno)",
|
||||
"tools": "Nástroje souborů a YAML HA-MCP (volitelné)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "Nástroje souborů a YAML HA-MCP",
|
||||
"description": "Nastavuje privilegované služby pro úpravy konfigurace souborů a YAML, které zůstávají chráněny za volitelnými nástroji pro úpravy souborů/YAML v ha-mcp (funkční příznaky, ve výchozím nastavení vypnuto) – to platí pro každý typ serveru, včetně vnitroprocesového HA-MCP Serveru. Pokud váš server ha-mcp běží mimo Home Assistanta, tato položka mu také poskytuje schopnosti komponenty běžící v procesu, které nemůže dosáhnout zvenčí (rychlé vyhledávání v procesu, metadata položky konfigurace, čtení podpořená registry), které stojí za to mít i s vypnutými nástroji pro soubory/YAML. Tuto položku můžete kdykoliv přidat nebo odebrat."
|
||||
},
|
||||
"server": {
|
||||
"title": "Server HA-MCP",
|
||||
"description": "Tím se spustí kompletní server ha-mcp uvnitř Home Assistanta a zpřístupní se vzdáleně prostřednictvím webhooku Home Assistanta (dostupného přes Nabu Casa nebo libovolný reverzní proxy server). Jedná se o samostatný server a kompletní náhradu za instalační metody App (add-on), Docker, uvx/PyPI a stdio – nespouštějte jej současně s jiným serverem ha-mcp. Výběrem možnosti **Odeslat** jej spustíte; port, vazbu a ověřování můžete později změnit v nastavení integrace."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Tato položka je již nastavena.",
|
||||
"unsupported_home_assistant": "In-process HA-MCP Server vyžaduje Home Assistant {required} nebo novější, ale tato instance běží na verzi {installed}. Záznam HA-MCP File & YAML Tools této komponenty můžete stále používat s externím serverem ha-mcp běžícím jako app (add-on) nebo kontejner Docker."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "Nástroje souborů a YAML HA-MCP",
|
||||
"description": "Nakonfigurujte privilegované služby pro úpravu souborů a YAML používané volitelnými nástroji souborů/YAML ha-mcp. Tato nastavení se také zobrazují v uživatelském rozhraní nastavení serveru ha-mcp a aplikují se okamžitě, bez restartu. Nepřepisovatelná úroveň zákazu stále blokuje citlivé cesty (např. .storage) a klíče hranice důvěry (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Další adresáře souborů",
|
||||
"extra_yaml_keys": "Další klíče pro zápis YAML"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Adresáře vzhledem ke konfiguračnímu adresáři, každému je uděleno právo čtení a zápisu (např. pyscript). Průchod a položky mimo konfiguraci jsou zahozeny.",
|
||||
"extra_yaml_keys": "Klíče nejvyšší úrovně, které ha_config_set_yaml může zapisovat navíc k vestavěným, pro integrace s prioritou YAML v této instalaci (např. alert2). Klíče hranice důvěry jsou zahozeny."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "Server HA-MCP",
|
||||
"description": "Nakonfigurujte server HA-MCP. {panel_hint}Změny zde se použijí po uložení.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "Kanál vydání",
|
||||
"auto_update": "Automatické aktualizace serveru",
|
||||
"server_port": "Naslouchací port serveru MCP",
|
||||
"bind_host": "Síťový přístup",
|
||||
"webhook_auth": "Režim ověřování",
|
||||
"oauth_client_id_override": "Legacy OAuth: vlastní ID klienta (volitelné)",
|
||||
"oauth_client_secret_override": "Legacy OAuth: vlastní tajný klíč klienta (volitelné)",
|
||||
"oauth_regenerate": "Legacy OAuth: vygenerovat ID/tajný klíč klienta nyní",
|
||||
"pip_spec": "Vývojář: přepsání balíčku ha-mcp",
|
||||
"server_url": "URL Home Assistanta (pokročilé)",
|
||||
"external_url": "Externí URL (volitelné)",
|
||||
"webhook_id_override": "Vlastní tajný klíč webhooku (volitelné)",
|
||||
"secret_path_override": "Vlastní cesta pro přímý přístup (volitelné)",
|
||||
"regenerate_secrets": "Vygenerovat tajné klíče připojení nyní",
|
||||
"enable_webhook": "Vzdálený přístup přes webhook",
|
||||
"enable_llm_api": "API LLM konverzačního agenta",
|
||||
"llm_api_exposure": "Zpřístupnění nástrojů konverzačního agenta",
|
||||
"enable_startup_notification": "Oznámení o spuštění",
|
||||
"enable_sidebar_panel": "Panel nastavení postranního panelu"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "Stabilní instaluje nejnovější stabilní vydání; Vývoj instaluje nejnovější vývojovou verzi. Když jsou zapnuty automatické aktualizace, opětovné načtení nebo restart, plus pravidelná kontrola, nainstalují nejnovější sestavení vybraného kanálu. Přepsání balíčku vývojáře níže má přednost a zakazuje automatické aktualizace.",
|
||||
"auto_update": "Když je zapnuto, nejnovější vydání vybraného kanálu se nainstaluje automaticky – při opětovném načtení nebo restartu a prostřednictvím pravidelné kontroly. Když je vypnuto, server zůstane na aktuálně nainstalované verzi, dokud to znovu nezapnete. Toto ovládá pouze balíček serveru ha-mcp; aktualizace samotné vlastní komponenty HA-MCP stále přicházejí přes HACS.",
|
||||
"server_port": "Port, na kterém tento server naslouchá. Aplikace (doplněk) ha-mcp používá port 9583, takže ve výchozím nastavení je nastaveno 9584, aby mohly oba běžet vedle sebe – pokud aplikaci (doplněk) nepoužíváte, funguje jakýkoliv volný port.",
|
||||
"bind_host": "Kdo se může připojit přímo k portu MCP serveru. Výchozí nastavení odpovídá aplikaci (doplněku): dosažitelné ve vaší lokální síti, přičemž přihlašovacím údajem je tajná cesta. Zvolte loopback, chcete-li povolit připojení pouze ze samotného stroje Home Assistant – adresa URL webhooku a panel postranního panelu fungují v obou případech.",
|
||||
"webhook_auth": "Jak se klienti MCP ověřují na URL webhooku. S tajnou URL je samotný odkaz jediným pověřením – OAuth stále běží pro klienty, kteří jej používají, ale každé přihlášení je schváleno automaticky: není potřeba žádné přihlášení ani Client ID nebo Secret. S přihlášením Home Assistant se klienti jako claude.ai přihlašují pomocí administrátorského účtu Home Assistant (OAuth). S legacy OAuth tato integrace spouští vlastní přihlašovací službu a vydává Client ID a Secret k vložení do klientů, kteří je vyžadují – k úplnému zapnutí nebo vypnutí může být potřeba restart Home Assistanta.",
|
||||
"oauth_client_id_override": "Nahrazuje automaticky generované legacy OAuth ID klienta. Ponechte prázdné, abyste zachovali stávající. Používá se pouze, když je režim ověřování nastaven na legacy OAuth.",
|
||||
"oauth_client_secret_override": "Nahrazuje automaticky generovaný legacy OAuth tajný klíč klienta. Ponechte prázdné, abyste zachovali stávající. Používá se pouze, když je režim ověřování nastaven na legacy OAuth.",
|
||||
"oauth_regenerate": "Jednorázová akce: vytvoří nové ID klienta a tajný klíč pro režim legacy OAuth. Projeví se až po restartu Home Assistanta, o který požádá výzva k opravě – dokud nerestartujete, předchozí ID klienta a tajný klíč fungují a nové ne. Také vymaže dvě pole pro přepsání výše.",
|
||||
"pip_spec": "Ponechte prázdné. Pouze pro testování konkrétní sestavy ha-mcp (například předběžné vydání); přepíše kanál vydání a zakáže automatické aktualizace, dokud nebude vymazáno.",
|
||||
"server_url": "Adresa URL, kterou server běžící ve stejném procesu používá k připojení k vašemu Home Assistantovi (obvykle k této instanci samotné). Ponechte prázdné, aby se odvodila z portu a nastavení SSL této instance; hodnotu nastavte pouze tehdy, když server potřebuje jinou cestu.",
|
||||
"external_url": "Zobrazeno jako primární URL pro připojení – použijte toto, když Home Assistant běží za vaší vlastní doménou nebo reverzním proxy. Zadejte celou základní adresu včetně schématu. Musí směřovat přímo na Home Assistanta – otevření v prohlížeči by mělo vést na vaši přihlašovací stránku HA – a nesmí obsahovat port, jako je :8123 (nebo jakýkoli jiný port), jinak jej vzdálení klienti MCP nebudou moci dosáhnout. Ponechte prázdné pro automatické použití Nabu Casa / místní adresy.",
|
||||
"webhook_id_override": "Nahrazuje náhodný tajný klíč webhooku v URL pro připojení (/api/webhook/...). URL je pověření – použijte dlouhou, těžko uhodnutelnou hodnotu. Ponechte prázdné, abyste zachovali stávající.",
|
||||
"secret_path_override": "Nahrazuje náhodnou cestu použitou pro přímý přístup na portu serveru. Stejné pravidlo: cesta je pověření. Ponechte prázdné, abyste zachovali stávající.",
|
||||
"regenerate_secrets": "Jednorázová akce: vytvoří nové náhodné tajemství webhooku a cestu pro přímý přístup, okamžitě zneplatní staré připojovací URL. Také vymaže dvě výše uvedená pole pro přepsání.",
|
||||
"enable_webhook": "Vypněte pro režim pouze lokální: webhook Home Assistanta není vůbec registrován, takže nic – včetně Nabu Casa – nemůže dosáhnout na server přes Home Assistant. Přímý port serveru a boční panel zůstanou funkční.",
|
||||
"enable_llm_api": "Nabídněte kompletní sadu nástrojů konverzačním agentům Home Assistanta (OpenAI, Google, Ollama, ...): pokud je povoleno, agenti mohou vybrat „HA-MCP Server“ pod „Control Home Assistant“ a ovládat nástroje z chatu a hlasu Assist. Povolení pouze zpřístupní výběr – nic není vystaveno, dokud to nevyberete u agenta. Průvodce použitím: {llm_api_docs_url}",
|
||||
"llm_api_exposure": "Tvar sady nástrojů nabízených konverzačním agentům. Vyhledávání nástrojů (výchozí) udržuje kontext agenta malý: kompaktní API s připnutými nástroji plus meta-nástroje pro vyhledávání/spouštění. Úplný katalog uvádí každý vystavený nástroj přímo – lepší pro modely s velkým kontextem. Obě možnosti registrují obě vedle sebe, takže si každý agent vybere svou vlastní pod „Control Home Assistant“. Vystavení jednotlivých nástrojů se spravuje v panelu nastavení HA-MCP; podrobnosti: {llm_api_docs_url}",
|
||||
"enable_startup_notification": "Zobrazit oznámení při každém spuštění serveru, odkazující na nastavení pouze pro administrátory. Vypněte pro tiché spuštění – připojovací URL se stále zobrazují v protokolu Home Assistanta.",
|
||||
"enable_sidebar_panel": "Zobrazit panel nastavení HA-MCP v postranním panelu (pouze pro administrátory). Vypněte pro odstranění položky z postranního panelu – možnosti serveru zůstanou dostupné na této obrazovce."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "Server HA-MCP běžící ve stejném procesu se nepodařilo spustit",
|
||||
"description": "Server HA-MCP běžící ve stejném procesu se nepodařilo spustit uvnitř Home Assistanta:\n\n{detail}\n\nZkontrolujte protokoly Home Assistanta a poté znovu načtěte integraci (nebo opravte základní problém a znovu načtěte) pro opakování."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "Balíček serveru HA-MCP běžícího ve stejném procesu se nepodařilo nainstalovat",
|
||||
"description": "Instalace balíčku ha-mcp pro server běžící ve stejném procesu selhala:\n\n{detail}\n\nVyřešte výše popsaný problém s kompatibilitou nebo instalací a poté znovu načtěte integraci pro opakování."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "Aktualizujte vlastní komponentu HA-MCP přes HACS",
|
||||
"description": "Nainstalovaný server ha-mcp vyžaduje vlastní komponentu HA-MCP {required} nebo novější, ale máte {installed}. Aktualizujte komponentu přes HACS (otevřete položku „HA-MCP Custom Component“ a použijte „Update information“, pokud se ještě nezobrazuje žádná aktualizace) a restartujte Home Assistanta. Server mezitím zůstane spuštěný, ale některé novější funkce nemusí fungovat, dokud nebude komponenta aktualizována."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "Aktualizace serveru HA-MCP čeká na aktualizaci komponenty",
|
||||
"description": "Server ha-mcp {latest} je k dispozici, ale tato verze také aktualizovala vlastní komponentu HA-MCP (na {shipped}; vy používáte {running}). Aby se zabránilo spuštění verze serveru, se kterou spuštěná komponenta nikdy nebyla testována, je automatická aktualizace serveru pozastavena, dokud nebude komponenta aktualizována.\n\nAktualizujte komponentu přes HACS (otevřete položku „HA-MCP Custom Component“ a použijte „Update information“, pokud se ještě nezobrazuje žádná aktualizace), poté restartujte Home Assistanta – aktualizace serveru se poté nainstaluje automaticky. Chcete-li aktualizaci serveru nainstalovat i tak, stiskněte „Install“ na entitě aktualizace serveru HA-MCP."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Komponenta nainstalovaná z původního repozitáře",
|
||||
"description": "HACS sleduje hlavní repozitář serveru ha-mcp pro tuto komponentu, takže HACS zde zobrazuje čísla verzí a poznámky k vydání serveru namísto vlastních (2.x) komponenty. Aktualizace stále fungují, ale zůstávají takto chybně označeny. Pro opravu: odeberte tento repozitář z HACS (nastavení vaší integrace a konfigurační záznamy zůstanou zachovány), přidejte homeassistant-ai/ha-mcp-integration jako vlastní repozitář, přeinstalujte z něj komponentu a restartujte Home Assistanta."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Restartujte Home Assistanta pro použití nastavení legacy OAuth",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Restartovat Home Assistanta?",
|
||||
"description": "Odesláním tohoto formuláře se restartuje Home Assistant a použijí se nastavení legacy OAuth.\n\nKlikněte na **Odeslat** pro restartování nyní."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "Stabilní (doporučeno)",
|
||||
"dev": "Vývojová (nejnovější sestavení)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "Tajná URL webhooku (výchozí)",
|
||||
"ha_auth": "Přihlásit se pomocí Home Assistanta (OAuth)",
|
||||
"legacy": "Legacy OAuth (tato integrace vydává Client ID/Secret k vložení)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "Vyhledávání nástrojů (kompaktní, výchozí)",
|
||||
"full": "Úplný katalog",
|
||||
"both": "Obě (vyberte pro každého agenta)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "Aktualizovat"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Přímý přístup ze stroje Home Assistanta: {url}",
|
||||
"connect_local_lan": "Lokální/LAN (když je „Network access“ „Local network“): {url}",
|
||||
"connect_remote_url": "Vzdálená připojovací URL: {url}",
|
||||
"connect_urls_label": "Připojovací URL:",
|
||||
"connect_urls_pending": "Připojovací URL se zde (a v protokolu Home Assistanta) objeví, jakmile se server spustí.",
|
||||
"connect_webhook_disabled": "Vzdálený přístup přes webhook je zakázán (režim pouze lokální).",
|
||||
"oauth_creds_pending": "ID klienta a tajný klíč klienta se zde objeví, jakmile se server spustí.",
|
||||
"oauth_not_serving": "Legacy OAuth je zatím neposkytuje — restartujte Home Assistanta, až vás o to požádá, abyste je aktivovali.",
|
||||
"oauth_select_legacy_mode": "Nastavte režim ověřování na „legacy OAuth“ výše a uložte pro vygenerování ID klienta a tajného klíče klienta.",
|
||||
"panel_hint": "Otevřete [panel nastavení HA-MCP](/ha-mcp) pro správu nástrojů a nastavení serveru.",
|
||||
"tools_module_installed": "Modul beta/pokročilých nástrojů pro soubory a YAML (volitelné): Nainstalováno",
|
||||
"tools_module_not_installed": "Modul beta/pokročilých nástrojů pro soubory a YAML (volitelné): Není nainstalováno – stiskněte „Add entry“ na stránce této integrace a vyberte „HA-MCP File & YAML Tools“ pro jeho přidání",
|
||||
"tools_module_not_loaded": "Modul beta/pokročilých nástrojů pro soubory a YAML (volitelné): Nainstalováno, ale nenačteno – povolte nebo znovu načtěte položku „HA-MCP File & YAML Tools“ na stránce této integrace",
|
||||
"version_line": "Komponenta {component_version} – Server ha-mcp {server_version} (kanál {channel})",
|
||||
"version_not_installed": "zatím nenainstalováno",
|
||||
"version_unknown": "neznámá"
|
||||
}
|
||||
}
|
||||
@@ -1,161 +1,168 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "HA-MCP Custom Component",
|
||||
"description": "Wähle aus, was du hinzufügen möchtest. **HA-MCP Server** führt den vollständigen ha-mcp-Server in Home Assistant aus und stellt ihn über einen Home Assistant Webhook bereit – das ist die Installation, die die meisten Nutzer brauchen. Er ist ein eigenständiger Server und ein vollständiger Ersatz für alle anderen Installationsmethoden (Add-on, Docker, uvx/PyPI, stdio); wenn du ihn nutzt, solltest du keine dieser anderen Methoden gleichzeitig verwenden. **HA-MCP File & YAML Tools** fügt die privilegierten Datei- und YAML-Bearbeitungsdienste hinzu, die nur benötigt werden, wenn du die opt-in Datei/YAML-Tools von ha-mcp aktivierst. Wenn dein ha-mcp-Server bereits woanders läuft (Add-on, Docker, uvx), benötigst du den Server-Eintrag nicht – füge nur diesen File & YAML-Eintrag hinzu, und auch nur, wenn du diese Tools verwendest. Er funktioniert mit jedem Server-Typ und kann jederzeit später hinzugefügt werden.",
|
||||
"menu_options": {
|
||||
"server": "HA-MCP Server (empfohlen)",
|
||||
"tools": "HA-MCP File & YAML Tools (optional)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "HA-MCP File & YAML Tools",
|
||||
"description": "Richtet die privilegierten Datei- und YAML-Konfigurationsdienste ein. Nur erforderlich, wenn du die opt-in Datei/YAML-Bearbeitungstools von ha-mcp aktivierst (Feature-Flags, standardmäßig deaktiviert) – dies gilt für jeden Server-Typ, einschließlich des In-Process HA-MCP Servers. Du kannst diesen Eintrag jederzeit hinzufügen oder entfernen."
|
||||
},
|
||||
"server": {
|
||||
"title": "HA-MCP Server",
|
||||
"description": "Dies führt den vollständigen ha-mcp-Server in Home Assistant aus und stellt ihn remote über einen Home Assistant Webhook bereit (erreichbar über Nabu Casa oder jeden Reverse Proxy). Er ist ein eigenständiger Server und ein vollständiger Ersatz für die Add-on-, Docker-, uvx/PyPI- und stdio-Installationsmethoden – führe ihn nicht neben einem anderen ha-mcp-Server aus. Wähle **Absenden**, um ihn zu starten; du kannst Port, Binding und Authentifizierung danach in den Integrationsoptionen ändern."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Dieser Eintrag ist bereits eingerichtet.",
|
||||
"unsupported_home_assistant": "Der In-Process HA-MCP Server benötigt Home Assistant {required} oder neuer, aber diese Instanz läuft mit {installed}. Du kannst weiterhin den HA-MCP File & YAML Tools-Eintrag dieses Components mit einem externen ha-mcp-Server verwenden, der als Add-on oder Docker-Container läuft."
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "HA-MCP Custom Component",
|
||||
"description": "Wähle aus, was hinzugefügt werden soll. **HA-MCP Server** führt den vollständigen ha-mcp-Server innerhalb von Home Assistant aus und stellt ihn über einen Home Assistant Webhook bereit – dies ist die Installation, die die meisten Anwohner wünschen. Es ist ein eigenständiger Server und ein vollständiger Ersatz für alle anderen Installationsmethoden: App (Add-on), Docker, uvx/PyPI, stdio; wenn du ihn ausführst, betreibe nicht gleichzeitig eine dieser anderen. **HA-MCP File & YAML Tools** hat zwei Rollen. Wenn dein ha-mcp-Server außerhalb von Home Assistant läuft – App, Docker, uvx/PyPI, stdio – verleiht dieser Eintrag diesem Server außerdem die Funktionen der In-Process-Komponente, die er von außen nicht erreichen kann (schnelle In-Process-Suche, Konfigurationseintrags-Metadaten, registerbasierte Lesevorgänge), weshalb es sich lohnt, ihn jeder externen Installation hinzuzufügen, und er sichert die privilegierten Datei- und YAML-Bearbeitungsdienste ab, die von den optionalen Datei-/YAML-Tools von ha-mcp genutzt werden. Wenn du stattdessen den Eintrag **HA-MCP Server** hinzufügst, stellt dieser Eintrag diese gemeinsamen Funktionen bereits bereit, sodass dieser zweite Eintrag danach nur noch als Absicherung für die optionalen Datei-/YAML-Tools benötigt wird. Er funktioniert mit jedem Servertyp und kann jederzeit später hinzugefügt werden.",
|
||||
"menu_options": {
|
||||
"server": "HA-MCP Server (empfohlen)",
|
||||
"tools": "HA-MCP File & YAML Tools (optional)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "HA-MCP File & YAML Tools",
|
||||
"description": "Richtet die privilegierten Datei- und YAML-Konfigurationsdienste ein, die durch die optionalen Datei-/YAML-Bearbeitungswerkzeuge von ha-mcp abgesichert bleiben (Funktions-Flags, standardmäßig deaktiviert) – dies gilt für jeden Servertyp, einschließlich des In-Process-**HA-MCP Server**. Wenn dein ha-mcp-Server außerhalb von Home Assistant läuft, verleiht dieser Eintrag ihm zudem die Funktionen der In-Process-Komponente, die von außen nicht erreichbar sind (schnelle In-Process-Suche, Konfigurationseintrags-Metadaten, registerbasierte Lesevorgänge), die sich auch dann lohnen, wenn die Datei-/YAML-Tools deaktiviert sind. Du kannst diesen Eintrag jederzeit hinzufügen oder entfernen."
|
||||
},
|
||||
"server": {
|
||||
"title": "HA-MCP Server",
|
||||
"description": "Dies führt den vollständigen ha-mcp-Server innerhalb von Home Assistant aus und stellt ihn über einen Home Assistant-Webhook (erreichbar über Nabu Casa oder einen beliebigen Reverse-Proxy) remote bereit. Es ist ein eigenständiger Server und ein vollständiger Ersatz für die Installationsmethoden App (Add-on), Docker, uvx/PyPI und stdio – betreibe ihn nicht parallel zu einem anderen ha-mcp-Server. Wähle **Absenden**, um ihn zu starten; du kannst Port, Bindung und Authentifizierung später in den Integrationsoptionen ändern."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "HA-MCP File & YAML Tools",
|
||||
"description": "Konfiguriere die privilegierten Datei- und YAML-Bearbeitungsdienste, die von den opt-in Datei/YAML-Tools von ha-mcp verwendet werden. Diese Einstellungen erscheinen auch in der eigenen Einstellungs-UI des ha-mcp-Servers und werden live wirksam, ohne Neustart. Eine nicht überschreibbare Deny-Grenze blockiert weiterhin sensible Pfade (z. B. .storage) und Vertrauensgrenzen-Keys (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Zusätzliche Datei-Verzeichnisse",
|
||||
"extra_yaml_keys": "Zusätzliche YAML-Schreib-Keys"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Verzeichnisse relativ zum Konfigurationsverzeichnis, jeweils mit Lese- und Schreibzugriff (z. B. pyscript). Traversal- und Einträge außerhalb des Konfig-Verzeichnisses werden verworfen.",
|
||||
"extra_yaml_keys": "Top-Level-Keys, die ha_config_set_yaml zusätzlich zu den eingebauten schreiben darf, für YAML-first-Integrationen auf dieser Installation (z. B. alert2). Vertrauensgrenzen-Keys werden verworfen."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "HA-MCP Server",
|
||||
"description": "Konfiguriere den HA-MCP Server. {panel_hint}Änderungen hier werden beim Speichern angewendet.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "Release-Kanal",
|
||||
"auto_update": "Automatische Server-Updates",
|
||||
"server_port": "MCP-Server Listening-Port",
|
||||
"bind_host": "Netzwerkzugriff",
|
||||
"webhook_auth": "Authentifizierungsmodus",
|
||||
"oauth_client_id_override": "Legacy OAuth: benutzerdefinierte Client ID (optional)",
|
||||
"oauth_client_secret_override": "Legacy OAuth: benutzerdefiniertes Client Secret (optional)",
|
||||
"oauth_regenerate": "Legacy OAuth: Client ID/Secret jetzt neu generieren",
|
||||
"pip_spec": "Entwickler: ha-mcp Package Override",
|
||||
"server_url": "Home Assistant URL (erweitert)",
|
||||
"external_url": "Externe URL (optional)",
|
||||
"webhook_id_override": "Benutzerdefiniertes Webhook-Secret (optional)",
|
||||
"secret_path_override": "Benutzerdefinierter Direktzugriffs-Pfad (optional)",
|
||||
"regenerate_secrets": "Verbindungs-Secrets jetzt neu generieren",
|
||||
"enable_webhook": "Remote-Zugriff über Webhook",
|
||||
"enable_llm_api": "Conversation-Agent LLM API",
|
||||
"llm_api_exposure": "Conversation-Agent Tool-Bereitstellung",
|
||||
"enable_startup_notification": "Start-Benachrichtigung",
|
||||
"enable_sidebar_panel": "Sidebar-Einstellungen-Panel"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "Stable installiert das neueste stabile Release; Development installiert den neuesten Development-Build. Wenn automatische Updates an sind, installieren ein Reload oder Restart sowie eine regelmäßige Prüfung den neuesten Build des ausgewählten Kanals. Ein Developer Package Override unten hat Vorrang und deaktiviert automatische Updates.",
|
||||
"auto_update": "Wenn an, wird das neueste Release des ausgewählten Kanals automatisch installiert – bei einem Reload oder Restart und über eine regelmäßige Prüfung. Wenn aus, bleibt der Server auf der aktuell installierten Version, bis du dies wieder einschaltest. Dies steuert nur das ha-mcp Server-Package; Updates für das HA-MCP Custom Component selbst kommen weiterhin über HACS.",
|
||||
"server_port": "Der Port, auf dem dieser Server lauscht. Das ha-mcp Add-on nutzt 9583, daher ist hier standardmäßig 9584 eingestellt, damit beide parallel laufen können – wenn du das Add-on nicht nutzt, funktioniert jeder freie Port.",
|
||||
"bind_host": "Wer sich direkt mit dem MCP-Server-Port verbinden kann. Der Standard entspricht dem Add-on: erreichbar in deinem lokalen Netzwerk, mit dem Secret-Pfad als Credential. Wähle Loopback, um nur Verbindungen von der Home Assistant Maschine selbst zuzulassen – die Webhook-URL und das Sidebar-Panel funktionieren in beiden Fällen.",
|
||||
"webhook_auth": "Wie sich MCP-Clients an der Webhook-URL authentifizieren. Mit der Secret-URL ist der Link selbst das Credential. Mit Home Assistant Sign-in melden sich Clients wie claude.ai mit einem Home Assistant Administrator-Konto an (OAuth). Mit Legacy OAuth stellt diese Integration eigene Client ID und Secret aus, die in Clients eingefügt werden, die eines benötigen, wie Google Gemini Spark – ein Home Assistant Restart ist zum Ein- oder Ausschalten erforderlich.",
|
||||
"oauth_client_id_override": "Ersetzt die automatisch generierte Legacy OAuth Client ID. Leer lassen, um die aktuelle zu behalten. Wird nur verwendet, während der Authentifizierungsmodus auf Legacy OAuth gesetzt ist.",
|
||||
"oauth_client_secret_override": "Ersetzt das automatisch generierte Legacy OAuth Client Secret. Leer lassen, um das aktuelle zu behalten. Wird nur verwendet, während der Authentifizierungsmodus auf Legacy OAuth gesetzt ist.",
|
||||
"oauth_regenerate": "Einmalige Aktion: erstellt eine neue Client ID und Secret für den Legacy OAuth Modus. Sie wird erst nach dem Home Assistant Restart wirksam, zu dem die Repair-Meldung auffordert – bis zum Restart funktionieren die vorherigen Client ID und Secret weiter, die neuen noch nicht. Löscht auch die beiden Override-Felder oben.",
|
||||
"pip_spec": "Leer lassen. Nur zum Testen eines spezifischen ha-mcp Builds (z.B. ein Pre-Release-Pin); überschreibt den Release-Kanal und deaktiviert automatische Updates, bis es geleert wird.",
|
||||
"server_url": "Die URL, die der In-Process-Server nutzt, um deinen Home Assistant zu erreichen (normalerweise diese Instanz selbst). Leer lassen, um sie aus Port und SSL-Einstellungen dieser Instanz abzuleiten; setze nur einen Wert, wenn der Server eine andere Route benötigt.",
|
||||
"external_url": "Wird als primäre Verbindungs-URL angezeigt – nutze dies, wenn Home Assistant hinter deiner eigenen Domain oder einem Reverse Proxy liegt. Gib die vollständige Basisadresse inklusive Schema ein. Sie muss direkt auf Home Assistant zeigen – beim Öffnen im Browser sollte deine HA-Login-Seite erscheinen – und darf keinen Port wie :8123 (oder einen anderen Port) enthalten, sonst können remote MCP-Clients sie nicht erreichen. Leer lassen, um automatisch Nabu Casa / die lokale Adresse zu verwenden.",
|
||||
"webhook_id_override": "Ersetzt das zufällige Webhook-Secret in der Verbindungs-URL (/api/webhook/...). Die URL ist das Credential – nutze einen langen, schwer zu erratenden Wert. Leer lassen, um das aktuelle zu behalten.",
|
||||
"secret_path_override": "Ersetzt den zufälligen Pfad für den Direktzugriff auf dem Server-Port. Gleiche Regel: Der Pfad ist das Credential. Leer lassen, um den aktuellen zu behalten.",
|
||||
"regenerate_secrets": "Einmalige Aktion: erstellt ein neues zufälliges Webhook-Secret und einen Direktzugriffs-Pfad, wodurch die alten Verbindungs-URLs sofort ungültig werden. Löscht auch die beiden Override-Felder oben.",
|
||||
"enable_webhook": "Ausschalten für nur-lokal-Modus: Der Home Assistant Webhook wird gar nicht registriert, sodass nichts – einschließlich Nabu Casa – den Server über Home Assistant erreichen kann. Der direkte Server-Port und das Sidebar-Panel funktionieren weiter.",
|
||||
"enable_llm_api": "Biete das vollständige Toolset für Home Assistant Conversation-Agenten (OpenAI, Google, Ollama, ...) an: Während aktiviert, können Agenten 'HA-MCP Server' unter Control Home Assistant auswählen und die Tools aus Assist-Chat und Sprache nutzen. Das Aktivieren macht es nur auswählbar – nichts wird bereitgestellt, bis du es bei einem Agenten auswählst. Nutzungsanleitung: {llm_api_docs_url}",
|
||||
"llm_api_exposure": "Form des Toolsets, das Conversation-Agenten angeboten wird. Tool Search (Standard) hält den Kontext des Agenten klein: eine kompakte API mit angehefteten Tools plus Such/Ausführungs-Meta-Tools. Full Catalog listet jedes bereitgestellte Tool direkt auf – besser für Modelle mit großem Kontext. Both registriert beide nebeneinander, sodass jeder Agent unter Control Home Assistant sein eigenes wählt. Die Tool-Bereitstellung pro Tool wird im HA-MCP Settings Panel verwaltet; Details: {llm_api_docs_url}",
|
||||
"enable_startup_notification": "Zeige bei jedem Server-Start eine Benachrichtigung, die auf die nur-für-Administratoren-Einstellungsoberflächen verweist. Ausschalten, um still zu starten – die Verbindungs-URLs erscheinen weiterhin im Home Assistant Log.",
|
||||
"enable_sidebar_panel": "Zeige das HA-MCP Settings Panel in der Sidebar (nur Administratoren). Ausschalten, um den Sidebar-Eintrag zu entfernen – Server-Optionen bleiben auf diesem Bildschirm verfügbar."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "Der HA-MCP In-Process-Server konnte nicht gestartet werden",
|
||||
"description": "Der HA-MCP In-Process-Server konnte nicht in Home Assistant gestartet werden:\n\n{detail}\n\nPrüfe die Home Assistant Logs und lade dann die Integration neu (oder behebe das zugrunde liegende Problem und lade neu), um es erneut zu versuchen."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "Das HA-MCP In-Process-Server Package konnte nicht installiert werden",
|
||||
"description": "Die Installation des ha-mcp Packages für den In-Process-Server ist fehlgeschlagen:\n\n{detail}\n\nBehebe das oben beschriebene Kompatibilitäts- oder Installationsproblem und lade dann die Integration neu, um es erneut zu versuchen."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "Aktualisiere das HA-MCP Custom Component über HACS",
|
||||
"description": "Der installierte ha-mcp Server benötigt HA-MCP Custom Component {required} oder neuer, aber du hast {installed}. Aktualisiere das Component über HACS (öffne den HA-MCP Custom Component-Eintrag und nutze 'Update information', falls noch kein Update angezeigt wird) und starte Home Assistant neu. Der Server läuft in der Zwischenzeit weiter, aber einige neuere Features funktionieren möglicherweise nicht, bis das Component aktualisiert ist."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "HA-MCP Server-Update wartet auf ein Component-Update",
|
||||
"description": "ha-mcp Server {latest} ist verfügbar, aber dieses Release hat auch das HA-MCP Custom Component aktualisiert (auf {shipped}; du nutzt {running}). Um zu vermeiden, dass eine Server-Version gestartet wird, die nie mit dem laufenden Component getestet wurde, ist das automatische Server-Update ausgesetzt, bis das Component aktualisiert ist.\n\nAktualisiere das Component über HACS (öffne den HA-MCP Custom Component-Eintrag und nutze 'Update information', falls noch kein Update angezeigt wird), starte dann Home Assistant neu – das Server-Update wird danach automatisch installiert. Um das Server-Update trotzdem zu installieren, drücke Install bei der HA-MCP Server Update-Entität."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Component aus dem Legacy-Repository installiert",
|
||||
"description": "HACS verfolgt das Haupt-ha-mcp-Server-Repository für dieses Component, daher zeigt HACS hier die Versionsnummern des Servers (7.x) und die Release-Notes des Servers statt der eigenen des Components (1.x). Updates funktionieren weiter, bleiben aber so falsch beschriftet. Um zu beheben: Entferne dieses Repository aus HACS (deine Integrationseinstellungen und Config-Einträge bleiben erhalten), füge homeassistant-ai/ha-mcp-integration als Custom Repository hinzu, installiere das Component daraus neu und starte Home Assistant neu."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Starte Home Assistant neu, um die Legacy OAuth-Änderung anzuwenden",
|
||||
"description": "Der Legacy OAuth-Authentifizierungsmodus registriert eigene /authorize- und /token-Web-Endpunkte, die Home Assistant nur bei einem vollständigen Restart binden oder freigeben kann – egal ob du diesen Modus gerade ein- oder ausgeschaltet oder Client ID/Secret geändert hast. Starte Home Assistant neu (Einstellungen - System - Neustart), um die Änderung anzuwenden; bis dahin bleibt das vorherige Verhalten aktiv."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "Stable (empfohlen)",
|
||||
"dev": "Development (neuester Build)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "Secret Webhook URL (Standard)",
|
||||
"ha_auth": "Mit Home Assistant anmelden (OAuth)",
|
||||
"legacy": "Legacy OAuth (Client ID/Secret, für Google Gemini Spark)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "Tool Search (kompakt, Standard)",
|
||||
"full": "Full Catalog",
|
||||
"both": "Beides (pro Agent wählen)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "Update"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Direkter Zugriff von der Home Assistant Maschine: {url}",
|
||||
"connect_local_lan": "Lokal/LAN (wenn Netzwerkzugriff auf „Local network“ steht): {url}",
|
||||
"connect_remote_url": "Remote-Verbindungs-URL: {url}",
|
||||
"connect_urls_label": "Verbindungs-URL(s):",
|
||||
"connect_urls_pending": "Die Verbindungs-URLs erscheinen hier (und im Home Assistant Log), sobald der Server gestartet ist.",
|
||||
"connect_webhook_disabled": "Remote-Zugriff über Webhook ist deaktiviert (nur-lokal-Modus).",
|
||||
"oauth_creds_pending": "Client ID und Client Secret erscheinen hier, sobald der Server gestartet ist.",
|
||||
"oauth_not_serving": "Legacy OAuth bedient diese noch nicht — starte Home Assistant neu, wenn du dazu aufgefordert wirst, um sie zu aktivieren.",
|
||||
"oauth_select_legacy_mode": "Setze den Authentifizierungsmodus oben auf Legacy OAuth und speichere, um eine Client ID und ein Client Secret zu erzeugen.",
|
||||
"panel_hint": "Öffne das [HA-MCP Einstellungs-Panel](/ha-mcp) für Tool-Verwaltung und Servereinstellungen.",
|
||||
"tools_module_installed": "Beta-/Erweitert-Modul für Datei- & YAML-Tools (optional): Installiert",
|
||||
"tools_module_not_installed": "Beta-/Erweitert-Modul für Datei- & YAML-Tools (optional): Nicht installiert — klicke auf „Eintrag hinzufügen“ auf der Seite dieser Integration und wähle „HA-MCP File & YAML Tools“",
|
||||
"tools_module_not_loaded": "Beta-/Erweitert-Modul für Datei- & YAML-Tools (optional): Installiert, aber nicht geladen — aktiviere oder lade den Eintrag „HA-MCP File & YAML Tools“ auf der Seite dieser Integration neu",
|
||||
"version_line": "Komponente {component_version} - Server ha-mcp {server_version} ({channel}-Kanal)",
|
||||
"version_not_installed": "noch nicht installiert",
|
||||
"version_unknown": "unbekannt"
|
||||
"abort": {
|
||||
"already_configured": "Dieser Eintrag ist bereits eingerichtet.",
|
||||
"unsupported_home_assistant": "Der In-Process HA-MCP Server erfordert Home Assistant {required} oder neuer, aber diese Instanz läuft mit {installed}. Du kannst den HA-MCP File & YAML Tools-Eintrag dieser Komponente weiterhin mit einem externen ha-mcp-Server verwenden, der als App (Add-on) oder Docker-Container läuft."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "HA-MCP File & YAML Tools",
|
||||
"description": "Konfiguriere die privilegierten Datei- und YAML-Bearbeitungsdienste, die von den opt-in Datei/YAML-Tools von ha-mcp verwendet werden. Diese Einstellungen erscheinen auch in der eigenen Einstellungs-UI des ha-mcp-Servers und werden live wirksam, ohne Neustart. Eine nicht überschreibbare Deny-Grenze blockiert weiterhin sensible Pfade (z. B. .storage) und Vertrauensgrenzen-Keys (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Zusätzliche Datei-Verzeichnisse",
|
||||
"extra_yaml_keys": "Zusätzliche YAML-Schreib-Keys"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Verzeichnisse relativ zum Konfigurationsverzeichnis, jeweils mit Lese- und Schreibzugriff (z. B. pyscript). Traversal- und Einträge außerhalb des Konfig-Verzeichnisses werden verworfen.",
|
||||
"extra_yaml_keys": "Top-Level-Keys, die ha_config_set_yaml zusätzlich zu den eingebauten schreiben darf, für YAML-first-Integrationen auf dieser Installation (z. B. alert2). Vertrauensgrenzen-Keys werden verworfen."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "HA-MCP Server",
|
||||
"description": "Konfiguriere den HA-MCP Server. {panel_hint}Änderungen hier werden beim Speichern angewendet.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "Release-Kanal",
|
||||
"auto_update": "Automatische Server-Updates",
|
||||
"server_port": "MCP-Server Listening-Port",
|
||||
"bind_host": "Netzwerkzugriff",
|
||||
"webhook_auth": "Authentifizierungsmodus",
|
||||
"oauth_client_id_override": "Legacy OAuth: benutzerdefinierte Client ID (optional)",
|
||||
"oauth_client_secret_override": "Legacy OAuth: benutzerdefiniertes Client Secret (optional)",
|
||||
"oauth_regenerate": "Legacy OAuth: Client ID/Secret jetzt neu generieren",
|
||||
"pip_spec": "Entwickler: ha-mcp Package Override",
|
||||
"server_url": "Home Assistant URL (erweitert)",
|
||||
"external_url": "Externe URL (optional)",
|
||||
"webhook_id_override": "Benutzerdefiniertes Webhook-Secret (optional)",
|
||||
"secret_path_override": "Benutzerdefinierter Direktzugriffs-Pfad (optional)",
|
||||
"regenerate_secrets": "Verbindungs-Secrets jetzt neu generieren",
|
||||
"enable_webhook": "Remote-Zugriff über Webhook",
|
||||
"enable_llm_api": "Conversation-Agent LLM API",
|
||||
"llm_api_exposure": "Conversation-Agent Tool-Bereitstellung",
|
||||
"enable_startup_notification": "Start-Benachrichtigung",
|
||||
"enable_sidebar_panel": "Sidebar-Einstellungen-Panel"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "Stable installiert das neueste stabile Release; Development installiert den neuesten Development-Build. Wenn automatische Updates an sind, installieren ein Reload oder Restart sowie eine regelmäßige Prüfung den neuesten Build des ausgewählten Kanals. Ein Developer Package Override unten hat Vorrang und deaktiviert automatische Updates.",
|
||||
"auto_update": "Wenn an, wird das neueste Release des ausgewählten Kanals automatisch installiert – bei einem Reload oder Restart und über eine regelmäßige Prüfung. Wenn aus, bleibt der Server auf der aktuell installierten Version, bis du dies wieder einschaltest. Dies steuert nur das ha-mcp Server-Package; Updates für das HA-MCP Custom Component selbst kommen weiterhin über HACS.",
|
||||
"server_port": "Der Port, auf dem dieser Server lauscht. Die ha-mcp App (Add-on) verwendet 9583, daher ist standardmäßig 9584 voreingestellt, damit beide nebeneinander laufen können – wenn du die App (Add-on) nicht nutzt, funktioniert jeder freie Port.",
|
||||
"bind_host": "Wer sich direkt mit dem MCP-Server-Port verbinden kann. Der Standardwert entspricht der App (Add-on): erreichbar in deinem lokalen Netzwerk, mit dem geheimen Pfad als Zugangsdaten. Wähle Loopback, um nur Verbindungen von der Home Assistant-Maschine selbst zuzulassen – die Webhook-URL und das Seitenleifen-Panel funktionieren in beiden Fällen.",
|
||||
"webhook_auth": "Wie sich MCP-Clients an der Webhook-URL authentifizieren. Bei der geheimen URL ist der Link selbst die einzige Anmeldeinformation – OAuth läuft weiterhin für Clients, die es nutzen, aber jede Anmeldung wird automatisch genehmigt: kein Login und keine Client-ID oder kein Secret erforderlich. Bei der Home Assistant-Anmeldung melden sich Clients wie claude.ai mit einem Home Assistant-Administratorkonto (OAuth) an. Bei Legacy OAuth betreibt diese Integration einen eigenen Anmeldedienst und stellt eine Client-ID und ein Secret aus, die in Clients eingefügt werden müssen, die eine solche benötigen – ein Neustart von Home Assistant kann erforderlich sein, um sie vollständig ein- oder auszuschalten.",
|
||||
"oauth_client_id_override": "Ersetzt die automatisch generierte Legacy OAuth Client ID. Leer lassen, um die aktuelle zu behalten. Wird nur verwendet, während der Authentifizierungsmodus auf Legacy OAuth gesetzt ist.",
|
||||
"oauth_client_secret_override": "Ersetzt das automatisch generierte Legacy OAuth Client Secret. Leer lassen, um das aktuelle zu behalten. Wird nur verwendet, während der Authentifizierungsmodus auf Legacy OAuth gesetzt ist.",
|
||||
"oauth_regenerate": "Einmalige Aktion: erstellt eine neue Client ID und Secret für den Legacy OAuth Modus. Sie wird erst nach dem Home Assistant Restart wirksam, zu dem die Repair-Meldung auffordert – bis zum Restart funktionieren die vorherigen Client ID und Secret weiter, die neuen noch nicht. Löscht auch die beiden Override-Felder oben.",
|
||||
"pip_spec": "Leer lassen. Nur zum Testen eines spezifischen ha-mcp Builds (z.B. ein Pre-Release-Pin); überschreibt den Release-Kanal und deaktiviert automatische Updates, bis es geleert wird.",
|
||||
"server_url": "Die URL, die der In-Process-Server nutzt, um deinen Home Assistant zu erreichen (normalerweise diese Instanz selbst). Leer lassen, um sie aus Port und SSL-Einstellungen dieser Instanz abzuleiten; setze nur einen Wert, wenn der Server eine andere Route benötigt.",
|
||||
"external_url": "Wird als primäre Verbindungs-URL angezeigt – nutze dies, wenn Home Assistant hinter deiner eigenen Domain oder einem Reverse Proxy liegt. Gib die vollständige Basisadresse inklusive Schema ein. Sie muss direkt auf Home Assistant zeigen – beim Öffnen im Browser sollte deine HA-Login-Seite erscheinen – und darf keinen Port wie :8123 (oder einen anderen Port) enthalten, sonst können remote MCP-Clients sie nicht erreichen. Leer lassen, um automatisch Nabu Casa / die lokale Adresse zu verwenden.",
|
||||
"webhook_id_override": "Ersetzt das zufällige Webhook-Secret in der Verbindungs-URL (/api/webhook/...). Die URL ist das Credential – nutze einen langen, schwer zu erratenden Wert. Leer lassen, um das aktuelle zu behalten.",
|
||||
"secret_path_override": "Ersetzt den zufälligen Pfad für den Direktzugriff auf dem Server-Port. Gleiche Regel: Der Pfad ist das Credential. Leer lassen, um den aktuellen zu behalten.",
|
||||
"regenerate_secrets": "Einmalige Aktion: erstellt ein neues zufälliges Webhook-Secret und einen Direktzugriffs-Pfad, wodurch die alten Verbindungs-URLs sofort ungültig werden. Löscht auch die beiden Override-Felder oben.",
|
||||
"enable_webhook": "Ausschalten für nur-lokal-Modus: Der Home Assistant Webhook wird gar nicht registriert, sodass nichts – einschließlich Nabu Casa – den Server über Home Assistant erreichen kann. Der direkte Server-Port und das Sidebar-Panel funktionieren weiter.",
|
||||
"enable_llm_api": "Biete das vollständige Toolset für Home Assistant Conversation-Agenten (OpenAI, Google, Ollama, ...) an: Während aktiviert, können Agenten 'HA-MCP Server' unter Control Home Assistant auswählen und die Tools aus Assist-Chat und Sprache nutzen. Das Aktivieren macht es nur auswählbar – nichts wird bereitgestellt, bis du es bei einem Agenten auswählst. Nutzungsanleitung: {llm_api_docs_url}",
|
||||
"llm_api_exposure": "Form des Toolsets, das Conversation-Agenten angeboten wird. Tool Search (Standard) hält den Kontext des Agenten klein: eine kompakte API mit angehefteten Tools plus Such/Ausführungs-Meta-Tools. Full Catalog listet jedes bereitgestellte Tool direkt auf – besser für Modelle mit großem Kontext. Both registriert beide nebeneinander, sodass jeder Agent unter Control Home Assistant sein eigenes wählt. Die Tool-Bereitstellung pro Tool wird im HA-MCP Settings Panel verwaltet; Details: {llm_api_docs_url}",
|
||||
"enable_startup_notification": "Zeige bei jedem Server-Start eine Benachrichtigung, die auf die nur-für-Administratoren-Einstellungsoberflächen verweist. Ausschalten, um still zu starten – die Verbindungs-URLs erscheinen weiterhin im Home Assistant Log.",
|
||||
"enable_sidebar_panel": "Zeige das HA-MCP Settings Panel in der Sidebar (nur Administratoren). Ausschalten, um den Sidebar-Eintrag zu entfernen – Server-Optionen bleiben auf diesem Bildschirm verfügbar."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "Der HA-MCP In-Process-Server konnte nicht gestartet werden",
|
||||
"description": "Der HA-MCP In-Process-Server konnte nicht in Home Assistant gestartet werden:\n\n{detail}\n\nPrüfe die Home Assistant Logs und lade dann die Integration neu (oder behebe das zugrunde liegende Problem und lade neu), um es erneut zu versuchen."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "Das HA-MCP In-Process-Server Package konnte nicht installiert werden",
|
||||
"description": "Die Installation des ha-mcp Packages für den In-Process-Server ist fehlgeschlagen:\n\n{detail}\n\nBehebe das oben beschriebene Kompatibilitäts- oder Installationsproblem und lade dann die Integration neu, um es erneut zu versuchen."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "Aktualisiere das HA-MCP Custom Component über HACS",
|
||||
"description": "Der installierte ha-mcp Server benötigt HA-MCP Custom Component {required} oder neuer, aber du hast {installed}. Aktualisiere das Component über HACS (öffne den HA-MCP Custom Component-Eintrag und nutze 'Update information', falls noch kein Update angezeigt wird) und starte Home Assistant neu. Der Server läuft in der Zwischenzeit weiter, aber einige neuere Features funktionieren möglicherweise nicht, bis das Component aktualisiert ist."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "HA-MCP Server-Update wartet auf ein Component-Update",
|
||||
"description": "ha-mcp Server {latest} ist verfügbar, aber dieses Release hat auch das HA-MCP Custom Component aktualisiert (auf {shipped}; du nutzt {running}). Um zu vermeiden, dass eine Server-Version gestartet wird, die nie mit dem laufenden Component getestet wurde, ist das automatische Server-Update ausgesetzt, bis das Component aktualisiert ist.\n\nAktualisiere das Component über HACS (öffne den HA-MCP Custom Component-Eintrag und nutze 'Update information', falls noch kein Update angezeigt wird), starte dann Home Assistant neu – das Server-Update wird danach automatisch installiert. Um das Server-Update trotzdem zu installieren, drücke Install bei der HA-MCP Server Update-Entität."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Component aus dem Legacy-Repository installiert",
|
||||
"description": "HACS verfolgt das Haupt-Repository des ha-mcp-Servers für diese Komponente, daher zeigt HACS hier die Versionsnummern und Release Notes des Servers anstelle der eigenen der Komponente (2.x). Updates funktionieren weiterhin, bleiben aber auf diese Weise falsch beschriftet. Zur Behebung: Entferne dieses Repository aus HACS (deine Integrationseinstellungen und Konfigurationseinträge bleiben erhalten), füge homeassistant-ai/ha-mcp-integration als benutzerdefiniertes Repository hinzu, installiere die Komponente daraus neu und starte Home Assistant neu."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Starte Home Assistant neu, um die Legacy OAuth-Einstellungen anzuwenden",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Home Assistant neu starten?",
|
||||
"description": "Das Absenden dieses Formulars startet Home Assistant neu und wendet die Legacy OAuth-Einstellungen an.\n\nKlicke auf **Senden**, um jetzt neu zu starten."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "Stable (empfohlen)",
|
||||
"dev": "Development (neuester Build)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "Secret Webhook URL (Standard)",
|
||||
"ha_auth": "Mit Home Assistant anmelden (OAuth)",
|
||||
"legacy": "Legacy OAuth (diese Integration stellt eine Client-ID/ein Secret zum Einfügen aus)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "Tool Search (kompakt, Standard)",
|
||||
"full": "Full Catalog",
|
||||
"both": "Beides (pro Agent wählen)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "Update"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Direkter Zugriff von der Home Assistant Maschine: {url}",
|
||||
"connect_local_lan": "Lokal/LAN (wenn Netzwerkzugriff auf „Local network“ steht): {url}",
|
||||
"connect_remote_url": "Remote-Verbindungs-URL: {url}",
|
||||
"connect_urls_label": "Verbindungs-URL(s):",
|
||||
"connect_urls_pending": "Die Verbindungs-URLs erscheinen hier (und im Home Assistant Log), sobald der Server gestartet ist.",
|
||||
"connect_webhook_disabled": "Remote-Zugriff über Webhook ist deaktiviert (nur-lokal-Modus).",
|
||||
"oauth_creds_pending": "Client ID und Client Secret erscheinen hier, sobald der Server gestartet ist.",
|
||||
"oauth_not_serving": "Legacy OAuth bedient diese noch nicht — starte Home Assistant neu, wenn du dazu aufgefordert wirst, um sie zu aktivieren.",
|
||||
"oauth_select_legacy_mode": "Setze den Authentifizierungsmodus oben auf Legacy OAuth und speichere, um eine Client ID und ein Client Secret zu erzeugen.",
|
||||
"panel_hint": "Öffne das [HA-MCP Einstellungs-Panel](/ha-mcp) für Tool-Verwaltung und Servereinstellungen.",
|
||||
"tools_module_installed": "Beta-/Erweitert-Modul für Datei- & YAML-Tools (optional): Installiert",
|
||||
"tools_module_not_installed": "Beta-/Erweitert-Modul für Datei- & YAML-Tools (optional): Nicht installiert — klicke auf „Eintrag hinzufügen“ auf der Seite dieser Integration und wähle „HA-MCP File & YAML Tools“",
|
||||
"tools_module_not_loaded": "Beta-/Erweitert-Modul für Datei- & YAML-Tools (optional): Installiert, aber nicht geladen — aktiviere oder lade den Eintrag „HA-MCP File & YAML Tools“ auf der Seite dieser Integration neu",
|
||||
"version_line": "Komponente {component_version} - Server ha-mcp {server_version} ({channel}-Kanal)",
|
||||
"version_not_installed": "noch nicht installiert",
|
||||
"version_unknown": "unbekannt"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "HA-MCP Custom Component",
|
||||
"description": "Choose what to add. **HA-MCP Server** runs the full ha-mcp server inside Home Assistant and exposes it through a Home Assistant webhook - this is the install most people want. It is a standalone server and a complete replacement for every other install method (add-on, Docker, uvx/PyPI, stdio); if you run it, do not also run one of those. **HA-MCP File & YAML Tools** adds the privileged file and YAML editing services, needed only if you turn on ha-mcp's opt-in file/YAML tools. If your ha-mcp server already runs elsewhere (add-on, Docker, uvx), you do not need the server entry - add only this File & YAML entry, and only if you use those tools. It works with every server type and can be added later at any time.",
|
||||
"description": "Choose what to add. **HA-MCP Server** runs the full ha-mcp server inside Home Assistant and exposes it through a Home Assistant webhook - this is the install most people want. It is a standalone server and a complete replacement for every other install method: App (add-on), Docker, uvx/PyPI, stdio; if you run it, do not also run one of those. **HA-MCP File & YAML Tools** has two roles. If your ha-mcp server runs outside Home Assistant - app, Docker, uvx/PyPI, stdio - this entry also gives that server the in-process component capabilities it cannot reach from outside (fast in-process search, config entry metadata, registry-backed reads), so it is worth adding to every external install, and it gates the privileged file and YAML editing services used by ha-mcp's opt-in file/YAML tools. If you add the HA-MCP Server entry instead, that entry already provides those shared capabilities, so this second entry is then needed only as the gate for the opt-in file/YAML tools. It works with every server type and can be added later at any time.",
|
||||
"menu_options": {
|
||||
"server": "HA-MCP Server (recommended)",
|
||||
"tools": "HA-MCP File & YAML Tools (optional)"
|
||||
@@ -11,16 +11,16 @@
|
||||
},
|
||||
"tools": {
|
||||
"title": "HA-MCP File & YAML Tools",
|
||||
"description": "Sets up the privileged file and YAML configuration services. Only needed if you enable ha-mcp's opt-in file/YAML editing tools (feature flags, off by default) - this applies to every server type, including the in-process HA-MCP Server. You can add or remove this entry at any time."
|
||||
"description": "Sets up the privileged file and YAML configuration services, which stay gated behind ha-mcp's opt-in file/YAML editing tools (feature flags, off by default) - this applies to every server type, including the in-process HA-MCP Server. If your ha-mcp server runs outside Home Assistant, this entry also gives it the in-process component capabilities it cannot reach from outside (fast in-process search, config entry metadata, registry-backed reads), which are worth having even with the file/YAML tools off. You can add or remove this entry at any time."
|
||||
},
|
||||
"server": {
|
||||
"title": "HA-MCP Server",
|
||||
"description": "This runs the full ha-mcp server inside Home Assistant and exposes it remotely through a Home Assistant webhook (reachable via Nabu Casa or any reverse proxy). It is a standalone server and a complete replacement for the add-on, Docker, uvx/PyPI, and stdio install methods - do not run it alongside another ha-mcp server. Select **Submit** to start it; you can change the port, binding, and authentication afterward in the integration options."
|
||||
"description": "This runs the full ha-mcp server inside Home Assistant and exposes it remotely through a Home Assistant webhook (reachable via Nabu Casa or any reverse proxy). It is a standalone server and a complete replacement for the app (add-on), Docker, uvx/PyPI, and stdio install methods - do not run it alongside another ha-mcp server. Select **Submit** to start it; you can change the port, binding, and authentication afterward in the integration options."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This entry is already set up.",
|
||||
"unsupported_home_assistant": "The in-process HA-MCP Server requires Home Assistant {required} or newer, but this instance is running {installed}. You can still use this component's HA-MCP File & YAML Tools entry with an external ha-mcp server running as an add-on or Docker container."
|
||||
"unsupported_home_assistant": "The in-process HA-MCP Server requires Home Assistant {required} or newer, but this instance is running {installed}. You can still use this component's HA-MCP File & YAML Tools entry with an external ha-mcp server running as an app (add-on) or Docker container."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -29,11 +29,11 @@
|
||||
"title": "HA-MCP File & YAML Tools",
|
||||
"description": "Configure the privileged file and YAML editing services used by ha-mcp's opt-in file/YAML tools. These settings also appear in the ha-mcp server's own settings UI and apply live, with no restart. A non-overridable deny floor still blocks sensitive paths (e.g. .storage) and trust-boundary keys (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Extra file directories",
|
||||
"allowed_dirs": "Extra file paths",
|
||||
"extra_yaml_keys": "Extra YAML write keys"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Directories relative to the config directory, each granted read and write (e.g. pyscript). Traversal and out-of-config entries are dropped.",
|
||||
"allowed_dirs": "Paths relative to the config directory, including exact filenames such as sensor.yaml, or paths under /share, /media, /ssl, and /backup. Each entry is granted read and write for that path and anything below it; enter every path as a separate list item. Traversal and other disallowed paths are dropped.",
|
||||
"extra_yaml_keys": "Top-level keys ha_config_set_yaml may write in addition to the built-in ones, for YAML-first integrations on this install (e.g. alert2). Trust-boundary keys are dropped."
|
||||
}
|
||||
},
|
||||
@@ -64,9 +64,9 @@
|
||||
"data_description": {
|
||||
"channel": "Stable installs the latest stable release; Development installs the newest development build. When automatic updates are on, a reload or restart, plus a periodic check, install the newest build of the selected channel. A developer package override below takes precedence and disables automatic updates.",
|
||||
"auto_update": "When on, the newest release of the selected channel is installed automatically - on a reload or restart, and via a periodic check. When off, the server stays on the version currently installed until you turn this back on. This controls the ha-mcp server package only; updates to the HA-MCP Custom Component itself still come through HACS.",
|
||||
"server_port": "The port this server listens on. The ha-mcp add-on uses 9583, so this defaults to 9584 to let both run side by side - if you don't run the add-on, any free port works.",
|
||||
"bind_host": "Who can connect to the MCP server port directly. The default matches the add-on: reachable on your local network, with the secret path as the credential. Choose loopback to allow only connections from the Home Assistant machine itself - the webhook URL and the sidebar panel work either way.",
|
||||
"webhook_auth": "How MCP clients prove themselves at the webhook URL. With the secret URL, the link itself is the credential. With Home Assistant sign-in, clients such as claude.ai log in with a Home Assistant administrator account (OAuth). With legacy OAuth, this integration issues its own Client ID and Secret to paste into clients that require one, such as Google Gemini Spark - it needs a Home Assistant restart to turn on or off.",
|
||||
"server_port": "The port this server listens on. The ha-mcp app (add-on) uses 9583, so this defaults to 9584 to let both run side by side - if you don't run the app (add-on), any free port works.",
|
||||
"bind_host": "Who can connect to the MCP server port directly. The default matches the app (add-on): reachable on your local network, with the secret path as the credential. Choose loopback to allow only connections from the Home Assistant machine itself - the webhook URL and the sidebar panel work either way.",
|
||||
"webhook_auth": "How MCP clients prove themselves at the webhook URL. With the secret URL, the link itself is the only credential - OAuth still runs for clients that use it, but every sign-in is approved automatically: no login and no Client ID or Secret needed. With Home Assistant sign-in, clients such as claude.ai log in with a Home Assistant administrator account (OAuth). With legacy OAuth, this integration runs its own sign-in service and issues a Client ID and Secret to paste into clients that require one - a Home Assistant restart may be needed to fully turn it on or off.",
|
||||
"oauth_client_id_override": "Replaces the auto-generated legacy OAuth Client ID. Leave empty to keep the current one. Only used while Authentication mode is set to legacy OAuth.",
|
||||
"oauth_client_secret_override": "Replaces the auto-generated legacy OAuth Client Secret. Leave empty to keep the current one. Only used while Authentication mode is set to legacy OAuth.",
|
||||
"oauth_regenerate": "One-time action: mints a fresh Client ID and Secret for legacy OAuth mode. It takes effect only after the Home Assistant restart the repair prompt asks for - until you restart, the previous Client ID and Secret keep working and the new ones do not. Also clears the two override fields above.",
|
||||
@@ -104,11 +104,18 @@
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Component installed from the legacy repository",
|
||||
"description": "HACS is tracking the main ha-mcp server repository for this component, so HACS shows the server's version numbers (7.x) and the server's release notes here instead of the component's own (1.x). Updates keep working, but stay mislabeled this way. To fix: remove this repository from HACS (your integration settings and config entries are kept), add homeassistant-ai/ha-mcp-integration as a custom repository, reinstall the component from it, and restart Home Assistant."
|
||||
"description": "HACS is tracking the main ha-mcp server repository for this component, so HACS shows the server's version numbers and release notes here instead of the component's own (2.x). Updates keep working, but stay mislabeled this way. To fix: remove this repository from HACS (your integration settings and config entries are kept), add homeassistant-ai/ha-mcp-integration as a custom repository, reinstall the component from it, and restart Home Assistant."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Restart Home Assistant to apply the legacy OAuth change",
|
||||
"description": "The legacy OAuth authentication mode registers its own /authorize and /token web endpoints, which Home Assistant can only bind or release when it fully restarts - whether you just turned this mode on, turned it off, or changed its Client ID/Secret. Restart Home Assistant (Settings - System - Restart) to apply the change; until then the previous behavior stays in effect."
|
||||
"title": "Restart Home Assistant to apply the legacy OAuth settings",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Restart Home Assistant?",
|
||||
"description": "Submitting this form will restart Home Assistant and apply the legacy OAuth settings.\n\nClick **Submit** to restart now."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
@@ -122,7 +129,7 @@
|
||||
"options": {
|
||||
"none": "Secret webhook URL (default)",
|
||||
"ha_auth": "Sign in with Home Assistant (OAuth)",
|
||||
"legacy": "Legacy OAuth (Client ID/Secret, for Google Gemini Spark)"
|
||||
"legacy": "Legacy OAuth (this integration issues a Client ID/Secret to paste)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "HA-MCP Propra Komponanto",
|
||||
"description": "Elektu kion aldoni. **HA-MCP Server** rulas la plenan ha-mcp-servilon ene de Home Assistant kaj elmetas ĝin per Home Assistant-retooko (webhook) — ĉi tio estas la instalo, kiun la plej multaj homoj deziras. Ĝi estas memstara servilo kaj kompleta anstataŭaĵo por ĉiu alia instala metodo: Apo (aldonaĵo), Docker, uvx/PyPI, stdio; se vi rulas ĝin, ne krome rulas iun el tiuj. **HA-MCP File & YAML Tools** havas du rolojn. Se via ha-mcp-servilo rulas ekster Home Assistant — apo, Docker, uvx/PyPI, stdio — ĉi tiu enigo ankaŭ donas al tiu servilo la enprocezajn komponentajn kapablojn, kiujn ĝi ne povas atingi de ekstere (rapida enproceza serĉo, metadatenoj de agorda enigo, legadoj subtenataj de registro), do indas aldoni ĝin al ĉiu ekstera instalo, kaj ĝi regas la privilegiitajn dosierajn kaj YAML-redaktajn servojn uzatajn de la laŭvolaj dosieraj/YAML-iloj de ha-mcp. Se vi aldonas la enigon de HA-MCP Server anstataŭe, tiu enigo jam provizas tiujn komunajn kapablojn, do ĉi tiu dua enigo tiam necesas nur kiel la pordego por la laŭvolaj dosieraj/YAML-iloj. Ĝi funkcias kun ĉiu servilspeco kaj povas esti aldonita poste iam ajn.",
|
||||
"menu_options": {
|
||||
"server": "HA-MCP Servilo (rekomendita)",
|
||||
"tools": "HA-MCP Dosieraj & YAML Iloj (laŭvola)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "HA-MCP Dosieraj & YAML Iloj",
|
||||
"description": "Agordas la privilegiitajn dosierajn kaj YAML-agordajn servojn, kiuj restas regataj malantaŭ la laŭvolaj dosieraj/YAML-redaktaj iloj de ha-mcp (funkciaj flagoj, defaŭlte malŝaltitaj) — ĉi tio aplikiĝas al ĉiu servilspeco, inkluzive de la enproceza HA-MCP Server. Se via ha-mcp-servilo rulas ekster Home Assistant, ĉi tiu enigo ankaŭ donas al ĝi la enprocezajn komponentajn kapablojn, kiujn ĝi ne povas atingi de ekstere (rapida enproceza serĉo, metadatenoj de agorda enigo, legadoj subtenataj de registro), kiuj indas havi eĉ kiam la dosieraj/YAML-iloj estas malŝaltitaj. Vi povas aldoni aŭ forigi ĉi tiun enigon iam ajn."
|
||||
},
|
||||
"server": {
|
||||
"title": "HA-MCP Servilo",
|
||||
"description": "Ĉi tio rulas la plenan ha-mcp servilon ene de Home Assistant kaj elmetas ĝin malproksime per Home Assistant rethoko (webhook) (atingebla per Nabu Casa aŭ iu ajn inversa prokurilo). Ĝi estas memstara servilo kaj kompleta anstataŭaĵo por la apo (aldonaĵo), Docker, uvx/PyPI, kaj stdio instalaj metodoj — ne rulu ĝin kune kun alia ha-mcp servilo. Elektu **Submit** por starti ĝin; vi povas ŝalti la pordon, ligadon, kaj aŭtentigon poste en la integriĝaj opcioj."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Ĉi tiu eniro jam estas agordita.",
|
||||
"unsupported_home_assistant": "La enproceza HA-MCP Server postulas Home Assistant {required} aŭ pli novan, sed ĉi tiu instanco rulas {installed}. Vi ankoraŭ povas uzi la eniron HA-MCP File & YAML Tools de ĉi tiu kompenento kun ekstera ha-mcp servilo rulanta kiel apo (aldonaĵo) aŭ Docker-ujo."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "HA-MCP Dosieraj & YAML Iloj",
|
||||
"description": "Agordu la privilegiajn dosierajn kaj YAML-redaktajn servojn uzatajn de la laŭvolaj dosieraj/YAML-iloj de ha-mcp. Ĉi tiuj agordoj ankaŭ aperas en la propra agorda UI de la ha-mcp servilo kaj aplikiĝas tuj, sen rekomenco. Ne-superregebla malpermesa limo ankoraŭ blokas sentemajn vojojn (ekz. .storage) kaj fid-limajn ŝlosilojn (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Ekstraj dosierujoj",
|
||||
"extra_yaml_keys": "Ekstraj YAML-skribaj ŝlosiloj"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Dosierujoj rilataj al la agorda dosierujo, ĉiu kun legado kaj skribado permesita (ekz. pyscript). Trairo kaj ekster-agordaj eniroj estas forĵetitaj.",
|
||||
"extra_yaml_keys": "Ĉefnivelaj ŝlosiloj, kiujn ha_config_set_yaml povas skribi krom la enkonstruitaj, por YAML-unuaj integriĝoj en ĉi tiu instalaĵo (ekz. alert2). Fid-limaj ŝlosiloj estas forĵetitaj."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "HA-MCP Servilo",
|
||||
"description": "Agordu la HA-MCP servilon. {panel_hint}Ŝanĝoj ĉi tie aplikiĝas post konservado.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "Eldona kanalo",
|
||||
"auto_update": "Aŭtomataj servilaj ĝisdatigoj",
|
||||
"server_port": "Aŭskulta pordo de MCP servilo",
|
||||
"bind_host": "Reta aliro",
|
||||
"webhook_auth": "Aŭtentiga reĝimo",
|
||||
"oauth_client_id_override": "Hereda OAuth: propra Klienta ID (laŭvola)",
|
||||
"oauth_client_secret_override": "Hereda OAuth: propra Klienta Sekreto (laŭvola)",
|
||||
"oauth_regenerate": "Hereda OAuth: regeneru Klientan ID/Sekreton nun",
|
||||
"pip_spec": "Programisto: ha-mcp pakaĵa superrego",
|
||||
"server_url": "Home Assistant URL (altnivela)",
|
||||
"external_url": "Ekstera URL (laŭvola)",
|
||||
"webhook_id_override": "Propra retkroĉa sekreto (laŭvola)",
|
||||
"secret_path_override": "Propra rekta-alira vojo (laŭvola)",
|
||||
"regenerate_secrets": "Regeneru konektajn sekretojn nun",
|
||||
"enable_webhook": "Malproksima aliro per retkroĉo",
|
||||
"enable_llm_api": "Konversacia-agenta LLM API",
|
||||
"llm_api_exposure": "Konversacia-agenta ilo-ekspono",
|
||||
"enable_startup_notification": "Komenca sciigo",
|
||||
"enable_sidebar_panel": "Flankbreta agorda panelo"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "Stabila instalas la plej novan stabilan eldonon; Disvolva instalas la plej novan disvolvan konstruaĵon. Kiam aŭtomataj ĝisdatigoj estas ŝaltitaj, reŝargo aŭ rekomenco, plus perioda kontrolo, instalas la plej novan konstruaĵon de la elektita kanalo. Programista pakaĵa superrego sube havas prioritaton kaj malŝaltas aŭtomatajn ĝisdatigojn.",
|
||||
"auto_update": "Kiam ŝaltita, la plej nova eldono de la elektita kanalo estas instalita aŭtomate — dum reŝargo aŭ rekomenco, kaj per perioda kontrolo. Kiam malŝaltita, la servilo restas ĉe la nuntempe instalita versio ĝis vi reŝaltas ĉi tion. Ĉi tio regas nur la ha-mcp servilan pakaĵon; ĝisdatigoj al la HA-MCP Propra Komponanto mem ankoraŭ venas per HACS.",
|
||||
"server_port": "La pordo, kiun ĉi tiu servilo aŭskultas. La ha-mcp apo (aldonaĵo) uzas 9583, do ĉi tio defaŭltas al 9584 por lasi ambaŭ ruli flank-al-flanke — se vi ne rulas la apon (aldonaĵon), iu ajn libera pordo funkcias.",
|
||||
"bind_host": "Kiu povas konekti al la MCP-servila pordo rekte. La defaŭlto kongruas kun la apo (aldonaĵo): atingebla en via loka retumo, kun la sekreta vojo kiel la akreditaĵo. Elektu enbuklon (loopback) por permesi nur konektojn de la Home Assistant-maŝino mem — la rethoka URL kaj la flanka panelo funkcias ambaŭkaze.",
|
||||
"webhook_auth": "Kiel MCP-klientoj pruvas sin ĉe la retkroĉa URL. Kun la sekreta URL, la ligilo mem estas la sola akreditaĵo — OAuth ankoraŭ funkcias por klientoj, kiuj uzas ĝin, sed ĉiu ensaluto estas aŭtomate aprobita: neniu ensaluto kaj neniu Klienta ID aŭ Sekreto necesas. Kun Home Assistant-ensaluto, klientoj kiel claude.ai ensalutas per Home Assistant-administranta konto (OAuth). Kun hereda OAuth, ĉi tiu integriĝo funkciigas sian propran ensalutan servon kaj eldonas Klientan ID-on kaj Sekreton por alglui en klientojn, kiuj postulas tian — rekomenco de Home Assistant eble necesos por plene ŝalti aŭ malŝalti ĝin.",
|
||||
"oauth_client_id_override": "Anstataŭigas la aŭtomate generitan heredan OAuth Klientan ID. Lasu malplena por konservi la nunan. Nur uzata kiam Aŭtentiga reĝimo estas agordita al hereda OAuth.",
|
||||
"oauth_client_secret_override": "Anstataŭigas la aŭtomate generitan heredan OAuth Klientan Sekreton. Lasu malplena por konservi la nunan. Nur uzata kiam Aŭtentiga reĝimo estas agordita al hereda OAuth.",
|
||||
"oauth_regenerate": "Unufoja ago: kreas novan Klientan ID kaj Sekreton por hereda OAuth-reĝimo. Ĝi ekvalidas nur post la rekomenco de Home Assistant, kiun la ripara peto petas — ĝis vi rekomencos, la antaŭaj Klienta ID kaj Sekreto daŭre funkcias kaj la novaj ne. Ankaŭ malplenigas la du superregajn kampojn supre.",
|
||||
"pip_spec": "Lasu malplena. Nur por testi specifan ha-mcp konstruaĵon (ekzemple antaŭ-eldona fiksado); superregas la eldonan kanalon kaj malŝaltas aŭtomatajn ĝisdatigojn ĝis malplenigite.",
|
||||
"server_url": "La URL, kiun la servilo funkcianta en la sama procezo uzas por atingi vian Home Assistant (kutime ĉi tiu instanco mem). Lasu ĝin malplena por derivi la valoron el la pordo kaj la SSL-agordoj de ĉi tiu instanco; agordu valoron nur kiam la servilo bezonas alian vojon.",
|
||||
"external_url": "Montrata kiel la ĉefa konekta URL — uzu ĉi tion kiam Home Assistant sidas malantaŭ via propra domajno aŭ inversa prokurilo. Enigu la plenan bazan adreson inkluzive de la skemo. Ĝi devas montri rekte al Home Assistant — malfermante ĝin en retumilo devus atingi vian HA ensalutan paĝon — kaj ne devas enhavi pordon kiel :8123 (aŭ iu ajn alia pordo), alie malproksimaj MCP klientoj ne povos atingi ĝin. Lasu malplena por uzi Nabu Casa / la lokan adreson aŭtomate.",
|
||||
"webhook_id_override": "Anstataŭigas la hazardan retkroĉan sekreton en la konekta URL (/api/webhook/...). La URL estas la akreditaĵo — uzu longan, malfacile diveneblan valoron. Lasu malplena por konservi la nunan.",
|
||||
"secret_path_override": "Anstataŭigas la hazardan vojon uzatan por rekta aliro ĉe la servila pordo. Sama regulo: la vojo estas la akreditaĵo. Lasu malplena por konservi la nunan.",
|
||||
"regenerate_secrets": "Unufoja ago: kreas novan hazardan retkroĉan sekreton kaj rektaliran vojon, tuj malvalidigante la malnovajn konektajn URL-ojn. Ankaŭ malplenigas la du superajn anstataŭigajn kampojn.",
|
||||
"enable_webhook": "Malŝaltu por nur-loka reĝimo: la retkroĉo de Home Assistant tute ne estas registrita, do nenio — inkluzive de Nabu Casa — povas atingi la servilon per Home Assistant. La rekta servila pordo kaj la flanka panelo daŭre funkcias.",
|
||||
"enable_llm_api": "Proponu la plenan ilaron al konversaciaj agentoj de Home Assistant (OpenAI, Google, Ollama, ...): dum ebligite, agentoj povas elekti 'HA-MCP Server' sub Kontroli Home Assistant kaj uzi la ilojn el Asista babilejo kaj voĉo. Ebligo nur igas ĝin elektebla — nenio estas elmontrita ĝis vi elektas ĝin ĉe agento. Gvidilo pri uzado: {llm_api_docs_url}",
|
||||
"llm_api_exposure": "Formo de la ilaro proponita al konversaciaj agentoj. Ilo-serĉo (defaŭlta) tenas la kuntekston de la agento malgranda: kompakta API kun fiksitaj iloj plus serĉaj/ekzekutaj meta-iloj. Plena katalogo listigas ĉiun elmontritan ilon rekte — pli bone por grand-kuntekstaj modeloj. Ambaŭ registras la du flank-al-flanke, do ĉiu agento elektas sian propran sub Kontroli Home Assistant. Elmontro laŭ ilo estas administrata en la agorda panelo de HA-MCP; detaloj: {llm_api_docs_url}",
|
||||
"enable_startup_notification": "Montru sciigon ĉiufoje kiam la servilo startas, montrante al la nur-administraj agordaj surfacoj. Malŝaltu por starti silente — la konektaj URL-oj ankoraŭ aperas en la protokolo de Home Assistant.",
|
||||
"enable_sidebar_panel": "Montru la agordan panelon de HA-MCP en la flanka kolumno (nur por administrantoj). Malŝaltu por forigi la flankan eniron — servilaj opcioj restas disponeblaj sur ĉi tiu ekrano."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "La en-proceza servilo de HA-MCP ne sukcesis starti",
|
||||
"description": "La en-proceza servilo de HA-MCP ne povis esti startigita ene de Home Assistant:\n\n{detail}\n\nKontrolu la protokolojn de Home Assistant, poste reŝarĝu la integradon (aŭ riparu la suban problemon kaj reŝarĝu) por reprovi."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "La pakaĵo de la en-proceza servilo de HA-MCP ne povis esti instalita",
|
||||
"description": "Instalado de la pakaĵo ha-mcp por la en-proceza servilo malsukcesis:\n\n{detail}\n\nSolvu la kongruecan aŭ instalan problemon priskribitan supre, poste reŝarĝu la integradon por reprovi."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "Ĝisdatigu la Personan Komponanton de HA-MCP per HACS",
|
||||
"description": "La instalita servilo ha-mcp postulas Personan Komponanton de HA-MCP {required} aŭ pli novan, sed vi havas {installed}. Ĝisdatigu la komponanton per HACS (malfermu la eniron de la Persona Komponanto de HA-MCP kaj uzu 'Ĝisdatigi informojn' se neniu ĝisdatigo ankoraŭ montriĝas) kaj rekomencu Home Assistant. La servilo daŭre funkcias dume, sed iuj pli novaj funkcioj eble ne funkcios ĝis la komponanto estos ĝisdatigita."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "Ĝisdatigo de servilo HA-MCP atendas ĝisdatigon de komponanto",
|
||||
"description": "Servilo ha-mcp {latest} estas disponebla, sed tiu eldono ankaŭ ĝisdatigis la Personan Komponanton de HA-MCP (al {shipped}; vi uzas {running}). Por eviti startigi servilan version, kun kiu la nuna komponanto neniam estis testita, la aŭtomata servila ĝisdatigo estas prokrastita ĝis la komponanto estos ĝisdatigita.\n\nĜisdatigu la komponanton per HACS (malfermu la eniron de la Persona Komponanto de HA-MCP kaj uzu 'Ĝisdatigi informojn' se neniu ĝisdatigo ankoraŭ montriĝas), poste rekomencu Home Assistant — la servila ĝisdatigo instaliĝos aŭtomate poste. Por instali la servilan ĝisdatigon ĉiukaze, premu Instali ĉe la ĝisdatiga ento de la servilo HA-MCP."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Komponanto instalita el la hereda deponejo",
|
||||
"description": "HACS spuras la ĉefan deponejon de la ha-mcp-servilo por ĉi tiu komponanto, do HACS montras la versinumerojn kaj eldondetalojn de la servilo ĉi tie anstataŭ tiujn de la komponanto mem (2.x). Ĝisdatigoj daŭre funkcias, sed restas misetikeditaj tiel. Por ripari: forigu ĉi tiun deponejon el HACS (viaj integriĝaj agordoj kaj agordaj enskriboj estas konservitaj), aldonu homeassistant-ai/ha-mcp-integration kiel propran deponejon, reinstalu la komponanton el ĝi, kaj rekomencu Home Assistant."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Rekomencu Home Assistant por apliki la heredajn OAuth-agordojn",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Rekomencu Home Assistant?",
|
||||
"description": "Sendi ĉi tiun formularon rekomencos Home Assistant kaj aplikos la heredajn OAuth-agordojn.\n\nAlklaku **Sendi** por rekomenci nun."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "Stabila (rekomendita)",
|
||||
"dev": "Disvolva (plej nova konstruo)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "Sekreta retkroĉa URL (defaŭlta)",
|
||||
"ha_auth": "Ensaluti per Home Assistant (OAuth)",
|
||||
"legacy": "Hereda OAuth (ĉi tiu integriĝo eldonas Klientan ID-on/Sekreton por alglui)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "Ilo-serĉo (kompakta, defaŭlta)",
|
||||
"full": "Plena katalogo",
|
||||
"both": "Ambaŭ (elektu laŭ agento)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "Ĝisdatigo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Rekta aliro el la maŝino de Home Assistant: {url}",
|
||||
"connect_local_lan": "Loka/LAN (kiam Reto-aliro estas \"Local network\"): {url}",
|
||||
"connect_remote_url": "Fora konekta URL: {url}",
|
||||
"connect_urls_label": "Konektaj URL(j):",
|
||||
"connect_urls_pending": "La konektaj URL-oj aperos ĉi tie (kaj en la protokolo de Home Assistant) post kiam la servilo startos.",
|
||||
"connect_webhook_disabled": "Fora aliro per retkroĉo estas malŝaltita (nur-loka reĝimo).",
|
||||
"oauth_creds_pending": "La Klienta ID kaj Klienta Sekreto aperos ĉi tie post kiam la servilo startos.",
|
||||
"oauth_not_serving": "Legacy OAuth ankoraŭ ne liveras ilin — rekomencu Home Assistant, kiam ĝi petas vin pri tio, por aktivigi ilin.",
|
||||
"oauth_select_legacy_mode": "Agordu Aŭtentigan reĝimon al hereda OAuth supre kaj konservu por generi Klientan ID kaj Klientan Sekreton.",
|
||||
"panel_hint": "Malfermu la [agordan panelon de HA-MCP](/ha-mcp) por ilo-administrado kaj servilaj agordoj.",
|
||||
"tools_module_installed": "Beta/altnivela modulo de dosieraj & YAML-iloj (laŭvola): Instalita",
|
||||
"tools_module_not_installed": "Beta/altnivela modulo de dosieraj & YAML-iloj (laŭvola): Ne instalita — premu \"Aldoni eniron\" sur la paĝo de ĉi tiu integriĝo kaj elektu \"HA-MCP File & YAML Tools\" por aldoni ĝin",
|
||||
"tools_module_not_loaded": "Beta/altnivela modulo de dosieraj & YAML-iloj (laŭvola): Instalita sed ne ŝargita — ebligu aŭ reŝarĝu la eniron \"HA-MCP File & YAML Tools\" sur la paĝo de ĉi tiu integriĝo",
|
||||
"version_line": "Komponanto {component_version} - Servilo ha-mcp {server_version} ({channel} kanalo)",
|
||||
"version_not_installed": "ankoraŭ ne instalita",
|
||||
"version_unknown": "nekonata"
|
||||
}
|
||||
}
|
||||
@@ -1,161 +1,168 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Componente personalizado HA-MCP",
|
||||
"description": "Elige qué quieres añadir. **Servidor HA-MCP** ejecuta el servidor ha-mcp completo dentro de Home Assistant y lo expone a través de un webhook de Home Assistant: es la instalación que quiere la mayoría de la gente. Es un servidor autónomo y un sustituto completo de cualquier otro método de instalación (complemento, Docker, uvx/PyPI, stdio); si lo usas, no uses además uno de esos. **Herramientas de archivos y YAML de HA-MCP** añade los servicios privilegiados de edición de archivos y YAML, necesarios solo si activas las herramientas opcionales de archivos/YAML de ha-mcp. Si tu servidor ha-mcp ya se ejecuta en otro sitio (complemento, Docker, uvx), no necesitas la entrada del servidor: añade solo esta entrada de archivos y YAML, y solo si usas esas herramientas. Funciona con cualquier tipo de servidor y se puede añadir más adelante en cualquier momento.",
|
||||
"menu_options": {
|
||||
"server": "Servidor HA-MCP (recomendado)",
|
||||
"tools": "Herramientas de archivos y YAML de HA-MCP (opcional)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "Herramientas de archivos y YAML de HA-MCP",
|
||||
"description": "Configura los servicios privilegiados de archivos y de configuración YAML. Solo hacen falta si activas las herramientas opcionales de edición de archivos/YAML de ha-mcp (indicadores de función, desactivados de forma predeterminada); esto vale para cualquier tipo de servidor, incluido el servidor HA-MCP integrado en el proceso. Puedes añadir o quitar esta entrada en cualquier momento."
|
||||
},
|
||||
"server": {
|
||||
"title": "Servidor HA-MCP",
|
||||
"description": "Esto ejecuta el servidor ha-mcp completo dentro de Home Assistant y lo expone de forma remota a través de un webhook de Home Assistant (accesible mediante Nabu Casa o cualquier proxy inverso). Es un servidor autónomo y un sustituto completo de los métodos de instalación por complemento, Docker, uvx/PyPI y stdio: no lo ejecutes junto a otro servidor ha-mcp. Pulsa **Enviar** para iniciarlo; después puedes cambiar el puerto, la vinculación y la autenticación en las opciones de la integración."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Esta entrada ya está configurada.",
|
||||
"unsupported_home_assistant": "El servidor HA-MCP integrado en el proceso requiere Home Assistant {required} o posterior, pero esta instancia ejecuta {installed}. Aun así puedes usar la entrada HA-MCP File & YAML Tools de este componente con un servidor ha-mcp externo que se ejecute como complemento o contenedor Docker."
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Componente personalizado HA-MCP",
|
||||
"description": "Elige qué añadir. **HA-MCP Server** ejecuta el servidor completo de ha-mcp dentro de Home Assistant y lo expone a través de un webhook de Home Assistant; esta es la instalación que la mayoría de la gente prefiere. Es un servidor independiente y un reemplazo completo de cualquier otro método de instalación: aplicación (complemento), Docker, uvx/PyPI, stdio; si lo ejecutes, no ejecutes también uno de esos. **HA-MCP File & YAML Tools** tiene dos funciones. Si tu servidor ha-mcp se ejecuta fuera de Home Assistant (aplicación, Docker, uvx/PyPI, stdio), esta entrada también le otorga al servidor las capacidades de componentes internos a las que no puede acceder desde el exterior (búsqueda rápida interna, metadatos de entradas de configuración, lecturas respaldadas por el registro), por lo que vale la pena añadirla a cada instalación externa, y sirve como puerta de enlace para los servicios privilegiados de edición de archivos y YAML utilizados por las herramientas opcionales de archivos/YAML de ha-mcp. Si añades la entrada HA-MCP Server en su lugar, dicha entrada ya proporciona esas capacidades compartidas, por lo que esta segunda entrada solo será necesaria como puerta de enlace para las herramientas opcionales de archivos/YAML. Funciona con cualquier tipo de servidor y se puede añadir más tarde en cualquier momento.",
|
||||
"menu_options": {
|
||||
"server": "Servidor HA-MCP (recomendado)",
|
||||
"tools": "Herramientas de archivos y YAML de HA-MCP (opcional)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "Herramientas de archivos y YAML de HA-MCP",
|
||||
"description": "Configura los servicios privilegiados de configuración de archivos y YAML, que permanecen protegidos detrás de las herramientas opcionales de edición de archivos/YAML de ha-mcp (conmutadores de características, desactivados por defecto); esto se aplica a todos los tipos de servidor, incluido el HA-MCP Server interno. Si tu servidor ha-mcp se ejecuta fuera de Home Assistant, esta entrada también le otorga las capacidades de componentes internos a las que no puede acceder desde el exterior (búsqueda rápida interna, metadatos de entradas de configuración, lecturas respaldadas por el registro), las cuales vale la pena tener incluso con las herramientas de archivos/YAML desactivadas. Puedes añadir o eliminar esta entrada en cualquier momento."
|
||||
},
|
||||
"server": {
|
||||
"title": "Servidor HA-MCP",
|
||||
"description": "Esto ejecuta el servidor ha-mcp completo dentro de Home Assistant y lo expone de forma remota a través de un webhook de Home Assistant (accesible mediante Nabu Casa o cualquier proxy inverso). Es un servidor independiente y reemplaza por completo los métodos de instalación de aplicación (complemento), Docker, uvx/PyPI y stdio; no lo ejecutes junto con otro servidor ha-mcp. Selecciona **Enviar** para iniciarlo; puedes cambiar el puerto, la vinculación y la autenticación más adelante en las opciones de la integración."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "Herramientas de archivos y YAML de HA-MCP",
|
||||
"description": "Configura los servicios privilegiados de edición de archivos y YAML que usan las herramientas opcionales de archivos/YAML de ha-mcp. Estos ajustes aparecen también en la propia interfaz de ajustes del servidor ha-mcp y se aplican en caliente, sin reiniciar. Un mínimo de denegación no anulable sigue bloqueando rutas sensibles (p. ej. .storage) y claves de la frontera de confianza (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Directorios de archivos adicionales",
|
||||
"extra_yaml_keys": "Claves YAML de escritura adicionales"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Directorios relativos al directorio de configuración, cada uno con permiso de lectura y escritura (p. ej. pyscript). Se descartan los recorridos de ruta y las entradas fuera de la configuración.",
|
||||
"extra_yaml_keys": "Claves de primer nivel que ha_config_set_yaml puede escribir además de las integradas, para integraciones basadas en YAML de esta instalación (p. ej. alert2). Las claves de la frontera de confianza se descartan."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "Servidor HA-MCP",
|
||||
"description": "Configura el servidor HA-MCP. {panel_hint}Los cambios de aquí se aplican al guardar.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "Canal de versiones",
|
||||
"auto_update": "Actualizaciones automáticas del servidor",
|
||||
"server_port": "Puerto de escucha del servidor MCP",
|
||||
"bind_host": "Acceso desde la red",
|
||||
"webhook_auth": "Modo de autenticación",
|
||||
"oauth_client_id_override": "OAuth heredado: Client ID personalizado (opcional)",
|
||||
"oauth_client_secret_override": "OAuth heredado: Client Secret personalizado (opcional)",
|
||||
"oauth_regenerate": "OAuth heredado: regenerar ahora el Client ID y el Client Secret",
|
||||
"pip_spec": "Desarrollo: paquete ha-mcp alternativo",
|
||||
"server_url": "URL de Home Assistant (avanzado)",
|
||||
"external_url": "URL externa (opcional)",
|
||||
"webhook_id_override": "Secreto de webhook personalizado (opcional)",
|
||||
"secret_path_override": "Ruta de acceso directo personalizada (opcional)",
|
||||
"regenerate_secrets": "Regenerar ahora los secretos de conexión",
|
||||
"enable_webhook": "Acceso remoto mediante webhook",
|
||||
"enable_llm_api": "API de LLM para agentes de conversación",
|
||||
"llm_api_exposure": "Exposición de herramientas al agente de conversación",
|
||||
"enable_startup_notification": "Notificación de inicio",
|
||||
"enable_sidebar_panel": "Panel de ajustes en la barra lateral"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "Estable instala la última versión estable; Desarrollo instala la compilación de desarrollo más reciente. Con las actualizaciones automáticas activadas, una recarga o un reinicio, más una comprobación periódica, instalan la compilación más reciente del canal elegido. El paquete alternativo de desarrollo de abajo tiene prioridad y desactiva las actualizaciones automáticas.",
|
||||
"auto_update": "Cuando está activado, la versión más reciente del canal elegido se instala automáticamente: en una recarga o reinicio y mediante una comprobación periódica. Cuando está desactivado, el servidor se queda en la versión instalada hasta que lo vuelvas a activar. Esto controla únicamente el paquete del servidor ha-mcp; las actualizaciones del propio componente personalizado HA-MCP siguen llegando por HACS.",
|
||||
"server_port": "El puerto en el que escucha este servidor. El complemento ha-mcp usa el 9583, así que aquí el valor predeterminado es 9584 para que ambos puedan convivir; si no usas el complemento, sirve cualquier puerto libre.",
|
||||
"bind_host": "Quién puede conectarse directamente al puerto del servidor MCP. El valor predeterminado coincide con el del complemento: accesible en tu red local, con la ruta secreta como credencial. Elige loopback para permitir solo conexiones desde la propia máquina de Home Assistant; la URL del webhook y el panel de la barra lateral funcionan en ambos casos.",
|
||||
"webhook_auth": "Cómo se identifican los clientes MCP en la URL del webhook. Con la URL secreta, el propio enlace es la credencial. Con el inicio de sesión de Home Assistant, clientes como claude.ai entran con una cuenta de administrador de Home Assistant (OAuth). Con OAuth heredado, esta integración emite su propio Client ID y Client Secret para pegarlos en clientes que los exijan, como Google Gemini Spark; activarlo o desactivarlo requiere reiniciar Home Assistant.",
|
||||
"oauth_client_id_override": "Sustituye el Client ID de OAuth heredado generado automáticamente. Déjalo vacío para conservar el actual. Solo se usa mientras el modo de autenticación esté en OAuth heredado.",
|
||||
"oauth_client_secret_override": "Sustituye el Client Secret de OAuth heredado generado automáticamente. Déjalo vacío para conservar el actual. Solo se usa mientras el modo de autenticación esté en OAuth heredado.",
|
||||
"oauth_regenerate": "Acción puntual: genera un Client ID y un Client Secret nuevos para el modo OAuth heredado. Solo surte efecto tras el reinicio de Home Assistant que pide el aviso de reparación; hasta que reinicies siguen funcionando el Client ID y el Client Secret anteriores, y no los nuevos. También vacía los dos campos de sustitución de arriba.",
|
||||
"pip_spec": "Déjalo vacío. Solo sirve para probar una compilación concreta de ha-mcp (por ejemplo, fijar una versión preliminar); tiene prioridad sobre el canal de versiones y desactiva las actualizaciones automáticas hasta que lo vacíes.",
|
||||
"server_url": "La URL que usa el servidor integrado para llegar a tu Home Assistant (normalmente esta misma instancia). Déjala vacía para deducirla del puerto y los ajustes SSL de esta instancia; indica un valor solo si el servidor necesita otra ruta.",
|
||||
"external_url": "Se muestra como URL de conexión principal: úsala cuando Home Assistant esté detrás de tu propio dominio o proxy inverso. Introduce la dirección base completa, incluido el esquema. Debe apuntar directamente a Home Assistant —abrirla en un navegador debería llevar a tu página de inicio de sesión de HA— y no debe contener un puerto como :8123 (ni ningún otro), o los clientes MCP remotos no podrán llegar. Déjala vacía para usar automáticamente Nabu Casa o la dirección local.",
|
||||
"webhook_id_override": "Sustituye el secreto aleatorio del webhook en la URL de conexión (/api/webhook/...). La URL es la credencial: usa un valor largo y difícil de adivinar. Déjalo vacío para conservar el actual.",
|
||||
"secret_path_override": "Sustituye la ruta aleatoria que se usa para el acceso directo en el puerto del servidor. Misma regla: la ruta es la credencial. Déjalo vacío para conservar la actual.",
|
||||
"regenerate_secrets": "Acción puntual: genera un secreto de webhook y una ruta de acceso directo aleatorios nuevos, invalidando de inmediato las URL de conexión antiguas. También vacía los dos campos de sustitución de arriba.",
|
||||
"enable_webhook": "Desactívalo para el modo solo local: el webhook de Home Assistant no se registra en absoluto, así que nada —Nabu Casa incluida— puede llegar al servidor a través de Home Assistant. El puerto directo del servidor y el panel de la barra lateral siguen funcionando.",
|
||||
"enable_llm_api": "Ofrece el conjunto completo de herramientas a los agentes de conversación de Home Assistant (OpenAI, Google, Ollama, ...): mientras esté activado, los agentes pueden seleccionar 'HA-MCP Server' en Controlar Home Assistant y manejar las herramientas desde el chat y la voz de Assist. Activarlo solo lo hace seleccionable: no se expone nada hasta que lo elijas en un agente. Guía de uso: {llm_api_docs_url}",
|
||||
"llm_api_exposure": "Forma del conjunto de herramientas que se ofrece a los agentes de conversación. La búsqueda de herramientas (predeterminada) mantiene pequeño el contexto del agente: una API compacta con herramientas fijadas más metaherramientas de búsqueda y ejecución. El catálogo completo lista directamente todas las herramientas expuestas, mejor para modelos con contexto grande. Ambos registra las dos opciones en paralelo para que cada agente elija la suya en Controlar Home Assistant. La exposición por herramienta se gestiona en el panel de ajustes de HA-MCP; detalles: {llm_api_docs_url}",
|
||||
"enable_startup_notification": "Muestra una notificación cada vez que arranca el servidor, con enlaces a las superficies de ajustes reservadas a administradores. Desactívalo para arrancar en silencio: las URL de conexión siguen apareciendo en el registro de Home Assistant.",
|
||||
"enable_sidebar_panel": "Muestra el panel de ajustes de HA-MCP en la barra lateral (solo para administradores). Desactívalo para quitar la entrada de la barra lateral: las opciones del servidor siguen disponibles en esta pantalla."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "El servidor HA-MCP integrado en el proceso no ha podido arrancar",
|
||||
"description": "El servidor HA-MCP integrado en el proceso no se ha podido iniciar dentro de Home Assistant:\n\n{detail}\n\nRevisa los registros de Home Assistant y vuelve a cargar la integración (o corrige el problema de fondo y recárgala) para reintentarlo."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "No se ha podido instalar el paquete del servidor HA-MCP integrado en el proceso",
|
||||
"description": "Ha fallado la instalación del paquete ha-mcp para el servidor integrado en el proceso:\n\n{detail}\n\nResuelve el problema de compatibilidad o de instalación descrito arriba y vuelve a cargar la integración para reintentarlo."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "Actualiza el componente personalizado HA-MCP mediante HACS",
|
||||
"description": "El servidor ha-mcp instalado requiere el componente personalizado HA-MCP {required} o posterior, pero tienes {installed}. Actualiza el componente mediante HACS (abre la entrada del componente personalizado HA-MCP y usa 'Actualizar información' si todavía no aparece ninguna actualización) y reinicia Home Assistant. Mientras tanto el servidor sigue funcionando, pero puede que algunas funciones nuevas no funcionen hasta que se actualice el componente."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "Actualización del servidor HA-MCP a la espera de una actualización del componente",
|
||||
"description": "Está disponible el servidor ha-mcp {latest}, pero esa versión actualizó también el componente personalizado HA-MCP (a {shipped}; tú ejecutas {running}). Para evitar arrancar una versión del servidor con la que el componente en uso nunca se ha probado, la actualización automática del servidor queda en espera hasta que se actualice el componente.\n\nActualiza el componente mediante HACS (abre la entrada del componente personalizado HA-MCP y usa 'Actualizar información' si todavía no aparece ninguna actualización) y reinicia Home Assistant: después, la actualización del servidor se instala automáticamente. Para instalar la actualización del servidor de todos modos, pulsa Instalar en la entidad de actualización del servidor HA-MCP."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Componente instalado desde el repositorio antiguo",
|
||||
"description": "HACS está siguiendo el repositorio principal del servidor ha-mcp para este componente, así que aquí muestra los números de versión del servidor (7.x) y sus notas de la versión en lugar de los del propio componente (1.x). Las actualizaciones siguen funcionando, pero permanecen mal etiquetadas. Para arreglarlo: quita este repositorio de HACS (se conservan tus ajustes de integración y tus entradas de configuración), añade homeassistant-ai/ha-mcp-integration como repositorio personalizado, reinstala el componente desde ahí y reinicia Home Assistant."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Reinicia Home Assistant para aplicar el cambio de OAuth heredado",
|
||||
"description": "El modo de autenticación OAuth heredado registra sus propios extremos web /authorize y /token, que Home Assistant solo puede enlazar o liberar en un reinicio completo, tanto si acabas de activar este modo como si lo has desactivado o has cambiado su Client ID o Client Secret. Reinicia Home Assistant (Ajustes - Sistema - Reiniciar) para aplicar el cambio; hasta entonces se mantiene el comportamiento anterior."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "Estable (recomendado)",
|
||||
"dev": "Desarrollo (última compilación)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "URL de webhook secreta (predeterminada)",
|
||||
"ha_auth": "Iniciar sesión con Home Assistant (OAuth)",
|
||||
"legacy": "OAuth heredado (Client ID y Client Secret, para Google Gemini Spark)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "Búsqueda de herramientas (compacta, predeterminada)",
|
||||
"full": "Catálogo completo",
|
||||
"both": "Ambas (elegir por agente)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "Actualizar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Acceso directo desde la máquina de Home Assistant: {url}",
|
||||
"connect_local_lan": "Local/LAN (cuando el acceso desde la red es «Local network»): {url}",
|
||||
"connect_remote_url": "URL de conexión remota: {url}",
|
||||
"connect_urls_label": "URL de conexión:",
|
||||
"connect_urls_pending": "Las URL de conexión aparecen aquí (y en el registro de Home Assistant) una vez que el servidor se ha iniciado.",
|
||||
"connect_webhook_disabled": "El acceso remoto mediante webhook está desactivado (modo solo local).",
|
||||
"oauth_creds_pending": "El Client ID y el Client Secret aparecen aquí una vez que el servidor se ha iniciado.",
|
||||
"oauth_not_serving": "OAuth heredado aún no los está sirviendo — reinicia Home Assistant cuando te lo pida para activarlos.",
|
||||
"oauth_select_legacy_mode": "Pon el modo de autenticación de arriba en OAuth heredado y guarda para generar un Client ID y un Client Secret.",
|
||||
"panel_hint": "Abre el [panel de ajustes de HA-MCP](/ha-mcp) para la gestión de herramientas y los ajustes del servidor.",
|
||||
"tools_module_installed": "Módulo beta/avanzado de herramientas de archivos y YAML (opcional): Instalado",
|
||||
"tools_module_not_installed": "Módulo beta/avanzado de herramientas de archivos y YAML (opcional): No instalado — pulsa «Añadir entrada» en la página de esta integración y elige «HA-MCP File & YAML Tools»",
|
||||
"tools_module_not_loaded": "Módulo beta/avanzado de herramientas de archivos y YAML (opcional): Instalado pero no cargado — activa o vuelve a cargar la entrada «HA-MCP File & YAML Tools» en la página de esta integración",
|
||||
"version_line": "Componente {component_version} - Servidor ha-mcp {server_version} (canal {channel})",
|
||||
"version_not_installed": "aún no instalado",
|
||||
"version_unknown": "desconocido"
|
||||
"abort": {
|
||||
"already_configured": "Esta entrada ya está configurada.",
|
||||
"unsupported_home_assistant": "El servidor HA-MCP integrado en el proceso requiere Home Assistant {required} o posterior, pero esta instancia está ejecutando {installed}. Aún puedes usar la entrada HA-MCP File & YAML Tools de este componente con un servidor ha-mcp externo que se ejecute como aplicación (complemento) o contenedor de Docker."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "Herramientas de archivos y YAML de HA-MCP",
|
||||
"description": "Configura los servicios privilegiados de edición de archivos y YAML que usan las herramientas opcionales de archivos/YAML de ha-mcp. Estos ajustes aparecen también en la propia interfaz de ajustes del servidor ha-mcp y se aplican en caliente, sin reiniciar. Un mínimo de denegación no anulable sigue bloqueando rutas sensibles (p. ej. .storage) y claves de la frontera de confianza (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Directorios de archivos adicionales",
|
||||
"extra_yaml_keys": "Claves YAML de escritura adicionales"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Directorios relativos al directorio de configuración, cada uno con permiso de lectura y escritura (p. ej. pyscript). Se descartan los recorridos de ruta y las entradas fuera de la configuración.",
|
||||
"extra_yaml_keys": "Claves de primer nivel que ha_config_set_yaml puede escribir además de las integradas, para integraciones basadas en YAML de esta instalación (p. ej. alert2). Las claves de la frontera de confianza se descartan."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "Servidor HA-MCP",
|
||||
"description": "Configura el servidor HA-MCP. {panel_hint}Los cambios de aquí se aplican al guardar.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "Canal de versiones",
|
||||
"auto_update": "Actualizaciones automáticas del servidor",
|
||||
"server_port": "Puerto de escucha del servidor MCP",
|
||||
"bind_host": "Acceso desde la red",
|
||||
"webhook_auth": "Modo de autenticación",
|
||||
"oauth_client_id_override": "OAuth heredado: Client ID personalizado (opcional)",
|
||||
"oauth_client_secret_override": "OAuth heredado: Client Secret personalizado (opcional)",
|
||||
"oauth_regenerate": "OAuth heredado: regenerar ahora el Client ID y el Client Secret",
|
||||
"pip_spec": "Desarrollo: paquete ha-mcp alternativo",
|
||||
"server_url": "URL de Home Assistant (avanzado)",
|
||||
"external_url": "URL externa (opcional)",
|
||||
"webhook_id_override": "Secreto de webhook personalizado (opcional)",
|
||||
"secret_path_override": "Ruta de acceso directo personalizada (opcional)",
|
||||
"regenerate_secrets": "Regenerar ahora los secretos de conexión",
|
||||
"enable_webhook": "Acceso remoto mediante webhook",
|
||||
"enable_llm_api": "API de LLM para agentes de conversación",
|
||||
"llm_api_exposure": "Exposición de herramientas al agente de conversación",
|
||||
"enable_startup_notification": "Notificación de inicio",
|
||||
"enable_sidebar_panel": "Panel de ajustes en la barra lateral"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "Estable instala la última versión estable; Desarrollo instala la compilación de desarrollo más reciente. Con las actualizaciones automáticas activadas, una recarga o un reinicio, más una comprobación periódica, instalan la compilación más reciente del canal elegido. El paquete alternativo de desarrollo de abajo tiene prioridad y desactiva las actualizaciones automáticas.",
|
||||
"auto_update": "Cuando está activado, la versión más reciente del canal elegido se instala automáticamente: en una recarga o reinicio y mediante una comprobación periódica. Cuando está desactivado, el servidor se queda en la versión instalada hasta que lo vuelvas a activar. Esto controla únicamente el paquete del servidor ha-mcp; las actualizaciones del propio componente personalizado HA-MCP siguen llegando por HACS.",
|
||||
"server_port": "El puerto en el que escucha este servidor. La aplicación (complemento) de ha-mcp usa el 9583, por lo que el valor predeterminado es el 9584 para permitir que ambos se ejecuten simultáneamente; si no ejecutas la aplicación (complemento), cualquier puerto libre sirve.",
|
||||
"bind_host": "Quién puede conectarse directamente al puerto del servidor MCP. El valor predeterminado coincide con el de la aplicación (complemento): accesible en tu red local, con la ruta secreta como credencial. Elige bucle local (loopback) para permitir solo conexiones desde la propia máquina de Home Assistant; la URL del webhook y el panel de la barra lateral funcionan en ambos casos.",
|
||||
"webhook_auth": "Cómo los clientes de MCP se autentican en la URL del webhook. Con la URL secreta, el propio enlace es la única credencial — OAuth sigue funcionando para los clientes que lo usan, pero cada inicio de sesión se aprueba automáticamente: no se necesita iniciar sesión ni un ID de cliente o Secreto. Con el inicio de sesión de Home Assistant, clientes como claude.ai inician sesión con una cuenta de administrador de Home Assistant (OAuth). Con OAuth heredado, esta integración ejecuta su propio servicio de inicio de sesión y emite un ID de cliente y un Secreto para pegar en los clientes que lo requieran — puede ser necesario reiniciar Home Assistant para activarlo o desactivarlo por completo.",
|
||||
"oauth_client_id_override": "Sustituye el Client ID de OAuth heredado generado automáticamente. Déjalo vacío para conservar el actual. Solo se usa mientras el modo de autenticación esté en OAuth heredado.",
|
||||
"oauth_client_secret_override": "Sustituye el Client Secret de OAuth heredado generado automáticamente. Déjalo vacío para conservar el actual. Solo se usa mientras el modo de autenticación esté en OAuth heredado.",
|
||||
"oauth_regenerate": "Acción puntual: genera un Client ID y un Client Secret nuevos para el modo OAuth heredado. Solo surte efecto tras el reinicio de Home Assistant que pide el aviso de reparación; hasta que reinicies siguen funcionando el Client ID y el Client Secret anteriores, y no los nuevos. También vacía los dos campos de sustitución de arriba.",
|
||||
"pip_spec": "Déjalo vacío. Solo sirve para probar una compilación concreta de ha-mcp (por ejemplo, fijar una versión preliminar); tiene prioridad sobre el canal de versiones y desactiva las actualizaciones automáticas hasta que lo vacíes.",
|
||||
"server_url": "La URL que usa el servidor integrado para llegar a tu Home Assistant (normalmente esta misma instancia). Déjala vacía para deducirla del puerto y los ajustes SSL de esta instancia; indica un valor solo si el servidor necesita otra ruta.",
|
||||
"external_url": "Se muestra como URL de conexión principal: úsala cuando Home Assistant esté detrás de tu propio dominio o proxy inverso. Introduce la dirección base completa, incluido el esquema. Debe apuntar directamente a Home Assistant —abrirla en un navegador debería llevar a tu página de inicio de sesión de HA— y no debe contener un puerto como :8123 (ni ningún otro), o los clientes MCP remotos no podrán llegar. Déjala vacía para usar automáticamente Nabu Casa o la dirección local.",
|
||||
"webhook_id_override": "Sustituye el secreto aleatorio del webhook en la URL de conexión (/api/webhook/...). La URL es la credencial: usa un valor largo y difícil de adivinar. Déjalo vacío para conservar el actual.",
|
||||
"secret_path_override": "Sustituye la ruta aleatoria que se usa para el acceso directo en el puerto del servidor. Misma regla: la ruta es la credencial. Déjalo vacío para conservar la actual.",
|
||||
"regenerate_secrets": "Acción puntual: genera un secreto de webhook y una ruta de acceso directo aleatorios nuevos, invalidando de inmediato las URL de conexión antiguas. También vacía los dos campos de sustitución de arriba.",
|
||||
"enable_webhook": "Desactívalo para el modo solo local: el webhook de Home Assistant no se registra en absoluto, así que nada —Nabu Casa incluida— puede llegar al servidor a través de Home Assistant. El puerto directo del servidor y el panel de la barra lateral siguen funcionando.",
|
||||
"enable_llm_api": "Ofrece el conjunto completo de herramientas a los agentes de conversación de Home Assistant (OpenAI, Google, Ollama, ...): mientras esté activado, los agentes pueden seleccionar 'HA-MCP Server' en Controlar Home Assistant y manejar las herramientas desde el chat y la voz de Assist. Activarlo solo lo hace seleccionable: no se expone nada hasta que lo elijas en un agente. Guía de uso: {llm_api_docs_url}",
|
||||
"llm_api_exposure": "Forma del conjunto de herramientas que se ofrece a los agentes de conversación. La búsqueda de herramientas (predeterminada) mantiene pequeño el contexto del agente: una API compacta con herramientas fijadas más metaherramientas de búsqueda y ejecución. El catálogo completo lista directamente todas las herramientas expuestas, mejor para modelos con contexto grande. Ambos registra las dos opciones en paralelo para que cada agente elija la suya en Controlar Home Assistant. La exposición por herramienta se gestiona en el panel de ajustes de HA-MCP; detalles: {llm_api_docs_url}",
|
||||
"enable_startup_notification": "Muestra una notificación cada vez que arranca el servidor, con enlaces a las superficies de ajustes reservadas a administradores. Desactívalo para arrancar en silencio: las URL de conexión siguen apareciendo en el registro de Home Assistant.",
|
||||
"enable_sidebar_panel": "Muestra el panel de ajustes de HA-MCP en la barra lateral (solo para administradores). Desactívalo para quitar la entrada de la barra lateral: las opciones del servidor siguen disponibles en esta pantalla."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "El servidor HA-MCP integrado en el proceso no ha podido arrancar",
|
||||
"description": "El servidor HA-MCP integrado en el proceso no se ha podido iniciar dentro de Home Assistant:\n\n{detail}\n\nRevisa los registros de Home Assistant y vuelve a cargar la integración (o corrige el problema de fondo y recárgala) para reintentarlo."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "No se ha podido instalar el paquete del servidor HA-MCP integrado en el proceso",
|
||||
"description": "Ha fallado la instalación del paquete ha-mcp para el servidor integrado en el proceso:\n\n{detail}\n\nResuelve el problema de compatibilidad o de instalación descrito arriba y vuelve a cargar la integración para reintentarlo."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "Actualiza el componente personalizado HA-MCP mediante HACS",
|
||||
"description": "El servidor ha-mcp instalado requiere el componente personalizado HA-MCP {required} o posterior, pero tienes {installed}. Actualiza el componente mediante HACS (abre la entrada del componente personalizado HA-MCP y usa 'Actualizar información' si todavía no aparece ninguna actualización) y reinicia Home Assistant. Mientras tanto el servidor sigue funcionando, pero puede que algunas funciones nuevas no funcionen hasta que se actualice el componente."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "Actualización del servidor HA-MCP a la espera de una actualización del componente",
|
||||
"description": "Está disponible el servidor ha-mcp {latest}, pero esa versión actualizó también el componente personalizado HA-MCP (a {shipped}; tú ejecutas {running}). Para evitar arrancar una versión del servidor con la que el componente en uso nunca se ha probado, la actualización automática del servidor queda en espera hasta que se actualice el componente.\n\nActualiza el componente mediante HACS (abre la entrada del componente personalizado HA-MCP y usa 'Actualizar información' si todavía no aparece ninguna actualización) y reinicia Home Assistant: después, la actualización del servidor se instala automáticamente. Para instalar la actualización del servidor de todos modos, pulsa Instalar en la entidad de actualización del servidor HA-MCP."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Componente instalado desde el repositorio antiguo",
|
||||
"description": "HACS está rastreando el repositorio principal del servidor ha-mcp para este componente, por lo que HACS muestra aquí los números de versión y las notas de la versión del servidor en lugar de los propios del componente (2.x). Las actualizaciones siguen funcionando, pero permanecen mal etiquetadas de esta manera. Para solucionarlo: elimina este repositorio de HACS (se conservan tus ajustes de integración y entradas de configuración), añade homeassistant-ai/ha-mcp-integration como un repositorio personalizado, reinstala el componente desde él y reinicia Home Assistant."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Reinicia Home Assistant para aplicar los ajustes de OAuth heredado",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "¿Reiniciar Home Assistant?",
|
||||
"description": "Al enviar este formulario se reiniciará Home Assistant y se aplicarán los ajustes de OAuth heredado.\n\nHaz clic en <strong>Enviar</strong> para reiniciar ahora."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "Estable (recomendado)",
|
||||
"dev": "Desarrollo (última compilación)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "URL de webhook secreta (predeterminada)",
|
||||
"ha_auth": "Iniciar sesión con Home Assistant (OAuth)",
|
||||
"legacy": "OAuth heredado (esta integración emite un ID de cliente/Secreto para pegar)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "Búsqueda de herramientas (compacta, predeterminada)",
|
||||
"full": "Catálogo completo",
|
||||
"both": "Ambas (elegir por agente)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "Actualizar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Acceso directo desde la máquina de Home Assistant: {url}",
|
||||
"connect_local_lan": "Local/LAN (cuando el acceso desde la red es «Local network»): {url}",
|
||||
"connect_remote_url": "URL de conexión remota: {url}",
|
||||
"connect_urls_label": "URL de conexión:",
|
||||
"connect_urls_pending": "Las URL de conexión aparecen aquí (y en el registro de Home Assistant) una vez que el servidor se ha iniciado.",
|
||||
"connect_webhook_disabled": "El acceso remoto mediante webhook está desactivado (modo solo local).",
|
||||
"oauth_creds_pending": "El Client ID y el Client Secret aparecen aquí una vez que el servidor se ha iniciado.",
|
||||
"oauth_not_serving": "OAuth heredado aún no los está sirviendo — reinicia Home Assistant cuando te lo pida para activarlos.",
|
||||
"oauth_select_legacy_mode": "Pon el modo de autenticación de arriba en OAuth heredado y guarda para generar un Client ID y un Client Secret.",
|
||||
"panel_hint": "Abre el [panel de ajustes de HA-MCP](/ha-mcp) para la gestión de herramientas y los ajustes del servidor.",
|
||||
"tools_module_installed": "Módulo beta/avanzado de herramientas de archivos y YAML (opcional): Instalado",
|
||||
"tools_module_not_installed": "Módulo beta/avanzado de herramientas de archivos y YAML (opcional): No instalado — pulsa «Añadir entrada» en la página de esta integración y elige «HA-MCP File & YAML Tools»",
|
||||
"tools_module_not_loaded": "Módulo beta/avanzado de herramientas de archivos y YAML (opcional): Instalado pero no cargado — activa o vuelve a cargar la entrada «HA-MCP File & YAML Tools» en la página de esta integración",
|
||||
"version_line": "Componente {component_version} - Servidor ha-mcp {server_version} (canal {channel})",
|
||||
"version_not_installed": "aún no instalado",
|
||||
"version_unknown": "desconocido"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,161 +1,168 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Composant personnalisé HA-MCP",
|
||||
"description": "Choisissez ce que vous souhaitez ajouter. **Serveur HA-MCP** exécute le serveur ha-mcp complet à l'intérieur de Home Assistant et l'expose via un webhook Home Assistant – c'est l'installation que la plupart des utilisateurs veulent. C'est un serveur autonome et un remplacement complet de toutes les autres méthodes d'installation (add-on, Docker, uvx/PyPI, stdio) ; si vous l'utilisez, n'en exécutez pas une autre en parallèle. **Outils Fichier & YAML HA-MCP** ajoute les services privilégiés d'édition de fichiers et de YAML, nécessaires uniquement si vous activez les outils fichier/YAML optionnels de ha-mcp. Si votre serveur ha-mcp tourne déjà ailleurs (add-on, Docker, uvx), vous n'avez pas besoin de l'entrée serveur – ajoutez uniquement cette entrée Fichier & YAML, et seulement si vous utilisez ces outils. Elle fonctionne avec tous les types de serveur et peut être ajoutée à tout moment.",
|
||||
"menu_options": {
|
||||
"server": "Serveur HA-MCP (recommandé)",
|
||||
"tools": "Outils Fichier & YAML HA-MCP (optionnel)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "Outils Fichier & YAML HA-MCP",
|
||||
"description": "Configure les services privilégiés d'édition de fichiers et de YAML. Nécessaire uniquement si vous activez les outils d'édition fichier/YAML optionnels de ha-mcp (drapeaux de fonctionnalité, désactivés par défaut) – cela s'applique à tous les types de serveur, y compris le serveur HA-MCP intégré. Vous pouvez ajouter ou supprimer cette entrée à tout moment."
|
||||
},
|
||||
"server": {
|
||||
"title": "Serveur HA-MCP",
|
||||
"description": "Cela exécute le serveur ha-mcp complet à l'intérieur de Home Assistant et l'expose à distance via un webhook Home Assistant (accessible via Nabu Casa ou tout reverse proxy). C'est un serveur autonome et un remplacement complet des méthodes d'installation add-on, Docker, uvx/PyPI et stdio – ne l'exécutez pas en parallèle d'un autre serveur ha-mcp. Sélectionnez **Soumettre** pour le démarrer ; vous pourrez modifier le port, la liaison et l'authentification ensuite dans les options de l'intégration."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Cette entrée est déjà configurée.",
|
||||
"unsupported_home_assistant": "Le serveur HA-MCP intégré nécessite Home Assistant {required} ou plus récent, mais cette instance utilise {installed}. Vous pouvez toujours utiliser l'entrée HA-MCP File & YAML Tools de ce composant avec un serveur ha-mcp externe fonctionnant en add-on ou conteneur Docker."
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Composant personnalisé HA-MCP",
|
||||
"description": "Choisissez ce qu'il faut ajouter. **HA-MCP Server** exécute le serveur ha-mcp complet au sein de Home Assistant et l'expose via un webhook Home Assistant — c'est l'installation que la plupart des utilisateurs souhaitent. Il s'agit d'un serveur autonome et d'un remplacement complet pour toutes les autres méthodes d'installation : App (add-on), Docker, uvx/PyPI, stdio ; si vous l'exécutez, n'en exécutez pas un autre en même temps. **HA-MCP File & YAML Tools** a deux rôles. Si votre serveur ha-mcp s'exécute en dehors de Home Assistant — app, Docker, uvx/PyPI, stdio — cette entrée confère également à ce serveur les capacités de composants intégrés qu'il ne peut pas atteindre depuis l'extérieur (recherche rapide intégrée, métadonnées d'entrée de configuration, lectures basées sur le registre), il vaut donc la peine de l'ajouter à chaque installation externe, et elle sert de passerelle pour les services privilégiés d'édition de fichiers et de YAML utilisés par les outils de fichiers/YAML optionnels de ha-mcp. Si vous ajoutez l'entrée HA-MCP Server à la place, cette entrée fournit déjà ces capacités partagées, de sorte que cette seconde entrée n'est alors nécessaire que comme passerelle pour les outils de fichiers/YAML optionnels. Elle fonctionne avec tous les types de serveur et peut être ajoutée ultérieurement à tout moment.",
|
||||
"menu_options": {
|
||||
"server": "Serveur HA-MCP (recommandé)",
|
||||
"tools": "Outils Fichier & YAML HA-MCP (optionnel)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "Outils Fichier & YAML HA-MCP",
|
||||
"description": "Configure les services privilégiés de configuration de fichiers et de YAML, qui restent protégés derrière les outils d'édition de fichiers/YAML optionnels de ha-mcp (indicateurs de fonctionnalités, désactivés par défaut) — cela s'applique à tous les types de serveur, y compris le HA-MCP Server intégré. Si votre serveur ha-mcp s'exécute en dehors de Home Assistant, cette entrée lui confère également les capacités de composants intégrés qu'il ne peut pas atteindre depuis l'extérieur (recherche rapide intégrée, métadonnées d'entrée de configuration, lectures basées sur le registre), qui valent la peine d'être activées même si les outils de fichiers/YAML sont désactivés. Vous pouvez ajouter ou supprimer cette entrée à tout moment."
|
||||
},
|
||||
"server": {
|
||||
"title": "Serveur HA-MCP",
|
||||
"description": "Ceci exécute le serveur ha-mcp complet au sein de Home Assistant et l'expose à distance via un webhook Home Assistant (accessible via Nabu Casa ou tout autre proxy inverse). Il s'agit d'un serveur autonome et d'un remplacement complet pour les méthodes d'installation application (add-on), Docker, uvx/PyPI et stdio – ne l'exécutez pas en même temps qu'un autre serveur ha-mcp. Sélectionnez **Submit** pour le démarrer ; vous pourrez modifier le port, la liaison et l'authentification ultérieurement dans les options de l'intégration."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "Outils Fichier & YAML HA-MCP",
|
||||
"description": "Configurez les services privilégiés d'édition de fichiers et de YAML utilisés par les outils fichier/YAML optionnels de ha-mcp. Ces paramètres apparaissent également dans l'interface de configuration du serveur ha-mcp et s'appliquent en direct, sans redémarrage. Un plancher de refus non contournable bloque toujours les chemins sensibles (ex. .storage) et les clés de limite de confiance (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Répertoires de fichiers supplémentaires",
|
||||
"extra_yaml_keys": "Clés d'écriture YAML supplémentaires"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Répertoires relatifs au répertoire de configuration, chacun avec accès en lecture et écriture (ex. pyscript). Les traversées et les entrées hors du répertoire de configuration sont ignorées.",
|
||||
"extra_yaml_keys": "Clés de premier niveau que ha_config_set_yaml peut écrire en plus de celles intégrées, pour les intégrations YAML-first de cette installation (ex. alert2). Les clés de limite de confiance sont ignorées."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "Serveur HA-MCP",
|
||||
"description": "Configurez le serveur HA-MCP. {panel_hint}Les modifications ici sont appliquées à l'enregistrement.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "Canal de publication",
|
||||
"auto_update": "Mises à jour automatiques du serveur",
|
||||
"server_port": "Port d'écoute du serveur MCP",
|
||||
"bind_host": "Accès réseau",
|
||||
"webhook_auth": "Mode d'authentification",
|
||||
"oauth_client_id_override": "OAuth hérité : Client ID personnalisé (optionnel)",
|
||||
"oauth_client_secret_override": "OAuth hérité : Client Secret personnalisé (optionnel)",
|
||||
"oauth_regenerate": "OAuth hérité : régénérer Client ID/Secret maintenant",
|
||||
"pip_spec": "Développeur : surcharge du paquet ha-mcp",
|
||||
"server_url": "URL Home Assistant (avancé)",
|
||||
"external_url": "URL externe (optionnel)",
|
||||
"webhook_id_override": "Secret webhook personnalisé (optionnel)",
|
||||
"secret_path_override": "Chemin d'accès direct personnalisé (optionnel)",
|
||||
"regenerate_secrets": "Régénérer les secrets de connexion maintenant",
|
||||
"enable_webhook": "Accès distant via webhook",
|
||||
"enable_llm_api": "API LLM pour agents de conversation",
|
||||
"llm_api_exposure": "Exposition des outils pour agents de conversation",
|
||||
"enable_startup_notification": "Notification au démarrage",
|
||||
"enable_sidebar_panel": "Panneau de configuration dans la barre latérale"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "Stable installe la dernière version stable ; Développement installe le dernier build de développement. Lorsque les mises à jour automatiques sont activées, un rechargement ou redémarrage, ainsi qu'une vérification périodique, installent le dernier build du canal sélectionné. Une surcharge de paquet développeur ci-dessous a la priorité et désactive les mises à jour automatiques.",
|
||||
"auto_update": "Lorsqu'activé, la dernière version du canal sélectionné est installée automatiquement – lors d'un rechargement ou redémarrage, et via une vérification périodique. Lorsqu'éteint, le serveur reste sur la version actuellement installée jusqu'à ce que vous le réactiviez. Cela contrôle uniquement le paquet serveur ha-mcp ; les mises à jour du composant personnalisé HA-MCP lui-même passent toujours par HACS.",
|
||||
"server_port": "Le port sur lequel ce serveur écoute. L'add-on ha-mcp utilise 9583, donc la valeur par défaut ici est 9584 pour permettre aux deux de coexister – si vous n'utilisez pas l'add-on, n'importe quel port libre convient.",
|
||||
"bind_host": "Qui peut se connecter directement au port du serveur MCP. La valeur par défaut correspond à l'add-on : accessible sur votre réseau local, avec le chemin secret comme identifiant. Choisissez loopback pour n'autoriser que les connexions depuis la machine Home Assistant elle-même – l'URL webhook et le panneau latéral fonctionnent dans les deux cas.",
|
||||
"webhook_auth": "Comment les clients MCP s'authentifient à l'URL webhook. Avec l'URL secrète, le lien lui-même est l'identifiant. Avec la connexion Home Assistant, les clients comme claude.ai se connectent avec un compte administrateur Home Assistant (OAuth). Avec OAuth hérité, cette intégration émet son propre Client ID et Secret à coller dans les clients qui en ont besoin, comme Google Gemini Spark – un redémarrage de Home Assistant est nécessaire pour l'activer ou le désactiver.",
|
||||
"oauth_client_id_override": "Remplace le Client ID OAuth hérité généré automatiquement. Laissez vide pour conserver l'actuel. Utilisé uniquement lorsque le mode d'authentification est défini sur OAuth hérité.",
|
||||
"oauth_client_secret_override": "Remplace le Client Secret OAuth hérité généré automatiquement. Laissez vide pour conserver l'actuel. Utilisé uniquement lorsque le mode d'authentification est défini sur OAuth hérité.",
|
||||
"oauth_regenerate": "Action unique : génère un nouveau Client ID et Secret pour le mode OAuth hérité. Cela ne prend effet qu'après le redémarrage de Home Assistant demandé par l'invite de réparation – jusqu'au redémarrage, les anciens Client ID et Secret continuent de fonctionner et les nouveaux ne sont pas encore actifs. Efface également les deux champs de surcharge ci-dessus.",
|
||||
"pip_spec": "Laissez vide. Uniquement pour tester un build spécifique de ha-mcp (par exemple un pin de pré-version) ; surcharge le canal de publication et désactive les mises à jour automatiques jusqu'à ce qu'il soit vidé.",
|
||||
"server_url": "L'URL que le serveur intégré utilise pour atteindre votre Home Assistant (généralement cette instance elle-même). Laissez vide pour la dériver du port et des paramètres SSL de cette instance ; ne définissez une valeur que si le serveur a besoin d'une autre route.",
|
||||
"external_url": "Affichée comme URL de connexion principale – utilisez-la lorsque Home Assistant est derrière votre propre domaine ou un reverse proxy. Entrez l'adresse de base complète, y compris le schéma. Elle doit pointer directement vers Home Assistant – l'ouvrir dans un navigateur doit atteindre votre page de connexion HA – et ne doit pas contenir de port tel que :8123 (ou tout autre port), sinon les clients MCP distants ne pourront pas l'atteindre. Laissez vide pour utiliser automatiquement Nabu Casa / l'adresse locale.",
|
||||
"webhook_id_override": "Remplace le secret webhook aléatoire dans l'URL de connexion (/api/webhook/...). L'URL est l'identifiant – utilisez une valeur longue et difficile à deviner. Laissez vide pour conserver l'actuel.",
|
||||
"secret_path_override": "Remplace le chemin aléatoire utilisé pour l'accès direct sur le port du serveur. Même règle : le chemin est l'identifiant. Laissez vide pour conserver l'actuel.",
|
||||
"regenerate_secrets": "Action unique : génère un nouveau secret webhook aléatoire et un chemin d'accès direct, invalidant immédiatement les anciennes URL de connexion. Efface également les deux champs de surcharge ci-dessus.",
|
||||
"enable_webhook": "Désactivez pour le mode local uniquement : le webhook Home Assistant n'est pas enregistré du tout, donc rien – y compris Nabu Casa – ne peut atteindre le serveur via Home Assistant. Le port serveur direct et le panneau latéral continuent de fonctionner.",
|
||||
"enable_llm_api": "Propose l'ensemble complet d'outils aux agents de conversation Home Assistant (OpenAI, Google, Ollama, ...) : lorsqu'activé, les agents peuvent sélectionner « HA-MCP Server » sous Contrôler Home Assistant et utiliser les outils depuis le chat Assist et la voix. L'activation le rend seulement sélectionnable – rien n'est exposé tant que vous ne le choisissez pas sur un agent. Guide d'utilisation : {llm_api_docs_url}",
|
||||
"llm_api_exposure": "Forme de l'ensemble d'outils proposé aux agents de conversation. Recherche d'outils (par défaut) garde le contexte de l'agent compact : une API compacte avec des outils épinglés plus des méta-outils de recherche/exécution. Catalogue complet liste directement chaque outil exposé – mieux pour les modèles à grand contexte. Les deux enregistre les deux côte à côte afin que chaque agent choisisse le sien sous Contrôler Home Assistant. L'exposition par outil est gérée dans le panneau de configuration HA-MCP ; détails : {llm_api_docs_url}",
|
||||
"enable_startup_notification": "Affiche une notification à chaque démarrage du serveur, pointant vers les surfaces de configuration réservées aux administrateurs. Désactivez pour démarrer silencieusement – les URL de connexion apparaissent toujours dans le journal Home Assistant.",
|
||||
"enable_sidebar_panel": "Affiche le panneau de configuration HA-MCP dans la barre latérale (administrateurs uniquement). Désactivez pour supprimer l'entrée de la barre latérale – les options du serveur restent disponibles sur cet écran."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "Échec du démarrage du serveur HA-MCP intégré",
|
||||
"description": "Le serveur HA-MCP intégré n'a pas pu être démarré à l'intérieur de Home Assistant :\n\n{detail}\n\nVérifiez les journaux Home Assistant, puis rechargez l'intégration (ou corrigez le problème sous-jacent et rechargez) pour réessayer."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "Échec de l'installation du paquet serveur HA-MCP intégré",
|
||||
"description": "L'installation du paquet ha-mcp pour le serveur intégré a échoué :\n\n{detail}\n\nRésolvez le problème de compatibilité ou d'installation décrit ci-dessus, puis rechargez l'intégration pour réessayer."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "Mettez à jour le composant personnalisé HA-MCP via HACS",
|
||||
"description": "Le serveur ha-mcp installé nécessite le composant personnalisé HA-MCP {required} ou plus récent, mais vous avez {installed}. Mettez à jour le composant via HACS (ouvrez l'entrée Composant personnalisé HA-MCP et utilisez « Update information » si aucune mise à jour n'est encore affichée) et redémarrez Home Assistant. Le serveur continue de fonctionner entre-temps, mais certaines fonctionnalités plus récentes peuvent ne pas fonctionner tant que le composant n'est pas mis à jour."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "Mise à jour du serveur HA-MCP en attente d'une mise à jour du composant",
|
||||
"description": "Le serveur ha-mcp {latest} est disponible, mais cette version a également mis à jour le composant personnalisé HA-MCP (vers {shipped} ; vous utilisez {running}). Pour éviter de démarrer une version du serveur avec laquelle le composant en cours n'a jamais été testé, la mise à jour automatique du serveur est en attente jusqu'à la mise à jour du composant.\n\nMettez à jour le composant via HACS (ouvrez l'entrée Composant personnalisé HA-MCP et utilisez « Update information » si aucune mise à jour n'est encore affichée), puis redémarrez Home Assistant – la mise à jour du serveur s'installera automatiquement ensuite. Pour installer quand même la mise à jour du serveur, appuyez sur Installer sur l'entité de mise à jour du serveur HA-MCP."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Composant installé depuis le dépôt hérité",
|
||||
"description": "HACS suit le dépôt principal du serveur ha-mcp pour ce composant, donc HACS affiche ici les numéros de version du serveur (7.x) et les notes de version du serveur au lieu de celles du composant (1.x). Les mises à jour continuent de fonctionner, mais restent mal étiquetées de cette façon. Pour corriger : supprimez ce dépôt de HACS (vos paramètres d'intégration et entrées de configuration sont conservés), ajoutez homeassistant-ai/ha-mcp-integration comme dépôt personnalisé, réinstallez le composant depuis celui-ci, et redémarrez Home Assistant."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Redémarrez Home Assistant pour appliquer le changement OAuth hérité",
|
||||
"description": "Le mode d'authentification OAuth hérité enregistre ses propres points de terminaison web /authorize et /token, que Home Assistant ne peut lier ou libérer que lors d'un redémarrage complet – que vous veniez d'activer ce mode, de le désactiver, ou de modifier son Client ID/Secret. Redémarrez Home Assistant (Paramètres - Système - Redémarrer) pour appliquer le changement ; jusqu'alors le comportement précédent reste en vigueur."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "Stable (recommandé)",
|
||||
"dev": "Développement (dernier build)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "URL webhook secrète (par défaut)",
|
||||
"ha_auth": "Connexion avec Home Assistant (OAuth)",
|
||||
"legacy": "OAuth hérité (Client ID/Secret, pour Google Gemini Spark)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "Recherche d'outils (compact, par défaut)",
|
||||
"full": "Catalogue complet",
|
||||
"both": "Les deux (choix par agent)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "Mise à jour"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Accès direct depuis la machine Home Assistant : {url}",
|
||||
"connect_local_lan": "Local/LAN (lorsque l'accès réseau est « Local network ») : {url}",
|
||||
"connect_remote_url": "URL de connexion distante : {url}",
|
||||
"connect_urls_label": "URL de connexion :",
|
||||
"connect_urls_pending": "Les URL de connexion apparaissent ici (et dans le journal Home Assistant) une fois le serveur démarré.",
|
||||
"connect_webhook_disabled": "L'accès distant via webhook est désactivé (mode local uniquement).",
|
||||
"oauth_creds_pending": "Le Client ID et le Client Secret apparaissent ici une fois le serveur démarré.",
|
||||
"oauth_not_serving": "OAuth hérité ne les sert pas encore — redémarrez Home Assistant lorsqu'il vous le demande pour les activer.",
|
||||
"oauth_select_legacy_mode": "Réglez le mode d'authentification ci-dessus sur OAuth hérité et enregistrez pour générer un Client ID et un Client Secret.",
|
||||
"panel_hint": "Ouvrez le [panneau de réglages HA-MCP](/ha-mcp) pour la gestion des outils et les réglages du serveur.",
|
||||
"tools_module_installed": "Module bêta/avancé d'outils fichiers & YAML (facultatif) : Installé",
|
||||
"tools_module_not_installed": "Module bêta/avancé d'outils fichiers & YAML (facultatif) : Non installé — appuyez sur « Ajouter une entrée » sur la page de cette intégration et choisissez « HA-MCP File & YAML Tools »",
|
||||
"tools_module_not_loaded": "Module bêta/avancé d'outils fichiers & YAML (facultatif) : Installé mais non chargé — activez ou rechargez l'entrée « HA-MCP File & YAML Tools » sur la page de cette intégration",
|
||||
"version_line": "Composant {component_version} - Serveur ha-mcp {server_version} (canal {channel})",
|
||||
"version_not_installed": "pas encore installé",
|
||||
"version_unknown": "inconnu"
|
||||
"abort": {
|
||||
"already_configured": "Cette entrée est déjà configurée.",
|
||||
"unsupported_home_assistant": "Le serveur HA-MCP intégré nécessite Home Assistant {required} ou une version ultérieure, mais cette instance exécute {installed}. Vous pouvez toujours utiliser l'entrée HA-MCP File & YAML Tools de ce composant avec un serveur ha-mcp externe s'exécutant en tant qu'application (add-on) ou conteneur Docker."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "Outils Fichier & YAML HA-MCP",
|
||||
"description": "Configurez les services privilégiés d'édition de fichiers et de YAML utilisés par les outils fichier/YAML optionnels de ha-mcp. Ces paramètres apparaissent également dans l'interface de configuration du serveur ha-mcp et s'appliquent en direct, sans redémarrage. Un plancher de refus non contournable bloque toujours les chemins sensibles (ex. .storage) et les clés de limite de confiance (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Répertoires de fichiers supplémentaires",
|
||||
"extra_yaml_keys": "Clés d'écriture YAML supplémentaires"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Répertoires relatifs au répertoire de configuration, chacun avec accès en lecture et écriture (ex. pyscript). Les traversées et les entrées hors du répertoire de configuration sont ignorées.",
|
||||
"extra_yaml_keys": "Clés de premier niveau que ha_config_set_yaml peut écrire en plus de celles intégrées, pour les intégrations YAML-first de cette installation (ex. alert2). Les clés de limite de confiance sont ignorées."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "Serveur HA-MCP",
|
||||
"description": "Configurez le serveur HA-MCP. {panel_hint}Les modifications ici sont appliquées à l'enregistrement.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "Canal de publication",
|
||||
"auto_update": "Mises à jour automatiques du serveur",
|
||||
"server_port": "Port d'écoute du serveur MCP",
|
||||
"bind_host": "Accès réseau",
|
||||
"webhook_auth": "Mode d'authentification",
|
||||
"oauth_client_id_override": "OAuth hérité : Client ID personnalisé (optionnel)",
|
||||
"oauth_client_secret_override": "OAuth hérité : Client Secret personnalisé (optionnel)",
|
||||
"oauth_regenerate": "OAuth hérité : régénérer Client ID/Secret maintenant",
|
||||
"pip_spec": "Développeur : surcharge du paquet ha-mcp",
|
||||
"server_url": "URL Home Assistant (avancé)",
|
||||
"external_url": "URL externe (optionnel)",
|
||||
"webhook_id_override": "Secret webhook personnalisé (optionnel)",
|
||||
"secret_path_override": "Chemin d'accès direct personnalisé (optionnel)",
|
||||
"regenerate_secrets": "Régénérer les secrets de connexion maintenant",
|
||||
"enable_webhook": "Accès distant via webhook",
|
||||
"enable_llm_api": "API LLM pour agents de conversation",
|
||||
"llm_api_exposure": "Exposition des outils pour agents de conversation",
|
||||
"enable_startup_notification": "Notification au démarrage",
|
||||
"enable_sidebar_panel": "Panneau de configuration dans la barre latérale"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "Stable installe la dernière version stable ; Développement installe le dernier build de développement. Lorsque les mises à jour automatiques sont activées, un rechargement ou redémarrage, ainsi qu'une vérification périodique, installent le dernier build du canal sélectionné. Une surcharge de paquet développeur ci-dessous a la priorité et désactive les mises à jour automatiques.",
|
||||
"auto_update": "Lorsqu'activé, la dernière version du canal sélectionné est installée automatiquement – lors d'un rechargement ou redémarrage, et via une vérification périodique. Lorsqu'éteint, le serveur reste sur la version actuellement installée jusqu'à ce que vous le réactiviez. Cela contrôle uniquement le paquet serveur ha-mcp ; les mises à jour du composant personnalisé HA-MCP lui-même passent toujours par HACS.",
|
||||
"server_port": "Le port sur lequel ce serveur écoute. L'application (add-on) ha-mcp utilise le port 9583, celui-ci utilise donc 9584 par défaut pour permettre aux deux de fonctionner côte à côte – si vous n'exécutez pas l'application (add-on), n'importe quel port libre convient.",
|
||||
"bind_host": "Qui peut se connecter directement au port du serveur MCP. La valeur par défaut correspond à l'application (add-on) : accessible sur votre réseau local, avec le chemin secret comme identifiant. Choisissez loopback pour n'autoriser que les connexions provenant de la machine Home Assistant elle-même – l'URL du webhook et le panneau de la barre latérale fonctionnent dans les deux cas.",
|
||||
"webhook_auth": "Comment les clients MCP s'authentifient à l'URL du webhook. Avec l'URL secrète, le lien lui-même est la seule information d'identification – OAuth fonctionne toujours pour les clients qui l'utilisent, mais chaque connexion est approuvée automatiquement : pas de connexion et pas de Client ID ou Secret nécessaires. Avec la connexion Home Assistant, les clients tels que claude.ai se connectent avec un compte administrateur Home Assistant (OAuth). Avec OAuth hérité, cette intégration exécute son propre service de connexion et émet un Client ID et un Secret à coller dans les clients qui en nécessitent un – un redémarrage de Home Assistant peut être nécessaire pour l'activer ou le désactiver complètement.",
|
||||
"oauth_client_id_override": "Remplace le Client ID OAuth hérité généré automatiquement. Laissez vide pour conserver l'actuel. Utilisé uniquement lorsque le mode d'authentification est défini sur OAuth hérité.",
|
||||
"oauth_client_secret_override": "Remplace le Client Secret OAuth hérité généré automatiquement. Laissez vide pour conserver l'actuel. Utilisé uniquement lorsque le mode d'authentification est défini sur OAuth hérité.",
|
||||
"oauth_regenerate": "Action unique : génère un nouveau Client ID et Secret pour le mode OAuth hérité. Cela ne prend effet qu'après le redémarrage de Home Assistant demandé par l'invite de réparation – jusqu'au redémarrage, les anciens Client ID et Secret continuent de fonctionner et les nouveaux ne sont pas encore actifs. Efface également les deux champs de surcharge ci-dessus.",
|
||||
"pip_spec": "Laissez vide. Uniquement pour tester un build spécifique de ha-mcp (par exemple un pin de pré-version) ; surcharge le canal de publication et désactive les mises à jour automatiques jusqu'à ce qu'il soit vidé.",
|
||||
"server_url": "L'URL que le serveur intégré utilise pour atteindre votre Home Assistant (généralement cette instance elle-même). Laissez vide pour la dériver du port et des paramètres SSL de cette instance ; ne définissez une valeur que si le serveur a besoin d'une autre route.",
|
||||
"external_url": "Affichée comme URL de connexion principale – utilisez-la lorsque Home Assistant est derrière votre propre domaine ou un reverse proxy. Entrez l'adresse de base complète, y compris le schéma. Elle doit pointer directement vers Home Assistant – l'ouvrir dans un navigateur doit atteindre votre page de connexion HA – et ne doit pas contenir de port tel que :8123 (ou tout autre port), sinon les clients MCP distants ne pourront pas l'atteindre. Laissez vide pour utiliser automatiquement Nabu Casa / l'adresse locale.",
|
||||
"webhook_id_override": "Remplace le secret webhook aléatoire dans l'URL de connexion (/api/webhook/...). L'URL est l'identifiant – utilisez une valeur longue et difficile à deviner. Laissez vide pour conserver l'actuel.",
|
||||
"secret_path_override": "Remplace le chemin aléatoire utilisé pour l'accès direct sur le port du serveur. Même règle : le chemin est l'identifiant. Laissez vide pour conserver l'actuel.",
|
||||
"regenerate_secrets": "Action unique : génère un nouveau secret webhook aléatoire et un chemin d'accès direct, invalidant immédiatement les anciennes URL de connexion. Efface également les deux champs de surcharge ci-dessus.",
|
||||
"enable_webhook": "Désactivez pour le mode local uniquement : le webhook Home Assistant n'est pas enregistré du tout, donc rien – y compris Nabu Casa – ne peut atteindre le serveur via Home Assistant. Le port serveur direct et le panneau latéral continuent de fonctionner.",
|
||||
"enable_llm_api": "Propose l'ensemble complet d'outils aux agents de conversation Home Assistant (OpenAI, Google, Ollama, ...) : lorsqu'activé, les agents peuvent sélectionner « HA-MCP Server » sous Contrôler Home Assistant et utiliser les outils depuis le chat Assist et la voix. L'activation le rend seulement sélectionnable – rien n'est exposé tant que vous ne le choisissez pas sur un agent. Guide d'utilisation : {llm_api_docs_url}",
|
||||
"llm_api_exposure": "Forme de l'ensemble d'outils proposé aux agents de conversation. Recherche d'outils (par défaut) garde le contexte de l'agent compact : une API compacte avec des outils épinglés plus des méta-outils de recherche/exécution. Catalogue complet liste directement chaque outil exposé – mieux pour les modèles à grand contexte. Les deux enregistre les deux côte à côte afin que chaque agent choisisse le sien sous Contrôler Home Assistant. L'exposition par outil est gérée dans le panneau de configuration HA-MCP ; détails : {llm_api_docs_url}",
|
||||
"enable_startup_notification": "Affiche une notification à chaque démarrage du serveur, pointant vers les surfaces de configuration réservées aux administrateurs. Désactivez pour démarrer silencieusement – les URL de connexion apparaissent toujours dans le journal Home Assistant.",
|
||||
"enable_sidebar_panel": "Affiche le panneau de configuration HA-MCP dans la barre latérale (administrateurs uniquement). Désactivez pour supprimer l'entrée de la barre latérale – les options du serveur restent disponibles sur cet écran."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "Échec du démarrage du serveur HA-MCP intégré",
|
||||
"description": "Le serveur HA-MCP intégré n'a pas pu être démarré à l'intérieur de Home Assistant :\n\n{detail}\n\nVérifiez les journaux Home Assistant, puis rechargez l'intégration (ou corrigez le problème sous-jacent et rechargez) pour réessayer."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "Échec de l'installation du paquet serveur HA-MCP intégré",
|
||||
"description": "L'installation du paquet ha-mcp pour le serveur intégré a échoué :\n\n{detail}\n\nRésolvez le problème de compatibilité ou d'installation décrit ci-dessus, puis rechargez l'intégration pour réessayer."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "Mettez à jour le composant personnalisé HA-MCP via HACS",
|
||||
"description": "Le serveur ha-mcp installé nécessite le composant personnalisé HA-MCP {required} ou plus récent, mais vous avez {installed}. Mettez à jour le composant via HACS (ouvrez l'entrée Composant personnalisé HA-MCP et utilisez « Update information » si aucune mise à jour n'est encore affichée) et redémarrez Home Assistant. Le serveur continue de fonctionner entre-temps, mais certaines fonctionnalités plus récentes peuvent ne pas fonctionner tant que le composant n'est pas mis à jour."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "Mise à jour du serveur HA-MCP en attente d'une mise à jour du composant",
|
||||
"description": "Le serveur ha-mcp {latest} est disponible, mais cette version a également mis à jour le composant personnalisé HA-MCP (vers {shipped} ; vous utilisez {running}). Pour éviter de démarrer une version du serveur avec laquelle le composant en cours n'a jamais été testé, la mise à jour automatique du serveur est en attente jusqu'à la mise à jour du composant.\n\nMettez à jour le composant via HACS (ouvrez l'entrée Composant personnalisé HA-MCP et utilisez « Update information » si aucune mise à jour n'est encore affichée), puis redémarrez Home Assistant – la mise à jour du serveur s'installera automatiquement ensuite. Pour installer quand même la mise à jour du serveur, appuyez sur Installer sur l'entité de mise à jour du serveur HA-MCP."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Composant installé depuis le dépôt hérité",
|
||||
"description": "HACS suit le dépôt principal du serveur ha-mcp pour ce composant, donc HACS affiche ici les numéros de version et les notes de publication du serveur au lieu de ceux du composant lui-même (2.x). Les mises à jour continuent de fonctionner, mais restent mal étiquetées de cette manière. Pour corriger : supprimez ce dépôt de HACS (vos paramètres d'intégration et entrées de configuration sont conservés), ajoutez homeassistant-ai/ha-mcp-integration comme dépôt personnalisé, réinstallez le composant à partir de celui-ci, et redémarrez Home Assistant."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Redémarrez Home Assistant pour appliquer les paramètres OAuth hérités",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Redémarrer Home Assistant ?",
|
||||
"description": "La soumission de ce formulaire redémarrera Home Assistant et appliquera les paramètres OAuth hérités.\n\nCliquez sur **Soumettre** pour redémarrer maintenant."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "Stable (recommandé)",
|
||||
"dev": "Développement (dernier build)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "URL webhook secrète (par défaut)",
|
||||
"ha_auth": "Connexion avec Home Assistant (OAuth)",
|
||||
"legacy": "OAuth hérité (cette intégration émet un Client ID/Secret à coller)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "Recherche d'outils (compact, par défaut)",
|
||||
"full": "Catalogue complet",
|
||||
"both": "Les deux (choix par agent)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "Mise à jour"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Accès direct depuis la machine Home Assistant : {url}",
|
||||
"connect_local_lan": "Local/LAN (lorsque l'accès réseau est « Local network ») : {url}",
|
||||
"connect_remote_url": "URL de connexion distante : {url}",
|
||||
"connect_urls_label": "URL de connexion :",
|
||||
"connect_urls_pending": "Les URL de connexion apparaissent ici (et dans le journal Home Assistant) une fois le serveur démarré.",
|
||||
"connect_webhook_disabled": "L'accès distant via webhook est désactivé (mode local uniquement).",
|
||||
"oauth_creds_pending": "Le Client ID et le Client Secret apparaissent ici une fois le serveur démarré.",
|
||||
"oauth_not_serving": "OAuth hérité ne les sert pas encore — redémarrez Home Assistant lorsqu'il vous le demande pour les activer.",
|
||||
"oauth_select_legacy_mode": "Réglez le mode d'authentification ci-dessus sur OAuth hérité et enregistrez pour générer un Client ID et un Client Secret.",
|
||||
"panel_hint": "Ouvrez le [panneau de réglages HA-MCP](/ha-mcp) pour la gestion des outils et les réglages du serveur.",
|
||||
"tools_module_installed": "Module bêta/avancé d'outils fichiers & YAML (facultatif) : Installé",
|
||||
"tools_module_not_installed": "Module bêta/avancé d'outils fichiers & YAML (facultatif) : Non installé — appuyez sur « Ajouter une entrée » sur la page de cette intégration et choisissez « HA-MCP File & YAML Tools »",
|
||||
"tools_module_not_loaded": "Module bêta/avancé d'outils fichiers & YAML (facultatif) : Installé mais non chargé — activez ou rechargez l'entrée « HA-MCP File & YAML Tools » sur la page de cette intégration",
|
||||
"version_line": "Composant {component_version} - Serveur ha-mcp {server_version} (canal {channel})",
|
||||
"version_not_installed": "pas encore installé",
|
||||
"version_unknown": "inconnu"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,161 +1,168 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Componente personalizzato HA-MCP",
|
||||
"description": "Scegli cosa aggiungere. **Server HA-MCP** esegue il server ha-mcp completo dentro Home Assistant e lo espone tramite un webhook di Home Assistant: è l'installazione che va bene per la maggior parte delle persone. È un server autonomo e sostituisce completamente ogni altro metodo di installazione (componente aggiuntivo, Docker, uvx/PyPI, stdio); se usi questo, non usarne anche un altro. **Strumenti file e YAML di HA-MCP** aggiunge i servizi privilegiati di modifica di file e YAML, necessari solo se attivi gli strumenti file/YAML opzionali di ha-mcp. Se il tuo server ha-mcp gira già altrove (componente aggiuntivo, Docker, uvx), non ti serve la voce del server: aggiungi solo questa voce file e YAML, e solo se usi quegli strumenti. Funziona con qualsiasi tipo di server e si può aggiungere in seguito in qualsiasi momento.",
|
||||
"menu_options": {
|
||||
"server": "Server HA-MCP (consigliato)",
|
||||
"tools": "Strumenti file e YAML di HA-MCP (facoltativo)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "Strumenti file e YAML di HA-MCP",
|
||||
"description": "Configura i servizi privilegiati per i file e per la configurazione YAML. Servono solo se attivi gli strumenti opzionali di modifica file/YAML di ha-mcp (indicatori di funzione, disattivati per impostazione predefinita): vale per qualsiasi tipo di server, incluso il server HA-MCP eseguito nel processo. Puoi aggiungere o rimuovere questa voce in qualsiasi momento."
|
||||
},
|
||||
"server": {
|
||||
"title": "Server HA-MCP",
|
||||
"description": "Esegue il server ha-mcp completo dentro Home Assistant e lo espone da remoto tramite un webhook di Home Assistant (raggiungibile con Nabu Casa o con qualsiasi proxy inverso). È un server autonomo e sostituisce completamente i metodi di installazione tramite componente aggiuntivo, Docker, uvx/PyPI e stdio: non eseguirlo insieme a un altro server ha-mcp. Premi **Invia** per avviarlo; porta, associazione di rete e autenticazione si possono cambiare dopo, nelle opzioni dell'integrazione."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Questa voce è già configurata.",
|
||||
"unsupported_home_assistant": "Il server HA-MCP eseguito nel processo richiede Home Assistant {required} o successivo, ma questa istanza esegue {installed}. Puoi comunque usare la voce HA-MCP File & YAML Tools di questo componente con un server ha-mcp esterno eseguito come componente aggiuntivo o container Docker."
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Componente personalizzato HA-MCP",
|
||||
"description": "Scegli cosa aggiungere. **HA-MCP Server** esegue il server ha-mcp completo all'interno di Home Assistant e lo espone tramite un webhook di Home Assistant: questa è l'installazione che la maggior parte delle persone desidera. È un server autonomo e una sostituzione completa per qualsiasi altro metodo di installazione: App (add-on), Docker, uvx/PyPI, stdio; se lo esegui, non eseguirne anche un altro. **HA-MCP File & YAML Tools** ha due ruoli. Se il tuo server ha-mcp viene eseguito al di fuori di Home Assistant - app, Docker, uvx/PyPI, stdio - questa voce fornisce anche a quel server le funzionalità del componente in-process che non può raggiungere dall'esterno (ricerca rapida in-process, metadati delle voci di configurazione, letture basate sul registro), quindi vale la pena aggiungerla a ogni installazione esterna, e fa da cancello per i servizi privilegiati di modifica di file e YAML utilizzati dagli strumenti opt-in per file/YAML di ha-mcp. Se invece aggiungi la voce HA-MCP Server, quella voce fornisce già tali capacità condivise, quindi questa seconda voce è necessaria solo come cancello per gli strumenti opt-in per file/YAML. Funziona con qualsiasi tipo di server e può essere aggiunta in seguito in qualsiasi momento.",
|
||||
"menu_options": {
|
||||
"server": "Server HA-MCP (consigliato)",
|
||||
"tools": "Strumenti file e YAML di HA-MCP (facoltativo)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "Strumenti file e YAML di HA-MCP",
|
||||
"description": "Configura i servizi privilegiati di configurazione di file e YAML, che rimangono protetti dietro gli strumenti opt-in di modifica file/YAML di ha-mcp (flag di funzionalità, disabilitati per impostazione predefinita) - questo vale per qualsiasi tipo di server, incluso l'**HA-MCP Server** in-process. Se il tuo server ha-mcp viene eseguito al di fuori di Home Assistant, questa voce gli fornisce anche le capacità del componente in-process che non può raggiungere dall'esterno (ricerca rapida in-process, metadati delle voci di configurazione, letture basate sul registro), che vale la pena avere anche con gli strumenti file/YAML disattivati. Puoi aggiungere o rimuovere questa voce in qualsiasi momento."
|
||||
},
|
||||
"server": {
|
||||
"title": "Server HA-MCP",
|
||||
"description": "Questo esegue l'intero server ha-mcp all'interno di Home Assistant e lo espone da remoto tramite un webhook di Home Assistant (raggiungibile tramite Nabu Casa o qualsiasi reverse proxy). È un server autonomo e sostituisce completamente i metodi di installazione app (add-on), Docker, uvx/PyPI e stdio: non eseguirlo insieme a un altro server ha-mcp. Seleziona **Invia** per avviarlo; puoi modificare la porta, il binding e l'autenticazione in un secondo momento nelle opzioni dell'integrazione."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "Strumenti file e YAML di HA-MCP",
|
||||
"description": "Configura i servizi privilegiati di modifica di file e YAML usati dagli strumenti file/YAML opzionali di ha-mcp. Queste impostazioni compaiono anche nell'interfaccia delle impostazioni del server ha-mcp e si applicano subito, senza riavvio. Una soglia di divieto non aggirabile continua a bloccare i percorsi sensibili (per esempio .storage) e le chiavi del confine di fiducia (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Directory di file aggiuntive",
|
||||
"extra_yaml_keys": "Chiavi YAML scrivibili aggiuntive"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Directory relative alla directory di configurazione, ciascuna con permesso di lettura e scrittura (per esempio pyscript). I percorsi che risalgono l'albero e le voci fuori dalla configurazione vengono scartati.",
|
||||
"extra_yaml_keys": "Chiavi di primo livello che ha_config_set_yaml può scrivere oltre a quelle integrate, per le integrazioni basate su YAML di questa installazione (per esempio alert2). Le chiavi del confine di fiducia vengono scartate."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "Server HA-MCP",
|
||||
"description": "Configura il server HA-MCP. {panel_hint}Le modifiche qui vengono applicate al salvataggio.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "Canale di rilascio",
|
||||
"auto_update": "Aggiornamenti automatici del server",
|
||||
"server_port": "Porta di ascolto del server MCP",
|
||||
"bind_host": "Accesso dalla rete",
|
||||
"webhook_auth": "Modalità di autenticazione",
|
||||
"oauth_client_id_override": "OAuth legacy: Client ID personalizzato (facoltativo)",
|
||||
"oauth_client_secret_override": "OAuth legacy: Client Secret personalizzato (facoltativo)",
|
||||
"oauth_regenerate": "OAuth legacy: rigenera subito Client ID e Client Secret",
|
||||
"pip_spec": "Sviluppo: pacchetto ha-mcp alternativo",
|
||||
"server_url": "URL di Home Assistant (avanzato)",
|
||||
"external_url": "URL esterno (facoltativo)",
|
||||
"webhook_id_override": "Segreto del webhook personalizzato (facoltativo)",
|
||||
"secret_path_override": "Percorso di accesso diretto personalizzato (facoltativo)",
|
||||
"regenerate_secrets": "Rigenera subito i segreti di connessione",
|
||||
"enable_webhook": "Accesso remoto tramite webhook",
|
||||
"enable_llm_api": "API LLM per gli agenti conversazionali",
|
||||
"llm_api_exposure": "Esposizione degli strumenti agli agenti conversazionali",
|
||||
"enable_startup_notification": "Notifica all'avvio",
|
||||
"enable_sidebar_panel": "Pannello impostazioni nella barra laterale"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "Stabile installa l'ultima versione stabile; Sviluppo installa la build di sviluppo più recente. Con gli aggiornamenti automatici attivi, una ricarica o un riavvio, più un controllo periodico, installano la build più recente del canale scelto. Il pacchetto alternativo di sviluppo qui sotto ha la precedenza e disattiva gli aggiornamenti automatici.",
|
||||
"auto_update": "Quando è attivo, la versione più recente del canale scelto viene installata automaticamente: a una ricarica o a un riavvio e tramite un controllo periodico. Quando è disattivo, il server resta alla versione installata finché non lo riattivi. Questo riguarda solo il pacchetto del server ha-mcp; gli aggiornamenti del componente personalizzato HA-MCP continuano ad arrivare tramite HACS.",
|
||||
"server_port": "La porta su cui questo server ascolta. Il componente aggiuntivo ha-mcp usa la 9583, quindi qui il valore predefinito è 9584 per farli convivere; se non usi il componente aggiuntivo, va bene qualsiasi porta libera.",
|
||||
"bind_host": "Chi può collegarsi direttamente alla porta del server MCP. Il valore predefinito coincide con quello del componente aggiuntivo: raggiungibile sulla rete locale, con il percorso segreto come credenziale. Scegli loopback per consentire solo le connessioni dalla macchina di Home Assistant stessa: l'URL del webhook e il pannello nella barra laterale funzionano in entrambi i casi.",
|
||||
"webhook_auth": "Come i client MCP si identificano all'URL del webhook. Con l'URL segreto, il collegamento stesso è la credenziale. Con l'accesso tramite Home Assistant, client come claude.ai entrano con un account amministratore di Home Assistant (OAuth). Con OAuth legacy questa integrazione emette un proprio Client ID e Client Secret da incollare nei client che li richiedono, come Google Gemini Spark: attivarlo o disattivarlo richiede il riavvio di Home Assistant.",
|
||||
"oauth_client_id_override": "Sostituisce il Client ID OAuth legacy generato automaticamente. Lascialo vuoto per mantenere quello attuale. Viene usato solo mentre la modalità di autenticazione è impostata su OAuth legacy.",
|
||||
"oauth_client_secret_override": "Sostituisce il Client Secret OAuth legacy generato automaticamente. Lascialo vuoto per mantenere quello attuale. Viene usato solo mentre la modalità di autenticazione è impostata su OAuth legacy.",
|
||||
"oauth_regenerate": "Azione una tantum: genera un Client ID e un Client Secret nuovi per la modalità OAuth legacy. Ha effetto solo dopo il riavvio di Home Assistant che la richiesta di riparazione chiede; fino ad allora continuano a funzionare i precedenti e non i nuovi. Svuota anche i due campi di sostituzione qui sopra.",
|
||||
"pip_spec": "Lascialo vuoto. Serve solo per provare una build specifica di ha-mcp (per esempio una versione preliminare fissata); ha la precedenza sul canale di rilascio e disattiva gli aggiornamenti automatici finché non lo svuoti.",
|
||||
"server_url": "L'URL che il server nel processo usa per raggiungere il tuo Home Assistant (di solito questa stessa istanza). Lascialo vuoto per ricavarlo dalla porta e dalle impostazioni SSL di questa istanza; indica un valore solo se al server serve un percorso diverso.",
|
||||
"external_url": "Viene mostrato come URL di connessione principale: usalo quando Home Assistant sta dietro un tuo dominio o un proxy inverso. Inserisci l'indirizzo di base completo, schema incluso. Deve puntare direttamente a Home Assistant — aprendolo in un browser dovresti arrivare alla pagina di accesso di HA — e non deve contenere una porta come :8123 (né altre), altrimenti i client MCP remoti non riusciranno a raggiungerlo. Lascialo vuoto per usare automaticamente Nabu Casa o l'indirizzo locale.",
|
||||
"webhook_id_override": "Sostituisce il segreto casuale del webhook nell'URL di connessione (/api/webhook/...). L'URL è la credenziale: usa un valore lungo e difficile da indovinare. Lascialo vuoto per mantenere quello attuale.",
|
||||
"secret_path_override": "Sostituisce il percorso casuale usato per l'accesso diretto sulla porta del server. Stessa regola: il percorso è la credenziale. Lascialo vuoto per mantenere quello attuale.",
|
||||
"regenerate_secrets": "Azione una tantum: genera un nuovo segreto casuale per il webhook e un nuovo percorso di accesso diretto, invalidando subito i vecchi URL di connessione. Svuota anche i due campi di sostituzione qui sopra.",
|
||||
"enable_webhook": "Disattivalo per la modalità solo locale: il webhook di Home Assistant non viene registrato affatto, quindi nulla — Nabu Casa inclusa — può raggiungere il server attraverso Home Assistant. La porta diretta del server e il pannello nella barra laterale continuano a funzionare.",
|
||||
"enable_llm_api": "Offre l'intero set di strumenti agli agenti conversazionali di Home Assistant (OpenAI, Google, Ollama, ...): mentre è attivo, gli agenti possono selezionare «HA-MCP Server» sotto Controlla Home Assistant e usare gli strumenti dalla chat e dalla voce di Assist. Attivarlo lo rende solo selezionabile: non viene esposto nulla finché non lo scegli su un agente. Guida all'uso: {llm_api_docs_url}",
|
||||
"llm_api_exposure": "Forma del set di strumenti offerto agli agenti conversazionali. La ricerca degli strumenti (predefinita) mantiene piccolo il contesto dell'agente: un'API compatta con strumenti fissati più meta-strumenti di ricerca ed esecuzione. Il catalogo completo elenca direttamente ogni strumento esposto, meglio per modelli con contesto ampio. Entrambi registra le due opzioni affiancate, così ogni agente sceglie la propria sotto Controlla Home Assistant. L'esposizione per singolo strumento si gestisce nel pannello impostazioni di HA-MCP; dettagli: {llm_api_docs_url}",
|
||||
"enable_startup_notification": "Mostra una notifica a ogni avvio del server, che rimanda alle superfici di impostazioni riservate agli amministratori. Disattivalo per un avvio silenzioso: gli URL di connessione restano comunque nel registro di Home Assistant.",
|
||||
"enable_sidebar_panel": "Mostra il pannello impostazioni di HA-MCP nella barra laterale (solo per gli amministratori). Disattivalo per togliere la voce dalla barra laterale: le opzioni del server restano disponibili in questa schermata."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "Il server HA-MCP nel processo non è riuscito ad avviarsi",
|
||||
"description": "Non è stato possibile avviare il server HA-MCP nel processo dentro Home Assistant:\n\n{detail}\n\nControlla i registri di Home Assistant, poi ricarica l'integrazione (o risolvi il problema di fondo e ricaricala) per riprovare."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "Non è stato possibile installare il pacchetto del server HA-MCP nel processo",
|
||||
"description": "L'installazione del pacchetto ha-mcp per il server nel processo non è riuscita:\n\n{detail}\n\nRisolvi il problema di compatibilità o di installazione descritto sopra, poi ricarica l'integrazione per riprovare."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "Aggiorna il componente personalizzato HA-MCP tramite HACS",
|
||||
"description": "Il server ha-mcp installato richiede il componente personalizzato HA-MCP {required} o successivo, ma hai {installed}. Aggiorna il componente tramite HACS (apri la voce del componente personalizzato HA-MCP e usa «Aggiorna informazioni» se non compare ancora alcun aggiornamento) e riavvia Home Assistant. Nel frattempo il server continua a funzionare, ma alcune funzioni più recenti potrebbero non funzionare finché il componente non viene aggiornato."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "Aggiornamento del server HA-MCP in attesa di un aggiornamento del componente",
|
||||
"description": "È disponibile il server ha-mcp {latest}, ma quella versione ha aggiornato anche il componente personalizzato HA-MCP (alla {shipped}; tu esegui la {running}). Per evitare di avviare una versione del server con cui il componente in uso non è mai stato provato, l'aggiornamento automatico del server resta sospeso finché il componente non viene aggiornato.\n\nAggiorna il componente tramite HACS (apri la voce del componente personalizzato HA-MCP e usa «Aggiorna informazioni» se non compare ancora alcun aggiornamento), poi riavvia Home Assistant: l'aggiornamento del server si installa automaticamente subito dopo. Per installare comunque l'aggiornamento del server, premi Installa sull'entità di aggiornamento del server HA-MCP."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Componente installato dal repository obsoleto",
|
||||
"description": "Per questo componente HACS sta seguendo il repository principale del server ha-mcp, quindi qui mostra i numeri di versione del server (7.x) e le sue note di rilascio invece di quelli del componente (1.x). Gli aggiornamenti continuano a funzionare, ma restano etichettati in modo sbagliato. Per rimediare: rimuovi questo repository da HACS (le impostazioni dell'integrazione e le voci di configurazione vengono mantenute), aggiungi homeassistant-ai/ha-mcp-integration come repository personalizzato, reinstalla il componente da lì e riavvia Home Assistant."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Riavvia Home Assistant per applicare la modifica a OAuth legacy",
|
||||
"description": "La modalità di autenticazione OAuth legacy registra propri endpoint web /authorize e /token, che Home Assistant può associare o rilasciare solo con un riavvio completo, sia che tu abbia appena attivato questa modalità, sia che l'abbia disattivata o ne abbia cambiato Client ID o Client Secret. Riavvia Home Assistant (Impostazioni - Sistema - Riavvia) per applicare la modifica; fino ad allora resta valido il comportamento precedente."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "Stabile (consigliato)",
|
||||
"dev": "Sviluppo (ultima build)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "URL webhook segreto (predefinito)",
|
||||
"ha_auth": "Accedi con Home Assistant (OAuth)",
|
||||
"legacy": "OAuth legacy (Client ID e Client Secret, per Google Gemini Spark)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "Ricerca degli strumenti (compatta, predefinita)",
|
||||
"full": "Catalogo completo",
|
||||
"both": "Entrambi (scelta per agente)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "Aggiornamento"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Accesso diretto dalla macchina Home Assistant: {url}",
|
||||
"connect_local_lan": "Locale/LAN (quando l'accesso dalla rete è «Local network»): {url}",
|
||||
"connect_remote_url": "URL di connessione remota: {url}",
|
||||
"connect_urls_label": "URL di connessione:",
|
||||
"connect_urls_pending": "Gli URL di connessione compaiono qui (e nel registro di Home Assistant) una volta avviato il server.",
|
||||
"connect_webhook_disabled": "L'accesso remoto tramite webhook è disattivato (modalità solo locale).",
|
||||
"oauth_creds_pending": "Il Client ID e il Client Secret compaiono qui una volta avviato il server.",
|
||||
"oauth_not_serving": "OAuth legacy non li sta ancora servendo — riavvia Home Assistant quando te lo chiede per attivarli.",
|
||||
"oauth_select_legacy_mode": "Imposta la modalità di autenticazione qui sopra su OAuth legacy e salva per generare un Client ID e un Client Secret.",
|
||||
"panel_hint": "Apri il [pannello impostazioni HA-MCP](/ha-mcp) per la gestione degli strumenti e le impostazioni del server.",
|
||||
"tools_module_installed": "Modulo beta/avanzato di strumenti per file e YAML (facoltativo): Installato",
|
||||
"tools_module_not_installed": "Modulo beta/avanzato di strumenti per file e YAML (facoltativo): Non installato — premi «Aggiungi voce» nella pagina di questa integrazione e scegli «HA-MCP File & YAML Tools»",
|
||||
"tools_module_not_loaded": "Modulo beta/avanzato di strumenti per file e YAML (facoltativo): Installato ma non caricato — attiva o ricarica la voce «HA-MCP File & YAML Tools» nella pagina di questa integrazione",
|
||||
"version_line": "Componente {component_version} - Server ha-mcp {server_version} (canale {channel})",
|
||||
"version_not_installed": "non ancora installato",
|
||||
"version_unknown": "sconosciuto"
|
||||
"abort": {
|
||||
"already_configured": "Questa voce è già configurata.",
|
||||
"unsupported_home_assistant": "L'in-process HA-MCP Server richiede Home Assistant {required} o superiore, ma questa istanza sta eseguendo {installed}. Puoi comunque utilizzare la voce HA-MCP File & YAML Tools di questo componente con un server ha-mcp esterno in esecuzione come app (add-on) o container Docker."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "Strumenti file e YAML di HA-MCP",
|
||||
"description": "Configura i servizi privilegiati di modifica di file e YAML usati dagli strumenti file/YAML opzionali di ha-mcp. Queste impostazioni compaiono anche nell'interfaccia delle impostazioni del server ha-mcp e si applicano subito, senza riavvio. Una soglia di divieto non aggirabile continua a bloccare i percorsi sensibili (per esempio .storage) e le chiavi del confine di fiducia (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Directory di file aggiuntive",
|
||||
"extra_yaml_keys": "Chiavi YAML scrivibili aggiuntive"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Directory relative alla directory di configurazione, ciascuna con permesso di lettura e scrittura (per esempio pyscript). I percorsi che risalgono l'albero e le voci fuori dalla configurazione vengono scartati.",
|
||||
"extra_yaml_keys": "Chiavi di primo livello che ha_config_set_yaml può scrivere oltre a quelle integrate, per le integrazioni basate su YAML di questa installazione (per esempio alert2). Le chiavi del confine di fiducia vengono scartate."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "Server HA-MCP",
|
||||
"description": "Configura il server HA-MCP. {panel_hint}Le modifiche qui vengono applicate al salvataggio.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "Canale di rilascio",
|
||||
"auto_update": "Aggiornamenti automatici del server",
|
||||
"server_port": "Porta di ascolto del server MCP",
|
||||
"bind_host": "Accesso dalla rete",
|
||||
"webhook_auth": "Modalità di autenticazione",
|
||||
"oauth_client_id_override": "OAuth legacy: Client ID personalizzato (facoltativo)",
|
||||
"oauth_client_secret_override": "OAuth legacy: Client Secret personalizzato (facoltativo)",
|
||||
"oauth_regenerate": "OAuth legacy: rigenera subito Client ID e Client Secret",
|
||||
"pip_spec": "Sviluppo: pacchetto ha-mcp alternativo",
|
||||
"server_url": "URL di Home Assistant (avanzato)",
|
||||
"external_url": "URL esterno (facoltativo)",
|
||||
"webhook_id_override": "Segreto del webhook personalizzato (facoltativo)",
|
||||
"secret_path_override": "Percorso di accesso diretto personalizzato (facoltativo)",
|
||||
"regenerate_secrets": "Rigenera subito i segreti di connessione",
|
||||
"enable_webhook": "Accesso remoto tramite webhook",
|
||||
"enable_llm_api": "API LLM per gli agenti conversazionali",
|
||||
"llm_api_exposure": "Esposizione degli strumenti agli agenti conversazionali",
|
||||
"enable_startup_notification": "Notifica all'avvio",
|
||||
"enable_sidebar_panel": "Pannello impostazioni nella barra laterale"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "Stabile installa l'ultima versione stabile; Sviluppo installa la build di sviluppo più recente. Con gli aggiornamenti automatici attivi, una ricarica o un riavvio, più un controllo periodico, installano la build più recente del canale scelto. Il pacchetto alternativo di sviluppo qui sotto ha la precedenza e disattiva gli aggiornamenti automatici.",
|
||||
"auto_update": "Quando è attivo, la versione più recente del canale scelto viene installata automaticamente: a una ricarica o a un riavvio e tramite un controllo periodico. Quando è disattivo, il server resta alla versione installata finché non lo riattivi. Questo riguarda solo il pacchetto del server ha-mcp; gli aggiornamenti del componente personalizzato HA-MCP continuano ad arrivare tramite HACS.",
|
||||
"server_port": "La porta su cui questo server è in ascolto. L'app (add-on) ha-mcp utilizza la porta 9583, quindi questa è impostata per predefinita su 9584 per consentire a entrambi di funzionare fianco a fianco; se non esegui l'app (add-on), va bene qualsiasi porta libera.",
|
||||
"bind_host": "Chi può connettersi direttamente alla porta del server MCP. Il valore predefinito corrisponde all'app (add-on): raggiungibile sulla tua rete locale, con il percorso segreto come credenziale. Scegli loopback per consentire solo le connessioni dalla macchina Home Assistant stessa: l'URL del webhook e il pannello della barra laterale funzionano in entrambi i casi.",
|
||||
"webhook_auth": "Come i client MCP si autenticano all'URL del webhook. Con l'URL segreto, il link stesso è l'unica credenziale - OAuth è ancora attivo per i client che lo usano, ma ogni accesso è approvato automaticamente: non sono necessari login, ID client o Secret. Con l'accesso di Home Assistant, i client come claude.ai accedono con un account amministratore di Home Assistant (OAuth). Con OAuth legacy, questa integrazione esegue il proprio servizio di accesso e rilascia un ID client e un Secret da incollare nei client che ne richiedono uno - potrebbe essere necessario un riavvio di Home Assistant per attivarlo o disattivarlo completamente.",
|
||||
"oauth_client_id_override": "Sostituisce il Client ID OAuth legacy generato automaticamente. Lascialo vuoto per mantenere quello attuale. Viene usato solo mentre la modalità di autenticazione è impostata su OAuth legacy.",
|
||||
"oauth_client_secret_override": "Sostituisce il Client Secret OAuth legacy generato automaticamente. Lascialo vuoto per mantenere quello attuale. Viene usato solo mentre la modalità di autenticazione è impostata su OAuth legacy.",
|
||||
"oauth_regenerate": "Azione una tantum: genera un Client ID e un Client Secret nuovi per la modalità OAuth legacy. Ha effetto solo dopo il riavvio di Home Assistant che la richiesta di riparazione chiede; fino ad allora continuano a funzionare i precedenti e non i nuovi. Svuota anche i due campi di sostituzione qui sopra.",
|
||||
"pip_spec": "Lascialo vuoto. Serve solo per provare una build specifica di ha-mcp (per esempio una versione preliminare fissata); ha la precedenza sul canale di rilascio e disattiva gli aggiornamenti automatici finché non lo svuoti.",
|
||||
"server_url": "L'URL che il server nel processo usa per raggiungere il tuo Home Assistant (di solito questa stessa istanza). Lascialo vuoto per ricavarlo dalla porta e dalle impostazioni SSL di questa istanza; indica un valore solo se al server serve un percorso diverso.",
|
||||
"external_url": "Viene mostrato come URL di connessione principale: usalo quando Home Assistant sta dietro un tuo dominio o un proxy inverso. Inserisci l'indirizzo di base completo, schema incluso. Deve puntare direttamente a Home Assistant — aprendolo in un browser dovresti arrivare alla pagina di accesso di HA — e non deve contenere una porta come :8123 (né altre), altrimenti i client MCP remoti non riusciranno a raggiungerlo. Lascialo vuoto per usare automaticamente Nabu Casa o l'indirizzo locale.",
|
||||
"webhook_id_override": "Sostituisce il segreto casuale del webhook nell'URL di connessione (/api/webhook/...). L'URL è la credenziale: usa un valore lungo e difficile da indovinare. Lascialo vuoto per mantenere quello attuale.",
|
||||
"secret_path_override": "Sostituisce il percorso casuale usato per l'accesso diretto sulla porta del server. Stessa regola: il percorso è la credenziale. Lascialo vuoto per mantenere quello attuale.",
|
||||
"regenerate_secrets": "Azione una tantum: genera un nuovo segreto casuale per il webhook e un nuovo percorso di accesso diretto, invalidando subito i vecchi URL di connessione. Svuota anche i due campi di sostituzione qui sopra.",
|
||||
"enable_webhook": "Disattivalo per la modalità solo locale: il webhook di Home Assistant non viene registrato affatto, quindi nulla — Nabu Casa inclusa — può raggiungere il server attraverso Home Assistant. La porta diretta del server e il pannello nella barra laterale continuano a funzionare.",
|
||||
"enable_llm_api": "Offre l'intero set di strumenti agli agenti conversazionali di Home Assistant (OpenAI, Google, Ollama, ...): mentre è attivo, gli agenti possono selezionare «HA-MCP Server» sotto Controlla Home Assistant e usare gli strumenti dalla chat e dalla voce di Assist. Attivarlo lo rende solo selezionabile: non viene esposto nulla finché non lo scegli su un agente. Guida all'uso: {llm_api_docs_url}",
|
||||
"llm_api_exposure": "Forma del set di strumenti offerto agli agenti conversazionali. La ricerca degli strumenti (predefinita) mantiene piccolo il contesto dell'agente: un'API compatta con strumenti fissati più meta-strumenti di ricerca ed esecuzione. Il catalogo completo elenca direttamente ogni strumento esposto, meglio per modelli con contesto ampio. Entrambi registra le due opzioni affiancate, così ogni agente sceglie la propria sotto Controlla Home Assistant. L'esposizione per singolo strumento si gestisce nel pannello impostazioni di HA-MCP; dettagli: {llm_api_docs_url}",
|
||||
"enable_startup_notification": "Mostra una notifica a ogni avvio del server, che rimanda alle superfici di impostazioni riservate agli amministratori. Disattivalo per un avvio silenzioso: gli URL di connessione restano comunque nel registro di Home Assistant.",
|
||||
"enable_sidebar_panel": "Mostra il pannello impostazioni di HA-MCP nella barra laterale (solo per gli amministratori). Disattivalo per togliere la voce dalla barra laterale: le opzioni del server restano disponibili in questa schermata."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "Il server HA-MCP nel processo non è riuscito ad avviarsi",
|
||||
"description": "Non è stato possibile avviare il server HA-MCP nel processo dentro Home Assistant:\n\n{detail}\n\nControlla i registri di Home Assistant, poi ricarica l'integrazione (o risolvi il problema di fondo e ricaricala) per riprovare."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "Non è stato possibile installare il pacchetto del server HA-MCP nel processo",
|
||||
"description": "L'installazione del pacchetto ha-mcp per il server nel processo non è riuscita:\n\n{detail}\n\nRisolvi il problema di compatibilità o di installazione descritto sopra, poi ricarica l'integrazione per riprovare."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "Aggiorna il componente personalizzato HA-MCP tramite HACS",
|
||||
"description": "Il server ha-mcp installato richiede il componente personalizzato HA-MCP {required} o successivo, ma hai {installed}. Aggiorna il componente tramite HACS (apri la voce del componente personalizzato HA-MCP e usa «Aggiorna informazioni» se non compare ancora alcun aggiornamento) e riavvia Home Assistant. Nel frattempo il server continua a funzionare, ma alcune funzioni più recenti potrebbero non funzionare finché il componente non viene aggiornato."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "Aggiornamento del server HA-MCP in attesa di un aggiornamento del componente",
|
||||
"description": "È disponibile il server ha-mcp {latest}, ma quella versione ha aggiornato anche il componente personalizzato HA-MCP (alla {shipped}; tu esegui la {running}). Per evitare di avviare una versione del server con cui il componente in uso non è mai stato provato, l'aggiornamento automatico del server resta sospeso finché il componente non viene aggiornato.\n\nAggiorna il componente tramite HACS (apri la voce del componente personalizzato HA-MCP e usa «Aggiorna informazioni» se non compare ancora alcun aggiornamento), poi riavvia Home Assistant: l'aggiornamento del server si installa automaticamente subito dopo. Per installare comunque l'aggiornamento del server, premi Installa sull'entità di aggiornamento del server HA-MCP."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Componente installato dal repository obsoleto",
|
||||
"description": "HACS sta tracciando il repository principale del server ha-mcp per questo componente, quindi HACS mostra qui i numeri di versione e le note di rilascio del server invece di quelli propri del componente (2.x). Gli aggiornamenti continuano a funzionare, ma rimangono etichettati in modo errato. Per risolvere: rimuovi questo repository da HACS (le tue impostazioni di integrazione e le voci di configurazione vengono mantenute), aggiungi homeassistant-ai/ha-mcp-integration come repository personalizzato, reinstalla il componente da esso e riavvia Home Assistant."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Riavvia Home Assistant per applicare le impostazioni OAuth legacy",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Riavvia Home Assistant?",
|
||||
"description": "L'invio di questo modulo riavvierà Home Assistant e applicherà le impostazioni OAuth legacy.\n\nFai clic su **Invia** per riavviare ora."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "Stabile (consigliato)",
|
||||
"dev": "Sviluppo (ultima build)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "URL webhook segreto (predefinito)",
|
||||
"ha_auth": "Accedi con Home Assistant (OAuth)",
|
||||
"legacy": "OAuth legacy (questa integrazione rilascia un ID client/Secret da incollare)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "Ricerca degli strumenti (compatta, predefinita)",
|
||||
"full": "Catalogo completo",
|
||||
"both": "Entrambi (scelta per agente)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "Aggiornamento"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Accesso diretto dalla macchina Home Assistant: {url}",
|
||||
"connect_local_lan": "Locale/LAN (quando l'accesso dalla rete è «Local network»): {url}",
|
||||
"connect_remote_url": "URL di connessione remota: {url}",
|
||||
"connect_urls_label": "URL di connessione:",
|
||||
"connect_urls_pending": "Gli URL di connessione compaiono qui (e nel registro di Home Assistant) una volta avviato il server.",
|
||||
"connect_webhook_disabled": "L'accesso remoto tramite webhook è disattivato (modalità solo locale).",
|
||||
"oauth_creds_pending": "Il Client ID e il Client Secret compaiono qui una volta avviato il server.",
|
||||
"oauth_not_serving": "OAuth legacy non li sta ancora servendo — riavvia Home Assistant quando te lo chiede per attivarli.",
|
||||
"oauth_select_legacy_mode": "Imposta la modalità di autenticazione qui sopra su OAuth legacy e salva per generare un Client ID e un Client Secret.",
|
||||
"panel_hint": "Apri il [pannello impostazioni HA-MCP](/ha-mcp) per la gestione degli strumenti e le impostazioni del server.",
|
||||
"tools_module_installed": "Modulo beta/avanzato di strumenti per file e YAML (facoltativo): Installato",
|
||||
"tools_module_not_installed": "Modulo beta/avanzato di strumenti per file e YAML (facoltativo): Non installato — premi «Aggiungi voce» nella pagina di questa integrazione e scegli «HA-MCP File & YAML Tools»",
|
||||
"tools_module_not_loaded": "Modulo beta/avanzato di strumenti per file e YAML (facoltativo): Installato ma non caricato — attiva o ricarica la voce «HA-MCP File & YAML Tools» nella pagina di questa integrazione",
|
||||
"version_line": "Componente {component_version} - Server ha-mcp {server_version} (canale {channel})",
|
||||
"version_not_installed": "non ancora installato",
|
||||
"version_unknown": "sconosciuto"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "HA-MCP 사용자 지정 구성요소",
|
||||
"description": "추가할 항목을 선택하세요. **HA-MCP Server**는 Home Assistant 내부에 전체 ha-mcp 서버를 실행하고 Home Assistant 웹훅을 통해 노출합니다. 대부분의 사용자에게 권장되는 설치 방식입니다. 독립 실행형 서버이며 앱(애드온), Docker, uvx/PyPI, stdio 등 다른 모든 설치 방법을 완전히 대체하므로, 이 방식을 실행하는 경우 다른 방식을 함께 실행하지 마세요. **HA-MCP File & YAML Tools**에는 두 가지 역할이 있습니다. ha-mcp 서버가 Home Assistant 외부(앱, Docker, uvx/PyPI, stdio)에서 실행되는 경우, 이 항목은 외부에서 접근할 수 없는 프로세스 내 컴포넌트 기능(빠른 프로세스 내 검색, 설정 항목 메타데이터, 레지스트리 기반 읽기)을 해당 서버에 제공하므로 모든 외부 설치에 추가할 가치가 있으며, ha-mcp의 선택적 파일/YAML 도구에서 사용하는 권한 있는 파일 및 YAML 편집 서비스를 제어합니다. 대신 HA-MCP Server 항목을 추가하는 경우, 해당 항목이 이미 공유 기능을 제공하므로 이 두 번째 항목은 선택적 파일/YAML 도구의 제어 목적으로만 필요합니다. 모든 서버 유형과 호환되며 언제든지 나중에 추가할 수 있습니다.",
|
||||
"menu_options": {
|
||||
"server": "HA-MCP 서버(권장)",
|
||||
"tools": "HA-MCP 파일 및 YAML 도구(선택)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "HA-MCP 파일 및 YAML 도구",
|
||||
"description": "ha-mcp의 선택적 파일/YAML 편집 도구(기본적으로 비활성화된 기능 플래그)로 제한되는 권한 있는 파일 및 YAML 설정 서비스를 설정합니다. 이는 프로세스 내 HA-MCP Server를 포함한 모든 서버 유형에 적용됩니다. ha-mcp 서버가 Home Assistant 외부에서 실행되는 경우, 이 항목은 외부에서 접근할 수 없는 프로세스 내 컴포넌트 기능(빠른 프로세스 내 검색, 설정 항목 메타데이터, 레지스트리 기반 읽기)도 제공하므로 파일/YAML 도구가 꺼져 있는 경우에도 추가하는 것이 좋습니다. 이 항목은 언제든지 추가하거나 제거할 수 있습니다."
|
||||
},
|
||||
"server": {
|
||||
"title": "HA-MCP 서버",
|
||||
"description": "Home Assistant 안에서 전체 ha-mcp 서버를 실행하고 Home Assistant 웹훅을 통해 원격으로 노출합니다(Nabu Casa나 임의의 리버스 프록시로 접근 가능). 이는 독립 실행 서버로서 앱(애드온), 도커, uvx/PyPI, stdio 설치 방식을 완전히 대체하므로 다른 ha-mcp 서버와 함께 실행하지 마세요. **제출**을 선택하면 시작됩니다. 포트, 바인딩, 인증은 나중에 통합 구성요소 옵션에서 변경할 수 있습니다."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "이 항목은 이미 설정되어 있습니다.",
|
||||
"unsupported_home_assistant": "프로세스 내 HA-MCP 서버는 Home Assistant {required} 이상이 필요하지만 이 인스턴스는 {installed}을(를) 실행 중입니다. 앱(애드온)이나 도커 컨테이너로 실행 중인 외부 ha-mcp 서버와 함께라면 이 구성요소의 HA-MCP 파일 및 YAML 도구 항목은 계속 사용할 수 있습니다."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "HA-MCP 파일 및 YAML 도구",
|
||||
"description": "ha-mcp의 선택형 파일/YAML 도구가 사용하는, 권한이 필요한 파일 및 YAML 편집 서비스를 구성합니다. 이 설정은 ha-mcp 서버 자체의 설정 UI에도 나타나며 다시 시작하지 않아도 즉시 적용됩니다. 재정의할 수 없는 기본 차단 규칙은 민감한 경로(예: .storage)와 신뢰 경계 키(homeassistant, http, frontend, lovelace)를 계속 차단합니다.",
|
||||
"data": {
|
||||
"allowed_dirs": "추가 파일 디렉터리",
|
||||
"extra_yaml_keys": "추가 YAML 쓰기 허용 키"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "설정 디렉터리 기준 상대 디렉터리이며 각각 읽기와 쓰기가 허용됩니다(예: pyscript). 경로 탈출과 설정 디렉터리 밖의 항목은 무시됩니다.",
|
||||
"extra_yaml_keys": "이 설치본에서 YAML 우선으로 동작하는 통합 구성요소(예: alert2)를 위해, ha_config_set_yaml이 기본 허용 키 외에 추가로 쓸 수 있는 최상위 키입니다. 신뢰 경계 키는 무시됩니다."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "HA-MCP 서버",
|
||||
"description": "HA-MCP 서버를 구성합니다. {panel_hint}여기서 변경한 내용은 저장할 때 적용됩니다.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "릴리스 채널",
|
||||
"auto_update": "서버 자동 업데이트",
|
||||
"server_port": "MCP 서버 수신 포트",
|
||||
"bind_host": "네트워크 접근 범위",
|
||||
"webhook_auth": "인증 방식",
|
||||
"oauth_client_id_override": "레거시 OAuth: 사용자 지정 클라이언트 ID(선택)",
|
||||
"oauth_client_secret_override": "레거시 OAuth: 사용자 지정 클라이언트 시크릿(선택)",
|
||||
"oauth_regenerate": "레거시 OAuth: 지금 클라이언트 ID/시크릿 재생성",
|
||||
"pip_spec": "개발자용: ha-mcp 패키지 재정의",
|
||||
"server_url": "Home Assistant URL(고급)",
|
||||
"external_url": "외부 URL(선택)",
|
||||
"webhook_id_override": "사용자 지정 웹훅 시크릿(선택)",
|
||||
"secret_path_override": "사용자 지정 직접 접속 경로(선택)",
|
||||
"regenerate_secrets": "지금 접속 시크릿 재생성",
|
||||
"enable_webhook": "웹훅을 통한 원격 접속",
|
||||
"enable_llm_api": "대화 에이전트 LLM API",
|
||||
"llm_api_exposure": "대화 에이전트 도구 노출 방식",
|
||||
"enable_startup_notification": "시작 알림",
|
||||
"enable_sidebar_panel": "사이드바 설정 패널"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "안정 채널은 최신 정식 릴리스를, 개발 채널은 최신 개발 빌드를 설치합니다. 자동 업데이트가 켜져 있으면 다시 불러오기나 재시작 때, 그리고 주기적인 확인을 통해 선택한 채널의 최신 빌드가 설치됩니다. 아래의 개발자용 패키지 재정의가 우선하며, 그 경우 자동 업데이트는 꺼집니다.",
|
||||
"auto_update": "켜면 선택한 채널의 최신 릴리스가 자동으로 설치됩니다. 다시 불러오기나 재시작 때, 그리고 주기적인 확인을 통해 설치됩니다. 끄면 다시 켤 때까지 현재 설치된 버전이 유지됩니다. 이 설정은 ha-mcp 서버 패키지에만 적용되며, HA-MCP 사용자 지정 구성요소 자체의 업데이트는 계속 HACS를 통해 이루어집니다.",
|
||||
"server_port": "이 서버가 수신할 포트입니다. ha-mcp 앱(애드온)이 9583을 사용하므로 둘을 함께 실행할 수 있도록 기본값은 9584입니다. 앱(애드온)을 실행하지 않는다면 비어 있는 아무 포트나 사용할 수 있습니다.",
|
||||
"bind_host": "MCP 서버 포트에 직접 접속할 수 있는 대상을 정합니다. 기본값은 앱(애드온)과 동일하게 로컬 네트워크에서 접근할 수 있으며, 비밀 경로가 자격 증명 역할을 합니다. 루프백을 고르면 Home Assistant가 실행 중인 머신에서만 접속할 수 있습니다. 웹훅 URL과 사이드바 패널은 어느 쪽이든 동작합니다.",
|
||||
"webhook_auth": "MCP 클라이언트가 웹훅 URL에서 자신을 증명하는 방식입니다. 비밀 URL 방식에서는 링크 자체가 유일한 자격 증명입니다. OAuth를 사용하는 클라이언트에서는 OAuth가 동작하지만 모든 로그인이 자동으로 승인되므로, 로그인도 클라이언트 ID나 시크릿도 필요 없습니다. Home Assistant 로그인 방식에서는 claude.ai 같은 클라이언트가 Home Assistant 관리자 계정으로 로그인합니다(OAuth). 레거시 OAuth 방식에서는 이 통합 구성요소가 자체 로그인 서비스를 실행하고, 클라이언트에 붙여 넣을 클라이언트 ID와 시크릿을 발급합니다. 이 방식을 완전히 켜거나 끄려면 Home Assistant를 다시 시작해야 할 수 있습니다.",
|
||||
"oauth_client_id_override": "자동 생성된 레거시 OAuth 클라이언트 ID를 대체합니다. 현재 값을 유지하려면 비워 두세요. 인증 방식이 레거시 OAuth일 때만 사용됩니다.",
|
||||
"oauth_client_secret_override": "자동 생성된 레거시 OAuth 클라이언트 시크릿을 대체합니다. 현재 값을 유지하려면 비워 두세요. 인증 방식이 레거시 OAuth일 때만 사용됩니다.",
|
||||
"oauth_regenerate": "일회성 작업입니다. 레거시 OAuth 모드용 클라이언트 ID와 시크릿을 새로 발급합니다. 복구 안내가 요청하는 Home Assistant 재시작을 마쳐야 적용되며, 재시작 전까지는 이전 클라이언트 ID와 시크릿이 계속 동작하고 새 값은 동작하지 않습니다. 위의 재정의 항목 두 개도 함께 비워집니다.",
|
||||
"pip_spec": "비워 두세요. 특정 ha-mcp 빌드를 테스트할 때(예: 사전 릴리스 고정)만 사용합니다. 릴리스 채널보다 우선하며, 값을 지울 때까지 자동 업데이트가 꺼집니다.",
|
||||
"server_url": "프로세스 내 서버가 Home Assistant(보통 이 인스턴스 자신)에 접속할 때 사용하는 URL입니다. 이 인스턴스의 포트와 SSL 설정에서 자동으로 유추하게 하려면 비워 두고, 서버가 다른 경로로 접속해야 할 때만 값을 지정하세요.",
|
||||
"external_url": "기본 접속 URL로 표시됩니다. Home Assistant가 사용자 소유 도메인이나 리버스 프록시 뒤에 있을 때 사용하세요. 스킴을 포함한 전체 기본 주소를 입력합니다. 반드시 Home Assistant를 직접 가리켜야 하며(브라우저에서 열었을 때 HA 로그인 페이지가 나와야 합니다), :8123 같은 포트(그 밖의 어떤 포트도)를 포함하면 안 됩니다. 그렇지 않으면 원격 MCP 클라이언트가 접속하지 못합니다. Nabu Casa나 로컬 주소를 자동으로 사용하려면 비워 두세요.",
|
||||
"webhook_id_override": "접속 URL(/api/webhook/...)에 들어가는 무작위 웹훅 시크릿을 대체합니다. URL 자체가 자격 증명이므로 길고 추측하기 어려운 값을 사용하세요. 현재 값을 유지하려면 비워 두세요.",
|
||||
"secret_path_override": "서버 포트로 직접 접속할 때 사용하는 무작위 경로를 대체합니다. 마찬가지로 경로 자체가 자격 증명입니다. 현재 값을 유지하려면 비워 두세요.",
|
||||
"regenerate_secrets": "일회성 작업입니다. 무작위 웹훅 시크릿과 직접 접속 경로를 새로 만들어 기존 접속 URL을 즉시 무효화합니다. 위의 재정의 항목 두 개도 함께 비워집니다.",
|
||||
"enable_webhook": "로컬 전용 모드로 쓰려면 끄세요. Home Assistant 웹훅이 아예 등록되지 않으므로 Nabu Casa를 포함해 무엇도 Home Assistant를 통해 서버에 접속할 수 없습니다. 서버 포트 직접 접속과 사이드바 패널은 계속 동작합니다.",
|
||||
"enable_llm_api": "Home Assistant 대화 에이전트(OpenAI, Google, Ollama 등)에 전체 도구 모음을 제공합니다. 켜면 에이전트의 'Home Assistant 제어' 항목에서 'HA-MCP Server'를 선택해 Assist 채팅과 음성에서 도구를 사용할 수 있습니다. 켜는 것만으로는 선택 가능해질 뿐이며, 에이전트에서 실제로 선택하기 전에는 아무것도 노출되지 않습니다. 사용 안내: {llm_api_docs_url}",
|
||||
"llm_api_exposure": "대화 에이전트에 제공할 도구 모음의 형태입니다. 도구 검색(기본값)은 고정 도구와 검색·실행 메타 도구로 이루어진 간결한 API를 제공해 에이전트의 컨텍스트를 작게 유지합니다. 전체 카탈로그는 노출된 모든 도구를 그대로 나열하며, 컨텍스트가 큰 모델에 더 적합합니다. 둘 다를 고르면 두 가지를 나란히 등록해 각 에이전트가 'Home Assistant 제어'에서 원하는 쪽을 선택할 수 있습니다. 도구별 노출 여부는 HA-MCP 설정 패널에서 관리합니다. 자세한 내용: {llm_api_docs_url}",
|
||||
"enable_startup_notification": "서버가 시작될 때마다 관리자 전용 설정 화면을 안내하는 알림을 표시합니다. 조용히 시작하려면 끄세요. 접속 URL은 Home Assistant 로그에 계속 표시됩니다.",
|
||||
"enable_sidebar_panel": "사이드바에 HA-MCP 설정 패널을 표시합니다(관리자 전용). 사이드바 항목을 없애려면 끄세요. 서버 옵션은 이 화면에서 계속 사용할 수 있습니다."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "HA-MCP 프로세스 내 서버를 시작하지 못했습니다",
|
||||
"description": "Home Assistant 안에서 HA-MCP 프로세스 내 서버를 시작하지 못했습니다:\n\n{detail}\n\nHome Assistant 로그를 확인한 뒤 통합 구성요소를 다시 불러와(또는 근본 원인을 해결한 뒤 다시 불러와) 재시도하세요."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "HA-MCP 프로세스 내 서버 패키지를 설치하지 못했습니다",
|
||||
"description": "프로세스 내 서버용 ha-mcp 패키지 설치에 실패했습니다:\n\n{detail}\n\n위에 설명된 호환성 또는 설치 문제를 해결한 뒤 통합 구성요소를 다시 불러와 재시도하세요."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "HACS에서 HA-MCP 사용자 지정 구성요소를 업데이트하세요",
|
||||
"description": "설치된 ha-mcp 서버는 HA-MCP 사용자 지정 구성요소 {required} 이상을 요구하지만 현재 {installed}을(를) 사용 중입니다. HACS에서 구성요소를 업데이트한 뒤(HA-MCP 사용자 지정 구성요소 항목을 열고 업데이트가 아직 보이지 않으면 '정보 업데이트'를 사용하세요) Home Assistant를 다시 시작하세요. 그동안에도 서버는 계속 동작하지만, 구성요소를 업데이트하기 전까지 일부 최신 기능이 동작하지 않을 수 있습니다."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "HA-MCP 서버 업데이트가 구성요소 업데이트를 기다리는 중입니다",
|
||||
"description": "ha-mcp 서버 {latest}을(를) 사용할 수 있지만, 해당 릴리스는 HA-MCP 사용자 지정 구성요소도 함께 업데이트했습니다(구성요소 {shipped} 기준, 현재 {running} 사용 중). 현재 구성요소로 한 번도 검증되지 않은 서버 버전이 시작되는 것을 막기 위해, 구성요소가 업데이트될 때까지 서버 자동 업데이트를 보류합니다.\n\nHACS에서 구성요소를 업데이트한 뒤(HA-MCP 사용자 지정 구성요소 항목을 열고 업데이트가 아직 보이지 않으면 '정보 업데이트'를 사용하세요) Home Assistant를 다시 시작하면 서버 업데이트가 자동으로 설치됩니다. 그래도 서버 업데이트를 지금 설치하려면 HA-MCP 서버 업데이트 엔티티에서 설치를 누르세요."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "구성요소가 이전 저장소에서 설치되었습니다",
|
||||
"description": "HACS가 이 구성요소에 대해 ha-mcp 서버 본체 저장소를 추적하고 있어, 구성요소 자체의 버전(2.x)이 아니라 서버의 버전 번호와 릴리스 노트가 표시됩니다. 업데이트는 계속 동작하지만 표기는 계속 어긋납니다. 해결하려면 HACS에서 이 저장소를 제거하고(통합 구성요소 설정과 설정 항목은 유지됩니다), homeassistant-ai/ha-mcp-integration을 사용자 지정 저장소로 추가한 뒤 거기에서 구성요소를 다시 설치하고 Home Assistant를 다시 시작하세요."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "레거시 OAuth 설정을 적용하려면 Home Assistant를 다시 시작하세요",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Home Assistant를 다시 시작할까요?",
|
||||
"description": "이 양식을 제출하면 Home Assistant가 다시 시작되고 레거시 OAuth 설정이 적용됩니다.\n\n지금 다시 시작하려면 **제출**을 누르세요."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "안정(권장)",
|
||||
"dev": "개발(최신 빌드)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "비밀 웹훅 URL(기본값)",
|
||||
"ha_auth": "Home Assistant로 로그인(OAuth)",
|
||||
"legacy": "레거시 OAuth(이 통합 구성요소가 붙여 넣을 클라이언트 ID/시크릿을 발급)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "도구 검색(간결, 기본값)",
|
||||
"full": "전체 카탈로그",
|
||||
"both": "둘 다(에이전트별로 선택)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "업데이트"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Home Assistant 머신에서 직접 접속: {url}",
|
||||
"connect_local_lan": "로컬/LAN(네트워크 접근 범위가 \"Local network\"일 때): {url}",
|
||||
"connect_remote_url": "원격 접속 URL: {url}",
|
||||
"connect_urls_label": "접속 URL:",
|
||||
"connect_urls_pending": "서버가 시작되면 접속 URL이 여기(그리고 Home Assistant 로그)에 표시됩니다.",
|
||||
"connect_webhook_disabled": "웹훅을 통한 원격 접속이 꺼져 있습니다(로컬 전용 모드).",
|
||||
"oauth_creds_pending": "서버가 시작되면 클라이언트 ID와 클라이언트 시크릿이 여기에 표시됩니다.",
|
||||
"oauth_not_serving": "레거시 OAuth가 아직 이 값들을 제공하지 않습니다 — 안내가 나오면 Home Assistant를 다시 시작해 활성화하세요.",
|
||||
"oauth_select_legacy_mode": "위에서 인증 방식을 레거시 OAuth로 설정하고 저장하면 클라이언트 ID와 클라이언트 시크릿이 생성됩니다.",
|
||||
"panel_hint": "도구 관리와 서버 설정은 [HA-MCP 설정 패널](/ha-mcp)에서 할 수 있습니다.",
|
||||
"tools_module_installed": "베타/고급 파일 및 YAML 도구 모듈(선택): 설치됨",
|
||||
"tools_module_not_installed": "베타/고급 파일 및 YAML 도구 모듈(선택): 설치되지 않음 — 이 통합 구성요소 페이지에서 \"항목 추가\"를 누르고 \"HA-MCP 파일 및 YAML 도구(HA-MCP File & YAML Tools)\"를 선택해 추가하세요",
|
||||
"tools_module_not_loaded": "베타/고급 파일 및 YAML 도구 모듈(선택): 설치되었지만 불러오지 않음 — 이 통합 구성요소 페이지에서 \"HA-MCP 파일 및 YAML 도구(HA-MCP File & YAML Tools)\" 항목을 활성화하거나 다시 불러오세요",
|
||||
"version_line": "구성요소 {component_version} - 서버 ha-mcp {server_version}({channel} 채널)",
|
||||
"version_not_installed": "아직 설치되지 않음",
|
||||
"version_unknown": "알 수 없음"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "HA-MCP Aangepaste Component",
|
||||
"description": "Kies wat je wilt toevoegen. **HA-MCP Server** draait de volledige ha-mcp server binnen Home Assistant en stelt deze beschikbaar via een Home Assistant webhook - dit is de installatie die de meeste mensen willen. Het is een standalone server en een volledige vervanging voor elke andere installatiemethode: App (add-on), Docker, uvx/PyPI, stdio; als je deze uitvoert, draai dan niet tegelijkertijd een van de andere. **HA-MCP File & YAML Tools** heeft twee rollen. Als je ha-mcp server buiten Home Assistant draait - app, Docker, uvx/PyPI, stdio - geeft deze vermelding die server ook de in-process componentmogelijkheden die deze van buitenaf niet kan bereiken (snelle in-process zoekopdrachten, config entry metadata, op registers gebaseerde leesbewerkingen), dus het is de moeite waard om toe te voegen aan elke externe installatie, en het fungeert als poortwachter voor de bevoorrechte file- en YAML-bewerkingsservices die worden gebruikt door de optionele file/YAML-tools van ha-mcp. Als je in plaats daarvan de HA-MCP Server-vermelding toevoegt, biedt die vermelding al deze gedeelde mogelijkeheden, dus deze tweede vermelding is dan alleen nodig als poortwachter voor de optionele file/YAML-tools. Het werkt met elk servertype en kan op elk moment later worden toegevoegd.",
|
||||
"menu_options": {
|
||||
"server": "HA-MCP Server (aanbevolen)",
|
||||
"tools": "HA-MCP Bestands- & YAML-hulpmiddelen (optioneel)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "HA-MCP Bestands- & YAML-hulpmiddelen",
|
||||
"description": "Stelt de bevoorrechte file- en YAML-configuratieservices in, die afgeschermd blijven achter de optionele file/YAML-bewerkingstools van ha-mcp (feature flags, standaard uitgeschakeld) - dit geldt voor elk servertype, inclusief de in-process HA-MCP Server. Als je ha-mcp server buiten Home Assistant draait, geeft deze vermelding de server ook de in-process componentmogelijkheden die deze van buitenaf niet kan bereiken (snelle in-process zoekopdrachten, config entry metadata, op registers gebaseerde leesbewerkingen), die de moeite waard zijn om te hebben, zelfs als de file/YAML-tools zijn uitgeschakeld. Je kunt deze vermelding op elk moment toevoegen of verwijderen."
|
||||
},
|
||||
"server": {
|
||||
"title": "HA-MCP Server",
|
||||
"description": "Hiermee draait de volledige ha-mcp server binnen Home Assistant en wordt deze op afstand beschikbaar gesteld via een Home Assistant webhook (bereikbaar via Nabu Casa of een omgekeerde proxy / reverse proxy). Het is een standalone server en een volledige vervanging voor de app (add-on)-, Docker-, uvx/PyPI- en stdio-installatiemethoden - draai deze niet tegelijkertijd met een andere ha-mcp server. Selecteer **Verzenden** om te starten; je kunt de poort, binding en authenticatie achteraf wijzigen in de integratie-opties."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Deze ingang is al ingesteld.",
|
||||
"unsupported_home_assistant": "De in-process HA-MCP Server vereist Home Assistant {required} of nieuwer, maar deze instantie draait {installed}. Je kunt nog steeds de HA-MCP File & YAML Tools-invoer van dit component gebruiken met een externe ha-mcp-server die als app (add-on) of Docker-container draait."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "HA-MCP Bestands- & YAML-hulpmiddelen",
|
||||
"description": "Configureer de geprivilegieerde bestands- en YAML-bewerkingsservices die worden gebruikt door de opt-in bestands-/YAML-hulpmiddelen van ha-mcp. Deze instellingen verschijnen ook in de eigen instellingen-UI van de ha-mcp server en worden direct toegepast, zonder herstart. Een niet-overbrugbare blokkade weigert nog steeds gevoelige paden (bijv. .storage) en trust-boundary sleutels (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Extra bestandsmappen",
|
||||
"extra_yaml_keys": "Extra YAML-schrijfsleutels"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Mappen relatief aan de configuratiemap, elk met lees- en schrijfrechten (bijv. pyscript). Traversale en buiten-configuratie ingangen worden genegeerd.",
|
||||
"extra_yaml_keys": "Top-level sleutels die ha_config_set_yaml mag schrijven naast de ingebouwde, voor YAML-first integraties op deze installatie (bijv. alert2). Trust-boundary sleutels worden genegeerd."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "HA-MCP Server",
|
||||
"description": "Configureer de HA-MCP server. {panel_hint}Wijzigingen hier worden toegepast bij opslaan.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "Releasekanaal",
|
||||
"auto_update": "Automatische serverupdates",
|
||||
"server_port": "Luisterpoort MCP server",
|
||||
"bind_host": "Netwerktoegang",
|
||||
"webhook_auth": "Authenticatiemodus",
|
||||
"oauth_client_id_override": "Legacy OAuth: aangepaste Client ID (optioneel)",
|
||||
"oauth_client_secret_override": "Legacy OAuth: aangepaste Client Secret (optioneel)",
|
||||
"oauth_regenerate": "Legacy OAuth: genereer nu Client ID/Secret opnieuw",
|
||||
"pip_spec": "Ontwikkelaar: ha-mcp pakketoverheersing",
|
||||
"server_url": "Home Assistant URL (geavanceerd)",
|
||||
"external_url": "Externe URL (optioneel)",
|
||||
"webhook_id_override": "Aangepast webhook geheim (optioneel)",
|
||||
"secret_path_override": "Aangepast direct-toegangspad (optioneel)",
|
||||
"regenerate_secrets": "Genereer verbindingsgeheimen nu opnieuw",
|
||||
"enable_webhook": "Toegang op afstand via webhook",
|
||||
"enable_llm_api": "Conversatie-agent LLM API",
|
||||
"llm_api_exposure": "Blootstelling van conversatie-agent hulpmiddelen",
|
||||
"enable_startup_notification": "Opstartmelding",
|
||||
"enable_sidebar_panel": "Zijbalk instellingenpaneel"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "Stabiel installeert de nieuwste stabiele release; Ontwikkeling installeert de nieuwste ontwikkelingsbuild. Wanneer automatische updates zijn ingeschakeld, installeert een herlaadbeurt of herstart, plus een periodieke controle, de nieuwste build van het geselecteerde kanaal. Een ontwikkelaarspakket overschrijving hieronder heeft voorrang en schakelt automatische updates uit.",
|
||||
"auto_update": "Wanneer ingeschakeld, wordt de nieuwste release van het geselecteerde kanaal automatisch geïnstalleerd - bij een herlaadbeurt of herstart, en via een periodieke controle. Wanneer uitgeschakeld, blijft de server op de momenteel geïnstalleerde versie totdat je dit weer inschakelt. Dit beheert alleen het ha-mcp serverpakket; updates voor de HA-MCP Aangepaste Component zelf komen nog steeds via HACS.",
|
||||
"server_port": "De poort waarop deze server luistert. De ha-mcp app (add-on) gebruikt 9583, dus de standaardwaarde is 9584 zodat beide naast elkaar kunnen draaien - als je de app (add-on) niet gebruikt, werkt elke vrije poort.",
|
||||
"bind_host": "Wie er rechtstreeks verbinding kan maken met de MCP-serverpoort. De standaardwaarde komt overeen met de app (add-on): bereikbaar op je lokale netwerk, met het geheime pad als inloggegeven. Kies loopback om alleen verbindingen toe te staan vanaf de Home Assistant-machine zelf - de webhook-URL en het zijbalkpaneel werken in beide gevallen.",
|
||||
"webhook_auth": "Hoe MCP-clients zich bewijzen bij de webhook-URL. Met de geheime URL is de link zelf de enige referentie - OAuth draait nog steeds voor clients die het gebruiken, maar elke aanmelding wordt automatisch goedgekeurd: geen login en geen Client ID of Secret nodig. Met Home Assistant-aanmelding loggen clients zoals claude.ai in met een Home Assistant-beheerdersaccount (OAuth). Met legacy OAuth draait deze integratie zijn eigen aanmeldingsservice en geeft een Client ID en Secret uit om te plakken in clients die er een nodig hebben - een herstart van Home Assistant kan nodig zijn om het volledig in of uit te schakelen.",
|
||||
"oauth_client_id_override": "Vervangt de automatisch gegenereerde legacy OAuth Client ID. Laat leeg om de huidige te behouden. Alleen gebruikt wanneer Authenticatiemodus is ingesteld op legacy OAuth.",
|
||||
"oauth_client_secret_override": "Vervangt de automatisch gegenereerde legacy OAuth Client Secret. Laat leeg om de huidige te behouden. Alleen gebruikt wanneer Authenticatiemodus is ingesteld op legacy OAuth.",
|
||||
"oauth_regenerate": "Eenmalige actie: genereert een nieuwe Client ID en Secret voor legacy OAuth-modus. Het wordt pas van kracht na de Home Assistant herstart waar de reparatieprompt om vraagt - totdat je herstart, blijven de vorige Client ID en Secret werken en de nieuwe niet. Maakt ook de twee overschrijvingsvelden hierboven leeg.",
|
||||
"pip_spec": "Laat leeg. Alleen voor het testen van een specifieke ha-mcp build (bijvoorbeeld een pre-release pin); overschrijft het releasekanaal en schakelt automatische updates uit totdat het leeg is gemaakt.",
|
||||
"server_url": "De URL die de in-process server gebruikt om je Home Assistant te bereiken (meestal deze instantie zelf). Laat leeg om het af te leiden van de poort- en SSL-instellingen van deze instantie; stel alleen een waarde in wanneer de server een andere route nodig heeft.",
|
||||
"external_url": "Weergegeven als de primaire verbindings-URL - gebruik dit wanneer Home Assistant achter je eigen domein of reverse proxy zit. Voer het volledige basisadres in, inclusief het schema. Het moet direct naar Home Assistant wijzen - het openen in een browser zou je HA-inlogpagina moeten bereiken - en mag geen poort bevatten zoals :8123 (of enige andere poort), anders kunnen externe MCP-clients het niet bereiken. Laat leeg om Nabu Casa / het lokale adres automatisch te gebruiken.",
|
||||
"webhook_id_override": "Vervangt het willekeurige webhook-geheim in de verbindings-URL (/api/webhook/...). De URL is de referentie - gebruik een lange, moeilijk te raden waarde. Laat leeg om de huidige te behouden.",
|
||||
"secret_path_override": "Vervangt het willekeurige pad dat wordt gebruikt voor directe toegang op de serverpoort. Dezelfde regel: het pad is de referentie. Laat leeg om de huidige te behouden.",
|
||||
"regenerate_secrets": "Eenmalige actie: genereert een nieuw willekeurig webhook-geheim en direct-toegangspad, waardoor de oude verbindings-URL's onmiddellijk ongeldig worden. Maakt ook de twee bovenstaande overschrijvingsvelden leeg.",
|
||||
"enable_webhook": "Schakel uit voor de lokale-modus: de Home Assistant-webhook wordt helemaal niet geregistreerd, dus niets - inclusief Nabu Casa - kan de server via Home Assistant bereiken. De directe serverpoort en het zijbalkpaneel blijven werken.",
|
||||
"enable_llm_api": "Bied de volledige toolset aan Home Assistant conversatie-agents (OpenAI, Google, Ollama, ...): wanneer ingeschakeld, kunnen agents 'HA-MCP Server' selecteren onder Home Assistant bedienen en de tools aansturen vanuit Assist chat en spraak. Inschakelen maakt het alleen selecteerbaar - niets wordt blootgesteld totdat je het kiest op een agent. Gebruikershandleiding: {llm_api_docs_url}",
|
||||
"llm_api_exposure": "Vorm van de toolset die wordt aangeboden aan conversatie-agents. Tool zoeken (standaard) houdt de context van de agent klein: een compacte API met vastgezette tools plus zoek-/uitvoer meta-tools. Volledige catalogus toont elke blootgestelde tool direct - beter voor modellen met een grote context. Beide registreert de twee naast elkaar, zodat elke agent zijn eigen kiest onder Home Assistant bedienen. Blootstelling per tool wordt beheerd in het HA-MCP instellingenpaneel; details: {llm_api_docs_url}",
|
||||
"enable_startup_notification": "Toon een melding elke keer dat de server start, verwijzend naar de instellingenpagina's die alleen voor beheerders zijn. Schakel uit om stil te starten - de verbindings-URL's verschijnen nog steeds in het Home Assistant-logboek.",
|
||||
"enable_sidebar_panel": "Toon het HA-MCP instellingenpaneel in de zijbalk (alleen beheerders). Schakel uit om de zijbalkvermelding te verwijderen - serveropties blijven beschikbaar op dit scherm."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "De HA-MCP in-process server kon niet starten",
|
||||
"description": "De HA-MCP in-process server kon niet worden gestart binnen Home Assistant:\n\n{detail}\n\nControleer de Home Assistant-logboeken en herlaad vervolgens de integratie (of los het onderliggende probleem op en herlaad) om opnieuw te proberen."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "Het HA-MCP in-process serverpakket kon niet worden geïnstalleerd",
|
||||
"description": "Het installeren van het ha-mcp pakket voor de in-process server is mislukt:\n\n{detail}\n\nLos het hierboven beschreven compatibiliteits- of installatieprobleem op en herlaad vervolgens de integratie om opnieuw te proberen."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "Update de HA-MCP Custom Component via HACS",
|
||||
"description": "De geïnstalleerde ha-mcp server vereist HA-MCP Custom Component {required} of nieuwer, maar je hebt {installed}. Update de component via HACS (open de HA-MCP Custom Component-vermelding en gebruik 'Update informatie' als er nog geen update wordt getoond) en herstart Home Assistant. De server blijft ondertussen draaien, maar sommige nieuwere functies werken mogelijk niet totdat de component is bijgewerkt."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "HA-MCP serverupdate wacht op een componentupdate",
|
||||
"description": "ha-mcp server {latest} is beschikbaar, maar die release heeft ook de HA-MCP Custom Component bijgewerkt (naar {shipped}; je draait {running}). Om te voorkomen dat een serverversie wordt gestart waarmee de draaiende component nog nooit is getest, wordt de automatische serverupdate uitgesteld totdat de component is bijgewerkt.\n\nUpdate de component via HACS (open de HA-MCP Custom Component-vermelding en gebruik 'Update informatie' als er nog geen update wordt getoond) en herstart vervolgens Home Assistant - de serverupdate wordt daarna automatisch geïnstalleerd. Om de serverupdate toch te installeren, druk je op Installeren op de HA-MCP serverupdate-entiteit."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Component geïnstalleerd vanuit de verouderde repository",
|
||||
"description": "HACS volgt de hoofdrepository van de ha-mcp-server voor dit component, dus HACS toont hier de versienummers en releasenotes van de server in plaats van die van het component zelf (2.x). Updates blijven werken, maar blijven op deze manier verkeerd gelabeld. Om dit te verhelpen: verwijder deze repository uit HACS (je integratie-instellingen en configuratie-items blijven behouden), voeg homeassistant-ai/ha-mcp-integration toe als een aangepaste repository, installeer het component opnieuw hiervandaan, en herstart Home Assistant."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Herstart Home Assistant om de legacy OAuth-instellingen toe te passen",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Home Assistant opnieuw opstarten?",
|
||||
"description": "Het indienen van dit formulier zal Home Assistant opnieuw opstarten en de legacy OAuth-instellingen toepassen.\n\nKlik op <strong>Verzenden</strong> om nu opnieuw op te starten."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "Stabiel (aanbevolen)",
|
||||
"dev": "Ontwikkeling (laatste build)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "Geheime webhook-URL (standaard)",
|
||||
"ha_auth": "Aanmelden met Home Assistant (OAuth)",
|
||||
"legacy": "Legacy OAuth (deze integratie geeft een Client ID/Secret uit om te plakken)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "Tool zoeken (compact, standaard)",
|
||||
"full": "Volledige catalogus",
|
||||
"both": "Beide (kies per agent)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "Update"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Directe toegang vanaf de Home Assistant-machine: {url}",
|
||||
"connect_local_lan": "Lokaal/LAN (wanneer Netwerktoegang \"Local network\" is): {url}",
|
||||
"connect_remote_url": "Verbindings-URL op afstand: {url}",
|
||||
"connect_urls_label": "Verbindings-URL('s):",
|
||||
"connect_urls_pending": "De verbindings-URL's verschijnen hier (en in het Home Assistant-logboek) zodra de server is gestart.",
|
||||
"connect_webhook_disabled": "Toegang op afstand via webhook is uitgeschakeld (alleen-lokale modus).",
|
||||
"oauth_creds_pending": "De Client ID en Client Secret verschijnen hier zodra de server is gestart.",
|
||||
"oauth_not_serving": "Legacy OAuth levert deze nog niet — start Home Assistant opnieuw op wanneer je daarom wordt gevraagd, om ze te activeren.",
|
||||
"oauth_select_legacy_mode": "Stel de authenticatiemodus hierboven in op legacy OAuth en sla op om een Client ID en Client Secret te genereren.",
|
||||
"panel_hint": "Open het [HA-MCP instellingenpaneel](/ha-mcp) voor toolbeheer en serverinstellingen.",
|
||||
"tools_module_installed": "Bèta/geavanceerde bestands- & YAML-tools module (optioneel): Geïnstalleerd",
|
||||
"tools_module_not_installed": "Bèta/geavanceerde bestands- & YAML-tools module (optioneel): Niet geïnstalleerd — druk op \"Item toevoegen\" op de pagina van deze integratie en kies \"HA-MCP File & YAML Tools\" om deze toe te voegen",
|
||||
"tools_module_not_loaded": "Bèta/geavanceerde bestands- & YAML-tools module (optioneel): Geïnstalleerd maar niet geladen — schakel het \"HA-MCP File & YAML Tools\" item op de pagina van deze integratie in of herlaad het",
|
||||
"version_line": "Component {component_version} - Server ha-mcp {server_version} ({channel} kanaal)",
|
||||
"version_not_installed": "nog niet geïnstalleerd",
|
||||
"version_unknown": "onbekend"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Niestandardowy komponent HA-MCP",
|
||||
"description": "Wybierz, co chcesz dodać. **HA-MCP Server** uruchamia pełny serwer ha-mcp wewnątrz Home Assistanta i udostępnia go za pomocą webhooka Home Assistanta – jest to instalacja, której wybiera większość osób. Jest to samodzielny serwer i kompletny zamiennik każdej innej metody instalacji: aplikacji (dodatku), Dockera, uvx/PyPI, stdio; jeśli go uruchamiasz, nie uruchamiaj równocześnie żadnej z nich. **HA-MCP File & YAML Tools** pełni dwie role. Jeśli Twój serwer ha-mcp działa poza Home Assistantem – w aplikacji, Dockerze, uvx/PyPI, stdio – ten wpis zapewnia również temu serwerowi możliwości komponentu w procesie, do których nie ma dostępu z zewnątrz (szybkie wyszukiwanie w procesie, metadane wpisów konfiguracji, odczyty oparte na rejestrze), więc warto go dodać do każdej zewnętrznej instalacji, a także stanowi on bramkę dla uprzywilejowanych usług edycji plików i YAML, używanych przez opcjonalne narzędzia plików/YAML w ha-mcp. Jeśli zamiast tego dodasz wpis HA-MCP Server, zapewnia on już te współdzielone możliwości, więc ten drugi wpis jest wtedy potrzebny tylko jako bramka dla opcjonalnych narzędzi plików/YAML. Działa z każdym typem serwera i można go dodać później w dowolnym momencie.",
|
||||
"menu_options": {
|
||||
"server": "Serwer HA-MCP (zalecane)",
|
||||
"tools": "Narzędzia plików i YAML HA-MCP (opcjonalne)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "Narzędzia plików i YAML HA-MCP",
|
||||
"description": "Konfiguruje uprzywilejowane usługi edycji plików i konfiguracji YAML, które pozostają zabezpieczone za opcjonalnymi narzędziami edycji plików/YAML ha-mcp (flagi funkcji, domyślnie wyłączone) – dotyczy to każdego typu serwera, w tym serwera HA-MCP Server działającego w procesie. Jeśli Twój serwer ha-mcp działa poza Home Assistantem, ten wpis zapewnia mu również możliwości komponentu w procesie, do których nie ma dostępu z zewnątrz (szybkie wyszukiwanie w procesie, metadane wpisów konfiguracji, odczyty oparte na rejestrze), które warto mieć nawet przy wyłączonych narzędziach plików/YAML. Możesz dodać lub usunąć ten wpis w dowolnym momencie."
|
||||
},
|
||||
"server": {
|
||||
"title": "Serwer HA-MCP",
|
||||
"description": "Uruchamia to pełny serwer ha-mcp wewnątrz Home Assistanta i udostępnia go zdalnie za pomocą webhooka Home Assistanta (dostępnego przez Nabu Casa lub dowolne proxy odwrotne). Jest to samodzielny serwer i kompletny zamiennik metod instalacji takich jak aplikacja (dodatek), Docker, uvx/PyPI oraz stdio – nie uruchamiaj go obok innego serwera ha-mcp. Wybierz **Wyślij**, aby go uruchomić; port, powiązanie i uwierzytelnianie możesz zmienić później w opcjach integracji."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Ten wpis jest już skonfigurowany.",
|
||||
"unsupported_home_assistant": "Wbudowany HA-MCP Server wymaga Home Assistanta w wersji {required} lub nowszej, ale ta instancja uruchomiona jest w wersji {installed}. Nadal możesz używać wpisu HA-MCP File & YAML Tools tego komponentu z zewnętrznym serwerem ha-mcp działającym jako aplikacja (dodatek) lub kontener Docker."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "Narzędzia plików i YAML HA-MCP",
|
||||
"description": "Skonfiguruj uprzywilejowane usługi edycji plików i YAML używane przez opcjonalne narzędzia plików/YAML ha-mcp. Te ustawienia pojawiają się również w interfejsie ustawień serwera ha-mcp i są stosowane na żywo, bez restartu. Nienadpisywalna blokada nadal blokuje wrażliwe ścieżki (np. .storage) i klucze graniczne zaufania (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Dodatkowe katalogi plików",
|
||||
"extra_yaml_keys": "Dodatkowe klucze zapisu YAML"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Katalogi względem katalogu konfiguracyjnego, każdy z uprawnieniami do odczytu i zapisu (np. pyscript). Wpisy poza konfiguracją i przechodzenie przez katalogi są odrzucane.",
|
||||
"extra_yaml_keys": "Klucze najwyższego poziomu, które ha_config_set_yaml może zapisywać oprócz wbudowanych, dla integracji opartych na YAML w tej instalacji (np. alert2). Klucze graniczne zaufania są odrzucane."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "Serwer HA-MCP",
|
||||
"description": "Skonfiguruj serwer HA-MCP. {panel_hint}Zmiany są stosowane po zapisaniu.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "Kanał wydania",
|
||||
"auto_update": "Automatyczne aktualizacje serwera",
|
||||
"server_port": "Port nasłuchu serwera MCP",
|
||||
"bind_host": "Dostęp sieciowy",
|
||||
"webhook_auth": "Tryb uwierzytelniania",
|
||||
"oauth_client_id_override": "Legacy OAuth: niestandardowy Client ID (opcjonalnie)",
|
||||
"oauth_client_secret_override": "Legacy OAuth: niestandardowy Client Secret (opcjonalnie)",
|
||||
"oauth_regenerate": "Legacy OAuth: wygeneruj ponownie Client ID/Secret teraz",
|
||||
"pip_spec": "Deweloper: nadpisanie pakietu ha-mcp",
|
||||
"server_url": "URL Home Assistanta (zaawansowane)",
|
||||
"external_url": "Zewnętrzny URL (opcjonalnie)",
|
||||
"webhook_id_override": "Niestandardowy sekret webhooka (opcjonalnie)",
|
||||
"secret_path_override": "Niestandardowa ścieżka bezpośredniego dostępu (opcjonalnie)",
|
||||
"regenerate_secrets": "Wygeneruj ponownie sekrety połączenia teraz",
|
||||
"enable_webhook": "Zdalny dostęp przez webhook",
|
||||
"enable_llm_api": "API LLM agenta konwersacyjnego",
|
||||
"llm_api_exposure": "Udostępnianie narzędzi agenta konwersacyjnego",
|
||||
"enable_startup_notification": "Powiadomienie o uruchomieniu",
|
||||
"enable_sidebar_panel": "Panel ustawień paska bocznego"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "„Stable” instaluje najnowszą stabilną wersję; „Development” instaluje najnowszą wersję rozwojową. Gdy automatyczne aktualizacje są włączone, przeładowanie lub restart, a także okresowe sprawdzanie, instalują najnowszą kompilację wybranego kanału. Nadpisanie pakietu deweloperskiego poniżej ma pierwszeństwo i wyłącza automatyczne aktualizacje.",
|
||||
"auto_update": "Gdy włączone, najnowsza wersja wybranego kanału jest instalowana automatycznie – podczas przeładowania lub restartu oraz poprzez okresowe sprawdzanie. Gdy wyłączone, serwer pozostaje na aktualnie zainstalowanej wersji, dopóki nie włączysz tej opcji ponownie. Kontroluje to tylko pakiet serwera ha-mcp; aktualizacje samego niestandardowego komponentu HA-MCP nadal pochodzą z HACS.",
|
||||
"server_port": "Port, na którym nasłuchuje ten serwer. Aplikacja (dodatek) ha-mcp używa portu 9583, więc domyślnie ustawiono 9584, aby umożliwić działanie obu obok siebie – jeśli nie używasz aplikacji (dodatku), odpowiedni będzie dowolny wolny port.",
|
||||
"bind_host": "Określa, kto może łączyć się bezpośrednio z portem serwera MCP. Wartość domyślna odpowiada aplikacji (dodatkowi): osiągalny w Twojej sieci lokalnej, przy czym tajna ścieżka służy jako poświadczenie. Wybierz loopback, aby zezwolić na połączenia wyłącznie z samej maszyny Home Assistanta – adres URL webhooka oraz panel na pasku bocznym działają w obu przypadkach.",
|
||||
"webhook_auth": "Jak klienci MCP uwierzytelniają się pod adresem URL webhooka. Z tajnym adresem URL, sam link jest jedynym poświadczeniem – OAuth nadal działa dla klientów, którzy go używają, ale każde logowanie jest zatwierdzane automatycznie: nie jest potrzebne logowanie ani Client ID czy Secret. Przy logowaniu przez Home Assistant, klienci tacy jak claude.ai logują się za pomocą konta administratora Home Assistant (OAuth). Przy Legacy OAuth, ta integracja uruchamia własną usługę logowania i wydaje Client ID oraz Secret do wklejenia do klientów, którzy tego wymagają – może być potrzebny restart Home Assistanta, aby w pełni włączyć lub wyłączyć tę funkcję.",
|
||||
"oauth_client_id_override": "Zastępuje automatycznie wygenerowany Client ID legacy OAuth. Pozostaw puste, aby zachować obecny. Używane tylko, gdy tryb uwierzytelniania jest ustawiony na legacy OAuth.",
|
||||
"oauth_client_secret_override": "Zastępuje automatycznie wygenerowany Client Secret legacy OAuth. Pozostaw puste, aby zachować obecny. Używane tylko, gdy tryb uwierzytelniania jest ustawiony na legacy OAuth.",
|
||||
"oauth_regenerate": "Jednorazowa akcja: generuje nowe Client ID i Secret dla trybu legacy OAuth. Wchodzi w życie dopiero po restarcie Home Assistanta, o który prosi monit naprawy – dopóki nie zrestartujesz, poprzednie Client ID i Secret nadal działają, a nowe nie. Czyści również dwa powyższe pola nadpisania.",
|
||||
"pip_spec": "Pozostaw puste. Tylko do testowania konkretnej kompilacji ha-mcp (na przykład przypiętej wersji przedpremierowej); nadpisuje kanał wydania i wyłącza automatyczne aktualizacje, dopóki nie zostanie wyczyszczone.",
|
||||
"server_url": "URL, którego serwer działający w procesie używa do dotarcia do Twojego Home Assistanta (zazwyczaj tej samej instancji). Pozostaw puste, aby wyprowadzić go z portu i ustawień SSL tej instancji; ustaw wartość tylko wtedy, gdy serwer potrzebuje innej trasy.",
|
||||
"external_url": "Wyświetlany jako główny URL połączenia — użyj go, gdy Home Assistant znajduje się za Twoją własną domeną lub odwrotnym proxy. Wprowadź pełny adres bazowy, włączając schemat. Musi wskazywać bezpośrednio na Home Assistant – otwarcie go w przeglądarce powinno prowadzić do strony logowania HA – i nie może zawierać portu, takiego jak :8123 (ani żadnego innego portu), w przeciwnym razie zdalne klienty MCP nie będą mogły się z nim połączyć. Pozostaw puste, aby automatycznie użyć Nabu Casa / adresu lokalnego.",
|
||||
"webhook_id_override": "Zastępuje losowy sekret webhooka w URL-u połączenia (/api/webhook/...). URL jest poświadczeniem – użyj długiej, trudnej do odgadnięcia wartości. Pozostaw puste, aby zachować obecny.",
|
||||
"secret_path_override": "Zastępuje losową ścieżkę używaną do bezpośredniego dostępu na porcie serwera. Ta sama zasada: ścieżka jest poświadczeniem. Pozostaw puste, aby zachować obecny.",
|
||||
"regenerate_secrets": "Jednorazowa akcja: generuje nowy, losowy sekret webhooka i ścieżkę bezpośredniego dostępu, natychmiast unieważniając stare URL-e połączenia. Czyści również dwa powyższe pola nadpisania.",
|
||||
"enable_webhook": "Wyłącz dla trybu tylko lokalnego: webhook Home Assistant nie jest w ogóle rejestrowany, więc nic – w tym Nabu Casa – nie może dotrzeć do serwera przez Home Assistant. Bezpośredni port serwera i panel boczny nadal działają.",
|
||||
"enable_llm_api": "Oferuj pełny zestaw narzędzi agentom konwersacyjnym Home Assistant (OpenAI, Google, Ollama, ...): gdy włączone, agenci mogą wybrać „HA-MCP Server” w sekcji „Steruj Home Assistant” i używać narzędzi z czatu i głosu Assist. Włączenie tylko sprawia, że jest ono dostępne do wyboru – nic nie jest ujawniane, dopóki nie wybierzesz go dla agenta. Przewodnik użytkowania: {llm_api_docs_url}",
|
||||
"llm_api_exposure": "Kształt zestawu narzędzi oferowanego agentom konwersacyjnym. Wyszukiwanie narzędzi (domyślne) utrzymuje mały kontekst agenta: kompaktowe API z przypiętymi narzędziami plus meta-narzędzia do wyszukiwania/wykonywania. Pełny katalog wyświetla każde ujawnione narzędzie bezpośrednio – lepsze dla modeli o dużym kontekście. Oba rejestrują się obok siebie, więc każdy agent wybiera własne w sekcji „Steruj Home Assistant”. Ujawnianie poszczególnych narzędzi jest zarządzane w panelu ustawień HA-MCP; szczegóły: {llm_api_docs_url}",
|
||||
"enable_startup_notification": "Pokazuj powiadomienie za każdym razem, gdy serwer się uruchamia, wskazujące na powierzchnie ustawień tylko dla administratorów. Wyłącz, aby uruchamiać cicho – URL-e połączenia nadal pojawiają się w logu Home Assistant.",
|
||||
"enable_sidebar_panel": "Pokaż panel ustawień HA-MCP na pasku bocznym (tylko dla administratorów). Wyłącz, aby usunąć wpis z paska bocznego – opcje serwera pozostają dostępne na tym ekranie."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "Serwer HA-MCP w procesie nie uruchomił się",
|
||||
"description": "Serwer HA-MCP w procesie nie mógł zostać uruchomiony wewnątrz Home Assistant:\n\n{detail}\n\nSprawdź logi Home Assistant, a następnie przeładuj integrację (lub napraw podstawowy problem i przeładuj), aby spróbować ponownie."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "Pakiet serwera HA-MCP w procesie nie mógł zostać zainstalowany",
|
||||
"description": "Instalacja pakietu ha-mcp dla serwera w procesie nie powiodła się:\n\n{detail}\n\nRozwiąż problem zgodności lub instalacji opisany powyżej, a następnie przeładuj integrację, aby spróbować ponownie."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "Zaktualizuj niestandardowy komponent HA-MCP przez HACS",
|
||||
"description": "Zainstalowany serwer ha-mcp wymaga niestandardowego komponentu HA-MCP {required} lub nowszego, ale masz {installed}. Zaktualizuj komponent przez HACS (otwórz wpis niestandardowego komponentu HA-MCP i użyj „Aktualizuj informacje”, jeśli aktualizacja nie jest jeszcze widoczna) i uruchom ponownie Home Assistant. Serwer działa w międzyczasie, ale niektóre nowsze funkcje mogą nie działać, dopóki komponent nie zostanie zaktualizowany."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "Aktualizacja serwera HA-MCP oczekuje na aktualizację komponentu",
|
||||
"description": "Serwer ha-mcp {latest} jest dostępny, ale ta wersja zaktualizowała również niestandardowy komponent HA-MCP (do {shipped}; używasz {running}). Aby uniknąć uruchamiania wersji serwera, z którą działający komponent nigdy nie był testowany, automatyczna aktualizacja serwera jest wstrzymana do czasu aktualizacji komponentu.\n\nZaktualizuj komponent przez HACS (otwórz wpis niestandardowego komponentu HA-MCP i użyj „Aktualizuj informacje”, jeśli aktualizacja nie jest jeszcze widoczna), a następnie uruchom ponownie Home Assistant – aktualizacja serwera zainstaluje się automatycznie później. Aby mimo wszystko zainstalować aktualizację serwera, naciśnij „Zainstaluj” na encji aktualizacji serwera HA-MCP."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Komponent zainstalowany z przestarzałego repozytorium",
|
||||
"description": "HACS śledzi główne repozytorium serwera ha-mcp dla tego komponentu, więc HACS wyświetla tutaj numery wersji serwera i uwagi do wydania zamiast własnych (2.x) komponentu. Aktualizacje nadal działają, ale pozostają błędnie oznaczone. Aby naprawić: usuń to repozytorium z HACS (Twoje ustawienia integracji i wpisy konfiguracyjne zostaną zachowane), dodaj homeassistant-ai/ha-mcp-integration jako niestandardowe repozytorium, zainstaluj ponownie komponent z niego i uruchom ponownie Home Assistanta."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Uruchom ponownie Home Assistanta, aby zastosować ustawienia Legacy OAuth",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Uruchomić ponownie Home Assistanta?",
|
||||
"description": "Wysłanie tego formularza spowoduje ponowne uruchomienie Home Assistanta i zastosowanie ustawień Legacy OAuth.\n\nKliknij **Wyślij**, aby uruchomić ponownie teraz."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "Stabilny (zalecany)",
|
||||
"dev": "Deweloperski (najnowsza kompilacja)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "Tajny URL webhooka (domyślny)",
|
||||
"ha_auth": "Zaloguj się za pomocą Home Assistant (OAuth)",
|
||||
"legacy": "Legacy OAuth (ta integracja wydaje Client ID/Secret do wklejenia)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "Wyszukiwanie narzędzi (kompaktowe, domyślne)",
|
||||
"full": "Pełny katalog",
|
||||
"both": "Oba (wybór na agenta)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "Aktualizacja"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Bezpośredni dostęp z maszyny Home Assistant: {url}",
|
||||
"connect_local_lan": "Lokalnie/LAN (gdy dostęp do sieci to \"Local network\"): {url}",
|
||||
"connect_remote_url": "Zdalny URL połączenia: {url}",
|
||||
"connect_urls_label": "URL(e) połączenia:",
|
||||
"connect_urls_pending": "URL-e połączenia pojawią się tutaj (i w logu Home Assistant) po uruchomieniu serwera.",
|
||||
"connect_webhook_disabled": "Zdalny dostęp przez webhook jest wyłączony (tryb tylko lokalny).",
|
||||
"oauth_creds_pending": "Client ID i Client Secret pojawią się tutaj po uruchomieniu serwera.",
|
||||
"oauth_not_serving": "Legacy OAuth jeszcze ich nie udostępnia — uruchom ponownie Home Assistanta, gdy pojawi się taka prośba, aby je aktywować.",
|
||||
"oauth_select_legacy_mode": "Ustaw tryb uwierzytelniania na legacy OAuth powyżej i zapisz, aby wygenerować Client ID i Client Secret.",
|
||||
"panel_hint": "Otwórz [panel ustawień HA-MCP](/ha-mcp) do zarządzania narzędziami i ustawieniami serwera.",
|
||||
"tools_module_installed": "Moduł narzędzi beta/zaawansowanych plików i YAML (opcjonalny): Zainstalowany",
|
||||
"tools_module_not_installed": "Moduł narzędzi beta/zaawansowanych plików i YAML (opcjonalny): Niezainstalowany — naciśnij „Dodaj wpis” na stronie tej integracji i wybierz „HA-MCP File & YAML Tools”, aby go dodać",
|
||||
"tools_module_not_loaded": "Moduł narzędzi beta/zaawansowanych plików i YAML (opcjonalny): Zainstalowany, ale niezaładowany — włącz lub przeładuj wpis „HA-MCP File & YAML Tools” na stronie tej integracji",
|
||||
"version_line": "Komponent {component_version} - Serwer ha-mcp {server_version} (kanał {channel})",
|
||||
"version_not_installed": "jeszcze niezainstalowany",
|
||||
"version_unknown": "nieznany"
|
||||
}
|
||||
}
|
||||
@@ -1,161 +1,168 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Пользовательский компонент HA-MCP",
|
||||
"description": "Выберите, что добавить. **Сервер HA-MCP** запускает полноценный сервер ha-mcp внутри Home Assistant и предоставляет к нему доступ через вебхук Home Assistant — этот вариант установки подходит большинству пользователей. Это автономный сервер, полностью заменяющий все остальные способы установки (дополнение, Docker, uvx/PyPI, stdio); если он запущен, не запускайте одновременно ни один из них. **Файловые инструменты и инструменты YAML HA-MCP** добавляют привилегированные службы для работы с файлами и редактирования YAML. Они нужны, только если вы включите отключённые по умолчанию файловые инструменты и инструменты YAML в ha-mcp. Если сервер ha-mcp уже работает в другом месте (как дополнение, в Docker или через uvx), добавлять серверную запись не нужно — добавьте только эту запись для файлов и YAML и только в том случае, если используете эти инструменты. Она работает с сервером любого типа, и её можно добавить позднее в любое время.",
|
||||
"menu_options": {
|
||||
"server": "Сервер HA-MCP (рекомендуется)",
|
||||
"tools": "Файловые инструменты и инструменты YAML HA-MCP (необязательно)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "Файловые инструменты и инструменты YAML HA-MCP",
|
||||
"description": "Настраивает привилегированные службы для работы с файлами и конфигурацией YAML. Требуется, только если вы включаете отключённые по умолчанию файловые инструменты и инструменты редактирования YAML в ha-mcp (флаги функций). Это относится к серверу любого типа, в том числе к внутрипроцессному серверу HA-MCP. Эту запись можно добавить или удалить в любое время."
|
||||
},
|
||||
"server": {
|
||||
"title": "Сервер HA-MCP",
|
||||
"description": "Запускает полноценный сервер ha-mcp внутри Home Assistant и предоставляет удалённый доступ к нему через вебхук Home Assistant (доступный через Nabu Casa или любой обратный прокси-сервер). Это автономный сервер, полностью заменяющий способы установки в виде дополнения, Docker, uvx/PyPI и stdio; не запускайте его одновременно с другим сервером ha-mcp. Нажмите **Отправить**, чтобы запустить его. После этого порт, сетевой интерфейс и способ аутентификации можно изменить в параметрах интеграции."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Эта запись уже настроена.",
|
||||
"unsupported_home_assistant": "Для внутрипроцессного сервера HA-MCP требуется Home Assistant версии {required} или новее, но в этом экземпляре установлена версия {installed}. Вы по-прежнему можете использовать запись «HA-MCP File & YAML Tools» этого компонента с внешним сервером ha-mcp, работающим как дополнение или контейнер Docker."
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Пользовательский компонент HA-MCP",
|
||||
"description": "Выберите, что добавить. **HA-MCP Server** запускает полноценный сервер ha-mcp внутри Home Assistant и предоставляет к нему доступ через веб-хук Home Assistant — это вариант установки, который подходит большинству пользователей. Это автономный сервер и полноценная замена для всех остальных способов установки: дополнение (app/add-on), Docker, uvx/PyPI, stdio; если вы используете его, не запускайте ничего из этого параллельно. **HA-MCP File & YAML Tools** выполняет две роли. Если ваш сервер ha-mcp работает вне Home Assistant (дополнение, Docker, uvx/PyPI, stdio), эта запись также наделяет такой сервер возможностями внутрипроцессного компонента, недоступными извне (быстрый внутрипроцессный поиск, метаданные записей конфигурации, чтение из реестра), поэтому ее стоит добавить к каждой внешней установке. Она также выступает в роли шлюза для привилегированных служб редактирования файлов и YAML, используемых инструментами работы с файлами/YAML от ha-mcp (по умолчанию отключены). Если вместо этого вы добавите запись HA-MCP Server, она уже предоставит эти общие возможности, поэтому вторая запись потребуется только в качестве шлюза для дополнительных инструментов работы с файлами и YAML. Она работает с серверами любого типа и может быть добавлена в любой момент.",
|
||||
"menu_options": {
|
||||
"server": "Сервер HA-MCP (рекомендуется)",
|
||||
"tools": "Файловые инструменты и инструменты YAML HA-MCP (необязательно)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "Файловые инструменты и инструменты YAML HA-MCP",
|
||||
"description": "Настраивает привилегированные службы редактирования конфигурационных файлов и YAML, которые остаются заблокированными за дополнительными инструментами работы с файлами/YAML от ha-mcp (управляются флагами функций, по умолчанию отключены) — это относится к серверам любого типа, включая встроенный HA-MCP Server. Если ваш сервер ha-mcp работает вне Home Assistant, эта запись также предоставляет ему возможности внутрипроцессного компонента, недоступные извне (быстрый внутрипроцессный поиск, метаданные записей конфигурации, чтение из реестра), которые полезны даже при отключенных инструментах работы с файлами/YAML. Вы можете добавить или удалить эту запись в любое время."
|
||||
},
|
||||
"server": {
|
||||
"title": "Сервер HA-MCP",
|
||||
"description": "Запускает полный сервер ha-mcp внутри Home Assistant и предоставляет к нему удаленный доступ через веб-хук Home Assistant (доступен через Nabu Casa или любой обратный прокси-сервер). Это автономный сервер и полноценная замена для способов установки через дополнение (app/add-on), Docker, uvx/PyPI и stdio — не запускайте его параллельно с другим сервером ha-mcp. Нажмите **Отправить**, чтобы запустить его; в дальнейшем вы сможете изменить порт, привязку и аутентификацию в настройках интеграции."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "Файловые инструменты и инструменты YAML HA-MCP",
|
||||
"description": "Настройка привилегированных служб для работы с файлами и редактирования YAML, которые используются отключёнными по умолчанию файловыми инструментами и инструментами YAML в ha-mcp. Эти параметры также отображаются в собственном интерфейсе настроек сервера ha-mcp и применяются сразу, без перезапуска. Неотключаемый базовый запрет по-прежнему блокирует чувствительные пути (например, .storage) и ключи границы доверия (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Дополнительные файловые каталоги",
|
||||
"extra_yaml_keys": "Дополнительные YAML-ключи для записи"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Каталоги относительно каталога конфигурации, каждому предоставляется доступ на чтение и запись (например, pyscript). Записи с обходом пути и вне каталога конфигурации отбрасываются.",
|
||||
"extra_yaml_keys": "Верхнеуровневые ключи, которые ha_config_set_yaml может записывать вдобавок к встроенным, — для YAML-ориентированных интеграций на этой установке (например, alert2). Ключи границы доверия отбрасываются."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "Сервер HA-MCP",
|
||||
"description": "Настройте сервер HA-MCP. {panel_hint}Изменения применяются при сохранении.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "Канал выпуска",
|
||||
"auto_update": "Автоматические обновления сервера",
|
||||
"server_port": "Порт прослушивания MCP-сервера",
|
||||
"bind_host": "Сетевой доступ",
|
||||
"webhook_auth": "Режим аутентификации",
|
||||
"oauth_client_id_override": "Устаревший OAuth: пользовательский Client ID (необязательно)",
|
||||
"oauth_client_secret_override": "Устаревший OAuth: пользовательский Client Secret (необязательно)",
|
||||
"oauth_regenerate": "Устаревший OAuth: создать новые Client ID и Secret сейчас",
|
||||
"pip_spec": "Для разработчиков: переопределение пакета ha-mcp",
|
||||
"server_url": "URL-адрес Home Assistant (расширенная настройка)",
|
||||
"external_url": "Внешний URL-адрес (необязательно)",
|
||||
"webhook_id_override": "Пользовательский секрет вебхука (необязательно)",
|
||||
"secret_path_override": "Пользовательский путь прямого доступа (необязательно)",
|
||||
"regenerate_secrets": "Создать новые секреты подключения сейчас",
|
||||
"enable_webhook": "Удалённый доступ через вебхук",
|
||||
"enable_llm_api": "LLM API диалогового агента",
|
||||
"llm_api_exposure": "Набор инструментов для диалогового агента",
|
||||
"enable_startup_notification": "Уведомление о запуске",
|
||||
"enable_sidebar_panel": "Панель настроек в боковой панели"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "Стабильный канал устанавливает последнюю стабильную версию, а канал разработки — самую новую сборку для разработки. Если автоматические обновления включены, последняя сборка выбранного канала устанавливается при перезагрузке интеграции или перезапуске, а также во время периодической проверки. Указанное ниже переопределение пакета для разработчиков имеет приоритет и отключает автоматические обновления.",
|
||||
"auto_update": "Если параметр включён, последняя версия выбранного канала устанавливается автоматически при перезагрузке интеграции или перезапуске, а также во время периодической проверки. Если параметр выключен, сервер остаётся на текущей установленной версии, пока вы снова не включите его. Это относится только к пакету сервера ha-mcp; обновления самого пользовательского компонента HA-MCP по-прежнему устанавливаются через HACS.",
|
||||
"server_port": "Порт, который прослушивает этот сервер. Дополнение ha-mcp использует порт 9583, поэтому по умолчанию выбран порт 9584, чтобы оба сервера могли работать одновременно. Если дополнение не используется, подойдёт любой свободный порт.",
|
||||
"bind_host": "Определяет, кто может подключаться непосредственно к порту MCP-сервера. Значение по умолчанию соответствует дополнению: сервер доступен в локальной сети, а секретный путь используется как учётные данные. Выберите loopback, чтобы разрешить подключения только с самого компьютера Home Assistant. URL-адрес вебхука и панель в боковом меню будут работать в любом случае.",
|
||||
"webhook_auth": "Определяет, как MCP-клиенты подтверждают свою подлинность при обращении к URL-адресу вебхука. При использовании секретного URL-адреса учётными данными служит сама ссылка. При использовании входа через Home Assistant такие клиенты, как claude.ai, входят с учётной записью администратора Home Assistant (OAuth). При использовании устаревшего OAuth интеграция выдаёт собственные Client ID и Secret, которые нужно вставить в клиенты, где они требуются, например Google Gemini Spark. Для включения или выключения этого режима необходимо перезапустить Home Assistant.",
|
||||
"oauth_client_id_override": "Заменяет автоматически созданный Client ID устаревшего OAuth. Оставьте поле пустым, чтобы сохранить текущее значение. Используется, только когда выбран режим аутентификации «Устаревший OAuth».",
|
||||
"oauth_client_secret_override": "Заменяет автоматически созданный Client Secret устаревшего OAuth. Оставьте поле пустым, чтобы сохранить текущее значение. Используется, только когда выбран режим аутентификации «Устаревший OAuth».",
|
||||
"oauth_regenerate": "Одноразовое действие: создаёт новые Client ID и Secret для режима устаревшего OAuth. Они начнут действовать только после перезапуска Home Assistant, который будет предложен в уведомлении о восстановлении. До перезапуска прежние Client ID и Secret продолжат работать, а новые работать не будут. Также очищает два поля переопределения выше.",
|
||||
"pip_spec": "Оставьте пустым. Предназначено только для тестирования определённой сборки ha-mcp (например, для фиксации предварительной версии). Переопределяет канал выпуска и отключает автоматические обновления, пока поле не будет очищено.",
|
||||
"server_url": "URL-адрес, по которому внутрипроцессный сервер обращается к Home Assistant (обычно к этому же экземпляру). Оставьте пустым, чтобы сформировать адрес из порта и настроек SSL этого экземпляра. Указывайте значение только в том случае, если сервер должен использовать другой маршрут.",
|
||||
"external_url": "Отображается как основной URL-адрес подключения. Используйте его, если Home Assistant доступен через собственный домен или обратный прокси-сервер. Введите полный базовый адрес, включая схему. Он должен указывать непосредственно на Home Assistant: при открытии в браузере должна появляться страница входа HA. Адрес не должен содержать порт, например :8123 или любой другой, иначе удалённые MCP-клиенты не смогут подключиться. Оставьте поле пустым, чтобы автоматически использовать Nabu Casa или локальный адрес.",
|
||||
"webhook_id_override": "Заменяет случайный секрет вебхука в URL-адресе подключения (/api/webhook/...). URL-адрес служит учётными данными, поэтому используйте длинное значение, которое трудно угадать. Оставьте поле пустым, чтобы сохранить текущее значение.",
|
||||
"secret_path_override": "Заменяет случайный путь для прямого доступа через порт сервера. Действует то же правило: путь служит учётными данными. Оставьте поле пустым, чтобы сохранить текущее значение.",
|
||||
"regenerate_secrets": "Одноразовое действие: создаёт новый случайный секрет вебхука и путь прямого доступа, немедленно делая прежние URL-адреса подключения недействительными. Также очищает два поля переопределения выше.",
|
||||
"enable_webhook": "Выключите для работы только в локальном режиме: вебхук Home Assistant вообще не будет зарегистрирован, поэтому ничто, включая Nabu Casa, не сможет обратиться к серверу через Home Assistant. Прямой порт сервера и панель в боковом меню продолжат работать.",
|
||||
"enable_llm_api": "Предоставляет полный набор инструментов диалоговым агентам Home Assistant (OpenAI, Google, Ollama и другим). Пока параметр включён, агенты могут выбрать «HA-MCP Server» в разделе управления Home Assistant и использовать инструменты из чата и голосового интерфейса Assist. Включение лишь делает его доступным для выбора: ничего не предоставляется, пока вы не выберете его в настройках агента. Руководство по использованию: {llm_api_docs_url}",
|
||||
"llm_api_exposure": "Определяет вид набора инструментов, предоставляемого диалоговым агентам. Поиск инструментов (по умолчанию) уменьшает контекст агента: используется компактный API с закреплёнными инструментами и метаинструментами поиска и выполнения. Полный каталог перечисляет все доступные инструменты напрямую и лучше подходит моделям с большим контекстом. Вариант «Оба» регистрирует оба набора параллельно, чтобы каждый агент мог выбрать свой в разделе управления Home Assistant. Доступность отдельных инструментов настраивается в панели HA-MCP; подробности: {llm_api_docs_url}",
|
||||
"enable_startup_notification": "Показывает уведомление при каждом запуске сервера со ссылками на доступные только администраторам страницы настроек. Выключите, чтобы запускать сервер без уведомлений. URL-адреса подключения всё равно будут записываться в журнал Home Assistant.",
|
||||
"enable_sidebar_panel": "Показывает панель настроек HA-MCP в боковом меню (только для администраторов). Выключите, чтобы убрать пункт из бокового меню. Параметры сервера останутся доступны на этом экране."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "Не удалось запустить внутрипроцессный сервер HA-MCP",
|
||||
"description": "Не удалось запустить внутрипроцессный сервер HA-MCP внутри Home Assistant:\n\n{detail}\n\nПроверьте журналы Home Assistant, затем перезагрузите интеграцию (либо устраните основную проблему и перезагрузите её), чтобы повторить попытку."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "Не удалось установить пакет внутрипроцессного сервера HA-MCP",
|
||||
"description": "Не удалось установить пакет ha-mcp для внутрипроцессного сервера:\n\n{detail}\n\nУстраните описанную выше проблему совместимости или установки, затем перезагрузите интеграцию, чтобы повторить попытку."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "Обновите пользовательский компонент HA-MCP через HACS",
|
||||
"description": "Для установленного сервера ha-mcp требуется пользовательский компонент HA-MCP версии {required} или новее, но у вас установлена версия {installed}. Обновите компонент через HACS (откройте запись пользовательского компонента HA-MCP и выберите «Обновить информацию», если обновление ещё не отображается) и перезапустите Home Assistant. Тем временем сервер продолжит работать, но некоторые новые функции могут не работать до обновления компонента."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "Обновление сервера HA-MCP ожидает обновления компонента",
|
||||
"description": "Доступен сервер ha-mcp версии {latest}, но в этом выпуске также обновлён пользовательский компонент HA-MCP (до версии {shipped}; у вас работает версия {running}). Чтобы не запускать версию сервера, которая не тестировалась с работающим компонентом, автоматическое обновление сервера приостановлено до обновления компонента.\n\nОбновите компонент через HACS (откройте запись пользовательского компонента HA-MCP и выберите «Обновить информацию», если обновление ещё не отображается), затем перезапустите Home Assistant. После этого обновление сервера установится автоматически. Чтобы всё равно установить обновление сервера, нажмите «Установить» у сущности обновления сервера HA-MCP."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Компонент установлен из устаревшего репозитория",
|
||||
"description": "HACS отслеживает для этого компонента основной репозиторий сервера ha-mcp, поэтому здесь отображаются номера версий сервера (7.x) и примечания к его выпускам вместо собственных версий компонента (1.x). Обновления продолжают работать, но будут и дальше обозначаться неверно. Чтобы исправить это, удалите этот репозиторий из HACS (настройки интеграции и записи конфигурации сохранятся), добавьте homeassistant-ai/ha-mcp-integration как пользовательский репозиторий, переустановите из него компонент и перезапустите Home Assistant."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Перезапустите Home Assistant, чтобы применить изменение устаревшего OAuth",
|
||||
"description": "Режим аутентификации с устаревшим OAuth регистрирует собственные веб-точки /authorize и /token, которые Home Assistant может привязать или освободить только при полном перезапуске — независимо от того, включили вы этот режим, выключили его или изменили Client ID/Secret. Перезапустите Home Assistant («Настройки» → «Система» → «Перезапустить»), чтобы применить изменение. До этого продолжит действовать прежнее поведение."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "Стабильный (рекомендуется)",
|
||||
"dev": "Для разработки (последняя сборка)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "Секретный URL-адрес вебхука (по умолчанию)",
|
||||
"ha_auth": "Вход через Home Assistant (OAuth)",
|
||||
"legacy": "Устаревший OAuth (Client ID/Secret, для Google Gemini Spark)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "Поиск инструментов (компактный, по умолчанию)",
|
||||
"full": "Полный каталог",
|
||||
"both": "Оба (выбор для каждого агента)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "Обновление"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Прямой доступ с компьютера Home Assistant: {url}",
|
||||
"connect_local_lan": "Локально/LAN (когда сетевой доступ — «Local network»): {url}",
|
||||
"connect_remote_url": "URL-адрес удалённого подключения: {url}",
|
||||
"connect_urls_label": "URL-адреса подключения:",
|
||||
"connect_urls_pending": "URL-адреса подключения появятся здесь (и в журнале Home Assistant) после запуска сервера.",
|
||||
"connect_webhook_disabled": "Удалённый доступ через вебхук отключён (только локальный режим).",
|
||||
"oauth_creds_pending": "Client ID и Client Secret появятся здесь после запуска сервера.",
|
||||
"oauth_not_serving": "Устаревший OAuth пока их не обслуживает — перезапустите Home Assistant, когда он об этом попросит, чтобы активировать их.",
|
||||
"oauth_select_legacy_mode": "Установите режим аутентификации выше на устаревший OAuth и сохраните, чтобы создать Client ID и Client Secret.",
|
||||
"panel_hint": "Откройте [панель настроек HA-MCP](/ha-mcp) для управления инструментами и настройками сервера.",
|
||||
"tools_module_installed": "Бета-модуль расширенных инструментов для файлов и YAML (необязательно): Установлен",
|
||||
"tools_module_not_installed": "Бета-модуль расширенных инструментов для файлов и YAML (необязательно): Не установлен — нажмите «Добавить запись» на странице этой интеграции и выберите «HA-MCP File & YAML Tools»",
|
||||
"tools_module_not_loaded": "Бета-модуль расширенных инструментов для файлов и YAML (необязательно): Установлен, но не загружен — включите или перезагрузите запись «HA-MCP File & YAML Tools» на странице этой интеграции",
|
||||
"version_line": "Компонент {component_version} - Сервер ha-mcp {server_version} (канал {channel})",
|
||||
"version_not_installed": "ещё не установлен",
|
||||
"version_unknown": "неизвестен"
|
||||
"abort": {
|
||||
"already_configured": "Эта запись уже настроена.",
|
||||
"unsupported_home_assistant": "Для встроенного сервера HA-MCP требуется Home Assistant версии {required} или новее, но на этом экземпляре запущенна версия {installed}. Вы по-прежнему можете использовать запись HA-MCP File & YAML Tools этой компоненты с внешним сервером ha-mcp, запущенным в качестве дополнения (app/add-on) или контейнера Docker."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "Файловые инструменты и инструменты YAML HA-MCP",
|
||||
"description": "Настройка привилегированных служб для работы с файлами и редактирования YAML, которые используются отключёнными по умолчанию файловыми инструментами и инструментами YAML в ha-mcp. Эти параметры также отображаются в собственном интерфейсе настроек сервера ha-mcp и применяются сразу, без перезапуска. Неотключаемый базовый запрет по-прежнему блокирует чувствительные пути (например, .storage) и ключи границы доверия (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Дополнительные файловые каталоги",
|
||||
"extra_yaml_keys": "Дополнительные YAML-ключи для записи"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Каталоги относительно каталога конфигурации, каждому предоставляется доступ на чтение и запись (например, pyscript). Записи с обходом пути и вне каталога конфигурации отбрасываются.",
|
||||
"extra_yaml_keys": "Верхнеуровневые ключи, которые ha_config_set_yaml может записывать вдобавок к встроенным, — для YAML-ориентированных интеграций на этой установке (например, alert2). Ключи границы доверия отбрасываются."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "Сервер HA-MCP",
|
||||
"description": "Настройте сервер HA-MCP. {panel_hint}Изменения применяются при сохранении.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "Канал выпуска",
|
||||
"auto_update": "Автоматические обновления сервера",
|
||||
"server_port": "Порт прослушивания MCP-сервера",
|
||||
"bind_host": "Сетевой доступ",
|
||||
"webhook_auth": "Режим аутентификации",
|
||||
"oauth_client_id_override": "Устаревший OAuth: пользовательский Client ID (необязательно)",
|
||||
"oauth_client_secret_override": "Устаревший OAuth: пользовательский Client Secret (необязательно)",
|
||||
"oauth_regenerate": "Устаревший OAuth: создать новые Client ID и Secret сейчас",
|
||||
"pip_spec": "Для разработчиков: переопределение пакета ha-mcp",
|
||||
"server_url": "URL-адрес Home Assistant (расширенная настройка)",
|
||||
"external_url": "Внешний URL-адрес (необязательно)",
|
||||
"webhook_id_override": "Пользовательский секрет вебхука (необязательно)",
|
||||
"secret_path_override": "Пользовательский путь прямого доступа (необязательно)",
|
||||
"regenerate_secrets": "Создать новые секреты подключения сейчас",
|
||||
"enable_webhook": "Удалённый доступ через вебхук",
|
||||
"enable_llm_api": "LLM API диалогового агента",
|
||||
"llm_api_exposure": "Набор инструментов для диалогового агента",
|
||||
"enable_startup_notification": "Уведомление о запуске",
|
||||
"enable_sidebar_panel": "Панель настроек в боковой панели"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "Стабильный канал устанавливает последнюю стабильную версию, а канал разработки — самую новую сборку для разработки. Если автоматические обновления включены, последняя сборка выбранного канала устанавливается при перезагрузке интеграции или перезапуске, а также во время периодической проверки. Указанное ниже переопределение пакета для разработчиков имеет приоритет и отключает автоматические обновления.",
|
||||
"auto_update": "Если параметр включён, последняя версия выбранного канала устанавливается автоматически при перезагрузке интеграции или перезапуске, а также во время периодической проверки. Если параметр выключен, сервер остаётся на текущей установленной версии, пока вы снова не включите его. Это относится только к пакету сервера ha-mcp; обновления самого пользовательского компонента HA-MCP по-прежнему устанавливаются через HACS.",
|
||||
"server_port": "Порт, на котором прослушивает этот сервер. Дополнение (app/add-on) ha-mcp использует порт 9583, поэтому по умолчанию установлено значение 9584, чтобы они могли работать одновременно. Если вы не используете дополнение, подойдет любой свободный порт.",
|
||||
"bind_host": "Кто может подключаться напрямую к порту MCP-сервера. Значение по умолчанию соответствует дополнению (app/add-on): доступ из вашей локальной сети, где секретный путь выступает в роли учетных данных. Выберите loopback, чтобы разрешить подключения только с самого компьютера Home Assistant — URL веб-хука и панель бокового меню будут работать в любом случае.",
|
||||
"webhook_auth": "Как клиенты MCP подтверждают свою подлинность по URL-адресу веб-хука. С секретным URL-адресом сама ссылка является единственным учётным данным — OAuth по-прежнему работает для клиентов, которые его используют, но каждый вход одобряется автоматически: не требуется ни логин, ни Client ID, ни Secret. При входе через Home Assistant клиенты, такие как claude.ai, входят в систему с учётной записью администратора Home Assistant (OAuth). При использовании устаревшего OAuth эта интеграция запускает собственную службу входа и выдаёт Client ID и Secret для вставки в клиенты, которым они требуются — для полного включения или выключения может потребоваться перезапуск Home Assistant.",
|
||||
"oauth_client_id_override": "Заменяет автоматически созданный Client ID устаревшего OAuth. Оставьте поле пустым, чтобы сохранить текущее значение. Используется, только когда выбран режим аутентификации «Устаревший OAuth».",
|
||||
"oauth_client_secret_override": "Заменяет автоматически созданный Client Secret устаревшего OAuth. Оставьте поле пустым, чтобы сохранить текущее значение. Используется, только когда выбран режим аутентификации «Устаревший OAuth».",
|
||||
"oauth_regenerate": "Одноразовое действие: создаёт новые Client ID и Secret для режима устаревшего OAuth. Они начнут действовать только после перезапуска Home Assistant, который будет предложен в уведомлении о восстановлении. До перезапуска прежние Client ID и Secret продолжат работать, а новые работать не будут. Также очищает два поля переопределения выше.",
|
||||
"pip_spec": "Оставьте пустым. Предназначено только для тестирования определённой сборки ha-mcp (например, для фиксации предварительной версии). Переопределяет канал выпуска и отключает автоматические обновления, пока поле не будет очищено.",
|
||||
"server_url": "URL-адрес, по которому внутрипроцессный сервер обращается к Home Assistant (обычно к этому же экземпляру). Оставьте пустым, чтобы сформировать адрес из порта и настроек SSL этого экземпляра. Указывайте значение только в том случае, если сервер должен использовать другой маршрут.",
|
||||
"external_url": "Отображается как основной URL-адрес подключения. Используйте его, если Home Assistant доступен через собственный домен или обратный прокси-сервер. Введите полный базовый адрес, включая схему. Он должен указывать непосредственно на Home Assistant: при открытии в браузере должна появляться страница входа HA. Адрес не должен содержать порт, например :8123 или любой другой, иначе удалённые MCP-клиенты не смогут подключиться. Оставьте поле пустым, чтобы автоматически использовать Nabu Casa или локальный адрес.",
|
||||
"webhook_id_override": "Заменяет случайный секрет вебхука в URL-адресе подключения (/api/webhook/...). URL-адрес служит учётными данными, поэтому используйте длинное значение, которое трудно угадать. Оставьте поле пустым, чтобы сохранить текущее значение.",
|
||||
"secret_path_override": "Заменяет случайный путь для прямого доступа через порт сервера. Действует то же правило: путь служит учётными данными. Оставьте поле пустым, чтобы сохранить текущее значение.",
|
||||
"regenerate_secrets": "Одноразовое действие: создаёт новый случайный секрет вебхука и путь прямого доступа, немедленно делая прежние URL-адреса подключения недействительными. Также очищает два поля переопределения выше.",
|
||||
"enable_webhook": "Выключите для работы только в локальном режиме: вебхук Home Assistant вообще не будет зарегистрирован, поэтому ничто, включая Nabu Casa, не сможет обратиться к серверу через Home Assistant. Прямой порт сервера и панель в боковом меню продолжат работать.",
|
||||
"enable_llm_api": "Предоставляет полный набор инструментов диалоговым агентам Home Assistant (OpenAI, Google, Ollama и другим). Пока параметр включён, агенты могут выбрать «HA-MCP Server» в разделе управления Home Assistant и использовать инструменты из чата и голосового интерфейса Assist. Включение лишь делает его доступным для выбора: ничего не предоставляется, пока вы не выберете его в настройках агента. Руководство по использованию: {llm_api_docs_url}",
|
||||
"llm_api_exposure": "Определяет вид набора инструментов, предоставляемого диалоговым агентам. Поиск инструментов (по умолчанию) уменьшает контекст агента: используется компактный API с закреплёнными инструментами и метаинструментами поиска и выполнения. Полный каталог перечисляет все доступные инструменты напрямую и лучше подходит моделям с большим контекстом. Вариант «Оба» регистрирует оба набора параллельно, чтобы каждый агент мог выбрать свой в разделе управления Home Assistant. Доступность отдельных инструментов настраивается в панели HA-MCP; подробности: {llm_api_docs_url}",
|
||||
"enable_startup_notification": "Показывает уведомление при каждом запуске сервера со ссылками на доступные только администраторам страницы настроек. Выключите, чтобы запускать сервер без уведомлений. URL-адреса подключения всё равно будут записываться в журнал Home Assistant.",
|
||||
"enable_sidebar_panel": "Показывает панель настроек HA-MCP в боковом меню (только для администраторов). Выключите, чтобы убрать пункт из бокового меню. Параметры сервера останутся доступны на этом экране."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "Не удалось запустить внутрипроцессный сервер HA-MCP",
|
||||
"description": "Не удалось запустить внутрипроцессный сервер HA-MCP внутри Home Assistant:\n\n{detail}\n\nПроверьте журналы Home Assistant, затем перезагрузите интеграцию (либо устраните основную проблему и перезагрузите её), чтобы повторить попытку."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "Не удалось установить пакет внутрипроцессного сервера HA-MCP",
|
||||
"description": "Не удалось установить пакет ha-mcp для внутрипроцессного сервера:\n\n{detail}\n\nУстраните описанную выше проблему совместимости или установки, затем перезагрузите интеграцию, чтобы повторить попытку."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "Обновите пользовательский компонент HA-MCP через HACS",
|
||||
"description": "Для установленного сервера ha-mcp требуется пользовательский компонент HA-MCP версии {required} или новее, но у вас установлена версия {installed}. Обновите компонент через HACS (откройте запись пользовательского компонента HA-MCP и выберите «Обновить информацию», если обновление ещё не отображается) и перезапустите Home Assistant. Тем временем сервер продолжит работать, но некоторые новые функции могут не работать до обновления компонента."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "Обновление сервера HA-MCP ожидает обновления компонента",
|
||||
"description": "Доступен сервер ha-mcp версии {latest}, но в этом выпуске также обновлён пользовательский компонент HA-MCP (до версии {shipped}; у вас работает версия {running}). Чтобы не запускать версию сервера, которая не тестировалась с работающим компонентом, автоматическое обновление сервера приостановлено до обновления компонента.\n\nОбновите компонент через HACS (откройте запись пользовательского компонента HA-MCP и выберите «Обновить информацию», если обновление ещё не отображается), затем перезапустите Home Assistant. После этого обновление сервера установится автоматически. Чтобы всё равно установить обновление сервера, нажмите «Установить» у сущности обновления сервера HA-MCP."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Компонент установлен из устаревшего репозитория",
|
||||
"description": "HACS отслеживает основной репозиторий сервера ha-mcp для этого компонента, поэтому HACS показывает здесь номера версий сервера и примечания к выпуску вместо собственных (2.x) для компонента. Обновления продолжают работать, но остаются неправильно помеченными. Чтобы исправить: удалите этот репозиторий из HACS (ваши настройки интеграции и записи конфигурации сохраняются), добавьте homeassistant-ai/ha-mcp-integration как пользовательский репозиторий, переустановите компонент из него и перезапустите Home Assistant."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Перезапустите Home Assistant, чтобы применить устаревшие настройки OAuth",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Перезапустить Home Assistant?",
|
||||
"description": "Отправка этой формы перезапустит Home Assistant и применит устаревшие настройки OAuth.\n\nНажмите <strong>Submit</strong>, чтобы перезапустить сейчас."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "Стабильный (рекомендуется)",
|
||||
"dev": "Для разработки (последняя сборка)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "Секретный URL-адрес вебхука (по умолчанию)",
|
||||
"ha_auth": "Вход через Home Assistant (OAuth)",
|
||||
"legacy": "Устаревший OAuth (эта интеграция выдаёт Client ID/Secret для вставки)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "Поиск инструментов (компактный, по умолчанию)",
|
||||
"full": "Полный каталог",
|
||||
"both": "Оба (выбор для каждого агента)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "Обновление"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Прямой доступ с компьютера Home Assistant: {url}",
|
||||
"connect_local_lan": "Локально/LAN (когда сетевой доступ — «Local network»): {url}",
|
||||
"connect_remote_url": "URL-адрес удалённого подключения: {url}",
|
||||
"connect_urls_label": "URL-адреса подключения:",
|
||||
"connect_urls_pending": "URL-адреса подключения появятся здесь (и в журнале Home Assistant) после запуска сервера.",
|
||||
"connect_webhook_disabled": "Удалённый доступ через вебхук отключён (только локальный режим).",
|
||||
"oauth_creds_pending": "Client ID и Client Secret появятся здесь после запуска сервера.",
|
||||
"oauth_not_serving": "Устаревший OAuth пока их не обслуживает — перезапустите Home Assistant, когда он об этом попросит, чтобы активировать их.",
|
||||
"oauth_select_legacy_mode": "Установите режим аутентификации выше на устаревший OAuth и сохраните, чтобы создать Client ID и Client Secret.",
|
||||
"panel_hint": "Откройте [панель настроек HA-MCP](/ha-mcp) для управления инструментами и настройками сервера.",
|
||||
"tools_module_installed": "Бета-модуль расширенных инструментов для файлов и YAML (необязательно): Установлен",
|
||||
"tools_module_not_installed": "Бета-модуль расширенных инструментов для файлов и YAML (необязательно): Не установлен — нажмите «Добавить запись» на странице этой интеграции и выберите «HA-MCP File & YAML Tools»",
|
||||
"tools_module_not_loaded": "Бета-модуль расширенных инструментов для файлов и YAML (необязательно): Установлен, но не загружен — включите или перезагрузите запись «HA-MCP File & YAML Tools» на странице этой интеграции",
|
||||
"version_line": "Компонент {component_version} - Сервер ha-mcp {server_version} (канал {channel})",
|
||||
"version_not_installed": "ещё не установлен",
|
||||
"version_unknown": "неизвестен"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "HA-MCP Anpassad Komponent",
|
||||
"description": "Välj vad som ska läggas till. **HA-MCP Server** kör hela ha-mcp-servern inne i Home Assistant och exponerar den via en Home Assistant-webhook – detta är installationen som de flesta vill ha. Den är en fristående server och en komplett ersättning för alla andra installationsmetoder: App (tillägg), Docker, uvx/PyPI, stdio; om du kör den, kör inte någon av dem samtidigt. **HA-MCP File & YAML Tools** har två roller. Om din ha-mcp-server körs utanför Home Assistant – app, Docker, uvx/PyPI, stdio – ger denna post även den servern de inbyggda komponentfunktioner den inte kan nå utifrån (snabb inbyggd sökning, metadata för konfigurationsposter, registerbaserade läsningar), så den är värd att lägga till för varje extern installation, och den fungerar som spärr för de privilegierade fil- och YAML-redigeringstjänsterna som används av ha-mcp:s valbara fil-/YAML-verktyg. Om du lägger till posten HA-MCP Server i stället, tillhandahåller den posten redan dessa delade funktioner, så denna andra post behövs då endast som spärr för de valbara fil-/YAML-verktygen. Den fungerar med alla servertyper och kan läggas till senare när som helst.",
|
||||
"menu_options": {
|
||||
"server": "HA-MCP Server (rekommenderas)",
|
||||
"tools": "HA-MCP Fil- & YAML-verktyg (valfritt)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "HA-MCP Fil- & YAML-verktyg",
|
||||
"description": "Konfigurerar de privilegierade fil- och YAML-konfigurationstjänsterna, som förblir spärrade bakom ha-mcp:s valbara fil-/YAML-redigeringsverktyg (funktionsflaggor, av som standard) – detta gäller för alla servertyper, inklusive den inbyggda HA-MCP Server. Om din ha-mcp-server körs utanför Home Assistant ger denna post den även de inbyggda komponentfunktioner den inte kan nå utifrån (snabb inbyggd sökning, metadata för konfigurationsposter, registerbaserade läsningar), vilka är värda att ha även när fil-/YAML-verktygen är avstängda. Du kan lägga till eller ta bort denna post när som helst."
|
||||
},
|
||||
"server": {
|
||||
"title": "HA-MCP Server",
|
||||
"description": "Detta kör hela ha-mcp-servern inne i Home Assistant och exponerar den på distans via en Home Assistant-webhook (nås via Nabu Casa eller vilken omvänd proxy som helst). Den är en fristående server och en komplett ersättning för installationsmetoderna app (tillägg), Docker, uvx/PyPI och stdio – kör den inte tillsammans med en annan ha-mcp-server. Välj **Skicka** för att starta den; du kan ändra port, bindning och autentisering i efterhand i integreringsalternativen."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Denna post är redan konfigurerad.",
|
||||
"unsupported_home_assistant": "Den inbyggda HA-MCP-servern kräver Home Assistant {required} eller nyare, men denna instans kör {installed}. Du kan fortfarande använda denna komponents post för HA-MCP File & YAML Tools med en extern ha-mcp-server som körs som en app (tillägg) eller Docker-behållare."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "HA-MCP Fil- & YAML-verktyg",
|
||||
"description": "Konfigurera de privilegierade fil- och YAML-redigeringstjänsterna som används av ha-mcp:s valfria fil-/YAML-verktyg. Dessa inställningar visas också i ha-mcp-serverns eget inställningsgränssnitt och tillämpas direkt, utan omstart. En icke-överskridbar spärr blockerar fortfarande känsliga sökvägar (t.ex. .storage) och nycklar för förtroendegränser (homeassistant, http, frontend, lovelace).",
|
||||
"data": {
|
||||
"allowed_dirs": "Extra filkataloger",
|
||||
"extra_yaml_keys": "Extra YAML-skrivnycklar"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "Kataloger i förhållande till konfigurationskatalogen, var och en beviljas läs- och skrivrättigheter (t.ex. pyscript). Genomgång och poster utanför konfigurationen ignoreras.",
|
||||
"extra_yaml_keys": "Toppnivånycklar som ha_config_set_yaml får skriva utöver de inbyggda, för YAML-först-integrationer på denna installation (t.ex. alert2). Nycklar för förtroendegränser ignoreras."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "HA-MCP Server",
|
||||
"description": "Konfigurera HA-MCP-servern. {panel_hint}Ändringar här tillämpas vid spara.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "Utgivningskanal",
|
||||
"auto_update": "Automatiska serveruppdateringar",
|
||||
"server_port": "MCP-serverns lyssningsport",
|
||||
"bind_host": "Nätverksåtkomst",
|
||||
"webhook_auth": "Autentiseringsläge",
|
||||
"oauth_client_id_override": "Äldre OAuth: anpassat klient-ID (valfritt)",
|
||||
"oauth_client_secret_override": "Äldre OAuth: anpassad klienthemlighet (valfritt)",
|
||||
"oauth_regenerate": "Äldre OAuth: generera nytt klient-ID/hemlighet nu",
|
||||
"pip_spec": "Utvecklare: åsidosättning av ha-mcp-paket",
|
||||
"server_url": "Home Assistant URL (avancerat)",
|
||||
"external_url": "Extern URL (valfritt)",
|
||||
"webhook_id_override": "Anpassad webhook-hemlighet (valfritt)",
|
||||
"secret_path_override": "Anpassad direktåtkomstsökväg (valfritt)",
|
||||
"regenerate_secrets": "Generera nya anslutningshemligheter nu",
|
||||
"enable_webhook": "Fjärråtkomst via webhook",
|
||||
"enable_llm_api": "Konversationsagent LLM API",
|
||||
"llm_api_exposure": "Exponering av konversationsagentverktyg",
|
||||
"enable_startup_notification": "Startmeddelande",
|
||||
"enable_sidebar_panel": "Sidopanel för inställningar"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "Stabil installerar den senaste stabila utgåvan; Utveckling installerar den nyaste utvecklingsversionen. När automatiska uppdateringar är aktiverade, installeras den nyaste versionen av den valda kanalen vid en omladdning eller omstart, samt via en periodisk kontroll. En åsidosättning av utvecklarpaketet nedan har företräde och inaktiverar automatiska uppdateringar.",
|
||||
"auto_update": "När aktiverat installeras den nyaste utgåvan av den valda kanalen automatiskt – vid en omladdning eller omstart, och via en periodisk kontroll. När inaktiverat stannar servern på den version som för närvarande är installerad tills du aktiverar detta igen. Detta styr endast ha-mcp-serverpaketet; uppdateringar till själva HA-MCP Anpassade Komponent kommer fortfarande via HACS.",
|
||||
"server_port": "Porten som denna server lyssnar på. Ha-mcp-appen (tillägget) använder 9583, så standardvärdet är 9584 för att låta båda köra sida vid sida – om du inte kör appen (tillägget) fungerar vilken ledig port som helst.",
|
||||
"bind_host": "Vem som kan ansluta direkt till MCP-serverporten. Standardvärdet matchar appen (tillägget): nås på ditt lokala nätverk, med den hemliga sökvägen som autentiseringsuppgift. Välj loopback för att endast tillåta anslutningar från själva Home Assistant-maskinen – webhook-URL:en och sidofältsspanelen fungerar oavsett vilket.",
|
||||
"webhook_auth": "Hur MCP-klienter autentiserar sig vid webhook-URL:en. Med den hemliga URL:en är länken i sig den enda autentiseringsuppgiften – OAuth körs fortfarande för klienter som använder det, men varje inloggning godkänns automatiskt: ingen inloggning och inget klient-ID eller hemlighet behövs. Med Home Assistant-inloggning loggar klienter som claude.ai in med ett Home Assistant-administratörskonto (OAuth). Med äldre OAuth kör denna integration sin egen inloggningstjänst och utfärdar ett klient-ID och en hemlighet att klistra in i klienter som kräver ett – en omstart av Home Assistant kan behövas för att helt aktivera eller inaktivera det.",
|
||||
"oauth_client_id_override": "Ersätter det automatiskt genererade äldre OAuth-klient-ID:t. Lämna tomt för att behålla det nuvarande. Används endast när autentiseringsläget är inställt på äldre OAuth.",
|
||||
"oauth_client_secret_override": "Ersätter den automatiskt genererade äldre OAuth-klienthemligheten. Lämna tomt för att behålla den nuvarande. Används endast när autentiseringsläget är inställt på äldre OAuth.",
|
||||
"oauth_regenerate": "Engångsåtgärd: skapar ett nytt klient-ID och hemlighet för äldre OAuth-läge. Det träder i kraft först efter den Home Assistant-omstart som reparationsmeddelandet ber om – tills du startar om fortsätter det tidigare klient-ID:t och hemligheten att fungera och de nya gör det inte. Rensar även de två åsidosättningsfälten ovan.",
|
||||
"pip_spec": "Lämna tomt. Endast för att testa en specifik ha-mcp-version (till exempel en förhandsversion); åsidosätter utgivningskanalen och inaktiverar automatiska uppdateringar tills det rensas.",
|
||||
"server_url": "URL:en som den inbyggda servern använder för att nå din Home Assistant (vanligtvis denna instans själv). Lämna tomt för att härleda den från denna instans port- och SSL-inställningar; ange ett värde endast när servern behöver en annan rutt.",
|
||||
"external_url": "Visas som den primära anslutnings-URL:en – använd detta när Home Assistant ligger bakom din egen domän eller omvänd proxy. Ange den fullständiga basadressen inklusive schemat. Den måste peka direkt på Home Assistant – att öppna den i en webbläsare ska nå din HA-inloggningssida – och får inte innehålla en port som :8123 (eller någon annan port), annars kommer fjärranslutna MCP-klienter inte att kunna nå den. Lämna tomt för att automatiskt använda Nabu Casa / den lokala adressen.",
|
||||
"webhook_id_override": "Ersätter den slumpmässiga webhook-hemligheten i anslutnings-URL:en (/api/webhook/...). URL:en är autentiseringsuppgiften – använd ett långt, svårt att gissa värde. Lämna tomt för att behålla den nuvarande.",
|
||||
"secret_path_override": "Ersätter den slumpmässiga sökvägen som används för direktåtkomst på serverporten. Samma regel: sökvägen är autentiseringsuppgiften. Lämna tomt för att behålla den nuvarande.",
|
||||
"regenerate_secrets": "Engångsåtgärd: skapar en ny slumpmässig webhook-hemlighet och direktåtkomstsökväg, vilket omedelbart ogiltigförklarar de gamla anslutnings-URL:erna. Rensar även de två åsidosättningsfälten ovan.",
|
||||
"enable_webhook": "Stäng av för endast-lokalt-läge: Home Assistant-webhooken registreras inte alls, så ingenting – inklusive Nabu Casa – kan nå servern via Home Assistant. Den direkta serverporten och sidopanelen fortsätter att fungera.",
|
||||
"enable_llm_api": "Erbjud hela verktygsuppsättningen till Home Assistant-konversationsagenter (OpenAI, Google, Ollama, ...): när aktiverat kan agenter välja 'HA-MCP Server' under Kontrollera Home Assistant och styra verktygen från Assist-chatt och röst. Aktivering gör det bara valbart – inget exponeras förrän du väljer det på en agent. Användarhandbok: {llm_api_docs_url}",
|
||||
"llm_api_exposure": "Form på verktygsuppsättningen som erbjuds konversationsagenter. Verktygssökning (standard) håller agentens kontext liten: ett kompakt API med fästa verktyg plus sök-/exekvera-metaverktyg. Fullständig katalog listar varje exponerat verktyg direkt – bättre för modeller med stor kontext. Båda registrerar de två sida vid sida så att varje agent väljer sin egen under Kontrollera Home Assistant. Exponering per verktyg hanteras i HA-MCP-inställningspanelen; detaljer: {llm_api_docs_url}",
|
||||
"enable_startup_notification": "Visa en avisering varje gång servern startar, som pekar på de endast-för-administratörer-inställningsytorna. Stäng av för att starta tyst – anslutnings-URL:erna visas fortfarande i Home Assistant-loggen.",
|
||||
"enable_sidebar_panel": "Visa HA-MCP-inställningspanelen i sidofältet (endast administratörer). Stäng av för att ta bort sidofältsinlägget – serveralternativen förblir tillgängliga på denna skärm."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "HA-MCP-servern i processen misslyckades att starta",
|
||||
"description": "HA-MCP-servern i processen kunde inte startas inuti Home Assistant:\n\n{detail}\n\nKontrollera Home Assistant-loggarna, ladda sedan om integrationen (eller åtgärda det underliggande problemet och ladda om) för att försöka igen."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "HA-MCP-serverpaketet i processen kunde inte installeras",
|
||||
"description": "Installationen av ha-mcp-paketet för servern i processen misslyckades:\n\n{detail}\n\nLös kompatibilitets- eller installationsproblemet som beskrivs ovan, ladda sedan om integrationen för att försöka igen."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "Uppdatera HA-MCP Custom Component via HACS",
|
||||
"description": "Den installerade ha-mcp-servern kräver HA-MCP Custom Component {required} eller nyare, men du har {installed}. Uppdatera komponenten via HACS (öppna HA-MCP Custom Component-posten och använd 'Uppdatera information' om ingen uppdatering visas ännu) och starta om Home Assistant. Servern fortsätter att köra under tiden, men vissa nyare funktioner kanske inte fungerar förrän komponenten är uppdaterad."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "HA-MCP-serveruppdatering väntar på en komponentuppdatering",
|
||||
"description": "ha-mcp-server {latest} är tillgänglig, men den versionen uppdaterade också HA-MCP Custom Component (till {shipped}; du kör {running}). För att undvika att starta en serverversion som den körande komponenten aldrig har testats med, är den automatiska serveruppdateringen pausad tills komponenten är uppdaterad.\n\nUppdatera komponenten via HACS (öppna HA-MCP Custom Component-posten och använd 'Uppdatera information' om ingen uppdatering visas ännu), starta sedan om Home Assistant – serveruppdateringen installeras automatiskt därefter. För att installera serveruppdateringen ändå, tryck på Installera på HA-MCP-serveruppdateringsentiteten."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "Komponenten installerad från det äldre arkivet",
|
||||
"description": "HACS spårar huvudförrådet för ha-mcp-servern för denna komponent, så HACS visar serverns versionsnummer och versionsanteckningar här istället för komponentens egna (2.x). Uppdateringar fortsätter att fungera, men förblir felmärkta på detta sätt. För att åtgärda: ta bort detta förråd från HACS (dina integrationsinställningar och konfigurationsposter behålls), lägg till homeassistant-ai/ha-mcp-integration som ett anpassat förråd, installera om komponenten från det, och starta om Home Assistant."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "Starta om Home Assistant för att tillämpa de äldre OAuth-inställningarna",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Starta om Home Assistant?",
|
||||
"description": "Att skicka in detta formulär kommer att starta om Home Assistant och tillämpa de äldre OAuth-inställningarna.\n\nKlicka på **Skicka** för att starta om nu."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "Stabil (rekommenderas)",
|
||||
"dev": "Utveckling (senaste bygget)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "Hemlig webhook-URL (standard)",
|
||||
"ha_auth": "Logga in med Home Assistant (OAuth)",
|
||||
"legacy": "Äldre OAuth (denna integration utfärdar ett klient-ID/en hemlighet att klistra in)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "Verktygssökning (kompakt, standard)",
|
||||
"full": "Fullständig katalog",
|
||||
"both": "Båda (välj per agent)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "Uppdatera"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Direktåtkomst från Home Assistant-maskinen: {url}",
|
||||
"connect_local_lan": "Lokalt/LAN (när Nätverksåtkomst är \"Local network\"): {url}",
|
||||
"connect_remote_url": "Fjärranslutnings-URL: {url}",
|
||||
"connect_urls_label": "Anslutnings-URL(er):",
|
||||
"connect_urls_pending": "Anslutnings-URL:erna visas här (och i Home Assistant-loggen) när servern har startat.",
|
||||
"connect_webhook_disabled": "Fjärråtkomst via webhook är inaktiverad (endast-lokalt-läge).",
|
||||
"oauth_creds_pending": "Klient-ID och klienthemlighet visas här när servern har startat.",
|
||||
"oauth_not_serving": "Legacy OAuth tillhandahåller dem inte ännu — starta om Home Assistant när du blir ombedd, så aktiveras de.",
|
||||
"oauth_select_legacy_mode": "Ställ in autentiseringsläge till äldre OAuth ovan och spara för att generera ett klient-ID och en klienthemlighet.",
|
||||
"panel_hint": "Öppna [HA-MCP-inställningspanelen](/ha-mcp) för verktygshantering och serverinställningar.",
|
||||
"tools_module_installed": "Beta/avancerad fil- & YAML-verktygsmodul (valfritt): Installerad",
|
||||
"tools_module_not_installed": "Beta/avancerad fil- & YAML-verktygsmodul (valfritt): Ej installerad — tryck på \"Lägg till post\" på denna integrationssida och välj \"HA-MCP File & YAML Tools\" för att lägga till den",
|
||||
"tools_module_not_loaded": "Beta/avancerad fil- & YAML-verktygsmodul (valfritt): Installerad men inte laddad — aktivera eller ladda om posten \"HA-MCP File & YAML Tools\" på denna integrationssida",
|
||||
"version_line": "Komponent {component_version} - Server ha-mcp {server_version} ({channel}-kanal)",
|
||||
"version_not_installed": "inte installerad ännu",
|
||||
"version_unknown": "okänd"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "HA-MCP component le'",
|
||||
"description": "Doch Dachel DaneHbogh yIwIv. Home Assistant qoDaq ha-mcp turwI' naQ QapmoH **HA-MCP Server**, 'ej Home Assistant webhook vegh latlhvaD 'ang. jomlu'meH mIwvam luwIv lo'wI'pu' HochHom. nIteb Qap turwI'vam, 'ej jomlu'meH mIw latlh Hoch (add-on, Docker, uvx/PyPI, stdio) qa'chu'; Dalo'chugh, mIwvetlh latlh yIQapmoHQo'. 'a'baqa' YAML je choHmeH patmey le' chel **HA-MCP 'a'baqa' & YAML janmey**; ha-mcp 'a'baqa'/YAML janmey wIvlu'bogh Dachu'chugh neH poQlu'. latlh DaqDaq (add-on, Docker, uvx) Qapchugh ha-mcp turwI'lIj, turwI' De' 'ay' DapoQbe' - 'a'baqa' & YAML De' 'ay'vam neH yIchel, 'ej janmeyvetlh Dalo'chugh neH yIchel. turwI' Segh HochvaD Qap, 'ej pa'QDI' vay' DaH chellaH.",
|
||||
"menu_options": {
|
||||
"server": "HA-MCP Server (chup)",
|
||||
"tools": "HA-MCP 'a'baqa' & YAML janmey (wIvlaH)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "HA-MCP 'a'baqa' & YAML janmey",
|
||||
"description": "'a'baqa' YAML config je choHmeH patmey chaw' le' ghajbogh cher. ha-mcp 'a'baqa'/YAML choHmeH janmey wIvlu'bogh Dachu'chugh neH poQlu' (DuH SeHwI'mey; motlh chu'Ha'lu') - turwI' Segh HochvaD, HA-MCP Server QaptaHboghvaD je Qap. De' 'ay'vam chellaH pagh teqlaH vay' poH Hoch."
|
||||
},
|
||||
"server": {
|
||||
"title": "HA-MCP Server",
|
||||
"description": "Home Assistant qoDaq ha-mcp turwI' naQ QapmoH, 'ej Home Assistant webhook vegh HopDaq 'ang (Nabu Casa proxy ghap vegh naw'laH). nIteb Qap turwI'vam, 'ej add-on, Docker, uvx/PyPI, stdio jomlu'meH mIwmey qa'chu' - latlh ha-mcp turwI' retlhDaq yIQapmoHQo'. QapchoHmeH **ngeH** yIwIv; ghIq integration DuHmeyDaq port, binding, chaw' 'olmeH mIw je DachoHlaH."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "De' 'ay'vam cherlu'pu'.",
|
||||
"unsupported_home_assistant": "Home Assistant {required} ghap Dotlh chu' law' poQ HA-MCP Server QaptaHbogh, 'ach {installed} lo' patvam. componentvam HA-MCP File & YAML Tools De' 'ay' Dalo'taHlaH; add-on Docker container ghapDaq Qapbogh ha-mcp turwI' Hur retlhDaq Qap."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "HA-MCP 'a'baqa' & YAML janmey",
|
||||
"description": "ha-mcp 'a'baqa'/YAML janmey wIvlu'bogh lo'bogh 'a'baqa' YAML choHmeH patmey le' yIcher. ha-mcp turwI' DuHmey UI-Daq DuHmeyvam leghlu' je, 'ej SIbI' Qap; pagh Dataghqa'moHnIS. Daqmey pegh (chovnatlh .storage) 'ej Hung veH ngoqmey (homeassistant, http, frontend, lovelace) bottaH botwI' bIngHom; juSlaH pagh.",
|
||||
"data": {
|
||||
"allowed_dirs": "'a'baqa' ghajwI' Daqmey latlh",
|
||||
"extra_yaml_keys": "YAML ghItlhmeH lIwmey chelbogh"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "config ghajwI' Daq'e' bopbogh Daqmey; HochvaD laDmeH ghItlhmeH je chaw' noblu' (chovnatlh pyscript). Daq veghlu'bogh 'ej config HurDaq 'oHbogh De' 'ay'mey luteqlu'.",
|
||||
"extra_yaml_keys": "ngoqmey jen'e' ghItlhlaH ha_config_set_yaml, ngoqmey motlh chellu'bogh retlhDaq; jomlu'bogh patvamDaq YAML wa'DIch lo'bogh integrationsvaD (chovnatlh alert2). Hung veH ngoqmey luteqlu'."
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "HA-MCP Server",
|
||||
"description": "HA-MCP turwI' yIcher. {panel_hint}polDI' choHmeyvam lo'lu'.\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "Dotlh He",
|
||||
"auto_update": "nuv vangnISbe'taHvIS turwI' yIchu'moH",
|
||||
"server_port": "MCP turwI' Qum lojmIt",
|
||||
"bind_host": "rarmeH naw'",
|
||||
"webhook_auth": "chaw' 'olmeH mIw",
|
||||
"oauth_client_id_override": "OAuth ngo': Client ID le' (wIvlaH)",
|
||||
"oauth_client_secret_override": "OAuth ngo': Client Secret le' (wIvlaH)",
|
||||
"oauth_regenerate": "OAuth ngo': DaH Client ID/Secret yIchenqa'moH",
|
||||
"pip_spec": "ghunwI': ha-mcp package le' yIlo'",
|
||||
"server_url": "Home Assistant URL ('Itlh)",
|
||||
"external_url": "URL Hur (wIvlaH)",
|
||||
"webhook_id_override": "webhook pegh le' (wIvlaH)",
|
||||
"secret_path_override": "naw' He le' (wIvlaH)",
|
||||
"regenerate_secrets": "DaH rarmeH peghmey yIchenqa'moH",
|
||||
"enable_webhook": "webhook vegh Hop naw' yIQotlhHa'",
|
||||
"enable_llm_api": "Qum-agent LLM API",
|
||||
"llm_api_exposure": "Qum-agentvaD janmey 'angmeH mIw",
|
||||
"enable_startup_notification": "QapchoH QIn yIQotlhHa'",
|
||||
"enable_sidebar_panel": "Dop UI DuHmey jIH yIQotlhHa'"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "Dotlh Qapchu'bogh wIvlu'chugh, release Qapchu'bogh chu'qu' jomlu'; ghunmeH Dotlh wIvlu'chugh, development build chu'qu' jomlu'. turwI' chu'moHmeH pat chu'lu'chugh, integration pagh Home Assistant taghqa'moHlu'DI', 'ej roD nuDlu'DI', He wIvlu'bogh build chu'qu' jom pat. bIngDaq ghunwI' package le' wIvlu'chugh, wa'DIch 'oH 'ej turwI' chu'moHmeH pat chu'Ha'moH.",
|
||||
"auto_update": "chu'lu'chugh, integration pagh Home Assistant taghqa'moHlu'DI', 'ej roD nuDlu'DI', He wIvlu'bogh release chu'qu' jom turwI' chu'moHmeH pat. chu'Ha'lu'chugh, Dotlh jomlu'bogh lo'taH turwI', latlhlogh DuHvam Dachu'pa'. ha-mcp turwI' package neH SeH; HACS vegh ghoS HA-MCP Custom Component De' chu'.",
|
||||
"server_port": "Qum lojmItvamDaq 'Ij turwI'vam. 9583 lo' ha-mcp add-on, vaj 9584 motlh lo' turwI'vam, cha' turwI'pu' retlhDaq QaplaHmeH - add-on Dalo'be'chugh, Qum lojmIt tlhab Dalo'laH.",
|
||||
"bind_host": "MCP turwI' Qum lojmItvaD rarlaHbogh ghotpu' SeH. add-on rur DuH motlh: SIr'o' Sumvo' naw'laH vay', 'ej pegh He 'oH chaw'e'. Home Assistant jan neHvo' rarmeH loopback yIwIv - webhook URL Dop jIH je QaptaH.",
|
||||
"webhook_auth": "webhook URL-Daq ghoSmeH, paghDaq chaw' ghaj 'e' 'ol MCP nIqHommey. pegh URL lo'lu'chugh, chaw' 'oH URL'e'. Home Assistant sign-in lo'lu'chugh, Home Assistant loHwI' mab lo'ta' claude.ai rur nIqHom (OAuth). OAuth ngo' lo'lu'chugh, integrationvam Client ID Secret je chenmoH; Google Gemini Spark rur nIqHom poQboghDaq lanlu'. Dachu'DI' pagh Dachu'Ha'DI' Home Assistant Dataghqa'moHnIS.",
|
||||
"oauth_client_id_override": "OAuth ngo' Client ID chenmoH pat; 'oH qa' DuHvam. chIm yIpol Client ID DaH lo'lu'bogh polmeH. chaw' 'olmeH mIw 'oHtaH OAuth ngo' neH lo'lu'.",
|
||||
"oauth_client_secret_override": "OAuth ngo' Client Secret chenmoH pat; 'oH qa' DuHvam. chIm yIpol Client Secret DaH lo'lu'bogh polmeH. chaw' 'olmeH mIw 'oHtaH OAuth ngo' neH lo'lu'.",
|
||||
"oauth_regenerate": "wa'logh vang: OAuth ngo' mIwvamvaD Client ID Secret je chu' chenmoH. repair QIn poQbogh Home Assistant Dataghqa'moHpu'DI' neH QapchoH - Home Assistant Dataghqa'moHpa', Client ID Secret je ngo' QaptaH, 'ej chu' Qapbe'. bIngDaq override 'ay'mey cha' chImmoH je.",
|
||||
"pip_spec": "chIm yIpol. ha-mcp build le' neH wa'logh nIDmeH lo' (chovnatlh pre-release pin); release He qa', 'ej chImmoHlu'pa' turwI' chu'moHmeH pat chu'Ha'moH.",
|
||||
"server_url": "Home AssistantlIj naw'meH turwI' QaptaHbogh lo'bogh URL (motlh patvam'e'). chIm yIpol patvam Qum lojmIt SSL DuHmey jevo' chenmoHmeH; latlh He poQ turwI' neHchugh, De' yIlan.",
|
||||
"external_url": "rarmeH URL wa'DIch 'anglu' - domainlIj proxy ghap 'emDaq ba'chugh Home Assistant, URLvam yIlo'. scheme ngaSbogh Quv wa'DIch naQ yIlan. Home Assistant neH ghoS net poQ - web leghwI'Daq DapoSmoHDI', HA login jIH Daghaj net poQ - 'ej :8123 rur port ghajbe'nIS (latlh port je), pagh Hop MCP nIqHommey naw'laHbe'. chIm yIpol; vaj Nabu Casa Quv Sum ghap wIv pat.",
|
||||
"webhook_id_override": "rarmeH URL (/api/webhook/...)Daq webhook pegh chenmoH pat; peghvetlh qa' DuHvam. chaw' 'oH URL'e' - nI'bogh, ghItlh Qatlhbogh De' yIlo'. chIm yIpol pegh DaH lo'lu'bogh polmeH.",
|
||||
"secret_path_override": "turwI' Qum lojmItDaq naw'meH He pIQ chenmoH pat; Hevetlh pegh, 'ej Hevetlh qa' DuHvam. rap chut: chaw' 'oH He'e'. chIm yIpol He DaH lo'lu'bogh polmeH.",
|
||||
"regenerate_secrets": "wa'logh vang: webhook pegh chu' chenmoH pat, 'ej turwI' Qum lojmItDaq naw'meH He pIQ chenmoHqa' pat, vaj SIbI' QapHa' rarmeH URLmey ngo'. bIngDaq override 'ay'mey cha' chImmoH je.",
|
||||
"enable_webhook": "Sum neH mIw lo'meH yIchu'Ha': Home Assistant webhook qonta'be'lu'chu', vaj turwI'Daq Home Assistant vegh naw'laH pagh - Nabu Casa je. turwI' Qum lojmIt vegh naw'lu'meH He pIQ Dop jIH je QaptaH.",
|
||||
"enable_llm_api": "Home Assistant Qum agentsvaD janmey naQ yInob (OpenAI, Google, Ollama, ...): chu'lu'taHvIS, Control Home Assistant bIngDaq 'HA-MCP Server' wIvlaH agents, 'ej Assist chat ghogh jevo' janmey SeHlaH. chu'moHlu' neHchugh wIvlaH; agentDaq wIvbe'lu'pa' pagh 'anglu'. lo'meH paq: {llm_api_docs_url}",
|
||||
"llm_api_exposure": "Qum agentsvaD noblu'bogh jan ghom SeH. jan nej (motlh) agent context mach pol: janmey lungu'lu'bogh, nej/lo' meta-janmey je ghajbogh API mach. jan tetlh naQ 'ang mIw naQ - context tIn ghajbogh modelvaD qaq. cha' mIwmey wIvlu'chugh, cha' mIwmey retlhDaq qonta'lu', vaj Control Home Assistant bIngDaq mIwvam wIvlaH Hoch agent. HA-MCP DuHmey jIHDaq Hoch jan 'angmeH DuH SeHlu'; De' latlh: {llm_api_docs_url}",
|
||||
"enable_startup_notification": "turwI' QapchoH Hochlogh QIn yI'ang; loHwI' neHvaD DuHmey Daqmey 'ang. tamtaH QapchoHmeH yIchu'Ha' - Home Assistant QonoSDaq rarmeH URLmey leghlu'taH.",
|
||||
"enable_sidebar_panel": "DopDaq HA-MCP DuHmey jIH yI'ang (loHwI' neH). Dop De' 'ay' teqmeH yIchu'Ha' - jIHvamDaq turwI' DuHmey tu'lu'taH."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "QapchoHlaHbe' HA-MCP turwI' QaptaHbogh",
|
||||
"description": "Home Assistant qoDaq HA-MCP turwI' QaptaHbogh QapchoHmoHlaHbe'lu':\n\n{detail}\n\nHome Assistant QonoSmey yInuD; ghIq nIDqa'meH integration yItaghqa'moH (pagh Qagh mung yItI' 'ej yItaghqa'moH)."
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "HA-MCP turwI' QaptaHbogh package jomlaHbe'lu'",
|
||||
"description": "HA-MCP turwI' QaptaHboghvaD ha-mcp package jomlu'DI' Qagh qaS:\n\n{detail}\n\nbIngDaq compatibility Qagh, jomlu'DI' Qagh ghap QIjlu'bogh yItI'; ghIq nIDqa'meH integration yItaghqa'moH."
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "HACS vegh HA-MCP Custom Component yIchu'moH",
|
||||
"description": "HA-MCP Custom Component {required} ghap chu' law' poQ ha-mcp turwI' jomlu'bogh, 'ach {installed} Daghaj. HACS vegh component yIchu'moH (HA-MCP Custom Component De' 'ay' yIpoS, 'ej De' chu' 'anglu'be'chugh 'Update information' yIlo'), ghIq Home Assistant yItaghqa'moH. qaStaHvIS QaptaH turwI', 'ach component chu'moHlu'pa' DuHmey chu' 'op Qapbe'laH."
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "component chu'moHghach loS HA-MCP turwI' chu'moHghach",
|
||||
"description": "ha-mcp turwI' {latest} tu'lu', 'ach HA-MCP Custom Component chu'moH je releasevetlh ({shipped}; {running} Dalo'taH). turwI' Dotlh'e' nIDbe'pu'bogh component QaptaHbogh QapchoHmoHlu'be'meH, component chu'moHlu'pa' turwI' chu'moHghach pat jomQo'.\n\nHACS vegh component yIchu'moH (HA-MCP Custom Component De' 'ay' yIpoS, 'ej De' chu' 'anglu'be'chugh 'Update information' yIlo'), ghIq Home Assistant yItaghqa'moH - ghIq nuv vangnISbe'taHvIS turwI' chu'moHghach jomlu'. turwI' chu'moHghach DaH jom DaneHchugh, HA-MCP turwI' chu'moHghach DolDaq jom leQ yIwIv."
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "repository ngo'vo' jomlu' component",
|
||||
"description": "componentvamvaD ha-mcp turwI' repository wa'DIch Hotlh HACS, vaj component Dotlh mI' (2.x) QInmey je 'angbe' 'ej turwI' Dotlh mI' QInmey je 'ang. chu'moHtaH HACS, 'ach pongmey muj pol. tI'meH: HACSvo' repositoryvam yIteq (integration DuHmey config De' 'ay'mey je pollu'), repository le' 'oHmeH homeassistant-ai/ha-mcp-integration yIchel, componentvo' yIjomqa', 'ej Home Assistant yItaghqa'moH."
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "OAuth ngo' choH lo'meH Home Assistant yItaghqa'moH",
|
||||
"description": "OAuth ngo' chaw' 'olmeH mIw /authorize /token web endpoints ghajbogh'e' qon; Home Assistant naQ taghqa'moHlu'DI' neH rarlaH pagh rarHa'laH - mIwvam Dachu'pu'chugh, Dachu'Ha'pu'chugh, pagh Client ID/Secret DachoHpu'chugh je, rap ghu'. choH lo'meH Home Assistant yItaghqa'moH (Settings - System - Restart); Home Assistant Dataghqa'moHpa' QaptaH mIw ngo'."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "Dotlh Qapchu'bogh (chup)",
|
||||
"dev": "ghunmeH Dotlh (build chu'qu')"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "webhook URL pegh (motlh)",
|
||||
"ha_auth": "Home Assistant lo'taHvIS yI'el (OAuth)",
|
||||
"legacy": "OAuth ngo' (Client ID/Secret; Google Gemini SparkvaD)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "jan nej (mach, motlh)",
|
||||
"full": "janmey naQ",
|
||||
"both": "cha' mIwmey (Hoch agentvaD yIwIv)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "chu'moH"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "Home Assistant janvo' naw' pIQ: {url}",
|
||||
"connect_local_lan": "SIr'o' Sum/LAN (rarmeH naw'Daq \"Local network\" wIvlu'chugh): {url}",
|
||||
"connect_remote_url": "Daq Hopvo' rarmeH URL: {url}",
|
||||
"connect_urls_label": "rarmeH URLmey:",
|
||||
"connect_urls_pending": "turwI' QapchoHpu'DI', naDev rarmeH URLmey 'anglu', 'ej Home Assistant QonoSDaq 'anglu' je.",
|
||||
"connect_webhook_disabled": "Daq Hopvo' webhook lo'bogh naw' Qotlhlu' (Daq Sumvo' neH naw'lu').",
|
||||
"oauth_creds_pending": "turwI' QapchoHpu'DI', naDev Client ID, Client Secret je 'anglu'.",
|
||||
"oauth_not_serving": "wej Dochmeyvam laj OAuth ngo' — Home Assistant Dataghqa'moH 'e' tlhoblu'DI', QapchoHmeH Dochmeyvam Home Assistant yItaghqa'moH.",
|
||||
"oauth_select_legacy_mode": "DungDaq chaw' 'olmeH mIwvaD OAuth ngo' yIwIv 'ej Client ID Client Secret je chenmoHlu'meH yIpol.",
|
||||
"panel_hint": "janmey Davu'meH 'ej turwI' DuHmey DachermeH [HA-MCP SeHlaw](/ha-mcp) yIpoSmoH.",
|
||||
"tools_module_installed": "'a'baqa' & YAML janmey bobcho' (Beta/'Itlh; wIvlaH): jomlu'pu'",
|
||||
"tools_module_not_installed": "'a'baqa' & YAML janmey bobcho' (Beta/'Itlh; wIvlaH): jomlu'be' — navvamDaq \"De' 'ay' chel\" yI'uy, 'ej bobcho'vam DachelmeH \"HA-MCP File & YAML Tools\" yIwIv",
|
||||
"tools_module_not_loaded": "'a'baqa' & YAML janmey bobcho' (Beta/'Itlh; wIvlaH): jomlu'pu' 'ach QaptaHbe' — navvamDaq \"HA-MCP File & YAML Tools\" De' 'ay' yIQotlhHa' pagh yItaghqa'moH",
|
||||
"version_line": "component {component_version} - ha-mcp turwI' {server_version} ({channel} Dotlh He)",
|
||||
"version_not_installed": "wej jomlu'",
|
||||
"version_unknown": "Sovbe'lu'"
|
||||
}
|
||||
}
|
||||
@@ -1,161 +1,168 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "HA-MCP 自定义组件",
|
||||
"description": "选择要添加的内容。**HA-MCP 服务器**在 Home Assistant 内部运行完整的 ha-mcp 服务器,并通过 Home Assistant Webhook 对外提供服务——这是大多数用户需要的安装方式。它是一个独立的服务器,可完全取代其他所有安装方式(插件、Docker、uvx/PyPI、stdio);如果运行它,就不要再同时运行其中任何一种。**HA-MCP 文件与 YAML 工具**添加特权文件和 YAML 编辑服务,仅在您启用 ha-mcp 的可选文件/YAML 工具时才需要。如果您的 ha-mcp 服务器已在别处运行(插件、Docker、uvx),则不需要服务器条目——只添加这个文件与 YAML 条目,并且仅在您使用这些工具时才添加。它适用于所有服务器类型,随时可以后续添加。",
|
||||
"menu_options": {
|
||||
"server": "HA-MCP 服务器(推荐)",
|
||||
"tools": "HA-MCP 文件与 YAML 工具(可选)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "HA-MCP 文件与 YAML 工具",
|
||||
"description": "设置特权文件和 YAML 配置服务。仅在您启用 ha-mcp 的可选文件/YAML 编辑工具(功能开关,默认关闭)时才需要——这适用于所有服务器类型,包括进程内的 HA-MCP 服务器。您可以随时添加或移除此条目。"
|
||||
},
|
||||
"server": {
|
||||
"title": "HA-MCP 服务器",
|
||||
"description": "这会在 Home Assistant 内部运行完整的 ha-mcp 服务器,并通过 Home Assistant Webhook 远程提供服务(可经由 Nabu Casa 或任意反向代理访问)。它是一个独立的服务器,可完全取代插件、Docker、uvx/PyPI 和 stdio 安装方式——不要与其他 ha-mcp 服务器同时运行。选择**提交**以启动它;启动后可在集成选项中更改端口、绑定方式和身份验证。"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "此条目已配置。",
|
||||
"unsupported_home_assistant": "进程内 HA-MCP 服务器需要 Home Assistant {required} 或更高版本,但此实例运行的是 {installed}。您仍可将本组件的 HA-MCP File & YAML Tools 条目与以插件或 Docker 容器方式运行的外部 ha-mcp 服务器搭配使用。"
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "HA-MCP 自定义组件",
|
||||
"description": "选择要添加的内容。**HA-MCP Server** 在 Home Assistant 内部运行完整的 ha-mcp 服务器,并通过 Home Assistant webhook 将其暴露——这是大多数人想要的安装方式。它是一个独立的服务器,可完全替代所有其他安装方法:App (add-on)、Docker、uvx/PyPI、stdio;如果您运行了它,请勿同时运行其他安装方式。**HA-MCP File & YAML Tools** 有两个作用。如果您的 ha-mcp server 运行在 Home Assistant 外部(app、Docker、uvx/PyPI、stdio),此条目还可为该服务器提供其无法从外部访问的进程内组件功能(快速进程内搜索、配置条目元数据、注册表支持的读取),因此非常值得添加到每个外部安装中,并且它还是 ha-mcp 的可选文件/YAML 工具所使用的特权文件与 YAML 编辑服务的访问控制关卡。如果您改为添加 HA-MCP Server 条目,该条目已提供这些共享功能,因此该第二个条目此时仅用作可选文件/YAML 工具的访问控制关卡。它适用于所有服务器类型,并且可以随时在以后添加。",
|
||||
"menu_options": {
|
||||
"server": "HA-MCP 服务器(推荐)",
|
||||
"tools": "HA-MCP 文件与 YAML 工具(可选)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"title": "HA-MCP 文件与 YAML 工具",
|
||||
"description": "设置特权文件和 YAML 配置服务,这些服务受控于 ha-mcp 的可选文件/YAML 编辑工具(功能标志,默认关闭)——这适用于所有服务器类型,包括进程内的 HA-MCP Server。如果您的 ha-mcp server 运行在 Home Assistant 外部,此条目还会为其提供无法从外部访问的进程内组件功能(快速进程内搜索、配置条目元数据、注册表支持的读取),即使关闭文件/YAML 工具,这些功能也非常值得拥有。您可以随时添加或删除此条目。"
|
||||
},
|
||||
"server": {
|
||||
"title": "HA-MCP 服务器",
|
||||
"description": "这会在 Home Assistant 内部运行完整的 ha-mcp 服务器,并通过 Home Assistant Webhook 将其远程公开(可通过 Nabu Casa 或任何反向代理访问)。这是一个独立的服务器,可完全替代 app (add-on)、Docker、uvx/PyPI 和 stdio 安装方式——请勿将其与其他 ha-mcp 服务器同时运行。选择**提交**以启动它;您随后可以在集成选项中更改端口、绑定和身份验证。"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "HA-MCP 文件与 YAML 工具",
|
||||
"description": "配置 ha-mcp 可选文件/YAML 工具所使用的特权文件和 YAML 编辑服务。这些设置也会出现在 ha-mcp 服务器自身的设置界面中,并实时生效,无需重启。不可覆盖的拒绝底线仍会阻止敏感路径(例如 .storage)和信任边界键(homeassistant、http、frontend、lovelace)。",
|
||||
"data": {
|
||||
"allowed_dirs": "额外的文件目录",
|
||||
"extra_yaml_keys": "额外的 YAML 可写键"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "相对于配置目录的目录,每个目录都授予读写权限(例如 pyscript)。路径穿越以及配置目录之外的条目会被丢弃。",
|
||||
"extra_yaml_keys": "除内置键外,ha_config_set_yaml 还可写入的顶层键,适用于本安装中以 YAML 为主的集成(例如 alert2)。信任边界键会被丢弃。"
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "HA-MCP 服务器",
|
||||
"description": "配置 HA-MCP 服务器。{panel_hint}此处的更改在保存时应用。\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "发布通道",
|
||||
"auto_update": "服务器自动更新",
|
||||
"server_port": "MCP 服务器监听端口",
|
||||
"bind_host": "网络访问",
|
||||
"webhook_auth": "身份验证模式",
|
||||
"oauth_client_id_override": "旧版 OAuth:自定义 Client ID(可选)",
|
||||
"oauth_client_secret_override": "旧版 OAuth:自定义 Client Secret(可选)",
|
||||
"oauth_regenerate": "旧版 OAuth:立即重新生成 Client ID/Secret",
|
||||
"pip_spec": "开发者:ha-mcp 软件包覆盖",
|
||||
"server_url": "Home Assistant URL(高级)",
|
||||
"external_url": "外部 URL(可选)",
|
||||
"webhook_id_override": "自定义 Webhook 密钥(可选)",
|
||||
"secret_path_override": "自定义直连路径(可选)",
|
||||
"regenerate_secrets": "立即重新生成连接密钥",
|
||||
"enable_webhook": "通过 Webhook 远程访问",
|
||||
"enable_llm_api": "对话智能体 LLM API",
|
||||
"llm_api_exposure": "对话智能体工具暴露方式",
|
||||
"enable_startup_notification": "启动通知",
|
||||
"enable_sidebar_panel": "侧边栏设置面板"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "稳定版安装最新的稳定发行版;开发版安装最新的开发构建。启用自动更新后,重新加载或重启以及定期检查都会安装所选通道的最新构建。下方的开发者软件包覆盖优先级更高,并会禁用自动更新。",
|
||||
"auto_update": "启用时,所选通道的最新发行版会自动安装——在重新加载或重启时,以及通过定期检查。关闭时,服务器会保持当前已安装的版本,直到您重新启用此项。它仅控制 ha-mcp 服务器软件包;HA-MCP 自定义组件自身的更新仍通过 HACS 进行。",
|
||||
"server_port": "此服务器监听的端口。ha-mcp 插件使用 9583,因此这里默认为 9584,以便两者并存——如果您不运行该插件,任何空闲端口都可以。",
|
||||
"bind_host": "谁可以直接连接到 MCP 服务器端口。默认值与插件一致:可在本地网络中访问,并以密钥路径作为凭据。选择 loopback 则仅允许来自 Home Assistant 所在主机的连接——无论哪种方式,Webhook URL 和侧边栏面板都能正常工作。",
|
||||
"webhook_auth": "MCP 客户端在 Webhook URL 上如何证明自己的身份。使用密钥 URL 时,链接本身就是凭据。使用 Home Assistant 登录时,claude.ai 等客户端会以 Home Assistant 管理员账户登录(OAuth)。使用旧版 OAuth 时,此集成会签发自己的 Client ID 和 Secret,供需要它们的客户端(例如 Google Gemini Spark)粘贴使用——开启或关闭该模式需要重启 Home Assistant。",
|
||||
"oauth_client_id_override": "替换自动生成的旧版 OAuth Client ID。留空则保留当前值。仅在身份验证模式设为旧版 OAuth 时使用。",
|
||||
"oauth_client_secret_override": "替换自动生成的旧版 OAuth Client Secret。留空则保留当前值。仅在身份验证模式设为旧版 OAuth 时使用。",
|
||||
"oauth_regenerate": "一次性操作:为旧版 OAuth 模式生成新的 Client ID 和 Secret。它只有在修复提示所要求的 Home Assistant 重启之后才会生效——在您重启之前,原有的 Client ID 和 Secret 仍然有效,新的尚未启用。同时会清空上面两个覆盖字段。",
|
||||
"pip_spec": "请留空。仅用于测试特定的 ha-mcp 构建(例如固定到某个预发行版);它会覆盖发布通道,并在清空之前禁用自动更新。",
|
||||
"server_url": "进程内服务器用于访问您的 Home Assistant 的 URL(通常就是此实例本身)。留空则根据此实例的端口和 SSL 设置推导;仅在服务器需要走其他路由时才设置。",
|
||||
"external_url": "作为主要连接 URL 显示——当 Home Assistant 位于您自己的域名或反向代理之后时使用。请输入包含协议在内的完整基础地址。它必须直接指向 Home Assistant——在浏览器中打开它应能到达您的 HA 登录页面——并且不能包含 :8123 之类的端口(任何端口都不行),否则远程 MCP 客户端将无法访问。留空则自动使用 Nabu Casa / 本地地址。",
|
||||
"webhook_id_override": "替换连接 URL(/api/webhook/...)中随机生成的 Webhook 密钥。该 URL 就是凭据——请使用足够长、难以猜测的值。留空则保留当前值。",
|
||||
"secret_path_override": "替换在服务器端口上用于直连的随机路径。规则相同:该路径就是凭据。留空则保留当前值。",
|
||||
"regenerate_secrets": "一次性操作:生成新的随机 Webhook 密钥和直连路径,并立即使旧的连接 URL 失效。同时会清空上面两个覆盖字段。",
|
||||
"enable_webhook": "关闭以进入仅本地模式:完全不注册 Home Assistant Webhook,因此任何一方——包括 Nabu Casa——都无法通过 Home Assistant 访问服务器。服务器端口直连和侧边栏面板仍可继续使用。",
|
||||
"enable_llm_api": "向 Home Assistant 对话智能体(OpenAI、Google、Ollama 等)提供完整工具集:启用后,智能体可在“控制 Home Assistant”下选择“HA-MCP Server”,并在 Assist 聊天和语音中使用这些工具。启用只是让它可供选择——在您为某个智能体选中它之前,不会暴露任何内容。使用指南:{llm_api_docs_url}",
|
||||
"llm_api_exposure": "提供给对话智能体的工具集形态。工具搜索(默认)可让智能体的上下文保持精简:一个包含固定工具以及搜索/执行元工具的紧凑 API。完整目录会直接列出每个已暴露的工具——更适合大上下文模型。两者会将二者并列注册,让每个智能体在“控制 Home Assistant”下各自选择。按工具的暴露设置在 HA-MCP 设置面板中管理;详情:{llm_api_docs_url}",
|
||||
"enable_startup_notification": "每次服务器启动时显示通知,指向仅管理员可见的设置界面。关闭则静默启动——连接 URL 仍会出现在 Home Assistant 日志中。",
|
||||
"enable_sidebar_panel": "在侧边栏中显示 HA-MCP 设置面板(仅管理员)。关闭则移除侧边栏条目——服务器选项仍可在此界面中使用。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "HA-MCP 进程内服务器启动失败",
|
||||
"description": "无法在 Home Assistant 内部启动 HA-MCP 进程内服务器:\n\n{detail}\n\n请检查 Home Assistant 日志,然后重新加载该集成(或先修复根本问题再重新加载)以重试。"
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "无法安装 HA-MCP 进程内服务器软件包",
|
||||
"description": "为进程内服务器安装 ha-mcp 软件包失败:\n\n{detail}\n\n请解决上述兼容性或安装问题,然后重新加载该集成以重试。"
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "请通过 HACS 更新 HA-MCP 自定义组件",
|
||||
"description": "已安装的 ha-mcp 服务器需要 HA-MCP 自定义组件 {required} 或更高版本,但您的版本是 {installed}。请通过 HACS 更新组件(打开 HA-MCP 自定义组件条目;如果尚未显示更新,请使用“Update information”),然后重启 Home Assistant。在此期间服务器仍会继续运行,但在组件更新之前,某些较新的功能可能无法使用。"
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "HA-MCP 服务器更新正在等待组件更新",
|
||||
"description": "ha-mcp 服务器 {latest} 已可用,但该发行版同时更新了 HA-MCP 自定义组件(更新至 {shipped};您正在运行 {running})。为避免启动一个当前组件从未测试过的服务器版本,服务器自动更新将暂缓,直到组件完成更新。\n\n请通过 HACS 更新组件(打开 HA-MCP 自定义组件条目;如果尚未显示更新,请使用“Update information”),然后重启 Home Assistant——之后服务器更新会自动安装。若仍要立即安装服务器更新,请在 HA-MCP 服务器更新实体上点击“安装”。"
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "组件安装自旧版仓库",
|
||||
"description": "HACS 为此组件跟踪的是 ha-mcp 服务器主仓库,因此 HACS 在此显示的是服务器的版本号(7.x)和服务器的发行说明,而不是组件自身的(1.x)。更新仍然可用,但会一直被这样错误标注。修复方法:从 HACS 中移除此仓库(您的集成设置和配置项会保留),将 homeassistant-ai/ha-mcp-integration 添加为自定义仓库,从中重新安装组件,然后重启 Home Assistant。"
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "请重启 Home Assistant 以应用旧版 OAuth 更改",
|
||||
"description": "旧版 OAuth 身份验证模式会注册自己的 /authorize 和 /token 网页端点,而 Home Assistant 只有在完全重启时才能绑定或释放它们——无论您是刚刚开启该模式、关闭它,还是更改了它的 Client ID/Secret。请重启 Home Assistant(设置 - 系统 - 重启)以应用更改;在此之前仍保持先前的行为。"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "稳定版(推荐)",
|
||||
"dev": "开发版(最新构建)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "密钥 Webhook URL(默认)",
|
||||
"ha_auth": "使用 Home Assistant 登录(OAuth)",
|
||||
"legacy": "旧版 OAuth(Client ID/Secret,适用于 Google Gemini Spark)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "工具搜索(紧凑,默认)",
|
||||
"full": "完整目录",
|
||||
"both": "两者(按智能体选择)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "更新"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "从 Home Assistant 所在主机直接访问:{url}",
|
||||
"connect_local_lan": "本地/局域网(当网络访问为“Local network”时):{url}",
|
||||
"connect_remote_url": "远程连接 URL:{url}",
|
||||
"connect_urls_label": "连接 URL:",
|
||||
"connect_urls_pending": "服务器启动后,连接 URL 将显示在此处(以及 Home Assistant 日志中)。",
|
||||
"connect_webhook_disabled": "通过 Webhook 远程访问已关闭(仅本地模式)。",
|
||||
"oauth_creds_pending": "服务器启动后,Client ID 和 Client Secret 将显示在此处。",
|
||||
"oauth_not_serving": "旧版 OAuth 尚未启用这些凭据——请在 Home Assistant 提示时重启以激活它们。",
|
||||
"oauth_select_legacy_mode": "将上面的身份验证模式设为旧版 OAuth 并保存,以生成 Client ID 和 Client Secret。",
|
||||
"panel_hint": "打开 [HA-MCP 设置面板](/ha-mcp) 以管理工具和服务器设置。",
|
||||
"tools_module_installed": "Beta/高级文件与 YAML 工具模块(可选):已安装",
|
||||
"tools_module_not_installed": "Beta/高级文件与 YAML 工具模块(可选):未安装——请在此集成页面上点击“添加条目”并选择“HA-MCP File & YAML Tools”",
|
||||
"tools_module_not_loaded": "Beta/高级文件与 YAML 工具模块(可选):已安装但未加载——请在此集成页面上启用或重新加载“HA-MCP File & YAML Tools”条目",
|
||||
"version_line": "组件 {component_version} - 服务器 ha-mcp {server_version}({channel} 通道)",
|
||||
"version_not_installed": "尚未安装",
|
||||
"version_unknown": "未知"
|
||||
"abort": {
|
||||
"already_configured": "此条目已配置。",
|
||||
"unsupported_home_assistant": "进程内的 HA-MCP Server 需要 Home Assistant {required} 或更高版本,但此实例运行的是 {installed}。您仍然可以将此组件的 HA-MCP File & YAML Tools 条目与作为 app (add-on) 或 Docker 容器运行的外部 ha-mcp 服务器配合使用。"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"tools_info": {
|
||||
"title": "HA-MCP 文件与 YAML 工具",
|
||||
"description": "配置 ha-mcp 可选文件/YAML 工具所使用的特权文件和 YAML 编辑服务。这些设置也会出现在 ha-mcp 服务器自身的设置界面中,并实时生效,无需重启。不可覆盖的拒绝底线仍会阻止敏感路径(例如 .storage)和信任边界键(homeassistant、http、frontend、lovelace)。",
|
||||
"data": {
|
||||
"allowed_dirs": "额外的文件目录",
|
||||
"extra_yaml_keys": "额外的 YAML 可写键"
|
||||
},
|
||||
"data_description": {
|
||||
"allowed_dirs": "相对于配置目录的目录,每个目录都授予读写权限(例如 pyscript)。路径穿越以及配置目录之外的条目会被丢弃。",
|
||||
"extra_yaml_keys": "除内置键外,ha_config_set_yaml 还可写入的顶层键,适用于本安装中以 YAML 为主的集成(例如 alert2)。信任边界键会被丢弃。"
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"title": "HA-MCP 服务器",
|
||||
"description": "配置 HA-MCP 服务器。{panel_hint}此处的更改在保存时应用。\n\n{versions}\n\n{connect_url}\n\n{oauth_creds}",
|
||||
"data": {
|
||||
"channel": "发布通道",
|
||||
"auto_update": "服务器自动更新",
|
||||
"server_port": "MCP 服务器监听端口",
|
||||
"bind_host": "网络访问",
|
||||
"webhook_auth": "身份验证模式",
|
||||
"oauth_client_id_override": "旧版 OAuth:自定义 Client ID(可选)",
|
||||
"oauth_client_secret_override": "旧版 OAuth:自定义 Client Secret(可选)",
|
||||
"oauth_regenerate": "旧版 OAuth:立即重新生成 Client ID/Secret",
|
||||
"pip_spec": "开发者:ha-mcp 软件包覆盖",
|
||||
"server_url": "Home Assistant URL(高级)",
|
||||
"external_url": "外部 URL(可选)",
|
||||
"webhook_id_override": "自定义 Webhook 密钥(可选)",
|
||||
"secret_path_override": "自定义直连路径(可选)",
|
||||
"regenerate_secrets": "立即重新生成连接密钥",
|
||||
"enable_webhook": "通过 Webhook 远程访问",
|
||||
"enable_llm_api": "对话智能体 LLM API",
|
||||
"llm_api_exposure": "对话智能体工具暴露方式",
|
||||
"enable_startup_notification": "启动通知",
|
||||
"enable_sidebar_panel": "侧边栏设置面板"
|
||||
},
|
||||
"data_description": {
|
||||
"channel": "稳定版安装最新的稳定发行版;开发版安装最新的开发构建。启用自动更新后,重新加载或重启以及定期检查都会安装所选通道的最新构建。下方的开发者软件包覆盖优先级更高,并会禁用自动更新。",
|
||||
"auto_update": "启用时,所选通道的最新发行版会自动安装——在重新加载或重启时,以及通过定期检查。关闭时,服务器会保持当前已安装的版本,直到您重新启用此项。它仅控制 ha-mcp 服务器软件包;HA-MCP 自定义组件自身的更新仍通过 HACS 进行。",
|
||||
"server_port": "此服务器监听的端口。ha-mcp app (add-on) 使用 9583,因此默认设置为 9584 以允许两者并存——如果您不运行该 app (add-on),任何空闲端口均可。",
|
||||
"bind_host": "谁可以直接连接到 MCP 服务器端口。默认值与 app (add-on) 匹配:可在本地网络上访问,以机密路径作为凭据。选择 loopback 仅允许来自 Home Assistant 本机本身的连接——无论哪种方式,Webhook URL 和侧边栏面板都可以正常工作。",
|
||||
"webhook_auth": "MCP 客户端如何在 Webhook URL 处验证身份。使用秘密 URL 时,链接本身就是唯一的凭据——OAuth 仍会为使用它的客户端运行,但每次登录都会自动批准:无需登录,也无需客户端 ID 或密钥。使用 Home Assistant 登录时,claude.ai 等客户端会使用 Home Assistant 管理员账户登录 (OAuth)。使用旧版 OAuth 时,此集成会运行自己的登录服务,并颁发客户端 ID 和密钥以粘贴到需要它们的客户端中——可能需要重启 Home Assistant 才能完全启用或禁用它。",
|
||||
"oauth_client_id_override": "替换自动生成的旧版 OAuth Client ID。留空则保留当前值。仅在身份验证模式设为旧版 OAuth 时使用。",
|
||||
"oauth_client_secret_override": "替换自动生成的旧版 OAuth Client Secret。留空则保留当前值。仅在身份验证模式设为旧版 OAuth 时使用。",
|
||||
"oauth_regenerate": "一次性操作:为旧版 OAuth 模式生成新的 Client ID 和 Secret。它只有在修复提示所要求的 Home Assistant 重启之后才会生效——在您重启之前,原有的 Client ID 和 Secret 仍然有效,新的尚未启用。同时会清空上面两个覆盖字段。",
|
||||
"pip_spec": "请留空。仅用于测试特定的 ha-mcp 构建(例如固定到某个预发行版);它会覆盖发布通道,并在清空之前禁用自动更新。",
|
||||
"server_url": "进程内服务器用于访问您的 Home Assistant 的 URL(通常就是此实例本身)。留空则根据此实例的端口和 SSL 设置推导;仅在服务器需要走其他路由时才设置。",
|
||||
"external_url": "作为主要连接 URL 显示——当 Home Assistant 位于您自己的域名或反向代理之后时使用。请输入包含协议在内的完整基础地址。它必须直接指向 Home Assistant——在浏览器中打开它应能到达您的 HA 登录页面——并且不能包含 :8123 之类的端口(任何端口都不行),否则远程 MCP 客户端将无法访问。留空则自动使用 Nabu Casa / 本地地址。",
|
||||
"webhook_id_override": "替换连接 URL(/api/webhook/...)中随机生成的 Webhook 密钥。该 URL 就是凭据——请使用足够长、难以猜测的值。留空则保留当前值。",
|
||||
"secret_path_override": "替换在服务器端口上用于直连的随机路径。规则相同:该路径就是凭据。留空则保留当前值。",
|
||||
"regenerate_secrets": "一次性操作:生成新的随机 Webhook 密钥和直连路径,并立即使旧的连接 URL 失效。同时会清空上面两个覆盖字段。",
|
||||
"enable_webhook": "关闭以进入仅本地模式:完全不注册 Home Assistant Webhook,因此任何一方——包括 Nabu Casa——都无法通过 Home Assistant 访问服务器。服务器端口直连和侧边栏面板仍可继续使用。",
|
||||
"enable_llm_api": "向 Home Assistant 对话智能体(OpenAI、Google、Ollama 等)提供完整工具集:启用后,智能体可在“控制 Home Assistant”下选择“HA-MCP Server”,并在 Assist 聊天和语音中使用这些工具。启用只是让它可供选择——在您为某个智能体选中它之前,不会暴露任何内容。使用指南:{llm_api_docs_url}",
|
||||
"llm_api_exposure": "提供给对话智能体的工具集形态。工具搜索(默认)可让智能体的上下文保持精简:一个包含固定工具以及搜索/执行元工具的紧凑 API。完整目录会直接列出每个已暴露的工具——更适合大上下文模型。两者会将二者并列注册,让每个智能体在“控制 Home Assistant”下各自选择。按工具的暴露设置在 HA-MCP 设置面板中管理;详情:{llm_api_docs_url}",
|
||||
"enable_startup_notification": "每次服务器启动时显示通知,指向仅管理员可见的设置界面。关闭则静默启动——连接 URL 仍会出现在 Home Assistant 日志中。",
|
||||
"enable_sidebar_panel": "在侧边栏中显示 HA-MCP 设置面板(仅管理员)。关闭则移除侧边栏条目——服务器选项仍可在此界面中使用。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"server_start_failed": {
|
||||
"title": "HA-MCP 进程内服务器启动失败",
|
||||
"description": "无法在 Home Assistant 内部启动 HA-MCP 进程内服务器:\n\n{detail}\n\n请检查 Home Assistant 日志,然后重新加载该集成(或先修复根本问题再重新加载)以重试。"
|
||||
},
|
||||
"server_package_install_failed": {
|
||||
"title": "无法安装 HA-MCP 进程内服务器软件包",
|
||||
"description": "为进程内服务器安装 ha-mcp 软件包失败:\n\n{detail}\n\n请解决上述兼容性或安装问题,然后重新加载该集成以重试。"
|
||||
},
|
||||
"component_outdated": {
|
||||
"title": "请通过 HACS 更新 HA-MCP 自定义组件",
|
||||
"description": "已安装的 ha-mcp 服务器需要 HA-MCP 自定义组件 {required} 或更高版本,但您的版本是 {installed}。请通过 HACS 更新组件(打开 HA-MCP 自定义组件条目;如果尚未显示更新,请使用“Update information”),然后重启 Home Assistant。在此期间服务器仍会继续运行,但在组件更新之前,某些较新的功能可能无法使用。"
|
||||
},
|
||||
"server_update_held": {
|
||||
"title": "HA-MCP 服务器更新正在等待组件更新",
|
||||
"description": "ha-mcp 服务器 {latest} 已可用,但该发行版同时更新了 HA-MCP 自定义组件(更新至 {shipped};您正在运行 {running})。为避免启动一个当前组件从未测试过的服务器版本,服务器自动更新将暂缓,直到组件完成更新。\n\n请通过 HACS 更新组件(打开 HA-MCP 自定义组件条目;如果尚未显示更新,请使用“Update information”),然后重启 Home Assistant——之后服务器更新会自动安装。若仍要立即安装服务器更新,请在 HA-MCP 服务器更新实体上点击“安装”。"
|
||||
},
|
||||
"legacy_hacs_source": {
|
||||
"title": "组件安装自旧版仓库",
|
||||
"description": "HACS 正在跟踪此组件的主 ha-mcp 服务器仓库,因此 HACS 在此处显示的是服务器的版本号和发布说明,而非组件自身的 (2.x)。更新仍可正常工作,但会一直显示错误的标签。修复方法:从 HACS 中移除此仓库(您的集成设置和配置条目将保留),将 homeassistant-ai/ha-mcp-integration 添加为自定义仓库,从中重新安装组件,然后重启 Home Assistant。"
|
||||
},
|
||||
"legacy_oauth_restart": {
|
||||
"title": "重启 Home Assistant 以应用旧版 OAuth 设置",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "重启 Home Assistant?",
|
||||
"description": "提交此表单将重启 Home Assistant 并应用旧版 OAuth 设置。\n\n点击 <strong>提交</strong> 立即重启。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"server_channel": {
|
||||
"options": {
|
||||
"stable": "稳定版(推荐)",
|
||||
"dev": "开发版(最新构建)"
|
||||
}
|
||||
},
|
||||
"server_webhook_auth": {
|
||||
"options": {
|
||||
"none": "密钥 Webhook URL(默认)",
|
||||
"ha_auth": "使用 Home Assistant 登录(OAuth)",
|
||||
"legacy": "旧版 OAuth(此集成颁发客户端 ID/密钥供粘贴)"
|
||||
}
|
||||
},
|
||||
"llm_api_exposure": {
|
||||
"options": {
|
||||
"tool_search": "工具搜索(紧凑,默认)",
|
||||
"full": "完整目录",
|
||||
"both": "两者(按智能体选择)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"server_update": {
|
||||
"name": "更新"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"connect_direct_access": "从 Home Assistant 所在主机直接访问:{url}",
|
||||
"connect_local_lan": "本地/局域网(当网络访问为“Local network”时):{url}",
|
||||
"connect_remote_url": "远程连接 URL:{url}",
|
||||
"connect_urls_label": "连接 URL:",
|
||||
"connect_urls_pending": "服务器启动后,连接 URL 将显示在此处(以及 Home Assistant 日志中)。",
|
||||
"connect_webhook_disabled": "通过 Webhook 远程访问已关闭(仅本地模式)。",
|
||||
"oauth_creds_pending": "服务器启动后,Client ID 和 Client Secret 将显示在此处。",
|
||||
"oauth_not_serving": "旧版 OAuth 尚未启用这些凭据——请在 Home Assistant 提示时重启以激活它们。",
|
||||
"oauth_select_legacy_mode": "将上面的身份验证模式设为旧版 OAuth 并保存,以生成 Client ID 和 Client Secret。",
|
||||
"panel_hint": "打开 [HA-MCP 设置面板](/ha-mcp) 以管理工具和服务器设置。",
|
||||
"tools_module_installed": "Beta/高级文件与 YAML 工具模块(可选):已安装",
|
||||
"tools_module_not_installed": "Beta/高级文件与 YAML 工具模块(可选):未安装——请在此集成页面上点击“添加条目”并选择“HA-MCP File & YAML Tools”",
|
||||
"tools_module_not_loaded": "Beta/高级文件与 YAML 工具模块(可选):已安装但未加载——请在此集成页面上启用或重新加载“HA-MCP File & YAML Tools”条目",
|
||||
"version_line": "组件 {component_version} - 服务器 ha-mcp {server_version}({channel} 通道)",
|
||||
"version_not_installed": "尚未安装",
|
||||
"version_unknown": "未知"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,8 +109,10 @@ class ServerUpdateEntity(CoordinatorEntity[ServerVersionCoordinator], UpdateEnti
|
||||
|
||||
@property
|
||||
def auto_update(self) -> bool:
|
||||
"""Reflect the entry's automatic-update option."""
|
||||
return bool(self._entry.options.get(OPT_AUTO_UPDATE, DEFAULT_AUTO_UPDATE))
|
||||
"""Reflect the effective automatic-update policy."""
|
||||
return not self.coordinator.hass.config.skip_pip and bool(
|
||||
self._entry.options.get(OPT_AUTO_UPDATE, DEFAULT_AUTO_UPDATE)
|
||||
)
|
||||
|
||||
@property
|
||||
def release_url(self) -> str | None:
|
||||
@@ -126,8 +128,10 @@ class ServerUpdateEntity(CoordinatorEntity[ServerVersionCoordinator], UpdateEnti
|
||||
|
||||
@property
|
||||
def supported_features(self) -> UpdateEntityFeature:
|
||||
"""RELEASE_NOTES only on the stable channel — dev builds have no tags."""
|
||||
features = UpdateEntityFeature.INSTALL
|
||||
"""Expose install only when Home Assistant may manage the package."""
|
||||
features = UpdateEntityFeature(0)
|
||||
if not self.coordinator.hass.config.skip_pip:
|
||||
features |= UpdateEntityFeature.INSTALL
|
||||
data = self.coordinator.data
|
||||
if data is not None and data.dist != DIST_NAME_DEV:
|
||||
features |= UpdateEntityFeature.RELEASE_NOTES
|
||||
@@ -257,6 +261,14 @@ class ServerUpdateEntity(CoordinatorEntity[ServerVersionCoordinator], UpdateEnti
|
||||
that can still fail (review finding).
|
||||
"""
|
||||
data = self.coordinator.data
|
||||
if self.coordinator.hass.config.skip_pip:
|
||||
raise HomeAssistantError(
|
||||
"The HA-MCP server package is externally managed by the system "
|
||||
"package manager because Home Assistant was started with "
|
||||
"skip_pip. Install the update there, then reload this "
|
||||
"integration."
|
||||
)
|
||||
|
||||
target = version or self.latest_version
|
||||
if target is None:
|
||||
raise HomeAssistantError("No target version available to install.")
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
This module registers versioned ``ha_mcp_tools/*`` WebSocket commands that the
|
||||
ha-mcp server calls in-process (same HA core, no REST/WS round-trips) behind a
|
||||
capability gate. It registers twenty-two commands (twenty-two capabilities — the
|
||||
``search_visibility`` capability is a flag on the existing ``search`` command,
|
||||
and ``info`` itself carries no capability entry):
|
||||
capability gate. It registers twenty-three commands. It advertises twenty-five
|
||||
capabilities: twenty-two command capabilities plus three additive flags
|
||||
(dashboards_doc_search, search_visibility, and search_entity_membership);
|
||||
the info handshake carries no capability entry:
|
||||
|
||||
* ``ha_mcp_tools/info`` — the handshake: ``schema_version`` + ``capabilities[]``
|
||||
+ ``component_version`` + advisory ``limits`` + the instance ``timezone``
|
||||
@@ -13,6 +14,7 @@ and ``info`` itself carries no capability entry):
|
||||
negotiation, NOT a version floor).
|
||||
* ``ha_mcp_tools/search`` — a unified in-process search over live registries and
|
||||
states, joined and scored, mirroring today's ``ha_search`` response envelope.
|
||||
The search_entity_membership flag gates opt-in generic group metadata.
|
||||
* ``ha_mcp_tools/overview`` — the raw in-process reads the server's
|
||||
``get_system_overview`` + ``ha_get_overview`` wrapper consume (states,
|
||||
services, entity/device/area registries, ``hass.config``, persistent
|
||||
@@ -262,7 +264,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Collection, Mapping
|
||||
from dataclasses import dataclass
|
||||
from difflib import SequenceMatcher
|
||||
from pathlib import Path
|
||||
@@ -336,12 +338,15 @@ WS_BULK_CALL_SERVICE = f"{WS_API_PREFIX}/bulk_call_service"
|
||||
# bump it (the server checks ``schema_version >= N`` before using a new shape).
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
# Which commands exist. Grows one entry per shipped command; the server gates
|
||||
# Advertised command support and additive feature flags; the server gates
|
||||
# each consumer on ``capability in caps.capabilities``. Never remove an entry
|
||||
# without a major bump. (``info`` is always present in 1.1.0+, so it carries no
|
||||
# capability key of its own.)
|
||||
CAPABILITIES: list[str] = [
|
||||
"search",
|
||||
# A flag on search: gates its additive result_fields request and generic
|
||||
# is_group/member_entity_ids response fields.
|
||||
"search_entity_membership",
|
||||
"overview",
|
||||
"helpers_list",
|
||||
"states",
|
||||
@@ -526,9 +531,31 @@ _SPLIT_RE = re.compile(r"[._\-\s]+")
|
||||
def async_register_commands(hass: HomeAssistant) -> None:
|
||||
"""Register the ``ha_mcp_tools/*`` WebSocket commands.
|
||||
|
||||
Called from BOTH config-entry setups since component 2.1.0: the tools entry
|
||||
(alongside its service registrations) and the server entry (#2289/#2291).
|
||||
The surface is entry-agnostic — every handler reads HA-core state, none of
|
||||
it the tools entry's ``hass.data`` — so a server-entry-only install gets it
|
||||
too; only the filesystem/YAML HA *services* remain tools-entry-only.
|
||||
|
||||
Idempotent: HA's ``async_register_command`` overwrites an existing handler,
|
||||
so re-running on a config-entry reload is harmless. Called from the tools
|
||||
config-entry setup alongside the service registrations.
|
||||
so re-running on a config-entry reload, or from both entries on a dual-entry
|
||||
install, is harmless.
|
||||
|
||||
There is NO unregister. HA's ``websocket_api`` exposes no counterpart to
|
||||
``async_register_command``, so the commands outlive an entry unload and stay
|
||||
on the connection surface until Home Assistant restarts. That is deliberate
|
||||
and accepted (#2292). Unloading one entry must not strip the surface the
|
||||
other entry still serves from, and a surface left behind by a FULLY unloaded
|
||||
component holds no privilege of its own: every handler works off live
|
||||
HA-core state rather than anything the unloaded entry cached, HA core
|
||||
authenticates the connection, and ``@require_admin`` gates each command — so
|
||||
a caller reaching it can already do the same through HA's own WS API. The
|
||||
write commands are no exception: their D1 domain block refuses
|
||||
``domain == "ha_mcp_tools"`` unconditionally, so the leftover surface can
|
||||
never reach the privileged filesystem/YAML services, which
|
||||
:func:`~custom_components.ha_mcp_tools._async_unload_tools_entry` does remove
|
||||
on unload. Admin-gated commands answering from live core state until the
|
||||
next restart is the trade this makes.
|
||||
"""
|
||||
for schema, do_fn, prep in _command_specs():
|
||||
websocket_api.async_register_command(hass, _build_handler(schema, do_fn, prep))
|
||||
@@ -650,6 +677,7 @@ def _visibility_param_schema() -> Any:
|
||||
|
||||
|
||||
def _search_schema() -> dict[Any, Any]:
|
||||
"""Build the schema for search WebSocket requests."""
|
||||
return {
|
||||
vol.Required("type"): WS_SEARCH,
|
||||
vol.Optional("query"): vol.Any(str, None),
|
||||
@@ -657,6 +685,7 @@ def _search_schema() -> dict[Any, Any]:
|
||||
vol.Optional("domain_filter"): str,
|
||||
vol.Optional("area_filter"): str,
|
||||
vol.Optional("state_filter"): str,
|
||||
vol.Optional("result_fields"): [vol.In(("is_group", "member_entity_ids"))],
|
||||
vol.Optional("exact", default=True): bool,
|
||||
vol.Optional("include_hidden", default=True): bool,
|
||||
vol.Optional("include_config", default=False): bool,
|
||||
@@ -905,11 +934,18 @@ def _bulk_call_service_schema() -> dict[Any, Any]:
|
||||
def _do_info(hass: HomeAssistant | None = None) -> dict[str, Any]:
|
||||
"""Return the handshake payload.
|
||||
|
||||
``timezone`` is an additive field (``hass.config.time_zone``) consumers detect
|
||||
by presence — it carries NO capability entry and does NOT bump
|
||||
``timezone`` (``hass.config.time_zone``) and ``tools_services`` (whether
|
||||
the tools entry's HA services are registered) are additive fields consumers
|
||||
detect by presence — they carry NO capability entry and do NOT bump
|
||||
``schema_version``. ``hass`` is optional (defaulting to ``None`` so a direct
|
||||
``_do_info()`` still works for callers that only need the static handshake);
|
||||
when absent, ``timezone`` degrades to ``None``.
|
||||
when absent, both degrade to ``None``.
|
||||
|
||||
``tools_services`` exists because 2.1.0 registers this command surface from
|
||||
BOTH entry types (#2289): ``info`` answering no longer implies the tools
|
||||
entry — and its filesystem/YAML services — are present (#2292). The server's
|
||||
filesystem/YAML gate reads this field to keep raising its actionable
|
||||
"tools entry not set up" error on server-entry-only installs.
|
||||
"""
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
@@ -917,6 +953,7 @@ def _do_info(hass: HomeAssistant | None = None) -> dict[str, Any]:
|
||||
"capabilities": list(CAPABILITIES),
|
||||
"limits": dict(LIMITS),
|
||||
"timezone": _config_time_zone(hass),
|
||||
"tools_services": _tools_services_loaded(hass),
|
||||
}
|
||||
|
||||
|
||||
@@ -927,6 +964,23 @@ def _config_time_zone(hass: HomeAssistant | None) -> str | None:
|
||||
return tz if isinstance(tz, str) and tz else None
|
||||
|
||||
|
||||
def _tools_services_loaded(hass: HomeAssistant | None) -> bool | None:
|
||||
"""True when the tools entry's filesystem/YAML HA services are registered.
|
||||
|
||||
Probes the service registry itself (``read_file`` — one of the services
|
||||
``_async_setup_tools_entry`` registers and its unload removes) rather than
|
||||
the config-entry list, so the answer tracks what a caller can actually
|
||||
invoke. ``None`` when ``hass`` is absent (the direct ``_do_info()`` call);
|
||||
the WS handler always passes ``hass``, so the wire value is a bool.
|
||||
"""
|
||||
services = getattr(hass, "services", None)
|
||||
if services is None:
|
||||
return None
|
||||
# Literal matches SERVICE_READ_FILE in __init__.py (importing it here would
|
||||
# be circular).
|
||||
return bool(services.has_service(DOMAIN, "read_file"))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ha_mcp_tools/search
|
||||
# =============================================================================
|
||||
@@ -1011,6 +1065,7 @@ def _do_search(
|
||||
domain_filter = params.get("domain_filter")
|
||||
area_filter = params.get("area_filter")
|
||||
state_filter = params.get("state_filter")
|
||||
membership_requested = bool(params.get("result_fields"))
|
||||
# Opt-in visibility filter (search_visibility capability). A non-empty dict of
|
||||
# the server's raw VisibilityConfig fields; applied as a hard entity exclude.
|
||||
visibility = params.get("visibility")
|
||||
@@ -1023,6 +1078,7 @@ def _do_search(
|
||||
# filter is applied. Surfaced additively so the fast path isn't silent about
|
||||
# incomplete filtering (parity with the server's load_hidden_set warnings).
|
||||
visibility_warnings: list[str] = []
|
||||
hidden: set[str] = set()
|
||||
|
||||
# ``secret_values`` (loaded off-loop by _search_prep) scrubs resolved-!secret
|
||||
# plaintext from the config-body match corpus: a YAML-loaded automation/script/
|
||||
@@ -1045,6 +1101,7 @@ def _do_search(
|
||||
domain_filter=domain_filter,
|
||||
area_filter=area_filter,
|
||||
state_filter=state_filter,
|
||||
include_membership=membership_requested,
|
||||
)
|
||||
# Opt-in visibility filter: a hard exclude applied BEFORE counts/pagination,
|
||||
# exactly where the legacy path drops ``visibility_hidden`` entities at the
|
||||
@@ -1078,8 +1135,17 @@ def _do_search(
|
||||
scored_entities.sort(key=lambda r: (-r["score"], r["entity_id"]))
|
||||
entity_total = len(scored_entities)
|
||||
page = scored_entities[offset : offset + limit]
|
||||
_redact_hidden_members(
|
||||
page,
|
||||
hidden,
|
||||
view=view,
|
||||
include_hidden=include_hidden,
|
||||
enabled=membership_requested,
|
||||
)
|
||||
entity_has_more = offset + len(page) < entity_total
|
||||
entities = [_project_entity(r) for r in page]
|
||||
entities = [
|
||||
_project_entity(r, include_membership=membership_requested) for r in page
|
||||
]
|
||||
|
||||
# --- Config surfaces (automations + scripts + scenes + helpers) ----------
|
||||
# One combined pagination window, mirroring the server's config branch.
|
||||
@@ -1277,6 +1343,7 @@ def _search_entities(
|
||||
domain_filter: str | None,
|
||||
area_filter: str | None,
|
||||
state_filter: str | None,
|
||||
include_membership: bool = False,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Score every state against the query over the joined registry view."""
|
||||
results: list[dict[str, Any]] = []
|
||||
@@ -1286,7 +1353,7 @@ def _search_entities(
|
||||
# matches state_filter="vacation").
|
||||
state_filter_lower = state_filter.lower() if state_filter is not None else None
|
||||
for state in _iter_states(hass):
|
||||
rec = _entity_record(state, view)
|
||||
rec = _entity_record(state, view, include_membership=include_membership)
|
||||
if domain_filter and rec["domain"] != domain_filter:
|
||||
continue
|
||||
if rec["_hidden"] and not include_hidden:
|
||||
@@ -1448,12 +1515,15 @@ def _registry_enrichment(view: _RegistryView, entity_id: str) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _entity_record(state: Any, view: _RegistryView) -> dict[str, Any]:
|
||||
def _entity_record(
|
||||
state: Any, view: _RegistryView, *, include_membership: bool = False
|
||||
) -> dict[str, Any]:
|
||||
"""Join a state with the entity/device/area/floor/label registries."""
|
||||
entity_id = getattr(state, "entity_id", "") or ""
|
||||
domain = entity_id.split(".")[0] if "." in entity_id else ""
|
||||
attrs = getattr(state, "attributes", None) or {}
|
||||
friendly = attrs.get("friendly_name", entity_id)
|
||||
members = _normalize_member_entity_ids(attrs) if include_membership else None
|
||||
|
||||
join = _registry_enrichment(view, entity_id)
|
||||
area_name = join["area"]
|
||||
@@ -1481,6 +1551,14 @@ def _entity_record(state: Any, view: _RegistryView) -> dict[str, Any]:
|
||||
"floor": floor_name,
|
||||
"labels": label_names,
|
||||
"aliases": aliases,
|
||||
**(
|
||||
{
|
||||
"is_group": members is not None,
|
||||
**({"member_entity_ids": members} if members is not None else {}),
|
||||
}
|
||||
if include_membership
|
||||
else {}
|
||||
),
|
||||
"_hidden": join["_hidden"],
|
||||
"_area_id": join["_area_id"],
|
||||
"_match_texts": match_texts,
|
||||
@@ -1495,7 +1573,9 @@ def _entity_matches_area(rec: dict[str, Any], area_filter_lower: str) -> bool:
|
||||
return bool(area_name and str(area_name).lower() == area_filter_lower)
|
||||
|
||||
|
||||
def _project_entity(rec: dict[str, Any]) -> dict[str, Any]:
|
||||
def _project_entity(
|
||||
rec: dict[str, Any], *, include_membership: bool = False
|
||||
) -> dict[str, Any]:
|
||||
"""Strip internal ``_``-prefixed keys for the wire response."""
|
||||
return {
|
||||
"entity_id": rec["entity_id"],
|
||||
@@ -1508,9 +1588,80 @@ def _project_entity(rec: dict[str, Any]) -> dict[str, Any]:
|
||||
"aliases": rec["aliases"],
|
||||
"score": rec["score"],
|
||||
"match_type": rec["match_type"],
|
||||
**(
|
||||
{"is_group": rec["is_group"]}
|
||||
if include_membership and "is_group" in rec
|
||||
else {}
|
||||
),
|
||||
**(
|
||||
{"member_entity_ids": rec["member_entity_ids"]}
|
||||
if include_membership and "member_entity_ids" in rec
|
||||
else {}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _redact_hidden_members(
|
||||
records: list[dict[str, Any]],
|
||||
hidden: set[str],
|
||||
*,
|
||||
view: _RegistryView | None = None,
|
||||
include_hidden: bool = True,
|
||||
enabled: bool = True,
|
||||
) -> None:
|
||||
"""Withhold members excluded by visibility or include_hidden."""
|
||||
if not enabled:
|
||||
return
|
||||
for record in records:
|
||||
members = record.get("member_entity_ids")
|
||||
denied = bool(members and hidden.intersection(members))
|
||||
if members and not include_hidden and view is not None:
|
||||
denied = denied or any(
|
||||
getattr(_reg_entity(view, member), "hidden_by", None) is not None
|
||||
for member in members
|
||||
)
|
||||
if denied:
|
||||
record.pop("member_entity_ids", None)
|
||||
|
||||
|
||||
def _normalize_member_entity_ids(attributes: Any) -> list[str] | None:
|
||||
"""Normalize HA's modern or historical explicit group membership."""
|
||||
if not isinstance(attributes, Mapping):
|
||||
return None
|
||||
for key in ("group_entities", "entity_id"):
|
||||
raw = attributes.get(key)
|
||||
if isinstance(raw, (str, bytes, bytearray, Mapping)):
|
||||
continue
|
||||
if not isinstance(raw, Collection):
|
||||
continue
|
||||
members: set[str] = set()
|
||||
valid = True
|
||||
for value in raw:
|
||||
if not _is_entity_id(value):
|
||||
valid = False
|
||||
break
|
||||
members.add(value)
|
||||
if valid:
|
||||
return sorted(members)
|
||||
return None
|
||||
|
||||
|
||||
def _is_entity_id(value: Any) -> bool:
|
||||
"""Return whether a value has the Home Assistant entity ID shape."""
|
||||
if not isinstance(value, str) or value.count(".") != 1:
|
||||
return False
|
||||
domain, object_id = value.split(".", 1)
|
||||
return bool(
|
||||
domain
|
||||
and object_id
|
||||
and value == value.lower()
|
||||
and all(
|
||||
char in "abcdefghijklmnopqrstuvwxyz0123456789_"
|
||||
for char in domain + object_id
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# --- Config surfaces (automation/script/scene) -------------------------------
|
||||
def _search_config_surface(
|
||||
hass: HomeAssistant,
|
||||
@@ -3316,7 +3467,7 @@ def _do_config_entries(
|
||||
) -> dict[str, Any]:
|
||||
"""Return config entries in the ``config_entries/get`` WS shape.
|
||||
|
||||
``{entries: [{created_at, modified_at, entry_id, domain, title, state, source,
|
||||
``{entries: [{created_at, modified_at, entry_id, domain, unique_id, title, state, source,
|
||||
supports_options, supports_remove_device, supports_unload, supports_reconfigure,
|
||||
supported_subentry_types, pref_disable_new_entities, pref_disable_polling,
|
||||
disabled_by, reason, error_reason_translation_key,
|
||||
@@ -3324,7 +3475,9 @@ def _do_config_entries(
|
||||
The FULL ``as_json_fragment`` field set (``created_at`` / ``modified_at`` as
|
||||
``.timestamp()`` floats, ``supported_subentry_types`` as core emits it), so the
|
||||
component row carries the same fields the legacy REST row does — no field is
|
||||
dropped on the component path. Filtered by ``domain`` when
|
||||
dropped on the component path — PLUS ``unique_id``, the one deliberate
|
||||
superset field (core withholds it everywhere; the reconfigure identity
|
||||
anchors need it). Filtered by ``domain`` when
|
||||
given, or the single entry by ``entry_id``
|
||||
(``hass.config_entries.async_get_entry`` — an id that matches nothing,
|
||||
including an empty string, yields an empty list). Only a WHOLLY ABSENT
|
||||
@@ -3424,6 +3577,15 @@ def _config_entry_row(entry: Any, secret_values: frozenset[str]) -> dict[str, An
|
||||
"modified_at": _timestamp(getattr(entry, "modified_at", None)),
|
||||
"entry_id": getattr(entry, "entry_id", None),
|
||||
"domain": getattr(entry, "domain", None),
|
||||
# The ONE field this row adds beyond core's as_json_fragment. Core
|
||||
# deliberately withholds unique_id from every config-entry endpoint
|
||||
# (REST list, config_entries/get and get_single all serialize that
|
||||
# fragment), so a server needing it as an identity anchor has no other
|
||||
# source. Additive within schema_version 1: a server reading an older
|
||||
# component sees the KEY ABSENT, which is distinguishable from a
|
||||
# present-but-None value, so this needs no version gate — the same
|
||||
# discipline as device_get's opt-in entities join.
|
||||
"unique_id": getattr(entry, "unique_id", None),
|
||||
"title": getattr(entry, "title", None),
|
||||
"state": _enum_value(getattr(entry, "state", None)),
|
||||
"source": getattr(entry, "source", None),
|
||||
|
||||
@@ -8,6 +8,7 @@ from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.auth import EVENT_USER_REMOVED
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import (
|
||||
@@ -51,6 +52,7 @@ from .const import (
|
||||
CONF_INSTALL_ASSIST_SENTENCES,
|
||||
CONF_OBJECT,
|
||||
CONF_PANEL_ENABLED,
|
||||
CONF_SHOPPING_LIST_ENTITY,
|
||||
CONF_TASKS,
|
||||
CONF_WEEKLY_DIGEST_ENABLED,
|
||||
DEFAULT_PANEL_ENABLED,
|
||||
@@ -141,6 +143,7 @@ SERVICE_COMPLETE_SCHEMA = vol.Schema(
|
||||
# #133: when the maintenance was actually performed (backfill a past
|
||||
# completion). Future values are rejected by the coordinator.
|
||||
vol.Optional("completed_at"): cv.datetime,
|
||||
vol.Optional("via_tag_scan"): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -422,6 +425,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
await doc_store.async_load()
|
||||
hass.data[DOMAIN][DOCUMENT_STORE_KEY] = doc_store
|
||||
|
||||
|
||||
# Authenticated upload + serve endpoints for document blobs (the blobs live
|
||||
# under /config, so they must never be exposed via an unauthenticated path).
|
||||
from .views import async_register_document_views
|
||||
@@ -478,6 +482,9 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
reading_value=call.data.get("reading_value"),
|
||||
completed_by=completed_by,
|
||||
completed_at=call.data.get("completed_at"),
|
||||
# Proof of presence: an automation that reacted to the physical
|
||||
# scan itself passes via_tag_scan to satisfy require_tag_scan.
|
||||
tag_verified=bool(call.data.get("via_tag_scan")),
|
||||
)
|
||||
|
||||
async def _handle_reset(call: ServiceCall) -> None:
|
||||
@@ -765,7 +772,14 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
try:
|
||||
if action_type == "complete":
|
||||
_LOGGER.info("Completing task %s via notification action", task_id)
|
||||
await runtime_data.coordinator.complete_maintenance(task_id=task_id, unattended=True)
|
||||
await runtime_data.coordinator.complete_maintenance(
|
||||
task_id=task_id,
|
||||
unattended=True,
|
||||
# mobile_app fires the action with the registration's
|
||||
# user in the context - credit them (#128 attribution,
|
||||
# feeds least_completed rotation); bug audit 2026-08-29.
|
||||
completed_by=event.context.user_id if event.context else None,
|
||||
)
|
||||
elif action_type == "skip":
|
||||
_LOGGER.info("Skipping task %s via notification action", task_id)
|
||||
await runtime_data.coordinator.skip_maintenance(task_id=task_id, reason="Skipped from notification")
|
||||
@@ -835,6 +849,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
completed_by=user_id,
|
||||
notes="Completed via NFC tag",
|
||||
unattended=True,
|
||||
tag_verified=True,
|
||||
)
|
||||
except ServiceValidationError as err:
|
||||
# A tag tap cannot capture a cost or a photo. Log it
|
||||
@@ -862,7 +877,10 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
# (DeviceInfo.suggested_area only fires once, on first device creation).
|
||||
@callback
|
||||
def _object_entries_for_device(
|
||||
hass: HomeAssistant, device: dr.DeviceEntry
|
||||
# object: HA 2026.9 registry lookups return DeviceEntry |
|
||||
# ChildDeviceEntry (a union 2026.7 cannot import); the body reads
|
||||
# attributes via getattr already.
|
||||
hass: HomeAssistant, device: object
|
||||
) -> list[ConfigEntry]:
|
||||
"""Our object entries that live on this device.
|
||||
|
||||
@@ -894,7 +912,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
for ce in hass.config_entries.async_entries(DOMAIN):
|
||||
if ce.entry_id in seen or ce.unique_id == GLOBAL_UNIQUE_ID:
|
||||
continue
|
||||
if (ce.data.get(CONF_OBJECT) or {}).get("ha_device_id") == device.id:
|
||||
if (ce.data.get(CONF_OBJECT) or {}).get("ha_device_id") == getattr(device, "id", None):
|
||||
found.append(ce)
|
||||
return found
|
||||
|
||||
@@ -940,6 +958,21 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
|
||||
from .helpers.entity_rename import rewrite_store, rewrite_tasks
|
||||
|
||||
# v2.67: the global shopping-list target is an entity reference too.
|
||||
gentry = next(
|
||||
(e for e in hass.config_entries.async_entries(DOMAIN) if e.unique_id == GLOBAL_UNIQUE_ID),
|
||||
None,
|
||||
)
|
||||
if gentry is not None and gentry.options.get(CONF_SHOPPING_LIST_ENTITY) == old_eid:
|
||||
hass.config_entries.async_update_entry(
|
||||
gentry, options={**gentry.options, CONF_SHOPPING_LIST_ENTITY: new_eid}
|
||||
)
|
||||
from .shopping_sync import SHOPPING_SYNC_KEY
|
||||
|
||||
sync = hass.data.get(DOMAIN, {}).get(SHOPPING_SYNC_KEY)
|
||||
if sync is not None:
|
||||
await sync.async_handle_rename(old_eid, new_eid)
|
||||
|
||||
for ce in hass.config_entries.async_entries(DOMAIN):
|
||||
if ce.unique_id == GLOBAL_UNIQUE_ID:
|
||||
continue
|
||||
@@ -984,6 +1017,20 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
|
||||
unsub_entity = hass.bus.async_listen(er.EVENT_ENTITY_REGISTRY_UPDATED, _on_entity_registry_update)
|
||||
|
||||
# A deleted HA user used to be reconciled only at the next boot: the
|
||||
# least_completed rotation kept assigning the ghost meanwhile and the
|
||||
# admin-panel allowlist kept the stale id (bug audit 2026-08-29).
|
||||
async def _on_user_removed(_event: Event) -> None:
|
||||
await _check_task_responsible_user_orphans(hass)
|
||||
gentry = next(
|
||||
(e for e in hass.config_entries.async_entries(DOMAIN) if e.unique_id == GLOBAL_UNIQUE_ID),
|
||||
None,
|
||||
)
|
||||
if gentry is not None:
|
||||
await _check_admin_panel_user_orphans(hass, gentry)
|
||||
|
||||
unsub_user_removed = hass.bus.async_listen(EVENT_USER_REMOVED, _on_user_removed)
|
||||
|
||||
# v2.10.0: daily archive / auto-delete retention sweep (completed one-offs).
|
||||
# The pure decision logic + the sweep live in helpers/retention; this just
|
||||
# paces it once a day. First fire is +24h, which is plenty for a day-granular
|
||||
@@ -1003,6 +1050,16 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
await async_maybe_send_lead_reminders(hass)
|
||||
|
||||
unsub_digest = async_track_time_change(hass, _weekly_digest_tick, hour=8, minute=0, second=0)
|
||||
|
||||
# The 08:00 tick drops lead reminders inside quiet hours; a quiet window
|
||||
# ending after 08:00 therefore silenced them for good (bug audit
|
||||
# 2026-08-29). A second, reminder-only pass at noon catches those - the
|
||||
# per-day dedup inside makes it a no-op when 08:00 already delivered.
|
||||
async def _lead_reminder_retry(now: Any) -> None:
|
||||
await async_maybe_send_lead_reminders(hass)
|
||||
|
||||
unsub_lead_retry = async_track_time_change(hass, _lead_reminder_retry, hour=12, minute=0, second=0)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, lambda _e: unsub_lead_retry())
|
||||
# async_track_time_change has no cancel_on_shutdown, so cancel it explicitly
|
||||
# on HA stop too — otherwise the daily timer lingers past shutdown when the
|
||||
# entries aren't formally unloaded (e.g. test teardown).
|
||||
@@ -1010,6 +1067,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
|
||||
# Store unsub callbacks so they can be cleaned up when domain is unloaded
|
||||
hass.data[DOMAIN][EVENT_UNSUBS_KEY] = [
|
||||
unsub_user_removed,
|
||||
unsub_notification,
|
||||
unsub_tag,
|
||||
unsub_action,
|
||||
@@ -1017,6 +1075,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
unsub_entity,
|
||||
unsub_retention,
|
||||
unsub_digest,
|
||||
unsub_lead_retry,
|
||||
]
|
||||
|
||||
# Once HA has started (all entries loaded), flag object entries that were
|
||||
@@ -1195,6 +1254,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf
|
||||
entry.async_on_unload(summary_coordinator.async_teardown_listeners)
|
||||
await summary_coordinator.async_refresh()
|
||||
|
||||
# v2.67: mirror auto "buy" tasks into the user's to-do list (no-op
|
||||
# until the shopping_list_entity option is set). Owned by the GLOBAL
|
||||
# entry's lifecycle - created here, torn down in async_unload_entry -
|
||||
# so a reload of the hub entry gets a fresh instance (the shared-
|
||||
# runtime setup only runs once per boot; bug audit 2026-08-29).
|
||||
from .shopping_sync import SHOPPING_SYNC_KEY, ShoppingListSync
|
||||
|
||||
if SHOPPING_SYNC_KEY not in hass.data[DOMAIN]:
|
||||
shopping_sync = ShoppingListSync(hass)
|
||||
await shopping_sync.async_setup()
|
||||
hass.data[DOMAIN][SHOPPING_SYNC_KEY] = shopping_sync
|
||||
|
||||
# One-time migration: auto-enable advanced feature flags for existing users
|
||||
options = dict(entry.options or entry.data)
|
||||
if CONF_ADVANCED_ADAPTIVE not in options:
|
||||
@@ -1563,6 +1634,13 @@ async def _async_global_options_updated(hass: HomeAssistant, entry: ConfigEntry)
|
||||
await async_sync_assist_sentences(
|
||||
hass, entry.options.get(CONF_INSTALL_ASSIST_SENTENCES, False)
|
||||
)
|
||||
# v2.67: the shopping-list target may have changed (set / cleared / swapped).
|
||||
# This listener does NOT reload the entry, so the sync must be nudged
|
||||
# explicitly - it re-reads the option, re-arms its state listener and
|
||||
# pulls its rows out of a list that is no longer the target.
|
||||
from .shopping_sync import schedule_resync
|
||||
|
||||
schedule_resync(hass)
|
||||
|
||||
|
||||
_ORPHAN_ISSUE_PREFIX = "orphan_admin_panel_user_"
|
||||
@@ -1693,6 +1771,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: MaintenanceSupporterCon
|
||||
if entry.unique_id == GLOBAL_UNIQUE_ID:
|
||||
# Unregister panel when global entry is unloaded
|
||||
await async_unregister_panel(hass)
|
||||
from .shopping_sync import SHOPPING_SYNC_KEY
|
||||
|
||||
shopping_sync = hass.data.get(DOMAIN, {}).pop(SHOPPING_SYNC_KEY, None)
|
||||
if shopping_sync is not None:
|
||||
shopping_sync.async_teardown()
|
||||
|
||||
# Flush a pending debounced store save BEFORE tearing down — belt and
|
||||
# suspenders next to the store cache: disk is current the moment the entry
|
||||
@@ -1773,6 +1856,10 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
async_dispatcher_send(hass, SIGNAL_OBJECT_ENTRY_REMOVED, entry.entry_id)
|
||||
# Its buy-task rows in the shopping list are orphans now (bug audit 2026-08-29).
|
||||
from .shopping_sync import schedule_resync as _schedule_shopping_resync
|
||||
|
||||
_schedule_shopping_resync(hass)
|
||||
|
||||
|
||||
async def async_remove_config_entry_device(
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (místo uložení: {loc})",
|
||||
"stock_low": " — na úrovni objednacího limitu nebo pod ním",
|
||||
"stock_untracked": "{part} — u tohoto dílu sklad nesleduju.",
|
||||
"part_not_found": "Nenašel jsem náhradní díl, který by odpovídal '{name}'."
|
||||
"part_not_found": "Nenašel jsem náhradní díl, který by odpovídal '{name}'.",
|
||||
"st_phase": " — další krok: {phase}",
|
||||
"st_priority": ", vysoká priorita",
|
||||
"completed_phase": "'{task}' na {object} dokončeno — krok: {phase}. Příště: {next}.",
|
||||
"guide_phase": "Aktuální krok: {phase} (krok {index} z {count})."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (opbevaringssted {loc})",
|
||||
"stock_low": " — på eller under genbestillingsgrænsen",
|
||||
"stock_untracked": "Der føres ikke lager for {part}.",
|
||||
"part_not_found": "Jeg kunne ikke finde en reservedel, der passer på '{name}'."
|
||||
"part_not_found": "Jeg kunne ikke finde en reservedel, der passer på '{name}'.",
|
||||
"st_phase": " — næste trin: {phase}",
|
||||
"st_priority": ", høj prioritet",
|
||||
"completed_phase": "'{task}' på {object} udført — trin: {phase}. Næste gang: {next}.",
|
||||
"guide_phase": "Aktuelt trin: {phase} (trin {index} af {count})."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (Lagerort: {loc})",
|
||||
"stock_low": " — an oder unter der Nachbestellgrenze",
|
||||
"stock_untracked": "Für {part} wird kein Bestand geführt.",
|
||||
"part_not_found": "Ich habe kein Ersatzteil zu '{name}' gefunden."
|
||||
"part_not_found": "Ich habe kein Ersatzteil zu '{name}' gefunden.",
|
||||
"st_phase": " — nächster Schritt: {phase}",
|
||||
"st_priority": ", hohe Priorität",
|
||||
"completed_phase": "'{task}' an {object} erledigt — Schritt: {phase}. Beim nächsten Mal: {next}.",
|
||||
"guide_phase": "Aktueller Schritt: {phase} (Schritt {index} von {count})."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (stored at {loc})",
|
||||
"stock_low": " — at or below the reorder threshold",
|
||||
"stock_untracked": "Stock isn't tracked for {part}.",
|
||||
"part_not_found": "I couldn't find a spare part matching '{name}'."
|
||||
"part_not_found": "I couldn't find a spare part matching '{name}'.",
|
||||
"st_phase": " — next step: {phase}",
|
||||
"st_priority": ", high priority",
|
||||
"completed_phase": "Completed '{task}' on {object} — step done: {phase}. Next time: {next}.",
|
||||
"guide_phase": "Current step: {phase} (step {index} of {count})."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (guardado en {loc})",
|
||||
"stock_low": " — en el umbral de reposición o por debajo",
|
||||
"stock_untracked": "De {part} no se lleva control de stock.",
|
||||
"part_not_found": "No he encontrado ningún recambio que coincida con '{name}'."
|
||||
"part_not_found": "No he encontrado ningún recambio que coincida con '{name}'.",
|
||||
"st_phase": " — siguiente paso: {phase}",
|
||||
"st_priority": ", prioridad alta",
|
||||
"completed_phase": "'{task}' en {object} completado — paso: {phase}. La próxima vez: {next}.",
|
||||
"guide_phase": "Paso actual: {phase} (paso {index} de {count})."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (säilytyspaikka {loc})",
|
||||
"stock_low": " — tilausrajalla tai sen alapuolella",
|
||||
"stock_untracked": "Varastosaldoa ei seurata varaosalle {part}.",
|
||||
"part_not_found": "En löytänyt varaosaa, joka vastaisi hakua '{name}'."
|
||||
"part_not_found": "En löytänyt varaosaa, joka vastaisi hakua '{name}'.",
|
||||
"st_phase": " — seuraava vaihe: {phase}",
|
||||
"st_priority": ", korkea prioriteetti",
|
||||
"completed_phase": "'{task}' ({object}) valmis — vaihe: {phase}. Ensi kerralla: {next}.",
|
||||
"guide_phase": "Nykyinen vaihe: {phase} (vaihe {index}/{count})."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (rangé à {loc})",
|
||||
"stock_low": " — au niveau ou en dessous du seuil de réapprovisionnement",
|
||||
"stock_untracked": "Le stock n'est pas suivi pour {part}.",
|
||||
"part_not_found": "Je n'ai pas trouvé de pièce détachée qui corresponde à '{name}'."
|
||||
"part_not_found": "Je n'ai pas trouvé de pièce détachée qui corresponde à '{name}'.",
|
||||
"st_phase": " — prochaine étape : {phase}",
|
||||
"st_priority": ", priorité haute",
|
||||
"completed_phase": "« {task} » sur {object} terminé — étape : {phase}. La prochaine fois : {next}.",
|
||||
"guide_phase": "Étape actuelle : {phase} (étape {index} sur {count})."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (रखने की जगह: {loc})",
|
||||
"stock_low": " — दोबारा ऑर्डर करने की सीमा पर या उससे नीचे",
|
||||
"stock_untracked": "{part} के लिए स्टॉक ट्रैक नहीं किया जाता।",
|
||||
"part_not_found": "'{name}' से मिलता-जुलता कोई स्पेयर पार्ट नहीं मिला।"
|
||||
"part_not_found": "'{name}' से मिलता-जुलता कोई स्पेयर पार्ट नहीं मिला।",
|
||||
"st_phase": " — अगला चरण: {phase}",
|
||||
"st_priority": ", उच्च प्राथमिकता",
|
||||
"completed_phase": "{object} पर '{task}' पूर्ण — चरण: {phase}। अगली बार: {next}।",
|
||||
"guide_phase": "वर्तमान चरण: {phase} ({count} में से चरण {index})।"
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (tárolási hely: {loc})",
|
||||
"stock_low": " — az újrarendelési küszöbön vagy alatta",
|
||||
"stock_untracked": "Ehhez nincs készletnyilvántartás: {part}.",
|
||||
"part_not_found": "Nem találtam ehhez illő alkatrészt: '{name}'."
|
||||
"part_not_found": "Nem találtam ehhez illő alkatrészt: '{name}'.",
|
||||
"st_phase": " — következő lépés: {phase}",
|
||||
"st_priority": ", magas prioritás",
|
||||
"completed_phase": "'{task}' ({object}) elvégezve — lépés: {phase}. Legközelebb: {next}.",
|
||||
"guide_phase": "Aktuális lépés: {phase} ({index}. lépés a(z) {count} közül)."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (conservato in {loc})",
|
||||
"stock_low": " — pari o sotto la soglia di riordino",
|
||||
"stock_untracked": "Per {part} non viene tenuta la giacenza.",
|
||||
"part_not_found": "Non ho trovato nessun ricambio che corrisponda a '{name}'."
|
||||
"part_not_found": "Non ho trovato nessun ricambio che corrisponda a '{name}'.",
|
||||
"st_phase": " — prossimo passo: {phase}",
|
||||
"st_priority": ", priorità alta",
|
||||
"completed_phase": "'{task}' su {object} completato — passo: {phase}. La prossima volta: {next}.",
|
||||
"guide_phase": "Passo attuale: {phase} (passo {index} di {count})."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": "(保管場所は{loc})",
|
||||
"stock_low": "。在庫は発注の目安以下です",
|
||||
"stock_untracked": "{part}の在庫は管理していません。",
|
||||
"part_not_found": "「{name}」に一致する交換部品は見つかりませんでした。"
|
||||
"part_not_found": "「{name}」に一致する交換部品は見つかりませんでした。",
|
||||
"st_phase": "(次のステップ: {phase})",
|
||||
"st_priority": "、優先度高",
|
||||
"completed_phase": "{object}の「{task}」を完了しました(ステップ: {phase})。次回は: {next}。",
|
||||
"guide_phase": "現在のステップ: {phase}({count}ステップ中{index}番目)。"
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (보관 위치: {loc})",
|
||||
"stock_low": ". 재주문 기준 이하예요",
|
||||
"stock_untracked": "{part} 재고는 관리하지 않아요.",
|
||||
"part_not_found": "'{name}'에 맞는 예비 부품을 찾지 못했어요."
|
||||
"part_not_found": "'{name}'에 맞는 예비 부품을 찾지 못했어요.",
|
||||
"st_phase": " — 다음 단계: {phase}",
|
||||
"st_priority": ", 높은 우선순위",
|
||||
"completed_phase": "{object}의 '{task}' 완료 — 단계: {phase}. 다음번: {next}.",
|
||||
"guide_phase": "현재 단계: {phase} ({count}단계 중 {index}단계)."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (oppbevaringssted {loc})",
|
||||
"stock_low": " — på eller under bestillingsgrensen",
|
||||
"stock_untracked": "Det føres ikke lager for {part}.",
|
||||
"part_not_found": "Jeg fant ingen reservedel som passer med '{name}'."
|
||||
"part_not_found": "Jeg fant ingen reservedel som passer med '{name}'.",
|
||||
"st_phase": " — neste trinn: {phase}",
|
||||
"st_priority": ", høy prioritet",
|
||||
"completed_phase": "'{task}' på {object} fullført — trinn: {phase}. Neste gang: {next}.",
|
||||
"guide_phase": "Gjeldende trinn: {phase} (trinn {index} av {count})."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (ligt in {loc})",
|
||||
"stock_low": " — op of onder de bestelgrens",
|
||||
"stock_untracked": "Van {part} wordt de voorraad niet bijgehouden.",
|
||||
"part_not_found": "Ik kon geen onderdeel vinden dat past bij '{name}'."
|
||||
"part_not_found": "Ik kon geen onderdeel vinden dat past bij '{name}'.",
|
||||
"st_phase": " — volgende stap: {phase}",
|
||||
"st_priority": ", hoge prioriteit",
|
||||
"completed_phase": "'{task}' op {object} afgerond — stap: {phase}. Volgende keer: {next}.",
|
||||
"guide_phase": "Huidige stap: {phase} (stap {index} van {count})."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (miejsce przechowywania: {loc})",
|
||||
"stock_low": " — na poziomie progu zamówienia lub poniżej",
|
||||
"stock_untracked": "{part} — nie prowadzę stanu magazynowego dla tej części.",
|
||||
"part_not_found": "Nie znalazłem części zamiennej pasującej do '{name}'."
|
||||
"part_not_found": "Nie znalazłem części zamiennej pasującej do '{name}'.",
|
||||
"st_phase": " — następny krok: {phase}",
|
||||
"st_priority": ", wysoki priorytet",
|
||||
"completed_phase": "Ukończono '{task}' ({object}) — krok: {phase}. Następnym razem: {next}.",
|
||||
"guide_phase": "Bieżący krok: {phase} (krok {index} z {count})."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (local: {loc})",
|
||||
"stock_low": " — no limite de reposição ou abaixo dele",
|
||||
"stock_untracked": "Não há controle de estoque para {part}.",
|
||||
"part_not_found": "Não encontrei nenhuma peça de reposição que corresponda a '{name}'."
|
||||
"part_not_found": "Não encontrei nenhuma peça de reposição que corresponda a '{name}'.",
|
||||
"st_phase": " — próximo passo: {phase}",
|
||||
"st_priority": ", prioridade alta",
|
||||
"completed_phase": "'{task}' em {object} concluído — passo: {phase}. Na próxima vez: {next}.",
|
||||
"guide_phase": "Passo atual: {phase} (passo {index} de {count})."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (guardado em {loc})",
|
||||
"stock_low": " — no limite de reposição ou abaixo",
|
||||
"stock_untracked": "O stock de {part} não é acompanhado.",
|
||||
"part_not_found": "Não encontrei nenhuma peça que corresponda a '{name}'."
|
||||
"part_not_found": "Não encontrei nenhuma peça que corresponda a '{name}'.",
|
||||
"st_phase": " — próximo passo: {phase}",
|
||||
"st_priority": ", prioridade alta",
|
||||
"completed_phase": "'{task}' em {object} concluído — passo: {phase}. Da próxima vez: {next}.",
|
||||
"guide_phase": "Passo atual: {phase} (passo {index} de {count})."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (место хранения: {loc})",
|
||||
"stock_low": " — на уровне порога дозаказа или ниже",
|
||||
"stock_untracked": "{part} — запас по этой детали не отслеживается.",
|
||||
"part_not_found": "Я не нашёл запчасть, подходящую под '{name}'."
|
||||
"part_not_found": "Я не нашёл запчасть, подходящую под '{name}'.",
|
||||
"st_phase": " — следующий шаг: {phase}",
|
||||
"st_priority": ", высокий приоритет",
|
||||
"completed_phase": "«{task}» ({object}) выполнено — шаг: {phase}. В следующий раз: {next}.",
|
||||
"guide_phase": "Текущий шаг: {phase} (шаг {index} из {count})."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (förvaringsplats {loc})",
|
||||
"stock_low": " — vid eller under beställningsgränsen",
|
||||
"stock_untracked": "Lagersaldot följs inte för {part}.",
|
||||
"part_not_found": "Jag hittade ingen reservdel som matchar '{name}'."
|
||||
"part_not_found": "Jag hittade ingen reservdel som matchar '{name}'.",
|
||||
"st_phase": " — nästa steg: {phase}",
|
||||
"st_priority": ", hög prioritet",
|
||||
"completed_phase": "'{task}' på {object} slutfört — steg: {phase}. Nästa gång: {next}.",
|
||||
"guide_phase": "Aktuellt steg: {phase} (steg {index} av {count})."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (saklama yeri: {loc})",
|
||||
"stock_low": " — yeniden sipariş sınırında ya da altında",
|
||||
"stock_untracked": "{part} için stok takibi yapılmıyor.",
|
||||
"part_not_found": "'{name}' ile eşleşen bir yedek parça bulamadım."
|
||||
"part_not_found": "'{name}' ile eşleşen bir yedek parça bulamadım.",
|
||||
"st_phase": " — sonraki adım: {phase}",
|
||||
"st_priority": ", yüksek öncelik",
|
||||
"completed_phase": "{object} üzerindeki '{task}' tamamlandı — adım: {phase}. Bir dahaki sefere: {next}.",
|
||||
"guide_phase": "Geçerli adım: {phase} ({count} adımdan {index}.)."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": " (місце зберігання: {loc})",
|
||||
"stock_low": " — на рівні порога дозамовлення або нижче",
|
||||
"stock_untracked": "{part} — запас цієї деталі не відстежується.",
|
||||
"part_not_found": "Я не знайшов запчастину, яка відповідає '{name}'."
|
||||
"part_not_found": "Я не знайшов запчастину, яка відповідає '{name}'.",
|
||||
"st_phase": " — наступний крок: {phase}",
|
||||
"st_priority": ", високий пріоритет",
|
||||
"completed_phase": "«{task}» ({object}) виконано — крок: {phase}. Наступного разу: {next}.",
|
||||
"guide_phase": "Поточний крок: {phase} (крок {index} з {count})."
|
||||
}
|
||||
|
||||
@@ -38,5 +38,9 @@
|
||||
"stock_loc": "(存放在{loc})",
|
||||
"stock_low": ",已达到或低于补货下限",
|
||||
"stock_untracked": "{part}的库存没有记录。",
|
||||
"part_not_found": "我没有找到和“{name}”对得上的备件。"
|
||||
"part_not_found": "我没有找到和“{name}”对得上的备件。",
|
||||
"st_phase": "——下一步:{phase}",
|
||||
"st_priority": ",高优先级",
|
||||
"completed_phase": "已完成{object}的“{task}”——步骤:{phase}。下次:{next}。",
|
||||
"guide_phase": "当前步骤:{phase}(第 {index} 步,共 {count} 步)。"
|
||||
}
|
||||
|
||||
@@ -117,7 +117,10 @@ class MaintenanceActionButton(MaintenanceEntity, ButtonEntity):
|
||||
raise HomeAssistantError(f"Task {self._task_id} no longer exists")
|
||||
if self._action == "complete":
|
||||
await self.coordinator.complete_maintenance(
|
||||
self._task_id, notes="Completed from dashboard button", unattended=True
|
||||
self._task_id,
|
||||
notes="Completed from dashboard button",
|
||||
unattended=True,
|
||||
completed_by=self._context.user_id if self._context else None,
|
||||
)
|
||||
elif self._action == "skip":
|
||||
await self.coordinator.skip_maintenance(self._task_id, reason="Skipped from dashboard button")
|
||||
|
||||
@@ -129,7 +129,10 @@ class MaintenanceSupporterConfigFlow(ScheduleStepsMixin, TriggerConfigMixin, Con
|
||||
{
|
||||
vol.Optional(CONF_DEFAULT_WARNING_DAYS, default=DEFAULT_WARNING_DAYS): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=1, max=WARNING_DAYS_RANGE[1], step=1, mode=selector.NumberSelectorMode.BOX
|
||||
min=WARNING_DAYS_RANGE[0],
|
||||
max=WARNING_DAYS_RANGE[1],
|
||||
step=1,
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_NOTIFICATIONS_ENABLED, default=False): selector.BooleanSelector(),
|
||||
|
||||
@@ -54,6 +54,7 @@ from .const import (
|
||||
CONF_QUIET_HOURS_ENABLED,
|
||||
CONF_QUIET_HOURS_END,
|
||||
CONF_QUIET_HOURS_START,
|
||||
CONF_SHOPPING_LIST_ENTITY,
|
||||
CONF_SNOOZE_DURATION_HOURS,
|
||||
DEFAULT_BUDGET_CURRENCY,
|
||||
DEFAULT_MAX_NOTIFICATIONS_PER_DAY,
|
||||
@@ -576,6 +577,14 @@ class GlobalOptionsFlow(OptionsFlow):
|
||||
else:
|
||||
user_input[CONF_NOTIFY_SERVICE] = normalized
|
||||
|
||||
# Shopping-list target: a cleared EntitySelector omits the key —
|
||||
# normalise to "" so clearing actually turns the sync off. Format
|
||||
# only (todo.*): the list's integration may load later.
|
||||
raw_list = (user_input.get(CONF_SHOPPING_LIST_ENTITY) or "").strip()
|
||||
if raw_list and not raw_list.startswith("todo."):
|
||||
errors[CONF_SHOPPING_LIST_ENTITY] = "invalid_shopping_list_entity"
|
||||
user_input[CONF_SHOPPING_LIST_ENTITY] = raw_list
|
||||
|
||||
# Trim + cap the optional sidebar title; blank clears the override
|
||||
# (panel falls back to the default "Maintenance").
|
||||
raw_title = user_input.get(CONF_PANEL_TITLE)
|
||||
@@ -631,6 +640,12 @@ class GlobalOptionsFlow(OptionsFlow):
|
||||
custom_value=True,
|
||||
)
|
||||
),
|
||||
# v2.67: mirror auto "buy" tasks into this to-do list
|
||||
# (blank = off). suggested_value keeps the field clearable.
|
||||
vol.Optional(
|
||||
CONF_SHOPPING_LIST_ENTITY,
|
||||
description={"suggested_value": current.get(CONF_SHOPPING_LIST_ENTITY, "")},
|
||||
): selector.EntitySelector(selector.EntitySelectorConfig(domain="todo")),
|
||||
vol.Optional(
|
||||
CONF_PANEL_ENABLED,
|
||||
default=current.get(CONF_PANEL_ENABLED, DEFAULT_PANEL_ENABLED),
|
||||
|
||||
@@ -264,6 +264,8 @@ class TaskCrudMixin:
|
||||
updated_task[CONF_TASK_NFC_TAG] = nfc_val
|
||||
else:
|
||||
updated_task.pop(CONF_TASK_NFC_TAG, None)
|
||||
# Proof of presence: completion only via NFC/QR scan.
|
||||
updated_task["require_tag_scan"] = bool(user_input.get("require_tag_scan"))
|
||||
# v2.20 (#83): reading unit — clear by submitting "".
|
||||
if CONF_TASK_READING_UNIT in user_input:
|
||||
ru = (user_input.get(CONF_TASK_READING_UNIT) or "").strip()
|
||||
@@ -483,6 +485,7 @@ class TaskCrudMixin:
|
||||
default=", ".join(task.get("labels", [])),
|
||||
): selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)),
|
||||
nfc_tag_key: selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)),
|
||||
vol.Optional("require_tag_scan", default=bool(task.get("require_tag_scan"))): selector.BooleanSelector(),
|
||||
reading_unit_key: selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)),
|
||||
vol.Optional("go_back", default=False): selector.BooleanSelector(),
|
||||
}
|
||||
|
||||
@@ -111,6 +111,9 @@ SOUTHERN_SEASONS: dict[str, list[int]] = {
|
||||
CONF_DEFAULT_WARNING_DAYS = "default_warning_days"
|
||||
CONF_NOTIFICATIONS_ENABLED = "notifications_enabled"
|
||||
CONF_NOTIFY_SERVICE = "notify_service"
|
||||
# v2.67: mirror auto "buy" tasks into a user-picked HA to-do list (todo.*).
|
||||
# Empty = feature off. Instance-bound → excluded from settings export.
|
||||
CONF_SHOPPING_LIST_ENTITY = "shopping_list_entity"
|
||||
CONF_PANEL_ENABLED = "panel_enabled"
|
||||
# v2.10.4 (#69 follow-up): the sidebar panel is the integration's hub — the
|
||||
# auto-dashboard's "Open Maintenance panel" button, QR codes, and notifications
|
||||
@@ -450,6 +453,20 @@ DEFAULT_DEGRADATION_LOOKBACK_DAYS = 30 # Days of recorder data for slope comput
|
||||
DEFAULT_DEGRADATION_MIN_POINTS = 10 # Min hourly data points to compute regression
|
||||
DEFAULT_DEGRADATION_SIGNIFICANCE = 0.05 # Min |slope|/mean ratio for rising/falling
|
||||
|
||||
# --- Cycle-aware degradation (ET-4800 ink finding, 2026-08) ---
|
||||
# Consumption sensors are sawtooths: the value drifts toward the threshold and
|
||||
# JUMPS back on a refill/service. A regression across that jump reports the
|
||||
# refill as a rising trend. The analysis therefore splits the series into
|
||||
# service cycles at significant against-the-drift jumps, regresses the CURRENT
|
||||
# cycle, and LEARNS the typical rate from prior cycles (median, blended in
|
||||
# while the current cycle is still thin).
|
||||
DEGRADATION_CYCLE_LOOKBACK_DAYS = 180 # window for learning prior cycles
|
||||
DEGRADATION_CYCLE_JUMP_FRACTION = 0.2 # jump ≥ this share of the observed range = boundary
|
||||
DEGRADATION_BLEND_HALF_POINTS = 48 # current-cycle points at which current/history blend 50/50
|
||||
DEGRADATION_MIN_CYCLE_HOURS = 24 # prior cycles shorter than this teach nothing
|
||||
DEGRADATION_CYCLE_NOISE_SIGMAS = 8 # a boundary must also exceed this many robust sigmas of hourly wobble
|
||||
DEGRADATION_CYCLE_SETTLE_POINTS = 24 # a jump that snaps back within this many points is modulation
|
||||
|
||||
# --- Environmental Adjustment Defaults (Phase 3) ---
|
||||
DEFAULT_ENVIRONMENTAL_LOOKBACK_DAYS = 90 # Days of env data for correlation
|
||||
DEFAULT_ENVIRONMENTAL_CORRELATION_MIN = 0.3 # Min |r| to apply adjustment
|
||||
|
||||
@@ -193,6 +193,9 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self._recent_manual_completions = {
|
||||
tid: ts for tid, ts in self._recent_manual_completions.items() if now_mono - ts < MANUAL_COMPLETION_DEDUP_SECONDS
|
||||
}
|
||||
self._recent_backfills = {
|
||||
key: ts for key, ts in self._recent_backfills.items() if now_mono - ts < MANUAL_COMPLETION_DEDUP_SECONDS
|
||||
}
|
||||
|
||||
result: dict[str, Any] = {
|
||||
CONF_OBJECT: obj.to_dict(),
|
||||
@@ -357,6 +360,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
task_result["_degradation_trend"] = deg.trend
|
||||
task_result["_degradation_r_squared"] = deg.r_squared
|
||||
task_result["_degradation_data_points"] = deg.data_points
|
||||
task_result["_prediction_cycles"] = deg.cycles_learned
|
||||
|
||||
# Threshold prediction
|
||||
if prediction.threshold_prediction:
|
||||
@@ -716,6 +720,15 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
_vac = get_vacation_state(self.hass)
|
||||
notifiable = [row for row in notifiable if not _vac.is_silent_for(row[0])]
|
||||
# A status the user switched off, or a task they snoozed, must neither
|
||||
# count toward the bundle threshold nor ride inside the bundle - the
|
||||
# per-task path checks both, the bundle path did not (bug audit
|
||||
# 2026-08-29).
|
||||
notifiable = [
|
||||
row
|
||||
for row in notifiable
|
||||
if nm._is_status_enabled(row[2]) and not nm._is_snoozed(f"{self.entry.entry_id}_{row[0]}_{row[2]}")
|
||||
]
|
||||
|
||||
# v2.26 notification routing: a saved-view scope ("only notify about
|
||||
# view X") drops tasks the view's label/user filters don't match —
|
||||
@@ -940,6 +953,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
auto: bool = False,
|
||||
unattended: bool = False,
|
||||
completed_at: datetime | None = None,
|
||||
tag_verified: bool = False,
|
||||
) -> None:
|
||||
"""Mark a task as completed and persist.
|
||||
|
||||
@@ -1012,6 +1026,55 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
},
|
||||
)
|
||||
|
||||
# Proof of presence: a task flagged require_tag_scan only completes
|
||||
# from a surface that proves someone stood at the thing — an NFC tap
|
||||
# or the QR quick-complete. Checked at THIS choke point so every
|
||||
# other surface (panel, card, to-do, voice, notification button)
|
||||
# gets the same refusal. Automatic completions are exempt (a
|
||||
# recovered trigger has nobody to send to the machine), and
|
||||
# automations may assert the scan via the service's `via_tag_scan`.
|
||||
if not auto and not tag_verified and merged[task_id].get("require_tag_scan"):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="tag_scan_required",
|
||||
translation_placeholders={
|
||||
"task_name": str(merged[task_id].get("name", task_id)),
|
||||
},
|
||||
)
|
||||
|
||||
# Inert tasks (bug audit 2026-08-29): the auto path refuses archived /
|
||||
# disabled tasks and paused objects (see async_auto_complete_on_recovery),
|
||||
# but the manual path let an NFC sticker on a retired machine, a stale
|
||||
# panel or a voice command record a completion on a task nobody can
|
||||
# see any more - rotation advanced, parts consumed. Same gate here.
|
||||
if not auto:
|
||||
_td = merged[task_id]
|
||||
if (
|
||||
_td.get("archived_at") is not None
|
||||
or _td.get("enabled") is False
|
||||
or self.entry.data.get(CONF_OBJECT, {}).get("paused_at") is not None
|
||||
):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="task_inactive",
|
||||
translation_placeholders={"task_name": str(_td.get("name", task_id))},
|
||||
)
|
||||
|
||||
# Completion window (earliest_completion_days) - at the choke point like
|
||||
# every other rule. The WS / to-do / voice surfaces pre-check it for a
|
||||
# friendly code, but the service, the dashboard button, an NFC tap and a
|
||||
# notification button reached this method with no gate at all (bug audit
|
||||
# 2026-08-29). A completion dated on a PAST day is a history correction,
|
||||
# not early work, and bypasses the window exactly like the WS pre-check.
|
||||
if not auto:
|
||||
_past_dated = completed_at is not None and completed_at.date() < dt_util.now().date()
|
||||
if not _past_dated and not MaintenanceTask.from_dict(merged[task_id]).can_complete_now:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="too_early",
|
||||
translation_placeholders={"task_name": str(merged[task_id].get("name", task_id))},
|
||||
)
|
||||
|
||||
# Household double-complete guard (journey M1): two people seeing the
|
||||
# same overdue task and both tapping Complete within seconds would
|
||||
# record two completions — duplicated history/cost and a DOUBLE
|
||||
@@ -1077,8 +1140,20 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
# stay correctable through the history editor like dialog ones.
|
||||
from .helpers.phases import effective_field as _phase_effective
|
||||
|
||||
# A pure backfill (dated before the last completion) performed no work
|
||||
# in the CURRENT cycle: the automatic consumes_parts / buy-task restock
|
||||
# must not run for it (bug audit 2026-08-29) - an explicit used_parts
|
||||
# selection is still honoured. Mirrors MaintenanceTask.complete's
|
||||
# is_latest split (ISO-date string compare).
|
||||
_is_backfill = (
|
||||
completed_at is not None
|
||||
and bool(task.last_performed)
|
||||
and effective_ts.date().isoformat() < str(task.last_performed)
|
||||
)
|
||||
record_links = (
|
||||
used_parts if used_parts is not None else (_phase_effective(merged[task_id], "consumes_parts") or [])
|
||||
used_parts
|
||||
if used_parts is not None
|
||||
else ([] if _is_backfill else (_phase_effective(merged[task_id], "consumes_parts") or []))
|
||||
)
|
||||
enriched_used: list[dict[str, Any]] | None = None
|
||||
if used_parts is not None or record_links:
|
||||
@@ -1144,10 +1219,24 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
task.adaptive_config["hemisphere"] = "south" if (self.hass.config.latitude or 0) < 0 else "north"
|
||||
task.adaptive_config["_current_month"] = effective_ts.month
|
||||
task.adaptive_config["_current_date"] = effective_ts.date().isoformat()
|
||||
updated_config = analyzer.update_on_completion(task.adaptive_config, actual_interval, feedback)
|
||||
task.adaptive_config = updated_config
|
||||
try:
|
||||
updated_config = analyzer.update_on_completion(task.adaptive_config, actual_interval, feedback)
|
||||
task.adaptive_config = updated_config
|
||||
except (TypeError, ValueError, ZeroDivisionError): # hand-edited / imported adaptive_config
|
||||
_LOGGER.warning("Adaptive learning skipped for %s: malformed adaptive_config", task_id)
|
||||
|
||||
await self._persist_and_signal_task_change(task_id, task)
|
||||
try:
|
||||
await self._persist_and_signal_task_change(task_id, task)
|
||||
except Exception:
|
||||
# The completion did not land on disk - release the double-tap
|
||||
# guard stamped above, or the user's retry within 30 s would be
|
||||
# swallowed as a duplicate of a completion that never happened
|
||||
# (bug audit 2026-08-29).
|
||||
if completed_at is None and not auto:
|
||||
self._recent_manual_completions.pop(task_id, None)
|
||||
elif completed_at is not None:
|
||||
self._recent_backfills.pop((task_id, completed_at.isoformat()), None)
|
||||
raise
|
||||
|
||||
# Shared-task rotation: advance_rotation() (inside task.complete)
|
||||
# mutates responsible_user_id, which is a STATIC config field — the
|
||||
@@ -1172,16 +1261,24 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
from .parts_runtime import async_handle_completion_parts
|
||||
|
||||
try:
|
||||
await async_handle_completion_parts(
|
||||
self.hass,
|
||||
self.entry,
|
||||
merged[task_id],
|
||||
restock_quantity=restock_quantity,
|
||||
used_parts=used_parts,
|
||||
)
|
||||
if is_latest or used_parts is not None:
|
||||
await async_handle_completion_parts(
|
||||
self.hass,
|
||||
self.entry,
|
||||
merged[task_id],
|
||||
restock_quantity=restock_quantity,
|
||||
used_parts=used_parts,
|
||||
)
|
||||
except Exception:
|
||||
_LOGGER.exception("Part consumption failed for task %s", task_id)
|
||||
|
||||
# A status configured to notify ONCE (interval 0) stays silenced by the
|
||||
# _SENT_ONCE sentinel until something clears it; nothing did after a
|
||||
# completion, so a sensor task that re-triggered weeks later was never
|
||||
# announced again until HA restarted (bug audit 2026-08-29).
|
||||
if is_latest:
|
||||
self._clear_notification_state(task_id)
|
||||
|
||||
_LOGGER.debug("Maintenance completed: %s on %s", task.name, self.maintenance_object.name)
|
||||
|
||||
# Fire event after persistence — power users wire HA automations on
|
||||
@@ -1276,6 +1373,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
# A reset starts a new cycle — a following completion is a NEW
|
||||
# real-world action, never a double-tap of the previous one.
|
||||
self._recent_manual_completions.pop(task_id, None)
|
||||
self._clear_notification_state(task_id)
|
||||
merged = self._get_merged_tasks_data()
|
||||
if task_id not in merged:
|
||||
_LOGGER.error("Task %s not found in entry %s", task_id, self.entry.title)
|
||||
@@ -1310,6 +1408,18 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
await self._persist_and_signal_task_change(task_id, task)
|
||||
_LOGGER.debug("Occurrence postponed to %s: %s on %s", until, task.name, self.maintenance_object.name)
|
||||
|
||||
def _clear_notification_state(self, task_id: str) -> None:
|
||||
"""Forget the per-status notification bookkeeping for a task.
|
||||
|
||||
The notification manager lives on the shared runtime; it may be absent
|
||||
in tests or during teardown - then there is nothing to clear.
|
||||
"""
|
||||
from .const import NOTIFICATION_MANAGER_KEY
|
||||
|
||||
nm = self.hass.data.get(DOMAIN, {}).get(NOTIFICATION_MANAGER_KEY)
|
||||
if nm is not None:
|
||||
nm.clear_task_state(self.entry.entry_id, task_id)
|
||||
|
||||
async def skip_maintenance(
|
||||
self,
|
||||
task_id: str,
|
||||
@@ -1320,6 +1430,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
# Same as reset: skipping restarts the cycle — clear the double-tap
|
||||
# window so a follow-up completion counts.
|
||||
self._recent_manual_completions.pop(task_id, None)
|
||||
self._clear_notification_state(task_id)
|
||||
merged = self._get_merged_tasks_data()
|
||||
if task_id not in merged:
|
||||
_LOGGER.error("Task %s not found in entry %s", task_id, self.entry.title)
|
||||
|
||||
@@ -58,7 +58,12 @@ class MaintenanceEntity(CoordinatorEntity[MaintenanceCoordinator]):
|
||||
coordinator.hass, device_id, own_entry_id=coordinator.entry.entry_id
|
||||
)
|
||||
if resolved is not None:
|
||||
self._linked_device = dr.async_get(coordinator.hass).async_get(resolved)
|
||||
# Any hop: HA 2026.9 lookups return DeviceEntry |
|
||||
# ChildDeviceEntry (both attach to entities the same way);
|
||||
# 2026.7 returns the plain entry — a cast would be redundant
|
||||
# on one version and required on the other.
|
||||
resolved_device: Any = dr.async_get(coordinator.hass).async_get(resolved)
|
||||
self._linked_device = resolved_device
|
||||
if self._linked_device is not None:
|
||||
self.device_entry = self._linked_device
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class MaintenanceSummaryCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
update_interval=None,
|
||||
)
|
||||
self._unsubs: list[CALLBACK_TYPE] = []
|
||||
self._attached: dict[str, CALLBACK_TYPE] = {}
|
||||
self._attached: dict[str, tuple[Any, CALLBACK_TYPE]] = {}
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Recompute counts from the shared aggregator (single source)."""
|
||||
@@ -62,12 +62,22 @@ class MaintenanceSummaryCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self._unsubs.append(self.hass.bus.async_listen(EVENT_TRIGGER_DEACTIVATED, self._on_event))
|
||||
|
||||
def _attach_entry(self, entry_id: str) -> None:
|
||||
"""Register a listener on a single object coordinator (once)."""
|
||||
if entry_id in self._attached:
|
||||
return
|
||||
"""Register a listener on a single object coordinator.
|
||||
|
||||
A reload swaps the coordinator and re-announces the entry; the old
|
||||
"once" guard left the summary sensors listening to the dead one (bug
|
||||
audit 2026-08-29), so re-attach whenever the instance changed.
|
||||
"""
|
||||
rd = get_runtime_data(self.hass, entry_id)
|
||||
if rd and rd.coordinator:
|
||||
self._attached[entry_id] = rd.coordinator.async_add_listener(self._schedule)
|
||||
coordinator = rd.coordinator if rd else None
|
||||
if coordinator is None:
|
||||
return
|
||||
previous = self._attached.get(entry_id)
|
||||
if previous is not None:
|
||||
if previous[0] is coordinator:
|
||||
return
|
||||
previous[1]()
|
||||
self._attached[entry_id] = (coordinator, coordinator.async_add_listener(self._schedule))
|
||||
|
||||
@callback
|
||||
def _on_new_entry(self, entry_id: str) -> None:
|
||||
@@ -78,9 +88,9 @@ class MaintenanceSummaryCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
@callback
|
||||
def _on_removed(self, entry_id: str) -> None:
|
||||
"""An object entry was deleted — detach its listener and recompute."""
|
||||
unsub = self._attached.pop(entry_id, None)
|
||||
if unsub is not None:
|
||||
unsub()
|
||||
attached = self._attached.pop(entry_id, None)
|
||||
if attached is not None:
|
||||
attached[1]()
|
||||
self._schedule()
|
||||
|
||||
@callback
|
||||
@@ -97,7 +107,7 @@ class MaintenanceSummaryCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Detach all listeners (called on global entry unload)."""
|
||||
for unsub in self._unsubs:
|
||||
unsub()
|
||||
for unsub in self._attached.values():
|
||||
for _coordinator, unsub in self._attached.values():
|
||||
unsub()
|
||||
self._unsubs.clear()
|
||||
self._attached.clear()
|
||||
|
||||
@@ -11,6 +11,11 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import (
|
||||
BATTERY_FLEET_EXCLUDED,
|
||||
BATTERY_FLEET_INCLUDED,
|
||||
BATTERY_FLEET_OBJECT_FLAG,
|
||||
BATTERY_FLEET_TASK_FLAG,
|
||||
BATTERY_FLEET_TRACK_SELF_CHARGING,
|
||||
CONF_OBJECT,
|
||||
CONF_TASKS,
|
||||
DEFAULT_WARNING_DAYS,
|
||||
@@ -111,6 +116,7 @@ def _build_export_object(
|
||||
"documentation_url": tdata.get("documentation_url"),
|
||||
"custom_icon": tdata.get("custom_icon"),
|
||||
"nfc_tag_id": tdata.get("nfc_tag_id"),
|
||||
"require_tag_scan": tdata.get("require_tag_scan"),
|
||||
"responsible_user_id": tdata.get("responsible_user_id"),
|
||||
"entity_slug": tdata.get("entity_slug"),
|
||||
"adaptive_config": tdata.get("adaptive_config"),
|
||||
@@ -155,6 +161,12 @@ def _build_export_object(
|
||||
if trigger_config:
|
||||
task["trigger_config"] = trigger_config
|
||||
|
||||
# Battery fleet identity: the single aggregate task is flagged so the
|
||||
# detail view renders the battery section — without the marker a
|
||||
# restored fleet task is a plain inspection task.
|
||||
if tdata.get(BATTERY_FLEET_TASK_FLAG):
|
||||
task[BATTERY_FLEET_TASK_FLAG] = True
|
||||
|
||||
if include_history:
|
||||
task["history"] = tdata.get("history") or []
|
||||
|
||||
@@ -167,33 +179,48 @@ def _build_export_object(
|
||||
object_id = obj_data.get("id", "")
|
||||
documents = _export_documents(doc_store, object_id) if doc_store is not None and object_id else []
|
||||
|
||||
export_obj: dict[str, Any] = {
|
||||
"name": obj_data.get("name", ""),
|
||||
"area_id": obj_data.get("area_id"),
|
||||
"manufacturer": obj_data.get("manufacturer"),
|
||||
"model": obj_data.get("model"),
|
||||
"serial_number": obj_data.get("serial_number"),
|
||||
"installation_date": obj_data.get("installation_date"),
|
||||
"warranty_expiry": obj_data.get("warranty_expiry"),
|
||||
# Round-tripped so a JSON backup restores the full asset record
|
||||
# (these were added in v1.4.0/v1.4.10 but missed here until #67).
|
||||
"documentation_url": obj_data.get("documentation_url"),
|
||||
"notes": obj_data.get("notes"),
|
||||
# 2.19: device link / parent hierarchy. Instance-specific ids —
|
||||
# meaningful when restoring on the SAME instance; dangling values
|
||||
# on a foreign instance are harmless (device_info falls back).
|
||||
"ha_device_id": obj_data.get("ha_device_id"),
|
||||
"parent_entry_id": obj_data.get("parent_entry_id"),
|
||||
# 2.20: seasonal pause (a paused pool restored in winter stays
|
||||
# paused) + replace-flow lineage (instance-specific entry ids,
|
||||
# same caveat as parent_entry_id above).
|
||||
"paused_at": obj_data.get("paused_at"),
|
||||
"paused_until": obj_data.get("paused_until"),
|
||||
"predecessor_entry_id": obj_data.get("predecessor_entry_id"),
|
||||
"replaced_by_entry_id": obj_data.get("replaced_by_entry_id"),
|
||||
# Object-level archive marker (the tasks carry their own pair
|
||||
# above). Without it an archived object came back ACTIVE after a
|
||||
# restore — with every task still archived (reason "object") and
|
||||
# object/unarchive refusing because the object "isn't archived".
|
||||
"archived_at": obj_data.get("archived_at"),
|
||||
}
|
||||
# Battery fleet identity — only emitted for the fleet object so a plain
|
||||
# object's export stays byte-identical to earlier versions. The importer
|
||||
# mirrors these (and keeps the deterministic ``batt_<type>`` part ids).
|
||||
if obj_data.get(BATTERY_FLEET_OBJECT_FLAG):
|
||||
export_obj[BATTERY_FLEET_OBJECT_FLAG] = True
|
||||
export_obj[BATTERY_FLEET_EXCLUDED] = list(obj_data.get(BATTERY_FLEET_EXCLUDED) or [])
|
||||
export_obj[BATTERY_FLEET_INCLUDED] = list(obj_data.get(BATTERY_FLEET_INCLUDED) or [])
|
||||
export_obj[BATTERY_FLEET_TRACK_SELF_CHARGING] = bool(obj_data.get(BATTERY_FLEET_TRACK_SELF_CHARGING))
|
||||
|
||||
return {
|
||||
"entry_id": entry.entry_id,
|
||||
"object": {
|
||||
"name": obj_data.get("name", ""),
|
||||
"area_id": obj_data.get("area_id"),
|
||||
"manufacturer": obj_data.get("manufacturer"),
|
||||
"model": obj_data.get("model"),
|
||||
"serial_number": obj_data.get("serial_number"),
|
||||
"installation_date": obj_data.get("installation_date"),
|
||||
"warranty_expiry": obj_data.get("warranty_expiry"),
|
||||
# Round-tripped so a JSON backup restores the full asset record
|
||||
# (these were added in v1.4.0/v1.4.10 but missed here until #67).
|
||||
"documentation_url": obj_data.get("documentation_url"),
|
||||
"notes": obj_data.get("notes"),
|
||||
# 2.19: device link / parent hierarchy. Instance-specific ids —
|
||||
# meaningful when restoring on the SAME instance; dangling values
|
||||
# on a foreign instance are harmless (device_info falls back).
|
||||
"ha_device_id": obj_data.get("ha_device_id"),
|
||||
"parent_entry_id": obj_data.get("parent_entry_id"),
|
||||
# 2.20: seasonal pause (a paused pool restored in winter stays
|
||||
# paused) + replace-flow lineage (instance-specific entry ids,
|
||||
# same caveat as parent_entry_id above).
|
||||
"paused_at": obj_data.get("paused_at"),
|
||||
"paused_until": obj_data.get("paused_until"),
|
||||
"predecessor_entry_id": obj_data.get("predecessor_entry_id"),
|
||||
"replaced_by_entry_id": obj_data.get("replaced_by_entry_id"),
|
||||
},
|
||||
"object": export_obj,
|
||||
"tasks": tasks,
|
||||
"documents": documents,
|
||||
# Spare parts: full static definition + the tracked stock (dynamic,
|
||||
@@ -293,7 +320,7 @@ def export_maintenance_data(
|
||||
# Global settings the export deliberately leaves behind: HA user ids are
|
||||
# instance-bound (and the panel-access allowlist is security-relevant), and
|
||||
# the adopted-task stash is transient re-adopt state.
|
||||
_NON_PORTABLE_SETTINGS = ("admin_panel_user_ids", "adopted_task_notes")
|
||||
_NON_PORTABLE_SETTINGS = ("admin_panel_user_ids", "adopted_task_notes", "shopping_list_entity")
|
||||
|
||||
|
||||
def build_settings_export(hass: HomeAssistant) -> dict[str, Any]:
|
||||
|
||||
+24
@@ -273,3 +273,27 @@ describe("complete-dialog cost suggestion from parts (#104 follow-up)", () => {
|
||||
expect(chip(el)).to.equal(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("complete-dialog require_tag_scan note", () => {
|
||||
it("shows the scan hint when the task demands proof of presence", async () => {
|
||||
const { hass } = createMockHass({});
|
||||
const el = await fixture<MaintenanceCompleteDialog>(html`
|
||||
<maintenance-complete-dialog
|
||||
.hass=${hass}
|
||||
.entryId=${"entry1"}
|
||||
.taskId=${"task1"}
|
||||
.taskName=${"Filter Wechsel"}
|
||||
.lang=${"en"}
|
||||
.requireTagScan=${true}
|
||||
></maintenance-complete-dialog>
|
||||
`);
|
||||
el.open();
|
||||
await el.updateComplete;
|
||||
expect(el.shadowRoot!.querySelector(".scan-required-note")).to.exist;
|
||||
});
|
||||
|
||||
it("stays silent for ordinary tasks", async () => {
|
||||
const { el } = await mount();
|
||||
expect(el.shadowRoot!.querySelector(".scan-required-note")).to.equal(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { expect } from "@open-wc/testing";
|
||||
import { formatDate, formatDateTime, setDateTimePrefs } from "../styles";
|
||||
|
||||
const reset = () => setDateTimePrefs({ date_format: undefined, time_format: undefined });
|
||||
const reset = () => setDateTimePrefs({ date_format: undefined, time_format: undefined }, null);
|
||||
|
||||
describe("formatDate with HA profile date_format (#97)", () => {
|
||||
afterEach(reset);
|
||||
@@ -66,3 +66,39 @@ describe("formatDateTime with HA profile time_format (#97)", () => {
|
||||
expect(out.toLowerCase()).to.match(/pm|nachm/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("server country regionalizes the language default (#140)", () => {
|
||||
afterEach(reset);
|
||||
|
||||
it('"en" + AU renders dd/mm/yyyy without any profile setting', () => {
|
||||
setDateTimePrefs({ date_format: "language" }, "AU");
|
||||
expect(formatDate("2026-08-10", "en")).to.equal("10/08/2026");
|
||||
});
|
||||
|
||||
it('"en" + US keeps mm/dd/yyyy; "de" + AT stays dd.mm.yyyy', () => {
|
||||
setDateTimePrefs({ date_format: "language" }, "US");
|
||||
expect(formatDate("2026-08-10", "en")).to.equal("08/10/2026");
|
||||
setDateTimePrefs({ date_format: "language" }, "AT");
|
||||
expect(formatDate("2026-08-10", "de")).to.equal("10.08.2026");
|
||||
});
|
||||
|
||||
it("an explicit profile format always beats the country", () => {
|
||||
setDateTimePrefs({ date_format: "MDY" }, "AU");
|
||||
expect(formatDate("2026-08-10", "en")).to.equal("08/10/2026");
|
||||
setDateTimePrefs({ date_format: "YMD" }, "AU");
|
||||
expect(formatDate("2026-08-10", "en")).to.equal("2026-08-10");
|
||||
});
|
||||
|
||||
it("garbage/unset country falls back to the language mapping", () => {
|
||||
setDateTimePrefs({ date_format: "language" }, "!!");
|
||||
expect(formatDate("2026-08-10", "en")).to.equal("08/10/2026");
|
||||
setDateTimePrefs({ date_format: "language" }, null);
|
||||
expect(formatDate("2026-08-10", "en")).to.equal("08/10/2026");
|
||||
});
|
||||
|
||||
it("country survives a later prefs update that omits it (undefined ≠ clear)", () => {
|
||||
setDateTimePrefs({ date_format: "language" }, "AU");
|
||||
setDateTimePrefs({ date_format: "language" });
|
||||
expect(formatDate("2026-08-10", "en")).to.equal("10/08/2026");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -114,6 +114,83 @@ describe("panel deep links (QR scan routing)", () => {
|
||||
expect(dlg.taskName).to.equal("No Defaults");
|
||||
});
|
||||
|
||||
it("quick_complete refused for missing details falls back to the dialog WITH the scan flag", async () => {
|
||||
setDeepLink("entry_id=e1&task_id=t1&action=quick_complete");
|
||||
const { el, sent } = await mountPanel(
|
||||
[obj("e1", [task({ name: "Needs Notes", require_tag_scan: true })])],
|
||||
{
|
||||
"maintenance_supporter/task/quick_complete": () => {
|
||||
throw { code: "completion_details_required", message: "notes required" };
|
||||
},
|
||||
},
|
||||
);
|
||||
await settleRaf(el);
|
||||
await settleRaf(el);
|
||||
|
||||
await waitForOpenCompleteDialog(el);
|
||||
const dlg = completeDialog(el)!;
|
||||
expect(dlg.shadowRoot!.querySelector("ha-dialog"), "fallback dialog open").to.exist;
|
||||
expect(dlg.taskName).to.equal("Needs Notes");
|
||||
// The scan already happened: the gate is met, the note is not shown …
|
||||
expect(dlg.viaTagScan).to.equal(true);
|
||||
expect(dlg.requireTagScan).to.equal(true);
|
||||
expect(dlg.shadowRoot!.querySelector(".scan-required-note")).to.be.null;
|
||||
// … and the completion carries via_tag_scan for the backend's gate.
|
||||
const buttons = [...dlg.shadowRoot!.querySelectorAll(".dialog-actions ha-button")];
|
||||
(buttons[buttons.length - 1] as HTMLElement).click();
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
const complete = sent.find((m) => m.type === "maintenance_supporter/task/complete");
|
||||
expect(complete, "task/complete sent").to.exist;
|
||||
expect(complete!.via_tag_scan).to.equal(true);
|
||||
});
|
||||
|
||||
it("a plain complete (no scan) opens the dialog WITHOUT the scan flag", async () => {
|
||||
setDeepLink("entry_id=e1&task_id=t1&action=complete");
|
||||
const { el } = await mountPanel([obj("e1", [task({ name: "Gated", require_tag_scan: true })])]);
|
||||
await settleRaf(el);
|
||||
await waitForOpenCompleteDialog(el);
|
||||
const dlg = completeDialog(el)!;
|
||||
expect(dlg.viaTagScan).to.equal(false);
|
||||
expect(dlg.shadowRoot!.querySelector(".scan-required-note"), "note shown").to.exist;
|
||||
});
|
||||
|
||||
it("any other quick_complete refusal relays the server's message", async () => {
|
||||
setDeepLink("entry_id=e1&task_id=t1&action=quick_complete");
|
||||
const { el } = await mountPanel(
|
||||
[obj("e1", [task({ name: "Early" })])],
|
||||
{
|
||||
"maintenance_supporter/task/quick_complete": () => {
|
||||
throw { code: "too_early", message: "Earliest completion is 2026-09-01" };
|
||||
},
|
||||
},
|
||||
);
|
||||
await settleRaf(el);
|
||||
await settleRaf(el);
|
||||
expect((el as unknown as { _toastMessage: string })._toastMessage).to.equal(
|
||||
"Earliest completion is 2026-09-01",
|
||||
);
|
||||
const dlg = completeDialog(el);
|
||||
expect(dlg?.shadowRoot?.querySelector("ha-dialog") ?? null, "no dialog").to.be.null;
|
||||
});
|
||||
|
||||
it("a silent quick_complete success reloads the data", async () => {
|
||||
setDeepLink("entry_id=e1&task_id=t1&action=quick_complete");
|
||||
const { el, sent } = await mountPanel(
|
||||
[obj("e1", [task({ name: "Quick" })])],
|
||||
{ "maintenance_supporter/task/quick_complete": () => ({ success: true, via: "quick" }) },
|
||||
);
|
||||
await settleRaf(el);
|
||||
await settleRaf(el);
|
||||
// The deep link is only routed once the objects are known, so every
|
||||
// objects fetch AFTER the quick_complete is the reload it triggered.
|
||||
const atQuick = sent.findIndex((m) => m.type === "maintenance_supporter/task/quick_complete");
|
||||
expect(atQuick, "quick_complete fired").to.be.greaterThan(-1);
|
||||
const reloaded = sent
|
||||
.slice(atQuick + 1)
|
||||
.some((m) => m.type === "maintenance_supporter/objects");
|
||||
expect(reloaded, "objects refetched after the silent complete").to.equal(true);
|
||||
});
|
||||
|
||||
it("an unknown entry_id lands safely on the overview", async () => {
|
||||
setDeepLink("entry_id=does-not-exist&task_id=t1&action=complete");
|
||||
const { el } = await mountPanel([obj("e1", [task({ name: "Real" })])]);
|
||||
|
||||
+13
@@ -215,6 +215,19 @@ describe("task-detail renderer", () => {
|
||||
expect(host2.querySelector(".kpi-bar")).to.be.null;
|
||||
});
|
||||
|
||||
it("history tab offers 'add past completion' routing to openComplete (#142)", () => {
|
||||
let completed: MaintenanceTask | null = null;
|
||||
const t2 = task();
|
||||
const host = mount(t2, ctx({
|
||||
activeTab: "history",
|
||||
openComplete: (tk) => { completed = tk; },
|
||||
}));
|
||||
const btn = host.querySelector(".history-add-past-btn") as HTMLElement;
|
||||
expect(btn, "add-past button").to.exist;
|
||||
btn.click();
|
||||
expect(completed).to.equal(t2);
|
||||
});
|
||||
|
||||
it("object-manual row falls back to an attached manual when the URL is empty", () => {
|
||||
let opened: unknown = null;
|
||||
const host = mount(task(), ctx({
|
||||
|
||||
@@ -43,6 +43,15 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
/** #139: "2/4 · Flip blades" — which phase of the cycle this completion
|
||||
* records. Purely informative; the backend advances the cursor itself. */
|
||||
@property() public phaseLabel = "";
|
||||
/** Proof of presence: the task completes only via NFC/QR scan — the
|
||||
* dialog says so up front instead of relaying the server's refusal. */
|
||||
@property({ type: Boolean }) public requireTagScan = false;
|
||||
/** This open is the fallback of a QR/NFC scan (quick-complete refused for
|
||||
* missing defaults/details): the completion carries `via_tag_scan` so the
|
||||
* tag-scan gate counts the scan that got the user here, and the "scan
|
||||
* required" note stays hidden — they just did. Set by open(); never
|
||||
* survives into the next open. */
|
||||
@property({ type: Boolean }) public viaTagScan = false;
|
||||
@state() private _open = false;
|
||||
@state() private _notes = "";
|
||||
@state() private _cost = "";
|
||||
@@ -66,9 +75,12 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
* prefill the dialog so nobody re-ticks what is already done. */
|
||||
@property({ attribute: false }) public checklistPrefill: Record<string, boolean> = {};
|
||||
|
||||
public open(): void {
|
||||
public open(opts: { viaTagScan?: boolean } = {}): void {
|
||||
if (this._open) return;
|
||||
this._open = true;
|
||||
// Reset on every open — a plain open after a scan fallback must not
|
||||
// keep claiming proof of presence.
|
||||
this.viaTagScan = !!opts.viaTagScan;
|
||||
this._notes = "";
|
||||
this._cost = "";
|
||||
this._duration = "";
|
||||
@@ -173,6 +185,12 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
if (this._photoDocId) {
|
||||
data.photo_doc_id = this._photoDocId;
|
||||
}
|
||||
// Scan fallback: the backend accepts via_tag_scan on task/complete so a
|
||||
// require_tag_scan task can still be finished from the dialog the scan
|
||||
// opened. Only ever sent when a scan actually happened.
|
||||
if (this.viaTagScan) {
|
||||
data.via_tag_scan = true;
|
||||
}
|
||||
if (this._completedAt) {
|
||||
// Client-side guard mirrors the backend rule — a picked future moment
|
||||
// fails fast with a localized message instead of a WS roundtrip.
|
||||
@@ -290,6 +308,7 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
<ha-dialog open @closed=${this._close}>
|
||||
<div class="dialog-title">${t("complete_title", L)}${this.taskName}</div>
|
||||
${this.phaseLabel ? html`<div class="phase-line">${t("phase_current", L)}: ${this.phaseLabel}</div>` : nothing}
|
||||
${this.requireTagScan && !this.viaTagScan ? html`<div class="scan-required-note">${t("require_tag_scan_hint", L)}</div>` : nothing}
|
||||
<div class="content">
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
|
||||
${this.checklist.length > 0 ? html`
|
||||
@@ -477,6 +496,14 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
font-weight: 500;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.scan-required-note {
|
||||
margin: -4px 0 12px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 152, 0, 0.12);
|
||||
color: var(--primary-text-color);
|
||||
font-size: 13px;
|
||||
}
|
||||
.phase-line {
|
||||
margin-top: -8px;
|
||||
padding-bottom: 12px;
|
||||
|
||||
@@ -39,6 +39,10 @@ export class MsTextfield extends LitElement {
|
||||
@property() public max?: string;
|
||||
@property() public pattern?: string;
|
||||
@property() public helper?: string;
|
||||
/** Render a <textarea> instead of an <input> (free-text notes). The
|
||||
* `input` event contract is identical — target.value works for both. */
|
||||
@property({ type: Boolean }) public multiline = false;
|
||||
@property({ type: Number }) public rows = 3;
|
||||
|
||||
/** Forwards the native input's value into our `value` property and
|
||||
* re-fires as a bubbling `input` event so consumers reading
|
||||
@@ -56,6 +60,16 @@ export class MsTextfield extends LitElement {
|
||||
return html`
|
||||
<label class="field">
|
||||
${this.label ? html`<span class="label">${this.label}${this.required ? html`<span class="req">*</span>` : nothing}</span>` : nothing}
|
||||
${this.multiline ? html`
|
||||
<textarea
|
||||
.value=${this.value ?? ""}
|
||||
rows=${this.rows}
|
||||
?required=${this.required}
|
||||
?disabled=${this.disabled}
|
||||
placeholder=${this.placeholder}
|
||||
@input=${this._onInput}
|
||||
@change=${this._onInput}
|
||||
></textarea>` : html`
|
||||
<input
|
||||
.value=${this.value ?? ""}
|
||||
.type=${this.type}
|
||||
@@ -68,7 +82,7 @@ export class MsTextfield extends LitElement {
|
||||
pattern=${this.pattern ?? nothing}
|
||||
@input=${this._onInput}
|
||||
@change=${this._onInput}
|
||||
/>
|
||||
/>`}
|
||||
${this.helper ? html`<span class="helper">${this.helper}</span>` : nothing}
|
||||
</label>
|
||||
`;
|
||||
@@ -103,6 +117,21 @@ export class MsTextfield extends LitElement {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
input:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
textarea {
|
||||
padding: 8px 10px;
|
||||
font-size: 14px;
|
||||
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||
color: var(--primary-text-color);
|
||||
border: 1px solid var(--divider-color, rgba(255,255,255,0.12));
|
||||
border-radius: 6px;
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
}
|
||||
textarea:focus { border-color: var(--primary-color); }
|
||||
textarea:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.helper {
|
||||
font-size: 11px;
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
@@ -217,6 +217,7 @@ export class MaintenanceObjectDialog extends LitElement {
|
||||
.value=${this._notes}
|
||||
@input=${(e: Event) => (this._notes = (e.target as HTMLTextAreaElement).value)}
|
||||
></textarea>
|
||||
<span class="md-hint">${t("notes_markdown_hint", L)}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
@@ -271,6 +272,9 @@ export class MaintenanceObjectDialog extends LitElement {
|
||||
.textarea-field textarea:focus {
|
||||
outline: none; border-color: var(--primary-color);
|
||||
}
|
||||
.md-hint {
|
||||
font-size: 11px; color: var(--secondary-text-color); font-style: italic;
|
||||
}
|
||||
.parent-select {
|
||||
padding: 8px 10px; font-size: 14px; font-family: inherit;
|
||||
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||
|
||||
+3
-1
@@ -10,6 +10,7 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { isSafeHttpUrl } from "../helpers/url";
|
||||
import { renderNotesMarkdown } from "../helpers/notes-markdown";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { t, STATUS_COLORS, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
@@ -186,7 +187,7 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
|
||||
? html`
|
||||
<div class="notes-section">
|
||||
<strong>${t("object_notes_label", L)}</strong>
|
||||
<div class="notes-body">${obj.notes}</div>
|
||||
<div class="notes-body">${renderNotesMarkdown(obj.notes)}</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
@@ -294,6 +295,7 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
|
||||
.task-name { flex: 1; font-size: 14px; }
|
||||
.task-status { font-size: 11px; color: var(--secondary-text-color); text-transform: uppercase; }
|
||||
.notes-body { white-space: pre-wrap; font-size: 13px; padding: 8px; background: var(--secondary-background-color); border-radius: 6px; }
|
||||
.notes-body ha-markdown { white-space: normal; }
|
||||
.actions { display: flex; gap: 8px; padding-top: 8px; border-top: 1px solid var(--divider-color); }
|
||||
.actions .btn { flex: 1; }
|
||||
.btn {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import { live } from "lit/directives/live.js";
|
||||
import type { HomeAssistant, AdvancedFeatures, BudgetStatus, HAUser } from "../types";
|
||||
import { t, langOf } from "../styles";
|
||||
import { signApiPath } from "../helpers/document-url";
|
||||
@@ -36,6 +37,8 @@ interface SettingsResponse {
|
||||
// so the picker matches the options-flow dropdown exactly. Optional for
|
||||
// backward-compat with an older backend that didn't send it.
|
||||
notify_targets?: string[];
|
||||
/** v2.67: todo.* entity the buy-task shopping sync mirrors into ("" = off). */
|
||||
shopping_list_entity?: string;
|
||||
panel_enabled: boolean;
|
||||
panel_title: string;
|
||||
/** Opt-in copy of the shipped Assist sentences into the config dir. */
|
||||
@@ -249,6 +252,11 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
this.dispatchEvent(new CustomEvent("settings-changed"));
|
||||
} catch {
|
||||
this._showToast(t("action_error", this._lang));
|
||||
// The control already shows the rejected value (the browser changed it
|
||||
// before we asked the server). `_settings` is unchanged, so a plain
|
||||
// re-render would not touch the DOM — the selects bind their value
|
||||
// through `live()` precisely so this re-render snaps them back.
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,15 +587,16 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
<h3>${t("settings_general", L)}</h3>
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_default_warning", L)}</span>
|
||||
<input type="number" min="1" max="365" .value=${String(g.default_warning_days)}
|
||||
<input type="number" min="0" max="365" .value=${String(g.default_warning_days)}
|
||||
@change=${(e: Event) => {
|
||||
const v = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (v >= 1 && v <= 365) this._updateSetting("default_warning_days", v);
|
||||
// 0 = no warning window (due soon only on the due date) — #145.
|
||||
if (v >= 0 && v <= 365) this._updateSetting("default_warning_days", v);
|
||||
}} />
|
||||
</label>
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_currency", L)}</span>
|
||||
<select @change=${(e: Event) => this._updateSetting("budget_currency", (e.target as HTMLSelectElement).value)}>
|
||||
<select .value=${live(b.currency)} @change=${(e: Event) => this._updateSetting("budget_currency", (e.target as HTMLSelectElement).value)}>
|
||||
${CURRENCIES.map((c) => html`<option value=${c} ?selected=${b.currency === c}>${c}</option>`)}
|
||||
</select>
|
||||
</label>
|
||||
@@ -652,10 +661,29 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
</div>
|
||||
` : nothing}
|
||||
` : nothing}
|
||||
|
||||
<label class="setting-row">
|
||||
<span class="setting-label" title=${t("settings_shopping_list_help", L)}>${t("settings_shopping_list", L)}</span>
|
||||
<select .value=${live(g.shopping_list_entity || "")}
|
||||
@change=${(e: Event) => this._updateSetting("shopping_list_entity", (e.target as HTMLSelectElement).value)}>
|
||||
<option value="" ?selected=${!g.shopping_list_entity}>${t("shopping_list_none", L)}</option>
|
||||
${this._todoEntities(g.shopping_list_entity || "").map((id) => html`
|
||||
<option value=${id} ?selected=${g.shopping_list_entity === id}>${id}</option>
|
||||
`)}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/** All todo.* entities, plus the saved value even when its integration
|
||||
* hasn't loaded (so an offline pick isn't silently dropped from the UI). */
|
||||
private _todoEntities(current: string): string[] {
|
||||
const ids = Object.keys(this.hass?.states || {}).filter((id) => id.startsWith("todo.")).sort();
|
||||
if (current && !ids.includes(current)) ids.unshift(current);
|
||||
return ids;
|
||||
}
|
||||
|
||||
// --- Section: Notifications ---
|
||||
|
||||
private _renderNotifications(L: string) {
|
||||
@@ -762,7 +790,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_notify_scope", L)}</span>
|
||||
<select
|
||||
.value=${n.scope_view_id || ""}
|
||||
.value=${live(n.scope_view_id || "")}
|
||||
@change=${(e: Event) => this._updateSetting("notify_scope_view_id", (e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
<option value="" ?selected=${!n.scope_view_id}>${t("settings_notify_scope_all", L)}</option>
|
||||
|
||||
@@ -244,6 +244,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
// NFC
|
||||
@state() private _lastPerformed = "";
|
||||
@state() private _nfcTagId = "";
|
||||
@state() private _requireTagScan = false;
|
||||
// v2.20 (#83): unit for `reading`-type tasks ("kWh", "m³", ...)
|
||||
@state() private _readingUnit = "";
|
||||
/** The picked links, keyed by `partLinkKey` — the (entry_id, part_id) pair,
|
||||
@@ -263,12 +264,16 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
// Task phases (#139): editable defs + the cycle order. The v1 editor keeps
|
||||
// each phase to name + checklist + at most ONE own-object part (the mower
|
||||
// "14 new blades" case); richer per-phase config exists at the WS level.
|
||||
// `carry` holds def fields the editor has no form for (notes,
|
||||
// required_completion_fields, future keys) and `extraParts` any links
|
||||
// beyond the first — both re-merged on save so a no-op edit never wipes
|
||||
// them (#106 pattern; same passthrough idea as trigger_config's carry).
|
||||
// `carry` holds def fields the editor has no form for (notes, future
|
||||
// keys) and `extraParts` any links beyond the first — both re-merged on
|
||||
// save so a no-op edit never wipes them (#106 pattern; same passthrough
|
||||
// idea as trigger_config's carry). Required completion fields are a real
|
||||
// form now (#139 follow-up): `reqOverride` distinguishes "inherit the
|
||||
// task's list" (off) from an explicit per-phase list — including the
|
||||
// meaningful EMPTY override "this phase demands nothing".
|
||||
@state() private _phaseDefs: Array<{
|
||||
id: string; name: string; checklistText: string; partId: string; partQty: string;
|
||||
reqOverride: boolean; reqFields: string[];
|
||||
extraParts: TaskPartLink[]; carry: Record<string, unknown>;
|
||||
}> = [];
|
||||
@state() private _phaseSeq: string[] = [];
|
||||
@@ -348,6 +353,11 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._entryId = entryId;
|
||||
this._taskId = task.id;
|
||||
this._error = "";
|
||||
// The object picker belongs to CREATE only. openCreate("", objects) left
|
||||
// the choices behind, so Create → Cancel → Edit showed the dropdown in
|
||||
// edit mode — and picking another object re-pointed _entryId while
|
||||
// _taskId kept the old task (audit 2026-08-29).
|
||||
this._objectChoices = [];
|
||||
this._name = task.name;
|
||||
this._type = task.type;
|
||||
this._scheduleType = task.schedule_type;
|
||||
@@ -389,6 +399,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._enabled = task.enabled !== false;
|
||||
this._lastPerformed = task.last_performed || "";
|
||||
this._nfcTagId = task.nfc_tag_id || "";
|
||||
this._requireTagScan = !!task.require_tag_scan;
|
||||
this._readingUnit = task.reading_unit || "";
|
||||
// Whole link, entry_id included — hydrating only part_id would turn every
|
||||
// shared-pool link into an own-part link on the next save (#111).
|
||||
@@ -401,14 +412,28 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
|
||||
this._checklistText = (task.checklist || []).join("\n");
|
||||
this._phaseDefs = Object.entries(task.phases || {}).map(([id, def]) => {
|
||||
const { name: _n, checklist: _c, consumes_parts: _p, ...carry } = def as unknown as Record<string, unknown>;
|
||||
const {
|
||||
name: _n, checklist: _c, consumes_parts: _p, required_completion_fields: _r,
|
||||
...carry
|
||||
} = def as unknown as Record<string, unknown>;
|
||||
// The editor's single part field is the first OWN link (no entry_id —
|
||||
// the picker only lists the object's own parts). Every other link,
|
||||
// shared pools (#111) first among them, rides along whole in
|
||||
// `extraParts` and is re-emitted on save: hydrating index 0 blindly
|
||||
// turned a phase whose first link is foreign into an own-part link
|
||||
// (or lost it) on the next save (audit 2026-08-29).
|
||||
const links = def.consumes_parts || [];
|
||||
const ownIdx = links.findIndex((l) => !l.entry_id);
|
||||
const own = ownIdx >= 0 ? links[ownIdx] : undefined;
|
||||
return {
|
||||
id,
|
||||
name: def.name || id,
|
||||
checklistText: (def.checklist || []).join("\n"),
|
||||
partId: def.consumes_parts?.[0]?.part_id || "",
|
||||
partQty: def.consumes_parts?.[0]?.quantity != null ? String(def.consumes_parts[0].quantity) : "",
|
||||
extraParts: (def.consumes_parts || []).slice(1),
|
||||
partId: own?.part_id || "",
|
||||
partQty: own?.quantity != null ? String(own.quantity) : "",
|
||||
reqOverride: def.required_completion_fields !== undefined,
|
||||
reqFields: [...(def.required_completion_fields || [])],
|
||||
extraParts: links.filter((_, i) => i !== ownIdx).map((l) => ({ ...l })),
|
||||
carry,
|
||||
};
|
||||
});
|
||||
@@ -525,6 +550,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._enabled = true;
|
||||
this._lastPerformed = "";
|
||||
this._nfcTagId = "";
|
||||
this._requireTagScan = false;
|
||||
this._readingUnit = "";
|
||||
this._consumesParts = {};
|
||||
this._responsibleUserId = null;
|
||||
@@ -1032,7 +1058,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
|
||||
private _addPhaseDef(): void {
|
||||
const id = this._phaseSlug(`phase-${this._phaseDefs.length + 1}`);
|
||||
this._phaseDefs = [...this._phaseDefs, { id, name: "", checklistText: "", partId: "", partQty: "", extraParts: [], carry: {} }];
|
||||
this._phaseDefs = [...this._phaseDefs, { id, name: "", checklistText: "", partId: "", partQty: "", reqOverride: false, reqFields: [], extraParts: [], carry: {} }];
|
||||
}
|
||||
|
||||
private _removePhaseDef(id: string): void {
|
||||
@@ -1040,7 +1066,10 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._phaseSeq = this._phaseSeq.filter((s) => s !== id);
|
||||
}
|
||||
|
||||
private _patchPhaseDef(id: string, patch: Partial<{ name: string; checklistText: string; partId: string; partQty: string }>): void {
|
||||
private _patchPhaseDef(
|
||||
id: string,
|
||||
patch: Partial<{ name: string; checklistText: string; partId: string; partQty: string; reqOverride: boolean; reqFields: string[] }>,
|
||||
): void {
|
||||
this._phaseDefs = this._phaseDefs.map((d) => (d.id === id ? { ...d, ...patch } : d));
|
||||
}
|
||||
|
||||
@@ -1084,6 +1113,33 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
@input=${(e: Event) => this._patchPhaseDef(d.id, { checklistText: (e.target as HTMLTextAreaElement).value })}
|
||||
></textarea>
|
||||
` : nothing}
|
||||
<label class="req-option phase-req-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${d.reqOverride}
|
||||
@change=${(e: Event) => this._patchPhaseDef(d.id, { reqOverride: (e.target as HTMLInputElement).checked })}
|
||||
/>
|
||||
<span>${t("phase_require_override", L)}</span>
|
||||
</label>
|
||||
${d.reqOverride ? html`
|
||||
<div class="required-completion phase-req-fields">
|
||||
${REQUIRED_COMPLETION_KEYS.map((field) => html`
|
||||
<label class="req-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${d.reqFields.includes(field)}
|
||||
@change=${(e: Event) => {
|
||||
const on = (e.target as HTMLInputElement).checked;
|
||||
const next = new Set(d.reqFields);
|
||||
if (on) next.add(field); else next.delete(field);
|
||||
this._patchPhaseDef(d.id, { reqFields: [...next] });
|
||||
}}
|
||||
/>
|
||||
<span>${t(REQUIRED_COMPLETION_LABELS[field], L)}</span>
|
||||
</label>
|
||||
`)}
|
||||
</div>
|
||||
` : nothing}
|
||||
</div>
|
||||
`)}
|
||||
<ha-button appearance="plain" @click=${this._addPhaseDef}>
|
||||
@@ -1196,6 +1252,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
data.enabled = this._enabled;
|
||||
data.last_performed = this._lastPerformed || null;
|
||||
data.nfc_tag_id = this._nfcTagId || null;
|
||||
data.require_tag_scan = this._requireTagScan;
|
||||
data.reading_unit = this._readingUnit.trim() || null;
|
||||
// Task phases (#139): always sent — null clears a removed cycle.
|
||||
{
|
||||
@@ -1210,8 +1267,19 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
const qty = parseFloat(d.partQty);
|
||||
links.push({ part_id: d.partId, quantity: Number.isFinite(qty) && qty > 0 ? qty : 1 });
|
||||
}
|
||||
links.push(...d.extraParts);
|
||||
// entry_id travels only when present (a shared pool, #111) — an
|
||||
// own-part link stays byte-identical to what shipped before.
|
||||
for (const l of d.extraParts) {
|
||||
links.push(
|
||||
l.entry_id
|
||||
? { part_id: l.part_id, quantity: l.quantity, entry_id: l.entry_id }
|
||||
: { part_id: l.part_id, quantity: l.quantity },
|
||||
);
|
||||
}
|
||||
if (links.length) def.consumes_parts = links;
|
||||
// Override on → send the list, EMPTY included ("demands nothing");
|
||||
// off → omit the key so the task-level list falls through.
|
||||
if (d.reqOverride) def.required_completion_fields = [...d.reqFields];
|
||||
defs[d.id] = def;
|
||||
}
|
||||
const seq = this._phaseSeq.filter((id) => id in defs);
|
||||
@@ -2432,7 +2500,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
<div class="dialog-title">${title}</div>
|
||||
<div class="content">
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
|
||||
${this._objectChoices.length > 0 ? html`
|
||||
${this._taskId === null && this._objectChoices.length > 0 ? html`
|
||||
<div class="select-row">
|
||||
<label>${t("object", L)}</label>
|
||||
<select
|
||||
@@ -2703,6 +2771,9 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
${this._renderAdaptiveSection(L)}
|
||||
<ms-textfield
|
||||
label="${t("notes_optional", L)}"
|
||||
multiline
|
||||
.rows=${3}
|
||||
.helper=${t("notes_markdown_hint", L)}
|
||||
.value=${this._notes}
|
||||
@input=${(e: Event) => (this._notes = (e.target as HTMLInputElement).value)}
|
||||
></ms-textfield>
|
||||
@@ -2751,6 +2822,15 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
</div>
|
||||
`
|
||||
}
|
||||
<label class="req-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${this._requireTagScan}
|
||||
@change=${(e: Event) => (this._requireTagScan = (e.target as HTMLInputElement).checked)}
|
||||
/>
|
||||
<span>${t("require_tag_scan", L)}</span>
|
||||
</label>
|
||||
${this._requireTagScan ? html`<div class="field-help">${t("require_tag_scan_help", L)}</div>` : nothing}
|
||||
<label class="toggle-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -2923,6 +3003,15 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
min-height: 56px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.phase-req-toggle {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.phase-req-fields {
|
||||
margin: 2px 0 0 22px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.phase-remove {
|
||||
--mdc-icon-button-size: 36px;
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
+34
-30
@@ -17,8 +17,8 @@ import { property, state } from "lit/decorators.js";
|
||||
import { sharedStyles, t, STATUS_COLORS, formatDate, formatDateTime, formatInterval, formatRecurrence, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { isoDateLocal } from "../helpers/calendar-bucket";
|
||||
import { partsForCompletion } from "../helpers/shared-parts";
|
||||
import { effectivePhase, phaseLabel } from "../helpers/phases";
|
||||
import { buildCompleteDialogArgs } from "../helpers/complete-dialog-args";
|
||||
import { phaseLabel } from "../helpers/phases";
|
||||
import { renderWeibullSection } from "../renderers/weibull";
|
||||
import { renderPredictionSection } from "../renderers/prediction";
|
||||
import { renderRecommendationBars } from "../renderers/recommendation";
|
||||
@@ -63,6 +63,9 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
};
|
||||
@state() private _toast = "";
|
||||
private _featuresLoaded = false;
|
||||
/** Currency symbol for the complete dialog's cost suggestion — read off
|
||||
* the same settings response as the feature flags. */
|
||||
private _currencySymbol = "";
|
||||
|
||||
private get _lang(): string {
|
||||
return langOf(this.hass);
|
||||
@@ -93,10 +96,12 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
try {
|
||||
const r = await this.hass.connection.sendMessagePromise<{
|
||||
features?: Partial<AdvancedFeatures>;
|
||||
budget?: { currency_symbol?: string };
|
||||
}>({ type: "maintenance_supporter/settings" });
|
||||
if (r?.features) {
|
||||
this._features = { ...this._features, ...r.features };
|
||||
}
|
||||
this._currencySymbol = r?.budget?.currency_symbol || "";
|
||||
this._featuresLoaded = true;
|
||||
} catch {
|
||||
// Settings endpoint unavailable — leave defaults (all false), the
|
||||
@@ -158,35 +163,34 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
// (bug audit 2026-08-22).
|
||||
import("../dialog-mount").then(async ({ openCompleteDialog }) => {
|
||||
const task = this._task!;
|
||||
const isBuy = !!(task as { part_ref?: string }).part_ref;
|
||||
// #139: a phased task completes the phase currently due, so the phase's
|
||||
// checklist/parts/required fields replace the task-level ones here.
|
||||
const phase = effectivePhase(task);
|
||||
const links = phase ? phase.consumesParts : (task.consumes_parts || []);
|
||||
let parts: Parameters<typeof openCompleteDialog>[0]["parts"] = [];
|
||||
if (!isBuy) {
|
||||
try {
|
||||
const r = await this.hass.connection.sendMessagePromise<{
|
||||
objects: MaintenanceObjectResponse[];
|
||||
}>({ type: "maintenance_supporter/objects", compact: true });
|
||||
parts = partsForCompletion({ consumes_parts: links }, this._entryId!, r.objects || [], this._lang);
|
||||
} catch {
|
||||
// Parts stay empty — the dialog still completes without them.
|
||||
}
|
||||
// The object list resolves the parts picker (own inventory + shared
|
||||
// pools, #99/#111), the "consumes" hint lines and a buy task's restock
|
||||
// default — fetched for every task kind, since a buy task needs its
|
||||
// part's restock quantity + unit cost too.
|
||||
let objects: MaintenanceObjectResponse[] = [];
|
||||
try {
|
||||
const r = await this.hass.connection.sendMessagePromise<{
|
||||
objects: MaintenanceObjectResponse[];
|
||||
}>({ type: "maintenance_supporter/objects", compact: true });
|
||||
objects = r.objects || [];
|
||||
} catch {
|
||||
// Parts stay empty — the dialog still completes without them.
|
||||
}
|
||||
const ok = openCompleteDialog({
|
||||
entry_id: this._entryId!,
|
||||
task_id: this._taskId!,
|
||||
task_name: task.name,
|
||||
checklist: phase ? phase.checklist : (task.checklist || []),
|
||||
adaptive_enabled: !!task.adaptive_config?.enabled,
|
||||
required_completion_fields: phase ? phase.requiredFields : (task.required_completion_fields || []),
|
||||
task_type: task.type || "",
|
||||
reading_unit: (task as { reading_unit?: string }).reading_unit || "",
|
||||
parts,
|
||||
consumes_parts: isBuy ? [] : links,
|
||||
phase_label: phaseLabel(task),
|
||||
});
|
||||
// Same derivation as the panel and the card (phase override, tag-scan
|
||||
// gate, checklist ticks …) — see helpers/complete-dialog-args.
|
||||
const ok = openCompleteDialog(
|
||||
buildCompleteDialogArgs({
|
||||
entryId: this._entryId!,
|
||||
taskId: this._taskId!,
|
||||
taskName: task.name,
|
||||
task,
|
||||
objects,
|
||||
lang: this._lang,
|
||||
checklist: task.checklist || [],
|
||||
adaptiveEnabled: !!task.adaptive_config?.enabled,
|
||||
currencySymbol: this._currencySymbol,
|
||||
}),
|
||||
);
|
||||
if (ok) {
|
||||
this._notifyChanged("complete");
|
||||
this.close();
|
||||
|
||||
@@ -45,6 +45,7 @@ import type { MaintenanceTaskQuickActionsDialog } from "./components/task-quick-
|
||||
import type { MaintenanceObjectQuickActionsDialog } from "./components/object-quick-actions-dialog";
|
||||
import type { HomeAssistant, MaintenanceObject } from "./types";
|
||||
import { ensureLocale, isLocaleLoaded, langOf } from "./styles";
|
||||
import { fillAndOpenCompleteDialog, type CompleteDialogArgs } from "./helpers/complete-dialog-args";
|
||||
|
||||
const OBJECT_DIALOG_TAG = "maintenance-object-dialog";
|
||||
const TASK_DIALOG_TAG = "maintenance-task-dialog";
|
||||
@@ -274,41 +275,16 @@ export function openHistoryEditDialog(draft: HistoryEntryDraft): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** v2.3.0: open the rich complete-dialog from any Lovelace context. */
|
||||
export function openCompleteDialog(args: {
|
||||
entry_id: string;
|
||||
task_id: string;
|
||||
task_name: string;
|
||||
checklist?: string[];
|
||||
adaptive_enabled?: boolean;
|
||||
/** Details the task demands before it counts as done (v2.44). */
|
||||
required_completion_fields?: string[];
|
||||
/** Reading tasks need type+unit or the value field never renders. */
|
||||
task_type?: string;
|
||||
reading_unit?: string;
|
||||
/** #99/#111: per-completion parts selection incl. shared pools. */
|
||||
parts?: MaintenanceCompleteDialog["parts"];
|
||||
consumes_parts?: MaintenanceCompleteDialog["consumesParts"];
|
||||
/** #139: "2/4 · Flip blades" — the phase this completion records. */
|
||||
phase_label?: string;
|
||||
}): boolean {
|
||||
/** v2.3.0: open the rich complete-dialog from any Lovelace context.
|
||||
*
|
||||
* Takes the full argument bag (`buildCompleteDialogArgs` derives it from a
|
||||
* task + object list). Every dialog field is assigned — the dialog is a
|
||||
* singleton, so an omitted field must reset, never leak the previous
|
||||
* task's value (tag-scan gate, restock default, checklist ticks included). */
|
||||
export function openCompleteDialog(args: CompleteDialogArgs): boolean {
|
||||
const dlg = getOrCreate<MaintenanceCompleteDialog>(COMPLETE_DIALOG_TAG);
|
||||
if (!syncHass(dlg)) return false;
|
||||
dlg.entryId = args.entry_id;
|
||||
dlg.taskId = args.task_id;
|
||||
dlg.taskName = args.task_name;
|
||||
dlg.checklist = args.checklist ?? [];
|
||||
dlg.adaptiveEnabled = !!args.adaptive_enabled;
|
||||
dlg.requiredFields = args.required_completion_fields ?? [];
|
||||
// Always assign — the dialog is a singleton, so an omitted field must not
|
||||
// leak the previous task's value.
|
||||
dlg.taskType = args.task_type ?? "";
|
||||
dlg.readingUnit = args.reading_unit ?? "";
|
||||
dlg.parts = args.parts ?? [];
|
||||
dlg.consumesParts = args.consumes_parts ?? [];
|
||||
dlg.phaseLabel = args.phase_label ?? "";
|
||||
dlg.lang = (getHass()?.language) || "en";
|
||||
dlg.open();
|
||||
fillAndOpenCompleteDialog(dlg, args, (getHass()?.language) || "en");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Prázdné = půlnoc (výchozí). Časové pásmo HA.",
|
||||
"at_time": "v",
|
||||
"notes_optional": "Poznámky (volitelné)",
|
||||
"notes_markdown_hint": "Markdown je podporován — **tučně**, seznamy, [odkazy](…)",
|
||||
"cost_optional": "Náklady (volitelné)",
|
||||
"duration_minutes": "Doba trvání v minutách (volitelné)",
|
||||
"completed_at_optional": "Dokončeno dne (volitelné, prázdné = nyní)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Název panelu",
|
||||
"settings_notifications": "Oznámení",
|
||||
"settings_notify_service": "Služba oznámení",
|
||||
"settings_shopping_list": "Nákupní seznam (nákupní úkoly)",
|
||||
"settings_shopping_list_help": "Upozornění na nákup docházejících dílů se objeví v tomto seznamu; odškrtnutí doplní zásobu.",
|
||||
"shopping_list_none": "Vypnuto — žádný nákupní seznam",
|
||||
"settings_install_assist_sentences": "Nainstalovat věty pro Assist",
|
||||
"settings_install_assist_sentences_hint": "Zkopíruje hlasové věty do vaší konfigurace, aby je klasický agent Assist rozpoznal. Soubor, který jste upravili, nebude nikdy přepsán.",
|
||||
"test_notification": "Testovací oznámení",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Přidat krok",
|
||||
"phase_current": "Aktuální fáze",
|
||||
"phase_set": "Nastavit jako aktuální",
|
||||
"chart_history_fallback": "Pro tuto entitu nejsou dlouhodobé statistiky — zobrazuje se historie stavů recorderu (obvykle posledních ~10 dní)"
|
||||
"chart_history_fallback": "Pro tuto entitu nejsou dlouhodobé statistiky — zobrazuje se historie stavů recorderu (obvykle posledních ~10 dní)",
|
||||
"chart_history_alarm": "Pohled spouštěče z historie stavů: 1 = stav výstrahy držen po dobu přidržení, 0 = v pořádku (obvykle posledních ~10 dní)",
|
||||
"chart_history_count": "Počet změn sestaven z historie stavů od posledního servisu (obvykle posledních ~10 dní)",
|
||||
"prediction_cycles": "Naučeno z cyklů",
|
||||
"phase_require_override": "Přepsat „Vyžadovat při dokončení“ pro tuto fázi",
|
||||
"history_add_past": "Přidat dřívější dokončení",
|
||||
"require_tag_scan": "Dokončit pouze naskenováním štítku",
|
||||
"require_tag_scan_help": "Důkaz přítomnosti: „Hotovo“ je všude blokováno, dokud nenaskenujete NFC štítek nebo QR kód na zařízení. Automatizace mohou službě complete předat 'via_tag_scan'.",
|
||||
"require_tag_scan_hint": "Tento úkol lze dokončit pouze naskenováním jeho NFC štítku nebo QR kódu na zařízení — uložení zde bude odmítnuto."
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "Tom = midnat (standard). HA-tidszone.",
|
||||
"at_time": "kl.",
|
||||
"notes_optional": "Noter (valgfrit)",
|
||||
"notes_markdown_hint": "Markdown understøttes — **fed**, lister, [links](…)",
|
||||
"cost_optional": "Omkostning (valgfrit)",
|
||||
"duration_minutes": "Varighed i minutter (valgfrit)",
|
||||
"completed_at_optional": "Udført den (valgfrit, tomt = nu)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "Sidepanelets titel",
|
||||
"settings_notifications": "Notifikationer",
|
||||
"settings_notify_service": "Notifikationstjeneste",
|
||||
"settings_shopping_list": "Indkøbsliste (købsopgaver)",
|
||||
"settings_shopping_list_help": "Købspåmindelser for dele med lav beholdning vises på denne liste; afkrydsning genopfylder delen.",
|
||||
"shopping_list_none": "Fra — ingen indkøbsliste",
|
||||
"settings_install_assist_sentences": "Installer Assist-sætninger",
|
||||
"settings_install_assist_sentences_hint": "Kopierer stemmesætningerne til din konfiguration, så den klassiske Assist-agent genkender dem. En fil, du selv har redigeret, overskrives aldrig.",
|
||||
"test_notification": "Testnotifikation",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Tilføj trin",
|
||||
"phase_current": "Aktuel fase",
|
||||
"phase_set": "Sæt som aktuel",
|
||||
"chart_history_fallback": "Ingen langtidsstatistik for denne enhed — recorderens tilstandshistorik vises (typisk de seneste ~10 dage)"
|
||||
"chart_history_fallback": "Ingen langtidsstatistik for denne enhed — recorderens tilstandshistorik vises (typisk de seneste ~10 dage)",
|
||||
"chart_history_alarm": "Udløservisning fra tilstandshistorikken: 1 = alarmtilstand fastholdt i holdetiden, 0 = ok (typisk de seneste ~10 dage)",
|
||||
"chart_history_count": "Antal skift genopbygget fra tilstandshistorikken siden sidste service (typisk de seneste ~10 dage)",
|
||||
"prediction_cycles": "Lært af cyklusser",
|
||||
"phase_require_override": "Tilsidesæt „Kræv ved udførelse“ for denne fase",
|
||||
"history_add_past": "Tilføj tidligere udførelse",
|
||||
"require_tag_scan": "Kan kun udføres ved tag-scanning",
|
||||
"require_tag_scan_help": "Tilstedeværelsesbevis: „Udført“ er blokeret overalt, indtil NFC-tagget eller QR-koden på selve tingen er scannet. Automatiseringer kan give 'via_tag_scan' til complete-tjenesten.",
|
||||
"require_tag_scan_hint": "Denne opgave udføres kun ved at scanne dens NFC-tag eller QR-kode på selve tingen — at gemme her vil blive afvist."
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "Leer = Mitternacht (Default). HA-Zeitzone.",
|
||||
"at_time": "um",
|
||||
"notes_optional": "Notizen (optional)",
|
||||
"notes_markdown_hint": "Markdown wird unterstützt — **fett**, Listen, [Links](…)",
|
||||
"cost_optional": "Kosten (optional)",
|
||||
"duration_minutes": "Dauer in Minuten (optional)",
|
||||
"completed_at_optional": "Erledigt am (optional, leer = jetzt)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "Panel-Titel",
|
||||
"settings_notifications": "Benachrichtigungen",
|
||||
"settings_notify_service": "Benachrichtigungsdienst",
|
||||
"settings_shopping_list": "Einkaufsliste (Kauf-Aufgaben)",
|
||||
"settings_shopping_list_help": "Kauf-Erinnerungen für knappe Teile erscheinen in dieser To-do-Liste; Abhaken füllt den Bestand auf.",
|
||||
"shopping_list_none": "Aus — keine Einkaufsliste",
|
||||
"settings_install_assist_sentences": "Assist-Sätze installieren",
|
||||
"settings_install_assist_sentences_hint": "Kopiert die Sprachbefehle in deine Konfiguration, damit der klassische Assist-Agent sie erkennt. Eine selbst bearbeitete Datei wird nie überschrieben.",
|
||||
"test_notification": "Test-Benachrichtigung",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Schritt anhängen",
|
||||
"phase_current": "Aktuelle Phase",
|
||||
"phase_set": "Als aktuell setzen",
|
||||
"chart_history_fallback": "Keine Langzeitstatistik für diese Entität — angezeigt wird der Recorder-Zustandsverlauf (üblicherweise die letzten ~10 Tage)"
|
||||
"chart_history_fallback": "Keine Langzeitstatistik für diese Entität — angezeigt wird der Recorder-Zustandsverlauf (üblicherweise die letzten ~10 Tage)",
|
||||
"chart_history_alarm": "Trigger-Sicht aus dem Recorder-Zustandsverlauf: 1 = Alarmzustand über die Haltezeit gehalten, 0 = in Ordnung (typisch die letzten ~10 Tage)",
|
||||
"chart_history_count": "Wechselzähler aus dem Recorder-Zustandsverlauf seit dem letzten Service (typisch die letzten ~10 Tage)",
|
||||
"prediction_cycles": "Gelernt aus Zyklen",
|
||||
"phase_require_override": "„Bei Erledigung verlangen“ für diese Phase überschreiben",
|
||||
"history_add_past": "Vergangene Erledigung nachtragen",
|
||||
"require_tag_scan": "Nur per Tag-Scan abschließbar",
|
||||
"require_tag_scan_help": "Anwesenheitsnachweis: „Erledigt“ ist überall blockiert, bis das NFC-Tag oder der QR-Code am Gerät gescannt wurde. Automationen können 'via_tag_scan' an den complete-Dienst übergeben.",
|
||||
"require_tag_scan_hint": "Diese Aufgabe kann nur per Scan des NFC-Tags oder QR-Codes am Gerät erledigt werden — Speichern hier wird abgelehnt."
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "Empty = midnight (default). HA timezone.",
|
||||
"at_time": "at",
|
||||
"notes_optional": "Notes (optional)",
|
||||
"notes_markdown_hint": "Markdown is supported — **bold**, lists, [links](…)",
|
||||
"cost_optional": "Cost (optional)",
|
||||
"duration_minutes": "Duration in minutes (optional)",
|
||||
"completed_at_optional": "Completed at (optional, empty = now)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "Sidebar panel title",
|
||||
"settings_notifications": "Notifications",
|
||||
"settings_notify_service": "Notification service",
|
||||
"settings_shopping_list": "Shopping list (buy tasks)",
|
||||
"settings_shopping_list_help": "Low-part buy reminders appear in this to-do list; checking one off restocks the part.",
|
||||
"shopping_list_none": "Off — no shopping list",
|
||||
"settings_install_assist_sentences": "Install Assist sentences",
|
||||
"settings_install_assist_sentences_hint": "Copies the voice sentences into your configuration so the classic Assist agent recognises them. A file you edited yourself is never overwritten.",
|
||||
"test_notification": "Test notification",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Add step",
|
||||
"phase_current": "Current phase",
|
||||
"phase_set": "Set as current",
|
||||
"chart_history_fallback": "No long-term statistics for this entity — showing recorder state history (typically the last ~10 days)"
|
||||
"chart_history_fallback": "No long-term statistics for this entity — showing recorder state history (typically the last ~10 days)",
|
||||
"chart_history_alarm": "Trigger view from recorder state history: 1 = alert state held for the hold time, 0 = fine (typically the last ~10 days)",
|
||||
"chart_history_count": "Change count rebuilt from recorder state history since the last service (typically the last ~10 days)",
|
||||
"prediction_cycles": "Learned from cycles",
|
||||
"phase_require_override": "Override “Require on completion” for this phase",
|
||||
"history_add_past": "Add past completion",
|
||||
"require_tag_scan": "Only complete by scanning the tag",
|
||||
"require_tag_scan_help": "Proof of presence: Done is blocked on every surface until the NFC tag or QR code on the thing itself is scanned. Automations can pass 'via_tag_scan' to the complete service.",
|
||||
"require_tag_scan_hint": "This task completes only by scanning its NFC tag or QR code on the thing itself — saving here will be refused."
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Vacío = medianoche (predeterminado). Zona horaria HA.",
|
||||
"at_time": "a las",
|
||||
"notes_optional": "Notas (opcional)",
|
||||
"notes_markdown_hint": "Se admite Markdown — **negrita**, listas, [enlaces](…)",
|
||||
"cost_optional": "Coste (opcional)",
|
||||
"duration_minutes": "Duración en minutos (opcional)",
|
||||
"completed_at_optional": "Completado el (opcional, vacío = ahora)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Título del panel lateral",
|
||||
"settings_notifications": "Notificaciones",
|
||||
"settings_notify_service": "Servicio de notificación",
|
||||
"settings_shopping_list": "Lista de la compra (tareas de compra)",
|
||||
"settings_shopping_list_help": "Los recordatorios de compra de piezas escasas aparecen en esta lista; marcar uno repone la pieza.",
|
||||
"shopping_list_none": "Desactivado — sin lista de la compra",
|
||||
"settings_install_assist_sentences": "Instalar frases de Assist",
|
||||
"settings_install_assist_sentences_hint": "Copia las frases de voz en tu configuración para que el agente clásico de Assist las reconozca. Un archivo que hayas editado nunca se sobrescribe.",
|
||||
"test_notification": "Notificación de prueba",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Añadir paso",
|
||||
"phase_current": "Fase actual",
|
||||
"phase_set": "Establecer como actual",
|
||||
"chart_history_fallback": "Sin estadísticas a largo plazo para esta entidad: se muestra el historial de estados del recorder (normalmente los últimos ~10 días)"
|
||||
"chart_history_fallback": "Sin estadísticas a largo plazo para esta entidad: se muestra el historial de estados del recorder (normalmente los últimos ~10 días)",
|
||||
"chart_history_alarm": "Vista del disparador desde el historial de estados: 1 = estado de alerta mantenido durante el tiempo de retención, 0 = correcto (normalmente los últimos ~10 días)",
|
||||
"chart_history_count": "Recuento de cambios reconstruido desde el historial de estados desde el último servicio (normalmente los últimos ~10 días)",
|
||||
"prediction_cycles": "Aprendido de ciclos",
|
||||
"phase_require_override": "Sobrescribir «Requerir al completar» para esta fase",
|
||||
"history_add_past": "Añadir una finalización pasada",
|
||||
"require_tag_scan": "Completar solo escaneando la etiqueta",
|
||||
"require_tag_scan_help": "Prueba de presencia: «Hecho» queda bloqueado en todas partes hasta escanear la etiqueta NFC o el código QR del propio objeto. Las automatizaciones pueden pasar 'via_tag_scan' al servicio complete.",
|
||||
"require_tag_scan_hint": "Esta tarea solo se completa escaneando su etiqueta NFC o código QR en el propio objeto; guardar aquí será rechazado."
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "Tyhjä = keskiyö (oletus). HA:n aikavyöhyke.",
|
||||
"at_time": "klo",
|
||||
"notes_optional": "Muistiinpanot (valinnainen)",
|
||||
"notes_markdown_hint": "Markdown on tuettu — **lihavointi**, listat, [linkit](…)",
|
||||
"cost_optional": "Kustannus (valinnainen)",
|
||||
"duration_minutes": "Kesto minuutteina (valinnainen)",
|
||||
"completed_at_optional": "Suoritettu (valinnainen, tyhjä = nyt)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "Sivupalkin paneelin otsikko",
|
||||
"settings_notifications": "Ilmoitukset",
|
||||
"settings_notify_service": "Ilmoituspalvelu",
|
||||
"settings_shopping_list": "Ostoslista (ostotehtävät)",
|
||||
"settings_shopping_list_help": "Vähissä olevien osien ostomuistutukset näkyvät tässä luettelossa; kuittaus täydentää varaston.",
|
||||
"shopping_list_none": "Pois — ei ostoslistaa",
|
||||
"settings_install_assist_sentences": "Asenna Assist-lauseet",
|
||||
"settings_install_assist_sentences_hint": "Kopioi äänikomennot asetuksiisi, jotta perinteinen Assist-agentti tunnistaa ne. Itse muokkaamaasi tiedostoa ei koskaan korvata.",
|
||||
"test_notification": "Testi-ilmoitus",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Lisää askel",
|
||||
"phase_current": "Nykyinen vaihe",
|
||||
"phase_set": "Aseta nykyiseksi",
|
||||
"chart_history_fallback": "Tälle entiteetille ei ole pitkän aikavälin tilastoja — näytetään recorderin tilahistoria (yleensä viimeiset ~10 päivää)"
|
||||
"chart_history_fallback": "Tälle entiteetille ei ole pitkän aikavälin tilastoja — näytetään recorderin tilahistoria (yleensä viimeiset ~10 päivää)",
|
||||
"chart_history_alarm": "Laukaisimen näkymä tilahistoriasta: 1 = hälytystila pysyi pitoajan, 0 = kunnossa (yleensä viimeiset ~10 päivää)",
|
||||
"chart_history_count": "Muutoslaskuri koottu tilahistoriasta viime huollon jälkeen (yleensä viimeiset ~10 päivää)",
|
||||
"prediction_cycles": "Opittu sykleistä",
|
||||
"phase_require_override": "Ohita ”Vaadi valmistuessa” tälle vaiheelle",
|
||||
"history_add_past": "Lisää aiempi suoritus",
|
||||
"require_tag_scan": "Suoritus vain tunnisteen skannauksella",
|
||||
"require_tag_scan_help": "Läsnäolotodistus: ”Valmis” on estetty kaikkialla, kunnes laitteen NFC-tunniste tai QR-koodi on skannattu. Automaatiot voivat välittää 'via_tag_scan' complete-palvelulle.",
|
||||
"require_tag_scan_hint": "Tämä tehtävä valmistuu vain skannaamalla sen NFC-tunniste tai QR-koodi itse laitteesta — tallennus tässä estetään."
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Vide = minuit (défaut). Fuseau horaire HA.",
|
||||
"at_time": "à",
|
||||
"notes_optional": "Notes (optionnel)",
|
||||
"notes_markdown_hint": "Markdown est pris en charge — **gras**, listes, [liens](…)",
|
||||
"cost_optional": "Coût (optionnel)",
|
||||
"duration_minutes": "Durée en minutes (optionnel)",
|
||||
"completed_at_optional": "Effectué le (optionnel, vide = maintenant)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Titre du panneau latéral",
|
||||
"settings_notifications": "Notifications",
|
||||
"settings_notify_service": "Service de notification",
|
||||
"settings_shopping_list": "Liste de courses (tâches d'achat)",
|
||||
"settings_shopping_list_help": "Les rappels d'achat pour pièces en rupture apparaissent dans cette liste ; cocher un élément réapprovisionne la pièce.",
|
||||
"shopping_list_none": "Désactivé — pas de liste de courses",
|
||||
"settings_install_assist_sentences": "Installer les phrases Assist",
|
||||
"settings_install_assist_sentences_hint": "Copie les phrases vocales dans votre configuration afin que l'agent Assist classique les reconnaisse. Un fichier que vous avez modifié n'est jamais écrasé.",
|
||||
"test_notification": "Notification de test",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Ajouter une étape",
|
||||
"phase_current": "Phase actuelle",
|
||||
"phase_set": "Définir comme actuelle",
|
||||
"chart_history_fallback": "Pas de statistiques à long terme pour cette entité — affichage de l'historique d'états du recorder (généralement les ~10 derniers jours)"
|
||||
"chart_history_fallback": "Pas de statistiques à long terme pour cette entité — affichage de l'historique d'états du recorder (généralement les ~10 derniers jours)",
|
||||
"chart_history_alarm": "Vue déclencheur issue de l'historique d'états : 1 = état d'alerte maintenu pendant le délai, 0 = normal (généralement les ~10 derniers jours)",
|
||||
"chart_history_count": "Nombre de changements reconstitué à partir de l'historique d'états depuis le dernier entretien (généralement les ~10 derniers jours)",
|
||||
"prediction_cycles": "Appris sur cycles",
|
||||
"phase_require_override": "Remplacer « Exiger à l’achèvement » pour cette phase",
|
||||
"history_add_past": "Ajouter une réalisation passée",
|
||||
"require_tag_scan": "Terminer uniquement en scannant le tag",
|
||||
"require_tag_scan_help": "Preuve de présence : « Terminé » est bloqué partout tant que le tag NFC ou le QR code sur l'objet n'a pas été scanné. Les automatisations peuvent passer 'via_tag_scan' au service complete.",
|
||||
"require_tag_scan_hint": "Cette tâche ne se termine qu'en scannant son tag NFC ou son QR code sur l'objet — l'enregistrement ici sera refusé."
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "खाली = मध्यरात्रि (डिफ़ॉल्ट)। HA समय क्षेत्र।",
|
||||
"at_time": "बजे",
|
||||
"notes_optional": "टिप्पणियाँ (वैकल्पिक)",
|
||||
"notes_markdown_hint": "Markdown समर्थित है — **बोल्ड**, सूचियाँ, [लिंक](…)",
|
||||
"cost_optional": "लागत (वैकल्पिक)",
|
||||
"duration_minutes": "मिनटों में अवधि (वैकल्पिक)",
|
||||
"completed_at_optional": "पूर्ण होने का समय (वैकल्पिक, खाली = अभी)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "साइडबार पैनल शीर्षक",
|
||||
"settings_notifications": "सूचनाएँ",
|
||||
"settings_notify_service": "सूचना सेवा",
|
||||
"settings_shopping_list": "शॉपिंग सूची (खरीद कार्य)",
|
||||
"settings_shopping_list_help": "कम स्टॉक वाले पुर्जों के खरीद अनुस्मारक इस सूची में दिखते हैं; टिक करने पर पुर्जा री-स्टॉक होता है।",
|
||||
"shopping_list_none": "बंद — कोई शॉपिंग सूची नहीं",
|
||||
"settings_install_assist_sentences": "Assist वाक्य इंस्टॉल करें",
|
||||
"settings_install_assist_sentences_hint": "वॉइस वाक्यों को आपके कॉन्फ़िगरेशन में कॉपी करता है ताकि क्लासिक Assist एजेंट उन्हें पहचान सके। आपके द्वारा संपादित फ़ाइल कभी अधिलेखित नहीं होती।",
|
||||
"test_notification": "परीक्षण सूचना",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "कदम जोड़ें",
|
||||
"phase_current": "वर्तमान चरण",
|
||||
"phase_set": "वर्तमान बनाएँ",
|
||||
"chart_history_fallback": "इस एंटिटी के लिए दीर्घकालिक आँकड़े नहीं हैं — recorder की स्थिति का इतिहास दिखाया जा रहा है (आमतौर पर पिछले ~10 दिन)"
|
||||
"chart_history_fallback": "इस एंटिटी के लिए दीर्घकालिक आँकड़े नहीं हैं — recorder की स्थिति का इतिहास दिखाया जा रहा है (आमतौर पर पिछले ~10 दिन)",
|
||||
"chart_history_alarm": "स्थिति इतिहास से ट्रिगर दृश्य: 1 = चेतावनी स्थिति होल्ड समय तक बनी रही, 0 = सामान्य (आमतौर पर पिछले ~10 दिन)",
|
||||
"chart_history_count": "पिछली सेवा के बाद से स्थिति इतिहास से पुनर्निर्मित परिवर्तन गणना (आमतौर पर पिछले ~10 दिन)",
|
||||
"prediction_cycles": "चक्रों से सीखा",
|
||||
"phase_require_override": "इस चरण के लिए “पूर्ण करते समय आवश्यक” को ओवरराइड करें",
|
||||
"history_add_past": "पिछली पूर्णता जोड़ें",
|
||||
"require_tag_scan": "केवल टैग स्कैन से पूर्ण करें",
|
||||
"require_tag_scan_help": "उपस्थिति प्रमाण: जब तक वस्तु पर लगा NFC टैग या QR कोड स्कैन न हो, हर जगह “पूर्ण” अवरुद्ध रहता है। ऑटोमेशन complete सेवा को 'via_tag_scan' दे सकते हैं।",
|
||||
"require_tag_scan_hint": "यह कार्य केवल वस्तु पर लगे NFC टैग या QR कोड को स्कैन करके ही पूर्ण होता है — यहाँ सहेजना अस्वीकार होगा।"
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "Üres = éjfél (alapértelmezés). HA időzóna.",
|
||||
"at_time": "ekkor:",
|
||||
"notes_optional": "Megjegyzések (opcionális)",
|
||||
"notes_markdown_hint": "A Markdown támogatott — **félkövér**, listák, [hivatkozások](…)",
|
||||
"cost_optional": "Költség (opcionális)",
|
||||
"duration_minutes": "Időtartam percben (opcionális)",
|
||||
"completed_at_optional": "Elvégezve ekkor (opcionális, üres = most)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "Oldalsáv-panel címe",
|
||||
"settings_notifications": "Értesítések",
|
||||
"settings_notify_service": "Értesítési szolgáltatás",
|
||||
"settings_shopping_list": "Bevásárlólista (vásárlási feladatok)",
|
||||
"settings_shopping_list_help": "A fogyóban lévő alkatrészek vásárlási emlékeztetői ezen a listán jelennek meg; a kipipálás feltölti a készletet.",
|
||||
"shopping_list_none": "Ki — nincs bevásárlólista",
|
||||
"settings_install_assist_sentences": "Assist mondatok telepítése",
|
||||
"settings_install_assist_sentences_hint": "Átmásolja a hangmondatokat a konfigurációdba, hogy a klasszikus Assist ügynök felismerje őket. Az általad szerkesztett fájlt soha nem írja felül.",
|
||||
"test_notification": "Tesztértesítés",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Lépés hozzáadása",
|
||||
"phase_current": "Aktuális fázis",
|
||||
"phase_set": "Beállítás aktuálisként",
|
||||
"chart_history_fallback": "Ehhez az entitáshoz nincs hosszú távú statisztika — a recorder állapotelőzményei jelennek meg (jellemzően az utolsó ~10 nap)"
|
||||
"chart_history_fallback": "Ehhez az entitáshoz nincs hosszú távú statisztika — a recorder állapotelőzményei jelennek meg (jellemzően az utolsó ~10 nap)",
|
||||
"chart_history_alarm": "Trigger nézet az állapotelőzményekből: 1 = riasztási állapot a tartási idő alatt fennállt, 0 = rendben (általában az utolsó ~10 nap)",
|
||||
"chart_history_count": "Változásszámláló az állapotelőzményekből az utolsó szerviz óta (általában az utolsó ~10 nap)",
|
||||
"prediction_cycles": "Ciklusokból tanulva",
|
||||
"phase_require_override": "A „Megkövetelés elvégzéskor” felülbírálása ennél a fázisnál",
|
||||
"history_add_past": "Korábbi elvégzés rögzítése",
|
||||
"require_tag_scan": "Csak címke beolvasásával fejezhető be",
|
||||
"require_tag_scan_help": "Jelenlét-igazolás: a „Kész” mindenhol blokkolva van, amíg be nem olvasod az eszközön lévő NFC-címkét vagy QR-kódot. Az automatizálások 'via_tag_scan' értéket adhatnak át a complete szolgáltatásnak.",
|
||||
"require_tag_scan_hint": "Ez a feladat csak az eszközön lévő NFC-címke vagy QR-kód beolvasásával fejezhető be — az itteni mentés el lesz utasítva."
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Vuoto = mezzanotte (default). Fuso orario HA.",
|
||||
"at_time": "alle",
|
||||
"notes_optional": "Note (opzionale)",
|
||||
"notes_markdown_hint": "Markdown è supportato — **grassetto**, elenchi, [link](…)",
|
||||
"cost_optional": "Costo (opzionale)",
|
||||
"duration_minutes": "Durata in minuti (opzionale)",
|
||||
"completed_at_optional": "Completato il (opzionale, vuoto = adesso)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Titolo pannello laterale",
|
||||
"settings_notifications": "Notifiche",
|
||||
"settings_notify_service": "Servizio di notifica",
|
||||
"settings_shopping_list": "Lista della spesa (attività di acquisto)",
|
||||
"settings_shopping_list_help": "I promemoria di acquisto per ricambi scarsi appaiono in questo elenco; spuntarne uno rifornisce il ricambio.",
|
||||
"shopping_list_none": "Disattivato — nessuna lista della spesa",
|
||||
"settings_install_assist_sentences": "Installa le frasi di Assist",
|
||||
"settings_install_assist_sentences_hint": "Copia le frasi vocali nella tua configurazione affinché l'agente Assist classico le riconosca. Un file modificato da te non viene mai sovrascritto.",
|
||||
"test_notification": "Notifica di test",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Aggiungi passo",
|
||||
"phase_current": "Fase attuale",
|
||||
"phase_set": "Imposta come attuale",
|
||||
"chart_history_fallback": "Nessuna statistica a lungo termine per questa entità — viene mostrata la cronologia degli stati del recorder (di solito gli ultimi ~10 giorni)"
|
||||
"chart_history_fallback": "Nessuna statistica a lungo termine per questa entità — viene mostrata la cronologia degli stati del recorder (di solito gli ultimi ~10 giorni)",
|
||||
"chart_history_alarm": "Vista del trigger dallo storico stati: 1 = stato di allarme mantenuto per il tempo di attesa, 0 = ok (in genere gli ultimi ~10 giorni)",
|
||||
"chart_history_count": "Conteggio dei cambi ricostruito dallo storico stati dall'ultimo intervento (in genere gli ultimi ~10 giorni)",
|
||||
"prediction_cycles": "Appreso da cicli",
|
||||
"phase_require_override": "Sostituisci «Richiedi al completamento» per questa fase",
|
||||
"history_add_past": "Aggiungi un completamento passato",
|
||||
"require_tag_scan": "Completa solo scansionando il tag",
|
||||
"require_tag_scan_help": "Prova di presenza: «Fatto» è bloccato ovunque finché non viene scansionato il tag NFC o il codice QR sull'oggetto. Le automazioni possono passare 'via_tag_scan' al servizio complete.",
|
||||
"require_tag_scan_hint": "Questa attività si completa solo scansionando il suo tag NFC o codice QR sull'oggetto — il salvataggio qui verrà rifiutato."
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "空欄=深夜0時(既定)。HAのタイムゾーン。",
|
||||
"at_time": "時刻",
|
||||
"notes_optional": "メモ(任意)",
|
||||
"notes_markdown_hint": "Markdownに対応 — **太字**、リスト、[リンク](…)",
|
||||
"cost_optional": "費用(任意)",
|
||||
"duration_minutes": "所要時間(分、任意)",
|
||||
"completed_at_optional": "完了日時(任意・空欄 = 現在)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "サイドバーパネルのタイトル",
|
||||
"settings_notifications": "通知",
|
||||
"settings_notify_service": "通知サービス",
|
||||
"settings_shopping_list": "買い物リスト(購入タスク)",
|
||||
"settings_shopping_list_help": "在庫が少ない部品の購入リマインダーがこのリストに表示され、チェックすると部品が補充されます。",
|
||||
"shopping_list_none": "オフ — 買い物リストなし",
|
||||
"settings_install_assist_sentences": "Assist の文を導入する",
|
||||
"settings_install_assist_sentences_hint": "音声フレーズを設定に複製し、従来の Assist エージェントが認識できるようにします。自分で編集したファイルが上書きされることはありません。",
|
||||
"test_notification": "テスト通知",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "ステップを追加",
|
||||
"phase_current": "現在のフェーズ",
|
||||
"phase_set": "現在に設定",
|
||||
"chart_history_fallback": "このエンティティには長期統計がありません — レコーダーの状態履歴を表示しています(通常は直近約10日分)"
|
||||
"chart_history_fallback": "このエンティティには長期統計がありません — レコーダーの状態履歴を表示しています(通常は直近約10日分)",
|
||||
"chart_history_alarm": "状態履歴からのトリガー表示:1 = 保持時間の間アラート状態が続いた、0 = 正常(通常は直近約10日)",
|
||||
"chart_history_count": "前回のサービス以降の状態履歴から再構築した変化回数(通常は直近約10日)",
|
||||
"prediction_cycles": "学習したサイクル数",
|
||||
"phase_require_override": "このフェーズで「完了時に必須」を上書き",
|
||||
"history_add_past": "過去の完了を追加",
|
||||
"require_tag_scan": "タグのスキャンでのみ完了可能",
|
||||
"require_tag_scan_help": "在席証明:対象物のNFCタグまたはQRコードをスキャンするまで、どの画面でも「完了」はブロックされます。オートメーションはcompleteサービスに 'via_tag_scan' を渡せます。",
|
||||
"require_tag_scan_hint": "このタスクは対象物のNFCタグまたはQRコードをスキャンしないと完了できません。ここでの保存は拒否されます。"
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "비워두면 자정(기본값)입니다. HA 시간대 기준.",
|
||||
"at_time": "시각",
|
||||
"notes_optional": "메모 (선택)",
|
||||
"notes_markdown_hint": "Markdown 지원 — **굵게**, 목록, [링크](…)",
|
||||
"cost_optional": "비용 (선택)",
|
||||
"duration_minutes": "소요 시간(분, 선택)",
|
||||
"completed_at_optional": "완료 시각 (선택, 비우면 지금)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "사이드바 패널 제목",
|
||||
"settings_notifications": "알림",
|
||||
"settings_notify_service": "알림 서비스",
|
||||
"settings_shopping_list": "쇼핑 목록 (구매 작업)",
|
||||
"settings_shopping_list_help": "부족한 부품의 구매 알림이 이 목록에 표시되며, 체크하면 부품이 재입고됩니다.",
|
||||
"shopping_list_none": "끄기 — 쇼핑 목록 없음",
|
||||
"settings_install_assist_sentences": "Assist 문장 설치",
|
||||
"settings_install_assist_sentences_hint": "음성 문장을 설정에 복사하여 기존 Assist 에이전트가 인식하도록 합니다. 직접 수정한 파일은 절대 덮어쓰지 않습니다.",
|
||||
"test_notification": "테스트 알림",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "스텝 추가",
|
||||
"phase_current": "현재 단계",
|
||||
"phase_set": "현재로 설정",
|
||||
"chart_history_fallback": "이 엔티티에는 장기 통계가 없습니다 — 레코더 상태 기록을 표시합니다(보통 최근 약 10일)"
|
||||
"chart_history_fallback": "이 엔티티에는 장기 통계가 없습니다 — 레코더 상태 기록을 표시합니다(보통 최근 약 10일)",
|
||||
"chart_history_alarm": "상태 기록 기반 트리거 보기: 1 = 유지 시간 동안 경보 상태 유지, 0 = 정상 (보통 최근 약 10일)",
|
||||
"chart_history_count": "마지막 서비스 이후 상태 기록으로 재구성한 변경 횟수 (보통 최근 약 10일)",
|
||||
"prediction_cycles": "학습한 주기 수",
|
||||
"phase_require_override": "이 단계에서 ‘완료 시 필수’ 재정의",
|
||||
"history_add_past": "지난 완료 추가",
|
||||
"require_tag_scan": "태그 스캔으로만 완료 가능",
|
||||
"require_tag_scan_help": "현장 확인: 기기의 NFC 태그나 QR 코드를 스캔할 때까지 모든 화면에서 '완료'가 차단됩니다. 자동화는 complete 서비스에 'via_tag_scan'을 전달할 수 있습니다.",
|
||||
"require_tag_scan_hint": "이 작업은 기기의 NFC 태그나 QR 코드를 스캔해야만 완료됩니다. 여기서 저장하면 거부됩니다."
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "Tom = midnatt (standard). HA-tidssone.",
|
||||
"at_time": "kl.",
|
||||
"notes_optional": "Notater (valgfritt)",
|
||||
"notes_markdown_hint": "Markdown støttes — **fet**, lister, [lenker](…)",
|
||||
"cost_optional": "Kostnad (valgfritt)",
|
||||
"duration_minutes": "Varighet i minutter (valgfritt)",
|
||||
"completed_at_optional": "Utført den (valgfritt, tomt = nå)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "Tittel på sidepanel",
|
||||
"settings_notifications": "Varsler",
|
||||
"settings_notify_service": "Varslingstjeneste",
|
||||
"settings_shopping_list": "Handleliste (kjøpsoppgaver)",
|
||||
"settings_shopping_list_help": "Kjøpspåminnelser for deler med lav beholdning vises i denne listen; avkryssing fyller på beholdningen.",
|
||||
"shopping_list_none": "Av — ingen handleliste",
|
||||
"settings_install_assist_sentences": "Installer Assist-setninger",
|
||||
"settings_install_assist_sentences_hint": "Kopierer talesetningene til konfigurasjonen din slik at den klassiske Assist-agenten gjenkjenner dem. En fil du selv har endret, blir aldri overskrevet.",
|
||||
"test_notification": "Testvarsel",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Legg til trinn",
|
||||
"phase_current": "Gjeldende fase",
|
||||
"phase_set": "Sett som gjeldende",
|
||||
"chart_history_fallback": "Ingen langtidsstatistikk for denne enheten — recorderens tilstandshistorikk vises (vanligvis de siste ~10 dagene)"
|
||||
"chart_history_fallback": "Ingen langtidsstatistikk for denne enheten — recorderens tilstandshistorikk vises (vanligvis de siste ~10 dagene)",
|
||||
"chart_history_alarm": "Utløservisning fra tilstandshistorikken: 1 = alarmtilstand holdt gjennom holdetiden, 0 = ok (vanligvis de siste ~10 dagene)",
|
||||
"chart_history_count": "Antall endringer gjenoppbygd fra tilstandshistorikken siden siste service (vanligvis de siste ~10 dagene)",
|
||||
"prediction_cycles": "Lært av sykluser",
|
||||
"phase_require_override": "Overstyr «Krev ved fullføring» for denne fasen",
|
||||
"history_add_past": "Legg til tidligere fullføring",
|
||||
"require_tag_scan": "Kan bare fullføres ved tag-skanning",
|
||||
"require_tag_scan_help": "Tilstedeværelsesbevis: «Fullført» er blokkert overalt til NFC-taggen eller QR-koden på selve tingen er skannet. Automatiseringer kan sende 'via_tag_scan' til complete-tjenesten.",
|
||||
"require_tag_scan_hint": "Denne oppgaven fullføres bare ved å skanne NFC-taggen eller QR-koden på selve tingen — lagring her vil bli avvist."
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Leeg = middernacht (standaard). HA-tijdzone.",
|
||||
"at_time": "om",
|
||||
"notes_optional": "Notities (optioneel)",
|
||||
"notes_markdown_hint": "Markdown wordt ondersteund — **vet**, lijsten, [links](…)",
|
||||
"cost_optional": "Kosten (optioneel)",
|
||||
"duration_minutes": "Duur in minuten (optioneel)",
|
||||
"completed_at_optional": "Voltooid op (optioneel, leeg = nu)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Titel zijbalkpaneel",
|
||||
"settings_notifications": "Meldingen",
|
||||
"settings_notify_service": "Meldingsservice",
|
||||
"settings_shopping_list": "Boodschappenlijst (kooptaken)",
|
||||
"settings_shopping_list_help": "Koopherinneringen voor schaarse onderdelen verschijnen in deze lijst; afvinken vult de voorraad aan.",
|
||||
"shopping_list_none": "Uit — geen boodschappenlijst",
|
||||
"settings_install_assist_sentences": "Assist-zinnen installeren",
|
||||
"settings_install_assist_sentences_hint": "Kopieert de spraakzinnen naar je configuratie zodat de klassieke Assist-agent ze herkent. Een bestand dat je zelf hebt bewerkt wordt nooit overschreven.",
|
||||
"test_notification": "Testmelding",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Stap toevoegen",
|
||||
"phase_current": "Huidige fase",
|
||||
"phase_set": "Instellen als huidige",
|
||||
"chart_history_fallback": "Geen langetermijnstatistieken voor deze entiteit — de recorder-statusgeschiedenis wordt getoond (meestal de laatste ~10 dagen)"
|
||||
"chart_history_fallback": "Geen langetermijnstatistieken voor deze entiteit — de recorder-statusgeschiedenis wordt getoond (meestal de laatste ~10 dagen)",
|
||||
"chart_history_alarm": "Triggerweergave uit de statusgeschiedenis: 1 = alarmstatus gedurende de wachttijd aangehouden, 0 = in orde (meestal de laatste ~10 dagen)",
|
||||
"chart_history_count": "Aantal wisselingen opnieuw opgebouwd uit de statusgeschiedenis sinds de laatste service (meestal de laatste ~10 dagen)",
|
||||
"prediction_cycles": "Geleerd uit cycli",
|
||||
"phase_require_override": "„Vereisen bij afronden” voor deze fase overschrijven",
|
||||
"history_add_past": "Eerdere afronding toevoegen",
|
||||
"require_tag_scan": "Alleen af te ronden via tag-scan",
|
||||
"require_tag_scan_help": "Aanwezigheidsbewijs: „Klaar” is overal geblokkeerd totdat de NFC-tag of QR-code op het apparaat zelf is gescand. Automatiseringen kunnen 'via_tag_scan' aan de complete-service meegeven.",
|
||||
"require_tag_scan_hint": "Deze taak wordt alleen afgerond door de NFC-tag of QR-code op het apparaat te scannen — opslaan hier wordt geweigerd."
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Puste = północ (domyślnie). Strefa czasowa HA.",
|
||||
"at_time": "o",
|
||||
"notes_optional": "Notatki (opcjonalne)",
|
||||
"notes_markdown_hint": "Obsługiwany jest Markdown — **pogrubienie**, listy, [linki](…)",
|
||||
"cost_optional": "Koszt (opcjonalne)",
|
||||
"duration_minutes": "Czas trwania w minutach (opcjonalne)",
|
||||
"completed_at_optional": "Wykonano dnia (opcjonalne, puste = teraz)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Tytuł panelu bocznego",
|
||||
"settings_notifications": "Powiadomienia",
|
||||
"settings_notify_service": "Usługa powiadomień",
|
||||
"settings_shopping_list": "Lista zakupów (zadania zakupowe)",
|
||||
"settings_shopping_list_help": "Przypomnienia o zakupie brakujących części pojawiają się na tej liście; odhaczenie uzupełnia zapas.",
|
||||
"shopping_list_none": "Wyłączone — brak listy zakupów",
|
||||
"settings_install_assist_sentences": "Zainstaluj zdania Assist",
|
||||
"settings_install_assist_sentences_hint": "Kopiuje zdania głosowe do Twojej konfiguracji, aby klasyczny agent Assist je rozpoznawał. Plik zmieniony przez Ciebie nigdy nie zostanie nadpisany.",
|
||||
"test_notification": "Powiadomienie testowe",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Dodaj krok",
|
||||
"phase_current": "Bieżąca faza",
|
||||
"phase_set": "Ustaw jako bieżącą",
|
||||
"chart_history_fallback": "Brak statystyk długoterminowych dla tej encji — wyświetlana jest historia stanów recordera (zwykle ostatnie ~10 dni)"
|
||||
"chart_history_fallback": "Brak statystyk długoterminowych dla tej encji — wyświetlana jest historia stanów recordera (zwykle ostatnie ~10 dni)",
|
||||
"chart_history_alarm": "Widok wyzwalacza z historii stanów: 1 = stan alarmu utrzymany przez czas podtrzymania, 0 = w porządku (zwykle ostatnie ~10 dni)",
|
||||
"chart_history_count": "Licznik zmian odtworzony z historii stanów od ostatniego serwisu (zwykle ostatnie ~10 dni)",
|
||||
"prediction_cycles": "Nauczone z cykli",
|
||||
"phase_require_override": "Nadpisz „Wymagaj przy ukończeniu” dla tej fazy",
|
||||
"history_add_past": "Dodaj wcześniejsze ukończenie",
|
||||
"require_tag_scan": "Ukończenie tylko przez skan tagu",
|
||||
"require_tag_scan_help": "Dowód obecności: „Gotowe” jest wszędzie zablokowane, dopóki nie zeskanujesz tagu NFC lub kodu QR na urządzeniu. Automatyzacje mogą przekazać 'via_tag_scan' do usługi complete.",
|
||||
"require_tag_scan_hint": "To zadanie można ukończyć tylko skanując jego tag NFC lub kod QR na urządzeniu — zapis tutaj zostanie odrzucony."
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "Vazio = meia-noite (padrão). Fuso horário do HA.",
|
||||
"at_time": "às",
|
||||
"notes_optional": "Observações (opcional)",
|
||||
"notes_markdown_hint": "Markdown é suportado — **negrito**, listas, [links](…)",
|
||||
"cost_optional": "Custo (opcional)",
|
||||
"duration_minutes": "Duração em minutos (opcional)",
|
||||
"completed_at_optional": "Concluído em (opcional, vazio = agora)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "Título do painel na barra lateral",
|
||||
"settings_notifications": "Notificações",
|
||||
"settings_notify_service": "Serviço de notificação",
|
||||
"settings_shopping_list": "Lista de compras (tarefas de compra)",
|
||||
"settings_shopping_list_help": "Os lembretes de compra de peças em falta aparecem nesta lista; marcar um repõe a peça.",
|
||||
"shopping_list_none": "Desligado — sem lista de compras",
|
||||
"settings_install_assist_sentences": "Instalar frases do Assist",
|
||||
"settings_install_assist_sentences_hint": "Copia as frases de voz para a sua configuração para que o agente Assist clássico as reconheça. Um arquivo que você editou nunca é sobrescrito.",
|
||||
"test_notification": "Notificação de teste",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Adicionar passo",
|
||||
"phase_current": "Fase atual",
|
||||
"phase_set": "Definir como atual",
|
||||
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — mostrando o histórico de estados do recorder (normalmente os últimos ~10 dias)"
|
||||
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — mostrando o histórico de estados do recorder (normalmente os últimos ~10 dias)",
|
||||
"chart_history_alarm": "Visão do gatilho a partir do histórico de estados: 1 = estado de alerta mantido pelo tempo de espera, 0 = normal (normalmente os últimos ~10 dias)",
|
||||
"chart_history_count": "Contagem de mudanças reconstruída a partir do histórico de estados desde o último serviço (normalmente os últimos ~10 dias)",
|
||||
"prediction_cycles": "Aprendido de ciclos",
|
||||
"phase_require_override": "Substituir “Exigir na conclusão” para esta fase",
|
||||
"history_add_past": "Adicionar conclusão passada",
|
||||
"require_tag_scan": "Concluir apenas escaneando a etiqueta",
|
||||
"require_tag_scan_help": "Prova de presença: “Concluído” fica bloqueado em toda parte até escanear a etiqueta NFC ou o código QR no próprio objeto. Automações podem passar 'via_tag_scan' ao serviço complete.",
|
||||
"require_tag_scan_hint": "Esta tarefa só é concluída escaneando sua etiqueta NFC ou código QR no próprio objeto — salvar aqui será recusado."
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Vazio = meia-noite (padrão). Fuso horário HA.",
|
||||
"at_time": "às",
|
||||
"notes_optional": "Notas (opcional)",
|
||||
"notes_markdown_hint": "Markdown é suportado — **negrito**, listas, [ligações](…)",
|
||||
"cost_optional": "Custo (opcional)",
|
||||
"duration_minutes": "Duração em minutos (opcional)",
|
||||
"completed_at_optional": "Concluído em (opcional, vazio = agora)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Título do painel lateral",
|
||||
"settings_notifications": "Notificações",
|
||||
"settings_notify_service": "Serviço de notificação",
|
||||
"settings_shopping_list": "Lista de compras (tarefas de compra)",
|
||||
"settings_shopping_list_help": "Os lembretes de compra de peças em falta aparecem nesta lista; marcar um repõe a peça.",
|
||||
"shopping_list_none": "Desligado — sem lista de compras",
|
||||
"settings_install_assist_sentences": "Instalar frases do Assist",
|
||||
"settings_install_assist_sentences_hint": "Copia as frases de voz para a sua configuração para que o agente Assist clássico as reconheça. Um ficheiro que editou nunca é substituído.",
|
||||
"test_notification": "Notificação de teste",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Adicionar passo",
|
||||
"phase_current": "Fase atual",
|
||||
"phase_set": "Definir como atual",
|
||||
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — a mostrar o histórico de estados do recorder (normalmente os últimos ~10 dias)"
|
||||
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — a mostrar o histórico de estados do recorder (normalmente os últimos ~10 dias)",
|
||||
"chart_history_alarm": "Vista do disparador a partir do histórico de estados: 1 = estado de alerta mantido pelo tempo de espera, 0 = normal (normalmente os últimos ~10 dias)",
|
||||
"chart_history_count": "Contagem de mudanças reconstruída a partir do histórico de estados desde o último serviço (normalmente os últimos ~10 dias)",
|
||||
"prediction_cycles": "Aprendido de ciclos",
|
||||
"phase_require_override": "Substituir «Exigir na conclusão» para esta fase",
|
||||
"history_add_past": "Adicionar conclusão passada",
|
||||
"require_tag_scan": "Concluir apenas lendo a etiqueta",
|
||||
"require_tag_scan_help": "Prova de presença: «Concluído» fica bloqueado em todo o lado até ler a etiqueta NFC ou o código QR no próprio objeto. As automações podem passar 'via_tag_scan' ao serviço complete.",
|
||||
"require_tag_scan_hint": "Esta tarefa só é concluída lendo a sua etiqueta NFC ou código QR no próprio objeto — guardar aqui será recusado."
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Пусто = полночь (по умолчанию). Часовой пояс HA.",
|
||||
"at_time": "в",
|
||||
"notes_optional": "Примечания (опционально)",
|
||||
"notes_markdown_hint": "Поддерживается Markdown — **жирный**, списки, [ссылки](…)",
|
||||
"cost_optional": "Стоимость (опционально)",
|
||||
"duration_minutes": "Длительность в минутах (опционально)",
|
||||
"completed_at_optional": "Выполнено (необязательно, пусто = сейчас)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Заголовок панели",
|
||||
"settings_notifications": "Уведомления",
|
||||
"settings_notify_service": "Сервис уведомлений",
|
||||
"settings_shopping_list": "Список покупок (задачи покупки)",
|
||||
"settings_shopping_list_help": "Напоминания о покупке заканчивающихся деталей появляются в этом списке; отметка пополняет запас.",
|
||||
"shopping_list_none": "Выкл. — без списка покупок",
|
||||
"settings_install_assist_sentences": "Установить фразы Assist",
|
||||
"settings_install_assist_sentences_hint": "Копирует голосовые фразы в вашу конфигурацию, чтобы классический агент Assist их распознавал. Файл, который вы изменили, никогда не перезаписывается.",
|
||||
"test_notification": "Тестовое уведомление",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Добавить шаг",
|
||||
"phase_current": "Текущая фаза",
|
||||
"phase_set": "Сделать текущей",
|
||||
"chart_history_fallback": "Для этой сущности нет долгосрочной статистики — показана история состояний recorder (обычно последние ~10 дней)"
|
||||
"chart_history_fallback": "Для этой сущности нет долгосрочной статистики — показана история состояний recorder (обычно последние ~10 дней)",
|
||||
"chart_history_alarm": "Вид триггера из истории состояний: 1 = состояние тревоги удерживалось в течение времени удержания, 0 = норма (обычно последние ~10 дней)",
|
||||
"chart_history_count": "Счётчик изменений восстановлен из истории состояний с момента последнего обслуживания (обычно последние ~10 дней)",
|
||||
"prediction_cycles": "Изучено по циклам",
|
||||
"phase_require_override": "Переопределить «Требовать при выполнении» для этой фазы",
|
||||
"history_add_past": "Добавить прошлое выполнение",
|
||||
"require_tag_scan": "Завершение только по скану метки",
|
||||
"require_tag_scan_help": "Подтверждение присутствия: «Готово» заблокировано везде, пока не отсканирована NFC-метка или QR-код на самом устройстве. Автоматизации могут передавать 'via_tag_scan' в службу complete.",
|
||||
"require_tag_scan_hint": "Эта задача завершается только сканированием её NFC-метки или QR-кода на самом устройстве — сохранение здесь будет отклонено."
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Tomt = midnatt (standard). HA-tidszon.",
|
||||
"at_time": "kl.",
|
||||
"notes_optional": "Anteckningar (valfritt)",
|
||||
"notes_markdown_hint": "Markdown stöds — **fetstil**, listor, [länkar](…)",
|
||||
"cost_optional": "Kostnad (valfritt)",
|
||||
"duration_minutes": "Varaktighet i minuter (valfritt)",
|
||||
"completed_at_optional": "Utförd den (valfritt, tomt = nu)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Sidopanelens titel",
|
||||
"settings_notifications": "Notifikationer",
|
||||
"settings_notify_service": "Notifikationstjänst",
|
||||
"settings_shopping_list": "Inköpslista (köpuppgifter)",
|
||||
"settings_shopping_list_help": "Köppåminnelser för delar som tar slut visas i denna lista; att bocka av en fyller på lagret.",
|
||||
"shopping_list_none": "Av — ingen inköpslista",
|
||||
"settings_install_assist_sentences": "Installera Assist-meningar",
|
||||
"settings_install_assist_sentences_hint": "Kopierar rösmeningarna till din konfiguration så att den klassiska Assist-agenten känner igen dem. En fil du själv har ändrat skrivs aldrig över.",
|
||||
"test_notification": "Testnotifikation",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Lägg till steg",
|
||||
"phase_current": "Aktuell fas",
|
||||
"phase_set": "Ange som aktuell",
|
||||
"chart_history_fallback": "Ingen långtidsstatistik för denna entitet — recorderns tillståndshistorik visas (vanligtvis de senaste ~10 dagarna)"
|
||||
"chart_history_fallback": "Ingen långtidsstatistik för denna entitet — recorderns tillståndshistorik visas (vanligtvis de senaste ~10 dagarna)",
|
||||
"chart_history_alarm": "Utlösarvy från tillståndshistoriken: 1 = larmläge hållet under hålltiden, 0 = ok (vanligtvis de senaste ~10 dagarna)",
|
||||
"chart_history_count": "Antal ändringar återskapat från tillståndshistoriken sedan senaste service (vanligtvis de senaste ~10 dagarna)",
|
||||
"prediction_cycles": "Lärt av cykler",
|
||||
"phase_require_override": "Åsidosätt ”Kräv vid slutförande” för denna fas",
|
||||
"history_add_past": "Lägg till tidigare slutförande",
|
||||
"require_tag_scan": "Kan endast slutföras via tagg-skanning",
|
||||
"require_tag_scan_help": "Närvarobevis: ”Klar” är blockerad överallt tills NFC-taggen eller QR-koden på själva saken har skannats. Automationer kan skicka 'via_tag_scan' till complete-tjänsten.",
|
||||
"require_tag_scan_hint": "Den här uppgiften slutförs endast genom att skanna dess NFC-tagg eller QR-kod på själva saken — att spara här kommer att nekas."
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "Boş = gece yarısı (varsayılan). HA saat dilimi.",
|
||||
"at_time": "saat",
|
||||
"notes_optional": "Notlar (isteğe bağlı)",
|
||||
"notes_markdown_hint": "Markdown desteklenir — **kalın**, listeler, [bağlantılar](…)",
|
||||
"cost_optional": "Maliyet (isteğe bağlı)",
|
||||
"duration_minutes": "Dakika cinsinden süre (isteğe bağlı)",
|
||||
"completed_at_optional": "Tamamlanma zamanı (isteğe bağlı, boş = şimdi)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "Kenar çubuğu paneli başlığı",
|
||||
"settings_notifications": "Bildirimler",
|
||||
"settings_notify_service": "Bildirim servisi",
|
||||
"settings_shopping_list": "Alışveriş listesi (satın alma görevleri)",
|
||||
"settings_shopping_list_help": "Azalan parçalar için satın alma hatırlatıcıları bu listede görünür; işaretlemek parçayı yeniden stoklar.",
|
||||
"shopping_list_none": "Kapalı — alışveriş listesi yok",
|
||||
"settings_install_assist_sentences": "Assist cümlelerini yükle",
|
||||
"settings_install_assist_sentences_hint": "Sesli komut cümlelerini yapılandırmanıza kopyalar, böylece klasik Assist aracısı bunları tanır. Kendi düzenlediğiniz bir dosyanın üzerine asla yazılmaz.",
|
||||
"test_notification": "Test bildirimi",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Adım ekle",
|
||||
"phase_current": "Geçerli aşama",
|
||||
"phase_set": "Geçerli olarak ayarla",
|
||||
"chart_history_fallback": "Bu varlık için uzun dönem istatistik yok — recorder durum geçmişi gösteriliyor (genellikle son ~10 gün)"
|
||||
"chart_history_fallback": "Bu varlık için uzun dönem istatistik yok — recorder durum geçmişi gösteriliyor (genellikle son ~10 gün)",
|
||||
"chart_history_alarm": "Durum geçmişinden tetikleyici görünümü: 1 = uyarı durumu bekletme süresi boyunca sürdü, 0 = normal (genellikle son ~10 gün)",
|
||||
"chart_history_count": "Değişim sayısı son servisten bu yana durum geçmişinden yeniden oluşturuldu (genellikle son ~10 gün)",
|
||||
"prediction_cycles": "Döngülerden öğrenildi",
|
||||
"phase_require_override": "Bu aşama için “Tamamlarken zorunlu tut” ayarını geçersiz kıl",
|
||||
"history_add_past": "Geçmiş tamamlama ekle",
|
||||
"require_tag_scan": "Yalnızca etiket taranarak tamamlanır",
|
||||
"require_tag_scan_help": "Yerinde olma kanıtı: Cihazın üzerindeki NFC etiketi veya QR kodu taranana kadar 'Tamamlandı' her yerde engellenir. Otomasyonlar complete servisine 'via_tag_scan' iletebilir.",
|
||||
"require_tag_scan_hint": "Bu görev yalnızca cihazın üzerindeki NFC etiketi veya QR kodu taranarak tamamlanır — buradan kaydetme reddedilecektir."
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Порожньо = опівночі (за замовчуванням). Часовий пояс HA.",
|
||||
"at_time": "о",
|
||||
"notes_optional": "Примітки (необов'язково)",
|
||||
"notes_markdown_hint": "Підтримується Markdown — **жирний**, списки, [посилання](…)",
|
||||
"cost_optional": "Вартість (необов'язково)",
|
||||
"duration_minutes": "Тривалість у хвилинах (необов'язково)",
|
||||
"completed_at_optional": "Виконано (необов'язково, порожньо = зараз)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Заголовок панелі",
|
||||
"settings_notifications": "Сповіщення",
|
||||
"settings_notify_service": "Служба сповіщень",
|
||||
"settings_shopping_list": "Список покупок (завдання покупки)",
|
||||
"settings_shopping_list_help": "Нагадування про купівлю деталей, що закінчуються, з'являються в цьому списку; відмітка поповнює запас.",
|
||||
"shopping_list_none": "Вимк. — без списку покупок",
|
||||
"settings_install_assist_sentences": "Встановити фрази Assist",
|
||||
"settings_install_assist_sentences_hint": "Копіює голосові фрази до вашої конфігурації, щоб класичний агент Assist їх розпізнавав. Файл, який ви редагували, ніколи не перезаписується.",
|
||||
"test_notification": "Тестове сповіщення",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Додати крок",
|
||||
"phase_current": "Поточна фаза",
|
||||
"phase_set": "Зробити поточною",
|
||||
"chart_history_fallback": "Для цієї сутності немає довгострокової статистики — показано історію станів recorder (зазвичай останні ~10 днів)"
|
||||
"chart_history_fallback": "Для цієї сутності немає довгострокової статистики — показано історію станів recorder (зазвичай останні ~10 днів)",
|
||||
"chart_history_alarm": "Вигляд тригера з історії станів: 1 = стан тривоги утримувався протягом часу утримання, 0 = норма (зазвичай останні ~10 днів)",
|
||||
"chart_history_count": "Лічильник змін відновлено з історії станів від останнього обслуговування (зазвичай останні ~10 днів)",
|
||||
"prediction_cycles": "Вивчено з циклів",
|
||||
"phase_require_override": "Перевизначити «Вимагати при виконанні» для цієї фази",
|
||||
"history_add_past": "Додати минуле виконання",
|
||||
"require_tag_scan": "Завершення лише скануванням мітки",
|
||||
"require_tag_scan_help": "Підтвердження присутності: «Готово» заблоковано всюди, доки не відскановано NFC-мітку або QR-код на самому пристрої. Автоматизації можуть передати 'via_tag_scan' службі complete.",
|
||||
"require_tag_scan_hint": "Це завдання завершується лише скануванням його NFC-мітки або QR-коду на самому пристрої — збереження тут буде відхилено."
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "留空 = 午夜 (默认)。基于 HA 时区。",
|
||||
"at_time": "于",
|
||||
"notes_optional": "备注 (可选)",
|
||||
"notes_markdown_hint": "支持 Markdown — **加粗**、列表、[链接](…)",
|
||||
"cost_optional": "成本 (可选)",
|
||||
"duration_minutes": "耗时 (分钟, 可选)",
|
||||
"completed_at_optional": "完成时间(可选,留空 = 现在)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "侧边栏面板标题",
|
||||
"settings_notifications": "通知",
|
||||
"settings_notify_service": "通知服务",
|
||||
"settings_shopping_list": "购物清单(购买任务)",
|
||||
"settings_shopping_list_help": "库存不足部件的购买提醒会显示在此清单中;勾选即可补充库存。",
|
||||
"shopping_list_none": "关闭 — 无购物清单",
|
||||
"settings_install_assist_sentences": "安装 Assist 语句",
|
||||
"settings_install_assist_sentences_hint": "将语音语句复制到你的配置中,让传统 Assist 代理能够识别它们。你自己修改过的文件永远不会被覆盖。",
|
||||
"test_notification": "测试通知",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "添加步骤",
|
||||
"phase_current": "当前阶段",
|
||||
"phase_set": "设为当前",
|
||||
"chart_history_fallback": "该实体没有长期统计数据——显示 recorder 状态历史(通常为最近约 10 天)"
|
||||
"chart_history_fallback": "该实体没有长期统计数据——显示 recorder 状态历史(通常为最近约 10 天)",
|
||||
"chart_history_alarm": "来自状态历史的触发视图:1 = 警报状态保持了保持时间,0 = 正常(通常为最近约 10 天)",
|
||||
"chart_history_count": "根据上次维护以来的状态历史重建的变化次数(通常为最近约 10 天)",
|
||||
"prediction_cycles": "已学习的周期数",
|
||||
"phase_require_override": "为此阶段覆盖“完成时要求”",
|
||||
"history_add_past": "补录过去的完成记录",
|
||||
"require_tag_scan": "仅可通过扫描标签完成",
|
||||
"require_tag_scan_help": "在场证明:在扫描设备上的 NFC 标签或二维码之前,所有界面的“完成”均被阻止。自动化可向 complete 服务传递 'via_tag_scan'。",
|
||||
"require_tag_scan_hint": "此任务只能通过扫描设备上的 NFC 标签或二维码来完成——在此保存将被拒绝。"
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@ import type {
|
||||
SavedViewFilters,
|
||||
} from "./types";
|
||||
import { UserService } from "./user-service";
|
||||
import { partsForCompletion } from "./helpers/shared-parts";
|
||||
import { effectivePhase, phaseLabel } from "./helpers/phases";
|
||||
import { phaseLabel } from "./helpers/phases";
|
||||
import { buildCompleteDialogArgs, fillAndOpenCompleteDialog } from "./helpers/complete-dialog-args";
|
||||
import "./maintenance-card-editor";
|
||||
import "./components/complete-dialog";
|
||||
import type { MaintenanceCompleteDialog } from "./components/complete-dialog";
|
||||
// The Battery Fleet card ships in this globally-loaded bundle so it is
|
||||
// available on every dashboard without another extra_module_url entry.
|
||||
import "./components/battery-fleet-card";
|
||||
@@ -514,32 +515,26 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
@click=${(e: Event) => {
|
||||
// Stop the row's open-task handler from also firing
|
||||
e.stopPropagation();
|
||||
const dlg = this.shadowRoot!.querySelector("maintenance-complete-dialog") as any;
|
||||
// #139: a phased task completes the phase
|
||||
// currently due — its fields override the
|
||||
// task-level ones (fallthrough included).
|
||||
const phase = effectivePhase(task);
|
||||
dlg.entryId = entry_id;
|
||||
dlg.taskId = task.id;
|
||||
dlg.taskName = task.name;
|
||||
dlg.checklist = phase ? phase.checklist : (task.checklist || []);
|
||||
dlg.adaptiveEnabled = !!task.adaptive_config?.enabled;
|
||||
dlg.taskType = task.type || "";
|
||||
dlg.readingUnit = (task as any).reading_unit || "";
|
||||
dlg.requiredFields = phase ? phase.requiredFields : (task.required_completion_fields || []);
|
||||
dlg.phaseLabel = phaseLabel(task);
|
||||
dlg.lang = L;
|
||||
// #99: editable per-completion parts selection
|
||||
// (skip on buy tasks — those restock instead).
|
||||
// #111: the list also carries the shared pools
|
||||
// this task draws on, each named after its
|
||||
// owner — resolving against the object's own
|
||||
// parts alone left a foreign link invisible.
|
||||
const isBuy = !!(task as any).part_ref;
|
||||
const links = phase ? phase.consumesParts : (task.consumes_parts || []);
|
||||
dlg.parts = isBuy ? [] : partsForCompletion({ consumes_parts: links }, entry_id, this._objects, L);
|
||||
dlg.consumesParts = isBuy ? [] : links;
|
||||
dlg.open();
|
||||
const dlg = this.shadowRoot!.querySelector<MaintenanceCompleteDialog>("maintenance-complete-dialog")!;
|
||||
// The SAME derivation the panel uses (phase
|
||||
// override, parts incl. shared pools, tag-scan
|
||||
// gate, restock default, checklist ticks) — the
|
||||
// card used to forward a subset.
|
||||
fillAndOpenCompleteDialog(
|
||||
dlg,
|
||||
buildCompleteDialogArgs({
|
||||
entryId: entry_id,
|
||||
taskId: task.id,
|
||||
taskName: task.name,
|
||||
task,
|
||||
objects: this._objects,
|
||||
lang: L,
|
||||
checklist: task.checklist || [],
|
||||
adaptiveEnabled: !!task.adaptive_config?.enabled,
|
||||
currencySymbol: this._stats?.budget?.currency_symbol || "",
|
||||
}),
|
||||
L,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ha-icon icon="mdi:check"></ha-icon>
|
||||
|
||||
@@ -15,9 +15,11 @@ import { buildObjectReportHtml, type ReportLabels } from "./helpers/report";
|
||||
import { warrantyStatus } from "./helpers/warranty";
|
||||
import { OBJECT_COLUMNS, DEFAULT_OBJECTS_TABLE_COLUMNS, sanitizeColumns } from "./helpers/object-columns";
|
||||
import { downloadTextFile } from "./helpers/download";
|
||||
import { renderNotesMarkdown } from "./helpers/notes-markdown";
|
||||
import { buildTaskWorksheetHtml, type WorksheetExcerpt, type WorksheetLabels } from "./helpers/worksheet";
|
||||
import { describePartLink, partsForCompletion } from "./helpers/shared-parts";
|
||||
import { effectivePhase, phaseLabel } from "./helpers/phases";
|
||||
import { describePartLink } from "./helpers/shared-parts";
|
||||
import { effectivePhase } from "./helpers/phases";
|
||||
import { buildCompleteDialogArgs, fillAndOpenCompleteDialog } from "./helpers/complete-dialog-args";
|
||||
import { describeWsError } from "./ws-errors";
|
||||
import { panelStyles } from "./panel-styles";
|
||||
import type {
|
||||
@@ -694,6 +696,20 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
if ((msg as SubscriptionEvent<MaintenanceObjectResponse>).objects) {
|
||||
writeObjectsCache(next, this._stats ?? null);
|
||||
}
|
||||
// The KPI row (_stats) and the budget card come from their own
|
||||
// endpoints — a pushed change (completion from the card, a timer
|
||||
// wave flipping a task overdue) left them frozen at the last
|
||||
// _loadData until the next user action.
|
||||
void this._refreshKpis();
|
||||
// The open task's full history is a separate fetch too; the
|
||||
// truncated list payload never wins the "longer list" comparison
|
||||
// in _historyCtx, so a pushed completion stayed invisible there.
|
||||
if (
|
||||
this._view === "task" && this._selectedEntryId && this._selectedTaskId
|
||||
&& (!!ev.objects || (ev.delta || []).some((d) => d.entry_id === this._selectedEntryId))
|
||||
) {
|
||||
void this._fetchFullHistory(this._selectedEntryId, this._selectedTaskId);
|
||||
}
|
||||
}
|
||||
},
|
||||
// deltas: only entries whose rebuilt response actually changed —
|
||||
@@ -713,6 +729,34 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Refetch the KPI statistics + budget status after a pushed object change.
|
||||
* Coalesced: a burst of deltas (import, timer wave) runs at most one
|
||||
* in-flight fetch plus one trailing catch-up instead of one per event. */
|
||||
private _kpiRefreshInFlight = false;
|
||||
private _kpiRefreshPending = false;
|
||||
|
||||
private async _refreshKpis(): Promise<void> {
|
||||
if (this._kpiRefreshInFlight) {
|
||||
this._kpiRefreshPending = true;
|
||||
return;
|
||||
}
|
||||
this._kpiRefreshInFlight = true;
|
||||
try {
|
||||
do {
|
||||
this._kpiRefreshPending = false;
|
||||
const [statsResult, budgetResult] = await Promise.all([
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/statistics" }).catch(() => null),
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/budget_status" }).catch(() => null),
|
||||
]);
|
||||
if (!this.isConnected) return;
|
||||
if (statsResult) this._stats = statsResult as StatisticsResponse;
|
||||
if (budgetResult) this._budget = budgetResult as BudgetStatus;
|
||||
} while (this._kpiRefreshPending);
|
||||
} finally {
|
||||
this._kpiRefreshInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Data accessors ---
|
||||
|
||||
private get _taskRows(): TaskRow[] {
|
||||
@@ -1734,17 +1778,27 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
this._showToast(t("quick_complete_success", this._lang));
|
||||
} catch (e: unknown) {
|
||||
const code = (e as { code?: string })?.code || "";
|
||||
if (code === "no_defaults") {
|
||||
// No defaults set — open the dialog so the user can fill them in.
|
||||
// Both refusals mean "the dialog has to collect something": no
|
||||
// quick-complete defaults configured, or the task demands completion
|
||||
// details a silent complete cannot supply. The dialog opens as the
|
||||
// scan's fallback — via_tag_scan keeps the proof of presence.
|
||||
if (code === "no_defaults" || code === "completion_details_required") {
|
||||
this._openCompleteDialog(
|
||||
entryId, taskId, task.name,
|
||||
this._features.checklists ? task.checklist : undefined,
|
||||
this._features.adaptive && !!task.adaptive_config?.enabled,
|
||||
{ viaTagScan: true },
|
||||
);
|
||||
} else {
|
||||
this._showToast(t("action_error", this._lang));
|
||||
// Relay the server's reason (too_early carries the earliest date)
|
||||
// instead of a generic "action failed".
|
||||
this._showToast(describeWsError(e, this._lang, t("action_error", this._lang)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Silent success: the list must reflect the completion right away — the
|
||||
// subscription delta may lag or be coalesced.
|
||||
try { await this._loadData(); } catch { /* subscription will sync */ }
|
||||
}
|
||||
|
||||
// v2.21: printable one-pager for a task — details, checklist tick boxes,
|
||||
@@ -1874,59 +1928,41 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _openCompleteDialog(entryId: string, taskId: string, taskName: string, checklist?: string[], adaptiveEnabled?: boolean): void {
|
||||
private _openCompleteDialog(
|
||||
entryId: string, taskId: string, taskName: string, checklist?: string[], adaptiveEnabled?: boolean,
|
||||
opts?: { viaTagScan?: boolean },
|
||||
): void {
|
||||
void this._ui<MaintenanceCompleteDialog>("maintenance-complete-dialog")
|
||||
.then((dlg) => dlg && this._fillAndOpenCompleteDialog(dlg, entryId, taskId, taskName, checklist, adaptiveEnabled));
|
||||
.then((dlg) => dlg && this._fillAndOpenCompleteDialog(dlg, entryId, taskId, taskName, checklist, adaptiveEnabled, opts));
|
||||
}
|
||||
|
||||
private _fillAndOpenCompleteDialog(dlg: MaintenanceCompleteDialog, entryId: string, taskId: string, taskName: string, checklist?: string[], adaptiveEnabled?: boolean): void {
|
||||
dlg.entryId = entryId;
|
||||
dlg.taskId = taskId;
|
||||
dlg.taskName = taskName;
|
||||
dlg.lang = this._lang;
|
||||
dlg.checklist = checklist || [];
|
||||
dlg.adaptiveEnabled = !!adaptiveEnabled;
|
||||
// v2.20 (#83): reading tasks get a value field — resolve type + unit here
|
||||
// so none of the many call sites need to thread them through.
|
||||
const tk = this._objects
|
||||
.find((o) => o.entry_id === entryId)
|
||||
?.tasks.find((tsk) => tsk.id === taskId);
|
||||
// #139: a phased task completes the phase currently due — its checklist,
|
||||
// parts and required fields override the task-level values (fallthrough
|
||||
// is already baked into effectivePhase). Resolved HERE so every call
|
||||
// site of _openCompleteDialog rotates for free.
|
||||
const phase = tk ? effectivePhase(tk) : null;
|
||||
dlg.phaseLabel = phase ? phaseLabel(tk) : "";
|
||||
dlg.taskType = tk?.type || "";
|
||||
dlg.readingUnit = tk?.reading_unit || "";
|
||||
// #73: ticks recorded during the cycle prefill the dialog's checklist.
|
||||
dlg.checklistPrefill = tk?.checklist_progress || {};
|
||||
dlg.requiredFields = phase ? phase.requiredFields : (tk?.required_completion_fields || []);
|
||||
if (phase) {
|
||||
dlg.checklist = this._features.checklists ? phase.checklist : [];
|
||||
}
|
||||
// Spare parts: a buy task gets an editable restock-qty field; a consuming
|
||||
// task shows what it will decrement (incl. the storage location).
|
||||
const objParts = this._objects.find((o) => o.entry_id === entryId)?.parts || [];
|
||||
const refPart = tk?.part_ref ? objParts.find((pt) => pt.id === tk.part_ref!.part_id) : undefined;
|
||||
dlg.restockDefault = tk?.part_ref ? (refPart?.restock_quantity ?? 1) : null;
|
||||
// #104 follow-up: parts carry unit costs — the dialog offers their sum
|
||||
// as a one-click cost suggestion (buy task: restock qty × unit cost).
|
||||
dlg.restockUnitCost = tk?.part_ref ? (refPart?.cost ?? null) : null;
|
||||
dlg.currencySymbol = this._currencySymbol;
|
||||
// #111: a link may point at another object's pool — name that object, and
|
||||
// never drop a line that fails to resolve (the old .filter(Boolean) hid it).
|
||||
const links = phase ? phase.consumesParts : (tk?.consumes_parts || []);
|
||||
dlg.consumesInfo = links.map((link) =>
|
||||
describePartLink(link, entryId, this._objects, this._lang),
|
||||
/** Every dialog field is derived in helpers/complete-dialog-args — the
|
||||
* ONE derivation the panel, the Lovelace card and the quick-actions
|
||||
* dialog share (phase override #139, reading type+unit #83, restock
|
||||
* default + unit cost #104, shared pools #111, checklist ticks #73,
|
||||
* tag-scan gate). Resolved HERE so none of the call sites need to
|
||||
* thread anything but the name through. */
|
||||
private _fillAndOpenCompleteDialog(
|
||||
dlg: MaintenanceCompleteDialog, entryId: string, taskId: string, taskName: string,
|
||||
checklist?: string[], adaptiveEnabled?: boolean, opts?: { viaTagScan?: boolean },
|
||||
): void {
|
||||
fillAndOpenCompleteDialog(
|
||||
dlg,
|
||||
buildCompleteDialogArgs({
|
||||
entryId,
|
||||
taskId,
|
||||
taskName,
|
||||
task: this._getTask(entryId, taskId),
|
||||
objects: this._objects,
|
||||
lang: this._lang,
|
||||
checklist,
|
||||
checklistsEnabled: this._features.checklists,
|
||||
adaptiveEnabled,
|
||||
currencySymbol: this._currencySymbol,
|
||||
viaTagScan: opts?.viaTagScan,
|
||||
}),
|
||||
this._lang,
|
||||
);
|
||||
// #99: editable per-completion parts selection (not on buy tasks — those
|
||||
// RESTOCK via the qty field instead of consuming). The list carries the
|
||||
// object's own parts plus the shared pools this task draws on, so a foreign
|
||||
// link is visible and untickable rather than silently absent.
|
||||
dlg.parts = tk?.part_ref ? [] : partsForCompletion({ consumes_parts: links }, entryId, this._objects, this._lang);
|
||||
dlg.consumesParts = tk?.part_ref ? [] : links;
|
||||
dlg.open();
|
||||
}
|
||||
|
||||
private _openQrForObject(entryId: string, objectName: string): void {
|
||||
@@ -3287,7 +3323,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
${o.notes
|
||||
? html`<div class="object-notes">
|
||||
<div class="object-notes-label">${t("object_notes_label", L)}</div>
|
||||
<div class="object-notes-body">${o.notes}</div>
|
||||
<div class="object-notes-body">${renderNotesMarkdown(o.notes)}</div>
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
|
||||
@@ -445,6 +445,12 @@ export const panelStyles = css`
|
||||
word-break: break-word;
|
||||
line-height: 1.45;
|
||||
}
|
||||
/* Markdown notes: <ha-markdown> parses the source itself — the container's
|
||||
pre-wrap must not leak into its rendered paragraphs. */
|
||||
.object-notes-body ha-markdown,
|
||||
.task-meta-notes ha-markdown {
|
||||
white-space: normal;
|
||||
}
|
||||
.empty { color: var(--secondary-text-color); font-style: italic; }
|
||||
.analysis-empty-state { text-align: center; padding: 24px 16px; }
|
||||
.analysis-empty-state .empty { font-size: 15px; margin-bottom: 8px; }
|
||||
@@ -1053,6 +1059,19 @@ export const panelStyles = css`
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* #142: "add a past completion" entry point in the history tab — the
|
||||
backdate field lives in the complete dialog, but people LOOK for it
|
||||
here. */
|
||||
.history-add-past {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.history-add-past-btn ha-icon {
|
||||
--mdc-icon-size: 18px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* #139: cycle-phase strip in the task overview */
|
||||
.phases-card {
|
||||
background: var(--card-background-color, #fff);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user