ha-mcp-dev 7.14.2.dev2100__tar.gz → 7.14.2.dev2105__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.dev2100/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.14.2.dev2105}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/backup_manager.py +26 -7
  4. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/client/rest_client.py +180 -28
  5. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/config.py +56 -0
  6. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/locales/de.json +2 -0
  7. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/locales/en.json +2 -0
  8. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/locales/ru.json +2 -0
  9. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/settings.js +22 -11
  10. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_config_scenes.py +97 -13
  11. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_filesystem.py +142 -0
  12. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_yaml_config.py +11 -1
  13. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  14. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/LICENSE +0 -0
  15. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/MANIFEST.in +0 -0
  16. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/README.md +0 -0
  17. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/setup.cfg +0 -0
  18. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/__init__.py +0 -0
  19. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/__main__.py +0 -0
  20. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/_pypi_marker +0 -0
  21. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/_version.py +0 -0
  22. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/auth/__init__.py +0 -0
  23. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/auth/consent_form.py +0 -0
  24. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/auth/provider.py +0 -0
  25. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/browser_landing.py +0 -0
  26. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/client/__init__.py +0 -0
  27. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/client/supervisor_client.py +0 -0
  28. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/client/websocket_client.py +0 -0
  29. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/client/websocket_listener.py +0 -0
  30. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  31. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  32. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  33. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  34. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  35. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  36. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/errors.py +0 -0
  37. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/llm_exposure.py +0 -0
  38. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/policy/__init__.py +0 -0
  39. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/policy/approval_queue.py +0 -0
  40. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/policy/evaluator.py +0 -0
  41. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/policy/handlers.py +0 -0
  42. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/policy/middleware.py +0 -0
  43. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/policy/model.py +0 -0
  44. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/policy/persistence.py +0 -0
  45. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/policy/value_sources.py +0 -0
  46. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/py.typed +0 -0
  47. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/read_only.py +0 -0
  48. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  49. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  50. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  51. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  52. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  53. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  54. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  55. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  56. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  57. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  58. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  59. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  60. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  61. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  62. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  63. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  64. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  65. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  66. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  67. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  68. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  69. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  70. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  71. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  72. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  73. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/server.py +0 -0
  74. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/__init__.py +0 -0
  75. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  76. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  77. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  78. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  79. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  80. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  81. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  82. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  83. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  84. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/_theme.py +0 -0
  85. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  86. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  87. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
  88. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/settings.css +0 -0
  89. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/settings_ui/settings.html +0 -0
  90. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/smoke_test.py +0 -0
  91. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  92. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/strict_bps.py +0 -0
  93. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/__init__.py +0 -0
  94. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/auto_backup.py +0 -0
  95. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/backup.py +0 -0
  96. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  97. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/component_api.py +0 -0
  98. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/component_config_reads.py +0 -0
  99. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/component_devices.py +0 -0
  100. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/component_registries.py +0 -0
  101. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  102. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  103. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/device_control.py +0 -0
  104. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/enhanced.py +0 -0
  105. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/helpers.py +0 -0
  106. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/radio/__init__.py +0 -0
  107. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/radio/base.py +0 -0
  108. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/radio/matter.py +0 -0
  109. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/radio/thread.py +0 -0
  110. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  111. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/radio/zwave.py +0 -0
  112. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/reference_validator.py +0 -0
  113. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/registry.py +0 -0
  114. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  115. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  116. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  117. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  118. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  119. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  120. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  121. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  122. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  123. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  124. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_addons.py +0 -0
  125. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_areas.py +0 -0
  126. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  127. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  128. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_calendar.py +0 -0
  129. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_camera.py +0 -0
  130. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_categories.py +0 -0
  131. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_code.py +0 -0
  132. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  133. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  134. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  135. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  136. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  137. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_dev.py +0 -0
  138. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_energy.py +0 -0
  139. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_entities.py +0 -0
  140. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_groups.py +0 -0
  141. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_hacs.py +0 -0
  142. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_history.py +0 -0
  143. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_integrations.py +0 -0
  144. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_labels.py +0 -0
  145. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_radio.py +0 -0
  146. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_registry.py +0 -0
  147. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_resources.py +0 -0
  148. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_search.py +0 -0
  149. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_service.py +0 -0
  150. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_services.py +0 -0
  151. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_system.py +0 -0
  152. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_themes.py +0 -0
  153. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_todo.py +0 -0
  154. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_traces.py +0 -0
  155. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_updates.py +0 -0
  156. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_utility.py +0 -0
  157. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  158. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  159. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/tools_zones.py +0 -0
  160. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/util_helpers.py +0 -0
  161. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/tools/validation_middleware.py +0 -0
  162. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/transforms/__init__.py +0 -0
  163. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/transforms/categorized_search.py +0 -0
  164. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  165. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/transport_security.py +0 -0
  166. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/update_check.py +0 -0
  167. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/utils/__init__.py +0 -0
  168. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/utils/config_hash.py +0 -0
  169. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/utils/data_paths.py +0 -0
  170. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/utils/domain_handlers.py +0 -0
  171. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  172. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  173. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/utils/operation_manager.py +0 -0
  174. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/utils/python_sandbox.py +0 -0
  175. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/utils/skill_loader.py +0 -0
  176. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/utils/usage_logger.py +0 -0
  177. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/visibility/__init__.py +0 -0
  178. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/visibility/model.py +0 -0
  179. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/visibility/persistence.py +0 -0
  180. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp/visibility/resolver.py +0 -0
  181. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  182. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  183. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  184. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  185. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  186. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/tests/__init__.py +0 -0
  187. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/tests/test_constants.py +0 -0
  188. {ha_mcp_dev-7.14.2.dev2100 → ha_mcp_dev-7.14.2.dev2105}/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.dev2100
