ha-mcp-dev 7.5.0.dev569__tar.gz → 7.5.0.dev570__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 (113) hide show
  1. {ha_mcp_dev-7.5.0.dev569/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev570}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/client/rest_client.py +16 -4
  4. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_automations.py +153 -91
  5. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  6. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/LICENSE +0 -0
  7. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/MANIFEST.in +0 -0
  8. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/README.md +0 -0
  9. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/setup.cfg +0 -0
  10. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/__init__.py +0 -0
  11. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/__main__.py +0 -0
  12. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/_pypi_marker +0 -0
  13. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/_version.py +0 -0
  14. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/auth/__init__.py +0 -0
  15. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/auth/consent_form.py +0 -0
  16. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/auth/provider.py +0 -0
  17. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/client/__init__.py +0 -0
  18. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/client/supervisor_client.py +0 -0
  19. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/client/websocket_client.py +0 -0
  20. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/client/websocket_listener.py +0 -0
  21. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/config.py +0 -0
  22. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/errors.py +0 -0
  23. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/py.typed +0 -0
  24. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  25. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  26. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  27. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  28. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  29. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  30. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  31. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  32. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  33. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  34. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  35. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  36. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  37. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  38. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  39. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  40. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  41. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  42. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  43. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  44. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  45. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  46. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/server.py +0 -0
  47. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/settings_ui.py +0 -0
  48. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/smoke_test.py +0 -0
  49. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/__init__.py +0 -0
  50. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/backup.py +0 -0
  51. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  52. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/device_control.py +0 -0
  53. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/enhanced.py +0 -0
  54. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/helpers.py +0 -0
  55. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/reference_validator.py +0 -0
  56. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/registry.py +0 -0
  57. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/smart_search.py +0 -0
  58. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_addons.py +0 -0
  59. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_areas.py +0 -0
  60. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  61. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  62. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_calendar.py +0 -0
  63. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_camera.py +0 -0
  64. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_categories.py +0 -0
  65. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_code.py +0 -0
  66. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  67. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  68. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  69. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  70. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  71. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_energy.py +0 -0
  72. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_entities.py +0 -0
  73. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  74. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_groups.py +0 -0
  75. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_hacs.py +0 -0
  76. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_history.py +0 -0
  77. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_integrations.py +0 -0
  78. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_labels.py +0 -0
  79. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  80. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_registry.py +0 -0
  81. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_resources.py +0 -0
  82. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_search.py +0 -0
  83. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_service.py +0 -0
  84. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_services.py +0 -0
  85. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_system.py +0 -0
  86. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_todo.py +0 -0
  87. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_traces.py +0 -0
  88. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_updates.py +0 -0
  89. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_utility.py +0 -0
  90. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  91. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  92. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/tools_zones.py +0 -0
  93. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/tools/util_helpers.py +0 -0
  94. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/transforms/__init__.py +0 -0
  95. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/transforms/categorized_search.py +0 -0
  96. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  97. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/__init__.py +0 -0
  98. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/config_hash.py +0 -0
  99. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/data_paths.py +0 -0
  100. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/domain_handlers.py +0 -0
  101. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  102. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  103. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/operation_manager.py +0 -0
  104. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/python_sandbox.py +0 -0
  105. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp/utils/usage_logger.py +0 -0
  106. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  107. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  108. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  109. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  110. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  111. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/tests/__init__.py +0 -0
  112. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/tests/test_constants.py +0 -0
  113. {ha_mcp_dev-7.5.0.dev569 → ha_mcp_dev-7.5.0.dev570}/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.5.0.dev569
3
+ Version: 7.5.0.dev570
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.5.0.dev569"
7
+ version = "7.5.0.dev570"
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"
@@ -7,6 +7,7 @@ import json
7
7
  import logging
8
8
  import os
9
9
  import ssl
10
+ import time
10
11
  from typing import Any
11
12
 
12
13
  import httpx
@@ -963,12 +964,18 @@ class HomeAssistantClient:
963
964
  ) from e
964
965
  raise
965
966
 
966
- # 3-attempt × 6s upper-bound budget; first poll 0.1s catches the
967
- # typical sub-1s entity-publish window.
968
- _POLL_CADENCE: tuple[float, ...] = (0.1, 1.0, 4.9)
967
+ # 3-attempt × 6s upper-bound budget; first poll 0.025s is a 5×
968
+ # cushion above the ~4ms HA-Core entity-registration latency
969
+ # measured by ``test_poll_cadence_measurement.py`` (#1389 p50
970
+ # 104.1-104.8 ms on the prior 0.1s first-poll, all from the sleep
971
+ # itself with ~4 ms of real registration work).
972
+ _POLL_CADENCE: tuple[float, ...] = (0.025, 1.0, 4.975)
969
973
 
