ha-mcp-dev 7.13.0.dev2029__tar.gz → 7.13.0.dev2033__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 (187) hide show
  1. {ha_mcp_dev-7.13.0.dev2029/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev2033}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/read_only.py +7 -4
  4. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/smart_search/_deep.py +97 -10
  5. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/smart_search/_fetch.py +89 -0
  6. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/smart_search/_scenes.py +33 -6
  7. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  8. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/LICENSE +0 -0
  9. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/MANIFEST.in +0 -0
  10. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/README.md +0 -0
  11. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/setup.cfg +0 -0
  12. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/__init__.py +0 -0
  13. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/__main__.py +0 -0
  14. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/_pypi_marker +0 -0
  15. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/_version.py +0 -0
  16. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/auth/__init__.py +0 -0
  17. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/auth/consent_form.py +0 -0
  18. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/auth/provider.py +0 -0
  19. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/backup_manager.py +0 -0
  20. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/browser_landing.py +0 -0
  21. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/client/__init__.py +0 -0
  22. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/client/rest_client.py +0 -0
  23. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/client/supervisor_client.py +0 -0
  24. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/client/websocket_client.py +0 -0
  25. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/client/websocket_listener.py +0 -0
  26. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/config.py +0 -0
  27. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  28. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  29. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  30. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  31. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  32. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  33. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/errors.py +0 -0
  34. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/llm_exposure.py +0 -0
  35. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/policy/__init__.py +0 -0
  36. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/policy/approval_queue.py +0 -0
  37. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/policy/evaluator.py +0 -0
  38. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/policy/handlers.py +0 -0
  39. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/policy/middleware.py +0 -0
  40. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/policy/model.py +0 -0
  41. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/policy/persistence.py +0 -0
  42. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/policy/value_sources.py +0 -0
  43. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/py.typed +0 -0
  44. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  45. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  46. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  47. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  48. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  49. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  50. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  51. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  52. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  53. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  54. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  55. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  56. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  57. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  58. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  59. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  60. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  61. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  62. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  63. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  64. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  65. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  66. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  67. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  68. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  69. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/server.py +0 -0
  70. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/__init__.py +0 -0
  71. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  72. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  73. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  74. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  75. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  76. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  77. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  78. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  79. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  80. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/_theme.py +0 -0
  81. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  82. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  83. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  84. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  85. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  86. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/settings.css +0 -0
  87. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/settings.html +0 -0
  88. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/settings_ui/settings.js +0 -0
  89. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/smoke_test.py +0 -0
  90. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  91. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/strict_bps.py +0 -0
  92. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/__init__.py +0 -0
  93. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/auto_backup.py +0 -0
  94. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/backup.py +0 -0
  95. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  96. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/component_api.py +0 -0
  97. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/component_config_reads.py +0 -0
  98. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/component_devices.py +0 -0
  99. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/component_registries.py +0 -0
  100. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  101. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  102. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/device_control.py +0 -0
  103. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/enhanced.py +0 -0
  104. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/helpers.py +0 -0
  105. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/radio/__init__.py +0 -0
  106. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/radio/base.py +0 -0
  107. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/radio/matter.py +0 -0
  108. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/radio/thread.py +0 -0
  109. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  110. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/radio/zwave.py +0 -0
  111. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/reference_validator.py +0 -0
  112. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/registry.py +0 -0
  113. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  114. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  115. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  116. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  117. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  118. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  119. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  120. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_addons.py +0 -0
  121. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_areas.py +0 -0
  122. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  123. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  124. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_calendar.py +0 -0
  125. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_camera.py +0 -0
  126. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_categories.py +0 -0
  127. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_code.py +0 -0
  128. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  129. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  130. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  131. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  132. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  133. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  134. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_dev.py +0 -0
  135. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_energy.py +0 -0
  136. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_entities.py +0 -0
  137. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  138. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_groups.py +0 -0
  139. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_hacs.py +0 -0
  140. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_history.py +0 -0
  141. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_integrations.py +0 -0
  142. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_labels.py +0 -0
  143. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_radio.py +0 -0
  144. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_registry.py +0 -0
  145. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_resources.py +0 -0
  146. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_search.py +0 -0
  147. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_service.py +0 -0
  148. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_services.py +0 -0
  149. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_system.py +0 -0
  150. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_themes.py +0 -0
  151. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_todo.py +0 -0
  152. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_traces.py +0 -0
  153. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_updates.py +0 -0
  154. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_utility.py +0 -0
  155. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  156. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  157. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  158. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/tools_zones.py +0 -0
  159. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/util_helpers.py +0 -0
  160. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/tools/validation_middleware.py +0 -0
  161. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/transforms/__init__.py +0 -0
  162. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/transforms/categorized_search.py +0 -0
  163. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  164. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/transport_security.py +0 -0
  165. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/update_check.py +0 -0
  166. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/utils/__init__.py +0 -0
  167. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/utils/config_hash.py +0 -0
  168. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/utils/data_paths.py +0 -0
  169. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/utils/domain_handlers.py +0 -0
  170. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  171. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  172. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/utils/operation_manager.py +0 -0
  173. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/utils/python_sandbox.py +0 -0
  174. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/utils/skill_loader.py +0 -0
  175. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/utils/usage_logger.py +0 -0
  176. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/visibility/__init__.py +0 -0
  177. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/visibility/model.py +0 -0
  178. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/visibility/persistence.py +0 -0
  179. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp/visibility/resolver.py +0 -0
  180. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  181. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  182. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  183. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  184. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  185. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/tests/__init__.py +0 -0
  186. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/tests/test_constants.py +0 -0
  187. {ha_mcp_dev-7.13.0.dev2029 → ha_mcp_dev-7.13.0.dev2033}/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.13.0.dev2029
