ha-mcp-dev 7.12.3.dev1877__tar.gz → 7.12.3.dev1879__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 (164) hide show
  1. {ha_mcp_dev-7.12.3.dev1877/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.3.dev1879}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/client/rest_client.py +12 -0
  4. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/client/websocket_client.py +24 -22
  5. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/server.py +16 -0
  6. ha_mcp_dev-7.12.3.dev1879/src/ha_mcp/tools/component_api.py +267 -0
  7. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/smart_search/_deep.py +127 -11
  8. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/smart_search/_entities.py +94 -33
  9. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/smart_search/_overview.py +40 -16
  10. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/smart_search/_scenes.py +21 -8
  11. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_config_automations.py +43 -20
  12. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_config_helpers.py +293 -6
  13. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_config_scenes.py +48 -26
  14. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_config_scripts.py +58 -34
  15. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_search.py +995 -190
  16. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/visibility/persistence.py +24 -2
  17. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/visibility/resolver.py +84 -0
  18. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  19. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
  20. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/LICENSE +0 -0
  21. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/MANIFEST.in +0 -0
  22. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/README.md +0 -0
  23. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/setup.cfg +0 -0
  24. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/__init__.py +0 -0
  25. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/__main__.py +0 -0
  26. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/_pypi_marker +0 -0
  27. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/_version.py +0 -0
  28. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/auth/__init__.py +0 -0
  29. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/auth/consent_form.py +0 -0
  30. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/auth/provider.py +0 -0
  31. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/backup_manager.py +0 -0
  32. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/browser_landing.py +0 -0
  33. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/client/__init__.py +0 -0
  34. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/client/supervisor_client.py +0 -0
  35. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/client/websocket_listener.py +0 -0
  36. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/config.py +0 -0
  37. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  38. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  39. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  40. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/errors.py +0 -0
  41. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/llm_exposure.py +0 -0
  42. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/policy/__init__.py +0 -0
  43. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/policy/approval_queue.py +0 -0
  44. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/policy/evaluator.py +0 -0
  45. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/policy/handlers.py +0 -0
  46. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/policy/middleware.py +0 -0
  47. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/policy/model.py +0 -0
  48. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/policy/persistence.py +0 -0
  49. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/policy/value_sources.py +0 -0
  50. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/py.typed +0 -0
  51. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/read_only.py +0 -0
  52. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  53. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  54. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  55. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  56. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  57. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  58. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  59. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  60. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  61. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  62. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  63. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  64. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  65. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  66. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  67. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  68. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  69. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  70. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  71. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  72. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  73. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  74. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  75. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  76. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  77. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/settings_ui/__init__.py +0 -0
  78. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/settings_ui/settings.css +0 -0
  79. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/settings_ui/settings.html +0 -0
  80. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/settings_ui/settings.js +0 -0
  81. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/smoke_test.py +0 -0
  82. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  83. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/__init__.py +0 -0
  84. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/auto_backup.py +0 -0
  85. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/backup.py +0 -0
  86. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  87. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  88. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/device_control.py +0 -0
  89. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/enhanced.py +0 -0
  90. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/helpers.py +0 -0
  91. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/radio/__init__.py +0 -0
  92. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/radio/base.py +0 -0
  93. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/radio/matter.py +0 -0
  94. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/radio/thread.py +0 -0
  95. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  96. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/radio/zwave.py +0 -0
  97. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/reference_validator.py +0 -0
  98. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/registry.py +0 -0
  99. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  100. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  101. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  102. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  103. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  104. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  105. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_addons.py +0 -0
  106. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_areas.py +0 -0
  107. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  108. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  109. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_calendar.py +0 -0
  110. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_camera.py +0 -0
  111. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_categories.py +0 -0
  112. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_code.py +0 -0
  113. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  114. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  115. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_dev.py +0 -0
  116. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_energy.py +0 -0
  117. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_entities.py +0 -0
  118. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  119. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_groups.py +0 -0
  120. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_hacs.py +0 -0
  121. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_history.py +0 -0
  122. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_integrations.py +0 -0
  123. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_labels.py +0 -0
  124. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  125. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_radio.py +0 -0
  126. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_registry.py +0 -0
  127. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_resources.py +0 -0
  128. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_service.py +0 -0
  129. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_services.py +0 -0
  130. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_system.py +0 -0
  131. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_themes.py +0 -0
  132. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_todo.py +0 -0
  133. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_traces.py +0 -0
  134. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_updates.py +0 -0
  135. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_utility.py +0 -0
  136. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  137. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  138. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/tools_zones.py +0 -0
  139. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/util_helpers.py +0 -0
  140. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/tools/validation_middleware.py +0 -0
  141. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/transforms/__init__.py +0 -0
  142. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/transforms/categorized_search.py +0 -0
  143. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  144. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/transport_security.py +0 -0
  145. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/update_check.py +0 -0
  146. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/utils/__init__.py +0 -0
  147. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/utils/config_hash.py +0 -0
  148. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/utils/data_paths.py +0 -0
  149. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/utils/domain_handlers.py +0 -0
  150. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  151. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  152. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/utils/operation_manager.py +0 -0
  153. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/utils/python_sandbox.py +0 -0
  154. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/utils/skill_loader.py +0 -0
  155. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/utils/usage_logger.py +0 -0
  156. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/visibility/__init__.py +0 -0
  157. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp/visibility/model.py +0 -0
  158. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  159. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  160. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  161. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  162. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/tests/__init__.py +0 -0
  163. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/tests/test_constants.py +0 -0
  164. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1879}/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.12.3.dev1877
