ha-mcp-dev 7.14.2.dev2098__tar.gz → 7.14.2.dev2103__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 (188) hide show
  1. {ha_mcp_dev-7.14.2.dev2098/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.14.2.dev2103}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/client/rest_client.py +180 -28
  4. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_config_scenes.py +97 -13
  5. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_search.py +209 -47
  6. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  7. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/LICENSE +0 -0
  8. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/MANIFEST.in +0 -0
  9. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/README.md +0 -0
  10. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/setup.cfg +0 -0
  11. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/__init__.py +0 -0
  12. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/__main__.py +0 -0
  13. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/_pypi_marker +0 -0
  14. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/_version.py +0 -0
  15. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/auth/__init__.py +0 -0
  16. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/auth/consent_form.py +0 -0
  17. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/auth/provider.py +0 -0
  18. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/backup_manager.py +0 -0
  19. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/browser_landing.py +0 -0
  20. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/client/__init__.py +0 -0
  21. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/client/supervisor_client.py +0 -0
  22. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/client/websocket_client.py +0 -0
  23. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/client/websocket_listener.py +0 -0
  24. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/config.py +0 -0
  25. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  26. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  27. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  28. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  29. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  30. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  31. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/errors.py +0 -0
  32. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/llm_exposure.py +0 -0
  33. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/policy/__init__.py +0 -0
  34. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/policy/approval_queue.py +0 -0
  35. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/policy/evaluator.py +0 -0
  36. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/policy/handlers.py +0 -0
  37. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/policy/middleware.py +0 -0
  38. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/policy/model.py +0 -0
  39. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/policy/persistence.py +0 -0
  40. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/policy/value_sources.py +0 -0
  41. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/py.typed +0 -0
  42. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/read_only.py +0 -0
  43. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  44. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  45. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  46. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  47. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  48. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  49. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  50. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  51. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  52. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  53. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  54. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  55. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  56. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  57. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  58. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  59. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  60. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  61. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  62. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  63. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  64. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  65. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  66. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  67. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  68. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/server.py +0 -0
  69. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/__init__.py +0 -0
  70. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  71. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  72. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  73. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  74. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  75. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  76. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  77. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  78. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  79. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/_theme.py +0 -0
  80. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  81. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  82. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  83. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  84. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  85. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
  86. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/settings.css +0 -0
  87. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/settings.html +0 -0
  88. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/settings_ui/settings.js +0 -0
  89. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/smoke_test.py +0 -0
  90. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  91. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/strict_bps.py +0 -0
  92. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/__init__.py +0 -0
  93. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/auto_backup.py +0 -0
  94. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/backup.py +0 -0
  95. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  96. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/component_api.py +0 -0
  97. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/component_config_reads.py +0 -0
  98. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/component_devices.py +0 -0
  99. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/component_registries.py +0 -0
  100. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  101. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  102. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/device_control.py +0 -0
  103. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/enhanced.py +0 -0
  104. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/helpers.py +0 -0
  105. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/radio/__init__.py +0 -0
  106. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/radio/base.py +0 -0
  107. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/radio/matter.py +0 -0
  108. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/radio/thread.py +0 -0
  109. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  110. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/radio/zwave.py +0 -0
  111. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/reference_validator.py +0 -0
  112. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/registry.py +0 -0
  113. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  114. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  115. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  116. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  117. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  118. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  119. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  120. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  121. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  122. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  123. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_addons.py +0 -0
  124. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_areas.py +0 -0
  125. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  126. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  127. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_calendar.py +0 -0
  128. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_camera.py +0 -0
  129. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_categories.py +0 -0
  130. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_code.py +0 -0
  131. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  132. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  133. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  134. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  135. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  136. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_dev.py +0 -0
  137. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_energy.py +0 -0
  138. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_entities.py +0 -0
  139. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  140. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_groups.py +0 -0
  141. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_hacs.py +0 -0
  142. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_history.py +0 -0
  143. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_integrations.py +0 -0
  144. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_labels.py +0 -0
  145. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_radio.py +0 -0
  146. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_registry.py +0 -0
  147. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_resources.py +0 -0
  148. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_service.py +0 -0
  149. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_services.py +0 -0
  150. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_system.py +0 -0
  151. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_themes.py +0 -0
  152. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_todo.py +0 -0
  153. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_traces.py +0 -0
  154. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_updates.py +0 -0
  155. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_utility.py +0 -0
  156. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  157. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  158. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  159. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/tools_zones.py +0 -0
  160. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/util_helpers.py +0 -0
  161. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/tools/validation_middleware.py +0 -0
  162. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/transforms/__init__.py +0 -0
  163. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/transforms/categorized_search.py +0 -0
  164. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  165. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/transport_security.py +0 -0
  166. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/update_check.py +0 -0
  167. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/utils/__init__.py +0 -0
  168. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/utils/config_hash.py +0 -0
  169. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/utils/data_paths.py +0 -0
  170. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/utils/domain_handlers.py +0 -0
  171. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  172. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  173. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/utils/operation_manager.py +0 -0
  174. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/utils/python_sandbox.py +0 -0
  175. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/utils/skill_loader.py +0 -0
  176. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/utils/usage_logger.py +0 -0
  177. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/visibility/__init__.py +0 -0
  178. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/visibility/model.py +0 -0
  179. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/visibility/persistence.py +0 -0
  180. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp/visibility/resolver.py +0 -0
  181. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  182. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  183. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  184. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  185. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  186. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/tests/__init__.py +0 -0
  187. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/tests/test_constants.py +0 -0
  188. {ha_mcp_dev-7.14.2.dev2098 → ha_mcp_dev-7.14.2.dev2103}/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.14.2.dev2098
