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.
Files changed (126) hide show
  1. {ha_mcp_dev-7.6.0.dev627/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.6.0.dev629}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/pyproject.toml +1 -2
  3. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/client/websocket_client.py +25 -1
  4. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/helpers.py +49 -22
  5. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_config_helpers.py +1962 -1771
  6. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  7. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/LICENSE +0 -0
  8. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/MANIFEST.in +0 -0
  9. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/README.md +0 -0
  10. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/setup.cfg +0 -0
  11. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/__init__.py +0 -0
  12. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/__main__.py +0 -0
  13. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/_pypi_marker +0 -0
  14. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/_version.py +0 -0
  15. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/auth/__init__.py +0 -0
  16. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/auth/consent_form.py +0 -0
  17. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/auth/provider.py +0 -0
  18. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/backup_manager.py +0 -0
  19. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/client/__init__.py +0 -0
  20. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/client/rest_client.py +0 -0
  21. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/client/supervisor_client.py +0 -0
  22. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/client/websocket_listener.py +0 -0
  23. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/config.py +0 -0
  24. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/errors.py +0 -0
  25. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/policy/__init__.py +0 -0
  26. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/policy/approval_queue.py +0 -0
  27. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/policy/evaluator.py +0 -0
  28. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/policy/handlers.py +0 -0
  29. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/policy/middleware.py +0 -0
  30. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/policy/model.py +0 -0
  31. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/policy/persistence.py +0 -0
  32. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/policy/value_sources.py +0 -0
  33. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/py.typed +0 -0
  34. {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
  35. {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
  36. {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
  37. {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
  38. {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
  39. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  40. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  41. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  42. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  43. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  44. {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
  45. {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
  46. {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
  47. {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
  48. {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
  49. {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
  50. {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
  51. {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
  52. {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
  53. {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
  54. {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
  55. {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
  56. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/server.py +0 -0
  57. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/settings_ui.py +0 -0
  58. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/smoke_test.py +0 -0
  59. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  60. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/__init__.py +0 -0
  61. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/auto_backup.py +0 -0
  62. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/backup.py +0 -0
  63. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  64. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/device_control.py +0 -0
  65. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/enhanced.py +0 -0
  66. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/reference_validator.py +0 -0
  67. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/registry.py +0 -0
  68. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/smart_search.py +0 -0
  69. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_addons.py +0 -0
  70. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_areas.py +0 -0
  71. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  72. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  73. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_calendar.py +0 -0
  74. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_camera.py +0 -0
  75. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_categories.py +0 -0
  76. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_code.py +0 -0
  77. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  78. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  79. {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
  80. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  81. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  82. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_energy.py +0 -0
  83. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_entities.py +0 -0
  84. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  85. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_groups.py +0 -0
  86. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_hacs.py +0 -0
  87. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_history.py +0 -0
  88. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_integrations.py +0 -0
  89. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_labels.py +0 -0
  90. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  91. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_registry.py +0 -0
  92. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_resources.py +0 -0
  93. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_search.py +0 -0
  94. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_service.py +0 -0
  95. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_services.py +0 -0
  96. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_system.py +0 -0
  97. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_todo.py +0 -0
  98. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_traces.py +0 -0
  99. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_updates.py +0 -0
  100. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_utility.py +0 -0
  101. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  102. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  103. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/tools_zones.py +0 -0
  104. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/util_helpers.py +0 -0
  105. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/tools/validation_middleware.py +0 -0
  106. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/transforms/__init__.py +0 -0
  107. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/transforms/categorized_search.py +0 -0
  108. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  109. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/__init__.py +0 -0
  110. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/config_hash.py +0 -0
  111. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/data_paths.py +0 -0
  112. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/domain_handlers.py +0 -0
  113. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  114. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  115. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/operation_manager.py +0 -0
  116. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/python_sandbox.py +0 -0
  117. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/skill_loader.py +0 -0
  118. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp/utils/usage_logger.py +0 -0
  119. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  120. {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
  121. {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
  122. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  123. {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
  124. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/tests/__init__.py +0 -0
  125. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/tests/test_constants.py +0 -0
  126. {ha_mcp_dev-7.6.0.dev627 → ha_mcp_dev-7.6.0.dev629}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.6.0.dev627
3
+ Version: 7.6.0.dev629
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -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.dev627"
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
- raise Exception("Failed to connect to Home Assistant WebSocket")
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="WebSocket connection could not be established",
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(ErrorCode.RESOURCE_NOT_FOUND, error_msg, context=context)
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(ErrorCode.SERVICE_CALL_FAILED, error_msg, context=context)
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(operation, timeout_seconds, details=error_msg, context=context)
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(r"expected (?:a |str|int|bool|dict|list|float|type|one of)", error_str)
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(ErrorCode.RESOURCE_NOT_FOUND, error_msg, context=context)
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("operation", 30, details=error_msg, context=context)
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 any(
307
- phrase in error_str
308
- for phrase in (
309
- "unauthorized",
310
- "authentication",
311
- "invalid token",
312
- "access denied",
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
- ) or "401" in error_str:
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(ErrorCode.SERVICE_CALL_FAILED, error_msg, context=context)
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, "An unexpected error occurred", details=error_msg, context=context
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 suggestions and "error" in error_response and isinstance(error_response["error"], dict):
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__}")