ha-mcp-dev 7.6.0.dev628__tar.gz → 7.6.0.dev630__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ha_mcp_dev-7.6.0.dev628/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.6.0.dev630}/PKG-INFO +1 -1
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/pyproject.toml +1 -1
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/client/websocket_client.py +25 -1
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/settings_ui.py +59 -10
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/helpers.py +49 -22
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_code.py +11 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/LICENSE +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/README.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/setup.cfg +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/tests/test_env_manager.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.6.0.
|
|
7
|
+
version = "7.6.0.dev630"
|
|
8
8
|
description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13,<3.14"
|
|
@@ -235,6 +235,10 @@ class HomeAssistantWebSocketClient:
|
|
|
235
235
|
self._send_lock: asyncio.Lock | None = None
|
|
236
236
|
self._lock_loop: asyncio.AbstractEventLoop | None = None
|
|
237
237
|
self._state = WebSocketConnectionState()
|
|
238
|
+
# Reason the most recent connect() attempt failed (exception text),
|
|
239
|
+
# or None. Surfaced by callers so the agent sees *why* a WebSocket
|
|
240
|
+
# connection failed instead of an opaque "Failed to connect" string.
|
|
241
|
+
self._last_connect_error: str | None = None
|
|
238
242
|
|
|
239
243
|
# Parse URL to get WebSocket endpoint
|
|
240
244
|
parsed = urlparse(self.base_url)
|
|
@@ -259,6 +263,7 @@ class HomeAssistantWebSocketClient:
|
|
|
259
263
|
try:
|
|
260
264
|
logger.info(f"Connecting to Home Assistant WebSocket: {self.ws_url}")
|
|
261
265
|
self._state.reset_connection()
|
|
266
|
+
self._last_connect_error = None
|
|
262
267
|
|
|
263
268
|
# Only configure an SSLContext for wss://; ws:// (Supervisor
|
|
264
269
|
# proxy) doesn't use TLS and gets ssl=None.
|
|
@@ -326,6 +331,7 @@ class HomeAssistantWebSocketClient:
|
|
|
326
331
|
return True
|
|
327
332
|
|
|
328
333
|
except Exception as e:
|
|
334
|
+
self._last_connect_error = f"{type(e).__name__}: {e}"
|
|
329
335
|
if _is_ssl_error(e) and self.verify_ssl:
|
|
330
336
|
logger.error(
|
|
331
337
|
"WebSocket TLS verification failed for %s: %s. "
|
|
@@ -925,6 +931,17 @@ class HomeAssistantWebSocketClient:
|
|
|
925
931
|
"""Check if WebSocket is connected and authenticated."""
|
|
926
932
|
return self._state.is_ready
|
|
927
933
|
|
|
934
|
+
@property
|
|
935
|
+
def last_connect_error(self) -> str | None:
|
|
936
|
+
"""Reason the most recent ``connect()`` attempt failed, or ``None``.
|
|
937
|
+
|
|
938
|
+
Captured from the underlying exception (e.g. an auth timeout, a
|
|
939
|
+
handshake HTTP/TLS error, or "Did not receive auth_required") so
|
|
940
|
+
callers can surface *why* the connection failed instead of an
|
|
941
|
+
opaque "Failed to connect to Home Assistant WebSocket".
|
|
942
|
+
"""
|
|
943
|
+
return self._last_connect_error
|
|
944
|
+
|
|
928
945
|
|
|
929
946
|
MAX_POOL_SIZE = 50
|
|
930
947
|
|
|
@@ -1059,7 +1076,14 @@ class WebSocketManager:
|
|
|
1059
1076
|
|
|
1060
1077
|
connected = await client.connect()
|
|
1061
1078
|
if not connected:
|
|
1062
|
-
|
|
1079
|
+
reason = client.last_connect_error
|
|
1080
|
+
# Append only an actual string reason; the isinstance guard
|
|
1081
|
+
# keeps a non-str (e.g. a MagicMock in tests) from polluting
|
|
1082
|
+
# the message with a repr.
|
|
1083
|
+
detail = f": {reason}" if isinstance(reason, str) else ""
|
|
1084
|
+
raise Exception(
|
|
1085
|
+
"Failed to connect to Home Assistant WebSocket" + detail
|
|
1086
|
+
)
|
|
1063
1087
|
|
|
1064
1088
|
self._clients[key] = client
|
|
1065
1089
|
self._last_used[key] = time.monotonic()
|
|
@@ -602,6 +602,10 @@ _SETTINGS_HTML = (
|
|
|
602
602
|
<meta charset="UTF-8">
|
|
603
603
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
604
604
|
<title>HA-MCP Tool Settings</title>
|
|
605
|
+
<!-- Empty data URI: tells the browser "no favicon" so it never requests
|
|
606
|
+
/favicon.ico (which would 404 and log a console error, since the
|
|
607
|
+
settings server serves no such asset in any deployment mode). -->
|
|
608
|
+
<link rel="icon" href="data:,">
|
|
605
609
|
<style>
|
|
606
610
|
:root {
|
|
607
611
|
--bg: #1c1c1e; --surface: #2c2c2e; --surface-hover: #3a3a3c;
|
|
@@ -2548,9 +2552,36 @@ function renderCodeModeSubRows(parentEl, masterOn, codeModeOn) {
|
|
|
2548
2552
|
|
|
2549
2553
|
const info = document.createElement('div');
|
|
2550
2554
|
info.className = 'feature-info';
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2555
|
+
// code_mode_saved_tools_path needs honest, field-specific copy:
|
|
2556
|
+
// - add-on mode: hardcoded by start.py (setdefault to /data); not
|
|
2557
|
+
// Supervisor-managed and absent from the addon schema, so it
|
|
2558
|
+
// genuinely can't be changed — don't imply a lever exists.
|
|
2559
|
+
// - standalone with the env var set: the "unset it" hint IS
|
|
2560
|
+
// actionable (the operator controls the env var), so keep it.
|
|
2561
|
+
// - standalone with no path: a blank path disables persistence —
|
|
2562
|
+
// warn that saved tools live in memory only.
|
|
2563
|
+
// Other env-locked code-mode sub-fields keep the shared helper.
|
|
2564
|
+
let lockedNote = '';
|
|
2565
|
+
if (f.field === 'code_mode_saved_tools_path') {
|
|
2566
|
+
if (IS_ADDON_MODE) {
|
|
2567
|
+
lockedNote =
|
|
2568
|
+
'<div class="feature-locked-note">Hardcoded to ' +
|
|
2569
|
+
'<code>/data/saved_tools.json</code> in add-on mode and cannot ' +
|
|
2570
|
+
'be changed (fixed here so saved tools survive add-on updates).' +
|
|
2571
|
+
'</div>';
|
|
2572
|
+
} else if (f.origin === 'env') {
|
|
2573
|
+
lockedNote =
|
|
2574
|
+
`<div class="feature-locked-note">${envLockedNoteHtml(f.env_var, f.field)}</div>`;
|
|
2575
|
+
} else if (!f.value) {
|
|
2576
|
+
lockedNote =
|
|
2577
|
+
'<div class="feature-locked-note">If blank, custom tools are kept ' +
|
|
2578
|
+
'in memory only and lost on restart. Set a path on persistent ' +
|
|
2579
|
+
'storage to keep them.</div>';
|
|
2580
|
+
}
|
|
2581
|
+
} else if (f.origin === 'env') {
|
|
2582
|
+
lockedNote =
|
|
2583
|
+
`<div class="feature-locked-note">${envLockedNoteHtml(f.env_var, f.field)}</div>`;
|
|
2584
|
+
}
|
|
2554
2585
|
info.innerHTML =
|
|
2555
2586
|
`<div class="feature-name">${escapeHtml(meta.label)}</div>` +
|
|
2556
2587
|
`<div class="feature-help">${escapeHtml(meta.help)}</div>` +
|
|
@@ -5373,16 +5404,34 @@ def build_settings_handlers(
|
|
|
5373
5404
|
if is_addon_synced:
|
|
5374
5405
|
addon_writes_present = True
|
|
5375
5406
|
elif os.environ.get(env_name) is not None:
|
|
5407
|
+
# Add-on mode has no env-var surface for non-schema keys
|
|
5408
|
+
# (e.g. CODE_MODE_SAVED_TOOLS_PATH, set by start.py), so
|
|
5409
|
+
# "unset it to edit here" is unactionable there — give
|
|
5410
|
+
# add-on-aware copy instead of implying a lever exists.
|
|
5411
|
+
if addon_mode:
|
|
5412
|
+
message = (
|
|
5413
|
+
f"{fname!r} is fixed by the add-on runtime and "
|
|
5414
|
+
"cannot be changed from the web UI."
|
|
5415
|
+
)
|
|
5416
|
+
suggestions = [
|
|
5417
|
+
"This value is baked into the add-on and is not "
|
|
5418
|
+
"exposed as an editable setting.",
|
|
5419
|
+
]
|
|
5420
|
+
else:
|
|
5421
|
+
message = (
|
|
5422
|
+
f"{fname!r} is set via {env_name} env var — "
|
|
5423
|
+
"unset it to edit here."
|
|
5424
|
+
)
|
|
5425
|
+
suggestions = [
|
|
5426
|
+
f"Unset the {env_name} environment variable (or "
|
|
5427
|
+
"remove it from your Docker config), then restart "
|
|
5428
|
+
"to edit this setting from the UI.",
|
|
5429
|
+
]
|
|
5376
5430
|
return JSONResponse(
|
|
5377
5431
|
create_error_response(
|
|
5378
5432
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
suggestions=[
|
|
5382
|
-
f"Unset the {env_name} environment variable (or "
|
|
5383
|
-
"remove it from your addon/Docker config), then "
|
|
5384
|
-
"restart to edit this setting from the UI.",
|
|
5385
|
-
],
|
|
5433
|
+
message,
|
|
5434
|
+
suggestions=suggestions,
|
|
5386
5435
|
context={"env_var": env_name},
|
|
5387
5436
|
),
|
|
5388
5437
|
status_code=409,
|
|
@@ -182,14 +182,19 @@ async def get_connected_ws_client(
|
|
|
182
182
|
ws_client = HomeAssistantWebSocketClient(base_url, token, verify_ssl=verify_ssl)
|
|
183
183
|
connected = await ws_client.connect()
|
|
184
184
|
if not connected:
|
|
185
|
+
reason = ws_client.last_connect_error
|
|
186
|
+
details = (
|
|
187
|
+
reason
|
|
188
|
+
if isinstance(reason, str)
|
|
189
|
+
else "WebSocket connection could not be established"
|
|
190
|
+
)
|
|
185
191
|
return None, create_connection_error(
|
|
186
192
|
"Failed to connect to Home Assistant WebSocket",
|
|
187
|
-
details=
|
|
193
|
+
details=details,
|
|
188
194
|
)
|
|
189
195
|
return ws_client, None
|
|
190
196
|
|
|
191
197
|
|
|
192
|
-
|
|
193
198
|
def _classify_api_status(
|
|
194
199
|
error: HomeAssistantAPIError,
|
|
195
200
|
error_msg: str,
|
|
@@ -202,13 +207,17 @@ def _classify_api_status(
|
|
|
202
207
|
if entity_id:
|
|
203
208
|
result = create_entity_not_found_error(entity_id, details=error_msg)
|
|
204
209
|
else:
|
|
205
|
-
result = create_error_response(
|
|
210
|
+
result = create_error_response(
|
|
211
|
+
ErrorCode.RESOURCE_NOT_FOUND, error_msg, context=context
|
|
212
|
+
)
|
|
206
213
|
case 401 | 403:
|
|
207
214
|
result = create_auth_error(error_msg, context=context)
|
|
208
215
|
case 400:
|
|
209
216
|
result = create_validation_error(error_msg, context=context)
|
|
210
217
|
case _:
|
|
211
|
-
result = create_error_response(
|
|
218
|
+
result = create_error_response(
|
|
219
|
+
ErrorCode.SERVICE_CALL_FAILED, error_msg, context=context
|
|
220
|
+
)
|
|
212
221
|
return result
|
|
213
222
|
|
|
214
223
|
|
|
@@ -244,7 +253,9 @@ def _classify_exception(
|
|
|
244
253
|
case TimeoutError():
|
|
245
254
|
operation = context.get("operation", "request") if context else "request"
|
|
246
255
|
timeout_seconds = context.get("timeout_seconds", 30) if context else 30
|
|
247
|
-
result = create_timeout_error(
|
|
256
|
+
result = create_timeout_error(
|
|
257
|
+
operation, timeout_seconds, details=error_msg, context=context
|
|
258
|
+
)
|
|
248
259
|
case ValueError():
|
|
249
260
|
result = create_validation_error(error_msg, context=context)
|
|
250
261
|
|
|
@@ -276,7 +287,9 @@ def _classify_by_message(
|
|
|
276
287
|
"unknown type",
|
|
277
288
|
)
|
|
278
289
|
)
|
|
279
|
-
or re.search(
|
|
290
|
+
or re.search(
|
|
291
|
+
r"expected (?:a |str|int|bool|dict|list|float|type|one of)", error_str
|
|
292
|
+
)
|
|
280
293
|
):
|
|
281
294
|
# Supervisor schema validation: vol.Invalid message arriving as a
|
|
282
295
|
# HomeAssistantCommandError via HA Core's hassio WS bridge. The
|
|
@@ -298,20 +311,27 @@ def _classify_by_message(
|
|
|
298
311
|
if entity_id:
|
|
299
312
|
result = create_entity_not_found_error(entity_id, details=error_msg)
|
|
300
313
|
else:
|
|
301
|
-
result = create_error_response(
|
|
314
|
+
result = create_error_response(
|
|
315
|
+
ErrorCode.RESOURCE_NOT_FOUND, error_msg, context=context
|
|
316
|
+
)
|
|
302
317
|
elif "timeout" in error_str:
|
|
303
|
-
result = create_timeout_error(
|
|
318
|
+
result = create_timeout_error(
|
|
319
|
+
"operation", 30, details=error_msg, context=context
|
|
320
|
+
)
|
|
304
321
|
elif "connection" in error_str or "connect" in error_str:
|
|
305
322
|
result = create_connection_error(error_msg, context=context)
|
|
306
|
-
elif
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
323
|
+
elif (
|
|
324
|
+
any(
|
|
325
|
+
phrase in error_str
|
|
326
|
+
for phrase in (
|
|
327
|
+
"unauthorized",
|
|
328
|
+
"authentication",
|
|
329
|
+
"invalid token",
|
|
330
|
+
"access denied",
|
|
331
|
+
)
|
|
313
332
|
)
|
|
314
|
-
|
|
333
|
+
or "401" in error_str
|
|
334
|
+
):
|
|
315
335
|
result = create_auth_error(error_msg, context=context)
|
|
316
336
|
elif error_str.startswith("command failed:"):
|
|
317
337
|
# HomeAssistantCommandError fallback: WS ``success=False`` with a
|
|
@@ -319,10 +339,15 @@ def _classify_by_message(
|
|
|
319
339
|
# known failure mode (the WS command itself failed), not an
|
|
320
340
|
# unexpected internal error — route to SERVICE_CALL_FAILED,
|
|
321
341
|
# mirroring the 4xx fallback in _classify_api_status.
|
|
322
|
-
result = create_error_response(
|
|
342
|
+
result = create_error_response(
|
|
343
|
+
ErrorCode.SERVICE_CALL_FAILED, error_msg, context=context
|
|
344
|
+
)
|
|
323
345
|
else:
|
|
324
346
|
result = create_error_response(
|
|
325
|
-
ErrorCode.INTERNAL_ERROR,
|
|
347
|
+
ErrorCode.INTERNAL_ERROR,
|
|
348
|
+
"An unexpected error occurred",
|
|
349
|
+
details=error_msg,
|
|
350
|
+
context=context,
|
|
326
351
|
)
|
|
327
352
|
return result
|
|
328
353
|
|
|
@@ -418,7 +443,11 @@ def exception_to_structured_error(
|
|
|
418
443
|
):
|
|
419
444
|
logger.exception("Unclassified exception: %s", error_msg)
|
|
420
445
|
|
|
421
|
-
if
|
|
446
|
+
if (
|
|
447
|
+
suggestions
|
|
448
|
+
and "error" in error_response
|
|
449
|
+
and isinstance(error_response["error"], dict)
|
|
450
|
+
):
|
|
422
451
|
# Set both `suggestion` (singular, first item) and `suggestions`
|
|
423
452
|
# (plural, full list). create_error_response (errors.py) sets the
|
|
424
453
|
# singular key; existing tests for exception_to_structured_error
|
|
@@ -535,6 +564,4 @@ def register_tool_methods(mcp: Any, instance: Any) -> None:
|
|
|
535
564
|
mcp.add_tool(method)
|
|
536
565
|
count += 1
|
|
537
566
|
if count == 0:
|
|
538
|
-
logger.warning(
|
|
539
|
-
f"No @tool-decorated methods found on {type(instance).__name__}"
|
|
540
|
-
)
|
|
567
|
+
logger.warning(f"No @tool-decorated methods found on {type(instance).__name__}")
|
|
@@ -1290,5 +1290,16 @@ def register_code_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
1290
1290
|
"in-memory cache. Check operator logs for the "
|
|
1291
1291
|
"underlying I/O error."
|
|
1292
1292
|
)
|
|
1293
|
+
elif not settings.code_mode_saved_tools_path:
|
|
1294
|
+
# Blank path = persistence disabled: the in-memory save
|
|
1295
|
+
# succeeded but is lost on restart. Surface a warning so
|
|
1296
|
+
# the agent doesn't assume the entry is durable — mirrors
|
|
1297
|
+
# the write-failure branch above.
|
|
1298
|
+
response["data"]["save_warning"] = (
|
|
1299
|
+
f"save_as={save_as!r} is kept in memory only — "
|
|
1300
|
+
"code_mode_saved_tools_path is unset, so custom tools "
|
|
1301
|
+
"are not persisted and are lost on restart. Set a path "
|
|
1302
|
+
"on persistent storage to keep them."
|
|
1303
|
+
)
|
|
1293
1304
|
|
|
1294
1305
|
return response
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/resources/skills-vendor/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/best_practice_checker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/tools_voice_assistant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/tools/validation_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/transforms/categorized_search.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/transforms/lite_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp/utils/kill_signal_diagnostics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev628 → ha_mcp_dev-7.6.0.dev630}/src/ha_mcp_dev.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|