ha-mcp-dev 7.0.0.dev264__tar.gz → 7.0.0.dev266__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 (95) hide show
  1. {ha_mcp_dev-7.0.0.dev264/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.0.0.dev266}/PKG-INFO +3 -3
  2. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/README.md +2 -2
  3. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/pyproject.toml +1 -1
  4. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/client/rest_client.py +19 -67
  5. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/client/websocket_client.py +84 -19
  6. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_system.py +13 -6
  7. ha_mcp_dev-7.0.0.dev266/src/ha_mcp/tools/tools_zones.py +337 -0
  8. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266/src/ha_mcp_dev.egg-info}/PKG-INFO +3 -3
  9. ha_mcp_dev-7.0.0.dev264/src/ha_mcp/tools/tools_zones.py +0 -387
  10. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/LICENSE +0 -0
  11. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/MANIFEST.in +0 -0
  12. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/setup.cfg +0 -0
  13. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/__init__.py +0 -0
  14. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/__main__.py +0 -0
  15. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/_pypi_marker +0 -0
  16. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/auth/__init__.py +0 -0
  17. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/auth/consent_form.py +0 -0
  18. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/auth/provider.py +0 -0
  19. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/client/__init__.py +0 -0
  20. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/client/websocket_listener.py +0 -0
  21. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/config.py +0 -0
  22. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/errors.py +0 -0
  23. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/py.typed +0 -0
  24. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/card_types.json +0 -0
  25. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/dashboard_guide.md +0 -0
  26. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  27. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  28. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  29. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  30. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  31. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  32. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  33. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  34. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  35. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  36. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  37. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  38. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  39. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  40. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  41. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  42. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/server.py +0 -0
  43. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/smoke_test.py +0 -0
  44. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/__init__.py +0 -0
  45. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/backup.py +0 -0
  46. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/device_control.py +0 -0
  47. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/enhanced.py +0 -0
  48. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/helpers.py +0 -0
  49. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/registry.py +0 -0
  50. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/smart_search.py +0 -0
  51. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_addons.py +0 -0
  52. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_areas.py +0 -0
  53. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  54. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  55. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_calendar.py +0 -0
  56. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_camera.py +0 -0
  57. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  58. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  59. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  60. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  61. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_config_info.py +0 -0
  62. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  63. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_entities.py +0 -0
  64. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  65. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_groups.py +0 -0
  66. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_hacs.py +0 -0
  67. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_history.py +0 -0
  68. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_integrations.py +0 -0
  69. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_labels.py +0 -0
  70. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  71. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_registry.py +0 -0
  72. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_resources.py +0 -0
  73. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_search.py +0 -0
  74. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_service.py +0 -0
  75. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_services.py +0 -0
  76. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_todo.py +0 -0
  77. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_traces.py +0 -0
  78. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_updates.py +0 -0
  79. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_utility.py +0 -0
  80. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  81. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/util_helpers.py +0 -0
  82. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/utils/__init__.py +0 -0
  83. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/utils/domain_handlers.py +0 -0
  84. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  85. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/utils/operation_manager.py +0 -0
  86. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/utils/python_sandbox.py +0 -0
  87. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/utils/usage_logger.py +0 -0
  88. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  89. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  90. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  91. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  92. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  93. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/tests/__init__.py +0 -0
  94. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/tests/test_constants.py +0 -0
  95. {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/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.0.0.dev264
3
+ Version: 7.0.0.dev266
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
@@ -159,7 +159,7 @@ Spend less time configuring, more time enjoying your smart home.
159
159
  | **💾 System** | Backup/restore, updates, add-ons, device registry |
160
160
 
161
161
  <details>
162
- <summary><b>🛠️ Complete Tool List (97 tools)</b></summary>
162
+ <summary><b>🛠️ Complete Tool List (96 tools)</b></summary>
163
163
 
164
164
  | Category | Tools |
165
165
  |----------|-------|
@@ -171,7 +171,7 @@ Spend less time configuring, more time enjoying your smart home.
171
171
  | **Dashboards** | `ha_config_get_dashboard`, `ha_config_set_dashboard`, `ha_config_delete_dashboard`, `ha_get_dashboard_guide`, `ha_get_card_documentation` |
172
172
  | **Areas & Floors** | `ha_config_list_areas`, `ha_config_set_area`, `ha_config_remove_area`, `ha_config_list_floors`, `ha_config_set_floor`, `ha_config_remove_floor` |
173
173
  | **Labels** | `ha_config_get_label`, `ha_config_set_label`, `ha_config_remove_label`, `ha_manage_entity_labels` |
174
- | **Zones** | `ha_get_zone`, `ha_create_zone`, `ha_update_zone`, `ha_delete_zone` |
174
+ | **Zones** | `ha_get_zone`, `ha_set_zone`, `ha_remove_zone` |
175
175
  | **Groups** | `ha_config_list_groups`, `ha_config_set_group`, `ha_config_remove_group` |
176
176
  | **Todo Lists** | `ha_get_todo`, `ha_add_todo_item`, `ha_update_todo_item`, `ha_remove_todo_item` |
177
177
  | **Calendar** | `ha_config_get_calendar_events`, `ha_config_set_calendar_event`, `ha_config_remove_calendar_event` |
@@ -129,7 +129,7 @@ Spend less time configuring, more time enjoying your smart home.
129
129
  | **💾 System** | Backup/restore, updates, add-ons, device registry |
130
130
 
131
131
  <details>
132
- <summary><b>🛠️ Complete Tool List (97 tools)</b></summary>
132
+ <summary><b>🛠️ Complete Tool List (96 tools)</b></summary>
133
133
 
134
134
  | Category | Tools |
135
135
  |----------|-------|
@@ -141,7 +141,7 @@ Spend less time configuring, more time enjoying your smart home.
141
141
  | **Dashboards** | `ha_config_get_dashboard`, `ha_config_set_dashboard`, `ha_config_delete_dashboard`, `ha_get_dashboard_guide`, `ha_get_card_documentation` |
142
142
  | **Areas & Floors** | `ha_config_list_areas`, `ha_config_set_area`, `ha_config_remove_area`, `ha_config_list_floors`, `ha_config_set_floor`, `ha_config_remove_floor` |
143
143
  | **Labels** | `ha_config_get_label`, `ha_config_set_label`, `ha_config_remove_label`, `ha_manage_entity_labels` |
144
- | **Zones** | `ha_get_zone`, `ha_create_zone`, `ha_update_zone`, `ha_delete_zone` |
144
+ | **Zones** | `ha_get_zone`, `ha_set_zone`, `ha_remove_zone` |
145
145
  | **Groups** | `ha_config_list_groups`, `ha_config_set_group`, `ha_config_remove_group` |
146
146
  | **Todo Lists** | `ha_get_todo`, `ha_add_todo_item`, `ha_update_todo_item`, `ha_remove_todo_item` |
147
147
  | **Calendar** | `ha_config_get_calendar_events`, `ha_config_set_calendar_event`, `ha_config_remove_calendar_event` |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.0.0.dev264"
7
+ version = "7.0.0.dev266"
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"
@@ -830,91 +830,43 @@ class HomeAssistantClient:
830
830
  self, ws_client: Any, message: dict[str, Any]
831
831
  ) -> dict[str, Any]:
832
832
  """Handle render_template WebSocket command with event-based response."""
833
-
834
- # Generate our own message ID to track the response
835
- message_id = ws_client.get_next_message_id()
836
-
837
- # Construct the full message with proper ID
838
- full_message = {
839
- "id": message_id,
840
- "type": "render_template",
841
- "template": message.get("template"),
842
- "timeout": message.get("timeout", 3),
843
- "report_errors": message.get("report_errors", True),
844
- }
845
-
846
- # Create futures for both result and event responses
847
- result_future = ws_client.register_pending_response(message_id)
848
- event_future = ws_client.register_render_template_event(message_id)
849
-
850
- # Use WebSocket client's send helper to transmit the message
851
- try:
852
- await ws_client.send_json_message(full_message)
853
- except Exception as e:
854
- ws_client.cancel_pending_response(message_id)
855
- ws_client.cancel_render_template_event(message_id)
856
- raise e
833
+ template_timeout = message.get("timeout", 3)
857
834
 
858
835
  try:
859
- # Wait for the initial result response (should be success with null result)
860
- result_response = await asyncio.wait_for(
861
- result_future, timeout=message.get("timeout", 3) + 2
836
+ _, event_response = await ws_client.send_command_with_event(
837
+ "render_template",
838
+ wait_timeout=template_timeout + 2,
839
+ template=message.get("template"),
840
+ timeout=template_timeout,
841
+ report_errors=message.get("report_errors", True),
862
842
  )
863
- logger.debug(f"WebSocket render_template result: {result_response}")
843
+ logger.debug(f"WebSocket render_template event: {event_response}")
844
+
845
+ # Extract template result from event
846
+ if "event" in event_response and "result" in event_response["event"]:
847
+ template_result = event_response["event"]["result"]
848
+ listeners_info = event_response["event"].get("listeners", {})
864
849
 
865
- if not result_response.get("success"):
866
- ws_client.cancel_render_template_event(message_id)
867
- error = result_response.get("error", "Unknown error")
868
850
  return {
869
- "success": False,
870
- "error": str(error),
851
+ "success": True,
852
+ "result": template_result,
871
853
  "template": message.get("template"),
854
+ "listeners": listeners_info,
872
855
  }
873
-
874
- # Wait for the event with the actual template result
875
- try:
876
- event_response = await asyncio.wait_for(
877
- event_future, timeout=message.get("timeout", 3) + 1
878
- )
879
- logger.debug(f"WebSocket render_template event: {event_response}")
880
-
881
- # Extract template result from event
882
- if "event" in event_response and "result" in event_response["event"]:
883
- template_result = event_response["event"]["result"]
884
- listeners_info = event_response["event"].get("listeners", {})
885
-
886
- return {
887
- "success": True,
888
- "result": template_result,
889
- "template": message.get("template"),
890
- "listeners": listeners_info,
891
- }
892
- else:
893
- return {
894
- "success": False,
895
- "error": "Invalid event response format",
896
- "template": message.get("template"),
897
- }
898
-
899
- except TimeoutError:
900
- ws_client.cancel_render_template_event(message_id)
856
+ else:
901
857
  return {
902
858
  "success": False,
903
- "error": "Event timeout - template result not received",
859
+ "error": "Invalid event response format",
904
860
  "template": message.get("template"),
905
861
  }
906
862
 
907
863
  except TimeoutError:
908
- ws_client.cancel_pending_response(message_id)
909
- ws_client.cancel_render_template_event(message_id)
910
864
  return {
911
865
  "success": False,
912
- "error": "Command timeout",
866
+ "error": "Event timeout - template result not received",
913
867
  "template": message.get("template"),
914
868
  }
915
869
  except Exception as e:
916
- ws_client.cancel_pending_response(message_id)
917
- ws_client.cancel_render_template_event(message_id)
918
870
  return {
919
871
  "success": False,
920
872
  "error": str(e),
@@ -33,7 +33,7 @@ class WebSocketConnectionState:
33
33
  self._message_id = 0
34
34
  self._pending_requests: dict[int, asyncio.Future[dict[str, Any]]] = {}
35
35
  self._auth_messages: dict[str, dict[str, Any]] = {}
36
- self._render_template_events: dict[int, asyncio.Future[dict[str, Any]]] = {}
36
+ self._event_responses: dict[int, asyncio.Future[dict[str, Any]]] = {}
37
37
  self._event_handlers: dict[
38
38
  str, set[Callable[[dict[str, Any]], Awaitable[None]]]
39
39
  ] = defaultdict(set)
@@ -64,25 +64,25 @@ class WebSocketConnectionState:
64
64
  if future and not future.done():
65
65
  future.cancel()
66
66
 
67
- def register_render_template_event(
67
+ def register_event_response(
68
68
  self, message_id: int
69
69
  ) -> asyncio.Future[dict[str, Any]]:
70
- """Create and register a future for a render_template follow-up event."""
70
+ """Create and register a future for a follow-up event."""
71
71
  future: asyncio.Future[dict[str, Any]] = (
72
72
  asyncio.get_running_loop().create_future()
73
73
  )
74
- self._render_template_events[message_id] = future
74
+ self._event_responses[message_id] = future
75
75
  return future
76
76
 
77
- def resolve_render_template_event(
77
+ def resolve_event_response(
78
78
  self, message_id: int
79
79
  ) -> asyncio.Future[dict[str, Any]] | None:
80
- """Resolve a stored render_template event future."""
81
- return self._render_template_events.pop(message_id, None)
80
+ """Resolve a stored event future."""
81
+ return self._event_responses.pop(message_id, None)
82
82
 
83
- def cancel_render_template_event(self, message_id: int) -> None:
84
- """Cancel a stored render_template event future."""
85
- future = self._render_template_events.pop(message_id, None)
83
+ def cancel_event_response(self, message_id: int) -> None:
84
+ """Cancel a stored event future."""
85
+ future = self._event_responses.pop(message_id, None)
86
86
  if future and not future.done():
87
87
  future.cancel()
88
88
 
@@ -105,10 +105,10 @@ class WebSocketConnectionState:
105
105
  future.cancel()
106
106
  self._pending_requests.clear()
107
107
 
108
- for future in self._render_template_events.values():
108
+ for future in self._event_responses.values():
109
109
  if not future.done():
110
110
  future.cancel()
111
- self._render_template_events.clear()
111
+ self._event_responses.clear()
112
112
 
113
113
  self._auth_messages.clear()
114
114
 
@@ -325,7 +325,7 @@ class HomeAssistantWebSocketClient:
325
325
  # Handle events
326
326
  if message_type == "event":
327
327
  if message_id is not None:
328
- render_future = self._state.resolve_render_template_event(message_id)
328
+ render_future = self._state.resolve_event_response(message_id)
329
329
  if render_future:
330
330
  if not render_future.cancelled():
331
331
  render_future.set_result(data)
@@ -384,15 +384,15 @@ class HomeAssistantWebSocketClient:
384
384
  """Cancel and drop a pending response future."""
385
385
  self._state.cancel_pending_request(message_id)
386
386
 
387
- def register_render_template_event(
387
+ def register_event_response(
388
388
  self, message_id: int
389
389
  ) -> asyncio.Future[dict[str, Any]]:
390
- """Register a future for a render_template follow-up event."""
391
- return self._state.register_render_template_event(message_id)
390
+ """Register a future for a follow-up event."""
391
+ return self._state.register_event_response(message_id)
392
392
 
393
- def cancel_render_template_event(self, message_id: int) -> None:
394
- """Cancel and drop a stored render_template event future."""
395
- self._state.cancel_render_template_event(message_id)
393
+ def cancel_event_response(self, message_id: int) -> None:
394
+ """Cancel and drop a stored event future."""
395
+ self._state.cancel_event_response(message_id)
396
396
 
397
397
  async def send_command(self, command_type: str, **kwargs: Any) -> dict[str, Any]:
398
398
  """Send command and wait for response.
@@ -457,6 +457,71 @@ class HomeAssistantWebSocketClient:
457
457
  self.cancel_pending_response(message_id)
458
458
  raise
459
459
 
460
+ async def send_command_with_event(
461
+ self,
462
+ command_type: str,
463
+ wait_timeout: float = 10.0,
464
+ **kwargs: Any,
465
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
466
+ """Send a command that returns a result followed by an event response.
467
+
468
+ Some HA WebSocket commands (e.g. system_health/info, render_template)
469
+ reply with an immediate result message and then deliver the actual data
470
+ in a subsequent event message sharing the same message ID.
471
+
472
+ Args:
473
+ command_type: Type of command to send.
474
+ wait_timeout: Seconds to wait for each response phase.
475
+ **kwargs: Additional fields merged into the outgoing message.
476
+
477
+ Returns:
478
+ A (result_response, event_response) tuple.
479
+ """
480
+ if not self._state.is_ready:
481
+ raise Exception("WebSocket not authenticated")
482
+
483
+ message_id = self.get_next_message_id()
484
+ message = {"id": message_id, "type": command_type, **kwargs}
485
+
486
+ result_future = self.register_pending_response(message_id)
487
+ event_future = self.register_event_response(message_id)
488
+
489
+ try:
490
+ await self.send_json_message(message)
491
+ except Exception:
492
+ self.cancel_pending_response(message_id)
493
+ self.cancel_event_response(message_id)
494
+ raise
495
+
496
+ try:
497
+ result_response = await asyncio.wait_for(
498
+ result_future, timeout=wait_timeout
499
+ )
500
+ except TimeoutError:
501
+ self.cancel_pending_response(message_id)
502
+ self.cancel_event_response(message_id)
503
+ raise
504
+
505
+ if not result_response.get("success"):
506
+ self.cancel_event_response(message_id)
507
+ error = result_response.get("error", {})
508
+ error_msg = (
509
+ error.get("message", str(error))
510
+ if isinstance(error, dict)
511
+ else str(error)
512
+ )
513
+ raise Exception(f"Command failed: {error_msg}")
514
+
515
+ try:
516
+ event_response = await asyncio.wait_for(
517
+ event_future, timeout=wait_timeout
518
+ )
519
+ except TimeoutError:
520
+ self.cancel_event_response(message_id)
521
+ raise
522
+
523
+ return result_response, event_response
524
+
460
525
  async def subscribe_events(self, event_type: str | None = None) -> int:
461
526
  """Subscribe to Home Assistant events.
462
527
 
@@ -333,16 +333,23 @@ def register_system_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
333
333
  "Failed to connect to Home Assistant WebSocket",
334
334
  ))
335
335
 
336
- # Request system health info via WebSocket
337
- health_response = await ws_client.send_command("system_health/info")
338
-
339
- if not health_response.get("success"):
336
+ # system_health/info returns a result + follow-up event
337
+ try:
338
+ _, event_response = await ws_client.send_command_with_event(
339
+ "system_health/info", wait_timeout=10.0
340
+ )
341
+ except TimeoutError:
342
+ raise_tool_error(create_error_response(
343
+ ErrorCode.SERVICE_CALL_FAILED,
344
+ "Timeout waiting for system health data",
345
+ ))
346
+ except Exception as e:
340
347
  raise_tool_error(create_error_response(
341
348
  ErrorCode.SERVICE_CALL_FAILED,
342
- health_response.get("error", "Failed to retrieve system health"),
349
+ str(e),
343
350
  ))
344
351
 
345
- health_info = health_response.get("result") or {}
352
+ health_info = event_response.get("event", {})
346
353
 
347
354
  return {
348
355
  "success": True,