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.
- {ha_mcp_dev-7.0.0.dev264/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.0.0.dev266}/PKG-INFO +3 -3
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/README.md +2 -2
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/pyproject.toml +1 -1
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/client/rest_client.py +19 -67
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/client/websocket_client.py +84 -19
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_system.py +13 -6
- ha_mcp_dev-7.0.0.dev266/src/ha_mcp/tools/tools_zones.py +337 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266/src/ha_mcp_dev.egg-info}/PKG-INFO +3 -3
- ha_mcp_dev-7.0.0.dev264/src/ha_mcp/tools/tools_zones.py +0 -387
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/LICENSE +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/setup.cfg +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/card_types.json +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/dashboard_guide.md +0 -0
- {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
- {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
- {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
- {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
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {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
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_config_info.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {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
- {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
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {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
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.0.0.dev264 → ha_mcp_dev-7.0.0.dev266}/tests/test_constants.py +0 -0
- {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.
|
|
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 (
|
|
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`, `
|
|
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 (
|
|
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`, `
|
|
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.
|
|
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
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
|
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":
|
|
870
|
-
"
|
|
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": "
|
|
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": "
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
74
|
+
self._event_responses[message_id] = future
|
|
75
75
|
return future
|
|
76
76
|
|
|
77
|
-
def
|
|
77
|
+
def resolve_event_response(
|
|
78
78
|
self, message_id: int
|
|
79
79
|
) -> asyncio.Future[dict[str, Any]] | None:
|
|
80
|
-
"""Resolve a stored
|
|
81
|
-
return self.
|
|
80
|
+
"""Resolve a stored event future."""
|
|
81
|
+
return self._event_responses.pop(message_id, None)
|
|
82
82
|
|
|
83
|
-
def
|
|
84
|
-
"""Cancel a stored
|
|
85
|
-
future = self.
|
|
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.
|
|
108
|
+
for future in self._event_responses.values():
|
|
109
109
|
if not future.done():
|
|
110
110
|
future.cancel()
|
|
111
|
-
self.
|
|
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.
|
|
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
|
|
387
|
+
def register_event_response(
|
|
388
388
|
self, message_id: int
|
|
389
389
|
) -> asyncio.Future[dict[str, Any]]:
|
|
390
|
-
"""Register a future for a
|
|
391
|
-
return self._state.
|
|
390
|
+
"""Register a future for a follow-up event."""
|
|
391
|
+
return self._state.register_event_response(message_id)
|
|
392
392
|
|
|
393
|
-
def
|
|
394
|
-
"""Cancel and drop a stored
|
|
395
|
-
self._state.
|
|
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
|
-
#
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
349
|
+
str(e),
|
|
343
350
|
))
|
|
344
351
|
|
|
345
|
-
health_info =
|
|
352
|
+
health_info = event_response.get("event", {})
|
|
346
353
|
|
|
347
354
|
return {
|
|
348
355
|
"success": True,
|