ha-mcp-dev 7.12.3.dev1859__tar.gz → 7.12.3.dev1863__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 (162) hide show
  1. {ha_mcp_dev-7.12.3.dev1859/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.3.dev1863}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/config.py +56 -6
  4. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/settings_ui/settings.js +6 -1
  5. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/smart_search/_config.py +11 -5
  6. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/smart_search/_deep.py +158 -81
  7. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/smart_search/_fetch.py +49 -8
  8. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/smart_search/_scenes.py +43 -5
  9. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  10. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/LICENSE +0 -0
  11. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/MANIFEST.in +0 -0
  12. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/README.md +0 -0
  13. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/setup.cfg +0 -0
  14. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/__init__.py +0 -0
  15. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/__main__.py +0 -0
  16. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/_pypi_marker +0 -0
  17. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/_version.py +0 -0
  18. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/auth/__init__.py +0 -0
  19. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/auth/consent_form.py +0 -0
  20. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/auth/provider.py +0 -0
  21. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/backup_manager.py +0 -0
  22. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/browser_landing.py +0 -0
  23. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/client/__init__.py +0 -0
  24. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/client/rest_client.py +0 -0
  25. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/client/supervisor_client.py +0 -0
  26. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/client/websocket_client.py +0 -0
  27. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/client/websocket_listener.py +0 -0
  28. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  29. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  30. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  31. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/errors.py +0 -0
  32. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/policy/__init__.py +0 -0
  33. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/policy/approval_queue.py +0 -0
  34. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/policy/evaluator.py +0 -0
  35. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/policy/handlers.py +0 -0
  36. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/policy/middleware.py +0 -0
  37. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/policy/model.py +0 -0
  38. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/policy/persistence.py +0 -0
  39. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/policy/value_sources.py +0 -0
  40. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/py.typed +0 -0
  41. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/read_only.py +0 -0
  42. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  43. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  44. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  45. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  46. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  47. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  48. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  49. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  50. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  51. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  52. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  53. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  54. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  55. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  56. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  57. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  58. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  59. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  60. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  61. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  62. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  63. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  64. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  65. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  66. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  67. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/server.py +0 -0
  68. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/settings_ui/__init__.py +0 -0
  69. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/settings_ui/settings.css +0 -0
  70. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/settings_ui/settings.html +0 -0
  71. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/smoke_test.py +0 -0
  72. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  73. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/__init__.py +0 -0
  74. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/auto_backup.py +0 -0
  75. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/backup.py +0 -0
  76. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  77. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  78. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/device_control.py +0 -0
  79. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/enhanced.py +0 -0
  80. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/helpers.py +0 -0
  81. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/radio/__init__.py +0 -0
  82. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/radio/base.py +0 -0
  83. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/radio/matter.py +0 -0
  84. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/radio/thread.py +0 -0
  85. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  86. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/radio/zwave.py +0 -0
  87. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/reference_validator.py +0 -0
  88. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/registry.py +0 -0
  89. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  90. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  91. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  92. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  93. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  94. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  95. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_addons.py +0 -0
  96. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_areas.py +0 -0
  97. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  98. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  99. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_calendar.py +0 -0
  100. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_camera.py +0 -0
  101. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_categories.py +0 -0
  102. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_code.py +0 -0
  103. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  104. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  105. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  106. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  107. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  108. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  109. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_dev.py +0 -0
  110. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_energy.py +0 -0
  111. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_entities.py +0 -0
  112. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  113. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_groups.py +0 -0
  114. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_hacs.py +0 -0
  115. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_history.py +0 -0
  116. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_integrations.py +0 -0
  117. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_labels.py +0 -0
  118. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  119. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_radio.py +0 -0
  120. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_registry.py +0 -0
  121. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_resources.py +0 -0
  122. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_search.py +0 -0
  123. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_service.py +0 -0
  124. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_services.py +0 -0
  125. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_system.py +0 -0
  126. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_themes.py +0 -0
  127. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_todo.py +0 -0
  128. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_traces.py +0 -0
  129. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_updates.py +0 -0
  130. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_utility.py +0 -0
  131. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  132. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  133. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/tools_zones.py +0 -0
  134. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/util_helpers.py +0 -0
  135. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/tools/validation_middleware.py +0 -0
  136. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/transforms/__init__.py +0 -0
  137. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/transforms/categorized_search.py +0 -0
  138. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  139. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/transport_security.py +0 -0
  140. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/update_check.py +0 -0
  141. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/utils/__init__.py +0 -0
  142. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/utils/config_hash.py +0 -0
  143. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/utils/data_paths.py +0 -0
  144. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/utils/domain_handlers.py +0 -0
  145. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  146. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  147. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/utils/operation_manager.py +0 -0
  148. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/utils/python_sandbox.py +0 -0
  149. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/utils/skill_loader.py +0 -0
  150. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/utils/usage_logger.py +0 -0
  151. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/visibility/__init__.py +0 -0
  152. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/visibility/model.py +0 -0
  153. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/visibility/persistence.py +0 -0
  154. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp/visibility/resolver.py +0 -0
  155. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  156. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  157. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  158. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  159. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  160. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/tests/__init__.py +0 -0
  161. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/tests/test_constants.py +0 -0
  162. {ha_mcp_dev-7.12.3.dev1859 → ha_mcp_dev-7.12.3.dev1863}/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.dev1859
