ha-mcp-dev 7.5.0.dev512__tar.gz → 7.5.0.dev514__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 (111) hide show
  1. {ha_mcp_dev-7.5.0.dev512/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev514}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_config_dashboards.py +106 -63
  4. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  5. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/tests/test_env_manager.py +16 -2
  6. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/LICENSE +0 -0
  7. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/MANIFEST.in +0 -0
  8. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/README.md +0 -0
  9. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/setup.cfg +0 -0
  10. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/__init__.py +0 -0
  11. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/__main__.py +0 -0
  12. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/_pypi_marker +0 -0
  13. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/_version.py +0 -0
  14. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/auth/__init__.py +0 -0
  15. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/auth/consent_form.py +0 -0
  16. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/auth/provider.py +0 -0
  17. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/client/__init__.py +0 -0
  18. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/client/rest_client.py +0 -0
  19. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/client/supervisor_client.py +0 -0
  20. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/client/websocket_client.py +0 -0
  21. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/client/websocket_listener.py +0 -0
  22. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/config.py +0 -0
  23. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/errors.py +0 -0
  24. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/py.typed +0 -0
  25. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  26. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  27. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  28. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  29. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  30. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  31. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  32. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  33. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  34. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  35. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  36. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  37. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  38. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  39. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  40. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  41. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  42. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  43. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  44. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  45. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/server.py +0 -0
  46. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/settings_ui.py +0 -0
  47. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/smoke_test.py +0 -0
  48. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/__init__.py +0 -0
  49. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/backup.py +0 -0
  50. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  51. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/device_control.py +0 -0
  52. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/enhanced.py +0 -0
  53. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/helpers.py +0 -0
  54. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/reference_validator.py +0 -0
  55. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/registry.py +0 -0
  56. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/smart_search.py +0 -0
  57. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_addons.py +0 -0
  58. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_areas.py +0 -0
  59. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  60. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  61. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_calendar.py +0 -0
  62. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_camera.py +0 -0
  63. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_categories.py +0 -0
  64. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_code.py +0 -0
  65. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  66. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  67. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  68. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  69. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  70. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_energy.py +0 -0
  71. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_entities.py +0 -0
  72. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  73. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_groups.py +0 -0
  74. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_hacs.py +0 -0
  75. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_history.py +0 -0
  76. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_integrations.py +0 -0
  77. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_labels.py +0 -0
  78. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  79. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_registry.py +0 -0
  80. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_resources.py +0 -0
  81. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_search.py +0 -0
  82. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_service.py +0 -0
  83. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_services.py +0 -0
  84. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_system.py +0 -0
  85. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_todo.py +0 -0
  86. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_traces.py +0 -0
  87. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_updates.py +0 -0
  88. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_utility.py +0 -0
  89. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  90. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  91. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/tools_zones.py +0 -0
  92. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/tools/util_helpers.py +0 -0
  93. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/transforms/__init__.py +0 -0
  94. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/transforms/categorized_search.py +0 -0
  95. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  96. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/utils/__init__.py +0 -0
  97. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/utils/config_hash.py +0 -0
  98. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/utils/data_paths.py +0 -0
  99. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/utils/domain_handlers.py +0 -0
  100. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  101. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  102. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/utils/operation_manager.py +0 -0
  103. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/utils/python_sandbox.py +0 -0
  104. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp/utils/usage_logger.py +0 -0
  105. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  106. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  107. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  108. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  109. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  110. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/tests/__init__.py +0 -0
  111. {ha_mcp_dev-7.5.0.dev512 → ha_mcp_dev-7.5.0.dev514}/tests/test_constants.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.5.0.dev512
3
+ Version: 7.5.0.dev514
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.dev512"
7
+ version = "7.5.0.dev514"
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"
@@ -21,12 +21,68 @@ from ..utils.python_sandbox import (
21
21
  get_security_documentation,
22
22
  safe_execute,
23
23
  )
24
- from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
24
+ from .helpers import (
25
+ exception_to_structured_error,
26
+ extract_tool_error_message,
27
+ log_tool_usage,
28
+ raise_tool_error,
29
+ )
25
30
  from .util_helpers import parse_json_param
26
31
 
27
32
  logger = logging.getLogger(__name__)
28
33
 
29
34
 
