ha-mcp-dev 7.12.3.dev1877__tar.gz → 7.12.3.dev1882__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.dev1882}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/client/rest_client.py +12 -0
  4. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/client/websocket_client.py +24 -22
  5. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/policy/value_sources.py +17 -1
  6. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/server.py +16 -0
  7. ha_mcp_dev-7.12.3.dev1882/src/ha_mcp/tools/component_api.py +267 -0
  8. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/config_entry_flow.py +177 -38
  9. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/smart_search/_deep.py +127 -11
  10. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/smart_search/_entities.py +94 -33
  11. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/smart_search/_overview.py +40 -16
  12. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/smart_search/_scenes.py +21 -8
  13. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_config_automations.py +43 -20
  14. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_config_helpers.py +293 -6
  15. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_config_scenes.py +48 -26
  16. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_config_scripts.py +58 -34
  17. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_integrations.py +202 -48
  18. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_search.py +995 -190
  19. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/visibility/persistence.py +24 -2
  20. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/visibility/resolver.py +84 -0
  21. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  22. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
  23. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/LICENSE +0 -0
  24. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/MANIFEST.in +0 -0
  25. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/README.md +0 -0
  26. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/setup.cfg +0 -0
  27. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/__init__.py +0 -0
  28. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/__main__.py +0 -0
  29. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/_pypi_marker +0 -0
  30. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/_version.py +0 -0
  31. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/auth/__init__.py +0 -0
  32. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/auth/consent_form.py +0 -0
  33. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/auth/provider.py +0 -0
  34. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/backup_manager.py +0 -0
  35. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/browser_landing.py +0 -0
  36. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/client/__init__.py +0 -0
  37. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/client/supervisor_client.py +0 -0
  38. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/client/websocket_listener.py +0 -0
  39. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/config.py +0 -0
  40. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  41. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  42. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  43. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/errors.py +0 -0
  44. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/llm_exposure.py +0 -0
  45. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/policy/__init__.py +0 -0
  46. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/policy/approval_queue.py +0 -0
  47. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/policy/evaluator.py +0 -0
  48. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/policy/handlers.py +0 -0
  49. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/policy/middleware.py +0 -0
  50. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/policy/model.py +0 -0
  51. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/policy/persistence.py +0 -0
  52. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/py.typed +0 -0
  53. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/read_only.py +0 -0
  54. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  55. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  56. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  57. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  58. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  59. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  60. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  61. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  62. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  63. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  64. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  65. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  66. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  67. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  68. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  69. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  70. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  71. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  72. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  73. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  74. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  75. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  76. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  77. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  78. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  79. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/settings_ui/__init__.py +0 -0
  80. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/settings_ui/settings.css +0 -0
  81. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/settings_ui/settings.html +0 -0
  82. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/settings_ui/settings.js +0 -0
  83. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/smoke_test.py +0 -0
  84. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  85. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/__init__.py +0 -0
  86. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/auto_backup.py +0 -0
  87. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/backup.py +0 -0
  88. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  89. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/device_control.py +0 -0
  90. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/enhanced.py +0 -0
  91. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/helpers.py +0 -0
  92. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/radio/__init__.py +0 -0
  93. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/radio/base.py +0 -0
  94. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/radio/matter.py +0 -0
  95. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/radio/thread.py +0 -0
  96. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  97. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/radio/zwave.py +0 -0
  98. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/reference_validator.py +0 -0
  99. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/registry.py +0 -0
  100. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  101. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  102. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  103. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  104. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  105. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  106. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_addons.py +0 -0
  107. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_areas.py +0 -0
  108. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  109. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  110. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_calendar.py +0 -0
  111. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_camera.py +0 -0
  112. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_categories.py +0 -0
  113. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_code.py +0 -0
  114. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  115. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  116. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_dev.py +0 -0
  117. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_energy.py +0 -0
  118. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_entities.py +0 -0
  119. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  120. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_groups.py +0 -0
  121. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_hacs.py +0 -0
  122. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_history.py +0 -0
  123. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_labels.py +0 -0
  124. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  125. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_radio.py +0 -0
  126. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_registry.py +0 -0
  127. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_resources.py +0 -0
  128. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_service.py +0 -0
  129. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_services.py +0 -0
  130. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_system.py +0 -0
  131. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_themes.py +0 -0
  132. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_todo.py +0 -0
  133. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_traces.py +0 -0
  134. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_updates.py +0 -0
  135. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_utility.py +0 -0
  136. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  137. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  138. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/tools_zones.py +0 -0
  139. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/util_helpers.py +0 -0
  140. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/tools/validation_middleware.py +0 -0
  141. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/transforms/__init__.py +0 -0
  142. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/transforms/categorized_search.py +0 -0
  143. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  144. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/transport_security.py +0 -0
  145. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/update_check.py +0 -0
  146. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/utils/__init__.py +0 -0
  147. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/utils/config_hash.py +0 -0
  148. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/utils/data_paths.py +0 -0
  149. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/utils/domain_handlers.py +0 -0
  150. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  151. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  152. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/utils/operation_manager.py +0 -0
  153. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/utils/python_sandbox.py +0 -0
  154. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/utils/skill_loader.py +0 -0
  155. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/utils/usage_logger.py +0 -0
  156. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/visibility/__init__.py +0 -0
  157. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/src/ha_mcp/visibility/model.py +0 -0
  158. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/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.dev1882}/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.dev1882}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  161. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/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.dev1882}/tests/__init__.py +0 -0
  163. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/tests/test_constants.py +0 -0
  164. {ha_mcp_dev-7.12.3.dev1877 → ha_mcp_dev-7.12.3.dev1882}/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.dev1882
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.dev1882"
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(
@@ -40,7 +40,7 @@ VALUE_SOURCE_REGISTRY: dict[tuple[str, str], str] = {
40
40
  ("ha_get_history", "args.entity_ids"): "ha_entities",
41
41
  ("ha_remove_entity", "args.entity_id"): "ha_entities",
42
42
  ("ha_get_entity_exposure", "args.entity_id"): "ha_entities",
43
- ("ha_set_integration_enabled", "args.entity_id"): "ha_entities",
43
+ ("ha_set_integration", "args.entry_id"): "ha_config_entries",
44
44
  }
45
45
 
46
46
  # Fetched-value TTL cache so the UI can click through path options
@@ -171,8 +171,24 @@ async def _fetch_ha_entities(client: Any, params: dict[str, str]) -> list[str]:
171
171
  return out
172
172
 
173
173
 
174
+ async def _fetch_ha_config_entries(client: Any, _params: dict[str, str]) -> list[str]:
175
+ entries = await client._request("GET", "/config/config_entries/entry")
176
+ if not isinstance(entries, list):
177
+ logger.warning(
178
+ "ha_config_entries: config entry list returned unexpected shape %s",
179
+ type(entries).__name__,
180
+ )
181
+ return []
182
+ return [
183
+ e["entry_id"]
184
+ for e in entries
185
+ if isinstance(e, dict) and isinstance(e.get("entry_id"), str)
186
+ ]
187
+
188
+
174
189
  _FETCHERS: dict[str, Callable[[Any, dict[str, str]], Awaitable[list[str]]]] = {
175
190
  "ha_domains": _fetch_ha_domains,
176
191
  "ha_services": _fetch_ha_services,
177
192
  "ha_entities": _fetch_ha_entities,
193
+ "ha_config_entries": _fetch_ha_config_entries,
178
194
  }
@@ -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)