3
+ Version: 7.12.3.dev1863
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.dev1859"
7
+ version = "7.12.3.dev1863"
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
@@ -83,6 +83,22 @@ class Settings(BaseSettings):
83
83
  20.0, alias="HAMCP_SCENE_CONFIG_TIME_BUDGET"
84
84
  )
85
85
 
86
+ # Per-request timeout and concurrency of the smart-search per-id
87
+ # config-fetch fallback (Attempt C). On HA servers that serve
88
+ # /config/<domain>/config/<id> serially, a full batch of concurrent
89
+ # requests queues behind one another and the tail of each batch can
90
+ # exceed the per-request timeout even though every request would
91
+ # succeed — lowering the batch size (toward 1) and/or raising the
92
+ # timeout lets such instances scan exhaustively (issue #1784). Same
93
+ # consumption model as the budgets above: import-time constants in
94
+ # tools/smart_search/_config.py, restart required.
95
+ individual_config_timeout: float = Field(
96
+ 5.0, alias="HAMCP_INDIVIDUAL_CONFIG_TIMEOUT"
97
+ )
98
+ individual_fetch_batch_size: int = Field(
99
+ 10, alias="HAMCP_INDIVIDUAL_FETCH_BATCH_SIZE"
100
+ )
101
+
86
102
  # Backup tool configuration
87
103
  backup_hint: str = Field("normal", alias="BACKUP_HINT")
88
104
 
@@ -350,21 +366,26 @@ class Settings(BaseSettings):
350
366
  "automation_config_time_budget",
351
367
  "script_config_time_budget",
352
368
  "scene_config_time_budget",
369
+ "individual_config_timeout",
370
+ "individual_fetch_batch_size",
353
371
  mode="before",
354
372
  )
355
373
  @classmethod
356
374
  def _lenient_time_budget(cls, v: object, info: ValidationInfo) -> object:
357
- """Coerce the three smart-search time budgets, falling back to the
358
- field default (with a warning) instead of crashing startup.
375
+ """Coerce the smart-search Attempt-C knobs (the three time budgets,
376
+ the per-request timeout, and the fetch batch size), falling back to
377
+ the field default (with a warning) instead of crashing startup.
359
378
 
360
379
  Preserves the parse-tolerance of the removed ``_env_float`` helper
361
380
  (empty / unparseable -> default) and additionally enforces the same
362
381
  ``_ADVANCED_SETTINGS_BOUNDS`` range as the override-file / UI-POST
363
382
  path, so the env-var path can't smuggle in an out-of-range or
364
- non-finite budget. A ``<= 0`` budget would silently disable the
365
- per-id config-fetch scan, and ``inf`` / ``nan`` would uncap it; the
366
- ``lo <= val <= hi`` test rejects all three (NaN comparisons are
367
- False), keeping the env and override-file paths consistent."""
383
+ non-finite value. A ``<= 0`` budget or timeout would silently
384
+ disable the per-id config-fetch scan, and ``inf`` / ``nan`` would
385
+ uncap it; the ``lo <= val <= hi`` test rejects all three (NaN
386
+ comparisons are False), keeping the env and override-file paths
387
+ consistent. Int fields (batch size) additionally reject fractional
388
+ values rather than truncating them."""
368
389
  field_name = info.field_name
369
390
  if field_name is None: # always set for field_validator; defensive
370
391
  return v
@@ -389,6 +410,17 @@ class Settings(BaseSettings):
389
410
  default,
390
411
  )
391
412
  return default
413
+ if isinstance(default, int) and not isinstance(default, bool):
414
+ if val != int(val):
415
+ logger.warning(
416
+ "Invalid value for %s=%r (must be a whole number); "
417
+ "using default %s",
418
+ field_name,
419
+ v,
420
+ default,
421
+ )
422
+ return default
423
+ return int(val)
392
424
  return val
393
425
 
394
426
  @property
@@ -759,6 +791,22 @@ ADVANCED_SETTINGS_FIELDS: tuple[AdvancedField, ...] = (
759
791
  "search",
760
792
  True,
761
793
  ),
794
+ # Attempt-C per-request timeout + batch size (#1784). Restart-required
795
+ # (same import-time consumption as the budgets above).
796
+ AdvancedField(
797
+ "individual_config_timeout",
798
+ "HAMCP_INDIVIDUAL_CONFIG_TIMEOUT",
799
+ float,
800
+ "search",
801
+ True,
802
+ ),
803
+ AdvancedField(
804
+ "individual_fetch_batch_size",
805
+ "HAMCP_INDIVIDUAL_FETCH_BATCH_SIZE",
806
+ int,
807
+ "search",
808
+ True,
809
+ ),
762
810
  # Operations.
763
811
  AdvancedField("backup_hint", "BACKUP_HINT", str, "operations", True),
764
812
  AdvancedField("enable_websocket", "ENABLE_WEBSOCKET", bool, "operations", True),
@@ -846,6 +894,8 @@ _ADVANCED_SETTINGS_BOUNDS: dict[str, tuple[float, float]] = {
846
894
  "automation_config_time_budget": (1.0, 600.0),
847
895
  "script_config_time_budget": (1.0, 600.0),
848
896
  "scene_config_time_budget": (1.0, 600.0),
897
+ "individual_config_timeout": (1.0, 600.0),
898
+ "individual_fetch_batch_size": (1, 100),
849
899
  "code_mode_max_duration": (1.0, 300.0),
850
900
  "code_mode_max_memory": (1_048_576, 268_435_456),
851
901
  "code_mode_max_recursion": (1, 10_000),
@@ -1553,7 +1553,7 @@ const FEATURE_META = {
1553
1553
  },
1554
1554
  enable_code_mode: {
1555
1555
  label: "Enable code-mode sandbox (beta)",
1556
- help: "Beta feature, disabled by default. Enables ha_manage_custom_tool, a sandboxed Python interpreter (pydantic-monty) that lets AI assistants write/run/save/delete custom tools when no built-in tool covers the request. Sandbox cannot touch the filesystem or arbitrary network, but CAN call any registered MCP tool, hit the HA REST API, or send HA WebSocket commands, effectively 'do whatever existing tools allow you to do, in any combination'. See docs/beta.md for known limitations. Requires restart to take effect.",
1556
+ help: "Beta feature, disabled by default. Enables ha_manage_custom_tool, a sandboxed Python interpreter (pydantic-monty) that lets AI assistants write/run/save/delete custom tools when no built-in tool covers the request. Sandbox cannot touch the filesystem or arbitrary network, but CAN call any registered MCP tool, hit the HA REST API, or send HA WebSocket commands, effectively 'do whatever existing tools allow you to do, in any combination'. Saved tools persist and are visible to any client that can connect to ha-mcp. See docs/beta.md for known limitations. Requires restart to take effect.",
1557
1557
  },
1558
1558
  enable_lite_docstrings: {
1559
1559
  label: "Enable lite tool docstrings (beta)",
@@ -2946,6 +2946,8 @@ const ADVANCED_FIELD_META = {
2946
2946
  automation_config_time_budget: { label: "Automation config time budget (s)", help: "Max seconds ha_search/ha_deep_search spends fetching automation configs before returning a partial result. Raise on instances with many automations. Range 1–600. Restart required." },
2947
2947
  script_config_time_budget: { label: "Script config time budget (s)", help: "Max seconds ha_search/ha_deep_search spends fetching script configs before returning a partial result. Range 1–600. Restart required." },
2948
2948
  scene_config_time_budget: { label: "Scene config time budget (s)", help: "Max seconds ha_search/ha_deep_search spends fetching scene configs before returning a partial result. Range 1–600. Restart required." },
2949
+ individual_config_timeout: { label: "Per-request config fetch timeout (s)", help: "Timeout for each individual automation/script/scene config fetch during deep search. On HA servers that serve config reads serially, raise this and/or lower the batch size so queued requests don't time out. Values above the HA request timeout (HA_TIMEOUT, default 30) have no extra effect — the HTTP client gives up first. Range 1–600. Restart required." },
2950
+ individual_fetch_batch_size: { label: "Config fetch batch size", help: "How many per-id config fetches deep search issues concurrently. Lower toward 1 on HA servers that serve config reads serially (symptom: 'timed out' partial-result warnings). Range 1–100. Restart required." },
2949
2951
  backup_hint: { label: "Backup-hint level", help: "Tunes how strongly the LLM is prompted to take a full-HA snapshot before risky writes." },
2950
2952
  dashboard_screenshot_engine_url: { label: "Dashboard screenshot engine URL", help: "Base URL of the screenshot engine (e.g. http://puppet:10000). Leave blank to auto-discover the Puppet App (add-on) via the Supervisor (HA OS / Supervised). Only used when the Dashboard Screenshot beta feature is enabled. Takes effect without a restart." },
2951
2953
  enable_websocket: { label: "Enable WebSocket", help: "WebSocket-based state monitoring. Disabling falls back to polling; many tools degrade. Restart required." },
@@ -2985,6 +2987,9 @@ const ADVANCED_RESTART_REQUIRED = new Set([
2985
2987
  // it is resolved live per capture, so it takes effect immediately.
2986
2988
  "automation_config_time_budget", "script_config_time_budget",
2987
2989
  "scene_config_time_budget",
2990
+ // The Attempt-C per-request timeout and batch size (#1784) share the
2991
+ // budgets' import-time consumption, so they need a restart too.
2992
+ "individual_config_timeout", "individual_fetch_batch_size",
2988
2993
  "code_mode_max_duration", "code_mode_max_memory",
2989
2994
  "code_mode_max_recursion", "code_mode_max_invocations",
2990
2995
  "code_mode_saved_tools_path",
@@ -13,23 +13,29 @@ DEFAULT_CONCURRENCY_LIMIT = 20
13
13
  # Bulk fetch timeouts (in seconds)
14
14
  BULK_REST_TIMEOUT = 5.0 # Timeout for bulk REST endpoint calls
15
15
  BULK_WEBSOCKET_TIMEOUT = 3.0 # Timeout for bulk WebSocket calls
16
- INDIVIDUAL_CONFIG_TIMEOUT = 5.0 # Timeout for individual config fetches
17
16
 
18
17
 
19
- # Time budgets for fallback individual fetching (in seconds). Sourced from
20
- # the resolved Settings (issue #1538) so the env var, the web Settings UI
18
+ # Attempt-C (per-id fallback) tuning knobs. Sourced from the resolved
19
+ # Settings (issues #1538 / #1784) so the env var, the web Settings UI
21
20
  # override file, and the field defaults all flow through one precedence path
22
21
  # — and so add-on users (who cannot set raw env vars) can tune them from the
23
22
  # Advanced panel. Read once at import as module-level constants; a change
24
23
  # takes effect on the next MCP-host restart (advanced settings already
25
24
  # carry a restart-required notice in the UI).
26
25
  _settings = get_global_settings()
26
+
27
+ # Time budgets for fallback individual fetching (in seconds).
27
28
  AUTOMATION_CONFIG_TIME_BUDGET = _settings.automation_config_time_budget
28
29
  SCRIPT_CONFIG_TIME_BUDGET = _settings.script_config_time_budget
29
30
  SCENE_CONFIG_TIME_BUDGET = _settings.scene_config_time_budget
30
31
 
31
- # Batch size for parallel individual config fetches (Attempt C fallback)
32
- INDIVIDUAL_FETCH_BATCH_SIZE = 10
32
+ # Per-request timeout (seconds) and batch size for the parallel individual
33
+ # config fetches. On HA servers that serve the per-id config endpoint
34
+ # serially, a batch's tail requests queue behind its head and can exceed
35
+ # the per-request timeout despite being perfectly healthy — tune batch
36
+ # size toward 1 and/or raise the timeout on such instances (issue #1784).
37
+ INDIVIDUAL_CONFIG_TIMEOUT = _settings.individual_config_timeout
38
+ INDIVIDUAL_FETCH_BATCH_SIZE = _settings.individual_fetch_batch_size
33
39
 
34
40
 
35
41
  def _simplify_states_summary(
@@ -18,8 +18,10 @@ from ._config import (
18
18
  BULK_REST_TIMEOUT,
19
19
  DEFAULT_CONCURRENCY_LIMIT,
20
20
  INDIVIDUAL_CONFIG_TIMEOUT,
21
+ INDIVIDUAL_FETCH_BATCH_SIZE,
21
22
  SCRIPT_CONFIG_TIME_BUDGET,
22
23
  )
24
+ from ._fetch import is_timeout_error
23
25
  from ._scenes import SceneSearchMixin
24
26
 
25
27
  logger = logging.getLogger(__name__)
@@ -111,6 +113,7 @@ class DeepSearchMixin(SceneSearchMixin):
111
113
  scene_stats: dict[str, Any] = {
112
114
  "failed": 0,
113
115
  "skipped": 0,
116
+ "timeout": 0,
114
117
  "integration_skipped": 0,
115
118
  "registry_failed": False,
116
119
  }
@@ -120,13 +123,16 @@ class DeepSearchMixin(SceneSearchMixin):
120
123
  # surface as ``partial: True`` so callers can detect "incomplete
121
124
  # zero" / "incomplete partial" results vs a true complete answer.
122
125
  # ``_skipped`` = budget exhausted before fetch; ``_failed`` =
123
- # fetch attempted but raised (caught at ``debug``-level).
126
+ # fetch attempted but raised (caught at ``debug``-level);
127
+ # ``_timeout`` = fetch exceeded the per-request timeout (#1784).
124
128
  automation_skipped = 0
125
129
  automation_failed = 0
126
130
  automation_yaml_skipped = 0
131
+ automation_timeout = 0
127
132
  script_skipped = 0
128
133
  script_failed = 0
129
134
  script_yaml_skipped = 0
135
+ script_timeout = 0
130
136
  helper_failed = 0
131
137
  dashboard_failed = 0
132
138
 
@@ -136,6 +142,7 @@ class DeepSearchMixin(SceneSearchMixin):
136
142
  automation_skipped,
137
143
  automation_failed,
138
144
  automation_yaml_skipped,
145
+ automation_timeout,
139
146
  ) = await self._deep_search_automations(
140
147
  all_entities,
141
148
  automation_unique_id_map,
@@ -157,6 +164,7 @@ class DeepSearchMixin(SceneSearchMixin):
157
164
  script_skipped,
158
165
  script_failed,
159
166
  script_yaml_skipped,
167
+ script_timeout,
160
168
  ) = await self._deep_search_scripts(
161
169
  all_entities,
162
170
  query_lower,
@@ -178,6 +186,7 @@ class DeepSearchMixin(SceneSearchMixin):
178
186
  scene_stats["skipped"],
179
187
  scene_stats["integration_skipped"],
180
188
  scene_stats["registry_failed"],
189
+ scene_stats["timeout"],
181
190
  ) = await self._deep_search_scenes(
182
191
  all_entities,
183
192
  query_lower,
@@ -233,9 +242,11 @@ class DeepSearchMixin(SceneSearchMixin):
233
242
  automation_skipped=automation_skipped,
234
243
  automation_failed=automation_failed,
235
244
  automation_yaml_skipped=automation_yaml_skipped,
245
+ automation_timeout=automation_timeout,
236
246
  script_skipped=script_skipped,
237
247
  script_failed=script_failed,
238
248
  script_yaml_skipped=script_yaml_skipped,
249
+ script_timeout=script_timeout,
239
250
  helper_failed=helper_failed,
240
251
  dashboard_failed=dashboard_failed,
241
252
  )
@@ -282,22 +293,26 @@ class DeepSearchMixin(SceneSearchMixin):
282
293
  exact_match: bool,
283
294
  *,
284
295
  config_time_budget: float | None = None,
285
- ) -> tuple[list[dict[str, Any]], int, int, int]:
296
+ ) -> tuple[list[dict[str, Any]], int, int, int, int]:
286
297
  """Deep-search automations: 3-tier config fetch (REST bulk -> WS bulk -> individual).
287
298
 
288
- Returns ``(matches, skipped_count, failed_count, yaml_skipped_count)``.
289
- ``skipped_count`` is non-zero only when bulk fetch fell back to the
290
- per-id Attempt-C path AND its wall-clock budget exhausted before all
291
- configs were fetched; ``failed_count`` is non-zero when individual
292
- config fetches raised a non-404 exception (caught at ``debug``-level);
293
- ``yaml_skipped_count`` is non-zero when individual config fetches
294
- returned 404, which is the documented HA behaviour for YAML-defined
295
- automations (the ``/config/automation/config/<id>`` REST endpoint
296
- only exposes UI-storage automations). All three surface as
297
- ``partial: True`` in the response so callers can distinguish a
298
- complete zero-result from an incomplete one — skipped/failed ids
299
- carry their score-without-config through the merge and fall below
300
- the match threshold silently otherwise.
299
+ Returns ``(matches, skipped_count, failed_count, yaml_skipped_count,
300
+ timeout_count)``. ``skipped_count`` is non-zero only when bulk fetch
301
+ fell back to the per-id Attempt-C path AND its wall-clock budget
302
+ exhausted before all configs were fetched; ``failed_count`` is
303
+ non-zero when individual config fetches raised a non-404, non-timeout
304
+ exception (caught at ``debug``-level); ``yaml_skipped_count`` is
305
+ non-zero when individual config fetches returned 404, which is the
306
+ documented HA behaviour for YAML-defined automations (the
307
+ ``/config/automation/config/<id>`` REST endpoint only exposes
308
+ UI-storage automations); ``timeout_count`` is non-zero when fetches
309
+ exceeded the per-request ``INDIVIDUAL_CONFIG_TIMEOUT`` — usually a
310
+ sign the HA server serves config reads serially and the concurrent
311
+ batch queued past the timeout, not that anything is broken (#1784).
312
+ All four surface as ``partial: True`` in the response so callers can
313
+ distinguish a complete zero-result from an incomplete one —
314
+ skipped/failed ids carry their score-without-config through the
315
+ merge and fall below the match threshold silently otherwise.
301
316
  """
302
317
  automation_entities = [
303
318
  e for e in all_entities if e.get("entity_id", "").startswith("automation.")
@@ -334,6 +349,9 @@ class DeepSearchMixin(SceneSearchMixin):
334
349
 
335
350
  # Attempt C: parallel individual REST calls with time budget (LAST RESORT)
336
351
  skipped_count = 0
352
+ failed_count = 0
353
+ yaml_skipped_count = 0
354
+ timeout_count = 0
337
355
  if not bulk_fetched:
338
356
  uids_to_fetch = [
339
357
  uid for _, _, uid, _ in scored if uid and uid not in configs
@@ -364,7 +382,27 @@ class DeepSearchMixin(SceneSearchMixin):
364
382
  f"Automation individual config fetch ({uid}) failed: {e}"
365
383
  )
366
384
  return (uid, None, "failed")
385
+ except TimeoutError:
386
+ # asyncio.wait_for hit INDIVIDUAL_CONFIG_TIMEOUT. Classify
387
+ # distinctly from "failed": on servers that serialize
388
+ # config reads, a batch's tail requests queue past the
389
+ # timeout while still perfectly healthy (#1784).
390
+ logger.debug(
391
+ f"Automation individual config fetch ({uid}) timed "
392
+ f"out after {INDIVIDUAL_CONFIG_TIMEOUT}s."
393
+ )
394
+ return (uid, None, "timeout")
367
395
  except Exception as e:
396
+ if is_timeout_error(e):
397
+ # The REST client's own httpx timeout (HA_TIMEOUT)
398
+ # fired first and arrived wrapped in a
399
+ # HomeAssistantConnectionError — still a timeout,
400
+ # not a failure. See is_timeout_error.
401
+ logger.debug(
402
+ f"Automation individual config fetch ({uid}) "
403
+ f"timed out (client-side HTTP timeout): {e}"
404
+ )
405
+ return (uid, None, "timeout")
368
406
  logger.debug(
369
407
  f"Automation individual config fetch ({uid}) failed: {e}"
370
408
  )
@@ -375,6 +413,7 @@ class DeepSearchMixin(SceneSearchMixin):
375
413
  failed_count,
376
414
  skipped_count,
377
415
  yaml_skipped_count,
416
+ timeout_count,
378
417
  ) = await self._individual_fetch_budgeted(
379
418
  uids_to_fetch,
380
419
  _fetch_automation_config,
@@ -385,9 +424,6 @@ class DeepSearchMixin(SceneSearchMixin):
385
424
  "automations",
386
425
  )
387
426
  configs.update(fetched_configs)
388
- else:
389
- failed_count = 0
390
- yaml_skipped_count = 0
391
427
 
392
428
  # Phase 3: Score with whatever configs we have
393
429
  matches = [
@@ -403,7 +439,7 @@ class DeepSearchMixin(SceneSearchMixin):
403
439
  scored, configs, query_lower, exact_match
404
440
  )
405
441
  ]
406
- return matches, skipped_count, failed_count, yaml_skipped_count
442
+ return matches, skipped_count, failed_count, yaml_skipped_count, timeout_count
407
443
 
408
444
  async def _deep_search_scripts(
409
445
  self,
@@ -412,13 +448,14 @@ class DeepSearchMixin(SceneSearchMixin):
412
448
  exact_match: bool,
413
449
  *,
414
450
  config_time_budget: float | None = None,
415
- ) -> tuple[list[dict[str, Any]], int, int, int]:
451
+ ) -> tuple[list[dict[str, Any]], int, int, int, int]:
416
452
  """Deep-search scripts: same 3-tier strategy as automations.
417
453
 
418
- Returns ``(matches, skipped_count, failed_count, yaml_skipped_count)``;
419
- semantics identical to ``_deep_search_automations`` — the 404 path
420
- catches YAML-defined scripts (which ``client.get_script_config``
421
- re-raises as ``HomeAssistantAPIError(status_code=404)``).
454
+ Returns ``(matches, skipped_count, failed_count, yaml_skipped_count,
455
+ timeout_count)``; semantics identical to ``_deep_search_automations``
456
+ — the 404 path catches YAML-defined scripts (which
457
+ ``client.get_script_config`` re-raises as
458
+ ``HomeAssistantAPIError(status_code=404)``).
422
459
  """
423
460
  script_entities = [
424
461
  e for e in all_entities if e.get("entity_id", "").startswith("script.")
@@ -451,6 +488,9 @@ class DeepSearchMixin(SceneSearchMixin):
451
488
 
452
489
  # Attempt C: parallel individual fetch with budget (see #879)
453
490
  skipped_count = 0
491
+ failed_count = 0
492
+ yaml_skipped_count = 0
493
+ timeout_count = 0
454
494
  if not bulk_fetched:
455
495
  sids_to_fetch = [
456
496
  sid for _, _, sid, _ in scored if sid and sid not in configs
@@ -478,7 +518,23 @@ class DeepSearchMixin(SceneSearchMixin):
478
518
  return (sid, None, "yaml_skipped")
479
519
  logger.debug(f"Script individual config fetch ({sid}) failed: {e}")
480
520
  return (sid, None, "failed")
521
+ except TimeoutError:
522
+ # See _fetch_automation_config: per-request timeout under
523
+ # batch concurrency, distinct from a real failure (#1784).
524
+ logger.debug(
525
+ f"Script individual config fetch ({sid}) timed out "
526
+ f"after {INDIVIDUAL_CONFIG_TIMEOUT}s."
527
+ )
528
+ return (sid, None, "timeout")
481
529
  except Exception as e:
530
+ if is_timeout_error(e):
531
+ # Client-side HTTP timeout arrived wrapped; still a
532
+ # timeout. See _fetch_automation_config.
533
+ logger.debug(
534
+ f"Script individual config fetch ({sid}) timed "
535
+ f"out (client-side HTTP timeout): {e}"
536
+ )
537
+ return (sid, None, "timeout")
482
538
  logger.debug(f"Script individual config fetch ({sid}) failed: {e}")
483
539
  return (sid, None, "failed")
484
540
 
@@ -487,6 +543,7 @@ class DeepSearchMixin(SceneSearchMixin):
487
543
  failed_count,
488
544
  skipped_count,
489
545
  yaml_skipped_count,
546
+ timeout_count,
490
547
  ) = await self._individual_fetch_budgeted(
491
548
  sids_to_fetch,
492
549
  _fetch_script_config,
@@ -497,9 +554,6 @@ class DeepSearchMixin(SceneSearchMixin):
497
554
  "scripts",
498
555
  )
499
556
  configs.update(fetched_configs)
500
- else:
501
- failed_count = 0
502
- yaml_skipped_count = 0
503
557
 
504
558
  # Phase 3: Score scripts
505
559
  matches = [
@@ -516,7 +570,7 @@ class DeepSearchMixin(SceneSearchMixin):
516
570
  scored, configs, query_lower, exact_match
517
571
  )
518
572
  ]
519
- return matches, skipped_count, failed_count, yaml_skipped_count
573
+ return matches, skipped_count, failed_count, yaml_skipped_count, timeout_count
520
574
 
521
575
  async def _search_helper_type(
522
576
  self,
@@ -796,6 +850,8 @@ class DeepSearchMixin(SceneSearchMixin):
796
850
  script_failed: int = 0,
797
851
  automation_yaml_skipped: int = 0,
798
852
  script_yaml_skipped: int = 0,
853
+ automation_timeout: int = 0,
854
+ script_timeout: int = 0,
799
855
  helper_failed: int = 0,
800
856
  dashboard_failed: int = 0,
801
857
  ) -> dict[str, Any]:
@@ -856,6 +912,8 @@ class DeepSearchMixin(SceneSearchMixin):
856
912
  script_failed=script_failed,
857
913
  automation_yaml_skipped=automation_yaml_skipped,
858
914
  script_yaml_skipped=script_yaml_skipped,
915
+ automation_timeout=automation_timeout,
916
+ script_timeout=script_timeout,
859
917
  helper_failed=helper_failed,
860
918
  dashboard_failed=dashboard_failed,
861
919
  )