970
974
  async def _poll_for_automation_entity(self, unique_id: str) -> str | None:
971
975
  """Poll HA state to find the entity_id assigned to a newly created automation."""
976
+ # Measure cumulative elapsed from function entry to first successful match.
977
+ # Feeds the #1389 p50/p99 validation of `_POLL_CADENCE`.
978
+ start_monotonic = time.monotonic()
972
979
  try:
973
980
  for sleep_time in self._POLL_CADENCE:
974
981
  await asyncio.sleep(sleep_time)
@@ -978,8 +985,13 @@ class HomeAssistantClient:
978
985
  continue
979
986
  if state.get("attributes", {}).get("id") == unique_id:
980
987
  entity_id = state.get("entity_id")
988
+ elapsed_ms = (time.monotonic() - start_monotonic) * 1000.0
981
989
  logger.debug(
982
- f"Found actual entity_id for unique_id {unique_id}: {entity_id}"
990
+ "entity-registration-elapsed: %.1fms "
991
+ "(unique_id=%s, entity_id=%s)",
992
+ elapsed_ms,
993
+ unique_id,
994
+ entity_id,
983
995
  )
984
996
  return entity_id
985
997
  except HomeAssistantError as e:
@@ -53,6 +53,17 @@ from .util_helpers import (
53
53
 
54
54
  logger = logging.getLogger(__name__)
55
55
 
56
+ # Distinctive prefix of the soft-failure warning emitted by
57
+ # ``ha_config_set_automation`` when ``_poll_for_automation_entity``
58
+ # exhausts ``_POLL_CADENCE`` without matching the new automation.
59
+ # Exported so tests (e.g. ``test_poll_cadence_measurement.py``) can
60
+ # detect a missed registration without hard-coding the literal —
61
+ # rewording the warning becomes a compile-time coupling rather than
62
+ # a silent test drift.
63
+ NOT_VERIFIED_WARNING_PREFIX = (
64
+ "Automation was submitted to Home Assistant but the entity was not found"
65
+ )
66
+
56
67
 
57
68
  def _normalize_automation_config(
58
69
  config: Any,
@@ -279,7 +290,9 @@ class AutomationConfigTools:
279
290
  ):
280
291
  return str(state["entity_id"])
281
292
  except Exception as e:
282
- logger.debug(f"Failed to resolve entity_id for automation {identifier}: {e}")
293
+ logger.debug(
294
+ f"Failed to resolve entity_id for automation {identifier}: {e}"
295
+ )
283
296
  return None
284
297
 
285
298
  @tool(
@@ -334,13 +347,17 @@ class AutomationConfigTools:
334
347
  "Use ha_search_entities(domain_filter='automation') to list automations",
335
348
  ],
336
349
  )
337
- normalized_config, config_hash = await self._get_automation_config_internal(identifier)
350
+ normalized_config, config_hash = await self._get_automation_config_internal(
351
+ identifier
352
+ )
338
353
 
339
354
  # Resolve entity_id and fetch category from entity registry
340
355
  # (injected after hash so transient registry failures don't affect the hash)
341
356
  entity_id = await self._resolve_automation_entity_id(identifier)
342
357
  if entity_id:
343
- cat_id = await fetch_entity_category(self._client, entity_id, "automation")
358
+ cat_id = await fetch_entity_category(
359
+ self._client, entity_id, "automation"
360
+ )
344
361
  if cat_id:
345
362
  normalized_config["category"] = cat_id
346
363
 
@@ -644,7 +661,10 @@ class AutomationConfigTools:
644
661
  "Provide the automation entity_id or unique_id",
645
662
  "Use ha_search_entities(domain_filter='automation') to find automations",
646
663
  ],
647
- context={"action": "python_transform", "identifier": identifier},
664
+ context={
665
+ "action": "python_transform",
666
+ "identifier": identifier,
667
+ },
648
668
  )
649
669
  )
650
670
  if config_hash is None:
@@ -656,7 +676,10 @@ class AutomationConfigTools:
656
676
  "Call ha_config_get_automation() first",
657
677
  "Use the config_hash from that response",
658
678
  ],
659
- context={"action": "python_transform", "identifier": identifier},
679
+ context={
680
+ "action": "python_transform",
681
+ "identifier": identifier,
682
+ },
660
683
  )