3
+ Version: 7.14.2.dev2103
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.14.2.dev2098"
7
+ version = "7.14.2.dev2103"
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
@@ -8,7 +8,8 @@ import logging
8
8
  import os
9
9
  import ssl
10
10
  import time
11
- from typing import Any
11
+ from dataclasses import dataclass
12
+ from typing import Any, NoReturn
12
13
 
13
14
  import httpx
14
15
  from websockets.exceptions import WebSocketException
@@ -97,6 +98,54 @@ class HomeAssistantAPIError(HomeAssistantError):
97
98
  self.response_data = response_data
98
99
 
99
100
 
101
+ @dataclass(frozen=True)
102
+ class SceneResolution:
103
+ """Outcome of resolving a scene identifier via the entity registry.
104
+
105
+ ``registry_hit`` records whether ``config/entity_registry/get`` returned a
106
+ usable ``unique_id`` - i.e. the entity actually EXISTS. It is the signal that
107
+ separates a genuinely-missing scene from one that exists but is not editable
108
+ through HA's scene config API (a Hue/vendor scene, or a raw-YAML scene): both
109
+ resolve fine yet 404 on ``config/scene/config/{id}``, because that endpoint is
110
+ backed only by the managed ``scenes.yaml`` (issue #1971). ``platform`` carries
111
+ the registry entry's integration domain (``"hue"``, ``"homeassistant"``, …)
112
+ for error-message enrichment only - it never drives the classification, since
113
+ a raw-YAML scene is ``platform="homeassistant"`` yet still 404s.
114
+ """
115
+
116
+ storage_key: str
117
+ registry_hit: bool
118
+ platform: str | None
119
+
120
+
121
+ class SceneStorageConfigNotFoundError(HomeAssistantAPIError):
122
+ """A scene entity resolved in the registry, but ``config/scene/config/{id}``
123
+ (backed by ``scenes.yaml``) has no matching entry.
124
+
125
+ The entity EXISTS; it is simply not an editable Home Assistant storage scene
126
+ (Hue/vendor scene, or a raw-YAML scene). Raised instead of a bare 404 so the
127
+ tools layer can surface ``CONFIG_NOT_FOUND`` rather than the misleading
128
+ ``ENTITY_NOT_FOUND``/``RESOURCE_NOT_FOUND`` (issue #1971). Subclasses
129
+ ``HomeAssistantAPIError`` so existing ``except HomeAssistantAPIError`` sites
130
+ keep treating it as the 404 it is.
131
+ """
132
+
133
+ def __init__(
134
+ self,
135
+ scene_id: str,
136
+ *,
137
+ platform: str | None = None,
138
+ storage_key: str | None = None,
139
+ ):
140
+ self.scene_id = scene_id
141
+ self.platform = platform
142
+ self.storage_key = storage_key
143
+ msg = f"Scene not found in editable storage: {scene_id}"
144
+ if storage_key and storage_key != scene_id:
145
+ msg += f" (resolved storage key: {storage_key})"
146
+ super().__init__(msg, status_code=404)
147
+
148
+
100
149
  class HomeAssistantCommandError(HomeAssistantError):
