ha-mcp-dev 7.5.0.dev554__tar.gz → 7.5.0.dev555__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.dev554/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev555}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/client/websocket_client.py +54 -10
  4. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/client/websocket_listener.py +51 -7
  5. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/settings_ui.py +42 -6
  6. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_config_helpers.py +8 -2
  7. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  8. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/LICENSE +0 -0
  9. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/MANIFEST.in +0 -0
  10. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/README.md +0 -0
  11. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/setup.cfg +0 -0
  12. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/__init__.py +0 -0
  13. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/__main__.py +0 -0
  14. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/_pypi_marker +0 -0
  15. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/_version.py +0 -0
  16. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/auth/__init__.py +0 -0
  17. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/auth/consent_form.py +0 -0
  18. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/auth/provider.py +0 -0
  19. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/client/__init__.py +0 -0
  20. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/client/rest_client.py +0 -0
  21. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/client/supervisor_client.py +0 -0
  22. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/config.py +0 -0
  23. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/errors.py +0 -0
  24. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/py.typed +0 -0
  25. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  26. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  27. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  28. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  29. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  30. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  31. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  32. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  33. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  34. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  35. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  36. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  37. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  38. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  39. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  40. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  41. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  42. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  43. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  44. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  45. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/server.py +0 -0
  46. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/smoke_test.py +0 -0
  47. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/__init__.py +0 -0
  48. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/backup.py +0 -0
  49. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  50. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/device_control.py +0 -0
  51. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/enhanced.py +0 -0
  52. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/helpers.py +0 -0
  53. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/reference_validator.py +0 -0
  54. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/registry.py +0 -0
  55. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/smart_search.py +0 -0
  56. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_addons.py +0 -0
  57. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_areas.py +0 -0
  58. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  59. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  60. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_calendar.py +0 -0
  61. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_camera.py +0 -0
  62. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_categories.py +0 -0
  63. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_code.py +0 -0
  64. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  65. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  66. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  67. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  68. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  69. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_energy.py +0 -0
  70. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_entities.py +0 -0
  71. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  72. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_groups.py +0 -0
  73. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_hacs.py +0 -0
  74. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_history.py +0 -0
  75. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_integrations.py +0 -0
  76. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_labels.py +0 -0
  77. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  78. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_registry.py +0 -0
  79. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_resources.py +0 -0
  80. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_search.py +0 -0
  81. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_service.py +0 -0
  82. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_services.py +0 -0
  83. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_system.py +0 -0
  84. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_todo.py +0 -0
  85. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_traces.py +0 -0
  86. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_updates.py +0 -0
  87. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_utility.py +0 -0
  88. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  89. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  90. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/tools_zones.py +0 -0
  91. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/tools/util_helpers.py +0 -0
  92. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/transforms/__init__.py +0 -0
  93. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/transforms/categorized_search.py +0 -0
  94. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  95. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/utils/__init__.py +0 -0
  96. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/utils/config_hash.py +0 -0
  97. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/utils/data_paths.py +0 -0
  98. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/utils/domain_handlers.py +0 -0
  99. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  100. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  101. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/utils/operation_manager.py +0 -0
  102. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/utils/python_sandbox.py +0 -0
  103. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp/utils/usage_logger.py +0 -0
  104. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  105. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  106. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  107. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  108. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  109. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/tests/__init__.py +0 -0
  110. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/tests/test_constants.py +0 -0
  111. {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev555}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.5.0.dev554
3
+ Version: 7.5.0.dev555
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.dev554"
7
+ version = "7.5.0.dev555"
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"
@@ -584,24 +584,68 @@ class HomeAssistantWebSocketClient:
584
584
  async def subscribe_events(self, event_type: str | None = None) -> int:
585
585
  """Subscribe to Home Assistant events.
586
586
 
587
+ HA's WebSocket API identifies a subscription by the ``id`` of the
588
+ original ``subscribe_events`` command — not by a field inside the
589
+ ``result`` payload. ``handle_subscribe_events`` in HA Core
590
+ (``websocket_api/commands.py``) ends with
591
+ ``connection.send_result(msg["id"])``, and ``send_result(msg_id)``
592
+ emits ``{"id": N, "type": "result", "success": true, "result": null}``.
593
+ Subsequent event deliveries arrive as
594
+ ``{"id": N, "type": "event", "event": {...}}`` with the same ``id``.
595
+
596
+ The previous implementation called ``send_command`` (which discards
597
+ the message_id it generated) and then looked for
598
+ ``response["result"]["subscription"]``, a field HA does not send.
599
+ That branch never matched, so this function always raised
600
+ ``"Failed to get subscription ID"`` — even though the underlying
601
+ subscription on HA's side WAS established. The ``WebSocketListenerService``
602
+ treated the raised exception as a startup failure and left
603
+ ``_listener_started = False``, so every device-control call
604
+ repeatedly retried (and re-failed) and ``OperationManager.process_state_change``
605
+ was never invoked, leaving every async operation in PENDING until
606
+ ``OperationManager.get_operation`` flipped it to TIMEOUT after
607
+ the 10s ``timeout_ms`` budget. Surfaced during PR #1375 HAOS log
608
+ audit (3x "Failed to get subscription ID" per bulk-control test).
609
+
587
610
  Args:
588
611
  event_type: Specific event type to subscribe to (None for all)
589
612
 
590
613
  Returns:
591
- Subscription ID
614
+ Subscription ID (the message_id used when subscribing)
592
615
  """
593
- kwargs = {}
616
+ if not self._state.is_ready:
617
+ raise HomeAssistantConnectionError("WebSocket not authenticated")
618
+
619
+ message_id = self.get_next_message_id()
620
+ message: dict[str, Any] = {"id": message_id, "type": "subscribe_events"}
594
621
  if event_type:
595
- kwargs["event_type"] = event_type
622
+ message["event_type"] = event_type
623
+
624
+ future = self.register_pending_response(message_id)
625
+ try:
626
+ await self.send_json_message(message)
627
+ except Exception:
628
+ self.cancel_pending_response(message_id)
629
+ raise
630
+
631
+ try:
632
+ response = await asyncio.wait_for(future, timeout=30.0)
633
+ except TimeoutError:
634
+ self.cancel_pending_response(message_id)
635
+ raise
596
636
 
597
- response = await self.send_command("subscribe_events", **kwargs)
598
- result = response.get("result")
599
- if isinstance(result, dict):
600
- subscription_id = result.get("subscription")
601
- if isinstance(subscription_id, int):
602
- return subscription_id
637
+ if response.get("type") == "result" and response.get("success"):
638
+ return message_id
603
639
 
604
- raise Exception("Failed to get subscription ID")
640
+ error = response.get("error", {})
641
+ error_msg = (
642
+ error.get("message", str(error))
643
+ if isinstance(error, dict)
644
+ else str(error)
645
+ )
646
+ raise HomeAssistantCommandError(
647
+ f"subscribe_events failed: {error_msg}"
648
+ )
605
649
 