3
+ Version: 7.12.3.dev1879
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.12.3.dev1877"
7
+ version = "7.12.3.dev1879"
8
8
  description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
9
9
  readme = "README.md"
10
10
  # Upper bound widened to <3.15 so the package installs on Home Assistant's
@@ -86,8 +86,20 @@ class HomeAssistantCommandError(HomeAssistantError):
86
86
  ``_classify_exception``'s match dispatch; classification then falls
87
87
  through to ``_classify_by_message`` for pattern matching on the
88
88
  error message.
89
+
90
+ ``code`` carries HA's structured WebSocket error code (the ``code`` field
91
+ of the response ``error`` object — e.g. ``"unknown_command"``,
92
+ ``"invalid_format"``) when available, so routing decisions can key off the
93
+ stable code rather than pattern-matching the human-readable message. It
94
+ defaults to ``None`` for exceptions raised without a code (older raise
95
+ sites, string error payloads, or direct construction in tests), which
96
+ keeps the single-positional-argument constructor backward-compatible.
89
97
  """
90
98
 
99
+ def __init__(self, message: str, code: str | None = None):
100
+ super().__init__(message)
101
+ self.code = code
102
+
91
103
 
92
104
  class HomeAssistantCommandTimeout(HomeAssistantError):
93
105
  """WebSocket ``send_command`` timed out waiting for HA's response.
@@ -40,6 +40,20 @@ logger = logging.getLogger(__name__)
40
40
  MAX_WS_MESSAGE_BYTES = 64 * 1024 * 1024
41
41
 
42
42
 
43
+ def _extract_ws_error(error: Any) -> tuple[str, str | None]:
44
+ """Split an HA WebSocket ``error`` payload into ``(message, code)``.
45
+
46
+ HA replies to a failed command with ``{"error": {"code": ..., "message":
47
+ ...}}``. Dict payloads yield both fields; a bare string (or other shape)
48
+ yields ``str(error)`` as the message and ``None`` for the code. The code is
49
+ threaded onto ``HomeAssistantCommandError`` so callers route on the stable
50
+ ``code`` (e.g. ``unknown_command``) instead of the message text.
51
+ """
52
+ if isinstance(error, dict):
53
+ return error.get("message", str(error)), error.get("code")
54
+ return str(error), None
55
+
56
+
43
57
  class WebSocketConnectionState:
44
58
  """Encapsulates mutable state used by the WebSocket client."""
45
59
 
@@ -632,13 +646,10 @@ class HomeAssistantWebSocketClient:
632
646
  # Process standard Home Assistant WebSocket response
633
647
  if response.get("type") == "result":
634
648
  if response.get("success") is False:
635
- error = response.get("error", {})
636
- error_msg = (
637
- error.get("message", str(error))
638
- if isinstance(error, dict)
639
- else str(error)
649
+ error_msg, error_code = _extract_ws_error(response.get("error", {}))
650
+ raise HomeAssistantCommandError(
651
+ f"Command failed: {error_msg}", error_code
640
652
  )
641
- raise HomeAssistantCommandError(f"Command failed: {error_msg}")
642
653
 
643
654
  # Return success response according to HA WebSocket format
644
655
  return {
@@ -715,13 +726,8 @@ class HomeAssistantWebSocketClient:
715
726
 
716
727
  if not result_response.get("success"):
717
728
  self.cancel_event_response(message_id)
718
- error = result_response.get("error", {})
719
- error_msg = (
720
- error.get("message", str(error))
721
- if isinstance(error, dict)
722
- else str(error)
723
- )
724
- raise HomeAssistantCommandError(f"Command failed: {error_msg}")
729
+ error_msg, error_code = _extract_ws_error(result_response.get("error", {}))
730
+ raise HomeAssistantCommandError(f"Command failed: {error_msg}", error_code)
725
731
 
726
732
  try:
727
733
  event_response = await asyncio.wait_for(event_future, timeout=wait_timeout)
@@ -787,11 +793,10 @@ class HomeAssistantWebSocketClient:
787
793
  if response.get("type") == "result" and response.get("success"):
788
794
  return message_id
789
795
 
790
- error = response.get("error", {})
791
- error_msg = (
792
- error.get("message", str(error)) if isinstance(error, dict) else str(error)
796
+ error_msg, error_code = _extract_ws_error(response.get("error", {}))
797
+ raise HomeAssistantCommandError(
798
+ f"subscribe_events failed: {error_msg}", error_code
793
799
  )
794
- raise HomeAssistantCommandError(f"subscribe_events failed: {error_msg}")
795
800
 
796
801
  async def unsubscribe_events(self, subscription_id: int) -> None:
797
802
  """Release a subscription previously returned by ``subscribe_events``.
@@ -885,12 +890,9 @@ class HomeAssistantWebSocketClient:
885
890
  return message_id, queue
886
891
 
887
892
  self._state.unregister_subscription_queue(message_id)
888
- error = response.get("error", {})
889
- error_msg = (
890
- error.get("message", str(error)) if isinstance(error, dict) else str(error)
891
- )
893
+ error_msg, error_code = _extract_ws_error(response.get("error", {}))
892
894
  raise HomeAssistantCommandError(
893
- f"subscribe_command({command_type!r}) failed: {error_msg}"
895
+ f"subscribe_command({command_type!r}) failed: {error_msg}", error_code
894
896
  )
895
897
 
896
898
  async def unsubscribe_command(
@@ -728,6 +728,22 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
728
728
  "For routing guidance and the full allowlist, see "
729
729
  "ha_get_skill_guide."
730
730
  ),
731
+ "ha_search": (
732
+ "Search Home Assistant for entities (by name, domain, or area) AND "
733
+ "inside automation/script/scene/helper/dashboard configs, in one "
734
+ "call. Returns tagged buckets — `entities` plus per-config-type "
735
+ "lists — paginated per-surface and combined "
736
+ "(`has_more`/`next_offset`).\n\n"
737
+ "Config-body search is skipped when domain/area/state filters signal "
738
+ "entity-only intent; a warning names the skip — pass `search_types` "
739
+ "to force it.\n\n"
740
+ "`partial: True` means results are NOT exhaustive: a surface failed "
741
+ "or the config branch lost data. Empty buckets with `partial: True` "
742
+ 'mean "search failed", not "no results" — see `partial_reason` '
743
+ "(also mirrored into `warnings`). Do not treat a partial response as "
744
+ "complete.\n\n"
745
+ "For parameters, schema, and examples, see ha_get_skill_guide."
746
+ ),
731
747
  }
732
748
 
733
749
  # Description overrides that REPLACE the original description for BM25.
@@ -0,0 +1,267 @@
1
+ """Capability gate for the ``ha_mcp_tools`` custom-component WebSocket API.
2
+
3
+ The custom component (``custom_components/ha_mcp_tools/``, manifest 1.1.0+)
4
+ registers in-process WebSocket commands under the ``ha_mcp_tools/`` namespace
5
+ that let the server answer read queries (search, config-get, overview, ...)
6
+ from HA's live registries in a single round-trip, instead of the multi-fetch
7
+ REST/WS pipelines the server uses today.
8
+
9
+ Because the component ships over HACS on its own release cadence, a given
10
+ install may be at any version — old (no WS surface at all), new (full surface),
11
+ or somewhere in between. This module negotiates that with a single cached
12
+ ``ha_mcp_tools/info`` probe per client:
13
+
14
+ - ``info`` enumerates the ``capabilities`` the running component actually
15
+ registered. The server checks ``component_supports(caps, "<capability>")``
16
+ before routing a tool through the component, so a capability that a released
17
+ component hasn't shipped yet simply never gets used — no version lockstep.
18
+ - If ``info`` itself is ``unknown_command``, the component predates the WS
19
+ surface entirely; caps are cached as ``None`` and every consumer falls back
20
+ to its legacy path.
21
+
22
+ This mirrors the caller-token cache in ``tools_filesystem.py``: weak-keyed by
23
+ client so multi-client / OAuth setups each negotiate independently and the
24
+ entry self-evicts when a client is garbage-collected.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import asyncio
30
+ import logging
31
+ import time
32
+ import weakref
33
+ from dataclasses import dataclass
34
+ from typing import Any
35
+
36
+ from ..client.rest_client import (
37
+ HomeAssistantCommandError,
38
+ HomeAssistantCommandTimeout,
39
+ HomeAssistantConnectionError,
40
+ )
41
+ from ..client.websocket_client import get_websocket_client
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+ # The WS command every 1.1.0+ component registers; probing it does double duty
46
+ # (existence check + capability enumeration — see module docstring).
47
+ INFO_COMMAND = "ha_mcp_tools/info"
48
+
49
+ # HA's structured error code for a command no handler is registered for. Routing
50
+ # keys off this (via the ``code`` attribute threaded onto
51
+ # ``HomeAssistantCommandError``) rather than matching the message text.
52
+ UNKNOWN_COMMAND_CODE = "unknown_command"
53
+
54
+ # The wire-format generation this server speaks. A probe advertising any other
55
+ # ``schema_version`` is treated as no-caps (see ``get_component_caps``): the
56
+ # routing gate can't trust command payloads shaped for a different generation.
57
+ SUPPORTED_SCHEMA_VERSION = 1
58
+
59
+ # Negative (None) caps entries expire after this many seconds of monotonic
60
+ # time, so a component installed / upgraded mid-session is re-probed and
61
+ # adopted instead of being pinned to "absent" for the whole process lifetime
62
+ # (the cache key is the process-lifetime REST client — see ``_CAPS_CACHE``).
63
+ # Positive entries never expire on this timer; they are dropped only by
64
+ # ``invalidate_caps`` (a supposedly-supported command coming back
65
+ # ``unknown_command``).
66
+ _NEGATIVE_CACHE_TTL_S = 300.0
67
+
68
+
69
+ def _monotonic() -> float:
70
+ """Monotonic clock read, isolated so tests can advance it deterministically."""
71
+ return time.monotonic()
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class ComponentCaps:
76
+ """Snapshot of what the running ``ha_mcp_tools`` component can serve.
77
+
78
+ ``capabilities`` is the routing gate — the set of ``ha_mcp_tools/*``
79
+ commands the component actually registered. ``schema_version`` guards the
80
+ wire-format generation of those commands (a consumer needing a reshaped
81
+ payload checks ``schema_version >= N``). ``component_version`` and
82
+ ``limits`` are advisory (display / body-size caps).
83
+ """
84
+
85
+ schema_version: int
86
+ component_version: str
87
+ capabilities: frozenset[str]
88
+ limits: dict[str, Any]
89
+
90
+
91
+ # Weak-keyed by client so the negotiated caps self-evict when the client is
92
+ # garbage-collected. The key is the process-lifetime REST client, NOT the WS
93
+ # connection: an HA restart drops the socket but reuses the same pooled client,
94
+ # so a positive entry persists for the whole process (dropped only by
95
+ # ``invalidate_caps`` on an ``unknown_command``). A ``None`` value is a cached
96
+ # *negative* ("probed, no usable WS surface"); absence means "not yet probed".
97
+ # Negatives carry an expiry in ``_NEGATIVE_CACHE_TS`` so a component installed /
98
+ # upgraded mid-session is eventually re-probed instead of pinned absent forever.
99
+ _CAPS_CACHE: weakref.WeakKeyDictionary[Any, ComponentCaps | None] = (
100
+ weakref.WeakKeyDictionary()
101
+ )
102
+ # Monotonic timestamp a negative entry was stored, keyed by the same client.
103
+ # Only populated for ``None`` values; positive entries never appear here.
104
+ _NEGATIVE_CACHE_TS: weakref.WeakKeyDictionary[Any, float] = weakref.WeakKeyDictionary()
105
+ _CAPS_LOCKS: weakref.WeakKeyDictionary[Any, asyncio.Lock] = weakref.WeakKeyDictionary()
106
+
107
+
108
+ def _get_caps_lock(client: Any) -> asyncio.Lock:
109
+ """Per-client lock so concurrent first-callers probe ``info`` exactly once."""
110
+ lock = _CAPS_LOCKS.get(client)
111
+ if lock is None:
112
+ lock = asyncio.Lock()
113
+ _CAPS_LOCKS[client] = lock
114
+ return lock
115
+
116
+
117
+ def _live_cache_entry(client: Any) -> tuple[bool, ComponentCaps | None]:
118
+ """Return ``(hit, caps)`` for a still-valid cache entry, else ``(False, None)``.
119
+
120
+ A positive entry is a hit for the process lifetime. A negative (``None``)
121
+ entry is a hit only within ``_NEGATIVE_CACHE_TTL_S`` of when it was stored
122
+ (monotonic clock); once that window lapses it reports a miss so the caller
123
+ re-probes and can adopt a component that appeared mid-session.
124
+ """
125
+ if client not in _CAPS_CACHE:
126
+ return False, None
127
+ cached = _CAPS_CACHE[client]
128
+ if cached is not None:
129
+ return True, cached
130
+ stored_at = _NEGATIVE_CACHE_TS.get(client)
131
+ if stored_at is not None and (_monotonic() - stored_at) < _NEGATIVE_CACHE_TTL_S:
132
+ return True, None
133
+ return False, None
134
+
135
+
136
+ def _store_caps(client: Any, caps: ComponentCaps | None) -> None:
137
+ """Cache ``caps`` for ``client``, stamping a negative with the current clock."""
138
+ _CAPS_CACHE[client] = caps
139
+ if caps is None:
140
+ _NEGATIVE_CACHE_TS[client] = _monotonic()
141
+ else:
142
+ _NEGATIVE_CACHE_TS.pop(client, None)
143
+
144
+
145
+ def _parse_caps(response: Any) -> ComponentCaps | None:
146
+ """Map an ``ha_mcp_tools/info`` response into ``ComponentCaps``.
147
+
148
+ Returns ``None`` for a malformed payload — the command responded, so this
149
+ is still a stable negative worth caching, just not a usable capability set.
150
+ """
151
+ result = response.get("result") if isinstance(response, dict) else None
152
+ if not isinstance(result, dict):
153
+ return None
154
+ raw_caps = result.get("capabilities")
155
+ capabilities = (
156
+ frozenset(c for c in raw_caps if isinstance(c, str))
157
+ if isinstance(raw_caps, list)
158
+ else frozenset()
159
+ )
160
+ raw_limits = result.get("limits")
161
+ try:
162
+ schema_version = int(result.get("schema_version", 0) or 0)
163
+ except (TypeError, ValueError):
164
+ schema_version = 0
165
+ return ComponentCaps(
166
+ schema_version=schema_version,
167
+ component_version=str(result.get("component_version", "")),
168
+ capabilities=capabilities,
169
+ limits=raw_limits if isinstance(raw_limits, dict) else {},
170
+ )
171
+
172
+
173
+ async def get_component_caps(client: Any) -> ComponentCaps | None:
174
+ """Return the cached (or freshly probed) capabilities of the component.
175
+
176
+ One ``ha_mcp_tools/info`` probe per client, cached. ``None`` means "no
177
+ usable component WS surface" — the caller falls back to its legacy path.
178
+
179
+ Cache-on-failure semantics follow the error taxonomy:
180
+
181
+ - ``HomeAssistantCommandError`` (``info`` is ``unknown_command`` on an old
182
+ component, or the handler raised): cache ``None`` with an expiry. The
183
+ negative is re-probed after ``_NEGATIVE_CACHE_TTL_S`` so a component
184
+ installed / upgraded mid-session (the REST client — the cache key — is not
185
+ recreated on an HA restart) is eventually adopted instead of pinned absent.
186
+ - ``HomeAssistantConnectionError`` / ``HomeAssistantCommandTimeout`` (WS
187
+ down or slow): do **not** cache — re-probe on the next call once the
188
+ connection recovers. The consuming tool's legacy path will surface the
189
+ transport failure on its own.
190
+ - No credentials on the client (a bare test double with no ``base_url`` /
191
+ ``token``): nothing to probe; return ``None`` without caching.
192
+ - Any other unexpected exception: logged at debug, returns ``None`` without
193
+ caching (a transient runtime fault re-probes on the next call).
194
+
195
+ A probe whose ``schema_version`` is not ``SUPPORTED_SCHEMA_VERSION`` is
196
+ treated as no-caps (cached negative, logged once): the server can't trust
197
+ command payloads shaped for a different wire-format generation.
198
+ """
199
+ hit, caps = _live_cache_entry(client)
200
+ if hit:
201
+ return caps
202
+
203
+ async with _get_caps_lock(client):
204
+ hit, caps = _live_cache_entry(client)
205
+ if hit:
206
+ return caps
207
+
208
+ base_url = getattr(client, "base_url", None)
209
+ token = getattr(client, "token", None)
210
+ if not base_url or not token:
211
+ # Not a credentialed HA client — no WS connection to negotiate over.
212
+ return None
213
+
214
+ try:
215
+ ws = await get_websocket_client(url=base_url, token=token)
216
+ response = await ws.send_command(INFO_COMMAND)
217
+ except HomeAssistantCommandError:
218
+ # Old component (unknown_command) or an info handler bug: the
219
+ # component can't serve WS commands. Cache the negative.
220
+ _store_caps(client, None)
221
+ return None
222
+ except (HomeAssistantConnectionError, HomeAssistantCommandTimeout):
223
+ logger.debug(
224
+ "%s probe skipped: WS unavailable", INFO_COMMAND, exc_info=True
225
+ )
226
+ return None
227
+ except Exception:
228
+ logger.debug("%s probe failed unexpectedly", INFO_COMMAND, exc_info=True)
229
+ return None
230
+
231
+ caps = _parse_caps(response)
232
+ if caps is not None and caps.schema_version != SUPPORTED_SCHEMA_VERSION:
233
+ logger.warning(
234
+ "ha_mcp_tools component schema_version %s unsupported "
235
+ "(server supports %s); routing no commands through the component",
236
+ caps.schema_version,
237
+ SUPPORTED_SCHEMA_VERSION,
238
+ )
239
+ caps = None
240
+ _store_caps(client, caps)
241
+ return caps
242
+
243
+
244
+ def component_supports(caps: ComponentCaps | None, capability: str) -> bool:
245
+ """Return True when the component advertised ``capability``."""
246
+ return caps is not None and capability in caps.capabilities
247
+
248
+
249
+ def is_unknown_command(exc: Exception) -> bool:
250
+ """Return True when ``exc`` is HA's ``unknown_command`` rejection.
251
+
252
+ Keys off the structured ``code`` threaded onto ``HomeAssistantCommandError``
253
+ (never the message text), so a downgraded component that drops a command
254
+ routes cleanly to the legacy fallback.
255
+ """
256
+ return getattr(exc, "code", None) == UNKNOWN_COMMAND_CODE
257
+
258
+
259
+ def invalidate_caps(client: Any) -> None:
260
+ """Drop the cached caps so the next call re-probes ``ha_mcp_tools/info``.
261
+
262
+ Called when a command believed to be supported comes back
263
+ ``unknown_command`` (e.g. the component was downgraded mid-session), so the
264
+ stale positive doesn't keep routing to a dead command.
265
+ """
266
+ _CAPS_CACHE.pop(client, None)
267
+ _NEGATIVE_CACHE_TS.pop(client, None)
@@ -41,6 +41,9 @@ class DeepSearchMixin(SceneSearchMixin):
41
41
  exact_match: bool = True,
42
42
  config_time_budget: float | None = None,
43
43
  ctx: Context | None = None,
44
+ *,
45
+ prefetched_states: list[dict[str, Any]] | None = None,
46
+ prefetched_registry: Any = None,
44
47
  ) -> dict[str, Any]:
45
48
  """
46
49
  Deep search across automation, script, scene, helper, and dashboard
@@ -58,6 +61,17 @@ class DeepSearchMixin(SceneSearchMixin):
58
61
  include_config: Include full config in results (default: False)
59
62
  concurrency_limit: Max concurrent API calls for config fetching
60
63
  exact_match: Use exact substring matching (default: True). Set False for fuzzy.
64
+ prefetched_states: Pre-fetched ``get_states()`` list shared by the
65
+ ha_search orchestrator when both search branches run; ``None``
66
+ means fetch here.
67
+ prefetched_registry: Pre-fetched ``config/entity_registry/list``
68
+ response threaded to the scene registry walk *and* the helper
69
+ name-staleness map. ``None`` means the scene walk fetches it
70
+ itself; the helper branch degrades to storage-name matching
71
+ (it never fetches the registry on its own — see
72
+ ``_deep_search_helpers``). The orchestrator only hands one down
73
+ when both the entity and config-body branches run, so direct
74
+ ``deep_search``/``ha_deep_search`` callers get ``None`` here.
61
75
 
62
76
  Returns:
63
77
  Dictionary with search results grouped by type
@@ -87,8 +101,14 @@ class DeepSearchMixin(SceneSearchMixin):
87
101
  message="fetching entity states",
88
102
  )
89
103
 
90
- # Fetch all entities once at the beginning to avoid repeated calls
91
- all_entities = await self.client.get_states()
104
+ # Fetch all entities once at the beginning to avoid repeated calls.
105
+ # The ha_search orchestrator may hand us a snapshot it already fetched
106
+ # for the entity branch so the two branches share one /api/states.
107
+ all_entities = (
108
+ prefetched_states
109
+ if prefetched_states is not None
110
+ else await self.client.get_states()
111
+ )
92
112
  phase_done = 1
93
113
  await safe_progress(
94
114
  ctx,
@@ -192,6 +212,7 @@ class DeepSearchMixin(SceneSearchMixin):
192
212
  query_lower,
193
213
  exact_match,
194
214
  config_time_budget=config_time_budget,
215
+ prefetched_registry=prefetched_registry,
195
216
  )
196
217
  phase_done += 1
197
218
  await safe_progress(
@@ -206,7 +227,11 @@ class DeepSearchMixin(SceneSearchMixin):
206
227
  results["helpers"],
207
228
  helper_failed,
208
229
  ) = await self._deep_search_helpers(
209
- query_lower, exact_match, semaphore, include_config
230
+ query_lower,
231
+ exact_match,
232
+ semaphore,
233
+ include_config,
234
+ prefetched_registry=prefetched_registry,
210
235
  )
211
236
  phase_done += 1
212
237
  await safe_progress(
@@ -572,12 +597,56 @@ class DeepSearchMixin(SceneSearchMixin):
572
597
  ]
573
598
  return matches, skipped_count, failed_count, yaml_skipped_count, timeout_count
574
599
 
600
+ @staticmethod
601
+ def _build_helper_registry_map(
602
+ prefetched_registry: Any,
603
+ helper_types: set[str],
604
+ ) -> dict[str, tuple[str, str | None]]:
605
+ """Map each input_* helper's storage ``unique_id`` to current ``(entity_id, name)``.
606
+
607
+ Derived from the entity-registry snapshot the ha_search orchestrator
608
+ already fetched — never fetched here, so the helper branch adds no
609
+ request (see ``_deep_search_helpers``). A UI rename updates only the
610
+ entity registry, so this lets helper scoring match a helper's CURRENT
611
+ entity_id and name while the ``<type>/list`` storage record still
612
+ carries its creation-time slug/name (issue #1794). For a storage helper
613
+ the registry ``unique_id`` equals the ``<type>/list`` record ``id`` and
614
+ ``platform`` equals the helper domain (e.g. ``input_boolean``); the
615
+ current name is the registry ``name`` (user override) falling back to
616
+ ``original_name`` — left ``None`` when both are absent so the caller
617
+ keeps the storage name. Entries with no usable ``unique_id`` /
618
+ ``entity_id``, a non-input_* platform, or a non-dict shape are skipped.
619
+
620
+ Returns ``{}`` when no snapshot was handed down (direct ``deep_search``
621
+ callers) or the snapshot is a soft failure — the caller then degrades
622
+ to storage-name-only matching.
623
+ """
624
+ if not (
625
+ isinstance(prefetched_registry, dict) and prefetched_registry.get("success")
626
+ ):
627
+ return {}
628
+ out: dict[str, tuple[str, str | None]] = {}
629
+ for entry in prefetched_registry.get("result") or []:
630
+ if not isinstance(entry, dict):
631
+ continue
632
+ if entry.get("platform") not in helper_types:
633
+ continue
634
+ uid = entry.get("unique_id")
635
+ entity_id = entry.get("entity_id")
636
+ if not uid or not entity_id:
637
+ continue
638
+ current_name = entry.get("name") or entry.get("original_name")
639
+ out[uid] = (entity_id, current_name)
640
+ return out
641
+
575
642
  async def _search_helper_type(
576
643
  self,
577
644
  helper_type: str,
578
645
  query_lower: str,
579
646
  exact_match: bool,
580
647
  semaphore: asyncio.Semaphore,
648
+ *,
649
+ registry_by_uid: dict[str, tuple[str, str | None]] | None = None,
581
650
  ) -> tuple[list[dict[str, Any]], bool]:
582
651
  """Fetch one input_* helper type via WS list and return query matches.
583
652
 
@@ -589,7 +658,20 @@ class DeepSearchMixin(SceneSearchMixin):
589
658
  swallowed to an empty list: otherwise the caller cannot tell a real
590
659
  zero-match from a partial backend outage (helpers run on every
591
660
  default ``ha_search`` call).
661
+
662
+ ``registry_by_uid`` maps a helper's storage ``unique_id`` to its
663
+ CURRENT ``(entity_id, name)`` from the entity registry (built by
664
+ ``_deep_search_helpers`` from the orchestrator's registry snapshot). A
665
+ UI rename updates only the registry, so the ``<type>/list`` record
666
+ still carries the creation-time entity_id slug and name (issue #1794);
667
+ when the map has an entry we score and emit the current values so a
668
+ search for the renamed name/entity_id matches. The storage name is
669
+ still scored (and the storage body still searched below), so config
670
+ references and un-renamed helpers never regress. ``None`` (direct
671
+ ``deep_search`` callers with no snapshot) degrades to the storage
672
+ values — today's behaviour.
592
673
  """
674
+ registry_by_uid = registry_by_uid or {}
593
675
  async with semaphore:
594
676
  try:
595
677
  resp = await self.client.send_websocket_message(
@@ -605,18 +687,32 @@ class DeepSearchMixin(SceneSearchMixin):
605
687
  matches: list[dict[str, Any]] = []
606
688
  for helper in resp.get("result", []):
607
689
  helper_id = helper.get("id", "")
608
- entity_id = f"{helper_type}.{helper_id}"
609
- name = helper.get("name", helper_id)
610
-
611
- name_match_score = self.fuzzy_searcher._calculate_entity_score(
612
- entity_id, name, helper_type, query_lower
690
+ storage_name = helper.get("name", helper_id)
691
+ # Prefer the registry's CURRENT entity_id + name after a UI
692
+ # rename; fall back to the storage-derived values when this
693
+ # helper isn't in the snapshot (or no snapshot was handed
694
+ # down at all).
695
+ reg_entity_id, reg_name = registry_by_uid.get(
696
+ helper_id, (None, None)
697
+ )
698
+ entity_id = reg_entity_id or f"{helper_type}.{helper_id}"
699
+ display_name = reg_name or storage_name
700
+
701
+ # Score the query against every name the helper answers to —
702
+ # its current (registry) name and its storage name — so
703
+ # neither the rename nor the pre-rename value can hide it.
704
+ name_match_score = max(
705
+ self.fuzzy_searcher._calculate_entity_score(
706
+ entity_id, candidate, helper_type, query_lower
707
+ )
708
+ for candidate in {display_name, storage_name}
613
709
  )
614
710
  config_match_score = self._search_in_dict(
615
711
  helper, query_lower, exact_match
616
712
  )
617
713
  total_score, threshold, match_in_name = self._score_deep_match(
618
714
  entity_id,
619
- name,
715
+ display_name,
620
716
  name_match_score,
621
717
  config_match_score,
622
718
  query_lower,
@@ -628,7 +724,7 @@ class DeepSearchMixin(SceneSearchMixin):
628
724
  {
629
725
  "entity_id": entity_id,
630
726
  "helper_type": helper_type,
631
- "name": name,
727
+ "name": display_name,
632
728
  "score": total_score,
633
729
  "match_in_name": match_in_name,
634
730
  "match_in_config": config_match_score >= threshold,
@@ -646,6 +742,8 @@ class DeepSearchMixin(SceneSearchMixin):
646
742
  exact_match: bool,
647
743
  semaphore: asyncio.Semaphore,
648
744
  include_config: bool,
745
+ *,
746
+ prefetched_registry: Any = None,
649
747
  ) -> tuple[list[dict[str, Any]], int]:
650
748
  """Deep-search helpers: parallel input_* WS lists plus flow-based helpers.
651
749
 
@@ -662,6 +760,14 @@ class DeepSearchMixin(SceneSearchMixin):
662
760
  ``ha_search`` call, so silent failures here mean the caller cannot
663
761
  tell "no helpers match" from "helper backend partially down" —
664
762
  surfaced via ``partial: True``.
763
+
764
+ ``prefetched_registry`` is the orchestrator's already-fetched
765
+ ``config/entity_registry/list`` response, reused (never re-fetched
766
+ here — this path adds no request) to map each input_* helper's storage
767
+ ``unique_id`` to its CURRENT entity_id + name so a UI rename doesn't
768
+ hide the helper from a search for its new name (#1794). ``None`` on the
769
+ direct ``deep_search``/``ha_deep_search`` path yields an empty map and
770
+ storage-name-only matching.
665
771
  """
666
772
  helper_types = [
667
773
  "input_boolean",
@@ -672,11 +778,21 @@ class DeepSearchMixin(SceneSearchMixin):
672
778
  "input_button",
673
779
  ]
674
780
 
781
+ registry_by_uid = self._build_helper_registry_map(
782
+ prefetched_registry, set(helper_types)
783
+ )
784
+
675
785
  results: list[dict[str, Any]] = []
676
786
  failed_type_count = 0
677
787
  type_results = await asyncio.gather(
678
788
  *[
679
- self._search_helper_type(ht, query_lower, exact_match, semaphore)
789
+ self._search_helper_type(
790
+ ht,
791
+ query_lower,
792
+ exact_match,
793
+ semaphore,
794
+ registry_by_uid=registry_by_uid,
795
+ )
680
796
  for ht in helper_types
681
797
  ],
682
798
  return_exceptions=True,