3
+ Version: 7.13.0.dev2033
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.13.0.dev2029"
7
+ version = "7.13.0.dev2033"
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
@@ -66,9 +66,11 @@ class ReadOnlyExemption(NamedTuple):
66
66
  def _backup_write(args: dict[str, Any]) -> str | None:
67
67
  scope = args.get("scope")
68
68
  action = args.get("action")
69
- # Read-only-safe: per-edit backup listing/viewing, and snapshot listing
69
+ # Read-only-safe: per-edit backup listing/viewing/diffing, and snapshot listing
70
70
  # (issue #1586 — pure ``backup/info`` read, no tarball mutation).
71
- if scope == "edits" and action in ("list", "view"):
71
+ # ``diff`` (added in #1632) only reads the stored snapshot and fetches
72
+ # the live config to compare; it never mutates (issue #1944).
73
+ if scope == "edits" and action in ("list", "view", "diff"):
72
74
  return None
73
75
  if scope == "snapshot" and action == "list":
74
76
  return None
@@ -195,8 +197,9 @@ READ_ONLY_EXEMPT_TOOLS: dict[str, ReadOnlyExemption] = {
195
197
  ),
196
198
  "ha_manage_backup": ReadOnlyExemption(
197
199
  _backup_write,
198
- "listing and viewing per-edit backups (scope='edits', action='list' or "
199
- "'view') and listing snapshots (scope='snapshot', action='list')",
200
+ "listing, viewing, and diffing per-edit backups (scope='edits', "
201
+ "action='list', 'view', or 'diff') and listing snapshots "
202
+ "(scope='snapshot', action='list')",
200
203
  ),
201
204
  "ha_manage_addon": ReadOnlyExemption(
202
205
  _addon_write,
@@ -21,7 +21,11 @@ from ._config import (
21
21
  INDIVIDUAL_FETCH_BATCH_SIZE,
22
22
  SCRIPT_CONFIG_TIME_BUDGET,
23
23
  )
24
- from ._fetch import is_timeout_error
24
+ from ._fetch import (
25
+ http_500_diagnosis_hint,
26
+ is_timeout_error,
27
+ record_first_failure,
28
+ )
25
29
  from ._scenes import SceneSearchMixin
26
30
 
27
31
  logger = logging.getLogger(__name__)
@@ -136,6 +140,7 @@ class DeepSearchMixin(SceneSearchMixin):
136
140
  "timeout": 0,
137
141
  "integration_skipped": 0,
138
142
  "registry_failed": False,
143
+ "failed_sample": None,
139
144
  }
140
145
 
141
146
  # Diagnostic counters for the Attempt-C path on automations/