3
+ Version: 7.14.2.dev2105
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.dev2100"
7
+ version = "7.14.2.dev2105"
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
@@ -2128,8 +2128,19 @@ async def _restore_yaml(client: Any, entity_id: str, config: Any) -> Any:
2128
2128
  ``edit_yaml_config`` is the only write path that reaches HA config
2129
2129
  files (``write_file`` rejects them), so YAML restore goes through it
2130
2130
  with ``action="replace"``.
2131
+
2132
+ The operator's extra write keys (#1887) must ride along: the write that
2133
+ produced this snapshot was allowed only because of them, so omitting
2134
+ them here would make an auto-captured backup unrestorable. Note the
2135
+ asymmetry with ``disabled_packages_keys``, whose default is permissive
2136
+ and can therefore be left off.
2131
2137
  """
2132
- from .tools.tools_filesystem import call_mcp_tools_service
2138
+ from .config import get_global_settings
2139
+ from .tools.tools_filesystem import (
2140
+ assert_extra_yaml_keys_supported,
2141
+ call_mcp_tools_service,
2142
+ effective_extra_yaml_write_keys,
2143
+ )
2133
2144
  from .tools.util_helpers import unwrap_service_response
2134
2145
 
2135
2146
  # Split on the LAST "::" (see _fetch_yaml) so an exotic file path
@@ -2137,15 +2148,23 @@ async def _restore_yaml(client: Any, entity_id: str, config: Any) -> Any:
2137
2148
  file, sep, yaml_path = entity_id.rpartition("::")
2138
2149
  if not sep or not file or not yaml_path:
2139
2150
  raise ValueError(f"Invalid yaml snapshot target: {entity_id!r}")
2151
+ service_data: dict[str, Any] = {
2152
+ "file": file,
2153
+ "action": "replace",
2154
+ "yaml_path": yaml_path,
2155
+ "content": str(config),
2156
+ }
2157
+ extra_keys = await effective_extra_yaml_write_keys(client, get_global_settings())
2158
+ if extra_keys:
2159
+ # Same version gate as the write path: without it a component that
2160
+ # predates the field rejects the whole restore call over an option
2161
+ # the snapshot being restored may not even use.
2162
+ await assert_extra_yaml_keys_supported(client, extra_keys)
2163
+ service_data["extra_allowed_keys"] = extra_keys
2140
2164
  result = await call_mcp_tools_service(
2141
2165
  client,
2142
2166
  "edit_yaml_config",
2143
- {
2144
- "file": file,
2145
- "action": "replace",
2146
- "yaml_path": yaml_path,
2147
- "content": str(config),
2148
- },
2167
+ service_data,
2149
2168
  )
2150
2169
  if isinstance(result, dict):
2151
2170
  result = unwrap_service_response(result)
@@ -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. "
@@ -203,6 +203,25 @@ class Settings(BaseSettings):
203
203
  )
204
204
  enable_yaml_packages_scene: bool = Field(False, alias="ENABLE_YAML_PACKAGES_SCENE")
205
205
 
206
+ # Operator-configured extra top-level keys ha_config_set_yaml may write,
207
+ # comma-separated, on top of the custom component's built-in allowlist
208
+ # (#1887). For YAML-first integrations that are valid on one install but
209
+ # not worth hardcoding globally. Additive only, and never a way past the
210
+ # component's YAML_KEY_DENYLIST: that floor is enforced component-side
211
+ # (the authoritative layer) and is deliberately not mirrored here, so
212
+ # there is one copy to keep correct. A denied key typed into this setting
213
+ # is simply ignored, with the component's explanation on first use.
214
+ # Nor does it lift the packages-only restriction on automation/script/
215
+ # scene: those keep reaching packages/*.yaml through their own per-key
216
+ # toggles and stay rejected in configuration.yaml.
217
+ # Empty (the default) keeps today's behaviour exactly.
218
+ # Registered in ADVANCED_SETTINGS_FIELDS (section ``beta_yamlkeys``), not
219
+ # FEATURE_FLAG_FIELDS, because it is a value rather than a toggle – the
220
+ # same placement the code-mode sub-settings use. Meaningful only when
221
+ # ``enable_yaml_config_editing`` is on; the UI nests it under that parent
222
+ # like the per-key toggles above.
223
+ extra_yaml_write_keys: str = Field("", alias="HA_MCP_EXTRA_YAML_KEYS")
224
+
206
225
  # Seed values for tool visibility (comma-separated tool names).
207
226
  # Used as initial config when no tool_config.json exists.
208
227
  # The web settings UI (/settings) is the primary interface for managing these.
@@ -621,6 +640,7 @@ AdvancedSection = Literal[
621
640
  "tools_surface",
622
641
  "sidecar",
623
642
  "beta_codemode",
643
+ "beta_yamlkeys",
624
644
  "developer",
625
645
  ]
626
646
 
@@ -896,6 +916,18 @@ ADVANCED_SETTINGS_FIELDS: tuple[AdvancedField, ...] = (
896
916
  "beta_codemode",
897
917
  True,
898
918
  ),
919
+ # Extra YAML write keys (issue #1887). Same shape as the code-mode
920
+ # sub-settings above: a non-bool value that belongs visually under a
921
+ # feature toggle rather than in the advanced panel, so it lives here
922
+ # with its own section and the features renderer nests it beneath
923
+ # "Enable YAML config editing".
924
+ AdvancedField(
925
+ "extra_yaml_write_keys",
926
+ "HA_MCP_EXTRA_YAML_KEYS",
927
+ str,
928
+ "beta_yamlkeys",
929
+ True,
930
+ ),
899
931
  # Developer mode (issue #1775). Lives in ADVANCED_SETTINGS_FIELDS —
900
932
  # not FEATURE_FLAG_FIELDS — so it renders in its own "Developer"
901
933
  # section at the very bottom of the Server Settings tab instead of
@@ -1185,6 +1217,30 @@ def _apply_one_feature_flag_override(
1185
1217
  )
1186
1218
 
1187
1219
 
1220
+ def parse_extra_yaml_write_keys(settings: "Settings") -> list[str]:
1221
+ """Parse ``extra_yaml_write_keys`` into a clean key list (#1887).
1222
+
1223
+ Whitespace and empty entries are dropped and the result is deduplicated
1224
+ and sorted, so the service payload is deterministic and unaffected by how
1225
+ the operator spaced the setting.
1226
+
1227
+ The component's ``YAML_KEY_DENYLIST`` is NOT applied here: that floor
1228
+ lives component-side, in the layer that authorizes the write. Mirroring it
1229
+ would mean a second copy to keep in lockstep for no gain: a denied key sent
1230
+ on the wire is dropped there anyway.
1231
+
1232
+ Lives in this module rather than next to the YAML tool because the backup
1233
+ restore path needs it too, and ``backup_manager`` importing a ``tools_*``
1234
+ module would add an import edge that binds ``tools_yaml_config``'s
1235
+ module-level names at restore time.
1236
+ """
1237
+ # Direct attribute access, matching ``_disabled_packages_keys``: a future
1238
+ # rename must raise loudly rather than silently return an empty list,
1239
+ # which would read as "the operator configured nothing".
1240
+ raw = settings.extra_yaml_write_keys or ""
1241
+ return sorted({segment.strip() for segment in raw.split(",") if segment.strip()})
1242
+
1243
+
1188
1244
  def _apply_beta_master_gate(settings: "Settings") -> None:
1189
1245
  """Force BETA_FEATURE_FIELDS to False when ``enable_beta_features`` is off.
1190
1246
 
@@ -412,6 +412,8 @@
412
412
  "advanced.code_mode_max_invocations.help": "API-/Tool-Aufruf-Grenze pro Sandbox-Lauf. Neustart erforderlich.",
413
413
  "advanced.code_mode_saved_tools_path.label": "Pfad für gespeicherte Tools",
414
414
  "advanced.code_mode_saved_tools_path.help": "JSON-Datei, die verwendet wird, um benutzerdefinierte Tools über Neustarts hinweg beizubehalten. Neustart erforderlich.",
415
+ "advanced.extra_yaml_write_keys.label": "Zusätzliche YAML-Schreib-Keys",
416
+ "advanced.extra_yaml_write_keys.help": "Kommagetrennte Top-Level-Keys, die ha_config_set_yaml zusätzlich zu den eingebauten schreiben darf – für YAML-first-Integrationen auf dieser Installation (z. B. alert2). Keys, die die Vertrauensgrenze von Home Assistant selbst verändern, lassen sich nie hinzufügen und werden ignoriert. Erfordert Custom Component 1.2.4 oder neuer.",
415
417
  "advanced.sidecar_pin_port.label": "Einstellungs-UI-Sidecar-Port",
416
418
  "advanced.sidecar_pin_port.help": "0 wählt bei jedem Neustart einen freien Port; 1024-65535 heftet einen bevorzugten Port an. Neustart erforderlich.",
417
419
  "advanced.enable_dev_mode.label": "Entwicklermodus",
@@ -412,6 +412,8 @@
412
412
  "advanced.code_mode_max_invocations.help": "API/tool-call cap per sandbox run. Restart required.",
413
413
  "advanced.code_mode_saved_tools_path.label": "Saved-tools path",
414
414
  "advanced.code_mode_saved_tools_path.help": "JSON file used to persist custom tools across restarts. Restart required.",
415
+ "advanced.extra_yaml_write_keys.label": "Extra YAML write keys",
416
+ "advanced.extra_yaml_write_keys.help": "Comma-separated top-level keys ha_config_set_yaml may write in addition to the built-in ones, for YAML-first integrations on this install (e.g. alert2). Keys that redefine Home Assistant's own trust boundary can never be added and are ignored. Requires custom component 1.2.4 or newer.",
415
417
  "advanced.sidecar_pin_port.label": "Settings UI sidecar port",
416
418
  "advanced.sidecar_pin_port.help": "0 chooses a free port each restart; 1024–65535 pins a preferred port. Restart required.",
417
419
  "advanced.enable_dev_mode.label": "Developer mode",
@@ -412,6 +412,8 @@
412
412
  "advanced.code_mode_max_invocations.help": "Лимит вызовов API и инструментов за один запуск. Нужен перезапуск.",
413
413
  "advanced.code_mode_saved_tools_path.label": "Путь к сохранённым инструментам",
414
414
  "advanced.code_mode_saved_tools_path.help": "JSON-файл для хранения пользовательских инструментов между перезапусками. Нужен перезапуск.",
415
+ "advanced.extra_yaml_write_keys.label": "Дополнительные YAML-ключи для записи",
416
+ "advanced.extra_yaml_write_keys.help": "Список верхнеуровневых ключей через запятую, которые ha_config_set_yaml может записывать вдобавок к встроенным – для YAML-ориентированных интеграций на этой установке (например, alert2). Ключи, меняющие саму границу доверия Home Assistant, добавить нельзя, они игнорируются. Требуется пользовательский компонент 1.2.4 или новее.",
415
417
  "advanced.sidecar_pin_port.label": "Порт sidecar интерфейса настроек",
416
418
  "advanced.sidecar_pin_port.help": "0 выбирает свободный порт при каждом запуске; 1024–65535 закрепляет желаемый порт. Нужен перезапуск.",
417
419
  "advanced.enable_dev_mode.label": "Режим разработчика",
@@ -2167,7 +2167,9 @@ function renderFeatureFlags(flags) {
2167
2167
  // bottom.
2168
2168
  if (fieldName === 'enable_code_mode') {
2169
2169
  const codeModeOn = !!f.value;
2170
- renderCodeModeSubRows(targetBody, masterOn, codeModeOn);
2170
+ renderAdvancedSubRows(
2171
+ targetBody, 'beta_codemode', 'codemode-sub', !masterOn || !codeModeOn
2172
+ );
2171
2173
  }
2172
2174
  // After rendering the enable_yaml_config_editing parent, inject
2173
2175
  // its 3 per-key sub-rows (automation/script/scene). Dimmed when
@@ -2179,6 +2181,11 @@ function renderFeatureFlags(flags) {
2179
2181
  cssClass: 'yaml-packages-sub',
2180
2182
  lockedByGate: !masterOn || !parentOn,
2181
2183
  });
2184
+ // Extra write keys (#1887) – same nesting depth as the per-key
2185
+ // toggles above, but a text value from the advanced cache.
2186
+ renderAdvancedSubRows(
2187
+ targetBody, 'beta_yamlkeys', 'yaml-packages-sub', !masterOn || !parentOn
2188
+ );
2182
2189
  }