661
684
  )
662
685
 
@@ -675,7 +698,10 @@ class AutomationConfigTools:
675
698
  ErrorCode.VALIDATION_FAILED,
676
699
  message,
677
700
  suggestions=suggestions,
678
- context={"action": "python_transform", "identifier": identifier},
701
+ context={
702
+ "action": "python_transform",
703
+ "identifier": identifier,
704
+ },
679
705
  )
680
706
  )
681
707
 
@@ -698,11 +724,20 @@ class AutomationConfigTools:
698
724
 
699
725
  # Re-apply category if present
700
726
  entity_id = result.get("entity_id")
701
- if not entity_id and identifier and identifier.startswith("automation."):
727
+ if (
728
+ not entity_id
729
+ and identifier
730
+ and identifier.startswith("automation.")
731
+ ):
702
732
  entity_id = identifier
703
733
  if transform_category and entity_id:
704
734
  await apply_entity_category(
705
- self._client, entity_id, transform_category, "automation", result, "automation"
735
+ self._client,
736
+ entity_id,
737
+ transform_category,
738
+ "automation",
739
+ result,
740
+ "automation",
706
741
  )
707
742
 
708
743
  response: dict[str, Any] = {
@@ -762,12 +797,14 @@ class AutomationConfigTools:
762
797
  self._client, config_dict
763
798
  )
764
799
 
765
- result = await self._client.upsert_automation_config(config_dict, identifier)
800
+ result = await self._client.upsert_automation_config(
801
+ config_dict, identifier
802
+ )
766
803
 
767
804
  # If the client could not verify the entity was registered, warn but don't hard-fail.
768
805
  if result.get("entity_not_verified"):