@@ -149,10 +154,12 @@ class DeepSearchMixin(SceneSearchMixin):
149
154
  automation_failed = 0
150
155
  automation_yaml_skipped = 0
151
156
  automation_timeout = 0
157
+ automation_failed_sample: str | None = None
152
158
  script_skipped = 0
153
159
  script_failed = 0
154
160
  script_yaml_skipped = 0
155
161
  script_timeout = 0
162
+ script_failed_sample: str | None = None
156
163
  helper_failed = 0
157
164
  dashboard_failed = 0
158
165
 
@@ -163,6 +170,7 @@ class DeepSearchMixin(SceneSearchMixin):
163
170
  automation_failed,
164
171
  automation_yaml_skipped,
165
172
  automation_timeout,
173
+ automation_failed_sample,
166
174
  ) = await self._deep_search_automations(
167
175
  all_entities,
168
176
  automation_unique_id_map,
@@ -185,6 +193,7 @@ class DeepSearchMixin(SceneSearchMixin):
185
193
  script_failed,
186
194
  script_yaml_skipped,
187
195
  script_timeout,
196
+ script_failed_sample,
188
197
  ) = await self._deep_search_scripts(
189
198
  all_entities,
190
199
  query_lower,
@@ -207,6 +216,7 @@ class DeepSearchMixin(SceneSearchMixin):
207
216
  scene_stats["integration_skipped"],
208
217
  scene_stats["registry_failed"],
209
218
  scene_stats["timeout"],
219
+ scene_stats["failed_sample"],
210
220
  ) = await self._deep_search_scenes(
211
221
  all_entities,
212
222
  query_lower,
@@ -268,10 +278,12 @@ class DeepSearchMixin(SceneSearchMixin):
268
278
  automation_failed=automation_failed,
269
279
  automation_yaml_skipped=automation_yaml_skipped,
270
280
  automation_timeout=automation_timeout,
281
+ automation_failed_sample=automation_failed_sample,
271
282
  script_skipped=script_skipped,
272
283
  script_failed=script_failed,
273
284
  script_yaml_skipped=script_yaml_skipped,
274
285
  script_timeout=script_timeout,
286
+ script_failed_sample=script_failed_sample,
275
287
  helper_failed=helper_failed,
276
288
  dashboard_failed=dashboard_failed,
277
289
  )
@@ -318,11 +330,11 @@ class DeepSearchMixin(SceneSearchMixin):
318
330
  exact_match: bool,
319
331
  *,
320
332
  config_time_budget: float | None = None,
321
- ) -> tuple[list[dict[str, Any]], int, int, int, int]:
333
+ ) -> tuple[list[dict[str, Any]], int, int, int, int, str | None]:
322
334
  """Deep-search automations: two-tier config fetch (REST bulk -> budgeted individual).
323
335
 
324
336
  Returns ``(matches, skipped_count, failed_count, yaml_skipped_count,
325
- timeout_count)``. ``skipped_count`` is non-zero only when bulk fetch
337
+ timeout_count, failed_sample)``. ``skipped_count`` is non-zero only when bulk fetch
326
338
  fell back to the per-id Attempt-C path AND its wall-clock budget
327
339
  exhausted before all configs were fetched; ``failed_count`` is
328
340
  non-zero when individual config fetches raised a non-404, non-timeout
@@ -338,6 +350,11 @@ class DeepSearchMixin(SceneSearchMixin):
338
350
  distinguish a complete zero-result from an incomplete one —
339
351
  skipped/failed ids carry their score-without-config through the
340
352
  merge and fall below the match threshold silently otherwise.
353
+ ``failed_sample`` is one representative
354
+ ``summarize_fetch_error`` summary of a ``failed``-class exception
355
+ (``None`` when none occurred), surfaced in the failed fragment so
356
+ the response names WHAT raised instead of pointing at debug logs
357
+ (#1784 follow-up).
341
358
  """
