ha-mcp-dev 7.6.0.dev617__tar.gz → 7.6.0.dev618__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 (124) hide show
  1. {ha_mcp_dev-7.6.0.dev617/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.6.0.dev618}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/best_practice_checker.py +42 -7
  4. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  5. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/LICENSE +0 -0
  6. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/MANIFEST.in +0 -0
  7. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/README.md +0 -0
  8. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/setup.cfg +0 -0
  9. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/__init__.py +0 -0
  10. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/__main__.py +0 -0
  11. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/_pypi_marker +0 -0
  12. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/_version.py +0 -0
  13. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/auth/__init__.py +0 -0
  14. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/auth/consent_form.py +0 -0
  15. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/auth/provider.py +0 -0
  16. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/backup_manager.py +0 -0
  17. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/client/__init__.py +0 -0
  18. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/client/rest_client.py +0 -0
  19. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/client/supervisor_client.py +0 -0
  20. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/client/websocket_client.py +0 -0
  21. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/client/websocket_listener.py +0 -0
  22. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/config.py +0 -0
  23. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/errors.py +0 -0
  24. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/policy/__init__.py +0 -0
  25. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/policy/approval_queue.py +0 -0
  26. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/policy/evaluator.py +0 -0
  27. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/policy/handlers.py +0 -0
  28. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/policy/middleware.py +0 -0
  29. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/policy/model.py +0 -0
  30. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/policy/persistence.py +0 -0
  31. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/policy/value_sources.py +0 -0
  32. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/py.typed +0 -0
  33. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  34. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  35. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  36. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  37. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  38. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  39. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  40. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  41. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  42. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  43. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  44. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  45. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  46. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  47. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  48. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  49. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  50. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  51. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  52. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  53. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  54. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  55. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/server.py +0 -0
  56. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/settings_ui.py +0 -0
  57. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/smoke_test.py +0 -0
  58. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  59. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/__init__.py +0 -0
  60. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/auto_backup.py +0 -0
  61. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/backup.py +0 -0
  62. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/device_control.py +0 -0
  63. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/enhanced.py +0 -0
  64. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/helpers.py +0 -0
  65. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/reference_validator.py +0 -0
  66. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/registry.py +0 -0
  67. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/smart_search.py +0 -0
  68. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_addons.py +0 -0
  69. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_areas.py +0 -0
  70. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  71. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  72. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_calendar.py +0 -0
  73. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_camera.py +0 -0
  74. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_categories.py +0 -0
  75. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_code.py +0 -0
  76. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  77. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  78. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  79. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  80. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  81. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  82. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_energy.py +0 -0
  83. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_entities.py +0 -0
  84. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  85. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_groups.py +0 -0
  86. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_hacs.py +0 -0
  87. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_history.py +0 -0
  88. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_integrations.py +0 -0
  89. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_labels.py +0 -0
  90. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  91. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_registry.py +0 -0
  92. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_resources.py +0 -0
  93. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_search.py +0 -0
  94. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_service.py +0 -0
  95. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_services.py +0 -0
  96. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_system.py +0 -0
  97. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_todo.py +0 -0
  98. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_traces.py +0 -0
  99. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_updates.py +0 -0
  100. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_utility.py +0 -0
  101. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  102. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  103. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/tools_zones.py +0 -0
  104. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/tools/util_helpers.py +0 -0
  105. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/transforms/__init__.py +0 -0
  106. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/transforms/categorized_search.py +0 -0
  107. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  108. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/utils/__init__.py +0 -0
  109. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/utils/config_hash.py +0 -0
  110. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/utils/data_paths.py +0 -0
  111. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/utils/domain_handlers.py +0 -0
  112. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  113. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  114. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/utils/operation_manager.py +0 -0
  115. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/utils/python_sandbox.py +0 -0
  116. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp/utils/usage_logger.py +0 -0
  117. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  118. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  119. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  120. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  121. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  122. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/tests/__init__.py +0 -0
  123. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/tests/test_constants.py +0 -0
  124. {ha_mcp_dev-7.6.0.dev617 → ha_mcp_dev-7.6.0.dev618}/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.dev617
3
+ Version: 7.6.0.dev618
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.dev617"
7
+ version = "7.6.0.dev618"
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"
@@ -78,11 +78,34 @@ _RE_STATE_IN = re.compile(r"states\s*\([^)]+\)\s+in\s+[\[(]")
78
78
  # Unsafe direct state access: states.sensor.x.state
79
79
  _RE_DIRECT_STATE = re.compile(r"\bstates\.\w+\.\w+\.state\b")
80
80
  # Duration/recency checks via last_changed or last_updated arithmetic.