606
650
  def add_event_handler(
607
651
  self,
@@ -107,19 +107,48 @@ class WebSocketListenerService:
107
107
  async def _handle_state_change(self, event: dict[str, Any]) -> None:
108
108
  """Handle state change events from Home Assistant.
109
109
 
110
+ HA's WS ``state_changed`` event arrives shaped as::
111
+
112
+ {
113
+ "event_type": "state_changed",
114
+ "data": {
115
+ "entity_id": "light.x",
116
+ "new_state": {...},
117
+ "old_state": {...}
118
+ },
119
+ "time_fired": "...",
120
+ "origin": "LOCAL",
121
+ "context": {...}
122
+ }
123
+
124
+ The handler receives the full event object (the WS dispatcher
125
+ in ``websocket_client._handle_event_message`` passes
126
+ ``message["event"]``). entity_id / new_state / old_state live
127
+ under ``event["data"]``, NOT at the top level. Previously this
128
+ function read from the top level and the guard fired silently
129
+ on every event — ``update_pending_operations`` was never
130
+ invoked and async device operations stayed PENDING until they
131
+ expired. Surfaced during PR #1375 HAOS log audit
132
+ (TIMEOUT_OPERATION x 3 per bulk-control test).
133
+
110
134
  Args:
111
- event: State change event data
135
+ event: HA state_changed event payload
112
136
  """
137
+ # Initialised here so the narrow ``except`` log at the bottom can
138
+ # safely reference them even if extraction below hasn't run yet.
139
+ entity_id: str | None = None
140
+ new_state: dict[str, Any] | None = None
113
141
  try:
114
142
  events_processed = self.stats["events_processed"]
115
143
  if isinstance(events_processed, int):
116
144
  self.stats["events_processed"] = events_processed + 1
117
145
  self.stats["last_event_time"] = datetime.now()
118
146
 
119
- # Extract event data
120
- entity_id = event.get("entity_id")
121
- new_state = event.get("new_state")
122
- old_state = event.get("old_state")
147
+ # Extract event data — fields are nested under ``event["data"]``.
148
+ event_data = event.get("data") or {}
149
+ entity_id = event_data.get("entity_id")
150
+ new_state = event_data.get("new_state")
151
+ old_state = event_data.get("old_state")
123
152
 
124
153
  if not entity_id or not new_state:
125
154
  return
@@ -138,8 +167,23 @@ class WebSocketListenerService:
138
167
  self.stats["operations_updated"] = operations_updated + len(updated_ops)
139
168
  logger.info(f"Updated {len(updated_ops)} operations for {entity_id}")
140
169
 
141
- except Exception as e:
142
- logger.error(f"Error handling state change event: {e}")
170
+ except (RuntimeError, ConnectionError, OSError) as e:
171
+ # Narrow catch: keep the listener alive on transport-level
172
+ # transients so a single bad event doesn't kill the
173
+ # subscription, but let code bugs (KeyError, TypeError,
174
+ # AttributeError) propagate. That discipline is what would
175
+ # have caught the data-nesting silent failure this PR just
176
+ # fixed — broad ``except Exception`` is how that bug
177
+ # survived for months. Use ``logger.exception`` to capture
178
+ # the traceback and include event context so the next
179
+ # regression is debuggable.
180
+ logger.exception(
181
+ "Transient error handling state_changed event "
182
+ "(entity_id=%r, event_type=%r): %r",
183
+ entity_id,
184
+ event.get("event_type"),
185
+ e,
186
+ )
143
187
 
144
188
  async def _connection_monitor(self) -> None:
145
189
  """Monitor WebSocket connection health."""
@@ -874,7 +874,7 @@ def register_settings_routes(
874
874
  }
875
875
  )
876
876
 
877
- async def _restart_addon(_: Request) -> JSONResponse:
877
+ async def _restart_addon(request: Request) -> JSONResponse:
878
878
  if not os.environ.get("SUPERVISOR_TOKEN"):