342
359
  automation_entities = [
343
360
  e for e in all_entities if e.get("entity_id", "").startswith("automation.")
@@ -376,6 +393,13 @@ class DeepSearchMixin(SceneSearchMixin):
376
393
  failed_count = 0
377
394
  yaml_skipped_count = 0
378
395
  timeout_count = 0
396
+ # One representative summary — only the FIRST ``failed``-class
397
+ # exception is kept (the fetch closure guards the append); it rides
398
+ # partial_reason as an ``e.g.`` (#1784 follow-up). The remaining
399
+ # failures are counted (``failed_count``) but not summarized, so the
400
+ # motivating "every per-id fetch 500s" case does N-1 fewer
401
+ # ``summarize_fetch_error`` calls.
402
+ failed_errors: list[str] = []
379
403
  if not bulk_fetched:
380
404
  uids_to_fetch = [
381
405
  uid for _, _, uid, _ in scored if uid and uid not in configs
@@ -405,6 +429,7 @@ class DeepSearchMixin(SceneSearchMixin):
405
429
  logger.debug(
406
430
  f"Automation individual config fetch ({uid}) failed: {e}"
407
431
  )
432
+ record_first_failure(failed_errors, e)
408
433
  return (uid, None, "failed")
409
434
  except TimeoutError:
410
435
  # asyncio.wait_for hit INDIVIDUAL_CONFIG_TIMEOUT. Classify
@@ -430,6 +455,7 @@ class DeepSearchMixin(SceneSearchMixin):
430
455
  logger.debug(
431
456
  f"Automation individual config fetch ({uid}) failed: {e}"
432
457
  )
458
+ record_first_failure(failed_errors, e)
433
459
  return (uid, None, "failed")
434
460
 
435
461
  (
@@ -463,7 +489,14 @@ class DeepSearchMixin(SceneSearchMixin):
463
489
  scored, configs, query_lower, exact_match
464
490
  )
465
491
  ]
466
- return matches, skipped_count, failed_count, yaml_skipped_count, timeout_count
492
+ return (
493
+ matches,
494
+ skipped_count,
495
+ failed_count,
496
+ yaml_skipped_count,
497
+ timeout_count,
498
+ failed_errors[0] if failed_errors else None,
499
+ )
467
500
 
468
501
  async def _deep_search_scripts(
469
502
  self,
@@ -472,11 +505,11 @@ class DeepSearchMixin(SceneSearchMixin):
472
505
  exact_match: bool,
473
506
  *,
474
507
  config_time_budget: float | None = None,
475
- ) -> tuple[list[dict[str, Any]], int, int, int, int]:
508
+ ) -> tuple[list[dict[str, Any]], int, int, int, int, str | None]:
476
509
  """Deep-search scripts: same two-tier strategy as automations.
477
510
 
478
511
  Returns ``(matches, skipped_count, failed_count, yaml_skipped_count,
479
- timeout_count)``; semantics identical to ``_deep_search_automations``
512
+ timeout_count, failed_sample)``; semantics identical to ``_deep_search_automations``
480
513
  — the 404 path catches YAML-defined scripts (which
481
514
  ``client.get_script_config`` re-raises as
482
515
  ``HomeAssistantAPIError(status_code=404)``).
@@ -514,6 +547,13 @@ class DeepSearchMixin(SceneSearchMixin):
514
547
  failed_count = 0
515
548
  yaml_skipped_count = 0
516
549
  timeout_count = 0
550
+ # One representative summary — only the FIRST ``failed``-class
551
+ # exception is kept (the fetch closure guards the append); it rides
552
+ # partial_reason as an ``e.g.`` (#1784 follow-up). The remaining
553
+ # failures are counted (``failed_count``) but not summarized, so the
554
+ # motivating "every per-id fetch 500s" case does N-1 fewer
555
+ # ``summarize_fetch_error`` calls.
556
+ failed_errors: list[str] = []
517
557
  if not bulk_fetched:
518
558
  sids_to_fetch = [
519
559
  sid for _, _, sid, _ in scored if sid and sid not in configs
@@ -540,6 +580,7 @@ class DeepSearchMixin(SceneSearchMixin):
540
580
  )
541
581
  return (sid, None, "yaml_skipped")
542
582
  logger.debug(f"Script individual config fetch ({sid}) failed: {e}")
583
+ record_first_failure(failed_errors, e)
543
584
  return (sid, None, "failed")
544
585
  except TimeoutError:
545
586
  # See _fetch_automation_config: per-request timeout under
@@ -559,6 +600,7 @@ class DeepSearchMixin(SceneSearchMixin):
559
600
  )
560
601
  return (sid, None, "timeout")
561
602
  logger.debug(f"Script individual config fetch ({sid}) failed: {e}")
603
+ record_first_failure(failed_errors, e)
562
604
  return (sid, None, "failed")
563
605
 
564
606
  (
@@ -593,7 +635,14 @@ class DeepSearchMixin(SceneSearchMixin):
593
635
  scored, configs, query_lower, exact_match
594
636
  )
595
637
  ]
596
- return matches, skipped_count, failed_count, yaml_skipped_count, timeout_count
638
+ return (
639
+ matches,
640
+ skipped_count,
641
+ failed_count,
642
+ yaml_skipped_count,
643
+ timeout_count,
644
+ failed_errors[0] if failed_errors else None,
645
+ )
597
646
 
598
647
  @staticmethod
599
648
  def _build_helper_registry_map(
@@ -966,6 +1015,8 @@ class DeepSearchMixin(SceneSearchMixin):
966
1015
  script_yaml_skipped: int = 0,
967
1016
  automation_timeout: int = 0,
968
1017
  script_timeout: int = 0,
1018
+ automation_failed_sample: str | None = None,
1019
+ script_failed_sample: str | None = None,
969
1020
  helper_failed: int = 0,
970
1021
  dashboard_failed: int = 0,
971
1022
  ) -> dict[str, Any]:
@@ -1028,6 +1079,8 @@ class DeepSearchMixin(SceneSearchMixin):
1028
1079
  script_yaml_skipped=script_yaml_skipped,
1029
1080
  automation_timeout=automation_timeout,
1030
1081
  script_timeout=script_timeout,
1082
+ automation_failed_sample=automation_failed_sample,
1083
+ script_failed_sample=script_failed_sample,
1031
1084
  helper_failed=helper_failed,
1032
1085
  dashboard_failed=dashboard_failed,
1033
1086
  )
@@ -1045,6 +1098,8 @@ class DeepSearchMixin(SceneSearchMixin):
1045
1098
  script_yaml_skipped: int = 0,
1046
1099
  automation_timeout: int = 0,
1047
1100
  script_timeout: int = 0,
1101
+ automation_failed_sample: str | None = None,
1102
+ script_failed_sample: str | None = None,
1048
1103
  helper_failed: int = 0,
1049
1104
  dashboard_failed: int = 0,
1050
1105
  ) -> None:
@@ -1077,6 +1132,15 @@ class DeepSearchMixin(SceneSearchMixin):
1077
1132
  rationalised away by blind agents who reported the result as
1078
1133
  complete; the harder phrasing closes that gap.
1079
1134
 
1135
+ ``automation_failed_sample`` / ``script_failed_sample`` carry one
1136
+ representative ``summarize_fetch_error`` summary for the generic
1137
+ ``failed`` class, appended to its fragment as an ``e.g.`` so the
1138
+ response names WHAT raised instead of pointing at debug logs
1139
+ (#1784 follow-up). When that sample is an HTTP 500 — whose body is
1140
+ aiohttp's generic placeholder, so the sample can't name the cause —
1141
+ a static HA-log diagnosis is appended too (``http_500_diagnosis_hint``).
1142
+ ``None`` keeps the fragment wording unchanged.
1143
+
1080
1144
  Append-safe: the existing ``partial_reason`` (if any) is preserved
1081
1145
  and the new reasons are concatenated with ``" ; "``.
1082
1146
  """
