ha-mcp-dev 7.0.0.dev265__tar.gz → 7.0.0.dev267__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.dev265/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.0.0.dev267}/PKG-INFO +1 -1
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/pyproject.toml +1 -1
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/client/rest_client.py +19 -67
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/client/websocket_client.py +84 -19
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/device_control.py +15 -5
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_services.py +2 -2
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_system.py +13 -6
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/utils/domain_handlers.py +10 -5
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/LICENSE +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/README.md +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/setup.cfg +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/card_types.json +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/dashboard_guide.md +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_config_info.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/tests/test_env_manager.py +0 -0
|
@@ -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.dev267"
|
|
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
|
|
|
@@ -90,7 +90,7 @@ class DeviceControlTools:
|
|
|
90
90
|
f"Invalid JSON in parameters: {parameters}",
|
|
91
91
|
suggestions=[
|
|
92
92
|
"Parameters should be a valid JSON object",
|
|
93
|
-
"Example: {'brightness': 102, '
|
|
93
|
+
"Example: {'brightness': 102, 'color_temp_kelvin': 4000}",
|
|
94
94
|
],
|
|
95
95
|
context={"entity_id": entity_id, "action": action},
|
|
96
96
|
))
|
|
@@ -268,12 +268,22 @@ class DeviceControlTools:
|
|
|
268
268
|
# Add parameters based on domain
|
|
269
269
|
if parameters:
|
|
270
270
|
if domain == "light":
|
|
271
|
+
# Backward compat: convert deprecated color temp parameters
|
|
272
|
+
if "color_temp_kelvin" not in parameters:
|
|
273
|
+
if "kelvin" in parameters:
|
|
274
|
+
parameters["color_temp_kelvin"] = parameters.pop("kelvin")
|
|
275
|
+
elif "color_temp" in parameters:
|
|
276
|
+
mired_val = parameters.pop("color_temp")
|
|
277
|
+
if isinstance(mired_val, (int, float)) and mired_val > 0:
|
|
278
|
+
parameters["color_temp_kelvin"] = round(
|
|
279
|
+
1_000_000 / mired_val
|
|
280
|
+
)
|
|
281
|
+
|
|
271
282
|
light_params = [
|
|
272
283
|
"brightness",
|
|
273
|
-
"
|
|
284
|
+
"color_temp_kelvin",
|
|
274
285
|
"rgb_color",
|
|
275
286
|
"effect",
|
|
276
|
-
"kelvin",
|
|
277
287
|
]
|
|
278
288
|
for param in light_params:
|
|
279
289
|
if param in parameters:
|
|
@@ -344,8 +354,8 @@ class DeviceControlTools:
|
|
|
344
354
|
if domain == "light" and action in ["on", "set"]:
|
|
345
355
|
if "brightness" in parameters:
|
|
346
356
|
expected["brightness"] = parameters["brightness"]
|
|
347
|
-
if "
|
|
348
|
-
expected["
|
|
357
|
+
if "color_temp_kelvin" in parameters:
|
|
358
|
+
expected["color_temp_kelvin"] = parameters["color_temp_kelvin"]
|
|
349
359
|
|
|
350
360
|
elif domain == "climate" and action in ["set", "heat", "cool", "auto"]:
|
|
351
361
|
if "temperature" in parameters:
|
|
@@ -325,8 +325,8 @@ def _get_field_type(selector: dict[str, Any]) -> str:
|
|
|
325
325
|
if "datetime" in selector:
|
|
326
326
|
return "datetime"
|
|
327
327
|
|
|
328
|
-
if "color_temp" in selector:
|
|
329
|
-
return "
|
|
328
|
+
if "color_temp" in selector or "color_temp_kelvin" in selector:
|
|
329
|
+
return "color_temp_kelvin"
|
|
330
330
|
|
|
331
331
|
if "color_rgb" in selector:
|
|
332
332
|
return "color_rgb"
|
|
@@ -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,
|
|
@@ -13,14 +13,19 @@ DOMAIN_HANDLERS = {
|
|
|
13
13
|
"valid_actions": ["on", "off", "toggle", "set", "adjust"],
|
|
14
14
|
"parameters": [
|
|
15
15
|
"brightness",
|
|
16
|
-
"
|
|
16
|
+
"color_temp_kelvin",
|
|
17
17
|
"rgb_color",
|
|
18
18
|
"effect",
|
|
19
|
-
"kelvin",
|
|
20
19
|
"hs_color",
|
|
21
20
|
],
|
|
22
21
|
"quick_actions": ["toggle", "dim", "brighten"],
|
|
23
|
-
"state_attributes": [
|
|
22
|
+
"state_attributes": [
|
|
23
|
+
"brightness",
|
|
24
|
+
"color_temp_kelvin",
|
|
25
|
+
"min_color_temp_kelvin",
|
|
26
|
+
"max_color_temp_kelvin",
|
|
27
|
+
"rgb_color",
|
|
28
|
+
],
|
|
24
29
|
"supports_dimming": True,
|
|
25
30
|
"supports_color": True,
|
|
26
31
|
},
|
|
@@ -312,8 +317,8 @@ def get_suggested_parameters(domain: str, action: str) -> list[str]:
|
|
|
312
317
|
# Action-specific parameter suggestions
|
|
313
318
|
action_params = {
|
|
314
319
|
"light": {
|
|
315
|
-
"set": ["brightness", "
|
|
316
|
-
"on": ["brightness", "
|
|
320
|
+
"set": ["brightness", "color_temp_kelvin", "rgb_color"],
|
|
321
|
+
"on": ["brightness", "color_temp_kelvin"],
|
|
317
322
|
"adjust": ["brightness"],
|
|
318
323
|
},
|
|
319
324
|
"climate": {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/resources/skills-vendor/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp/tools/tools_voice_assistant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.0.0.dev265 → ha_mcp_dev-7.0.0.dev267}/src/ha_mcp_dev.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|