879
879
  return JSONResponse(
880
880
  create_error_response(
@@ -884,13 +884,44 @@ def register_settings_routes(
884
884
  ),
885
885
  status_code=400,
886
886
  )
887
- # Short timeout the supervisor kills our process during restart so
888
- # the connection will drop. A connection drop is actually success.
887
+ # Optional slug from the request body lets callers restart a sibling
888
+ # addon instead of self. The UI's restart button posts an empty body
889
+ # and gets the historical self-restart behavior; the inaddon E2E
890
+ # suite uses ``slug`` to exercise the Supervisor restart wire
891
+ # contract against a non-test-critical addon (the dev addon's
892
+ # session would otherwise drop). The token's hassio_role gates
893
+ # whether the call actually succeeds for non-self targets.
894
+ #
895
+ # The slug is interpolated into the Supervisor endpoint URL, so it
896
+ # must be tightly constrained — Supervisor addon slugs are
897
+ # ``[a-z0-9_]+`` per the addon-config schema, but defending against
898
+ # path-traversal (``..``, ``/``, URL-encoded variants) at the edge
899
+ # is cheaper than relying on Supervisor to reject every bad shape.
900
+ # Reject anything outside ``[A-Za-z0-9_-]`` and silently fall back
901
+ # to ``self`` — same outcome as no body.
902
+ target_slug = "self"
903
+ try:
904
+ payload = await request.json()
905
+ except (ValueError, json.JSONDecodeError):
906
+ payload = None
907
+ if isinstance(payload, dict):
908
+ requested = payload.get("slug")
909
+ if (
910
+ isinstance(requested, str)
911
+ and requested.strip()
912
+ and all(c.isalnum() or c in "_-" for c in requested.strip())
913
+ ):
914
+ target_slug = requested.strip()
915
+
916
+ endpoint = f"/addons/{target_slug}/restart"
917
+ # Short timeout — when restarting self, the supervisor kills our
918
+ # process during restart so the connection will drop. A connection
919
+ # drop is actually success on that path.
889
920
  try:
890
921
  async with make_supervisor_httpx_client(
891
922
  timeout=5.0, verify=server.settings.verify_ssl
892
923
  ) as client:
893
- resp = await client.post("/addons/self/restart")
924
+ resp = await client.post(endpoint)
894
925
  except (httpx.ReadError, httpx.RemoteProtocolError):
895
926
  # Connection dropped mid-request — restart is happening.
896
927
  # `ConnectError` is deliberately NOT in this tuple: it fires
@@ -898,7 +929,7 @@ def register_settings_routes(
898
929
  # Supervisor socket misconfigured) and means the restart was
899
930
  # never initiated. Falls through to the `httpx.HTTPError`
900
931
  # handler below, which returns 502 + CONNECTION_FAILED.
901
- logger.info("Restart request connection dropped (expected during restart)")
932
+ logger.info("Restart request connection dropped (expected during self-restart)")
902
933
  return JSONResponse({"success": True, "message": "Restart initiated"})
903
934
  except httpx.HTTPError as e:
904
935
  logger.exception("Failed to reach Supervisor for restart")
@@ -912,7 +943,12 @@ def register_settings_routes(
912
943
 
913
944
  if resp.status_code >= 400:
914
945
  body = resp.text
915
- logger.error("Supervisor restart failed: %d %s", resp.status_code, body)
946
+ logger.error(
947
+ "Supervisor restart failed (slug=%s): %d %s",
948
+ target_slug,
949
+ resp.status_code,
950
+ body,
951
+ )
916
952
  return JSONResponse(
917
953
  create_error_response(
918
954
  ErrorCode.INTERNAL_ERROR,
@@ -2811,9 +2811,15 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2811
2811
  # ``result.get("warnings", [])`` uniformly.
2812
2812
  warnings: list[str] = []
2813
2813
 
2814
- # Wait for entity to be properly registered before proceeding
2814
+ # Wait for entity to be properly registered before proceeding.
2815
+ # Tags live in their own tag registry and never appear in
2816
+ # ``/api/states/<entity_id>`` — polling there always 404s
2817
+ # for the full timeout. The sibling ``helper_type == "tag"``
2818
+ # branch under the update action below already skips the
2819
+ # wait for this reason; mirror it here so create doesn't
2820
+ # burn 10s per tag on every CI run.
2815
2821
  wait_bool = coerce_bool_param(wait, "wait", default=True)
2816
- if wait_bool and entity_id:
2822
+ if wait_bool and entity_id and helper_type != "tag":
2817
2823
  try:
2818
2824
  registered = await wait_for_entity_registered(
2819
2825
  client, entity_id
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.5.0.dev554
3
+ Version: 7.5.0.dev555
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