@@ -1084,7 +1148,16 @@ class DeepSearchMixin(SceneSearchMixin):
1084
1148
  # The automation and script fragments are symmetric (noun, per-id
1085
1149
  # endpoint, budget env var); loop rather than duplicating the four
1086
1150
  # per-class fragments per type.
1087
- for noun, endpoint, budget_env, skipped, failed, yaml_skipped, timeout in (
1151
+ for (
1152
+ noun,
1153
+ endpoint,
1154
+ budget_env,
1155
+ skipped,
1156
+ failed,
1157
+ yaml_skipped,
1158
+ timeout,
1159
+ failed_sample,
1160
+ ) in (
1088
1161
  (
1089
1162
  "automation",
1090
1163
  "/config/automation/config",
@@ -1093,6 +1166,7 @@ class DeepSearchMixin(SceneSearchMixin):
1093
1166
  automation_failed,
1094
1167
  automation_yaml_skipped,
1095
1168
  automation_timeout,
1169
+ automation_failed_sample,
1096
1170
  ),
1097
1171
  (
1098
1172
  "script",
@@ -1102,6 +1176,7 @@ class DeepSearchMixin(SceneSearchMixin):
1102
1176
  script_failed,
1103
1177
  script_yaml_skipped,
1104
1178
  script_timeout,
1179
+ script_failed_sample,
1105
1180
  ),
1106
1181
  ):
1107
1182
  if skipped:
@@ -1114,10 +1189,22 @@ class DeepSearchMixin(SceneSearchMixin):
1114
1189
  "the web Settings UI's Advanced section)."
1115
1190
  )
