ha-mcp-dev 7.6.0.dev675__tar.gz → 7.6.0.dev677__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 (140) hide show
  1. {ha_mcp_dev-7.6.0.dev675/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.6.0.dev677}/PKG-INFO +5 -3
  2. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/README.md +4 -2
  3. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/pyproject.toml +1 -1
  4. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/settings.js +152 -0
  5. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/settings_ui.py +158 -0
  6. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/stdio_settings_sidecar.py +15 -0
  7. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/best_practice_checker.py +15 -5
  8. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_config_automations.py +142 -108
  9. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_filesystem.py +5 -20
  10. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_search.py +1 -1
  11. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_service.py +4 -1
  12. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/domain_handlers.py +4 -1
  13. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677/src/ha_mcp_dev.egg-info}/PKG-INFO +5 -3
  14. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/LICENSE +0 -0
  15. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/MANIFEST.in +0 -0
  16. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/setup.cfg +0 -0
  17. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/__init__.py +0 -0
  18. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/__main__.py +0 -0
  19. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/_pypi_marker +0 -0
  20. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/_version.py +0 -0
  21. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/auth/__init__.py +0 -0
  22. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/auth/consent_form.py +0 -0
  23. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/auth/provider.py +0 -0
  24. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/backup_manager.py +0 -0
  25. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/client/__init__.py +0 -0
  26. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/client/rest_client.py +0 -0
  27. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/client/supervisor_client.py +0 -0
  28. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/client/websocket_client.py +0 -0
  29. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/client/websocket_listener.py +0 -0
  30. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/config.py +0 -0
  31. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  32. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  33. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  34. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/errors.py +0 -0
  35. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/policy/__init__.py +0 -0
  36. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/policy/approval_queue.py +0 -0
  37. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/policy/evaluator.py +0 -0
  38. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/policy/handlers.py +0 -0
  39. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/policy/middleware.py +0 -0
  40. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/policy/model.py +0 -0
  41. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/policy/persistence.py +0 -0
  42. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/policy/value_sources.py +0 -0
  43. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/py.typed +0 -0
  44. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  45. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  46. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  47. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  48. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  49. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  50. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  51. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  52. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  53. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  54. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  55. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  56. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  57. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  58. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  59. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  60. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  61. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  62. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  63. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  64. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  65. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  66. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/server.py +0 -0
  67. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/settings.css +0 -0
  68. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/smoke_test.py +0 -0
  69. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/__init__.py +0 -0
  70. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/auto_backup.py +0 -0
  71. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/backup.py +0 -0
  72. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/device_control.py +0 -0
  73. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/enhanced.py +0 -0
  74. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/helpers.py +0 -0
  75. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/reference_validator.py +0 -0
  76. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/registry.py +0 -0
  77. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  78. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  79. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  80. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  81. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  82. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  83. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  84. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  85. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  86. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_addons.py +0 -0
  87. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_areas.py +0 -0
  88. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  89. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  90. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_calendar.py +0 -0
  91. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_camera.py +0 -0
  92. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_categories.py +0 -0
  93. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_code.py +0 -0
  94. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  95. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  96. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  97. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  98. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  99. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  100. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_energy.py +0 -0
  101. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_entities.py +0 -0
  102. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_groups.py +0 -0
  103. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_hacs.py +0 -0
  104. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_history.py +0 -0
  105. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_integrations.py +0 -0
  106. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_labels.py +0 -0
  107. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  108. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_registry.py +0 -0
  109. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_resources.py +0 -0
  110. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_services.py +0 -0
  111. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_system.py +0 -0
  112. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_todo.py +0 -0
  113. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_traces.py +0 -0
  114. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_updates.py +0 -0
  115. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_utility.py +0 -0
  116. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  117. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  118. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_zones.py +0 -0
  119. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/util_helpers.py +0 -0
  120. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/validation_middleware.py +0 -0
  121. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/transforms/__init__.py +0 -0
  122. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/transforms/categorized_search.py +0 -0
  123. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  124. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/__init__.py +0 -0
  125. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/config_hash.py +0 -0
  126. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/data_paths.py +0 -0
  127. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  128. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  129. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/operation_manager.py +0 -0
  130. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/python_sandbox.py +0 -0
  131. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/skill_loader.py +0 -0
  132. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/usage_logger.py +0 -0
  133. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  134. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  135. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  136. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  137. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  138. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/tests/__init__.py +0 -0
  139. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/tests/test_constants.py +0 -0
  140. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/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.6.0.dev675
