ha-mcp-dev 7.13.0.dev2007__tar.gz → 7.13.0.dev2011__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. {ha_mcp_dev-7.13.0.dev2007/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev2011}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/client/rest_client.py +69 -19
  4. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/component_api.py +9 -1
  5. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_config_automations.py +63 -10
  6. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_config_scenes.py +50 -16
  7. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_config_scripts.py +64 -16
  8. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_dev.py +141 -0
  9. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_traces.py +99 -90
  10. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/util_helpers.py +3 -2
  11. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  12. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/LICENSE +0 -0
  13. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/MANIFEST.in +0 -0
  14. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/README.md +0 -0
  15. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/setup.cfg +0 -0
  16. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/__init__.py +0 -0
  17. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/__main__.py +0 -0
  18. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/_pypi_marker +0 -0
  19. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/_version.py +0 -0
  20. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/auth/__init__.py +0 -0
  21. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/auth/consent_form.py +0 -0
  22. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/auth/provider.py +0 -0
  23. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/backup_manager.py +0 -0
  24. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/browser_landing.py +0 -0
  25. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/client/__init__.py +0 -0
  26. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/client/supervisor_client.py +0 -0
  27. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/client/websocket_client.py +0 -0
  28. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/client/websocket_listener.py +0 -0
  29. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/config.py +0 -0
  30. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  31. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  32. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  33. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  34. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  35. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  36. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/errors.py +0 -0
  37. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/llm_exposure.py +0 -0
  38. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/policy/__init__.py +0 -0
  39. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/policy/approval_queue.py +0 -0
  40. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/policy/evaluator.py +0 -0
  41. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/policy/handlers.py +0 -0
  42. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/policy/middleware.py +0 -0
  43. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/policy/model.py +0 -0
  44. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/policy/persistence.py +0 -0
  45. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/policy/value_sources.py +0 -0
  46. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/py.typed +0 -0
  47. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/read_only.py +0 -0
  48. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  49. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  50. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  51. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  52. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  53. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  54. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  55. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  56. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  57. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  58. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  59. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  60. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  61. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  62. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  63. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  64. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  65. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  66. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  67. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  68. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  69. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  70. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  71. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  72. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  73. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/server.py +0 -0
  74. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/__init__.py +0 -0
  75. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  76. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  77. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  78. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  79. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  80. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  81. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  82. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  83. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  84. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_theme.py +0 -0
  85. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  86. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  87. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  88. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  89. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  90. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/settings.css +0 -0
  91. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/settings.html +0 -0
  92. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/settings.js +0 -0
  93. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/smoke_test.py +0 -0
  94. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  95. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/strict_bps.py +0 -0
  96. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/__init__.py +0 -0
  97. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/auto_backup.py +0 -0
  98. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/backup.py +0 -0
  99. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  100. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/component_config_reads.py +0 -0
  101. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/component_devices.py +0 -0
  102. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/component_registries.py +0 -0
  103. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  104. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  105. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/device_control.py +0 -0
  106. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/enhanced.py +0 -0
  107. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/helpers.py +0 -0
  108. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/radio/__init__.py +0 -0
  109. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/radio/base.py +0 -0
  110. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/radio/matter.py +0 -0
  111. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/radio/thread.py +0 -0
  112. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  113. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/radio/zwave.py +0 -0
  114. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/reference_validator.py +0 -0
  115. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/registry.py +0 -0
  116. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  117. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  118. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  119. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  120. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  121. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  122. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  123. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  124. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  125. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  126. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_addons.py +0 -0
  127. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_areas.py +0 -0
  128. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  129. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  130. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_calendar.py +0 -0
  131. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_camera.py +0 -0
  132. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_categories.py +0 -0
  133. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_code.py +0 -0
  134. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  135. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  136. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  137. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_energy.py +0 -0
  138. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_entities.py +0 -0
  139. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  140. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_groups.py +0 -0
  141. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_hacs.py +0 -0
  142. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_history.py +0 -0
  143. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_integrations.py +0 -0
  144. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_labels.py +0 -0
  145. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_radio.py +0 -0
  146. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_registry.py +0 -0
  147. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_resources.py +0 -0
  148. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_search.py +0 -0
  149. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_service.py +0 -0
  150. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_services.py +0 -0
  151. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_system.py +0 -0
  152. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_themes.py +0 -0
  153. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_todo.py +0 -0
  154. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_updates.py +0 -0
  155. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_utility.py +0 -0
  156. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  157. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  158. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  159. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_zones.py +0 -0
  160. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/validation_middleware.py +0 -0
  161. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/transforms/__init__.py +0 -0
  162. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/transforms/categorized_search.py +0 -0
  163. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  164. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/transport_security.py +0 -0
  165. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/update_check.py +0 -0
  166. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/__init__.py +0 -0
  167. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/config_hash.py +0 -0
  168. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/data_paths.py +0 -0
  169. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/domain_handlers.py +0 -0
  170. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  171. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  172. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/operation_manager.py +0 -0
  173. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/python_sandbox.py +0 -0
  174. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/skill_loader.py +0 -0
  175. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/usage_logger.py +0 -0
  176. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/visibility/__init__.py +0 -0
  177. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/visibility/model.py +0 -0
  178. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/visibility/persistence.py +0 -0
  179. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/visibility/resolver.py +0 -0
  180. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  181. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  182. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  183. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  184. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  185. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/tests/__init__.py +0 -0
  186. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/tests/test_constants.py +0 -0
  187. {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.13.0.dev2007
3
+ Version: 7.13.0.dev2011
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.13.0.dev2007"
7
+ version = "7.13.0.dev2011"
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
@@ -975,7 +975,11 @@ class HomeAssistantClient:
975
975
  raise
976
976
 
977
977
  async def upsert_automation_config(
978
- self, config: dict[str, Any], identifier: str | None = None
978
+ self,
979
+ config: dict[str, Any],
980
+ identifier: str | None = None,
981
+ *,
982
+ _resolved: bool = False,
979
983
  ) -> dict[str, Any]:
980
984
  """
981
985
  Create new automation or update existing one.
@@ -983,6 +987,12 @@ class HomeAssistantClient:
983
987
  Args:
984
988
  config: Automation configuration dictionary
985
989
  identifier: Optional automation entity_id or unique_id (None = create new)
990
+ _resolved: When True, ``identifier`` is already the resolved unique_id
991
+ (the caller ran ``_resolve_automation_id``), so the redundant
992
+ entity_id→unique_id lookup is skipped. Ignored on the create path
993
+ (``identifier is None``). The #1404 config-id mismatch guard below
994
+ is unaffected — it still compares ``config['id']`` to the resolved
995
+ unique_id.
986
996
 
987
997
  Returns:
988
998
  Result with automation unique_id and status
@@ -996,7 +1006,11 @@ class HomeAssistantClient:
996
1006
  operation = "created"
997
1007
  logger.debug(f"Creating new automation with unique_id: {unique_id}")
998
1008
  else:
999
- unique_id = await self._resolve_automation_id(identifier)
1009
+ unique_id = (
1010
+ identifier
1011
+ if _resolved
1012
+ else await self._resolve_automation_id(identifier)
1013
+ )
1000
1014
  operation = "updated"
1001
1015
  logger.debug(f"Updating automation with unique_id: {unique_id}")
1002
1016
 
@@ -1592,14 +1606,33 @@ class HomeAssistantClient:
1592
1606
  raise
1593
1607
 
1594
1608
  async def upsert_script_config(
1595
- self, config: dict[str, Any], script_id: str
1609
+ self,
1610
+ config: dict[str, Any],
1611
+ script_id: str,
1612
+ *,
1613
+ resolved_id: str | None = None,
1596
1614
  ) -> dict[str, Any]:
1597
- """Create or update Home Assistant script configuration."""
1598
- resolved_id = await self._resolve_script_id(script_id)
1615
+ """Create or update Home Assistant script configuration.
1616
+
1617
+ ``resolved_id``, when provided, is the already-resolved storage key used
1618
+ as the write target (the caller ran :meth:`_resolve_script_id`),
1619
+ skipping the redundant entity-registry lookup. ``script_id`` stays the
1620
+ CALLER's identifier and is used only to default a missing ``alias`` — a
1621
+ renamed script (whose storage key differs from the entity slug the
1622
+ caller passed) keeps its caller-facing name rather than resetting the
1623
+ user-visible alias to the stale storage key (#1935).
1624
+ """
1625
+ write_id = (
1626
+ resolved_id
1627
+ if resolved_id is not None
1628
+ else await self._resolve_script_id(script_id)
1629
+ )
1599
1630
  try:
1600
- endpoint = f"config/script/config/{resolved_id}"
1631
+ endpoint = f"config/script/config/{write_id}"
1601
1632
 
1602
- # Validate required fields
1633
+ # Default a missing alias from the CALLER's ``script_id`` (not
1634
+ # ``write_id``) so a renamed script's user-visible name is preserved
1635
+ # instead of being reset to the storage key.
1603
1636
  if "alias" not in config:
1604
1637
  config["alias"] = script_id
1605
1638
 
@@ -1614,7 +1647,7 @@ class HomeAssistantClient:
1614
1647
 
1615
1648
  return {
1616
1649
  "success": True,
1617
- "script_id": resolved_id,
1650
+ "script_id": write_id,
1618
1651
  "result": response.get("result", "ok"),
1619
1652
  "operation": "created" if response.get("result") == "ok" else "updated",
1620
1653
  }
@@ -1722,22 +1755,39 @@ class HomeAssistantClient:
1722
1755
  raise
1723
1756
 
1724
1757
  async def upsert_scene_config(
1725
- self, config: dict[str, Any], scene_id: str, *, _resolved: bool = False
1758
+ self,
1759
+ config: dict[str, Any],
1760
+ scene_id: str,
1761
+ *,
1762
+ resolved_id: str | None = None,
1726
1763
  ) -> dict[str, Any]:
1727
1764
  """Create or update Home Assistant scene configuration.
1728
1765
 
1729
- ``_resolved=True`` signals ``scene_id`` is already the resolved storage
1730
- key, skipping the redundant registry lookup (``resolve_scene_id`` is
1731
- idempotent, so the endpoint id is unchanged).
1766
+ ``resolved_id``, when provided, is the already-resolved storage key used
1767
+ as the write target (the caller ran :meth:`resolve_scene_id`), skipping
1768
+ the redundant registry lookup. ``scene_id`` stays the CALLER's identifier
1769
+ and is used only to default a missing ``name`` — a renamed scene (whose
1770
+ storage key differs from the slug the caller passed) keeps its
1771
+ caller-facing name rather than resetting to the stale storage key
1772
+ (#1935, mirrors the script fix).
1732
1773
  """
1733
- resolved_id = scene_id if _resolved else await self.resolve_scene_id(scene_id)
1774
+ write_id = (
1775
+ resolved_id
1776
+ if resolved_id is not None
1777
+ else await self.resolve_scene_id(scene_id)
1778
+ )
1734
1779
  try:
1735
- endpoint = f"config/scene/config/{resolved_id}"
1736
-
1737
- # Default a name when missing mirrors the script upsert behaviour
1738
- # so a bare config dict is still acceptable.
1780
+ endpoint = f"config/scene/config/{write_id}"
1781
+
1782
+ # Default a name when missing from the CALLER's ``scene_id`` (not
1783
+ # ``write_id``) mirrors the script upsert behaviour so a bare
1784
+ # config dict is acceptable, without resetting a renamed scene's
1785
+ # name to the storage key. Strip the ``scene.`` prefix: a caller may
1786
+ # pass the entity_id form (``resolve_scene_id`` accepts it), and HA
1787
+ # derives the entity_id from the name slug — persisting the prefixed
1788
+ # form would rename the scene and change its entity_id on a plain update.
1739
1789
  if "name" not in config:
1740
- config["name"] = scene_id
1790
+ config["name"] = scene_id.removeprefix("scene.")
1741
1791
 
1742
1792
  # Validate required field. ``entities`` is a dict keyed by entity_id,
1743
1793
  # not a list — distinct from script ``sequence`` and automation ``action``.
@@ -1757,7 +1807,7 @@ class HomeAssistantClient:
1757
1807
  # activity should diff before/after via ``ha_config_get_scene``.
1758
1808
  return {
1759
1809
  "success": True,
1760
- "scene_id": resolved_id,
1810
+ "scene_id": write_id,
1761
1811
  "result": response.get("result", "ok"),
1762
1812
  }
1763
1813
  except Exception as e:
@@ -273,9 +273,17 @@ async def get_component_caps(client: Any) -> ComponentCaps | None:
273
273
  if not base_url or not token:
274
274
  # Not a credentialed HA client — no WS connection to negotiate over.
275
275
  return None
276
+ # Thread verify_ssl so a verify_ssl=False client keys (and can establish)
277
+ # its own pooled connection rather than failing the probe or colliding with
278
+ # a default-verification entry — otherwise EVERY component capability is
279
+ # silently unreachable on an HTTPS + verify_ssl=False install. ``None`` is
280
+ # the pool's settings default, so a client without the attr is unchanged.
281
+ verify_ssl = getattr(client, "verify_ssl", None)
276
282
 
277
283
  try:
278
- ws = await get_websocket_client(url=base_url, token=token)
284
+ ws = await get_websocket_client(
285
+ url=base_url, token=token, verify_ssl=verify_ssl
286
+ )
279
287
  response = await ws.send_command(INFO_COMMAND)
280
288
  except HomeAssistantCommandError:
281
289
  # Old component (unknown_command) or an info handler bug: the
@@ -801,9 +801,15 @@ class AutomationConfigTools:
801
801
  # (trigger -> triggers, action -> actions, condition -> conditions).
802
802
  config_dict = _normalize_automation_config(config_dict)
803
803
 
804
- # Optional hash check for full config updates
804
+ # Optional hash check for full config updates. When it runs it
805
+ # resolves ``identifier`` to the storage key — thread that through so
806
+ # the upsert doesn't re-resolve (issue #1813 Phase 0). Stays None on
807
+ # the no-hash update path (raw identifier resolved once, in upsert).
808
+ resolved_id: str | None = None
805
809
  if identifier and config_hash:
806
- await self._fetch_and_verify_hash(identifier, config_hash, "set")
810
+ _, resolved_id = await self._fetch_and_verify_hash(
811
+ identifier, config_hash, "set"
812
+ )
807
813
 
808
814
  self._validate_required_fields(config_dict, identifier)
809
815
  bp_warnings = _check_best_practices(config_dict)
@@ -820,6 +826,7 @@ class AutomationConfigTools:
820
826
  validation_meta,
821
827
  MandatoryBPS,
822
828
  conflict_warnings,
829
+ resolved_id,
823
830
  )
824
831
 
825
832
  except ToolError as te:
@@ -874,6 +881,30 @@ class AutomationConfigTools:
874
881
  augment_error_dict_with_skill_content(error, bp_warnings)
875
882
  raise_tool_error(error)
876
883
 
884
+ async def _upsert_automation(
885
+ self,
886
+ config: dict[str, Any],
887
+ identifier: str | None,
888
+ resolved_id: str | None,
889
+ ) -> dict[str, Any]:
890
+ """Upsert, threading a pre-resolved unique_id when available.
891
+
892
+ When ``resolved_id`` is set the caller already resolved ``identifier``
893
+ (via ``_fetch_and_verify_hash``); pass it with ``_resolved=True`` so the
894
+ REST client skips the redundant entity_id→unique_id lookup (issue #1813
895
+ Phase 0). Otherwise fall back to the raw ``identifier`` and let the REST
896
+ client resolve — the create path (``identifier is None``) and the
897
+ no-hash update path both land here unchanged.
898
+ """
899
+ result: dict[str, Any]
900
+ if resolved_id is not None:
901
+ result = await self._client.upsert_automation_config(
902
+ config, resolved_id, _resolved=True
903
+ )
904
+ else:
905
+ result = await self._client.upsert_automation_config(config, identifier)
906
+ return result
907
+
877
908
  async def _run_python_transform(
878
909
  self,
879
910
  identifier: str | None,
@@ -908,7 +939,7 @@ class AutomationConfigTools:
908
939
  )
909
940
  )
910
941
 
911
- current_config = await self._fetch_and_verify_hash(
942
+ current_config, resolved_id = await self._fetch_and_verify_hash(
912
943
  identifier, config_hash, "python_transform"
913
944
  )
914
945
 
@@ -938,8 +969,12 @@ class AutomationConfigTools:
938
969
  self._validate_required_fields(transformed_config, identifier)
939
970
  bp_warnings = _check_best_practices(transformed_config)
940
971
 
941
- result = await self._client.upsert_automation_config(
942
- transformed_config, identifier
972
+ # ``_fetch_and_verify_hash`` already resolved ``identifier`` to the
973
+ # storage key; thread it so the upsert skips the redundant re-resolve
974
+ # (issue #1813 Phase 0). Fall back to the raw identifier if the fetched
975
+ # body carried no ``id`` (not expected for a real automation).
976
+ result = await self._upsert_automation(
977
+ transformed_config, identifier, resolved_id
943
978
  )
944
979
  for warning in conflict_warnings:
945
980
  result.setdefault("warnings", []).append(warning)
@@ -988,9 +1023,15 @@ class AutomationConfigTools:
988
1023
  validation_meta: dict[str, Any],
989
1024
  MandatoryBPS: bool,
990
1025
  conflict_warnings: list[str] | None = None,
1026
+ resolved_id: str | None = None,
991
1027
  ) -> dict[str, Any]:
992
- """Execute config-replacement mode and return the tool response."""
993
- result = await self._client.upsert_automation_config(config_dict, identifier)
1028
+ """Execute config-replacement mode and return the tool response.
1029
+
1030
+ ``resolved_id`` (set only when the optional hash check pre-resolved
1031
+ ``identifier``) is threaded to the upsert so it skips the redundant
1032
+ re-resolve; None falls back to resolving inside the REST client.
1033
+ """
1034
+ result = await self._upsert_automation(config_dict, identifier, resolved_id)
994
1035
 
995
1036
  for warning in conflict_warnings or []:
996
1037
  result.setdefault("warnings", []).append(warning)
@@ -1137,10 +1178,18 @@ class AutomationConfigTools:
1137
1178
 
1138
1179
  async def _fetch_and_verify_hash(
1139
1180
  self, identifier: str, config_hash: str, action: str
1140
- ) -> dict[str, Any]:
1181
+ ) -> tuple[dict[str, Any], str | None]:
1141
1182
  """Fetch current automation config and verify config_hash for optimistic locking.
1142
1183
 
1143
- Returns the current normalized config dict.
1184
+ Returns ``(current_normalized_config, resolved_unique_id)``. The fetch
1185
+ already resolved ``identifier`` to the storage key inside
1186
+ ``get_automation_config``; HA returns the stored body keyed by that
1187
+ unique_id, so ``config['id']`` IS the resolved unique_id (the same value
1188
+ the #1404 guard compares against). Threading it lets the caller pass
1189
+ ``_resolved=True`` to ``upsert_automation_config`` and skip the redundant
1190
+ second resolve (issue #1813 Phase 0). ``None`` when the stored body has
1191
+ no ``id`` (not expected for a real automation) — the caller then falls
1192
+ back to re-resolving.
1144
1193
  Raises ToolError if the hash does not match (conflict).
1145
1194
  """
1146
1195
  current_config, current_hash = await self._get_automation_config_internal(
@@ -1158,7 +1207,11 @@ class AutomationConfigTools:
1158
1207
  context={"action": action, "identifier": identifier},
1159
1208
  )
1160
1209
  )
1161
- return current_config
1210
+ raw_id = current_config.get("id")
1211
+ # Stringify to match ``_resolve_automation_id``'s ``str()`` coercion so a
1212
+ # threaded id is byte-for-byte what the unthreaded path would produce.
1213
+ resolved_id = str(raw_id) if raw_id is not None else None
1214
+ return current_config, resolved_id
1162
1215
 
1163
1216
  @staticmethod
1164
1217
  def _parse_and_validate_config(config: str | dict[str, Any]) -> dict[str, Any]:
@@ -386,7 +386,7 @@ class ConfigSceneTools:
386
386
 
387
387
  async def _get_scene_config_internal(
388
388
  self, scene_id: str, *, _resolved: bool = False
389
- ) -> tuple[dict[str, Any], str, str]:
389
+ ) -> tuple[dict[str, Any], str, str | None]:
390
390
  """Fetch scene config without logging or category injection.
391
391
 
392
392
  Returns ``(actual_config, config_hash, resolved_id)`` where
@@ -394,27 +394,36 @@ class ConfigSceneTools:
394
394
  ``resolved_id`` is the storage key the rest-client resolved the input
395
395
  to (issue #1168 R3 blocker 6). Used by ``_fetch_and_verify_hash``.
396
396
 
397
+ ``resolved_id`` is ``None`` when the envelope omits ``scene_id`` — the
398
+ write target must then be re-resolved from the caller's input rather
399
+ than defaulting to the (unresolved) caller slug, which for a renamed
400
+ scene would target the wrong storage key. This is a structural
401
+ invariant, not a live path: ``get_scene_config`` always sets the key.
402
+
397
403
  ``_resolved=True`` forwards to ``get_scene_config`` that ``scene_id`` is
398
404
  already a resolved storage key, so the redundant registry lookup is
399
405
  skipped (e.g. the python_transform re-fetch, which already resolved).
400
406
  """
401
407
  envelope = await self._client.get_scene_config(scene_id, _resolved=_resolved)
402
408
  actual_config = envelope.get("config", envelope)
403
- resolved_id = envelope.get("scene_id", scene_id.removeprefix("scene."))
409
+ resolved_id = envelope.get("scene_id")
404
410
  config_hash_value = compute_config_hash(actual_config)
405
411
  return actual_config, config_hash_value, resolved_id
406
412
 
407
413
  async def _fetch_and_verify_hash(
408
414
  self, scene_id: str, config_hash: str, action: str
409
- ) -> tuple[dict[str, Any], str]:
415
+ ) -> tuple[dict[str, Any], str | None]:
410
416
  """Fetch current scene config and verify config_hash for optimistic locking.
411
417
 
412
418
  Returns ``(actual_config, resolved_id)`` — the inner scene body and
413
- the storage key the rest-client resolved the input to. Raises
414
- ``ToolError`` if the hash does not match (conflict). Issue #1168 R3
415
- blocker 6: callers thread ``resolved_id`` into responses so the
416
- outer ``scene_id`` matches the inner body's ``id`` regardless of
417
- whether the caller passed the entity_id slug or the storage key.
419
+ the storage key the rest-client resolved the input to. ``resolved_id``
420
+ is ``None`` when the envelope omitted the key, so the upsert re-resolves
421
+ rather than writing to the caller slug; the caller then re-binds it from
422
+ the upsert result. Raises ``ToolError`` if the hash does not match
423
+ (conflict). Issue #1168 R3 blocker 6: callers thread ``resolved_id`` into
424
+ responses so the outer ``scene_id`` matches the inner body's ``id``
425
+ regardless of whether the caller passed the entity_id slug or the
426
+ storage key.
418
427
  """
419
428
  (
420
429
  actual_config,
@@ -691,6 +700,12 @@ class ConfigSceneTools:
691
700
  actual_config, resolved_id = await self._fetch_and_verify_hash(
692
701
  scene_id, config_hash, "python_transform"
693
702
  )
703
+ # Capture the scene's own ``id`` BEFORE the transform mutates the
704
+ # config in place — it is the stable identity the id-change guard
705
+ # compares against (in production it equals ``resolved_id``; it is
706
+ # also correct when ``resolved_id`` is None because the envelope
707
+ # omitted ``scene_id``).
708
+ original_id = actual_config.get("id")
694
709
 
695
710
  try:
696
711
  transformed_config = safe_execute(python_transform, actual_config)
@@ -773,23 +788,26 @@ class ConfigSceneTools:
773
788
  # legitimate (HA treats ``id`` as optional) and should pass
774
789
  # through. R7 blocker 24: ``transformed_config["id"] = None``
775
790
  # is the in-place equivalent of ``del`` — normalise both
776
- # by checking against ``(None, resolved_id)``, so only an
777
- # explicit non-None mismatch triggers the reject.
791
+ # by checking against ``(None, original_id)``, so only an
792
+ # explicit non-None mismatch triggers the reject. Compare against
793
+ # the scene's own captured ``id`` (not ``resolved_id``, which may
794
+ # be None on the structural-invariant path) — in production the
795
+ # two are equal.
778
796
  if "id" in transformed_config and transformed_config["id"] not in (
779
797
  None,
780
- resolved_id,
798
+ original_id,
781
799
  ):
782
800
  raise_tool_error(
783
801
  create_error_response(
784
802
  ErrorCode.VALIDATION_FAILED,
785
803
  "Transform must not change ``config['id']``",
786
804
  suggestions=[
787
- f"Original scene_id is {resolved_id!r} — keep it unchanged",
805
+ f"Original scene_id is {original_id!r} — keep it unchanged",
788
806
  "To rename a scene, use ha_config_remove_scene + ha_config_set_scene",
789
807
  ],
790
808
  context={
791
809
  "action": "python_transform",
792
- "scene_id": resolved_id,
810
+ "scene_id": original_id,
793
811
  "attempted_id": transformed_config.get("id"),
794
812
  },
795
813
  )
@@ -828,9 +846,17 @@ class ConfigSceneTools:
828
846
  if category:
829
847
  await self._validate_category_id(category)
830
848
 
849
+ # ``resolved_id`` is the storage key (write target); ``scene_id``
850
+ # stays the caller id so a missing ``name`` defaults caller-facing
851
+ # rather than to the storage key (#1935).
831
852
  result = await self._client.upsert_scene_config(
832
- transformed_config, resolved_id, _resolved=True
853
+ transformed_config, scene_id, resolved_id=resolved_id
833
854
  )
855
+ # The upsert re-resolves when ``resolved_id`` was None (envelope
856
+ # omitted the key); re-bind to the authoritative write target it
857
+ # returned so the re-fetch, entity resolution, and response all
858
+ # use the resolved storage key rather than a possible None.
859
+ resolved_id = result.get("scene_id", resolved_id)
834
860
 
835
861
  # Re-fetch to get authoritative hash (HA may normalise after save).
836
862
  # ``resolved_id`` is already the storage key, so skip the re-resolve.
@@ -960,10 +986,18 @@ class ConfigSceneTools:
960
986
  )
961
987
 
962
988
  # ``resolved_id`` is already the storage key (from the hash-verify
963
- # fetch or the resolve above), so skip the redundant re-resolve.
989
+ # fetch or the resolve above) pass it as the write target to skip
990
+ # the redundant re-resolve. ``scene_id`` stays the caller id so a
991
+ # missing ``name`` defaults caller-facing, not to the storage key
992
+ # (#1935).
964
993
  result = await self._client.upsert_scene_config(
965
- config_dict, resolved_id, _resolved=True
994
+ config_dict, scene_id, resolved_id=resolved_id
966
995
  )
996
+ # The upsert re-resolves when ``resolved_id`` was None (envelope
997
+ # omitted the key); re-bind to the authoritative write target it
998
+ # returned so entity resolution and the response use the resolved
999
+ # storage key rather than a possible None.
1000
+ resolved_id = result.get("scene_id", resolved_id)
967
1001
 
968
1002
  # Resolve actual entity_id via registry — HA derives scene
969
1003
  # entity_ids from the 'name' slug, not the scene_id storage key,
@@ -302,30 +302,48 @@ class ConfigScriptTools:
302
302
 
303
303
  async def _get_script_config_internal(
304
304
  self, script_id: str
305
- ) -> tuple[dict[str, Any], str]:
305
+ ) -> tuple[dict[str, Any], str, str | None]:
306
306
  """Fetch script config without logging or category injection.
307
307
 
308
- Returns (actual_config, config_hash) tuple where actual_config is
309
- the inner script body (not the REST wrapper).
308
+ Returns ``(actual_config, config_hash, resolved_id)`` where
309
+ ``actual_config`` is the inner script body (not the REST wrapper) and
310
+ ``resolved_id`` is the storage key the REST client resolved the input to
311
+ (from the envelope's ``script_id``). Threading ``resolved_id`` lets the
312
+ upsert call site skip the redundant re-resolve (issue #1813 Phase 0).
310
313
  Used internally by _fetch_and_verify_hash and ha_config_get_script.
311
314
 
315
+ ``resolved_id`` is ``None`` when the envelope omits ``script_id`` — the
316
+ write target must then be re-resolved from the caller's input rather
317
+ than defaulting to the (unresolved) caller slug, which for a renamed
318
+ script would target the wrong storage key. This is a structural
319
+ invariant, not a live path: ``get_script_config`` always sets the key.
320
+
312
321
  404 responses from the REST client are mapped to a structured
313
322
  ``RESOURCE_NOT_FOUND`` ToolError via ``_fetch_script_config_envelope``.
314
323
  """
315
324
  config_result = await self._fetch_script_config_envelope(script_id)
316
325
  actual_config = config_result.get("config", config_result)
317
326
  config_hash_value = compute_config_hash(actual_config)
318
- return actual_config, config_hash_value
327
+ resolved_id = config_result.get("script_id")
328
+ return actual_config, config_hash_value, resolved_id
319
329
 
320
330
  async def _fetch_and_verify_hash(
321
331
  self, script_id: str, config_hash: str, action: str
322
- ) -> dict[str, Any]:
332
+ ) -> tuple[dict[str, Any], str | None]:
323
333
  """Fetch current script config and verify config_hash for optimistic locking.
324
334
 
325
- Returns the actual script config dict (inner body).
326
- Raises ToolError if the hash does not match (conflict).
335
+ Returns ``(actual_config, resolved_id)`` — the inner script body and the
336
+ storage key the REST client resolved the input to. Threading
337
+ ``resolved_id`` lets the upsert call site skip the redundant re-resolve
338
+ (issue #1813 Phase 0); it is ``None`` when the envelope omitted the key,
339
+ so the upsert re-resolves rather than writing to the caller slug. Raises
340
+ ToolError if the hash does not match (conflict).
327
341
  """
328
- actual_config, current_hash = await self._get_script_config_internal(script_id)
342
+ (
343
+ actual_config,
344
+ current_hash,
345
+ resolved_id,
346
+ ) = await self._get_script_config_internal(script_id)
329
347
  if current_hash != config_hash:
330
348
  raise_tool_error(
331
349
  create_error_response(
@@ -338,7 +356,24 @@ class ConfigScriptTools:
338
356
  context={"action": action, "script_id": script_id},
339
357
  )
340
358
  )
341
- return actual_config
359
+ return actual_config, resolved_id
360
+
361
+ async def _upsert_script(
362
+ self, config: dict[str, Any], script_id: str, resolved_id: str | None
363
+ ) -> dict[str, Any]:
364
+ """Upsert, threading a pre-resolved storage key when available.
365
+
366
+ ``script_id`` is always the CALLER's identifier (used to default a
367
+ missing alias). ``resolved_id`` — set only when ``_fetch_and_verify_hash``
368
+ already resolved the storage key — is passed as the write target so the
369
+ REST client skips the redundant entity-registry lookup (issue #1813
370
+ Phase 0); None lets the REST client resolve. Passing the caller id
371
+ separately keeps a renamed script's alias caller-facing (#1935).
372
+ """
373
+ result: dict[str, Any] = await self._client.upsert_script_config(
374
+ config, script_id, resolved_id=resolved_id
375
+ )
376
+ return result
342
377
 
343
378
  @staticmethod
344
379
  def _validate_script_config(
@@ -677,7 +712,7 @@ class ConfigScriptTools:
677
712
  )
678
713
 
679
714
  # Fetch current config and verify hash
680
- actual_config = await self._fetch_and_verify_hash(
715
+ actual_config, resolved_id = await self._fetch_and_verify_hash(
681
716
  script_id, config_hash, "python_transform"
682
717
  )
683
718
 
@@ -719,13 +754,20 @@ class ConfigScriptTools:
719
754
  )
720
755
  bp_warnings = _check_best_practices(transformed_config)
721
756
 
722
- # Save transformed config
757
+ # Save transformed config. ``_fetch_and_verify_hash`` already
758
+ # resolved the storage key; pass it as the write target so the
759
+ # upsert skips the redundant re-resolve (issue #1813 Phase 0).
760
+ # ``script_id`` stays the caller id (the fetched config already
761
+ # carries an alias here, so the default is a no-op, but keep the
762
+ # contract consistent — #1935).
723
763
  result = await self._client.upsert_script_config(
724
- transformed_config, script_id
764
+ transformed_config, script_id, resolved_id=resolved_id
725
765
  )
726
766
 
727
767
  # Re-fetch to get authoritative hash (HA may normalize after save)
728
- _, new_config_hash = await self._get_script_config_internal(script_id)
768
+ _, new_config_hash, _ = await self._get_script_config_internal(
769
+ script_id
770
+ )
729
771
 
730
772
  response: dict[str, Any] = {
731
773
  "success": True,
@@ -766,9 +808,15 @@ class ConfigScriptTools:
766
808
  category,
767
809
  )
768
810
 
769
- # Optional hash check for full config updates
811
+ # Optional hash check for full config updates. When it runs it
812
+ # resolves ``script_id`` to the storage key — thread that through so
813
+ # the upsert doesn't re-resolve (issue #1813 Phase 0). Stays None on
814
+ # the no-hash path (raw script_id resolved once, inside upsert).
815
+ resolved_key: str | None = None
770
816
  if config_hash:
771
- await self._fetch_and_verify_hash(script_id, config_hash, "set")
817
+ _, resolved_key = await self._fetch_and_verify_hash(
818
+ script_id, config_hash, "set"
819
+ )
772
820
 
773
821
  # Pre-check for best-practice issues.
774
822
  bp_warnings = _check_best_practices(config_dict)
@@ -780,7 +828,7 @@ class ConfigScriptTools:
780
828
  self._client, config_dict
781
829
  )
782
830
 
783
- result = await self._client.upsert_script_config(config_dict, script_id)
831
+ result = await self._upsert_script(config_dict, script_id, resolved_key)
784
832
 
785
833
  # Wait for script to be queryable
786
834
  entity_id = f"script.{script_id}"