ha-mcp-dev 7.5.0.dev554__tar.gz → 7.5.0.dev556__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.5.0.dev554/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev556}/PKG-INFO +1 -1
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/pyproject.toml +1 -1
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/client/websocket_client.py +54 -10
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/client/websocket_listener.py +51 -7
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/settings_ui.py +42 -6
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_config_helpers.py +8 -2
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/LICENSE +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/README.md +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/setup.cfg +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/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.5.0.
|
|
7
|
+
version = "7.5.0.dev556"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
598
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
142
|
-
|
|
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(
|
|
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
|
-
#
|
|
888
|
-
#
|
|
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(
|
|
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(
|
|
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,
|
{ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
@@ -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
|
|
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.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/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
|
{ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/best_practice_checker.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
|
{ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/tools/tools_voice_assistant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/transforms/categorized_search.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/transforms/lite_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp/utils/kill_signal_diagnostics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.5.0.dev554 → ha_mcp_dev-7.5.0.dev556}/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
|