35
+ async def _get_dashboard_config_internal(
36
+ client: Any, url_path: str | None
37
+ ) -> tuple[dict[str, Any], str]:
38
+ """Fetch dashboard config from HA and compute its hash.
39
+
40
+ Returns ``(config, config_hash)`` tuple where ``config`` is the
41
+ authoritative Lovelace config dict returned by HA's ``lovelace/config``
42
+ WebSocket call (with ``force=True`` to bypass any cache) and
43
+ ``config_hash`` is computed from that config via ``compute_config_hash``.
44
+
45
+ Used internally to obtain the authoritative post-save hash and as the
46
+ fetch+hash building block for the optimistic-locking pre-read paths.
47
+ Mirrors the ``_get_<entity>_config_internal`` helpers in the sibling
48
+ files (``tools_config_scripts.py``, ``tools_config_automations.py``,
49
+ ``tools_config_scenes.py``).
50
+
51
+ Raises ``ToolError`` with ``ErrorCode.SERVICE_CALL_FAILED`` if the
52
+ WebSocket call reports failure or the response is not a dict; callers
53
+ can rely on the returned tuple being populated.
54
+ """
55
+ get_data: dict[str, Any] = {"type": "lovelace/config", "force": True}
56
+ if url_path:
57
+ get_data["url_path"] = url_path
58
+
59
+ response = await client.send_websocket_message(get_data)
60
+
61
+ if isinstance(response, dict) and not response.get("success", True):
62
+ error_msg = response.get("error", {})
63
+ if isinstance(error_msg, dict):
64
+ error_msg = error_msg.get("message", str(error_msg))
65
+ raise_tool_error(
66
+ create_error_response(
67
+ ErrorCode.SERVICE_CALL_FAILED,
68
+ f"Dashboard fetch failed: {error_msg}",
69
+ context={"url_path": url_path},
70
+ )
71
+ )
72
+
73
+ config = response.get("result") if isinstance(response, dict) else response
74
+ if not isinstance(config, dict):
75
+ raise_tool_error(
76
+ create_error_response(
77
+ ErrorCode.SERVICE_CALL_FAILED,
78
+ "Dashboard config response was not a dict",
79
+ context={"url_path": url_path},
80
+ )
81
+ )
82
+
83
+ return cast(dict[str, Any], config), compute_config_hash(config)
84
+
85
+
30
86
  async def _verify_config_unchanged(
31
87
  client: Any,
32
88
  url_path: str,
@@ -1029,21 +1085,19 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
1029
1085
  )
1030
1086
  )
1031
1087
 