1116
1191
  if failed:
1192
+ # Name ONE representative error inline when the fetch path
1193
+ # captured one — the opaque bucket alone sent users on a
1194
+ # debug-log dive for trivially-diagnosable server errors
1195
+ # (#1784 follow-up: every per-id script fetch 500ing on a
1196
+ # ``!secret`` reference in scripts.yaml).
1197
+ sample_suffix = f"; e.g. {failed_sample}" if failed_sample else ""
1198
+ # An HTTP 500 body is aiohttp's generic "500 Internal Server
1199
+ # Error" — the real cause (e.g. a ``!secret`` the per-id
1200
+ # config endpoint rejects) is HA-log-only, never in the
1201
+ # response — so the sample can't name it. Append the static
1202
+ # diagnosis the sample can't carry (#1784 follow-up).
1203
+ hint = http_500_diagnosis_hint(failed_sample)
1117
1204
  reasons.append(
1118
1205
  f"{failed} {noun}(s) not scanned (per-id fetch raised "
1119
- "a non-404 error) — their match status is unknown; this "
1120
- "result is not exhaustive."
1206
+ f"a non-404 error{sample_suffix}) — their match status "
1207
+ f"is unknown; this result is not exhaustive.{hint}"
1121
1208
  )
1122
1209
  if yaml_skipped:
1123
1210
  reasons.append(
@@ -2,17 +2,106 @@
2
2
 
3
3
  import asyncio
4
4
  import logging
5
+ import re
5
6
  import time
6
7
  from collections.abc import Awaitable, Callable
7
8
  from typing import Any
8
9
 
9
10
  import httpx
10
11
 
12
+ from ...client.rest_client import HomeAssistantAPIError
11
13
  from ._config import INDIVIDUAL_FETCH_BATCH_SIZE
12
14
  from ._scoring import ScoringMixin
13
15
 
14
16
  logger = logging.getLogger(__name__)
15
17
 
18
+ # The REST client's own message prefix ("API error: <code> - "); stripped by
19
+ # ``summarize_fetch_error`` so the summary doesn't state the status twice.
20
+ _API_ERROR_PREFIX = re.compile(r"^API error:\s*\d+\s*-\s*")
21
+
22
+ # Hard cap on a ``summarize_fetch_error`` summary. partial_reason fragments
23
+ # are read inline by agents; one representative error needs a first line,
24
+ # not a body dump.
25
+ _ERROR_SAMPLE_MAX_LEN = 160
26
+
27
+
28
+ def summarize_fetch_error(exc: BaseException) -> str:
29
+ """One-line, length-capped summary of a per-id config-fetch failure.
30
+
31
+ The generic ``failed`` bucket collapses every non-404, non-timeout
32
+ exception into one opaque partial_reason fragment ("per-id fetch raised
33
+ a non-404 error"), with the real exception visible only at debug-level
34
+ logging. The follow-up report on issue #1784 showed what that hides: a
35
+ box where every per-id script fetch returns a fast HTTP 500 because
36
+ ``scripts.yaml`` contains a ``!secret`` reference (HA core's config view
37
+ file-loads the YAML per request and rejects secrets before the id
38
+ lookup) — a five-minute diagnosis if the response named the error, a
39
+ log dive otherwise. This summary rides the fragment as an ``e.g.``.
40
+
41
+ HTTP errors render as ``HTTP <code>: <first line of the message>`` (the
42
+ client's own ``API error: <code> - `` prefix is stripped rather than
43
+ duplicated); everything else as ``<ExceptionType>: <first line>``.
44
+ """
45
+ lines = str(exc).strip().splitlines()
46
+ text = lines[0].strip() if lines else ""
47
+ if isinstance(exc, HomeAssistantAPIError) and exc.status_code is not None:
48
+ text = _API_ERROR_PREFIX.sub("", text)
49
+ prefix = f"HTTP {exc.status_code}"
50
+ else:
51
+ prefix = type(exc).__name__
52
+ summary = f"{prefix}: {text}" if text else prefix
53
+ if len(summary) > _ERROR_SAMPLE_MAX_LEN:
54
+ summary = summary[: _ERROR_SAMPLE_MAX_LEN - 1] + "…"
55
+ return summary
56
+
57
+
58
+ # Marks a rendered ``summarize_fetch_error`` summary whose cause the sample
59
+ # cannot name: an HTTP 500's body is aiohttp's generic "500 Internal Server
60
+ # Error" placeholder, so the real error (a YAMLException the per-id config
61
+ # view raised, most often a ``!secret`` reference it rejects) lives only in
62
+ # the HA log, never in the response.
63
+ _HTTP_500_PREFIX = "HTTP 500:"
64
+
65
+ # Static diagnosis appended to an HTTP-500 failed fragment. The sample names
66
+ # the status but not the cause (see ``_HTTP_500_PREFIX``); this points the
67
+ # reader at the HA log and the single most common cause, delivering the
68
+ # five-minute diagnosis the #1784 follow-up asked for without claiming a
69
+ # cause the response can't actually confirm. Kept endpoint-agnostic — this
70
+ # same string rides the automation, script, AND scene fragments, so it names
71
+ # no specific YAML file (a scene 500 has nothing to do with scripts.yaml);
72
+ # "the config file HA loads for this entity" covers all three.
73
+ _HTTP_500_DIAGNOSIS = (
74
+ " A per-id config fetch returning HTTP 500 is most often a `!secret` "
75
+ "reference in the config file HA loads for this entity, which the per-id "
76
+ "config endpoint rejects; the specific cause is in the Home Assistant log "
77
+ "(the 500 body is a generic placeholder), not in this response."
78
+ )
79
+
80
+
81
+ def http_500_diagnosis_hint(failed_sample: str | None) -> str:
82
+ """Static HA-log hint to append when ``failed_sample`` is an HTTP 500.
83
+
84
+ Returns ``""`` for any other sample (or ``None``), so non-500 fragments
85
+ are byte-identical to before. See ``_HTTP_500_DIAGNOSIS``.
86
+ """
87
+ if failed_sample and failed_sample.startswith(_HTTP_500_PREFIX):
88
+ return _HTTP_500_DIAGNOSIS
89
+ return ""
90
+
91
+
92
+ def record_first_failure(failed_errors: list[str], exc: BaseException) -> None:
93
+ """Capture ``exc``'s summary as the representative ``failed`` sample, once.
94
+
95
+ Only the FIRST generic-``failed`` exception per fetch pass is summarized
96
+ (subsequent ones are counted but not summarized) — in the motivating
97
+ "every per-id fetch 500s" case that is N-1 fewer ``summarize_fetch_error``
98
+ calls. The append is a no-op once ``failed_errors`` is non-empty. Called
99
+ from the per-id fetch closures, which run under ``asyncio.gather`` but
100
+ never suspend between the check and the append, so no lock is needed.
101
+ """
102
+ if not failed_errors:
103
+ failed_errors.append(summarize_fetch_error(exc))
104
+
16
105
 
17
106
  def is_timeout_error(exc: BaseException) -> bool:
18
107
  """True when ``exc`` is, or was directly caused by, a request timeout.
@@ -10,7 +10,12 @@ from ._config import (
10
10
  INDIVIDUAL_FETCH_BATCH_SIZE,
11
11
  SCENE_CONFIG_TIME_BUDGET,
12
12
  )
13
- from ._fetch import ConfigFetchMixin, is_timeout_error
13
+ from ._fetch import (
14
+ ConfigFetchMixin,
15
+ http_500_diagnosis_hint,
16
+ is_timeout_error,
17
+ record_first_failure,
18
+ )
14
19
 
15
20
  logger = logging.getLogger(__name__)
16
21
 
@@ -211,15 +216,19 @@ class SceneSearchMixin(ConfigFetchMixin):
211
216
  *,
212
217
  config_time_budget: float | None = None,
213
218
  prefetched_registry: Any = None,
214
- ) -> tuple[list[dict[str, Any]], int, int, int, bool, int]:
219
+ ) -> tuple[list[dict[str, Any]], int, int, int, bool, int, str | None]:
215
220
  """Deep-search scenes: two-tier strategy plus registry-walk augmentation.
216
221
 
217
222
  Scenes have no listing primitive, so entities are enumerated from
218
223
  get_states() and configs fetched per id. Returns the scene results plus
219
- the five diagnostic signals feeding the response ``partial`` /
224
+ the six diagnostic signals feeding the response ``partial`` /
220
225
  ``partial_reason``:
221
226
  ``(results, failed_count, skipped_count, integration_skipped,
222
- registry_failed, timeout_count)``.
227
+ registry_failed, timeout_count, failed_sample)``. ``failed_sample``
228
+ is one representative ``summarize_fetch_error`` summary of a
229
+ ``failed``-class exception (``None`` when none occurred) — see the
230
+ automation/script mirror in ``_deep_search_automations`` (#1784
231
+ follow-up).
223
232
  """
224
233
  scene_entities = [
225
234
  e for e in all_entities if e.get("entity_id", "").startswith("scene.")
@@ -264,6 +273,11 @@ class SceneSearchMixin(ConfigFetchMixin):
264
273
  skipped_count = 0
265
274
  integration_skipped = 0
266
275
  timeout_count = 0
276
+ # One representative summary — only the FIRST ``failed``-class
277
+ # exception is kept (the fetch closure guards the append); it rides
278
+ # partial_reason as an ``e.g.`` (#1784 follow-up). The remaining
279
+ # failures are counted (``failed_count``) but not summarized.
280
+ failed_errors: list[str] = []
267
281
 
268
282
  # Attempt C: parallel per-id fetch with a wall-clock budget so a few
269
283
  # slow scenes don't tank the whole search.
@@ -300,6 +314,7 @@ class SceneSearchMixin(ConfigFetchMixin):
300
314
  )
301
315
  return (sid, None, "timeout")
302
316
  logger.debug(f"Scene individual config fetch ({sid}) failed: {e}")
317
+ record_first_failure(failed_errors, e)
303
318
  return (sid, None, "failed")
304
319
 
305
320
  (
@@ -347,6 +362,7 @@ class SceneSearchMixin(ConfigFetchMixin):
347
362
  integration_skipped,
348
363
  registry_failed,
349
364
  timeout_count,
365
+ failed_errors[0] if failed_errors else None,
350
366
  )
351
367
 
352
368
  @staticmethod
@@ -376,9 +392,20 @@ class SceneSearchMixin(ConfigFetchMixin):
376
392
  response["partial"] = True
377
393
  reason_parts: list[str] = []
378
394
  if failed:
395
+ # Name ONE representative error inline when Attempt C captured
396
+ # one (.get(): tolerate stats dicts built without the key) —
397
+ # mirrors the automation/script ``e.g.`` sample, #1784 follow-up.
398
+ failed_sample = scene_stats.get("failed_sample")
399
+ sample_suffix = f"; e.g. {failed_sample}" if failed_sample else ""
400
+ # An HTTP 500 sample names the status but not the cause (the body
401
+ # is aiohttp's generic placeholder); append the static HA-log
402
+ # diagnosis, mirroring the automation/script fragment (#1784).
403
+ hint = http_500_diagnosis_hint(failed_sample)
379
404
  reason_parts.append(
380
- f"{failed} scene(s) not scanned (per-id fetch raised) — "
381
- "their match status is unknown; this result is not exhaustive."
405
+ f"{failed} scene(s) not scanned (per-id fetch raised"
406
+ f"{sample_suffix}) — "
407
+ f"their match status is unknown; this result is not "
408
+ f"exhaustive.{hint}"
382
409
  )
383
410
  if timeout:
384
411
  reason_parts.append(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.13.0.dev2029
3
+ Version: 7.13.0.dev2033
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