ha-mcp-dev 7.12.0.dev1834__tar.gz → 7.12.0.dev1837__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 (162) hide show
  1. {ha_mcp_dev-7.12.0.dev1834/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.0.dev1837}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/pyproject.toml +70 -4
  3. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/best_practice_checker.py +201 -106
  4. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_calendar.py +116 -80
  5. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_config_dashboards.py +1323 -949
  6. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_mcp_component.py +80 -67
  7. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_resources.py +42 -36
  8. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_search.py +76 -41
  9. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_service.py +100 -48
  10. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_utility.py +138 -94
  11. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/util_helpers.py +80 -58
  12. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  13. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/LICENSE +0 -0
  14. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/MANIFEST.in +0 -0
  15. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/README.md +0 -0
  16. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/setup.cfg +0 -0
  17. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/__init__.py +0 -0
  18. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/__main__.py +0 -0
  19. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/_pypi_marker +0 -0
  20. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/_version.py +0 -0
  21. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/auth/__init__.py +0 -0
  22. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/auth/consent_form.py +0 -0
  23. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/auth/provider.py +0 -0
  24. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/backup_manager.py +0 -0
  25. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/browser_landing.py +0 -0
  26. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/client/__init__.py +0 -0
  27. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/client/rest_client.py +0 -0
  28. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/client/supervisor_client.py +0 -0
  29. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/client/websocket_client.py +0 -0
  30. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/client/websocket_listener.py +0 -0
  31. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/config.py +0 -0
  32. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  33. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  34. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  35. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/errors.py +0 -0
  36. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/policy/__init__.py +0 -0
  37. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/policy/approval_queue.py +0 -0
  38. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/policy/evaluator.py +0 -0
  39. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/policy/handlers.py +0 -0
  40. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/policy/middleware.py +0 -0
  41. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/policy/model.py +0 -0
  42. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/policy/persistence.py +0 -0
  43. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/policy/value_sources.py +0 -0
  44. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/py.typed +0 -0
  45. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/read_only.py +0 -0
  46. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  47. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  48. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  49. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  50. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  51. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  52. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  53. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  54. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  55. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  56. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  57. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  58. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  59. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  60. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  61. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  62. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  63. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  64. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  65. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  66. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  67. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  68. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  69. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  70. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  71. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/server.py +0 -0
  72. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/settings_ui/__init__.py +0 -0
  73. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/settings_ui/settings.css +0 -0
  74. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/settings_ui/settings.html +0 -0
  75. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/settings_ui/settings.js +0 -0
  76. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/smoke_test.py +0 -0
  77. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  78. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/__init__.py +0 -0
  79. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/auto_backup.py +0 -0
  80. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/backup.py +0 -0
  81. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  82. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/device_control.py +0 -0
  83. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/enhanced.py +0 -0
  84. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/helpers.py +0 -0
  85. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/radio/__init__.py +0 -0
  86. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/radio/base.py +0 -0
  87. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/radio/matter.py +0 -0
  88. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/radio/thread.py +0 -0
  89. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  90. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/radio/zwave.py +0 -0
  91. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/reference_validator.py +0 -0
  92. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/registry.py +0 -0
  93. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  94. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  95. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  96. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  97. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  98. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  99. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  100. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  101. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  102. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  103. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_addons.py +0 -0
  104. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_areas.py +0 -0
  105. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  106. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  107. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_camera.py +0 -0
  108. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_categories.py +0 -0
  109. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_code.py +0 -0
  110. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  111. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  112. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  113. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  114. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  115. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_dev.py +0 -0
  116. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_energy.py +0 -0
  117. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_entities.py +0 -0
  118. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  119. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_groups.py +0 -0
  120. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_hacs.py +0 -0
  121. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_history.py +0 -0
  122. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_integrations.py +0 -0
  123. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_labels.py +0 -0
  124. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_radio.py +0 -0
  125. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_registry.py +0 -0
  126. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_services.py +0 -0
  127. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_system.py +0 -0
  128. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_themes.py +0 -0
  129. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_todo.py +0 -0
  130. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_traces.py +0 -0
  131. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_updates.py +0 -0
  132. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  133. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  134. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/tools_zones.py +0 -0
  135. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/tools/validation_middleware.py +0 -0
  136. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/transforms/__init__.py +0 -0
  137. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/transforms/categorized_search.py +0 -0
  138. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  139. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/transport_security.py +0 -0
  140. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/update_check.py +0 -0
  141. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/utils/__init__.py +0 -0
  142. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/utils/config_hash.py +0 -0
  143. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/utils/data_paths.py +0 -0
  144. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/utils/domain_handlers.py +0 -0
  145. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  146. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  147. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/utils/operation_manager.py +0 -0
  148. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/utils/python_sandbox.py +0 -0
  149. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/utils/skill_loader.py +0 -0
  150. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/utils/usage_logger.py +0 -0
  151. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/visibility/__init__.py +0 -0
  152. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/visibility/model.py +0 -0
  153. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/visibility/persistence.py +0 -0
  154. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp/visibility/resolver.py +0 -0
  155. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  156. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  157. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  158. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  159. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  160. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/tests/__init__.py +0 -0
  161. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/tests/test_constants.py +0 -0
  162. {ha_mcp_dev-7.12.0.dev1834 → ha_mcp_dev-7.12.0.dev1837}/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.12.0.dev1834
3
+ Version: 7.12.0.dev1837
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.12.0.dev1834"
7
+ version = "7.12.0.dev1837"
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
@@ -129,6 +129,7 @@ select = [
129
129
  "I", # isort
130
130
  "B", # flake8-bugbear
131
131
  "C4", # flake8-comprehensions
132
+ "C90", # mccabe complexity
132
133
  "UP", # pyupgrade
133
134
  "RUF", # ruff-specific rules
134
135
  "PIE", # misc lints
@@ -159,9 +160,74 @@ ignore = [
159
160
  [tool.ruff.lint.per-file-ignores]
160
161
  "__init__.py" = ["F401"]
161
162
  "tests/**/*" = ["E501", "B011"]
162
- # C901 ignores for tools files with complex methods (see #925).
163
- # Remove lines as individual methods are simplified below threshold.
164
- "src/ha_mcp/tools/tools_config_dashboards.py" = ["C901"]
163
+ # C901 (mccabe complexity) grandfathered debt: newly enabled rule, these files
164
+ # already exceeded the threshold. Remove a line once its functions are
165
+ # refactored below threshold -- do not add new files here.
166
+ "custom_components/ha_mcp_tools/__init__.py" = ["C901"]
167
+ "custom_components/ha_mcp_tools/mcp_webhook.py" = ["C901"]
168
+ "homeassistant-addon/start.py" = ["C901"]
169
+ "homeassistant-addon-webhook-proxy-dev/mcp_proxy_dev/__init__.py" = ["C901"]
170
+ "homeassistant-addon-webhook-proxy-dev/start.py" = ["C901"]
171
+ "homeassistant-addon-webhook-proxy/mcp_proxy/__init__.py" = ["C901"]
172
+ "homeassistant-addon-webhook-proxy/start.py" = ["C901"]
173
+ "packaging/mcpb/generate_manifest.py" = ["C901"]
174
+ "scripts/extract_tools.py" = ["C901"]
175
+ "src/ha_mcp/backup_manager.py" = ["C901"]
176
+ "src/ha_mcp/client/rest_client.py" = ["C901"]
177
+ "src/ha_mcp/config.py" = ["C901"]
178
+ "src/ha_mcp/policy/evaluator.py" = ["C901"]
179
+ "src/ha_mcp/policy/handlers.py" = ["C901"]
180
+ "src/ha_mcp/server.py" = ["C901"]
181
+ "src/ha_mcp/settings_ui/__init__.py" = ["C901"]
182
+ "src/ha_mcp/tools/auto_backup.py" = ["C901"]
183
+ "src/ha_mcp/tools/backup.py" = ["C901"]
184
+ "src/ha_mcp/tools/radio/matter.py" = ["C901"]
185
+ "src/ha_mcp/tools/radio/zigbee.py" = ["C901"]
186
+ "src/ha_mcp/tools/radio/zwave.py" = ["C901"]
187
+ "src/ha_mcp/tools/reference_validator.py" = ["C901"]
188
+ "src/ha_mcp/tools/tools_addons.py" = ["C901"]
189
+ "src/ha_mcp/tools/tools_areas.py" = ["C901"]
190
+ "src/ha_mcp/tools/tools_code.py" = ["C901"]
191
+ "src/ha_mcp/tools/tools_config_automations.py" = ["C901"]
192
+ "src/ha_mcp/tools/tools_config_scenes.py" = ["C901"]
193
+ "src/ha_mcp/tools/tools_config_scripts.py" = ["C901"]
194
+ "src/ha_mcp/tools/tools_dev.py" = ["C901"]
195
+ "src/ha_mcp/tools/tools_energy.py" = ["C901"]
196
+ "src/ha_mcp/tools/tools_hacs.py" = ["C901"]
197
+ "src/ha_mcp/tools/tools_history.py" = ["C901"]
198
+ "src/ha_mcp/tools/tools_integrations.py" = ["C901"]
199
+ "src/ha_mcp/tools/tools_system.py" = ["C901"]
200
+ "src/ha_mcp/tools/tools_traces.py" = ["C901"]
201
+ "src/ha_mcp/tools/tools_updates.py" = ["C901"]
202
+ "src/ha_mcp/tools/tools_yaml_config.py" = ["C901"]
203
+ "src/ha_mcp/tools/tools_zones.py" = ["C901"]
204
+ "src/ha_mcp/transforms/categorized_search.py" = ["C901"]
205
+ "src/ha_mcp/utils/data_paths.py" = ["C901"]
206
+ "src/ha_mcp/utils/fuzzy_search.py" = ["C901"]
207
+ "src/ha_mcp/utils/skill_loader.py" = ["C901"]
208
+ "src/ha_mcp/visibility/resolver.py" = ["C901"]
209
+ "tests/src/e2e/conftest.py" = ["C901"]
210
+ "tests/src/e2e/error_handling/test_network_errors.py" = ["C901"]
211
+ "tests/src/e2e/policy/test_readonly_mode.py" = ["C901"]
212
+ "tests/src/e2e/utilities/cleanup.py" = ["C901"]
213
+ "tests/src/e2e/utilities/wait_helpers.py" = ["C901"]
214
+ "tests/src/e2e/workflows/automation/test_helpers.py" = ["C901"]
215
+ "tests/src/e2e/workflows/automation/test_lifecycle.py" = ["C901"]
216
+ "tests/src/e2e/workflows/core/test_bulk.py" = ["C901"]
217
+ "tests/src/e2e/workflows/device_control/test_lights.py" = ["C901"]
218
+ "tests/src/e2e/workflows/scripts/test_lifecycle.py" = ["C901"]
219
+ "tests/src/haos_runtime.py" = ["C901"]
220
+ "tests/src/unit/_embedded_stubs.py" = ["C901"]
221
+ "tests/src/unit/test_action_discriminator.py" = ["C901"]
222
+ "tests/src/unit/test_advanced_settings_coverage.py" = ["C901"]
223
+ "tests/src/unit/test_fields_projection_docstring_completeness.py" = ["C901"]
224
+ "tests/src/unit/test_read_only.py" = ["C901"]
225
+ "tests/src/unit/test_settings_ui_js_behavior.py" = ["C901"]
226
+ "tests/test_env_manager.py" = ["C901"]
227
+ "tests/uat/ha_wait.py" = ["C901"]
228
+ "tests/uat/openai_agent.py" = ["C901"]
229
+ "tests/uat/run_uat.py" = ["C901"]
230
+ "tests/uat/stories/run_story.py" = ["C901"]
165
231
 
166
232
  [tool.pytest.ini_options]
167
233
  testpaths = ["tests"]
@@ -388,6 +388,11 @@ def _check_template_string(
388
388
  prefix AND the suggestion text adapt if a future caller passes "trigger".
389
389
  The native shapes named here (numeric_state, state, time, sun) work as
390
390
  both conditions and triggers in HA — only the noun changes.
391
+
392
+ Detection is split across three helpers (comparison/sun/is_state, time
393
+ patterns, then state-list/direct-state/duration) purely to keep each
394
+ function's branching low. They run in the same order as before and
395
+ share ``duration_match`` so the split is invisible to callers.
391
396
  """
392
397
  initial_count = len(warnings)
393
398
  label = position.capitalize()
@@ -400,6 +405,44 @@ def _check_template_string(
400
405
  # fires instead).
401
406
  duration_match = _RE_DURATION_MATH.search(template)
402
407
 
408
+ _check_template_comparison_patterns(
409
+ template, warnings, skill_prefix, position, label, duration_match
410
+ )
411
+ _check_template_time_patterns(template, warnings, skill_prefix, position, label)
412
+ _check_template_state_and_duration_patterns(
413
+ template, warnings, skill_prefix, position, label, duration_match
414
+ )
415
+
416
+ # Generic fallback: any Jinja in this logic position that didn't match
417
+ # a specific detector. Catches new anti-patterns (issue #1011) and
418
+ # reframes #695 from "enumerate bad shapes" to "surface every template
419
+ # in a logic position". Specific detectors above keep their tailored
420
+ # messages.
421
+ if len(warnings) == initial_count and _RE_ANY_TEMPLATE.search(template):
422
+ _emit(
423
+ warnings,
424
+ f"Template detected in {position} — if this maps to a native option "
425
+ "(`numeric_state`, `state`, `time`, `sun`, `zone`, `device`), use that "
426
+ "instead. Templates fail silently at runtime and bypass schema validation.",
427
+ skill_prefix,
428
+ "template-guidelines.md#when-to-avoid-templates",
429
+ )
430
+
431
+
432
+ def _check_template_comparison_patterns(
433
+ template: str,
434
+ warnings: BestPracticeCheckResult,
435
+ skill_prefix: str | None,
436
+ position: str,
437
+ label: str,
438
+ duration_match: re.Match[str] | None,
439
+ ) -> None:
440
+ """Flag numeric-comparison, `sun.sun`, and `is_state()` template shapes.
441
+
442
+ Split out of :func:`_check_template_string` to keep its complexity low.
443
+ ``duration_match`` is precomputed by the caller so the numeric-comparison
444
+ check can suppress its suggestion when duration math is also present.
445
+ """
403
446
  if _RE_NUMERIC_CMP.search(template) and not duration_match:
404
447
  _emit(
405
448
  warnings,
@@ -429,6 +472,19 @@ def _check_template_string(
429
472
  skill_prefix,
430
473
  "automation-patterns.md#native-conditions",
431
474
  )
475
+
476
+
477
+ def _check_template_time_patterns(
478
+ template: str,
479
+ warnings: BestPracticeCheckResult,
480
+ skill_prefix: str | None,
481
+ position: str,
482
+ label: str,
483
+ ) -> None:
484
+ """Flag `now().hour/minute`, weekday, and date-based template shapes.
485
+
486
+ Split out of :func:`_check_template_string` to keep its complexity low.
487
+ """
432
488
  if _RE_NOW_TIME.search(template):
433
489
  _emit(
434
490
  warnings,
@@ -458,6 +514,21 @@ def _check_template_string(
458
514
  skill_prefix,
459
515
  "automation-patterns.md#native-conditions",
460
516
  )
517
+
518
+
519
+ def _check_template_state_and_duration_patterns(
520
+ template: str,
521
+ warnings: BestPracticeCheckResult,
522
+ skill_prefix: str | None,
523
+ position: str,
524
+ label: str,
525
+ duration_match: re.Match[str] | None,
526
+ ) -> None:
527
+ """Flag `states(...) in [...]`, direct-state-access, and duration-math shapes.
528
+
529
+ Split out of :func:`_check_template_string` to keep its complexity low.
530
+ ``duration_match`` is precomputed by the caller.
531
+ """
461
532
  if _RE_STATE_IN.search(template):
462
533
  _emit(
463
534
  warnings,
@@ -489,21 +560,6 @@ def _check_template_string(
489
560
  "automation-patterns.md#native-conditions",
490
561
  )
491
562
 
492
- # Generic fallback: any Jinja in this logic position that didn't match
493
- # a specific detector. Catches new anti-patterns (issue #1011) and
494
- # reframes #695 from "enumerate bad shapes" to "surface every template
495
- # in a logic position". Specific detectors above keep their tailored
496
- # messages.
497
- if len(warnings) == initial_count and _RE_ANY_TEMPLATE.search(template):
498
- _emit(
499
- warnings,
500
- f"Template detected in {position} — if this maps to a native option "
501
- "(`numeric_state`, `state`, `time`, `sun`, `zone`, `device`), use that "
502
- "instead. Templates fail silently at runtime and bypass schema validation.",
503
- skill_prefix,
504
- "template-guidelines.md#when-to-avoid-templates",
505
- )
506
-
507
563
 
508
564
  # ---------------------------------------------------------------------------
509
565
  # Action tree checks
@@ -687,42 +743,21 @@ def _check_target_dict(
687
743
  def _check_triggers(
688
744
  triggers: Any, warnings: BestPracticeCheckResult, skill_prefix: str | None
689
745
  ) -> None:
690
- """Check triggers for device_id and template anti-patterns."""
746
+ """Check triggers for device_id and template anti-patterns.
747
+
748
+ Per-trigger detection is split into focused helpers (renamed-key check,
749
+ deprecated-behavior check, template-trigger check, numeric_state-trigger
750
+ check) purely to keep this function's complexity low. They run in the
751
+ same order as before.
752
+ """
691
753
  for trigger in _as_list(triggers):
692
754
  if not isinstance(trigger, dict):
693
755
  continue
694
756
 
695
757
  platform = trigger.get("platform", trigger.get("trigger", ""))
696
758
 
697
- # 2026.7 renamed purpose-specific trigger keys — old keys no longer load.
698
- renamed_trigger = (
699
- _RENAMED_TRIGGER_KEYS.get(platform) if isinstance(platform, str) else None
700
- )
701
- if renamed_trigger:
702
- _emit(
703
- warnings,
704
- f"Trigger key `{platform}` was renamed to `{renamed_trigger}` "
705
- "in HA 2026.7 and the old key no longer loads — use "
706
- f"`trigger: {renamed_trigger}`.",
707
- skill_prefix,
708
- "automation-patterns.md#trigger-types",
709
- )
710
-
711
- # 2026.7 renamed trigger `options.behavior` values (any→each, last→all).
712
- options = trigger.get("options")
713
- if isinstance(options, dict):
714
- behavior = options.get("behavior")
715
- if isinstance(behavior, str) and behavior in _DEPRECATED_TRIGGER_BEHAVIOR:
716
- _emit(
717
- warnings,
718
- f"Trigger `options.behavior: {behavior}` was renamed to "
719
- f"`{_DEPRECATED_TRIGGER_BEHAVIOR[behavior]}` in HA 2026.7 — "
720
- "the old value still loads but raises a repair issue and "
721
- "will be removed. Valid trigger values: `each`, `first`, "
722
- "`all` (conditions keep `any`/`all`).",
723
- skill_prefix,
724
- "automation-patterns.md#trigger-types",
725
- )
759
+ _check_renamed_trigger_key(platform, warnings, skill_prefix)
760
+ _check_deprecated_trigger_behavior(trigger, warnings, skill_prefix)
726
761
 
727
762
  # Device trigger → prefer entity_id-based triggers
728
763
  if platform == "device":
@@ -739,71 +774,131 @@ def _check_triggers(
739
774
 
740
775
  # Template trigger — specific shapes first, generic fallback after.
741
776
  if platform == "template":
742
- vt = trigger.get("value_template", "")
743
- if isinstance(vt, str):
744
- initial = len(warnings)
745
- # See `_check_template_string`: duration math also trips the numeric
746
- # comparison detector, but maps to `for:`, not `numeric_state`. Suppress
747
- # the numeric_state suggestion when duration math is present.
748
- duration_match = _RE_DURATION_MATH.search(vt)
749
- if _RE_NUMERIC_CMP.search(vt) and not duration_match:
750
- _emit(
751
- warnings,
752
- "Trigger uses template with float/int comparison — "
753
- "use native `numeric_state` trigger instead "
754
- "(e.g., `platform: numeric_state, entity_id: sensor.temp, above: 30`).",
755
- skill_prefix,
756
- "automation-patterns.md#trigger-types",
757
- )
758
- if _RE_IS_STATE.search(vt):
759
- _emit(
760
- warnings,
761
- "Trigger uses template with `is_state()` — use "
762
- "native `state` trigger instead "
763
- "(e.g., `platform: state, entity_id: light.x, to: 'on'`).",
764
- skill_prefix,
765
- "automation-patterns.md#trigger-types",
766
- )
767
- if duration_match:
768
- _emit(
769
- warnings,
770
- "Trigger uses template for duration/recency check "
771
- "(`now() - X.last_changed/last_updated`) — use the native "
772
- "`for:` field on a `state` trigger instead "
773
- "(e.g., `platform: state, entity_id: binary_sensor.motion, "
774
- "to: 'off', for: {minutes: 5}`). Native `for:` is event-driven "
775
- "and doesn't re-evaluate on every state change.",
776
- skill_prefix,
777
- "automation-patterns.md#trigger-types",
778
- )
779
- # Generic fallback for unmatched template triggers.
780
- if len(warnings) == initial and _RE_ANY_TEMPLATE.search(vt):
781
- _emit(
782
- warnings,
783
- "Trigger uses `template` platform — if this maps to a native option "
784
- "(`state`, `numeric_state`, `time`, `time_pattern`, `sun`, `zone`, "
785
- "`event`), use that instead. Native triggers are event-driven; "
786
- "template triggers re-evaluate on every state change.",
787
- skill_prefix,
788
- "automation-patterns.md#trigger-types",
789
- )
777
+ _check_template_trigger(trigger, warnings, skill_prefix)
790
778
 
791
779
  # numeric_state trigger: value_template can also contain duration math
792
780
  # (e.g. transforming last_changed into a seconds value for the threshold).
793
781
  if platform == "numeric_state":
794
- vt = trigger.get("value_template", "")
795
- if isinstance(vt, str) and _RE_DURATION_MATH.search(vt):
796
- _emit(
797
- warnings,
798
- "`numeric_state` trigger uses `value_template` for duration/recency "
799
- "check (`now() - X.last_changed/last_updated`) — use the native "
800
- "`for:` field on a `state` trigger instead "
801
- "(e.g., `platform: state, entity_id: binary_sensor.motion, "
802
- "to: 'off', for: {minutes: 5}`). Native `for:` is event-driven "
803
- "and doesn't re-evaluate on every state change.",
804
- skill_prefix,
805
- "automation-patterns.md#trigger-types",
806
- )
782
+ _check_numeric_state_trigger(trigger, warnings, skill_prefix)
783
+
784
+
785
+ def _check_renamed_trigger_key(
786
+ platform: Any, warnings: BestPracticeCheckResult, skill_prefix: str | None
787
+ ) -> None:
788
+ """Flag HA 2026.7 purpose-specific trigger key renames (old keys no longer load)."""
789
+ renamed_trigger = (
790
+ _RENAMED_TRIGGER_KEYS.get(platform) if isinstance(platform, str) else None
791
+ )
792
+ if renamed_trigger:
793
+ _emit(
794
+ warnings,
795
+ f"Trigger key `{platform}` was renamed to `{renamed_trigger}` "
796
+ "in HA 2026.7 and the old key no longer loads — use "
797
+ f"`trigger: {renamed_trigger}`.",
798
+ skill_prefix,
799
+ "automation-patterns.md#trigger-types",
800
+ )
801
+
802
+
803
+ def _check_deprecated_trigger_behavior(
804
+ trigger: dict[str, Any], warnings: BestPracticeCheckResult, skill_prefix: str | None
805
+ ) -> None:
806
+ """Flag HA 2026.7 trigger `options.behavior` value renames (any→each, last→all)."""
807
+ options = trigger.get("options")
808
+ if not isinstance(options, dict):
809
+ return
810
+ behavior = options.get("behavior")
811
+ if isinstance(behavior, str) and behavior in _DEPRECATED_TRIGGER_BEHAVIOR:
812
+ _emit(
813
+ warnings,
814
+ f"Trigger `options.behavior: {behavior}` was renamed to "
815
+ f"`{_DEPRECATED_TRIGGER_BEHAVIOR[behavior]}` in HA 2026.7 — "
816
+ "the old value still loads but raises a repair issue and "
817
+ "will be removed. Valid trigger values: `each`, `first`, "
818
+ "`all` (conditions keep `any`/`all`).",
819
+ skill_prefix,
820
+ "automation-patterns.md#trigger-types",
821
+ )
822
+
823
+
824
+ def _check_template_trigger(
825
+ trigger: dict[str, Any], warnings: BestPracticeCheckResult, skill_prefix: str | None
826
+ ) -> None:
827
+ """Check a `template` trigger's `value_template` for anti-patterns.
828
+
829
+ Split out of :func:`_check_triggers` to keep its complexity low.
830
+ """
831
+ vt = trigger.get("value_template", "")
832
+ if not isinstance(vt, str):
833
+ return
834
+ initial = len(warnings)
835
+ # See `_check_template_string`: duration math also trips the numeric
836
+ # comparison detector, but maps to `for:`, not `numeric_state`. Suppress
837
+ # the numeric_state suggestion when duration math is present.
838
+ duration_match = _RE_DURATION_MATH.search(vt)
839
+ if _RE_NUMERIC_CMP.search(vt) and not duration_match:
840
+ _emit(
841
+ warnings,
842
+ "Trigger uses template with float/int comparison — "
843
+ "use native `numeric_state` trigger instead "
844
+ "(e.g., `platform: numeric_state, entity_id: sensor.temp, above: 30`).",
845
+ skill_prefix,
846
+ "automation-patterns.md#trigger-types",
847
+ )
848
+ if _RE_IS_STATE.search(vt):
849
+ _emit(
850
+ warnings,
851
+ "Trigger uses template with `is_state()` — use "
852
+ "native `state` trigger instead "
853
+ "(e.g., `platform: state, entity_id: light.x, to: 'on'`).",
854
+ skill_prefix,
855
+ "automation-patterns.md#trigger-types",
856
+ )
857
+ if duration_match:
858
+ _emit(
859
+ warnings,
860
+ "Trigger uses template for duration/recency check "
861
+ "(`now() - X.last_changed/last_updated`) — use the native "
862
+ "`for:` field on a `state` trigger instead "
863
+ "(e.g., `platform: state, entity_id: binary_sensor.motion, "
864
+ "to: 'off', for: {minutes: 5}`). Native `for:` is event-driven "
865
+ "and doesn't re-evaluate on every state change.",
866
+ skill_prefix,
867
+ "automation-patterns.md#trigger-types",
868
+ )
869
+ # Generic fallback for unmatched template triggers.
870
+ if len(warnings) == initial and _RE_ANY_TEMPLATE.search(vt):
871
+ _emit(
872
+ warnings,
873
+ "Trigger uses `template` platform — if this maps to a native option "
874
+ "(`state`, `numeric_state`, `time`, `time_pattern`, `sun`, `zone`, "
875
+ "`event`), use that instead. Native triggers are event-driven; "
876
+ "template triggers re-evaluate on every state change.",
877
+ skill_prefix,
878
+ "automation-patterns.md#trigger-types",
879
+ )
880
+
881
+
882
+ def _check_numeric_state_trigger(
883
+ trigger: dict[str, Any], warnings: BestPracticeCheckResult, skill_prefix: str | None
884
+ ) -> None:
885
+ """Flag a `numeric_state` trigger's `value_template` used for duration math.
886
+
887
+ Split out of :func:`_check_triggers` to keep its complexity low.
888
+ """
889
+ vt = trigger.get("value_template", "")
890
+ if isinstance(vt, str) and _RE_DURATION_MATH.search(vt):
891
+ _emit(
892
+ warnings,
893
+ "`numeric_state` trigger uses `value_template` for duration/recency "
894
+ "check (`now() - X.last_changed/last_updated`) — use the native "
895
+ "`for:` field on a `state` trigger instead "
896
+ "(e.g., `platform: state, entity_id: binary_sensor.motion, "
897
+ "to: 'off', for: {minutes: 5}`). Native `for:` is event-driven "
898
+ "and doesn't re-evaluate on every state change.",
899
+ skill_prefix,
900
+ "automation-patterns.md#trigger-types",
901
+ )
807
902
 
808
903
 
809
904
  # ---------------------------------------------------------------------------