101
150
  """WebSocket command returned success=False.
102
151
 
@@ -1773,20 +1822,23 @@ class HomeAssistantClient:
1773
1822
  ) from e
1774
1823
  raise
1775
1824
 
1776
- async def resolve_scene_id(self, identifier: str) -> str:
1777
- """
1778
- Resolve a scene identifier to its storage key via the entity registry.
1825
+ async def _resolve_scene(self, identifier: str) -> SceneResolution:
1826
+ """Resolve a scene identifier to its storage key AND registry metadata.
1779
1827
 
1780
- Scenes may be renamed in the HA UI, changing the entity_id but keeping
1781
- the original storage key. Mirrors :meth:`_resolve_script_id` scenes
1782
- likewise need a WebSocket entity registry lookup; their state attributes
1783
- do not surface the storage key.
1828
+ Backs :meth:`resolve_scene_id`. Returns the storage key together with
1829
+ ``registry_hit`` (did the entity actually resolve?) and ``platform`` - the
1830
+ two signals a bare storage key throws away. Callers that hit a config-API
1831
+ 404 need ``registry_hit`` to tell "entity missing" from "entity exists but
1832
+ has no editable storage config", and ``platform`` to name the owning
1833
+ integration in the resulting error (issue #1971).
1784
1834
 
1785
1835
  Args:
1786
1836
  identifier: Scene ID (with or without ``scene.`` prefix)
1787
1837
 
1788
1838
  Returns:
1789
- The storage key for the configuration API
1839
+ A :class:`SceneResolution`. On any lookup failure it falls back to the
1840
+ bare id with ``registry_hit=False`` - a write path must not treat the
1841
+ guessed bare id as a confirmed resolve.
1790
1842
  """
1791
1843
  bare_id = identifier.removeprefix("scene.")
1792
1844
  entity_id = f"scene.{bare_id}"
@@ -1795,13 +1847,18 @@ class HomeAssistantClient:
1795
1847
  {"type": "config/entity_registry/get", "entity_id": entity_id}
1796
1848
  )
1797
1849
  if result.get("success") is not False:
1798
- unique_id = result.get("result", {}).get("unique_id")
1850
+ entry = result.get("result") or {}
1851
+ unique_id = entry.get("unique_id")
1799
1852
  if unique_id:
1800
1853
  if unique_id != bare_id:
1801
1854
  logger.debug(
1802
1855
  f"Resolved scene entity_id {entity_id} to storage key {unique_id}"
1803
1856
  )
1804
- return str(unique_id)
1857
+ return SceneResolution(
1858
+ storage_key=str(unique_id),
1859
+ registry_hit=True,
1860
+ platform=entry.get("platform"),
1861
+ )
1805
1862
  except HomeAssistantConnectionError:
1806
1863
  # A dead transport must not become a silent guess: the fallback
1807
1864
  # below assumes the storage key equals the bare entity_id, which is
@@ -1814,19 +1871,111 @@ class HomeAssistantClient:
1814
1871
  f"Entity registry lookup failed for {entity_id}, using bare id: {bare_id}",
1815
1872
  exc_info=True,
1816
1873
  )
1817
- return bare_id
1874
+ return SceneResolution(storage_key=bare_id, registry_hit=False, platform=None)
1875
+
1876
+ async def resolve_scene_id(self, identifier: str) -> str:
1877
+ """
1878
+ Resolve a scene identifier to its storage key via the entity registry.
1879
+
1880
+ Scenes may be renamed in the HA UI, changing the entity_id but keeping
1881
+ the original storage key. Like :meth:`_resolve_script_id`, this returns
1882
+ the bare storage key and needs a WebSocket entity registry lookup because
1883
+ the state attributes do not surface it; the parity ends there, since the
1884
+ underlying :meth:`_resolve_scene` additionally carries registry metadata
1885
+ (``registry_hit``/``platform``) that the script resolver has no analogue for.
1886
+
1887
+ Thin wrapper over :meth:`_resolve_scene` for callers that only need the
1888
+ storage key (e.g. the write path); the read/delete paths use the richer
1889
+ :class:`SceneResolution` directly.
1890
+
1891
+ Args:
1892
+ identifier: Scene ID (with or without ``scene.`` prefix)
1893
+
1894
+ Returns:
1895
+ The storage key for the configuration API
1896
+ """
1897
+ return (await self._resolve_scene(identifier)).storage_key
1898
+
1899
+ async def _scene_state_exists(self, storage_key: str) -> bool:
1900
+ """Return True if ``scene.<storage_key>`` exists in the state machine.
1901
+
1902
+ An ``id``-less YAML scene has no registry ``unique_id`` (so
1903
+ ``registry_hit`` is False) yet still registers a state-machine entity.
1904
+ This one-shot existence check separates that real-but-not-storage scene
1905
+ from a genuinely-absent one after a config-API 404 (issue #1971). A 404
1906
+ here is a clean "absent"; a transport failure propagates (fail loud).
1907
+ """
1908
+ try:
1909
+ await self.get_entity_state(f"scene.{storage_key}")
1910
+ return True
1911
+ except HomeAssistantAPIError as e:
1912
+ if e.status_code == 404:
1913
+ return False
1914
+ raise
1915
+
1916
+ async def _raise_scene_config_404(
1917
+ self,
1918
+ scene_id: str,
1919
+ resolved_id: str,
1920
+ resolution: SceneResolution | None,
1921
+ ) -> NoReturn:
1922
+ """Translate a scene config-API 404 into the right exception.
1923
+
1924
+ When the registry resolve succeeded (``registry_hit``), the entity exists
1925
+ and the 404 means "no editable storage config" → raise
1926
+ :class:`SceneStorageConfigNotFoundError` carrying the platform. On a
1927
+ registry miss the entity may still be an ``id``-less YAML scene, which has
1928
+ no registry ``unique_id`` yet exists in the state machine: a state hit
1929
+ upgrades the 404 to the same not-storage-scene case with ``platform=None``
1930
+ (issue #1971). Only a state miss too is the historic genuinely-absent bare
1931
+ 404. The state check is skipped when ``resolution`` is None (the internal
1932
+ ``_resolved`` re-fetch fast path, where a 404 is a genuine disappearance).
1933
+ Always raises.
1934
+ """
1935
+ if resolution is not None:
1936
+ if resolution.registry_hit:
1937
+ raise SceneStorageConfigNotFoundError(
1938
+ scene_id, platform=resolution.platform, storage_key=resolved_id
1939
+ )
1940
+ if await self._scene_state_exists(resolved_id):
1941
+ raise SceneStorageConfigNotFoundError(
1942
+ scene_id, platform=None, storage_key=resolved_id
1943
+ )
1944
+ msg = f"Scene not found: {scene_id}"
1945
+ if resolved_id != scene_id:
1946
+ msg += f" (resolved storage key: {resolved_id})"
1947
+ raise HomeAssistantAPIError(msg, status_code=404)
1818
1948
 