@@ -871,18 +929,25 @@ class DeepSearchMixin(SceneSearchMixin):
871
929
  script_failed: int = 0,
872
930
  automation_yaml_skipped: int = 0,
873
931
  script_yaml_skipped: int = 0,
932
+ automation_timeout: int = 0,
933
+ script_timeout: int = 0,
874
934
  helper_failed: int = 0,
875
935
  dashboard_failed: int = 0,
876
936
  ) -> None:
877
937
  """Set ``partial: True`` when the deep-search per-type fetch path lost
878
938
  data — either the Attempt-C wall-clock budget exhausted
879
- (``*_skipped``), individual fetches raised non-404 exceptions
880
- (``*_failed``, caught at ``debug``-level so they would otherwise
881
- be silent), or individual fetches returned 404 because the entity
882
- is YAML-defined (``*_yaml_skipped``). ``helper_failed`` /
883
- ``dashboard_failed`` cover the helper- and dashboard-list/config
884
- backends, whose per-unit ``except`` blocks would otherwise swallow a
885
- backend outage to an empty list with no signal.
939
+ (``*_skipped``), individual fetches raised non-404 non-timeout
940
+ exceptions (``*_failed``, caught at ``debug``-level so they would
941
+ otherwise be silent), individual fetches returned 404 because the
942
+ entity is YAML-defined (``*_yaml_skipped``), or individual fetches
943
+ exceeded the per-request timeout (``*_timeout`` — on servers that
944
+ serialize config reads a concurrent batch's tail queues past the
945
+ timeout while perfectly healthy, so the wording points at the
946
+ batch-size/timeout knobs rather than at the entities, #1784).
947
+ ``helper_failed`` / ``dashboard_failed`` cover the helper- and
948
+ dashboard-list/config backends, whose per-unit ``except`` blocks
949
+ would otherwise swallow a backend outage to an empty list with no
950
+ signal.
886
951
 
887
952
  Mirrors ``_apply_scene_partial_flag`` 's "looks complete when it
888
953
  isn't" coverage onto the automation/script/helper/dashboard paths —
@@ -902,52 +967,64 @@ class DeepSearchMixin(SceneSearchMixin):
902
967
  and the new reasons are concatenated with ``" ; "``.
903
968
  """