3
+ Version: 7.6.0.dev677
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
@@ -372,7 +372,7 @@ For comprehensive testing documentation, see **[tests/README.md](tests/README.md
372
372
 
373
373
  Ha-mcp runs **locally** on your machine. Your smart home data stays on your network.
374
374
 
375
- - **No telemetry today** — anonymous usage stats are a planned future feature (as of May 2026); users will be notified when it lands and it will be opt-in only
375
+ - **No telemetry today** — anonymous usage stats are a planned future feature (as of June 2026); when it lands it will follow your Home Assistant analytics/telemetry setting (which you can override), announced prominently in the release notes and the web Settings UI at least one month beforehand
376
376
  - **No personal data collection** — we never collect entity names, configs, or device data
377
377
  - **User-controlled bug reports** — only sent with your explicit approval
378
378
 
@@ -435,13 +435,15 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
435
435
  - **[@griffinmartin](https://github.com/griffinmartin)** — Added OpenCode (by Anomaly) as a selectable AI client in the setup wizard, with both stdio and streamable HTTP support.
436
436
  - **[@hhopke](https://github.com/hhopke)** — Fixed addon API calls to route through HA Core ingress proxy instead of direct container connections, fixing `ha_manage_addon` proxy mode on addon installs.
437
437
  - **[@tomwilkie](https://github.com/tomwilkie)** — JMESPath middleware exploration (#1147) whose review-time token-measurement data informed the design of #1199 and #1225.
438
- - **[@SealKan](https://github.com/SealKan)** — `fields=`/`attribute_keys=` projection on six read-heavy tools (#1225), `ha_call_event` tool (#1239), and dashboards-list helper refactor (#1207).
438
+ - **[@SealKan](https://github.com/SealKan)** — `fields=`/`attribute_keys=` projection on six read-heavy tools (#1225), `ha_call_event` tool (#1239), dashboards-list helper refactor (#1207), `for:`-field duration-math detector in the best-practice checker (#1264), persistent DCR OAuth client registrations across restarts (#1265), and issue-triage prompt token-budgeting (#1522).
439
439
  - **[@KarelTestSpecial](https://github.com/KarelTestSpecial)** — Cached YAML instance to prevent CPU spikes during bulk edits (#1371).
440
440
  - **[@corgan2222](https://github.com/corgan2222)** — HA brand assets for custom integration (#1317).
441
441
  - **[@drseanwing](https://github.com/drseanwing)** — Progress emission via FastMCP `Context` in long-running tools (#1124); tool-discovery / categorized-search docs (#1123).
442
442
  - **[@fnordpig](https://github.com/fnordpig)** — Config subentry support (#1393) and Assist pipeline management tool (#1392).
443
443
  - **[@paul43210](https://github.com/paul43210)** — `array_patch` mode in `ha_manage_addon` for atomic GET-modify-POST (#1063).
444
444
  - **[@L1AD](https://github.com/L1AD)** — Filed #966 proposing tool security policies; pointed to PolicyLayer's MCP-security work as prior art that inspired the predicate DSL shape.
445
+ - **[@nightcityblade](https://github.com/nightcityblade)** — Updated stale Home Assistant Advanced Mode references after HA 2026.6 made formerly advanced options available by default (#1533).
446
+ - **[@emmelutzer](https://github.com/emmelutzer)** — Financial support via [GitHub Sponsors](https://github.com/sponsors/julienld). Thank you! ☕
445
447
 
446
448
  ---
447
449
 
@@ -342,7 +342,7 @@ For comprehensive testing documentation, see **[tests/README.md](tests/README.md
342
342
 
343
343
  Ha-mcp runs **locally** on your machine. Your smart home data stays on your network.
344
344
 
345
- - **No telemetry today** — anonymous usage stats are a planned future feature (as of May 2026); users will be notified when it lands and it will be opt-in only
345
+ - **No telemetry today** — anonymous usage stats are a planned future feature (as of June 2026); when it lands it will follow your Home Assistant analytics/telemetry setting (which you can override), announced prominently in the release notes and the web Settings UI at least one month beforehand
346
346
  - **No personal data collection** — we never collect entity names, configs, or device data
347
347
  - **User-controlled bug reports** — only sent with your explicit approval
348
348
 
@@ -405,13 +405,15 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
405
405
  - **[@griffinmartin](https://github.com/griffinmartin)** — Added OpenCode (by Anomaly) as a selectable AI client in the setup wizard, with both stdio and streamable HTTP support.
406
406
  - **[@hhopke](https://github.com/hhopke)** — Fixed addon API calls to route through HA Core ingress proxy instead of direct container connections, fixing `ha_manage_addon` proxy mode on addon installs.
407
407
  - **[@tomwilkie](https://github.com/tomwilkie)** — JMESPath middleware exploration (#1147) whose review-time token-measurement data informed the design of #1199 and #1225.
408
- - **[@SealKan](https://github.com/SealKan)** — `fields=`/`attribute_keys=` projection on six read-heavy tools (#1225), `ha_call_event` tool (#1239), and dashboards-list helper refactor (#1207).
408
+ - **[@SealKan](https://github.com/SealKan)** — `fields=`/`attribute_keys=` projection on six read-heavy tools (#1225), `ha_call_event` tool (#1239), dashboards-list helper refactor (#1207), `for:`-field duration-math detector in the best-practice checker (#1264), persistent DCR OAuth client registrations across restarts (#1265), and issue-triage prompt token-budgeting (#1522).
409
409
  - **[@KarelTestSpecial](https://github.com/KarelTestSpecial)** — Cached YAML instance to prevent CPU spikes during bulk edits (#1371).
410
410
  - **[@corgan2222](https://github.com/corgan2222)** — HA brand assets for custom integration (#1317).
411
411
  - **[@drseanwing](https://github.com/drseanwing)** — Progress emission via FastMCP `Context` in long-running tools (#1124); tool-discovery / categorized-search docs (#1123).
412
412
  - **[@fnordpig](https://github.com/fnordpig)** — Config subentry support (#1393) and Assist pipeline management tool (#1392).
413
413
  - **[@paul43210](https://github.com/paul43210)** — `array_patch` mode in `ha_manage_addon` for atomic GET-modify-POST (#1063).
414
414
  - **[@L1AD](https://github.com/L1AD)** — Filed #966 proposing tool security policies; pointed to PolicyLayer's MCP-security work as prior art that inspired the predicate DSL shape.
415
+ - **[@nightcityblade](https://github.com/nightcityblade)** — Updated stale Home Assistant Advanced Mode references after HA 2026.6 made formerly advanced options available by default (#1533).
416
+ - **[@emmelutzer](https://github.com/emmelutzer)** — Financial support via [GitHub Sponsors](https://github.com/sponsors/julienld). Thank you! ☕
415
417
 
416
418
  ---
417
419
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.6.0.dev675"
7
+ version = "7.6.0.dev677"
8
8
  description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.14"
@@ -888,6 +888,149 @@ async function saveBackupConfig() {
888
888
  }
889
889
  }
890
890
 
891
+ // ---- Custom filesystem directories (issue #1567) ---------------------------
892
+ // The list lives in the ha_mcp_tools custom component; the GET/POST endpoints
893
+ // proxy to it via authenticated HA service calls. Cached so the sub-form
894
+ // re-renders synchronously when the beta master / filesystem-tools toggle
895
+ // flips, without re-fetching.
896
+ // Consumed fields of the GET response: {available, paths, deny_floor, reason}
897
+ // (the endpoint also returns builtin_read_dirs/builtin_write_dirs, unused here).
898
+ let _fsCustomPathsData = null;
899
+
900
+ async function loadFsCustomPaths() {
901
+ try {
902
+ const resp = await fetch('./api/settings/fs-custom-paths');
903
+ if (!resp.ok) {
904
+ _fsCustomPathsData = {
905
+ available: false,
906
+ reason: `HTTP ${resp.status}`,
907
+ paths: [],
908
+ deny_floor: [],
909
+ };
910
+ } else {
911
+ _fsCustomPathsData = await resp.json();
912
+ }
913
+ } catch (err) {
914
+ _fsCustomPathsData = {
915
+ available: false,
916
+ reason: 'Network error: ' + String(err),
917
+ paths: [],
918
+ deny_floor: [],
919
+ };
920
+ }
921
+ // Re-render the feature panel so the sub-form reflects the loaded data.
922
+ if (Object.keys(_lastFeatureFlags).length) renderFeatureFlags(_lastFeatureFlags);
923
+ }
924
+
925
+ // Injected beneath the enable_filesystem_tools row in renderFeatureFlags.
926
+ // Second-level nested (under filesystem tools, itself beta-sub-nested under
927
+ // the master), dimmed when either the master beta or filesystem tools is off.
928
+ function renderFsCustomPathsSubForm(parentEl, masterOn, fsOn) {
929
+ const lockedByGate = !masterOn || !fsOn;
930
+ const d = _fsCustomPathsData;
931
+ const row = document.createElement('div');
932
+ row.className =
933
+ 'feature-row fs-custom-paths-sub' + (lockedByGate ? ' dimmed' : '');
934
+
935
+ const info = document.createElement('div');
936
+ info.className = 'feature-info';
937
+ const denyList =
938
+ d && Array.isArray(d.deny_floor) && d.deny_floor.length
939
+ ? d.deny_floor.join(', ')
940
+ : '.storage, secrets.yaml';
941
+ info.innerHTML =
942
+ `<div class="feature-name">Custom filesystem directories (advanced)</div>` +
943
+ `<div class="feature-help">Extra directories (one per line, relative to your config dir) that the file tools may READ and WRITE — e.g. <code>pyscript</code>, <code>python_scripts</code>. Each entry grants both read and write. Applies immediately; no restart needed.</div>` +
944
+ `<div class="feature-help">Always blocked (cannot be added): <code>${escapeHtml(denyList)}</code>, path traversal (<code>..</code>), and absolute paths.</div>`;
945
+
946
+ const control = document.createElement('div');
947
+ control.className = 'feature-control';
948
+
949
+ if (lockedByGate) {
950
+ const note = document.createElement('div');
951
+ note.className = 'feature-locked-note';
952
+ note.textContent =
953
+ 'Enable beta features and filesystem tools above to edit.';
954
+ control.appendChild(note);
955
+ } else if (!d) {
956
+ const note = document.createElement('div');
957
+ note.className = 'feature-help';
958
+ note.textContent = 'Loading…';
959
+ control.appendChild(note);
960
+ } else if (!d.available) {
961
+ const note = document.createElement('div');
962
+ note.className = 'feature-locked-note';
963
+ note.textContent =
964
+ d.reason || 'Custom directories are currently unavailable.';
965
+ control.appendChild(note);
966
+ } else {
967
+ const ta = document.createElement('textarea');
968
+ ta.id = 'fsCustomPathsInput';
969
+ ta.rows = 4;
970
+ ta.value = (d.paths || []).join('\n');
971
+ const btn = document.createElement('button');
972
+ btn.id = 'fsCustomPathsSave';
973
+ btn.className = 'adv-save-btn';
974
+ btn.textContent = 'Save directories';
975
+ btn.addEventListener('click', saveFsCustomPaths);
976
+ const status = document.createElement('div');
977
+ status.id = 'fsCustomPathsStatus';
978
+ status.className = 'feature-help';
979
+ control.appendChild(ta);
980
+ control.appendChild(btn);
981
+ control.appendChild(status);
982
+ }
983
+
984
+ row.appendChild(info);
985
+ row.appendChild(control);
986
+ parentEl.appendChild(row);
987
+ }
988
+
989
+ async function saveFsCustomPaths() {
990
+ const ta = document.getElementById('fsCustomPathsInput');
991
+ const btn = document.getElementById('fsCustomPathsSave');
992
+ const statusEl = document.getElementById('fsCustomPathsStatus');
993
+ if (!ta || !btn || !statusEl) return;
994
+ const paths = ta.value
995
+ .split('\n')
996
+ .map(s => s.trim())
997
+ .filter(s => s.length);
998
+ btn.disabled = true;
999
+ statusEl.textContent = 'Saving…';
1000
+ try {
1001
+ const resp = await fetch('./api/settings/fs-custom-paths', {
1002
+ method: 'POST',
1003
+ headers: { 'Content-Type': 'application/json' },
1004
+ body: JSON.stringify({ paths }),
1005
+ });
1006
+ const data = await resp.json();
1007
+ btn.disabled = false;
1008
+ if (!resp.ok || !data.success) {
1009
+ let msg = 'Save failed';
1010
+ if (data && data.error) {
1011
+ if (typeof data.error === 'string') msg = data.error;
1012
+ else if (data.error.message) msg = data.error.message;
1013
+ }
1014
+ statusEl.textContent = msg;
1015
+ return;
1016
+ }
1017
+ // Reflect the component's canonical normalized list; drop any rejected.
1018
+ _fsCustomPathsData = {
1019
+ ...(_fsCustomPathsData || {}),
1020
+ available: true,
1021
+ paths: data.paths || [],
1022
+ };
1023
+ ta.value = (data.paths || []).join('\n');
1024
+ const rejected = data.rejected || [];
1025
+ statusEl.textContent = rejected.length
1026
+ ? `Saved. Rejected (blocked or invalid): ${rejected.join(', ')}`
1027
+ : 'Saved.';
1028
+ } catch (err) {
1029
+ btn.disabled = false;
1030
+ statusEl.textContent = 'Network error: ' + String(err);
1031
+ }
1032
+ }
1033
+
891
1034
  async function loadBackups() {
892
1035
  const params = new URLSearchParams();
893
1036
  const d = document.getElementById('backupDomain').value.trim();
@@ -1351,6 +1494,14 @@ function renderFeatureFlags(flags) {
1351
1494
  const parentOn = !!f.value;
1352
1495
  renderYamlPackagesSubRows(flags, targetBody, masterOn, parentOn);
1353
1496
  }
1497
+ // After the enable_filesystem_tools row, inject the custom-directories
1498
+ // editor (issue #1567). Dimmed when either the master beta is off or
1499
+ // filesystem tools itself is off. The list is component-owned and fetched
1500
+ // separately via loadFsCustomPaths(); this renders from that cache.
1501
+ if (fieldName === 'enable_filesystem_tools') {
1502
+ const fsOn = !!f.value;
1503
+ renderFsCustomPathsSubForm(targetBody, masterOn, fsOn);
1504
+ }
1354
1505
  });
1355
1506
  }
1356
1507
 
@@ -2718,6 +2869,7 @@ document.getElementById('advSaveBtn').addEventListener('click', saveAdvancedSett
2718
2869
  loadFeatureFlags();
2719
2870
  loadAdvancedSettings();
2720
2871
  loadTools();
2872
+ loadFsCustomPaths();
2721
2873
 
2722
2874
  // Auto-activate tab from ?tab=<name> query string (used by approval URLs
2723
2875
  // generated by the policy middleware: /settings?tab=tool-security-policies&token=...).
@@ -2912,6 +2912,159 @@ def build_settings_handlers(
2912
2912
  }
2913
2913
  )
2914
2914
 
2915
+ async def _fs_custom_paths_call(service: str, data: dict[str, Any]) -> Any:
2916
+ """Invoke a ha_mcp_tools component service for the custom-paths editor,
2917
+ in any deployment mode (issue #1567).
2918
+
2919
+ Uses the live server's HA client in HTTP/add-on modes; in the stdio
2920
+ sidecar (``server is None``) builds a transient ``HomeAssistantClient``
2921
+ from the HA URL/token the sidecar inherits, and closes it afterward.
2922
+ The caller wraps this in try/except so an unreachable HA / missing
2923
+ token (e.g. OAuth mode, where ``server.client`` has no request-scoped
2924
+ token) degrades to an "unavailable" envelope rather than a 500.
2925
+ """
2926
+ from .tools.tools_filesystem import call_mcp_tools_service
2927
+
2928
+ own_client = None
2929
+ try:
2930
+ if server is not None:
2931
+ client = server.client
2932
+ else:
2933
+ from .client.rest_client import HomeAssistantClient
2934
+
2935
+ client = own_client = HomeAssistantClient()
2936
+ return await call_mcp_tools_service(client, service, data)
2937
+ finally:
2938
+ if own_client is not None and hasattr(own_client, "close"):
2939
+ with contextlib.suppress(Exception):
2940
+ await own_client.close()
2941
+
2942
+ async def _get_fs_custom_paths(_: Request) -> JSONResponse:
2943
+ """Return the user-configured extra filesystem directories from the
2944
+ ha_mcp_tools component, plus the non-overridable deny floor for the UI
2945
+ blurb (issue #1567).
2946
+
2947
+ Always 200s with an ``available`` flag: when filesystem tools are off,
2948
+ the component is missing/too old, or HA is unreachable, ``available``
2949
+ is False with a human-readable ``reason`` so the UI can show a disabled
2950
+ section instead of an error.
2951
+ """
2952
+ from .tools.tools_filesystem import is_filesystem_tools_enabled
2953
+ from .tools.util_helpers import unwrap_service_response
2954
+
2955
+ def _unavailable(reason: str) -> JSONResponse:
2956
+ return JSONResponse(
2957
+ {
2958
+ "success": True,
2959
+ "available": False,
2960
+ "reason": reason,
2961
+ "paths": [],
2962
+ "deny_floor": [],
2963
+ }
2964
+ )
2965
+
2966
+ if not is_filesystem_tools_enabled():
2967
+ return _unavailable(
2968
+ "Filesystem tools are disabled. Enable them (beta) to "
2969
+ "configure custom directories."
2970
+ )
2971
+ try:
2972
+ result = await _fs_custom_paths_call("get_allowed_paths", {})
2973
+ except Exception as exc:
2974
+ logger.warning("fs-custom-paths GET could not reach ha_mcp_tools: %s", exc)
2975
+ return _unavailable(f"Could not reach the ha_mcp_tools component: {exc}")
2976
+
2977
+ data = unwrap_service_response(result) if isinstance(result, dict) else {}
2978
+ if not isinstance(data, dict) or not data.get("success", False):
2979
+ reason = (
2980
+ data.get("error") if isinstance(data, dict) else None
2981
+ ) or "ha_mcp_tools returned an unexpected response."
2982
+ return _unavailable(str(reason))
2983
+ return JSONResponse(
2984
+ {
2985
+ "success": True,
2986
+ "available": True,
2987
+ "paths": data.get("paths", []),
2988
+ "deny_floor": data.get("deny_floor", []),
2989
+ "builtin_read_dirs": data.get("builtin_read_dirs", []),
2990
+ "builtin_write_dirs": data.get("builtin_write_dirs", []),
2991
+ }
2992
+ )
2993
+
2994
+ async def _save_fs_custom_paths(request: Request) -> JSONResponse:
2995
+ """Replace the user-configured extra filesystem directories via the
2996
+ ha_mcp_tools component (issue #1567).
2997
+
2998
+ The component validates each entry and drops anything that hits the
2999
+ deny floor or escapes the config dir; the dropped entries come back in
3000
+ ``rejected``. ``restart_required`` is False — the component applies the
3001
+ new allowlist live.
3002
+ """
3003
+ from .tools.tools_filesystem import is_filesystem_tools_enabled
3004
+ from .tools.util_helpers import unwrap_service_response
3005
+
3006
+ if not is_filesystem_tools_enabled():
3007
+ return JSONResponse(
3008
+ create_error_response(
3009
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
3010
+ "Filesystem tools are disabled; enable them (beta) before "
3011
+ "configuring custom directories.",
3012
+ ),
3013
+ status_code=409,
3014
+ )
3015
+ try:
3016
+ body = await request.json()
3017
+ except (ValueError, TypeError):
3018
+ return JSONResponse(
3019
+ create_error_response(
3020
+ ErrorCode.VALIDATION_INVALID_JSON,
3021
+ "Request body must be valid JSON.",
3022
+ ),
3023
+ status_code=400,
3024
+ )
3025
+ paths = body.get("paths") if isinstance(body, dict) else None
3026
+ if paths is None:
3027
+ paths = []
3028
+ if not isinstance(paths, list) or not all(isinstance(p, str) for p in paths):
3029
+ return JSONResponse(
3030
+ create_error_response(
3031
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
3032
+ "'paths' must be a list of directory strings.",
3033
+ ),
3034
+ status_code=400,
3035
+ )
3036
+ try:
3037
+ result = await _fs_custom_paths_call("set_allowed_paths", {"paths": paths})
3038
+ except Exception as exc:
3039
+ logger.warning("fs-custom-paths POST could not reach ha_mcp_tools: %s", exc)
3040
+ return JSONResponse(
3041
+ create_error_response(
3042
+ ErrorCode.SERVICE_CALL_FAILED,
3043
+ f"Could not reach the ha_mcp_tools component: {exc}",
3044
+ ),
3045
+ status_code=502,
3046
+ )
3047
+
3048
+ data = unwrap_service_response(result) if isinstance(result, dict) else {}
3049
+ if not isinstance(data, dict) or not data.get("success", False):
3050
+ reason = (
3051
+ data.get("error") if isinstance(data, dict) else None
3052
+ ) or "ha_mcp_tools rejected the update."
3053
+ return JSONResponse(
3054
+ create_error_response(ErrorCode.SERVICE_CALL_FAILED, str(reason)),
3055
+ status_code=502,
3056
+ )
3057
+ return JSONResponse(
3058
+ {
3059
+ "success": True,
3060
+ "applied": data.get("paths", []),
3061
+ "paths": data.get("paths", []),
3062
+ "rejected": data.get("rejected", []),
3063
+ "mode": "component",
3064
+ "restart_required": False,
3065
+ }
3066
+ )
3067
+
2915
3068
  handlers: dict[str, Any] = {
2916
3069
  "root_page": _root_page,
2917
3070
  "settings_page": _settings_page,
@@ -2931,6 +3084,8 @@ def build_settings_handlers(
2931
3084
  "delete_backups_bulk": _delete_backups_bulk,
2932
3085
  "get_backup_config": _get_backup_config,
2933
3086
  "save_backup_config": _save_backup_config,
3087
+ "get_fs_custom_paths": _get_fs_custom_paths,
3088
+ "save_fs_custom_paths": _save_fs_custom_paths,
2934
3089
  }
2935
3090
 
2936
3091
  # Tool security policies. The main server attaches an
@@ -3100,6 +3255,9 @@ def register_settings_routes(
3100
3255
  ("/api/settings/backups/{name}", ["DELETE"], "delete_backup"),
3101
3256
  ("/api/settings/backup-config", ["GET"], "get_backup_config"),
3102
3257
  ("/api/settings/backup-config", ["POST"], "save_backup_config"),
3258
+ # Custom filesystem directories (issue #1567) — component-owned list
3259
+ ("/api/settings/fs-custom-paths", ["GET"], "get_fs_custom_paths"),
3260
+ ("/api/settings/fs-custom-paths", ["POST"], "save_fs_custom_paths"),
3103
3261
  # Tool security policies endpoints
3104
3262
  ("/api/policy/config", ["GET"], "policy_get_config"),
3105
3263
  ("/api/policy/config", ["PUT"], "policy_put_config"),
@@ -602,6 +602,21 @@ def _build_app(
602
602
  handlers["save_feature_flags"],
603
603
  methods=["POST"],
604
604
  ),
605
+ # Custom filesystem directories (issue #1567). The sub-form is rendered
606
+ # in the features panel, so the stdio sidecar must serve these too — its
607
+ # route list is hand-maintained and does NOT derive from
608
+ # register_settings_routes. The handler builds a transient HA client
609
+ # from the inherited env when server is None (sidecar mode).
610
+ Route(
611
+ f"{secret_prefix}/api/settings/fs-custom-paths",
612
+ handlers["get_fs_custom_paths"],
613
+ methods=["GET"],
614
+ ),
615
+ Route(
616
+ f"{secret_prefix}/api/settings/fs-custom-paths",
617
+ handlers["save_fs_custom_paths"],
618
+ methods=["POST"],
619
+ ),
605
620
  # Tool security policies endpoints (#966). Pending/approve/deny
606
621
  # are wired as stubs that return 503 in sidecar mode — the
607
622
  # in-memory ApprovalQueue lives in the main server process, so
@@ -197,14 +197,24 @@ def check_automation_config(
197
197
 
198
198
  warnings = BestPracticeCheckResult()
199
199
 
200
+ # Read the canonical 2024.10+ plural root keys, falling back to the singular
201
+ # aliases. The internal pipeline always pre-normalizes to plural, so the
202
+ # fallback is defensive for direct/public callers of check_automation_config
203
+ # (HA accepts both forms). Mirrors _check_triggers' platform/trigger tolerance.
200
204
  # Condition templates
201
- _check_condition_templates(config.get("condition", []), warnings, skill_prefix)
205
+ _check_condition_templates(
206
+ config.get("conditions", config.get("condition", [])), warnings, skill_prefix
207
+ )
202
208
 
203
209
  # Action tree (wait_template + nested conditions + target templates)
204
- _check_action_tree(config.get("action", []), warnings, skill_prefix)
210
+ _check_action_tree(
211
+ config.get("actions", config.get("action", [])), warnings, skill_prefix
212
+ )
205
213
 
206
214
  # Trigger templates + device_id
207
- _check_triggers(config.get("trigger", []), warnings, skill_prefix)
215
+ _check_triggers(
216
+ config.get("triggers", config.get("trigger", [])), warnings, skill_prefix
217
+ )
208
218
 
209
219
  # Mode vs motion pattern
210
220
  _check_mode_motion(config, warnings, skill_prefix)
@@ -738,7 +748,7 @@ def _check_mode_motion(
738
748
  if mode != "single":
739
749
  return
740
750
 
741
- triggers = _as_list(config.get("trigger", []))
751
+ triggers = _as_list(config.get("triggers", config.get("trigger", [])))
742
752
  has_motion = any(
743
753
  isinstance(t, dict)
744
754
  and any(
@@ -750,7 +760,7 @@ def _check_mode_motion(
750
760
  if not has_motion:
751
761
  return
752
762
 
753
- if _has_delay_or_wait(config.get("action", [])):
763
+ if _has_delay_or_wait(config.get("actions", config.get("action", []))):
754
764
  _emit(
755
765
  warnings,
756
766
  "Automation uses motion trigger with delay/wait but "