1819
1949
  async def get_scene_config(
1820
- self, scene_id: str, *, _resolved: bool = False
1950
+ self,
1951
+ scene_id: str,
1952
+ *,
1953
+ _resolved: bool = False,
1954
+ resolution: SceneResolution | None = None,
1821
1955
  ) -> dict[str, Any]:
1822
1956
  """Get Home Assistant scene configuration by scene_id.
1823
1957
 
1958
+ ``resolution`` lets a caller that already ran :meth:`_resolve_scene` pass
1959
+ the full outcome through: the endpoint uses its ``storage_key`` and its
1960
+ ``registry_hit``/``platform`` drive the config-404 classification (issue
1961
+ #1971) with no extra lookup. It takes precedence over ``_resolved``.
1962
+
1824
1963
  ``_resolved=True`` signals ``scene_id`` is already the resolved storage
1825
1964
  key (the caller ran :meth:`resolve_scene_id`), skipping the redundant
1826
1965
  registry lookup. ``resolve_scene_id`` is idempotent, so the result is
1827
- identical either way.
1966
+ identical either way. This fast path is the internal re-fetch of an
1967
+ already-resolved scene (its sole caller is the post-write re-fetch), where
1968
+ a subsequent 404 is a genuine disappearance, not the not-storage-scene
1969
+ case; the ``registry_hit``/``platform`` signals a config 404 needs are
1970
+ deliberately unavailable there.
1828
1971
  """
1829
- resolved_id = scene_id if _resolved else await self.resolve_scene_id(scene_id)
1972
+ if resolution is not None:
1973
+ resolved_id = resolution.storage_key
1974
+ elif _resolved:
1975
+ resolved_id = scene_id
1976
+ else:
1977
+ resolution = await self._resolve_scene(scene_id)
1978
+ resolved_id = resolution.storage_key
1830
1979
  try:
1831
1980
  endpoint = f"config/scene/config/{resolved_id}"
1832
1981
  response = await self._request("GET", endpoint)
@@ -1834,10 +1983,7 @@ class HomeAssistantClient:
1834
1983
  return {"success": True, "scene_id": resolved_id, "config": response}
1835
1984
  except HomeAssistantAPIError as e:
1836
1985
  if e.status_code == 404:
1837
- msg = f"Scene not found: {scene_id}"
1838
- if resolved_id != scene_id:
1839
- msg += f" (resolved storage key: {resolved_id})"
1840
- raise HomeAssistantAPIError(msg, status_code=404) from e
1986
+ await self._raise_scene_config_404(scene_id, resolved_id, resolution)
1841
1987
  raise
1842
1988
  except Exception as e:
1843
1989
  logger.error(f"Failed to get scene config for {scene_id}: {e}")
@@ -1904,15 +2050,24 @@ class HomeAssistantClient:
1904
2050
  raise
1905
2051
 
1906
2052
  async def delete_scene_config(
1907
- self, scene_id: str, *, _resolved: bool = False
2053
+ self,
2054
+ scene_id: str,
2055
+ *,
2056
+ resolution: SceneResolution | None = None,
1908
2057
  ) -> dict[str, Any]:
1909
2058
  """Delete Home Assistant scene configuration.
1910
2059
 
1911
- ``_resolved=True`` signals ``scene_id`` is already the resolved storage
1912
- key, skipping the redundant registry lookup (``resolve_scene_id`` is
1913
- idempotent, so the endpoint id is unchanged).
2060
+ ``resolution`` lets a caller that already ran :meth:`_resolve_scene` pass
2061
+ the full outcome through - the endpoint uses its ``storage_key`` while
2062
+ ``scene_id`` stays the caller's slug for messages, and its ``registry_hit``
2063
+ drives the config-404 classification (issue #1971) with no extra lookup.
2064
+ Otherwise the id is resolved here.
1914
2065
  """
