ha-mcp-dev 7.6.0.dev675__tar.gz → 7.6.0.dev676__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.dev676}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/best_practice_checker.py +15 -5
  4. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_automations.py +142 -108
  5. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_search.py +1 -1
  6. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_service.py +4 -1
  7. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/domain_handlers.py +4 -1
  8. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  9. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/LICENSE +0 -0
  10. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/MANIFEST.in +0 -0
  11. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/README.md +0 -0
  12. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/setup.cfg +0 -0
  13. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/__init__.py +0 -0
  14. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/__main__.py +0 -0
  15. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/_pypi_marker +0 -0
  16. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/_version.py +0 -0
  17. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/auth/__init__.py +0 -0
  18. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/auth/consent_form.py +0 -0
  19. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/auth/provider.py +0 -0
  20. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/backup_manager.py +0 -0
  21. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/client/__init__.py +0 -0
  22. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/client/rest_client.py +0 -0
  23. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/client/supervisor_client.py +0 -0
  24. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/client/websocket_client.py +0 -0
  25. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/client/websocket_listener.py +0 -0
  26. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/config.py +0 -0
  27. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  28. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  29. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  30. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/errors.py +0 -0
  31. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/policy/__init__.py +0 -0
  32. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/policy/approval_queue.py +0 -0
  33. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/policy/evaluator.py +0 -0
  34. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/policy/handlers.py +0 -0
  35. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/policy/middleware.py +0 -0
  36. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/policy/model.py +0 -0
  37. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/policy/persistence.py +0 -0
  38. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/policy/value_sources.py +0 -0
  39. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/py.typed +0 -0
  40. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  41. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  42. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  43. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  44. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  45. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  46. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  47. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  48. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  49. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  50. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  51. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  52. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  53. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  54. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  55. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  56. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  57. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  58. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  59. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  60. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  61. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  62. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/server.py +0 -0
  63. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/settings.css +0 -0
  64. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/settings.js +0 -0
  65. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/settings_ui.py +0 -0
  66. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/smoke_test.py +0 -0
  67. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  68. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/__init__.py +0 -0
  69. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/auto_backup.py +0 -0
  70. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/backup.py +0 -0
  71. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/device_control.py +0 -0
  72. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/enhanced.py +0 -0
  73. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/helpers.py +0 -0
  74. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/reference_validator.py +0 -0
  75. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/registry.py +0 -0
  76. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  77. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  78. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  79. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  80. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  81. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  82. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  83. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  84. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  85. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_addons.py +0 -0
  86. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_areas.py +0 -0
  87. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  88. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  89. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_calendar.py +0 -0
  90. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_camera.py +0 -0
  91. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_categories.py +0 -0
  92. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_code.py +0 -0
  93. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  94. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  95. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  96. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  97. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  98. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  99. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_energy.py +0 -0
  100. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_entities.py +0 -0
  101. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  102. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_groups.py +0 -0
  103. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_hacs.py +0 -0
  104. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_history.py +0 -0
  105. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_integrations.py +0 -0
  106. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_labels.py +0 -0
  107. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  108. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_registry.py +0 -0
  109. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_resources.py +0 -0
  110. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_services.py +0 -0
  111. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_system.py +0 -0
  112. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_todo.py +0 -0
  113. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_traces.py +0 -0
  114. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_updates.py +0 -0
  115. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_utility.py +0 -0
  116. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  117. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  118. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/tools_zones.py +0 -0
  119. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/util_helpers.py +0 -0
  120. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/tools/validation_middleware.py +0 -0
  121. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/transforms/__init__.py +0 -0
  122. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/transforms/categorized_search.py +0 -0
  123. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  124. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/__init__.py +0 -0
  125. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/config_hash.py +0 -0
  126. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/data_paths.py +0 -0
  127. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  128. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  129. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/operation_manager.py +0 -0
  130. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/python_sandbox.py +0 -0
  131. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/skill_loader.py +0 -0
  132. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp/utils/usage_logger.py +0 -0
  133. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  134. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/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.dev676}/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.dev676}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  137. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/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.dev676}/tests/__init__.py +0 -0
  139. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/tests/test_constants.py +0 -0
  140. {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev676}/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.dev676
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.6.0.dev675"
7
+ version = "7.6.0.dev676"
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"
@@ -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 "
@@ -82,56 +82,37 @@ NOT_VERIFIED_WARNING_PREFIX = (
82
82
  )
83
83
 
84
84
 
85
- def _normalize_automation_config(
86
- config: Any,
87
- parent_key: str | None = None,
88
- in_choose_or_if: bool = False,
89
- is_root: bool = True,
90
- ) -> Any:
85
+ def _normalize_automation_config(config: Any, is_root: bool = True) -> Any:
91
86
  """
92
- Recursively normalize automation config field names to HA API format.
93
-
94
- Home Assistant accepts both singular ('trigger', 'action', 'condition')
95
- and plural ('triggers', 'actions', 'conditions') field names in YAML,
96
- but the API expects singular forms at the root level.
97
-
98
- IMPORTANT: 'triggers' -> 'trigger' and 'actions' -> 'action' normalization
99
- is ONLY applied at the root level. Deeper in the tree these keys are either
100
- invalid or semantically different, and normalizing them can produce keys
101
- that Home Assistant rejects (e.g., 'action' inside a delay object).
102
-
103
- IMPORTANT: Inside 'choose' and 'if' action blocks, the 'conditions' key
104
- (plural) is required by the HA schema and should NOT be normalized to
105
- 'condition' (singular).
106
-
107
- IMPORTANT: Inside compound condition blocks ('or', 'and', 'not'), the
108
- 'conditions' key (plural) is required and should NOT be normalized to
109
- 'condition' (singular).
87
+ Recursively normalize automation config field names to HA's canonical form.
88
+
89
+ Home Assistant's 2024.10+ canonical form uses the plural root list keys
90
+ ('triggers', 'actions', 'conditions'); the singular forms ('trigger',
91
+ 'action', 'condition') remain fully accepted as silent aliases. This tool
92
+ canonicalizes to the plural root forms so the config-API round-trip and the
93
+ downstream validators / best-practice checker all see one stable, modern
94
+ shape (HA accepts whichever we send).
95
+
96
+ Only the ROOT list keys are pluralized. The singular keys that act as type
97
+ discriminators or service calls inside trigger/condition/action items
98
+ ('trigger:' = trigger type, 'condition:' = condition type, 'action:' =
99
+ service call) are semantically different and are left untouched, as is the
100
+ singular 'sequence' key inside choose/if options and scripts. The nested
101
+ 'conditions' lists required inside choose/if and compound (or/and/not)
102
+ blocks are already plural and pass through unchanged (issue #498: never
103
+ rewrite these deeper keys).
110
104
 
111
105
  Args:
112
106
  config: Automation configuration (dict, list, or primitive)
113
- parent_key: The parent dictionary key (for context tracking)
114
- in_choose_or_if: Whether we're inside a choose/if option that requires
115
- 'conditions' (plural) to remain unchanged
116
- is_root: Whether this is the root-level automation config dict.
117
- Only root level gets 'triggers'->'trigger' and
118
- 'actions'->'action' normalization.
107
+ is_root: Whether this is the root-level automation config dict. Only the
108
+ root level gets the singular -> plural list-key normalization.
119
109
 
120
110
  Returns:
121
- Normalized configuration with singular field names at root level,
122
- but preserving 'conditions' (plural) inside choose/if blocks and
123
- compound condition blocks (or/and/not)
111
+ Normalized configuration with plural list field names at the root level.
124
112
  """
125
113
  # Handle lists - recursively process each item
126
114
  if isinstance(config, list):
127
- # If parent is 'choose' or 'if', items are options that need 'conditions' preserved
128
- is_option_list = parent_key in ("choose", "if")
129
- return [
130
- _normalize_automation_config(
131
- item, parent_key, is_option_list, is_root=False
132
- )
133
- for item in config
134
- ]
115
+ return [_normalize_automation_config(item, is_root=False) for item in config]
135
116
 