769
806
  result.setdefault("warnings", []).append(
770
- "Automation was submitted to Home Assistant but the entity was not found "
807
+ f"{NOT_VERIFIED_WARNING_PREFIX} "
771
808
  "after polling. The automation may still have been created -- check Home "
772
809
  "Assistant logs and try reloading automations. Common causes: "
773
810
  "automations.yaml vs automation.yaml filename mismatch, invalid config "
@@ -784,7 +821,9 @@ class AutomationConfigTools:
784
821
  if wait_bool and entity_id:
785
822
  action_word = "created" if identifier is None else "updated"
786
823
  try:
787
- registered = await wait_for_entity_registered(self._client, entity_id)
824
+ registered = await wait_for_entity_registered(
825
+ self._client, entity_id
826
+ )
788
827
  if not registered:
789
828
  result.setdefault("warnings", []).append(
790
829
  f"Automation {action_word} but {entity_id} not yet queryable. "
@@ -798,7 +837,12 @@ class AutomationConfigTools:
798
837
  # Apply category to entity registry if provided
799
838
  if effective_category and entity_id:
800
839
  await apply_entity_category(
801
- self._client, entity_id, effective_category, "automation", result, "automation"
840
+ self._client,
841
+ entity_id,
842
+ effective_category,
843
+ "automation",
844
+ result,
845
+ "automation",
802
846
  )
803
847
 
804
848
  if bp_warnings:
@@ -859,7 +903,9 @@ class AutomationConfigTools:
859
903
  Returns the current normalized config dict.
860
904
  Raises ToolError if the hash does not match (conflict).
861
905
  """
862
- current_config, current_hash = await self._get_automation_config_internal(identifier)
906
+ current_config, current_hash = await self._get_automation_config_internal(
907
+ identifier
908
+ )
863
909
  if current_hash != config_hash:
864
910
  raise_tool_error(
865
911
  create_error_response(
@@ -880,22 +926,26 @@ class AutomationConfigTools:
880
926
  try:
881
927
  parsed_config = parse_json_param(config, "config")
882
928
  except ValueError as e:
883
- raise_tool_error(create_error_response(
884
- code=ErrorCode.VALIDATION_INVALID_JSON,
885
- message=f"Invalid config parameter: {e}",
886
- suggestions=[
887
- "Pass 'config' as a dict, not a JSON string, to avoid escaping issues.",
888
- "Check for JSON syntax errors: unquoted keys, trailing commas, or invalid escape sequences.",
889
- ],
890
- context={"parameter": "config"},
891
- ))
929
+ raise_tool_error(
930
+ create_error_response(
931
+ code=ErrorCode.VALIDATION_INVALID_JSON,
932
+ message=f"Invalid config parameter: {e}",
933
+ suggestions=[
934
+ "Pass 'config' as a dict, not a JSON string, to avoid escaping issues.",
935
+ "Check for JSON syntax errors: unquoted keys, trailing commas, or invalid escape sequences.",
936
+ ],
937
+ context={"parameter": "config"},
938
+ )
939
+ )
892
940
 
893
941
  if parsed_config is None or not isinstance(parsed_config, dict):
894
- raise_tool_error(create_validation_error(
895
- "Config parameter must be a JSON object",
896
- parameter="config",
897
- details=f"Received type: {type(parsed_config).__name__}",
898
- ))
942
+ raise_tool_error(
943
+ create_validation_error(
944
+ "Config parameter must be a JSON object",
945
+ parameter="config",
946
+ details=f"Received type: {type(parsed_config).__name__}",
947
+ )
948
+ )
899
949
 
900
950
  return cast(dict[str, Any], parsed_config)
901
951
 
@@ -924,24 +974,28 @@ class AutomationConfigTools:
924
974
  context: dict[str, Any] = {"missing_fields": missing_fields}
925
975
  if identifier:
926
976
  context["identifier"] = identifier
927
- raise_tool_error(create_error_response(
928
- code=ErrorCode.CONFIG_MISSING_REQUIRED_FIELDS,
929
- message=f"Missing required fields: {', '.join(missing_fields)}",
930
- details=(
931
- "Config contains 'sequence', which belongs to scripts. "
932
- "Automations use 'trigger' and 'action'; scripts use 'sequence'."
933
- ),
934
- suggestions=[
935
- "Did you mean ha_config_set_script? Scripts use 'sequence' directly.",
936
- "For an automation, replace 'sequence' with 'action' and add a 'trigger'.",
937
- ],
938
- context=context,
939
- ))
940
- raise_tool_error(create_config_error(
941
- f"Missing required fields: {', '.join(missing_fields)}",
942
- identifier=identifier,
943
- missing_fields=missing_fields,
944
- ))
977
+ raise_tool_error(
978
+ create_error_response(
979
+ code=ErrorCode.CONFIG_MISSING_REQUIRED_FIELDS,
980
+ message=f"Missing required fields: {', '.join(missing_fields)}",
981
+ details=(
982
+ "Config contains 'sequence', which belongs to scripts. "
983
+ "Automations use 'trigger' and 'action'; scripts use 'sequence'."
984
+ ),
985
+ suggestions=[
986
+ "Did you mean ha_config_set_script? Scripts use 'sequence' directly.",
987
+ "For an automation, replace 'sequence' with 'action' and add a 'trigger'.",
988
+ ],
989
+ context=context,
990
+ )
991
+ )
992
+ raise_tool_error(
993
+ create_config_error(
994
+ f"Missing required fields: {', '.join(missing_fields)}",
995
+ identifier=identifier,
996
+ missing_fields=missing_fields,
997
+ )
998
+ )
945
999
 
946
1000
  # Issue #1169: reject configs that wrap ``scene.create`` in an
947
1001
  # automation with no functional trigger. Models occasionally produce
@@ -966,31 +1020,33 @@ class AutomationConfigTools:
966
1020
  if _action_contains_scene_create(a)
967
1021
  ]
968
1022
  if scene_create_indices:
969
- raise_tool_error(create_error_response(
970
- code=ErrorCode.VALIDATION_INVALID_PARAMETER,
971
- message=(
972
- "Empty trigger paired with a scene.create action — "
973
- "this automation can never fire. For a state snapshot "
974
- "of one or more entities, use ha_config_set_scene "
975
- "directly instead of wrapping scene.create in an "
976
- "automation."
977
- ),
978
- suggestions=[
979
- "ha_config_set_scene(scene_id='...', config={'name': "
980
- "'...', 'entities': {'<entity_id>': {...}}}) creates "
981
- "a scene without a trigger.",
982
- "If the snapshot really should be the result of an "
983
- "event, add the trigger that should fire it and keep "
984
- "the automation.",
985
- "For a state-derived value that recomputes when its "
986
- "inputs change, use "
987
- "ha_config_set_helper(helper_type='template') instead.",
988
- ],
989
- context={
990
- "scene_create_action_indices": scene_create_indices,
991
- "identifier": identifier,
992
- },
993
- ))
1023
+ raise_tool_error(
1024
+ create_error_response(
1025
+ code=ErrorCode.VALIDATION_INVALID_PARAMETER,
1026
+ message=(
1027
+ "Empty trigger paired with a scene.create action "
1028
+ "this automation can never fire. For a state snapshot "
1029
+ "of one or more entities, use ha_config_set_scene "
1030
+ "directly instead of wrapping scene.create in an "
1031
+ "automation."
1032
+ ),
1033
+ suggestions=[
1034
+ "ha_config_set_scene(scene_id='...', config={'name': "
1035
+ "'...', 'entities': {'<entity_id>': {...}}}) creates "
1036
+ "a scene without a trigger.",
1037
+ "If the snapshot really should be the result of an "
1038
+ "event, add the trigger that should fire it and keep "
1039
+ "the automation.",
1040
+ "For a state-derived value that recomputes when its "
1041
+ "inputs change, use "
1042
+ "ha_config_set_helper(helper_type='template') instead.",
1043
+ ],
1044
+ context={
1045
+ "scene_create_action_indices": scene_create_indices,
1046
+ "identifier": identifier,
1047
+ },
1048
+ )
1049
+ )
994
1050
 
995
1051
  # HA accepts conditions with 'platform' (trigger syntax) but then crashes
996
1052
  # with an unhelpful 500 rather than a 400 validation error.
@@ -998,30 +1054,34 @@ class AutomationConfigTools:
998
1054
  if not isinstance(cond, dict):
999
1055
  continue
1000
1056
  if "platform" in cond and "condition" not in cond:
1001
- raise_tool_error(create_error_response(
1002
- code=ErrorCode.VALIDATION_INVALID_PARAMETER,
1003
- message=(
1004
- f"Condition at index {idx} uses 'platform' (trigger syntax). "
1005
- "Conditions use 'condition', not 'platform'."
1006
- ),
1007
- suggestions=[
1008
- f"Replace 'platform' with 'condition': "
1009
- f"{{'condition': '{cond['platform']}', ...}}",
1010
- "Triggers use 'platform'; conditions use 'condition'.",
1011
- ],
1012
- context={"condition_index": idx, "found_key": "platform"},
1013
- ))
1057
+ raise_tool_error(
1058
+ create_error_response(
1059
+ code=ErrorCode.VALIDATION_INVALID_PARAMETER,
1060
+ message=(
1061
+ f"Condition at index {idx} uses 'platform' (trigger syntax). "
1062
+ "Conditions use 'condition', not 'platform'."
1063
+ ),
1064
+ suggestions=[
1065
+ f"Replace 'platform' with 'condition': "
1066
+ f"{{'condition': '{cond['platform']}', ...}}",
1067
+ "Triggers use 'platform'; conditions use 'condition'.",
1068
+ ],
1069
+ context={"condition_index": idx, "found_key": "platform"},
1070
+ )
1071
+ )
1014
1072
 
1015
1073
  # Prevent duplicate creation when config contains an existing automation id
1016
1074
  if identifier is None and "id" in config_dict:
1017
1075
  existing_id = config_dict["id"]
1018
- raise_tool_error(create_validation_error(
1019
- f"Config contains 'id' field ('{existing_id}') but no identifier was provided. "
1020
- "This would create a duplicate automation instead of updating the existing one.",
1021
- parameter="identifier",
1022
- details=f"To update, pass identifier='{existing_id}' (or the automation's entity_id). "
1023
- "To create a genuinely new automation, remove the 'id' field from the config.",
1024
- ))
1076
+ raise_tool_error(
1077
+ create_validation_error(
1078
+ f"Config contains 'id' field ('{existing_id}') but no identifier was provided. "
1079
+ "This would create a duplicate automation instead of updating the existing one.",
1080
+ parameter="identifier",
1081
+ details=f"To update, pass identifier='{existing_id}' (or the automation's entity_id). "
1082
+ "To create a genuinely new automation, remove the 'id' field from the config.",
1083
+ )
1084
+ )
1025
1085
 
1026
1086
  @tool(
1027
1087
  name="ha_config_remove_automation",
@@ -1086,7 +1146,9 @@ class AutomationConfigTools:
1086
1146
  wait_bool = coerce_bool_param(wait, "wait", default=True)
1087
1147
  if wait_bool and entity_id_for_wait:
1088
1148
  try:
1089
- removed = await wait_for_entity_removed(self._client, entity_id_for_wait)
1149
+ removed = await wait_for_entity_removed(
1150
+ self._client, entity_id_for_wait
1151
+ )
1090
1152
  if not removed:
1091
1153
  result.setdefault("warnings", []).append(
1092
1154
  f"Deletion confirmed by API but {entity_id_for_wait} may still appear briefly."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.5.0.dev569
3
+ Version: 7.5.0.dev570
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