1915
- resolved_id = scene_id if _resolved else await self.resolve_scene_id(scene_id)
2066
+ if resolution is not None:
2067
+ resolved_id = resolution.storage_key
2068
+ else:
2069
+ resolution = await self._resolve_scene(scene_id)
2070
+ resolved_id = resolution.storage_key
1916
2071
  try:
1917
2072
  endpoint = f"config/scene/config/{resolved_id}"
1918
2073
  response = await self._request("DELETE", endpoint)
@@ -1925,10 +2080,7 @@ class HomeAssistantClient:
1925
2080
  }
1926
2081
  except HomeAssistantAPIError as e:
1927
2082
  if e.status_code == 404:
1928
- msg = f"Scene not found: {scene_id}"
1929
- if resolved_id != scene_id:
1930
- msg += f" (resolved storage key: {resolved_id})"
1931
- raise HomeAssistantAPIError(msg, status_code=404) from e
2083
+ await self._raise_scene_config_404(scene_id, resolved_id, resolution)
1932
2084
  elif e.status_code == 405:
1933
2085
  raise HomeAssistantAPIError(
1934
2086
  f"Cannot delete scene '{scene_id}': The HTTP DELETE method is blocked. "
@@ -9,7 +9,7 @@ keyed by entity_id (not a list).
9
9
 
10
10
  import asyncio
11
11
  import logging
12
- from typing import Annotated, Any, cast
12
+ from typing import Annotated, Any, NoReturn, cast
13
13
 
14
14
  from fastmcp.exceptions import ToolError
15
15
  from fastmcp.tools import tool
@@ -19,6 +19,7 @@ from ..client.rest_client import (
19
19
  HomeAssistantAPIError,
20
20
  HomeAssistantAuthError,
21
21
  HomeAssistantConnectionError,
22
+ SceneStorageConfigNotFoundError,
22
23
  )
23
24
  from ..errors import ErrorCode, create_error_response
24
25
  from ..strict_bps import BestPracticeKeyParam
@@ -63,6 +64,50 @@ _SCENE_SKILL_FILES: tuple[str, ...] = ("SKILL.md",)
63
64
  logger = logging.getLogger(__name__)
64
65
 
65
66
 
67
+ def _raise_scene_not_storage_error(scene_id: str, platform: str | None) -> NoReturn:
68
+ """Raise ``CONFIG_NOT_FOUND`` for a scene that exists but has no editable config.
69
+
70
+ The scene entity was confirmed to exist - in the entity registry, or, for an
71
+ ``id``-less YAML scene that has no registry entry, in the state machine - but
72
+ ``config/scene/config/{id}``, backed only by the managed ``scenes.yaml``, has
73
+ no entry for it. That is the not-a-storage-scene case (a Hue/vendor scene, or
74
+ a scene defined directly in YAML): the entity EXISTS, so ``ENTITY_NOT_FOUND``
75
+ would be wrong (issue #1971). ``platform`` names the owning integration when
76
+ the registry exposed it; the message stays generic otherwise.
77
+ """
78
+ entity_id = f"scene.{scene_id.removeprefix('scene.')}"
79
+ suggestions = [
80
+ "Activate it with the scene.turn_on service: "
81
+ f"ha_call_service('scene', 'turn_on', {{'entity_id': '{entity_id}'}}).",
82
+ ]
83
+ if platform and platform != "homeassistant":
84
+ platform_note = (
85
+ f" It is provided by the '{platform}' integration, which manages its "
86
+ "own scenes outside Home Assistant's editable scene storage."
87
+ )
88
+ suggestions.append(
89
+ f"Edit this scene in the '{platform}' integration or its companion app; "
90
+ "ha_config_get_scene / ha_config_set_scene only edit Home Assistant "
91
+ "UI/storage scenes."
92
+ )
93
+ else:
94
+ platform_note = ""
95
+ suggestions.append(
96
+ "ha_config_get_scene / ha_config_set_scene only read and edit Home "
97
+ "Assistant UI/storage scenes (Settings > Automations & Scenes > Scenes). "
98
+ "Scenes defined directly in YAML are not editable through this API."
99
+ )
100
+ raise_tool_error(
101
+ create_error_response(
102
+ ErrorCode.CONFIG_NOT_FOUND,
103
+ f"Scene '{entity_id}' exists but has no editable Home Assistant "
104
+ f"storage configuration.{platform_note}",
105
+ suggestions=suggestions,
106
+ context={"scene_id": scene_id, "platform": platform},
107
+ )
108
+ )
109
+
110
+
66
111
  class ConfigSceneTools:
67
112
  """Scene configuration management tools for Home Assistant."""
68
113
 
@@ -352,6 +397,11 @@ class ConfigSceneTools:
352
397
  return await self._legacy_get_scene(scene_id)
353
398
  except ToolError:
354
399
  raise
400
+ except SceneStorageConfigNotFoundError as e:
401
+ # The entity resolved but scenes.yaml has no config for it - a Hue/
402
+ # vendor or raw-YAML scene. Surface CONFIG_NOT_FOUND, not the
403
+ # ENTITY_NOT_FOUND the generic handler below would emit (#1971).
404
+ _raise_scene_not_storage_error(e.scene_id, e.platform)
355
405
  except Exception as e:
356
406
  # Pass `entity_id` so a 404 from rest_client surfaces as
357
407
  # ENTITY_NOT_FOUND (not the generic RESOURCE_NOT_FOUND fallback).
@@ -746,6 +796,12 @@ class ConfigSceneTools:
746
796
 
747
797
  except ToolError as te:
748
798
  raise augment_tool_error_with_skill_content(te, bp_warnings=None) from None
799
+ except SceneStorageConfigNotFoundError as e:
800
+ # Set on a non-storage scene: the hash-verify / python_transform
801
+ # pre-fetch read 404s because the entity has no editable scenes.yaml
802
+ # config (Hue/vendor or raw-YAML). Same CONFIG_NOT_FOUND
803
+ # classification as get/delete (#1971), not a generic write failure.
804
+ _raise_scene_not_storage_error(e.scene_id, e.platform)
749
805
  except Exception as e:
750
806
  error = exception_to_structured_error(
751
807
  e,
@@ -1079,18 +1135,36 @@ class ConfigSceneTools:
1079
1135
  # so subsequent upsert/response builders use the storage key
1080
1136
  # consistently (issue #1168 R3 blocker 6).
1081
1137
  #
1082
- # Path branching: if a hash is supplied for a non-existent
1083
- # scene, the inner fetch raises 404 and surfaces as
1084
- # ENTITY_NOT_FOUND via the outer except which is the right
1085
- # caller-facing semantics ("you can't lock against a scene
1086
- # that doesn't exist"). The no-hash branch resolves separately
1087
- # so a fresh create still threads the resolved id correctly.
1138
+ # Path branching: the hash arm's inner fetch reads the config
1139
+ # first, so a non-storage scene (Hue/vendor, raw-YAML) surfaces
1140
+ # as CONFIG_NOT_FOUND and a genuinely-missing scene as the bare
1141
+ # 404 you cannot lock against a config that isn't there. The
1142
+ # no-hash arm has no such read, so it resolves richly and, when
1143
+ # the entity already exists, pre-checks the config API below
1144
+ # before the upsert (issue #1971) so a plain ``set`` on a
1145
+ # non-storage scene cannot silently shadow-create a duplicate.
1088
1146
  if config_hash:
1089
1147
  _, resolved_id = await self._fetch_and_verify_hash(
1090
1148
  scene_id, config_hash, "set"
1091
1149
  )
1092
1150
  else:
1093
- resolved_id = await self._client.resolve_scene_id(scene_id)
1151
+ resolution = await self._client._resolve_scene(scene_id)
1152
+ resolved_id = resolution.storage_key
1153
+ # If the scene already exists this is an EDIT, and without a hash
1154
+ # there is no prior read: a POST to ``config/scene/config/<key>``
1155
+ # for a Hue/vendor or YAML scene would CREATE a duplicate managed
1156
+ # ``scenes.yaml`` entry instead of erroring. Pre-check the config
1157
+ # API first – its 404 raises SceneStorageConfigNotFoundError, which
1158
+ # the outer handler maps to CONFIG_NOT_FOUND.
1159
+ #
1160
+ # Existence takes both signals: an ``id``-less YAML scene has no
1161
+ # registry unique_id yet still holds a state-machine entity, and
1162
+ # shadow-creates just the same. Only a scene missing from both is a
1163
+ # real create, at the price of one extra ``/states/`` GET there.
1164
+ if resolution.registry_hit or await self._client._scene_state_exists(
1165
+ resolved_id
1166
+ ):
1167
+ await self._client.get_scene_config(scene_id, resolution=resolution)
1094
1168
 
1095
1169
  # Cross-check literal service and entity references against the
1096
1170
  # live registries. Soft warnings only.
@@ -1229,8 +1303,12 @@ class ConfigSceneTools:
1229
1303
  # callsite (entity_id resolver, delete call, response) uses the
1230
1304
  # storage key consistently — outer ``scene_id`` matches the
1231
1305
  # inner body regardless of whether the caller passed the
1232
- # entity_id slug or the storage key.
1233
- resolved_id = await self._client.resolve_scene_id(scene_id)
1306
+ # entity_id slug or the storage key. The rich resolution also
1307
+ # carries ``registry_hit``/``platform`` so a config-API 404 on a
1308
+ # non-storage scene (Hue/vendor, raw-YAML) classifies as
1309
+ # CONFIG_NOT_FOUND rather than the misleading generic 404 (#1971).
1310
+ resolution = await self._client._resolve_scene(scene_id)
1311
+ resolved_id = resolution.storage_key
1234
1312
 
1235
1313
  # Resolve actual entity_id BEFORE delete — once the registry
1236
1314
  # entry is gone, the unique_id lookup can no longer find it.
@@ -1240,9 +1318,11 @@ class ConfigSceneTools:
1240
1318
  resolved_id, allow_component=True
1241
1319
  )
1242
1320
 
1243
- # ``resolved_id`` is already the storage key (resolved above), so
1244
- # skip the redundant re-resolve inside delete_scene_config.
1245
- result = await self._client.delete_scene_config(resolved_id, _resolved=True)
1321
+ # Thread the resolution through so delete_scene_config reuses the
1322
+ # storage key (no redundant re-resolve) and can classify a 404.
1323
+ result = await self._client.delete_scene_config(
1324
+ scene_id, resolution=resolution
1325
+ )
1246
1326
 
1247
1327
  if wait:
1248
1328
  try:
@@ -1264,6 +1344,10 @@ class ConfigSceneTools:
1264
1344
  }
1265
1345
  except ToolError:
1266
1346
  raise
1347
+ except SceneStorageConfigNotFoundError as e:
1348
+ # Entity resolved but scenes.yaml has no config for it - a Hue/
1349
+ # vendor or raw-YAML scene, not a genuinely missing entity (#1971).
1350
+ _raise_scene_not_storage_error(e.scene_id, e.platform)
1267
1351
  except Exception as e:
1268
1352
  exception_to_structured_error(
1269
1353
  e,