2183
2190
  // After the enable_mandatory_bps parent row, inject its strict-mode
2184
2191
  // sub-row. This whole group is non-beta, so the only gate is the
@@ -2210,7 +2217,7 @@ function renderFeatureFlags(flags) {
2210
2217
  // master beta AND parent; mandatory-bps: parent only) and pass the CSS
2211
2218
  // class that carries the group's indent/guide-bar depth. The number/text
2212
2219
  // code-mode sub-numerics are NOT rendered here — they save through
2213
- // commitAdvancedEdit and live in renderCodeModeSubRows.
2220
+ // commitAdvancedEdit and live in renderAdvancedSubRows.
2214
2221
  function renderSubFlagRows(flags, parentEl, subFieldNames, { cssClass, lockedByGate }) {
2215
2222
  subFieldNames.forEach(fieldName => {
2216
2223
  const f = flags[fieldName];
@@ -2276,8 +2283,13 @@ function renderSubFlagRows(flags, parentEl, subFieldNames, { cssClass, lockedByG
2276
2283
  });
2277
2284
  }
2278
2285
 
2279
- function renderCodeModeSubRows(parentEl, masterOn, codeModeOn) {
2280
- const cmRows = (_advancedFields || []).filter(x => x.section === 'beta_codemode');
2286
+ // Shared renderer for non-bool sub-rows that live in ADVANCED_SETTINGS_FIELDS
2287
+ // but render nested under a feature toggle (code-mode numerics under
2288
+ // enable_code_mode, extra YAML write keys under enable_yaml_config_editing).
2289
+ // They save through commitAdvancedEdit rather than saveFeatureFlag, which is
2290
+ // why renderSubFlagRows (checkbox-only) cannot serve them.
2291
+ function renderAdvancedSubRows(parentEl, section, cssClass, lockedByGate) {
2292
+ const cmRows = (_advancedFields || []).filter(x => x.section === section);
2281
2293
  cmRows.forEach(f => {
2282
2294
  const meta = localizeMeta(
2283
2295
  'advanced',
@@ -2285,9 +2297,7 @@ function renderCodeModeSubRows(parentEl, masterOn, codeModeOn) {
2285
2297
  ADVANCED_FIELD_META[f.field] || { label: f.field, help: '' }
2286
2298
  );
2287
2299
  const row = document.createElement('div');
2288
- const lockedByGate = !masterOn || !codeModeOn;
2289
- const dimmed = lockedByGate;
2290
- row.className = 'feature-row codemode-sub' + (dimmed ? ' dimmed' : '');
2300
+ row.className = 'feature-row ' + cssClass + (lockedByGate ? ' dimmed' : '');
2291
2301
 
2292
2302
  const info = document.createElement('div');
2293
2303
  info.className = 'feature-info';
@@ -3390,6 +3400,7 @@ const ADVANCED_FIELD_META = {
3390
3400
  code_mode_max_recursion: { label: "Code-mode max recursion", help: "Recursion-depth cap per sandbox run. Restart required." },
3391
3401
  code_mode_max_invocations: { label: "Code-mode max invocations", help: "API/tool-call cap per sandbox run. Restart required." },
3392
3402
  code_mode_saved_tools_path:{ label: "Saved-tools path", help: "JSON file where ha_manage_custom_tool persists saved tools across restarts. Restart required." },
3403
+ extra_yaml_write_keys: { label: "Extra YAML write keys", help: "Comma-separated top-level keys ha_config_set_yaml may write in addition to the built-in ones, for YAML-first integrations on this install (e.g. alert2). Keys that redefine Home Assistant's own trust boundary can never be added and are ignored. Requires custom component 1.2.4 or newer." },
3393
3404
  sidecar_pin_port: { label: "Settings UI sidecar port", help: "0 = a new free port each restart (default); set 1024–65535 to pin a fixed port so the settings URL stays stable across restarts. Falls back to a free port if the pinned one is busy. Restart required." },
3394
3405
  enable_dev_mode: { label: "Developer mode", help: "⚠ DANGER: registers hidden developer tools (ha_dev_manage_server, ha_dev_manage_settings) that let AI agents change server settings and replace the running server version (e.g. install a PR build). For development and testing only. Restart required." },
3395
3406
  };
@@ -3493,10 +3504,10 @@ async function loadAdvancedSettings() {
3493
3504
  renderAdvancedSection('advSidecar', bySection.sidecar || []);
3494
3505
  renderAdvancedSection('advDeveloper', bySection.developer || []);
3495
3506
  applySidecarAvailability(data.is_stdio !== false);
3496
- // Re-render feature flags so the code_mode sub-numerics show up
3497
- // beneath enable_code_mode (race: loadFeatureFlags may have run
3498
- // before _advancedFields was populated). Cheap no-op if feature
3499
- // flags haven't loaded yet.
3507
+ // Re-render feature flags so the advanced-backed sub-rows (code_mode
3508
+ // numerics, extra YAML write keys) show up beneath their parent toggles
3509
+ // (race: loadFeatureFlags may have run before _advancedFields was
3510
+ // populated). Cheap no-op if feature flags haven't loaded yet.
3500
3511
  if (Object.keys(_lastFeatureFlags).length > 0) {
3501
3512
  renderFeatureFlags(_lastFeatureFlags);
3502
3513
  }