ha-mcp-dev 7.4.1.dev465__tar.gz → 7.4.1.dev467__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 (109) hide show
  1. {ha_mcp_dev-7.4.1.dev465/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev467}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/_version.py +11 -0
  4. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/client/rest_client.py +15 -4
  5. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/settings_ui.py +2 -2
  6. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_bug_report.py +2 -1
  7. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_config_dashboards.py +59 -22
  8. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_utility.py +31 -1
  9. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  10. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/LICENSE +0 -0
  11. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/MANIFEST.in +0 -0
  12. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/README.md +0 -0
  13. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/setup.cfg +0 -0
  14. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/__init__.py +0 -0
  15. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/__main__.py +0 -0
  16. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/_pypi_marker +0 -0
  17. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/auth/__init__.py +0 -0
  18. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/auth/consent_form.py +0 -0
  19. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/auth/provider.py +0 -0
  20. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/client/__init__.py +0 -0
  21. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/client/websocket_client.py +0 -0
  22. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/client/websocket_listener.py +0 -0
  23. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/config.py +0 -0
  24. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/errors.py +0 -0
  25. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/py.typed +0 -0
  26. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  27. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  28. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  29. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  30. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  31. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  32. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  33. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  34. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  35. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  36. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  37. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  38. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  39. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  40. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  41. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  42. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  43. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  44. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  45. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  46. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/server.py +0 -0
  47. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/smoke_test.py +0 -0
  48. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/__init__.py +0 -0
  49. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/backup.py +0 -0
  50. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  51. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/device_control.py +0 -0
  52. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/enhanced.py +0 -0
  53. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/helpers.py +0 -0
  54. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/reference_validator.py +0 -0
  55. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/registry.py +0 -0
  56. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/smart_search.py +0 -0
  57. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_addons.py +0 -0
  58. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_areas.py +0 -0
  59. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  60. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_calendar.py +0 -0
  61. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_camera.py +0 -0
  62. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_categories.py +0 -0
  63. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_code.py +0 -0
  64. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  65. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  66. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  67. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  68. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  69. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_energy.py +0 -0
  70. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_entities.py +0 -0
  71. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  72. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_groups.py +0 -0
  73. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_hacs.py +0 -0
  74. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_history.py +0 -0
  75. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_integrations.py +0 -0
  76. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_labels.py +0 -0
  77. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  78. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_registry.py +0 -0
  79. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_resources.py +0 -0
  80. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_search.py +0 -0
  81. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_service.py +0 -0
  82. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_services.py +0 -0
  83. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_system.py +0 -0
  84. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_todo.py +0 -0
  85. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_traces.py +0 -0
  86. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_updates.py +0 -0
  87. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  88. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  89. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/tools_zones.py +0 -0
  90. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/tools/util_helpers.py +0 -0
  91. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/transforms/__init__.py +0 -0
  92. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/transforms/categorized_search.py +0 -0
  93. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/utils/__init__.py +0 -0
  94. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/utils/config_hash.py +0 -0
  95. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/utils/data_paths.py +0 -0
  96. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/utils/domain_handlers.py +0 -0
  97. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  98. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  99. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/utils/operation_manager.py +0 -0
  100. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/utils/python_sandbox.py +0 -0
  101. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp/utils/usage_logger.py +0 -0
  102. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  103. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  104. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  105. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  106. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  107. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/tests/__init__.py +0 -0
  108. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/tests/test_constants.py +0 -0
  109. {ha_mcp_dev-7.4.1.dev465 → ha_mcp_dev-7.4.1.dev467}/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.4.1.dev465
3
+ Version: 7.4.1.dev467
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.4.1.dev465"
7
+ version = "7.4.1.dev467"
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"
@@ -56,3 +56,14 @@ def is_running_in_addon() -> bool:
56
56
  users, who already see the dev/stable distinction in the HAOS add-on UI.
57
57
  """
58
58
  return bool(os.environ.get("SUPERVISOR_TOKEN"))
59
+
60
+
61
+ def get_supervisor_base_url() -> str:
62
+ """Return the base URL for direct Supervisor REST calls.
63
+
64
+ Defaults to ``http://supervisor`` (the in-addon Supervisor hostname). The
65
+ ``SUPERVISOR_BASE_URL`` env var override exists so E2E tests can point the
66
+ direct-Supervisor httpx call sites at a local mock without /etc/hosts or
67
+ DNS hacks. Production add-ons leave it unset.
68
+ """
69
+ return os.environ.get("SUPERVISOR_BASE_URL", "http://supervisor")
@@ -11,7 +11,7 @@ from typing import Any
11
11
 
12
12
  import httpx
13
13
 
14
- from .._version import is_running_in_addon
14
+ from .._version import get_supervisor_base_url, is_running_in_addon
15
15
  from ..config import get_global_settings
16
16
 
17
17
 
@@ -41,7 +41,19 @@ class HomeAssistantConnectionError(HomeAssistantError):
41
41
 
42
42
 
43
43
  class HomeAssistantAuthError(HomeAssistantError):
44
- """Authentication error with Home Assistant."""
44
+ """Authentication error with Home Assistant.
45
+
46
+ Sibling of ``HomeAssistantAPIError`` (not a subclass). The codebase has
47
+ 18 ``except HomeAssistantAPIError`` sites (util_helpers polling,
48
+ tools_integrations registry lookups, etc.) that deliberately rely on
49
+ auth errors NOT matching so they can propagate to a paired
50
+ ``except (HomeAssistantConnectionError, HomeAssistantAuthError): raise``
51
+ block. Subclassing AuthError under APIError silently swallowed those
52
+ auth errors as part of the local "this entity is not registered yet"
53
+ polling logic. Sites that specifically need to catch both must list
54
+ them explicitly (see ``_get_supervisor_log`` and
55
+ ``_get_system_service_log`` in ``tools_utility.py``).
56
+ """
45
57
 
46
58
 
47
59
  class HomeAssistantAPIError(HomeAssistantError):
@@ -543,7 +555,7 @@ class HomeAssistantClient:
543
555
  "(addon-mode gate fired but SUPERVISOR_TOKEN env var not set)"
544
556
  )
545
557
 
546
- url = f"http://supervisor/{path}/logs"
558
+ url = f"{get_supervisor_base_url()}/{path}/logs"
547
559
  logger.debug("Fetching %s via Supervisor direct", url)
548
560
 
549
561
  try:
@@ -1301,7 +1313,6 @@ class HomeAssistantClient:
1301
1313
  ) from e
1302
1314
  raise
1303
1315
 
1304
-
1305
1316
  async def resolve_scene_id(self, identifier: str) -> str:
1306
1317
  """
1307
1318
  Resolve a scene identifier to its storage key via the entity registry.
@@ -19,7 +19,7 @@ import httpx
19
19
  from starlette.requests import Request
20
20
  from starlette.responses import HTMLResponse, JSONResponse
21
21
 
22
- from ._version import is_running_in_addon
22
+ from ._version import get_supervisor_base_url, is_running_in_addon
23
23
  from .errors import ErrorCode, create_error_response
24
24
  from .transforms import DEFAULT_PINNED_TOOLS
25
25
  from .utils.data_paths import get_data_dir
@@ -910,7 +910,7 @@ def register_settings_routes(
910
910
  timeout=5.0, verify=server.settings.verify_ssl
911
911
  ) as client:
912
912
  resp = await client.post(
913
- "http://supervisor/addons/self/restart",
913
+ f"{get_supervisor_base_url()}/addons/self/restart",
914
914
  headers={"Authorization": f"Bearer {token}"},
915
915
  )
916
916
  except (httpx.ReadError, httpx.RemoteProtocolError):
@@ -20,6 +20,7 @@ from pydantic import Field
20
20
 
21
21
  from ha_mcp import __version__
22
22
 
23
+ from .._version import get_supervisor_base_url
23
24
  from ..config import Settings, get_global_settings
24
25
  from ..utils.usage_logger import (
25
26
  AVG_LOG_ENTRIES_PER_TOOL,
@@ -357,7 +358,7 @@ async def _fetch_addon_logs() -> str:
357
358
  timeout=10.0, verify=get_global_settings().verify_ssl
358
359
  ) as http_client:
359
360
  resp = await http_client.get(
360
- "http://supervisor/addons/self/logs",
361
+ f"{get_supervisor_base_url()}/addons/self/logs",
361
362
  headers={"Authorization": f"Bearer {token}"},
362
363
  )
363
364
  if resp.status_code != 200:
@@ -265,22 +265,37 @@ def _should_lazy_resolve(error_msg: str) -> bool:
265
265
  return _LAZY_RESOLVE_TRIGGER in error_msg
266
266
 
267
267
 
268
- async def _resolve_dashboard(client: Any, identifier: str) -> dict[str, str] | None:
268
+ async def _resolve_dashboard(
269
+ client: Any, identifier: str
270
+ ) -> tuple[dict[str, str] | None, list[dict[str, Any]] | None]:
269
271
  """Resolve a dashboard identifier (url_path or internal id) to both forms.
270
272
 
271
- Calls ``lovelace/dashboards/list`` and returns
272
- ``{"url_path": ..., "id": ...}`` when the identifier matches either field
273
- on a registry entry that has both fields populated; otherwise returns
274
- ``None``. Always pays the round-trip when called.
273
+ Calls ``lovelace/dashboards/list`` and returns a 2-tuple
274
+ ``(match, dashboards)``:
275
275
 
276
- Two call sites:
276
+ - ``match`` is ``{"url_path": ..., "id": ...}`` when the identifier
277
+ matches either field on a registry entry that has both fields
278
+ populated; otherwise ``None``.
279
+ - ``dashboards`` is the raw list as returned by HA when the
280
+ response shape is recognised (dict-with-``result`` or bare list);
281
+ ``None`` when the shape was unexpected and a warning was logged.
282
+
283
+ Returning ``dashboards`` alongside ``match`` lets callers reuse the
284
+ list for follow-on checks (existence, id lookup) instead of paying
285
+ a second ``lovelace/dashboards/list`` round-trip.
286
+
287
+ Three call sites:
277
288
  - **Lazy fallback** (``_lazy_resolve_and_retry``): only invoked after
278
289
  ``lovelace/config`` rejected the identifier with
279
290
  ``_LAZY_RESOLVE_TRIGGER`` — the round-trip is gated by the caller.
291
+ Discards ``dashboards``.
280
292
  - **Eager pre-resolve** (``ha_config_set_dashboard``): invoked before
281
293
  hyphen validation so callers may pass either form; gated on a
282
294
  cheap heuristic ("no hyphen, not 'lovelace'") rather than an error
283
- from HA.
295
+ from HA. Reuses ``dashboards`` for the existence-check below.
296
+ - **Delete** (``ha_config_delete_dashboard``): resolves either form
297
+ to the registry id before issuing the delete. Discards
298
+ ``dashboards``.
284
299
  """
285
300
  result = await client.send_websocket_message({"type": "lovelace/dashboards/list"})
286
301
  if isinstance(result, dict) and "result" in result:
@@ -297,7 +312,7 @@ async def _resolve_dashboard(client: Any, identifier: str) -> dict[str, str] | N
297
312
  "treating as no-match",
298
313
  type(result).__name__,
299
314
  )
300
- return None
315
+ return None, None
301
316
 
302
317
  for d in dashboards:
303
318
  if d.get("id") == identifier or d.get("url_path") == identifier:
@@ -309,8 +324,8 @@ async def _resolve_dashboard(client: Any, identifier: str) -> dict[str, str] | N
309
324
  # would be silently used by callers (e.g.
310
325
  # ``delete_dashboard`` would forward ``resolved_id=""``).
311
326
  continue
312
- return {"url_path": url_path, "id": entry_id}
313
- return None
327
+ return {"url_path": url_path, "id": entry_id}, dashboards
328
+ return None, dashboards
314
329
 
315
330
 
316
331
  @overload
