ha-mcp-dev 7.6.0.dev627__tar.gz → 7.6.0.dev629__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.dev627/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.6.0.dev629}/PKG-INFO +1 -1
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/pyproject.toml +1 -2
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/client/websocket_client.py +25 -1
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/helpers.py +49 -22
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_config_helpers.py +1962 -1771
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/LICENSE +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/README.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/setup.cfg +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/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.dev629"
|
|
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"
|
|
@@ -147,7 +147,6 @@ ignore = [
|
|
|
147
147
|
# C901 ignores for tools files with complex methods (see #925).
|
|
148
148
|
# Remove lines as individual methods are simplified below threshold.
|
|
149
149
|
"src/ha_mcp/tools/tools_config_dashboards.py" = ["C901"]
|
|
150
|
-
"src/ha_mcp/tools/tools_config_helpers.py" = ["C901"]
|
|
151
150
|
"src/ha_mcp/tools/tools_entities.py" = ["C901"]
|
|
152
151
|
"src/ha_mcp/tools/tools_registry.py" = ["C901"]
|
|
153
152
|
"src/ha_mcp/tools/tools_search.py" = ["C901"]
|
|
@@ -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()
|
|
@@ -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__}")
|