136
117
  # Handle primitives (strings, numbers, etc.)
137
118
  if not isinstance(config, dict):
@@ -140,39 +121,32 @@ def _normalize_automation_config(
140
121
  # Process dictionary
141
122
  normalized = config.copy()
142
123
 
143
- # Check if this dict is a compound condition block (or/and/not)
144
- # that needs its nested 'conditions' key preserved
145
- is_compound_condition_block = normalized.get("condition") in ("or", "and", "not")
146
-
147
- # Build field mappings based on context
124
+ # Build field mappings (source alias -> canonical key).
148
125
  field_mappings: dict[str, str] = {}
149
126
 
150
- # 'triggers' -> 'trigger' and 'actions' -> 'action' ONLY at root level.
151
- # Deeper in the tree these keys are invalid and normalizing them produces
152
- # keys HA rejects (e.g., 'action' inside a delay object -- see issue #498).
127
+ # Pluralize the root list keys to HA's 2024.10+ canonical form. ONLY at the
128
+ # root level: deeper in the tree 'trigger'/'action'/'condition' are type
129
+ # discriminators / service calls, not list keys, and must not be touched
130
+ # (e.g., 'action' inside a delay object -- see issue #498).
153
131
  if is_root:
154
- field_mappings["triggers"] = "trigger"
155
- field_mappings["actions"] = "action"
132
+ field_mappings["trigger"] = "triggers"
133
+ field_mappings["action"] = "actions"
134
+ field_mappings["condition"] = "conditions"
156
135
 
157
- # 'sequences' -> 'sequence' is safe at any level (only meaningful in choose options)
136
+ # 'sequences' -> 'sequence': the canonical key is singular at any level.
158
137
  field_mappings["sequences"] = "sequence"
159
138
 
160
- # Only add 'conditions' mapping if NOT inside a choose/if option
161
- # AND NOT a compound condition block (or/and/not)
162
- if not in_choose_or_if and not is_compound_condition_block:
163
- field_mappings["conditions"] = "condition"
164
-
165
- # Apply field mapping to current level
166
- for plural, singular in field_mappings.items():
167
- if plural in normalized and singular not in normalized:
168
- normalized[singular] = normalized.pop(plural)
169
- elif plural in normalized and singular in normalized:
170
- # Both exist - prefer singular, remove plural
171
- del normalized[plural]
139
+ # Apply field mapping to current level, preferring the canonical key.
140
+ for src, dst in field_mappings.items():
141
+ if src in normalized and dst not in normalized:
142
+ normalized[dst] = normalized.pop(src)
143
+ elif src in normalized and dst in normalized:
144
+ # Both present -- prefer the canonical key, drop the alias.
145
+ del normalized[src]
172
146
 
173
147
  # Recursively process all values in the dictionary
174
148
  for key, value in normalized.items():
175
- normalized[key] = _normalize_automation_config(value, key, is_root=False)
149
+ normalized[key] = _normalize_automation_config(value, is_root=False)
176
150
 
177
151
  return normalized
178
152
 
@@ -181,21 +155,32 @@ def _normalize_trigger_keys(triggers: list[dict[str, Any]]) -> list[dict[str, An
181
155
  """
182
156
  Normalize trigger objects for round-trip compatibility.
183
157
 
184
- Home Assistant GET API returns triggers with 'trigger' key for the platform type,
185
- but the SET API expects 'platform' key. This function converts between formats.
158
+ Older Home Assistant configs (and some integrations) still emit triggers
159
+ keyed by the legacy 'platform'. This tool canonicalizes each trigger to the
160
+ modern 'trigger' key (HA 2024.10+) so its pipeline and round-trip output use
161
+ one stable, current shape; HA accepts either form on the SET side.
186
162
 
187
163
  Args:
188
164
  triggers: List of trigger configuration dicts
189
165
 
190
166
  Returns:
191
- List of triggers with 'platform' key instead of 'trigger' key
167
+ List of triggers with 'trigger' key instead of 'platform' key
192
168
  """
193
169
  normalized_triggers = []
194
170
  for trigger in triggers:
171
+ # Defensive: a malformed (e.g. LLM-generated) item may not be a dict.
172
+ if not isinstance(trigger, dict):
173
+ normalized_triggers.append(trigger)
174
+ continue
195
175
  normalized_trigger = trigger.copy()
196
- # Convert 'trigger' key to 'platform' if present and 'platform' is not
197
- if "trigger" in normalized_trigger and "platform" not in normalized_trigger:
198
- normalized_trigger["platform"] = normalized_trigger.pop("trigger")
176
+ # Convert legacy 'platform' to modern 'trigger'. If both are present,
177
+ # drop the legacy alias so HA's strict schema doesn't reject the config
178
+ # with "extra keys not allowed" (mirrors _normalize_automation_config).
179
+ if "platform" in normalized_trigger:
180
+ if "trigger" not in normalized_trigger:
181
+ normalized_trigger["trigger"] = normalized_trigger.pop("platform")
182
+ else:
183
+ del normalized_trigger["platform"]
199
184
  normalized_triggers.append(normalized_trigger)
200
185
  return normalized_triggers
201
186
 
@@ -246,8 +231,9 @@ def _normalize_config_for_roundtrip(config: dict[str, Any]) -> dict[str, Any]:
246
231
  directly passed to ha_config_set_automation without modification.
247
232
 
248
233
  Transformations:
249
- 1. Field names: triggers -> trigger, actions -> action, conditions -> condition
250
- 2. Trigger keys: trigger -> platform (inside each trigger object)
234
+ 1. Field names: canonicalized to plural root keys (triggers/actions/conditions);
235
+ a stray `sequences` key is normalized to `sequence`.
236
+ 2. Trigger keys: platform -> trigger (inside each trigger object)
251
237
 
252
238
  Args:
253
239
  config: Raw automation configuration from HA API
@@ -255,16 +241,46 @@ def _normalize_config_for_roundtrip(config: dict[str, Any]) -> dict[str, Any]:
255
241
  Returns:
256
242
  Normalized configuration compatible with SET API
257
243
  """
258
- # First normalize field names (plural -> singular)
244
+ # First normalize field names (singular -> plural at the root level)
259
245
  normalized = _normalize_automation_config(config)
260
246
 
261
- # Then normalize trigger keys (trigger -> platform)
262
- if "trigger" in normalized and isinstance(normalized["trigger"], list):
263
- normalized["trigger"] = _normalize_trigger_keys(normalized["trigger"])
247
+ # Then normalize trigger keys (legacy 'platform' -> modern 'trigger')
248
+ if "triggers" in normalized and isinstance(normalized["triggers"], list):
249
+ normalized["triggers"] = _normalize_trigger_keys(normalized["triggers"])
264
250
 
265
251
  return cast(dict[str, Any], normalized)
266
252
 
267
253
 
254
+ def _detect_conflicting_root_keys(config: Any) -> list[str]:
255
+ """Warn when a config carries BOTH a singular alias and its canonical plural
256
+ root key with *different* values.
257
+
258
+ ``_normalize_automation_config`` keeps the canonical plural and silently drops
259
+ the singular alias, so a caller that set, say, ``config['trigger']`` on a
260
+ config that already has ``triggers`` would have that change discarded. The
261
+ config is malformed (a caller should send one form), but surfacing the
262
+ conflict beats dropping data silently.
263
+ """
264
+ if not isinstance(config, dict):
265
+ return []
266
+ warnings: list[str] = []
267
+ for singular, plural in (
268
+ ("trigger", "triggers"),
269
+ ("action", "actions"),
270
+ ("condition", "conditions"),
271
+ ):
272
+ if (
273
+ singular in config
274
+ and plural in config
275
+ and config[singular] != config[plural]
276
+ ):
277
+ warnings.append(
278
+ f"Config contains both '{singular}' and '{plural}' with different "
279
+ f"values; using the canonical '{plural}' and ignoring '{singular}'."
280
+ )
281
+ return warnings
282
+
283
+
268
284
  def _strip_redundant_identifier_echo(
269
285
  result: dict[str, Any],
270
286
  *,
@@ -420,8 +436,8 @@ class AutomationConfigTools:
420
436
  config: Annotated[
421
437
  dict[str, Any] | None,
422
438
  Field(
423
- description="Complete automation configuration with required fields: 'alias', 'trigger', 'action'. "
424
- "Optional: 'description', 'condition', 'mode', 'max', 'initial_state', 'variables'. "
439
+ description="Complete automation configuration with required fields: 'alias', 'triggers', 'actions'. "
440
+ "Optional: 'description', 'conditions', 'mode', 'max', 'initial_state', 'variables'. "
425
441
  "Mutually exclusive with python_transform.",
426
442
  default=None,
427
443
  ),
@@ -442,8 +458,8 @@ class AutomationConfigTools:
442
458
  "Requires identifier and config_hash for validation. "
443
459
  "WARNING: Expressions with infinite loops will hang the server. "
444
460
  "Examples: "
445
- "Simple: python_transform=\"config['action'][0]['data']['brightness'] = 255\" "
446
- "Pattern: python_transform=\"for a in config['action']: "
461
+ "Simple: python_transform=\"config['actions'][0]['data']['brightness'] = 255\" "
462
+ "Pattern: python_transform=\"for a in config['actions']: "
447
463
  "if a.get('alias') == 'My Step': a['data']['value'] = 100\" "
448
464
  "\n\n" + get_security_documentation(),
449
465
  ),
@@ -527,10 +543,11 @@ class AutomationConfigTools:
527
543
 
528
544
  IMPORTANT: python_transform requires 'identifier' and 'config_hash' from ha_config_get_automation().
529
545
 
530
- PYTHON TRANSFORM EXAMPLES:
531
- - Update action: python_transform="config['action'][0]['data']['brightness'] = 255"
532
- - Add trigger: python_transform="config['trigger'].append({'platform': 'state', 'entity_id': 'binary_sensor.motion', 'to': 'on'})"
533
- - Remove last action: python_transform="config['action'].pop()"
546
+ PYTHON TRANSFORM EXAMPLES (operate on the fetched config, which uses HA's
547
+ canonical plural root keys 'triggers'/'actions'/'conditions'):
548
+ - Update action: python_transform="config['actions'][0]['data']['brightness'] = 255"
549
+ - Add trigger: python_transform="config['triggers'].append({'trigger': 'state', 'entity_id': 'binary_sensor.motion', 'to': 'on'})"
550
+ - Remove last action: python_transform="config['actions'].pop()"
534
551
 
535
552
  Creates a new automation (if identifier omitted) or updates existing automation with provided configuration.
536
553
 
@@ -541,8 +558,8 @@ class AutomationConfigTools:
541
558
 
542
559
  REQUIRED FIELDS (Regular Automations):
543
560
  - alias: Human-readable automation name
544
- - trigger: List of trigger conditions (time, state, event, etc.)
545
- - action: List of actions to execute
561
+ - triggers: List of triggers (time, state, event, etc.)
562
+ - actions: List of actions to execute
546
563
 
547
564
  REQUIRED FIELDS (Blueprint Automations):
548
565
  - alias: Human-readable automation name
@@ -553,7 +570,7 @@ class AutomationConfigTools:
553
570
  OPTIONAL CONFIG FIELDS (Regular Automations):
554
571
  - description: Detailed description of the user's intent (RECOMMENDED: helps safely modify implementation later)
555
572
  - category: Category ID for organization (use ha_config_get_category to list, ha_config_set_category to create)
556
- - condition: Additional conditions that must be met
573
+ - conditions: Additional conditions that must be met
557
574
  - mode: 'single' (default), 'restart', 'queued', 'parallel'
558
575
  - max: Maximum concurrent executions (for queued/parallel modes)
559
576
  - initial_state: Whether automation starts enabled (true/false)
@@ -565,19 +582,19 @@ class AutomationConfigTools:
565
582
  ha_config_set_automation(config={
566
583
  "alias": "Morning Lights",
567
584
  "description": "Turn on bedroom lights at 7 AM to help wake up",
568
- "trigger": [{"platform": "time", "at": "07:00:00"}],
569
- "action": [{"action": "light.turn_on", "target": {"area_id": "bedroom"}}]
585
+ "triggers": [{"trigger": "time", "at": "07:00:00"}],
586
+ "actions": [{"action": "light.turn_on", "target": {"area_id": "bedroom"}}]
570
587
  })
571
588
 
572
589
  Motion-activated lighting — `for:` on the off-transition replaces action-delay:
573
590
  ha_config_set_automation(config={
574
591
  "alias": "Motion Light",
575
- "trigger": [
576
- {"platform": "state", "entity_id": "binary_sensor.motion", "to": "on", "id": "motion_on"},
577
- {"platform": "state", "entity_id": "binary_sensor.motion", "to": "off",
592
+ "triggers": [
593
+ {"trigger": "state", "entity_id": "binary_sensor.motion", "to": "on", "id": "motion_on"},
594
+ {"trigger": "state", "entity_id": "binary_sensor.motion", "to": "off",
578
595
  "for": {"minutes": 5}, "id": "motion_off"}
579
596
  ],
580
- "action": [
597
+ "actions": [
581
598
  {"choose": [
582
599
  {"conditions": [
583
600
  {"condition": "trigger", "id": "motion_on"},
@@ -595,8 +612,8 @@ class AutomationConfigTools:
595
612
  identifier="automation.morning_routine",
596
613
  config={
597
614
  "alias": "Updated Morning Routine",
598
- "trigger": [{"platform": "time", "at": "06:30:00"}],
599
- "action": [
615
+ "triggers": [{"trigger": "time", "at": "06:30:00"}],
616
+ "actions": [
600
617
  {"action": "light.turn_on", "target": {"area_id": "bedroom"}},
601
618
  {"action": "climate.set_temperature", "target": {"entity_id": "climate.bedroom"}, "data": {"temperature": 22}}
602
619
  ]
@@ -714,7 +731,12 @@ class AutomationConfigTools:
714
731
  config_category = config_dict.pop("category", None)
715
732
  effective_category = category if category is not None else config_category
716
733
 
717
- # Normalize field names (triggers -> trigger, actions -> action, etc.)
734
+ # Detect conflicting singular+plural root keys BEFORE normalization
735
+ # drops the singular alias (surface rather than silently discard).
736
+ conflict_warnings = _detect_conflicting_root_keys(config_dict)
737
+
738
+ # Normalize field names to HA's canonical plural root keys
739
+ # (trigger -> triggers, action -> actions, condition -> conditions).
718
740
  config_dict = _normalize_automation_config(config_dict)
719
741
 
720
742
  # Optional hash check for full config updates
@@ -735,6 +757,7 @@ class AutomationConfigTools:
735
757
  bp_warnings,
736
758
  validation_meta,
737
759
  MandatoryBPS,
760
+ conflict_warnings,
738
761
  )
739
762
 
740
763
  except ToolError as te:
@@ -750,7 +773,7 @@ class AutomationConfigTools:
750
773
  error_text = str(e)
751
774
  suggestions = [
752
775
  "Check automation configuration format",
753
- "Ensure required fields: alias, trigger, action",
776
+ "Ensure required fields: alias, triggers, actions",
754
777
  "Use entity_id format: automation.morning_routine or unique_id",
755
778
  "Use ha_search(domain_filter='automation') to find automations",
756
779
  "Use ha_get_skill_guide for automation examples",
@@ -844,6 +867,11 @@ class AutomationConfigTools:
844
867
  transform_category = transformed_config.pop("category", None)
845
868
  effective_category = category if category is not None else transform_category
846
869
 
870
+ # Detect conflicting singular+plural root keys (e.g. a transform that set
871
+ # the singular 'trigger' on a fetched plural config) before normalization
872
+ # drops the singular alias.
873
+ conflict_warnings = _detect_conflicting_root_keys(transformed_config)
874
+
847
875
  transformed_config = _normalize_automation_config(transformed_config)
848
876
  self._validate_required_fields(transformed_config, identifier)
849
877
  bp_warnings = _check_best_practices(transformed_config)
@@ -851,6 +879,8 @@ class AutomationConfigTools:
851
879
  result = await self._client.upsert_automation_config(
852
880
  transformed_config, identifier
853
881
  )
882
+ for warning in conflict_warnings:
883
+ result.setdefault("warnings", []).append(warning)
854
884
  refetched = await self._get_automation_config_internal(identifier)
855
885
  new_config_hash = refetched[1]
856
886
 
@@ -895,10 +925,14 @@ class AutomationConfigTools:
895
925
  bp_warnings: BestPracticeCheckResult,
896
926
  validation_meta: dict[str, Any],
897
927
  MandatoryBPS: bool,
928
+ conflict_warnings: list[str] | None = None,
898
929
  ) -> dict[str, Any]:
899
930
  """Execute config-replacement mode and return the tool response."""
900
931
  result = await self._client.upsert_automation_config(config_dict, identifier)
901
932
 
933
+ for warning in conflict_warnings or []:
934
+ result.setdefault("warnings", []).append(warning)
935
+
902
936
  if result.get("entity_not_verified"):
903
937
  result.setdefault("warnings", []).append(
904
938
  f"{NOT_VERIFIED_WARNING_PREFIX} "
@@ -1098,13 +1132,13 @@ class AutomationConfigTools:
1098
1132
  config_dict: dict[str, Any], identifier: str | None
1099
1133
  ) -> None:
1100
1134
  """Raise if an empty-trigger config wraps scene.create (common model misroute)."""
1101
- trigger_value = config_dict.get("trigger")
1135
+ trigger_value = config_dict.get("triggers")
1102
1136
  trigger_empty = trigger_value is None or (
1103
1137
  isinstance(trigger_value, list) and not trigger_value
1104
1138
  )
1105
1139
  if not trigger_empty:
1106
1140
  return
1107
- actions_list = coerce_to_list(config_dict.get("action"))
1141
+ actions_list = coerce_to_list(config_dict.get("actions"))
1108
1142
  scene_create_indices = [
1109
1143
  i for i, a in enumerate(actions_list) if _action_contains_scene_create(a)
1110
1144
  ]
@@ -1134,7 +1168,7 @@ class AutomationConfigTools:
1134
1168
  @staticmethod
1135
1169
  def _validate_condition_platform(config_dict: dict[str, Any]) -> None:
1136
1170
  """Raise if any condition uses 'platform' (trigger syntax) instead of 'condition'."""
1137
- for idx, cond in enumerate(coerce_to_list(config_dict.get("condition"))):
1171
+ for idx, cond in enumerate(coerce_to_list(config_dict.get("conditions"))):
1138
1172
  if not isinstance(cond, dict):
1139
1173
  continue
1140
1174
  if "platform" in cond and "condition" not in cond:
@@ -1148,7 +1182,7 @@ class AutomationConfigTools:
1148
1182
  suggestions=[
1149
1183
  f"Replace 'platform' with 'condition': "
1150
1184
  f"{{'condition': '{cond['platform']}', ...}}",
1151
- "Triggers use 'platform'; conditions use 'condition'.",
1185
+ "Triggers use 'trigger'; conditions use 'condition'.",
1152
1186
  ],
1153
1187
  context={"condition_index": idx, "found_key": "platform"},
1154
1188
  )
@@ -1161,12 +1195,12 @@ class AutomationConfigTools:
1161
1195
  """Validate required fields and prevent duplicate creation."""
1162
1196
  if "use_blueprint" in config_dict:
1163
1197
  required_fields = ["alias"]
1164
- # Strip empty trigger/action/condition arrays that would override blueprint
1165
- for field in ["trigger", "action", "condition"]:
1198
+ # Strip empty triggers/actions/conditions arrays that would override blueprint
1199
+ for field in ["triggers", "actions", "conditions"]:
1166
1200
  if field in config_dict and config_dict[field] == []:
1167
1201
  del config_dict[field]
1168
1202
  else:
1169
- required_fields = ["alias", "trigger", "action"]
1203
+ required_fields = ["alias", "triggers", "actions"]
1170
1204
 
1171
1205
  missing_fields = [f for f in required_fields if f not in config_dict]
1172
1206
  if missing_fields:
@@ -1174,7 +1208,7 @@ class AutomationConfigTools:
1174
1208
  # script — point them at ha_config_set_script instead of the generic
1175
1209
  # missing-fields error.
1176
1210
  if "sequence" in config_dict and (
1177
- "trigger" in missing_fields or "action" in missing_fields
1211
+ "triggers" in missing_fields or "actions" in missing_fields
1178
1212
  ):
1179
1213
  context: dict[str, Any] = {"missing_fields": missing_fields}
1180
1214
  if identifier:
@@ -1185,11 +1219,11 @@ class AutomationConfigTools:
1185
1219
  message=f"Missing required fields: {', '.join(missing_fields)}",
1186
1220
  details=(
1187
1221
  "Config contains 'sequence', which belongs to scripts. "
1188
- "Automations use 'trigger' and 'action'; scripts use 'sequence'."
1222
+ "Automations use 'triggers' and 'actions'; scripts use 'sequence'."
1189
1223
  ),
1190
1224
  suggestions=[
1191
1225
  "Did you mean ha_config_set_script? Scripts use 'sequence' directly.",
1192
- "For an automation, replace 'sequence' with 'action' and add a 'trigger'.",
1226
+ "For an automation, replace 'sequence' with 'actions' and add 'triggers'.",
1193
1227
  ],
1194
1228
  context=context,
1195
1229
  )
@@ -2331,7 +2331,7 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2331
2331
  default=None,
2332
2332
  description=(
2333
2333
  "Return only the specified keys from each entity's attributes dict "
2334
- '(e.g. ["brightness", "color_temp"] for lights). '
2334
+ '(e.g. ["brightness", "color_temp_kelvin"] for lights). '
2335
2335
  "None = full attributes (default). "
2336
2336
  "Unknown keys are silently dropped. "
2337
2337
  'Requires "attributes" to be present in fields= (or fields=None).'
@@ -81,7 +81,10 @@ _STATE_CHANGING_SERVICES = {
81
81
  "set_temperature",
82
82
  "set_hvac_mode",
83
83
  "set_fan_mode",
84
- "set_speed",
84
+ # fan.set_speed was removed in the HA percentage migration (gone in 2026.6);
85
+ # its state-changing successors are set_percentage / set_preset_mode.
86
+ "set_percentage",
87
+ "set_preset_mode",
85
88
  "select_option",
86
89
  "set_value",
87
90
  "set_datetime",
@@ -74,7 +74,10 @@ DOMAIN_HANDLERS = {
74
74
  },
75
75
  "fan": {
76
76
  "valid_actions": ["on", "off", "toggle", "set"],
77
- "parameters": ["speed", "percentage", "preset_mode", "direction"],
77
+ # HA removed the legacy `speed` param / `fan.set_speed` service in the
78
+ # 2021-2022 percentage migration (absent in 2026.6). Use percentage
79
+ # (fan.set_percentage) and preset_mode (fan.set_preset_mode).
80
+ "parameters": ["percentage", "preset_mode", "direction"],
78
81
  "quick_actions": ["toggle", "speed_up", "speed_down"],
79
82
  "state_attributes": ["percentage", "preset_mode"],
80
83
  "supports_speed": True,
@@ -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.dev676
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