@@ -376,7 +391,7 @@ async def _lazy_resolve_and_retry(
376
391
  return url_path, response
377
392
 
378
393
  try:
379
- resolved = await _resolve_dashboard(client, url_path)
394
+ resolved, _ = await _resolve_dashboard(client, url_path)
380
395
  except Exception as resolver_exc:
381
396
  # Resolver itself raised (timeout, network blip, etc.). Don't let
382
397
  # this exception escape and replace the original HA error with
@@ -941,12 +956,18 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
941
956
  # ``resolved_from`` on the success response so callers can
942
957
  # detect this redirect.
943
958
  pre_resolved_from: str | None = None
959
+ # When the pre-resolver fires and finds a match, ``_resolve_dashboard``
960
+ # has already fetched ``lovelace/dashboards/list``. Capture that list
961
+ # so the existence-check site below can reuse it instead of paying
962
+ # a second round-trip.
963
+ pre_fetched_dashboards: list[dict[str, Any]] | None = None
944
964
  if "-" not in url_path and url_path != "lovelace":
945
- resolved = await _resolve_dashboard(client, url_path)
965
+ resolved, dashboards = await _resolve_dashboard(client, url_path)
946
966
  if resolved is not None and resolved["url_path"]:
947
967
  original_url_path = url_path
948
968
  url_path = resolved["url_path"]
949
969
  pre_resolved_from = original_url_path
970
+ pre_fetched_dashboards = dashboards
950
971
  logger.info(
951
972
  "ha_config_set_dashboard pre-resolver mapped %r -> %r",
952
973
  original_url_path,
@@ -1129,16 +1150,32 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
1129
1150
  transform_result["resolved_from"] = pre_resolved_from
1130
1151
  return transform_result
1131
1152
 
1132
- # Check if dashboard exists
1133
- result = await client.send_websocket_message(
1134
- {"type": "lovelace/dashboards/list"}
1135
- )
1136
- if isinstance(result, dict) and "result" in result:
1137
- existing_dashboards = result["result"]
1138
- elif isinstance(result, list):
1139
- existing_dashboards = result
1153
+ # Check if dashboard exists. When the pre-resolver fired
1154
+ # and matched (internal-id branch), reuse its already-fetched
1155
+ # ``lovelace/dashboards/list`` response to skip a redundant
1156
+ # round-trip — the matched dashboard is guaranteed present in
1157
+ # that list.
1158
+ if pre_fetched_dashboards is not None:
1159
+ existing_dashboards = pre_fetched_dashboards
1140
1160
  else:
1141
- existing_dashboards = []
1161
+ result = await client.send_websocket_message(
1162
+ {"type": "lovelace/dashboards/list"}
1163
+ )
1164
+ if isinstance(result, dict) and "result" in result:
1165
+ existing_dashboards = result["result"]
1166
+ elif isinstance(result, list):
1167
+ existing_dashboards = result
1168
+ else:
1169
+ # Mirror the warning emitted by ``_resolve_dashboard`` on
1170
+ # the same response-shape failure, so a future HA shape
1171
+ # change shows up at every fetch site rather than going
1172
+ # silent on this one.
1173
+ logger.warning(
1174
+ "lovelace/dashboards/list returned an unexpected shape "
1175
+ "(type=%s); treating as no-match",
1176
+ type(result).__name__,
1177
+ )
1178
+ existing_dashboards = []
1142
1179
  dashboard_exists = any(
1143
1180
  d.get("url_path") == url_path for d in existing_dashboards
1144
1181
  )
@@ -1407,7 +1444,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
1407
1444
  Note: The default dashboard cannot be deleted via this method.
1408
1445
  """
1409
1446
  try:
1410
- resolved = await _resolve_dashboard(client, url_path)
1447
+ resolved, _ = await _resolve_dashboard(client, url_path)
1411
1448
  if resolved is None:
1412
1449
  raise_tool_error(
1413
1450
  create_resource_not_found_error(
@@ -12,7 +12,11 @@ from typing import Any, Literal
12
12
 
13
13
  from fastmcp.exceptions import ToolError
14
14
 
15
- from ..client.rest_client import HomeAssistantAPIError, HomeAssistantConnectionError
15
+ from ..client.rest_client import (
16
+ HomeAssistantAPIError,
17
+ HomeAssistantAuthError,
18
+ HomeAssistantConnectionError,
19
+ )
16
20
  from ..errors import ErrorCode, create_error_response
17
21
  from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
18
22
  from .util_helpers import (
@@ -755,6 +759,19 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
755
759
 
756
760
  except ToolError:
757
761
  raise
762
+ except HomeAssistantAuthError as e:
763
+ # Listed before HomeAssistantAPIError because AuthError is a sibling,
764
+ # not a subclass — without this explicit clause the 401 from
765
+ # _supervisor_logs_get propagates raw to FastMCP and surfaces
766
+ # without a structured `code` field.
767
+ exception_to_structured_error(
768
+ e,
769
+ context={"source": "supervisor", "slug": slug},
770
+ suggestions=[
771
+ "Verify SUPERVISOR_TOKEN is set correctly inside the add-on",
772
+ "Reinstall the add-on if the token may have rotated",
773
+ ],
774
+ )
758
775
  except HomeAssistantAPIError as e:
759
776
  status = getattr(e, "status_code", None)
760
777
  if status == 400:
@@ -857,6 +874,19 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
857
874
 
858
875
  except ToolError:
859
876
  raise
877
+ except HomeAssistantAuthError as e:
878
+ # Listed before HomeAssistantAPIError because AuthError is a sibling,
879
+ # not a subclass — without this explicit clause the 401 from
880
+ # _supervisor_logs_get propagates raw to FastMCP and surfaces
881
+ # without a structured `code` field.
882
+ exception_to_structured_error(
883
+ e,
884
+ context={"source": "system_service", "slug": service},
885
+ suggestions=[
886
+ "Verify SUPERVISOR_TOKEN is set correctly inside the add-on",
887
+ "Reinstall the add-on if the token may have rotated",
888
+ ],
889
+ )
860
890
  except HomeAssistantAPIError as e:
861
891
  status = getattr(e, "status_code", None)
862
892
  if status == 403:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.4.1.dev465
3
+ Version: 7.4.1.dev467
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