81
- # Both alternations require at least one dotted qualifier (e.g. ``states.sensor.x.``)
82
- # so bare Jinja variables named ``last_changed`` are not falsely flagged.
81
+ # Catches the shapes that compute "how long since X changed", all of which map
82
+ # to the native ``for:`` field:
83
+ # now() - X.last_changed (forward subtraction)
84
+ # X.last_changed (<|<=|>|>=) now() - <delta> (reversed; the subtraction is
85
+ # required — a bare ``X.last_changed < now()`` is *always* true and carries
86
+ # no duration, so it is intentionally NOT flagged: ``for:`` cannot express it)
87
+ # now() (<|<=|>|>=) X.last_changed + <delta> (now() on the left)
88
+ # X.last_changed + <delta> (<|<=|>|>=) now() (delta added to the attribute)
89
+ # now().timestamp() - X.last_changed.timestamp() (epoch subtraction)
90
+ # as_timestamp(now()) - as_timestamp(X.last_changed) (function form)
91
+ # as_timestamp(now()) - X.last_changed | as_timestamp (filter form)
92
+ # Every alternation requires a dotted qualifier ending on a word boundary, so
93
+ # bare Jinja variables literally named ``last_changed`` and longer look-alike
94
+ # attributes (``last_changed_at``) are not falsely flagged; a leading ``word.``
95
+ # (``trigger.``, ``states.sensor.x.``) is the minimum.
96
+ # Intentionally NOT matched (heuristic limits, low value): the reversed
97
+ # as_timestamp operand order, the reversed ``.timestamp()`` order, and
98
+ # ``state_attr(e, 'last_changed')`` / ``states('e').last_changed`` — the latter
99
+ # two are not valid ways to read a state's ``last_changed`` in HA anyway. These
100
+ # fall through to the generic template fallback in condition/trigger positions.
83
101
  _RE_DURATION_MATH = re.compile(
84
102
  r"\bnow\(\)\s*-\s*(?:\w+\.)+last_(?:changed|updated)\b"
85
- r"|\b(?:\w+\.)+last_(?:changed|updated)\s*<\s*now\(\)"
103
+ r"|\b(?:\w+\.)+last_(?:changed|updated)\b\s*[<>]=?\s*now\(\)\s*-"
104
+ r"|\bnow\(\)\s*[<>]=?\s*(?:\w+\.)+last_(?:changed|updated)\b\s*\+"
105
+ r"|\b(?:\w+\.)+last_(?:changed|updated)\b\s*\+\s*[^<>{}]+?[<>]=?\s*now\(\)"
106
+ r"|\bnow\(\)\.timestamp\(\)\s*-\s*(?:\w+\.)+last_(?:changed|updated)\.timestamp\(\)"
107
+ r"|\bas_timestamp\(\s*now\(\)\s*\)\s*-\s*as_timestamp\([^)]*\.last_(?:changed|updated)\b"
108
+ r"|\bas_timestamp\(\s*now\(\)\s*\)\s*-\s*(?:\w+\.)+last_(?:changed|updated)\b\s*\|\s*as_timestamp\b"
86
109
  )
87
110
  # Motion entity pattern
88
111
  _RE_MOTION = re.compile(r"binary_sensor\.\w*motion", re.IGNORECASE)
@@ -220,7 +243,15 @@ def _check_template_string(
220
243
  initial_count = len(warnings)
221
244
  label = position.capitalize()
222
245
 
223
- if _RE_NUMERIC_CMP.search(template):
246
+ # Duration/recency math (e.g. `(now() - X.last_changed).total_seconds() > 300`)
247
+ # also contains a numeric comparison, so it matches `_RE_NUMERIC_CMP` too. But its
248
+ # correct native replacement is the `for:` field, NOT `numeric_state`. Suppress the
249
+ # numeric_state suggestion when duration math is present so the user isn't handed
250
+ # two conflicting native alternatives for one template (the duration warning below
251
+ # fires instead).
252
+ duration_match = _RE_DURATION_MATH.search(template)
253
+
254
+ if _RE_NUMERIC_CMP.search(template) and not duration_match:
224
255
  warnings.append(
225
256
  f"{label} uses template with float/int comparison — use native "
226
257
  f"`numeric_state` {position} instead "
@@ -280,7 +311,7 @@ def _check_template_string(
280
311
  "function instead (returns 'unknown' if missing rather than raising)."
281
312
  + _ref(skill_prefix, "template-guidelines.md#common-patterns")
282
313
  )
283
- if _RE_DURATION_MATH.search(template):
314
+ if duration_match:
284
315
  warnings.append(
285
316
  f"{label} uses template for duration/recency check "
286
317
  "(`now() - X.last_changed/last_updated`) — use the native `for:` field "
@@ -502,7 +533,11 @@ def _check_triggers(
502
533
  vt = trigger.get("value_template", "")
503
534
  if isinstance(vt, str):
504
535
  initial = len(warnings)
505
- if _RE_NUMERIC_CMP.search(vt):
536
+ # See `_check_template_string`: duration math also trips the numeric
537
+ # comparison detector, but maps to `for:`, not `numeric_state`. Suppress
538
+ # the numeric_state suggestion when duration math is present.
539
+ duration_match = _RE_DURATION_MATH.search(vt)
540
+ if _RE_NUMERIC_CMP.search(vt) and not duration_match:
506
541
  warnings.append(
507
542
  "Trigger uses template with float/int comparison — "
508
543
  "use native `numeric_state` trigger instead "
@@ -522,7 +557,7 @@ def _check_triggers(
522
557
  "automation-patterns.md#trigger-types",
523
558
  )
524
559
  )
525
- if _RE_DURATION_MATH.search(vt):
560
+ if duration_match:
526
561
  warnings.append(
527
562
  "Trigger uses template for duration/recency check "
528
563
  "(`now() - X.last_changed/last_updated`) — use the native "
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.6.0.dev617
3
+ Version: 7.6.0.dev618
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