904
969
  reasons: list[str] = []
905
- if automation_skipped:
906
- reasons.append(
907
- f"{automation_skipped} automation(s) not scanned (time budget "
908
- "exhausted) — their match status is unknown; this result is "
909
- "not exhaustive. Pass `config_time_budget=` on `ha_search` to "
910
- "raise the per-call limit (or, for the default, set "
911
- "HAMCP_AUTOMATION_CONFIG_TIME_BUDGET or the matching field in "
912
- "the web Settings UI's Advanced section)."
913
- )
914
- if automation_failed:
915
- reasons.append(
916
- f"{automation_failed} automation(s) not scanned (per-id fetch "
917
- "raised a non-404 error) — their match status is unknown; "
918
- "this result is not exhaustive."
919
- )
920
- if automation_yaml_skipped:
921
- reasons.append(
922
- f"{automation_yaml_skipped} automation(s) not scanned "
923
- "(per-id config endpoint returned 404 — these are likely "
924
- "YAML-defined automations that the /config/automation/config "
925
- "REST endpoint does not expose) — their match status is "
926
- "unknown; this result is not exhaustive."
927
- )
928
- if script_skipped:
929
- reasons.append(
930
- f"{script_skipped} script(s) not scanned (time budget "
931
- "exhausted) — their match status is unknown; this result is "
932
- "not exhaustive. Pass `config_time_budget=` on `ha_search` to "
933
- "raise the per-call limit (or, for the default, set "
934
- "HAMCP_SCRIPT_CONFIG_TIME_BUDGET or the matching field in "
935
- "the web Settings UI's Advanced section)."
936
- )
937
- if script_failed:
938
- reasons.append(
939
- f"{script_failed} script(s) not scanned (per-id fetch raised "
940
- "a non-404 error) — their match status is unknown; this "
941
- "result is not exhaustive."
942
- )
943
- if script_yaml_skipped:
944
- reasons.append(
945
- f"{script_yaml_skipped} script(s) not scanned (per-id config "
946
- "endpoint returned 404 — these are likely YAML-defined "
947
- "scripts that the /config/script/config REST endpoint does "
948
- "not expose) — their match status is unknown; this result "
949
- "is not exhaustive."
950
- )
970
+ # The automation and script fragments are symmetric (noun, per-id
971
+ # endpoint, budget env var); loop rather than duplicating the four
972
+ # per-class fragments per type.
973
+ for noun, endpoint, budget_env, skipped, failed, yaml_skipped, timeout in (
974
+ (
975
+ "automation",
976
+ "/config/automation/config",
977
+ "HAMCP_AUTOMATION_CONFIG_TIME_BUDGET",
978
+ automation_skipped,
979
+ automation_failed,
980
+ automation_yaml_skipped,
981
+ automation_timeout,
982
+ ),
983
+ (
984
+ "script",
985
+ "/config/script/config",
986
+ "HAMCP_SCRIPT_CONFIG_TIME_BUDGET",
987
+ script_skipped,
988
+ script_failed,
989
+ script_yaml_skipped,
990
+ script_timeout,
991
+ ),
992
+ ):
993
+ if skipped:
994
+ reasons.append(
995
+ f"{skipped} {noun}(s) not scanned (time budget "
996
+ "exhausted) — their match status is unknown; this result "
997
+ "is not exhaustive. Pass `config_time_budget=` on "
998
+ "`ha_search` to raise the per-call limit (or, for the "
999
+ f"default, set {budget_env} or the matching field in "
1000
+ "the web Settings UI's Advanced section)."
1001
+ )
1002
+ if failed:
1003
+ reasons.append(
1004
+ f"{failed} {noun}(s) not scanned (per-id fetch raised "
1005
+ "a non-404 error) — their match status is unknown; this "
1006
+ "result is not exhaustive."
1007
+ )
1008
+ if yaml_skipped:
1009
+ reasons.append(
1010
+ f"{yaml_skipped} {noun}(s) not scanned (per-id config "
1011
+ "endpoint returned 404 — these are likely YAML-defined "
1012
+ f"{noun}s that the {endpoint} REST endpoint does not "
1013
+ "expose) — their match status is unknown; this result "
1014
+ "is not exhaustive."
1015
+ )
1016
+ if timeout:
1017
+ reasons.append(
1018
+ f"{timeout} {noun}(s) not scanned (per-id fetch timed "
1019
+ f"out after {INDIVIDUAL_CONFIG_TIMEOUT}s while "
1020
+ f"{INDIVIDUAL_FETCH_BATCH_SIZE} fetches ran concurrently "
1021
+ "— this usually means the HA server serves config reads "
1022
+ f"serially, not that the {noun}s are broken) — their "
1023
+ "match status is unknown; this result is not exhaustive. "
1024
+ "Lower HAMCP_INDIVIDUAL_FETCH_BATCH_SIZE and/or raise "
1025
+ "HAMCP_INDIVIDUAL_CONFIG_TIMEOUT (or the matching fields "
1026
+ "in the web Settings UI's Advanced section)."
1027
+ )
951
1028
  if helper_failed:
952
1029
  reasons.append(
953
1030
  f"{helper_failed} helper backend(s) not scanned (per-type list, "