1032
- # Fetch current dashboard config
1033
- get_data: dict[str, Any] = {"type": "lovelace/config", "force": True}
1034
- if url_path:
1035
- get_data["url_path"] = url_path
1036
-
1037
- response = await client.send_websocket_message(get_data)
1038
-
1039
- if isinstance(response, dict) and not response.get("success", True):
1040
- error_msg = response.get("error", {})
1041
- if isinstance(error_msg, dict):
1042
- error_msg = error_msg.get("message", str(error_msg))
1088
+ # Fetch current dashboard config + hash via the shared helper.
1089
+ # Re-wrap helper's generic fetch error with python_transform-
1090
+ # specific UX suggestions so the caller learns this branch
1091
+ # requires an existing dashboard.
1092
+ try:
1093
+ current_config, current_hash = await _get_dashboard_config_internal(
1094
+ client, url_path
1095
+ )
1096
+ except ToolError as e:
1043
1097
  raise_tool_error(
1044
1098
  create_error_response(
1045
1099
  ErrorCode.SERVICE_CALL_FAILED,
1046
- f"Dashboard not found or inaccessible: {error_msg}",
1100
+ f"Dashboard not found or inaccessible: {extract_tool_error_message(e)}",
1047
1101
  suggestions=[
1048
1102
  "python_transform requires an existing dashboard",
1049
1103
  "Use 'config' parameter to create a new dashboard",
@@ -1056,26 +1110,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
1056
1110
  )
1057
1111
  )
1058
1112
 
1059
- current_config = (
1060
- response.get("result") if isinstance(response, dict) else response
1061
- )
1062
- if not isinstance(current_config, dict):
1063
- raise_tool_error(
1064
- create_error_response(
1065
- ErrorCode.SERVICE_CALL_FAILED,
1066
- "Current dashboard config is invalid",
1067
- suggestions=[
1068
- "Initialize dashboard with 'config' parameter first"
1069
- ],
1070
- context={
1071
- "action": "python_transform",
1072
- "url_path": url_path,
1073
- },
1074
- )
1075
- )
1076
-
1077
1113
  # Validate config_hash for optimistic locking
1078
- current_hash = compute_config_hash(current_config)
1079
1114
  if current_hash != config_hash:
1080
1115
  raise_tool_error(
1081
1116
  create_error_response(
@@ -1153,8 +1188,10 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
1153
1188
  )
1154
1189
  )
1155
1190
 
1156
- # Compute new hash for potential chaining
1157
- new_config_hash = compute_config_hash(transformed_config)
1191
+ # Re-fetch to get authoritative hash (HA may normalize after save)
1192
+ _, new_config_hash = await _get_dashboard_config_internal(
1193
+ client, url_path
1194
+ )
1158
1195
 
1159
1196
  transform_result: dict[str, Any] = {
1160
1197
  "success": True,
@@ -1309,38 +1346,44 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
1309
1346
 
1310
1347
  # For existing dashboards, optionally validate config_hash and warn on large replacement
1311
1348
  if dashboard_exists:
1312
- # Fetch current config for validation/comparison
1313
- get_data = {
1314
- "type": "lovelace/config",
1315
- "force": True,
1316
- }
1317
- if url_path:
1318
- get_data["url_path"] = url_path
1319
- current_response = await client.send_websocket_message(get_data)
1320
- current_config = (
1321
- current_response.get("result")
1322
- if isinstance(current_response, dict)
1323
- else current_response
1324
- )
1325
-
1326
- if isinstance(current_config, dict):
1327
- existing_config_size = len(json.dumps(current_config))
1349
+ # Fetch current config + hash via the shared helper.
1350
+ # Tolerate fetch failures here — full-config replacement
1351
+ # should still proceed even if the pre-read can't load
1352
+ # the current state (force-replace path). The strict
1353
+ # ``ToolError`` raised by the helper is downgraded to a
1354
+ # skip of both the optimistic-locking check and the
1355
+ # large-config soft warning, matching the prior
1356
+ # silently-fall-through behaviour.
1357
+ # Distinct names from the python_transform branch's
1358
+ # ``current_config``/``current_hash`` so the optional
1359
+ # type here doesn't redefine the non-optional binding
1360
+ # mypy infers there.
1361
+ existing_config: dict[str, Any] | None = None
1362
+ existing_hash: str | None = None
1363
+ try:
1364
+ (
1365
+ existing_config,
1366
+ existing_hash,
1367
+ ) = await _get_dashboard_config_internal(client, url_path)
1368
+ except ToolError:
1369
+ pass
1370
+
1371
+ if isinstance(existing_config, dict):
1372
+ existing_config_size = len(json.dumps(existing_config))
1328
1373
 
1329
1374
  # Optional config_hash validation for full replacement
1330
- if config_hash is not None:
1331
- current_hash = compute_config_hash(current_config)
1332
- if current_hash != config_hash:
1333
- raise_tool_error(
1334
- create_error_response(
1335
- ErrorCode.SERVICE_CALL_FAILED,
1336
- "Dashboard modified since last read (conflict)",
1337
- suggestions=[
1338
- "Call ha_config_get_dashboard() again",
1339
- "Use the fresh config_hash, or omit config_hash to force replace",
1340
- ],
1341
- context={"action": "set", "url_path": url_path},
1342
- )
1375
+ if config_hash is not None and existing_hash != config_hash:
1376
+ raise_tool_error(
1377
+ create_error_response(
1378
+ ErrorCode.SERVICE_CALL_FAILED,
1379
+ "Dashboard modified since last read (conflict)",
1380
+ suggestions=[
1381
+ "Call ha_config_get_dashboard() again",
1382
+ "Use the fresh config_hash, or omit config_hash to force replace",
1383
+ ],
1384
+ context={"action": "set", "url_path": url_path},
1343
1385
  )
1386
+ )
1344
1387
 
1345
1388
  # Soft warning for large config full replacement (10KB ≈ 2-3k tokens)
1346
1389
  if existing_config_size >= 10000:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.5.0.dev512
3
+ Version: 7.5.0.dev514
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
@@ -17,6 +17,7 @@ import sys
17
17
  import time
18
18
  from pathlib import Path
19
19
 
20
+ import docker.errors
20
21
  import requests
21
22
  from testcontainers.core.container import DockerContainer
22
23
 
@@ -302,13 +303,26 @@ def main():
302
303
  env.print_status()
303
304
 
304
305
  if args.no_interactive:
305
- # Non-interactive mode: just wait for interrupt
306
+ # Non-interactive mode: wait for interrupt, and exit if the container dies
307
+ # so systemd's Restart=on-failure kicks in with a fresh instance.
306
308
  logger.info("🔄 Running in non-interactive mode. Press Ctrl+C to stop.")
307
309
  try:
308
310
  while True:
309
- time.sleep(1)
311
+ time.sleep(30)
312
+ if env.container:
313
+ wrapped = env.container.get_wrapped_container()
314
+ wrapped.reload()
315
+ if wrapped.status not in ("running",):
316
+ logger.error(
317
+ f"❌ Container stopped unexpectedly "
318
+ f"(status: {wrapped.status}), exiting for restart..."
319
+ )
320
+ sys.exit(1)
310
321
  except KeyboardInterrupt:
311
322
  logger.info("\n🛑 Received interrupt signal")
323
+ except (OSError, RuntimeError, docker.errors.DockerException) as e:
324
+ logger.error(f"❌ Watchdog failure: {e}, exiting for restart...")
325
+ sys.exit(1)
312
326
  else:
313
327
  # Interactive menu loop
314
328
  while True: