ha-mcp-dev 7.12.0.dev1835__tar.gz → 7.12.0.dev1839__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.dev1835/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.0.dev1839}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/pyproject.toml +1 -9
  3. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/best_practice_checker.py +201 -106
  4. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_calendar.py +116 -80
  5. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_mcp_component.py +80 -67
  6. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_resources.py +42 -36
  7. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_search.py +76 -41
  8. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_service.py +100 -48
  9. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_utility.py +138 -94
  10. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/util_helpers.py +80 -58
  11. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  12. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/LICENSE +0 -0
  13. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/MANIFEST.in +0 -0
  14. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/README.md +0 -0
  15. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/setup.cfg +0 -0
  16. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/__init__.py +0 -0
  17. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/__main__.py +0 -0
  18. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/_pypi_marker +0 -0
  19. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/_version.py +0 -0
  20. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/auth/__init__.py +0 -0
  21. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/auth/consent_form.py +0 -0
  22. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/auth/provider.py +0 -0
  23. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/backup_manager.py +0 -0
  24. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/browser_landing.py +0 -0
  25. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/client/__init__.py +0 -0
  26. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/client/rest_client.py +0 -0
  27. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/client/supervisor_client.py +0 -0
  28. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/client/websocket_client.py +0 -0
  29. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/client/websocket_listener.py +0 -0
  30. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/config.py +0 -0
  31. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  32. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  33. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  34. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/errors.py +0 -0
  35. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/policy/__init__.py +0 -0
  36. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/policy/approval_queue.py +0 -0
  37. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/policy/evaluator.py +0 -0
  38. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/policy/handlers.py +0 -0
  39. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/policy/middleware.py +0 -0
  40. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/policy/model.py +0 -0
  41. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/policy/persistence.py +0 -0
  42. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/policy/value_sources.py +0 -0
  43. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/py.typed +0 -0
  44. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/read_only.py +0 -0
  45. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  46. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  47. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  48. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  49. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  50. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  51. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  52. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  53. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  54. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  55. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  56. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  57. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  58. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  59. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  60. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  61. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  62. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  63. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  64. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  65. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  66. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  67. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  68. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  69. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  70. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/server.py +0 -0
  71. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/settings_ui/__init__.py +0 -0
  72. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/settings_ui/settings.css +0 -0
  73. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/settings_ui/settings.html +0 -0
  74. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/settings_ui/settings.js +0 -0
  75. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/smoke_test.py +0 -0
  76. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  77. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/__init__.py +0 -0
  78. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/auto_backup.py +0 -0
  79. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/backup.py +0 -0
  80. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  81. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/device_control.py +0 -0
  82. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/enhanced.py +0 -0
  83. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/helpers.py +0 -0
  84. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/radio/__init__.py +0 -0
  85. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/radio/base.py +0 -0
  86. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/radio/matter.py +0 -0
  87. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/radio/thread.py +0 -0
  88. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  89. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/radio/zwave.py +0 -0
  90. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/reference_validator.py +0 -0
  91. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/registry.py +0 -0
  92. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  93. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  94. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  95. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  96. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  97. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  98. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  99. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  100. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  101. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  102. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_addons.py +0 -0
  103. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_areas.py +0 -0
  104. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  105. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  106. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_camera.py +0 -0
  107. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_categories.py +0 -0
  108. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_code.py +0 -0
  109. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  110. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  111. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  112. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  113. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  114. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  115. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_dev.py +0 -0
  116. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_energy.py +0 -0
  117. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_entities.py +0 -0
  118. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  119. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_groups.py +0 -0
  120. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_hacs.py +0 -0
  121. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_history.py +0 -0
  122. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_integrations.py +0 -0
  123. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_labels.py +0 -0
  124. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_radio.py +0 -0
  125. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_registry.py +0 -0
  126. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_services.py +0 -0
  127. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_system.py +0 -0
  128. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_themes.py +0 -0
  129. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_todo.py +0 -0
  130. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_traces.py +0 -0
  131. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_updates.py +0 -0
  132. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  133. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  134. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_zones.py +0 -0
  135. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/validation_middleware.py +0 -0
  136. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/transforms/__init__.py +0 -0
  137. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/transforms/categorized_search.py +0 -0
  138. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  139. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/transport_security.py +0 -0
  140. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/update_check.py +0 -0
  141. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/__init__.py +0 -0
  142. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/config_hash.py +0 -0
  143. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/data_paths.py +0 -0
  144. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/domain_handlers.py +0 -0
  145. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  146. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  147. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/operation_manager.py +0 -0
  148. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/python_sandbox.py +0 -0
  149. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/skill_loader.py +0 -0
  150. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/usage_logger.py +0 -0
  151. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/visibility/__init__.py +0 -0
  152. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/visibility/model.py +0 -0
  153. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/visibility/persistence.py +0 -0
  154. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/visibility/resolver.py +0 -0
  155. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  156. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  157. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  158. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  159. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  160. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/tests/__init__.py +0 -0
  161. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/tests/test_constants.py +0 -0
  162. {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/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.dev1835
3
+ Version: 7.12.0.dev1839
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.dev1835"
7
+ version = "7.12.0.dev1839"
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
@@ -181,14 +181,12 @@ ignore = [
181
181
  "src/ha_mcp/settings_ui/__init__.py" = ["C901"]
182
182
  "src/ha_mcp/tools/auto_backup.py" = ["C901"]
183
183
  "src/ha_mcp/tools/backup.py" = ["C901"]
184
- "src/ha_mcp/tools/best_practice_checker.py" = ["C901"]
185
184
  "src/ha_mcp/tools/radio/matter.py" = ["C901"]
186
185
  "src/ha_mcp/tools/radio/zigbee.py" = ["C901"]
187
186
  "src/ha_mcp/tools/radio/zwave.py" = ["C901"]
188
187
  "src/ha_mcp/tools/reference_validator.py" = ["C901"]
189
188
  "src/ha_mcp/tools/tools_addons.py" = ["C901"]
190
189
  "src/ha_mcp/tools/tools_areas.py" = ["C901"]
191
- "src/ha_mcp/tools/tools_calendar.py" = ["C901"]
192
190
  "src/ha_mcp/tools/tools_code.py" = ["C901"]
193
191
  "src/ha_mcp/tools/tools_config_automations.py" = ["C901"]
194
192
  "src/ha_mcp/tools/tools_config_scenes.py" = ["C901"]
@@ -198,17 +196,11 @@ ignore = [
198
196
  "src/ha_mcp/tools/tools_hacs.py" = ["C901"]
199
197
  "src/ha_mcp/tools/tools_history.py" = ["C901"]
200
198
  "src/ha_mcp/tools/tools_integrations.py" = ["C901"]
201
- "src/ha_mcp/tools/tools_mcp_component.py" = ["C901"]
202
- "src/ha_mcp/tools/tools_resources.py" = ["C901"]
203
- "src/ha_mcp/tools/tools_search.py" = ["C901"]
204
- "src/ha_mcp/tools/tools_service.py" = ["C901"]
205
199
  "src/ha_mcp/tools/tools_system.py" = ["C901"]
206
200
  "src/ha_mcp/tools/tools_traces.py" = ["C901"]
207
201
  "src/ha_mcp/tools/tools_updates.py" = ["C901"]
208
- "src/ha_mcp/tools/tools_utility.py" = ["C901"]
209
202
  "src/ha_mcp/tools/tools_yaml_config.py" = ["C901"]
210
203
  "src/ha_mcp/tools/tools_zones.py" = ["C901"]
211
- "src/ha_mcp/tools/util_helpers.py" = ["C901"]
212
204
  "src/ha_mcp/transforms/categorized_search.py" = ["C901"]
213
205
  "src/ha_mcp/utils/data_paths.py" = ["C901"]
214
206
  "src/ha_mcp/utils/fuzzy_search.py" = ["C901"]
@@ -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
  # ---------------------------------------------------------------------------
@@ -170,6 +170,115 @@ class CalendarTools:
170
170
  )
171
171
  return None # unreachable: exception_to_structured_error always raises
172
172
 
173
+ async def _create_recurring_calendar_event(
174
+ self,
175
+ entity_id: str,
176
+ summary: str,
177
+ start: str,
178
+ end: str,
179
+ description: str | None,
180
+ location: str | None,
181
+ rrule: str,
182
+ ) -> Any:
183
+ """Create a recurring calendar event series via the WebSocket API.
184
+
185
+ The ``calendar.create_event`` service schema has no rrule field --
186
+ recurrence is only accepted by the WebSocket command
187
+ ``calendar/event/create`` (see HA Core
188
+ ``homeassistant/components/calendar/__init__.py``), the same split
189
+ that forces the delete tool below onto the WebSocket API.
190
+ """
191
+ event: dict[str, Any] = {
192
+ "summary": summary,
193
+ "dtstart": start,
194
+ "dtend": end,
195
+ "rrule": rrule,
196
+ }
197
+ if description is not None:
198
+ event["description"] = description
199
+ if location is not None:
200
+ event["location"] = location
201
+
202
+ ws_client, conn_error = await get_connected_ws_client(
203
+ self._client.base_url,
204
+ self._client.token,
205
+ verify_ssl=self._client.verify_ssl,
206
+ )
207
+ if conn_error or ws_client is None:
208
+ raise_tool_error(
209
+ conn_error
210
+ or create_error_response(
211
+ ErrorCode.CONNECTION_FAILED,
212
+ "Failed to connect to Home Assistant WebSocket",
213
+ context={"entity_id": entity_id},
214
+ )
215
+ )
216
+
217
+ try:
218
+ return await ws_client.send_command(
219
+ "calendar/event/create", entity_id=entity_id, event=event
220
+ )
221
+ finally:
222
+ # Guard disconnect: a transport-teardown error here would
223
+ # otherwise replace the original send_command exception.
224
+ try:
225
+ await ws_client.disconnect()
226
+ except Exception as disconnect_error:
227
+ logger.debug(
228
+ f"WebSocket disconnect after create_event for "
229
+ f"{entity_id}: {disconnect_error}"
230
+ )
231
+
232
+ async def _create_simple_calendar_event(
233
+ self,
234
+ entity_id: str,
235
+ summary: str,
236
+ start: str,
237
+ end: str,
238
+ description: str | None,
239
+ location: str | None,
240
+ ) -> Any:
241
+ """Create a one-off calendar event via the calendar.create_event service."""
242
+ service_data: dict[str, Any] = {
243
+ "entity_id": entity_id,
244
+ "summary": summary,
245
+ "start_date_time": start,
246
+ "end_date_time": end,
247
+ }
248
+
249
+ if description is not None:
250
+ service_data["description"] = description
251
+ if location is not None:
252
+ service_data["location"] = location
253
+
254
+ return await self._client.call_service("calendar", "create_event", service_data)
255
+
256
+ def _build_set_calendar_event_error_suggestions(
257
+ self, entity_id: str, rrule: str | None, error: Exception
258
+ ) -> list[str]:
259
+ """Build suggestions for a failed ha_config_set_calendar_event call."""
260
+ suggestions = [
261
+ f"Verify calendar entity '{entity_id}' exists and supports event creation",
262
+ "Check datetime format (ISO 8601)",
263
+ "Ensure end time is after start time",
264
+ "Some calendar integrations may be read-only",
265
+ ]
266
+ if rrule:
267
+ suggestions.insert(
268
+ 0,
269
+ "Check RRULE syntax (RFC 5545, without the 'RRULE:' prefix, "
270
+ "e.g. 'FREQ=WEEKLY;BYDAY=MO') and that this calendar "
271
+ "integration supports recurring events",
272
+ )
273
+
274
+ error_str = str(error)
275
+ if "404" in error_str or "not found" in error_str.lower():
276
+ suggestions.insert(0, f"Calendar entity '{entity_id}' not found")
277
+ if "not supported" in error_str.lower():
278
+ suggestions.insert(0, "This calendar does not support event creation")
279
+
280
+ return suggestions
281
+
173
282
  @tool(
174
283
  name="ha_config_set_calendar_event",
175
284
  tags={"Calendar"},
@@ -277,69 +386,12 @@ class CalendarTools:
277
386
  )
278
387
 
279
388
  if rrule:
280
- # The ``calendar.create_event`` service schema has no rrule
281
- # field — recurrence is only accepted by the WebSocket
282
- # command ``calendar/event/create`` (see HA Core
283
- # ``homeassistant/components/calendar/__init__.py``), the
284
- # same split that forces the delete tool below onto the
285
- # WebSocket API.
286
- event: dict[str, Any] = {
287
- "summary": summary,
288
- "dtstart": start,
289
- "dtend": end,
290
- "rrule": rrule,
291
- }
292
- if description:
293
- event["description"] = description
294
- if location:
295
- event["location"] = location
296
-
297
- ws_client, conn_error = await get_connected_ws_client(
298
- self._client.base_url,
299
- self._client.token,
300
- verify_ssl=self._client.verify_ssl,
389
+ result = await self._create_recurring_calendar_event(
390
+ entity_id, summary, start, end, description, location, rrule
301
391
  )
302
- if conn_error or ws_client is None:
303
- raise_tool_error(
304
- conn_error
305
- or create_error_response(
306
- ErrorCode.CONNECTION_FAILED,
307
- "Failed to connect to Home Assistant WebSocket",
308
- context={"entity_id": entity_id},
309
- )
310
- )
311
-
312
- try:
313
- result = await ws_client.send_command(
314
- "calendar/event/create", entity_id=entity_id, event=event
315
- )
316
- finally:
317
- # Guard disconnect: a transport-teardown error here would
318
- # otherwise replace the original send_command exception.
319
- try:
320
- await ws_client.disconnect()
321
- except Exception as disconnect_error:
322
- logger.debug(
323
- f"WebSocket disconnect after create_event for "
324
- f"{entity_id}: {disconnect_error}"
325
- )
326
392
  else:
327
- # Build service data
328
- service_data: dict[str, Any] = {
329
- "entity_id": entity_id,
330
- "summary": summary,
331
- "start_date_time": start,
332
- "end_date_time": end,
333
- }
334
-
335
- if description:
336
- service_data["description"] = description
337
- if location:
338
- service_data["location"] = location
339
-
340
- # Call the calendar.create_event service
341
- result = await self._client.call_service(
342
- "calendar", "create_event", service_data
393
+ result = await self._create_simple_calendar_event(
394
+ entity_id, summary, start, end, description, location
343
395
  )
344
396
 
345
397
  return {
@@ -362,25 +414,9 @@ class CalendarTools:
362
414
  except Exception as error:
363
415
  logger.error(f"Failed to create calendar event in {entity_id}: {error}")
364
416
 
365
- suggestions = [
366
- f"Verify calendar entity '{entity_id}' exists and supports event creation",
367
- "Check datetime format (ISO 8601)",
368
- "Ensure end time is after start time",
369
- "Some calendar integrations may be read-only",
370
- ]
371
- if rrule:
372
- suggestions.insert(
373
- 0,
374
- "Check RRULE syntax (RFC 5545, without the 'RRULE:' prefix, "
375
- "e.g. 'FREQ=WEEKLY;BYDAY=MO') and that this calendar "
376
- "integration supports recurring events",
377
- )
378
-
379
- error_str = str(error)
380
- if "404" in error_str or "not found" in error_str.lower():
381
- suggestions.insert(0, f"Calendar entity '{entity_id}' not found")
382
- if "not supported" in error_str.lower():
383
- suggestions.insert(0, "This calendar does not support event creation")
417
+ suggestions = self._build_set_calendar_event_error_suggestions(
418
+ entity_id, rrule, error
419
+ )
384
420
 
385
421
  exception_to_structured_error(
386
422
  error, context={"entity_id": entity_id}, suggestions=suggestions