ha-mcp-dev 7.4.1.dev421__tar.gz → 7.4.1.dev423__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.4.1.dev421/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev423}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/pyproject.toml +1 -1
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/client/rest_client.py +39 -3
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/client/websocket_client.py +55 -3
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/config.py +13 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/backup.py +6 -2
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/helpers.py +6 -2
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_addons.py +3 -1
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_history.py +3 -1
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_system.py +3 -1
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_traces.py +3 -1
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/setup.cfg +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/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.4.1.
|
|
7
|
+
version = "7.4.1.dev423"
|
|
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"
|
|
@@ -5,12 +5,27 @@ Home Assistant HTTP client with authentication and error handling.
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
|
+
import ssl
|
|
8
9
|
from typing import Any
|
|
9
10
|
|
|
10
11
|
import httpx
|
|
11
12
|
|
|
12
13
|
from ..config import get_global_settings
|
|
13
14
|
|
|
15
|
+
|
|
16
|
+
def _is_ssl_error(exc: BaseException) -> bool:
|
|
17
|
+
"""True if ``exc`` (or anything in its cause chain) is an SSL error.
|
|
18
|
+
|
|
19
|
+
httpx wraps ``ssl.SSLError`` inside ``httpx.ConnectError``; the only
|
|
20
|
+
reliable check is to walk ``__cause__`` / ``__context__``.
|
|
21
|
+
"""
|
|
22
|
+
cur: BaseException | None = exc
|
|
23
|
+
while cur is not None:
|
|
24
|
+
if isinstance(cur, ssl.SSLError):
|
|
25
|
+
return True
|
|
26
|
+
cur = cur.__cause__ or cur.__context__
|
|
27
|
+
return False
|
|
28
|
+
|
|
14
29
|
logger = logging.getLogger(__name__)
|
|
15
30
|
|
|
16
31
|
|
|
@@ -62,6 +77,7 @@ class HomeAssistantClient:
|
|
|
62
77
|
base_url: str | None = None,
|
|
63
78
|
token: str | None = None,
|
|
64
79
|
timeout: int | None = None,
|
|
80
|
+
verify_ssl: bool | None = None,
|
|
65
81
|
):
|
|
66
82
|
"""
|
|
67
83
|
Initialize Home Assistant client.
|
|
@@ -70,18 +86,31 @@ class HomeAssistantClient:
|
|
|
70
86
|
base_url: Home Assistant URL (defaults to config)
|
|
71
87
|
token: Long-lived access token (defaults to config)
|
|
72
88
|
timeout: Request timeout in seconds (defaults to config)
|
|
89
|
+
verify_ssl: Whether to verify the HA server's TLS certificate
|
|
90
|
+
(defaults to ``settings.verify_ssl``). Pass False to allow
|
|
91
|
+
self-signed certs or hostname mismatches.
|
|
73
92
|
"""
|
|
74
|
-
|
|
75
|
-
if base_url is None or token is None:
|
|
93
|
+
if base_url is None or token is None or verify_ssl is None:
|
|
76
94
|
settings = get_global_settings()
|
|
77
95
|
self.base_url = (base_url or settings.homeassistant_url).rstrip("/")
|
|
78
96
|
self.token = token or settings.homeassistant_token
|
|
79
97
|
self.timeout = timeout if timeout is not None else settings.timeout
|
|
98
|
+
self.verify_ssl = (
|
|
99
|
+
verify_ssl if verify_ssl is not None else settings.verify_ssl
|
|
100
|
+
)
|
|
80
101
|
else:
|
|
81
|
-
# All required parameters provided, use them directly without loading settings
|
|
82
102
|
self.base_url = base_url.rstrip("/")
|
|
83
103
|
self.token = token
|
|
84
104
|
self.timeout = timeout if timeout is not None else 30 # Default timeout
|
|
105
|
+
self.verify_ssl = verify_ssl
|
|
106
|
+
|
|
107
|
+
if not self.verify_ssl:
|
|
108
|
+
logger.warning(
|
|
109
|
+
"TLS verification disabled for Home Assistant REST client "
|
|
110
|
+
"(HA_VERIFY_SSL=false). Connections to %s will accept "
|
|
111
|
+
"self-signed and mismatched certificates.",
|
|
112
|
+
self.base_url,
|
|
113
|
+
)
|
|
85
114
|
|
|
86
115
|
# Create HTTP client with authentication headers
|
|
87
116
|
self.httpx_client = httpx.AsyncClient(
|
|
@@ -91,6 +120,7 @@ class HomeAssistantClient:
|
|
|
91
120
|
"Content-Type": "application/json",
|
|
92
121
|
},
|
|
93
122
|
timeout=httpx.Timeout(self.timeout),
|
|
123
|
+
verify=self.verify_ssl,
|
|
94
124
|
)
|
|
95
125
|
|
|
96
126
|
logger.info(f"Initialized Home Assistant client for {self.base_url}")
|
|
@@ -148,6 +178,12 @@ class HomeAssistantClient:
|
|
|
148
178
|
return response
|
|
149
179
|
|
|
150
180
|
except httpx.ConnectError as e:
|
|
181
|
+
if _is_ssl_error(e) and self.verify_ssl:
|
|
182
|
+
raise HomeAssistantConnectionError(
|
|
183
|
+
f"TLS verification failed for {self.base_url}: {e}. "
|
|
184
|
+
"If this is a self-signed certificate or hostname "
|
|
185
|
+
"mismatch, set HA_VERIFY_SSL=false to skip verification."
|
|
186
|
+
) from e
|
|
151
187
|
raise HomeAssistantConnectionError(
|
|
152
188
|
f"Failed to connect to Home Assistant: {e}"
|
|
153
189
|
) from e
|
|
@@ -11,6 +11,7 @@ import asyncio
|
|
|
11
11
|
import hashlib
|
|
12
12
|
import json
|
|
13
13
|
import logging
|
|
14
|
+
import ssl
|
|
14
15
|
import time
|
|
15
16
|
from collections import defaultdict
|
|
16
17
|
from collections.abc import Awaitable, Callable
|
|
@@ -20,7 +21,11 @@ from urllib.parse import urlparse
|
|
|
20
21
|
import websockets
|
|
21
22
|
|
|
22
23
|
from ..config import get_global_settings
|
|
23
|
-
from .rest_client import
|
|
24
|
+
from .rest_client import (
|
|
25
|
+
HomeAssistantCommandError,
|
|
26
|
+
HomeAssistantConnectionError,
|
|
27
|
+
_is_ssl_error,
|
|
28
|
+
)
|
|
24
29
|
|
|
25
30
|
logger = logging.getLogger(__name__)
|
|
26
31
|
|
|
@@ -158,15 +163,33 @@ class WebSocketConnectionState:
|
|
|
158
163
|
class HomeAssistantWebSocketClient:
|
|
159
164
|
"""WebSocket client for Home Assistant real-time communication."""
|
|
160
165
|
|
|
161
|
-
def __init__(self, url: str, token: str):
|
|
166
|
+
def __init__(self, url: str, token: str, verify_ssl: bool | None = None):
|
|
162
167
|
"""Initialize WebSocket client.
|
|
163
168
|
|
|
164
169
|
Args:
|
|
165
170
|
url: Home Assistant URL (e.g., 'https://homeassistant.local:8123')
|
|
166
171
|
token: Home Assistant long-lived access token
|
|
172
|
+
verify_ssl: Whether to verify the HA server's TLS certificate
|
|
173
|
+
for ``wss://`` connections. Defaults to
|
|
174
|
+
``settings.verify_ssl``. Pass False to allow self-signed
|
|
175
|
+
certs or hostname mismatches.
|
|
167
176
|
"""
|
|
168
177
|
self.base_url = url.rstrip("/")
|
|
169
178
|
self.token = token
|
|
179
|
+
if verify_ssl is None:
|
|
180
|
+
try:
|
|
181
|
+
verify_ssl = get_global_settings().verify_ssl
|
|
182
|
+
except Exception as e:
|
|
183
|
+
# A bad env var elsewhere should not silently flip TLS off:
|
|
184
|
+
# log which key tripped and fall back to the secure default.
|
|
185
|
+
logger.warning(
|
|
186
|
+
"Could not load settings while resolving verify_ssl "
|
|
187
|
+
"(%s); falling back to verify_ssl=True.",
|
|
188
|
+
e,
|
|
189
|
+
)
|
|
190
|
+
verify_ssl = True
|
|
191
|
+
self.verify_ssl = verify_ssl
|
|
192
|
+
self._warned_verify_disabled = False
|
|
170
193
|
self.websocket: websockets.ClientConnection | None = None
|
|
171
194
|
self.background_task: asyncio.Task | None = None
|
|
172
195
|
self._send_lock: asyncio.Lock | None = None
|
|
@@ -197,6 +220,25 @@ class HomeAssistantWebSocketClient:
|
|
|
197
220
|
logger.info(f"Connecting to Home Assistant WebSocket: {self.ws_url}")
|
|
198
221
|
self._state.reset_connection()
|
|
199
222
|
|
|
223
|
+
# Only configure an SSLContext for wss://; ws:// (Supervisor
|
|
224
|
+
# proxy) doesn't use TLS and gets ssl=None.
|
|
225
|
+
ssl_ctx: ssl.SSLContext | None = None
|
|
226
|
+
if self.ws_url.startswith("wss://"):
|
|
227
|
+
ssl_ctx = ssl.create_default_context()
|
|
228
|
+
if not self.verify_ssl:
|
|
229
|
+
if not self._warned_verify_disabled:
|
|
230
|
+
# Once per client — pool reconnects/HA restarts
|
|
231
|
+
# otherwise flood logs with the same warning.
|
|
232
|
+
logger.warning(
|
|
233
|
+
"TLS verification disabled for Home Assistant "
|
|
234
|
+
"WebSocket (HA_VERIFY_SSL=false). Connecting to "
|
|
235
|
+
"%s with hostname/cert checks off.",
|
|
236
|
+
self.ws_url,
|
|
237
|
+
)
|
|
238
|
+
self._warned_verify_disabled = True
|
|
239
|
+
ssl_ctx.check_hostname = False
|
|
240
|
+
ssl_ctx.verify_mode = ssl.CERT_NONE
|
|
241
|
+
|
|
200
242
|
# Connect to WebSocket
|
|
201
243
|
# Include Authorization header for Supervisor proxy compatibility
|
|
202
244
|
# (required when connecting via http://supervisor/core/websocket)
|
|
@@ -205,6 +247,7 @@ class HomeAssistantWebSocketClient:
|
|
|
205
247
|
ping_interval=30,
|
|
206
248
|
ping_timeout=10,
|
|
207
249
|
additional_headers={"Authorization": f"Bearer {self.token}"},
|
|
250
|
+
ssl=ssl_ctx,
|
|
208
251
|
# Increase max message size to 20MB for large responses
|
|
209
252
|
# (e.g., HACS repository list can be 2MB+)
|
|
210
253
|
max_size=20 * 1024 * 1024,
|
|
@@ -241,7 +284,16 @@ class HomeAssistantWebSocketClient:
|
|
|
241
284
|
return True
|
|
242
285
|
|
|
243
286
|
except Exception as e:
|
|
244
|
-
|
|
287
|
+
if _is_ssl_error(e) and self.verify_ssl:
|
|
288
|
+
logger.error(
|
|
289
|
+
"WebSocket TLS verification failed for %s: %s. "
|
|
290
|
+
"If this is a self-signed certificate or hostname "
|
|
291
|
+
"mismatch, set HA_VERIFY_SSL=false to skip verification.",
|
|
292
|
+
self.ws_url,
|
|
293
|
+
e,
|
|
294
|
+
)
|
|
295
|
+
else:
|
|
296
|
+
logger.error(f"WebSocket connection failed: {e}")
|
|
245
297
|
await self.disconnect()
|
|
246
298
|
return False
|
|
247
299
|
|
|
@@ -54,6 +54,9 @@ class Settings(BaseSettings):
|
|
|
54
54
|
timeout: int = Field(30, alias="HA_TIMEOUT")
|
|
55
55
|
max_retries: int = Field(3, alias="HA_MAX_RETRIES")
|
|
56
56
|
|
|
57
|
+
# False = skip TLS verification (self-signed / hostname mismatch). Trusted networks only.
|
|
58
|
+
verify_ssl: bool = Field(True, alias="HA_VERIFY_SSL")
|
|
59
|
+
|
|
57
60
|
# Tool configuration
|
|
58
61
|
fuzzy_threshold: int = Field(60, alias="FUZZY_THRESHOLD")
|
|
59
62
|
entity_search_limit: int = Field(20, alias="ENTITY_SEARCH_LIMIT")
|
|
@@ -231,3 +234,13 @@ def get_global_settings() -> Settings:
|
|
|
231
234
|
if _settings is None:
|
|
232
235
|
_settings = get_settings()
|
|
233
236
|
return _settings
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _reset_global_settings() -> None:
|
|
240
|
+
"""Drop the cached settings singleton.
|
|
241
|
+
|
|
242
|
+
Test-only seam so suites that mutate ``HA_*`` env vars can force a
|
|
243
|
+
re-read without reaching into module-private state.
|
|
244
|
+
"""
|
|
245
|
+
global _settings
|
|
246
|
+
_settings = None
|
|
@@ -179,7 +179,9 @@ async def create_backup(
|
|
|
179
179
|
|
|
180
180
|
try:
|
|
181
181
|
# Connect to WebSocket
|
|
182
|
-
ws_client, error = await get_connected_ws_client(
|
|
182
|
+
ws_client, error = await get_connected_ws_client(
|
|
183
|
+
client.base_url, client.token, verify_ssl=client.verify_ssl
|
|
184
|
+
)
|
|
183
185
|
if error:
|
|
184
186
|
raise_tool_error(error or create_error_response(
|
|
185
187
|
ErrorCode.CONNECTION_FAILED,
|
|
@@ -300,7 +302,9 @@ async def restore_backup(
|
|
|
300
302
|
|
|
301
303
|
try:
|
|
302
304
|
# Connect to WebSocket
|
|
303
|
-
ws_client, error = await get_connected_ws_client(
|
|
305
|
+
ws_client, error = await get_connected_ws_client(
|
|
306
|
+
client.base_url, client.token, verify_ssl=client.verify_ssl
|
|
307
|
+
)
|
|
304
308
|
if error:
|
|
305
309
|
raise_tool_error(error or create_error_response(
|
|
306
310
|
ErrorCode.CONNECTION_FAILED,
|
|
@@ -78,7 +78,7 @@ def extract_tool_error_message(te: ToolError) -> str:
|
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
async def get_connected_ws_client(
|
|
81
|
-
base_url: str, token: str
|
|
81
|
+
base_url: str, token: str, verify_ssl: bool | None = None
|
|
82
82
|
) -> tuple[HomeAssistantWebSocketClient | None, dict[str, Any] | None]:
|
|
83
83
|
"""
|
|
84
84
|
Create and connect a WebSocket client.
|
|
@@ -86,11 +86,15 @@ async def get_connected_ws_client(
|
|
|
86
86
|
Args:
|
|
87
87
|
base_url: Home Assistant base URL
|
|
88
88
|
token: Authentication token
|
|
89
|
+
verify_ssl: TLS verification override. Pass ``client.verify_ssl``
|
|
90
|
+
from the calling REST client so a programmatic
|
|
91
|
+
``HomeAssistantClient(verify_ssl=False)`` propagates to the
|
|
92
|
+
WebSocket too. ``None`` falls back to ``settings.verify_ssl``.
|
|
89
93
|
|
|
90
94
|
Returns:
|
|
91
95
|
Tuple of (ws_client, error_dict). If connection fails, ws_client is None.
|
|
92
96
|
"""
|
|
93
|
-
ws_client = HomeAssistantWebSocketClient(base_url, token)
|
|
97
|
+
ws_client = HomeAssistantWebSocketClient(base_url, token, verify_ssl=verify_ssl)
|
|
94
98
|
connected = await ws_client.connect()
|
|
95
99
|
if not connected:
|
|
96
100
|
return None, create_connection_error(
|
|
@@ -232,7 +232,9 @@ async def _supervisor_api_call(
|
|
|
232
232
|
"""
|
|
233
233
|
ws_client = None
|
|
234
234
|
try:
|
|
235
|
-
ws_client, error = await get_connected_ws_client(
|
|
235
|
+
ws_client, error = await get_connected_ws_client(
|
|
236
|
+
client.base_url, client.token, verify_ssl=client.verify_ssl
|
|
237
|
+
)
|
|
236
238
|
if error or ws_client is None:
|
|
237
239
|
return error or create_connection_error(
|
|
238
240
|
"Failed to establish WebSocket connection",
|
|
@@ -290,7 +290,9 @@ class HistoryTools:
|
|
|
290
290
|
|
|
291
291
|
# Connect to WebSocket (shared by both sources)
|
|
292
292
|
ws_client, error = await get_connected_ws_client(
|
|
293
|
-
self._client.base_url,
|
|
293
|
+
self._client.base_url,
|
|
294
|
+
self._client.token,
|
|
295
|
+
verify_ssl=self._client.verify_ssl,
|
|
294
296
|
)
|
|
295
297
|
if error or ws_client is None:
|
|
296
298
|
raise_tool_error(error or create_error_response(
|
|
@@ -415,7 +415,9 @@ class SystemTools:
|
|
|
415
415
|
for subsequent optional fetches.
|
|
416
416
|
"""
|
|
417
417
|
ws_client, error = await get_connected_ws_client(
|
|
418
|
-
self._client.base_url,
|
|
418
|
+
self._client.base_url,
|
|
419
|
+
self._client.token,
|
|
420
|
+
verify_ssl=self._client.verify_ssl,
|
|
419
421
|
)
|
|
420
422
|
if error or ws_client is None:
|
|
421
423
|
raise_tool_error(error or create_error_response(
|
|
@@ -167,7 +167,9 @@ class TraceTools:
|
|
|
167
167
|
|
|
168
168
|
# Connect to WebSocket
|
|
169
169
|
ws_client, error = await get_connected_ws_client(
|
|
170
|
-
self._client.base_url,
|
|
170
|
+
self._client.base_url,
|
|
171
|
+
self._client.token,
|
|
172
|
+
verify_ssl=self._client.verify_ssl,
|
|
171
173
|
)
|
|
172
174
|
if error or ws_client is None:
|
|
173
175
|
raise_tool_error(error or create_error_response(
|
|
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.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/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.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/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
|
{ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/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.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/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.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp/transforms/categorized_search.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.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev421 → ha_mcp_dev-7.4.1.dev423}/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
|