ha-mcp-dev 7.4.1.dev439__tar.gz → 7.4.1.dev441__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.dev439/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev441}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/pyproject.toml +1 -1
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/client/rest_client.py +217 -24
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_entities.py +277 -74
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_utility.py +228 -33
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/setup.cfg +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/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.dev441"
|
|
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,11 +5,13 @@ Home Assistant HTTP client with authentication and error handling.
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
|
+
import os
|
|
8
9
|
import ssl
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
11
12
|
import httpx
|
|
12
13
|
|
|
14
|
+
from .._version import is_running_in_addon
|
|
13
15
|
from ..config import get_global_settings
|
|
14
16
|
|
|
15
17
|
|
|
@@ -26,6 +28,7 @@ def _is_ssl_error(exc: BaseException) -> bool:
|
|
|
26
28
|
cur = cur.__cause__ or cur.__context__
|
|
27
29
|
return False
|
|
28
30
|
|
|
31
|
+
|
|
29
32
|
logger = logging.getLogger(__name__)
|
|
30
33
|
|
|
31
34
|
|
|
@@ -33,17 +36,14 @@ class HomeAssistantError(Exception):
|
|
|
33
36
|
"""Base exception for Home Assistant API errors."""
|
|
34
37
|
|
|
35
38
|
|
|
36
|
-
|
|
37
39
|
class HomeAssistantConnectionError(HomeAssistantError):
|
|
38
40
|
"""Connection error to Home Assistant."""
|
|
39
41
|
|
|
40
42
|
|
|
41
|
-
|
|
42
43
|
class HomeAssistantAuthError(HomeAssistantError):
|
|
43
44
|
"""Authentication error with Home Assistant."""
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
|
|
47
47
|
class HomeAssistantAPIError(HomeAssistantError):
|
|
48
48
|
"""API error from Home Assistant."""
|
|
49
49
|
|
|
@@ -125,7 +125,7 @@ class HomeAssistantClient:
|
|
|
125
125
|
|
|
126
126
|
logger.info(f"Initialized Home Assistant client for {self.base_url}")
|
|
127
127
|
|
|
128
|
-
async def __aenter__(self) ->
|
|
128
|
+
async def __aenter__(self) -> "HomeAssistantClient":
|
|
129
129
|
"""Async context manager entry."""
|
|
130
130
|
return self
|
|
131
131
|
|
|
@@ -192,7 +192,9 @@ class HomeAssistantClient:
|
|
|
192
192
|
except httpx.HTTPError as e:
|
|
193
193
|
raise HomeAssistantConnectionError(f"HTTP error: {e}") from e
|
|
194
194
|
|
|
195
|
-
async def _request(
|
|
195
|
+
async def _request(
|
|
196
|
+
self, method: str, endpoint: str, **kwargs: Any
|
|
197
|
+
) -> dict[str, Any]:
|
|
196
198
|
"""
|
|
197
199
|
Make authenticated request to Home Assistant API and parse JSON body.
|
|
198
200
|
|
|
@@ -267,8 +269,11 @@ class HomeAssistantClient:
|
|
|
267
269
|
return await self._request("POST", f"/states/{entity_id}", json=payload)
|
|
268
270
|
|
|
269
271
|
async def call_service(
|
|
270
|
-
self,
|
|
271
|
-
|
|
272
|
+
self,
|
|
273
|
+
domain: str,
|
|
274
|
+
service: str,
|
|
275
|
+
data: dict[str, Any] | None = None,
|
|
276
|
+
return_response: bool = False,
|
|
272
277
|
) -> list[dict[str, Any]] | dict[str, Any]:
|
|
273
278
|
"""
|
|
274
279
|
Call Home Assistant service.
|
|
@@ -284,7 +289,9 @@ class HomeAssistantClient:
|
|
|
284
289
|
Service response data - list of affected states normally, or dict with
|
|
285
290
|
service response if return_response=True
|
|
286
291
|
"""
|
|
287
|
-
logger.debug(
|
|
292
|
+
logger.debug(
|
|
293
|
+
f"Calling service {domain}.{service} (return_response={return_response})"
|
|
294
|
+
)
|
|
288
295
|
|
|
289
296
|
payload = data or {}
|
|
290
297
|
|
|
@@ -294,7 +301,10 @@ class HomeAssistantClient:
|
|
|
294
301
|
params["return_response"] = "true"
|
|
295
302
|
|
|
296
303
|
result = await self._request(
|
|
297
|
-
"POST",
|
|
304
|
+
"POST",
|
|
305
|
+
f"/services/{domain}/{service}",
|
|
306
|
+
json=payload,
|
|
307
|
+
params=params if params else None,
|
|
298
308
|
)
|
|
299
309
|
|
|
300
310
|
# When return_response is True, HA returns a dict with service_response key
|
|
@@ -366,7 +376,9 @@ class HomeAssistantClient:
|
|
|
366
376
|
Returns:
|
|
367
377
|
Logbook entries
|
|
368
378
|
"""
|
|
369
|
-
logger.debug(
|
|
379
|
+
logger.debug(
|
|
380
|
+
f"Fetching logbook entries for entity: {entity_id}, start: {start_time}, end: {end_time}"
|
|
381
|
+
)
|
|
370
382
|
|
|
371
383
|
# Build endpoint - start_time goes in URL path if provided
|
|
372
384
|
if start_time:
|
|
@@ -428,27 +440,66 @@ class HomeAssistantClient:
|
|
|
428
440
|
return await self._request("POST", "/config/core/check_config")
|
|
429
441
|
|
|
430
442
|
async def get_error_log(self) -> str:
|
|
431
|
-
"""Get Home Assistant error log.
|
|
432
|
-
|
|
443
|
+
"""Get Home Assistant error log.
|
|
444
|
+
|
|
445
|
+
Branch on ``is_running_in_addon()``: inside the add-on container,
|
|
446
|
+
HA Core's ``bootstrap.py`` sets ``err_log_path = None`` when the
|
|
447
|
+
``SUPERVISOR`` env var is present, so ``hass.data[DATA_LOGGING]``
|
|
448
|
+
is never populated and the ``APIErrorLog`` view is not registered
|
|
449
|
+
— ``/api/error_log`` returns 404 by-design on HA OS / Supervised.
|
|
450
|
+
Route to ``_supervisor_logs_get("core")`` on this branch: same
|
|
451
|
+
content (HA Core's container log) via a different transport
|
|
452
|
+
(Supervisor REST). On non-addon installs keep the
|
|
453
|
+
``/api/error_log`` proxy path.
|
|
454
|
+
|
|
455
|
+
Same root cause and fix shape as ``get_addon_logs`` — see #1116.
|
|
456
|
+
|
|
457
|
+
Raises:
|
|
458
|
+
HomeAssistantAuthError: 401, or empty ``SUPERVISOR_TOKEN`` on
|
|
459
|
+
the addon branch.
|
|
460
|
+
HomeAssistantAPIError: 403 (role too low — addon needs
|
|
461
|
+
``hassio_role: manager``), 404, other non-2xx.
|
|
462
|
+
HomeAssistantConnectionError: Network, timeout, or transport
|
|
463
|
+
error.
|
|
464
|
+
"""
|
|
465
|
+
if is_running_in_addon():
|
|
466
|
+
logger.debug("Fetching error log via Supervisor direct (core service)")
|
|
467
|
+
return await self._supervisor_logs_get("core")
|
|
468
|
+
|
|
469
|
+
logger.debug("Fetching error log via HA Core proxy")
|
|
433
470
|
response = await self._request("GET", "/error_log")
|
|
434
471
|
return response if isinstance(response, str) else str(response)
|
|
435
472
|
|
|
436
473
|
async def get_addon_logs(self, slug: str) -> str:
|
|
437
|
-
"""Fetch an add-on's container logs
|
|
474
|
+
"""Fetch an add-on's container logs.
|
|
475
|
+
|
|
476
|
+
Branch on ``is_running_in_addon()`` (which keys off ``SUPERVISOR_TOKEN``
|
|
477
|
+
in env): inside the add-on container goes directly to the Supervisor
|
|
478
|
+
REST API at ``http://supervisor/addons/{slug}/logs`` with the
|
|
479
|
+
Supervisor token. The HA Core proxy at
|
|
480
|
+
``/api/hassio/addons/{slug}/logs`` rejects this token+path combination
|
|
481
|
+
on current HA Core releases (see #1116) — the direct path bypasses
|
|
482
|
+
HA Core entirely and is the documented Supervisor contract.
|
|
438
483
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
484
|
+
On non-addon installs (Docker, pyinstaller, pip pointing at a normal
|
|
485
|
+
HA URL), falls back to the HA Core proxy path. That path requires an
|
|
486
|
+
admin LLA but works fine when not invoked from the add-on container.
|
|
487
|
+
|
|
488
|
+
Both branches return ``text/plain`` log content.
|
|
443
489
|
|
|
444
490
|
Raises:
|
|
445
|
-
HomeAssistantAuthError: 401
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
491
|
+
HomeAssistantAuthError: 401 response, or ``SUPERVISOR_TOKEN`` empty
|
|
492
|
+
at call time on the addon branch.
|
|
493
|
+
HomeAssistantAPIError: 403 (role too low — addon needs hassio_role
|
|
494
|
+
``manager``), 404 (unknown slug), or other non-2xx. The
|
|
495
|
+
``status_code`` attribute lets callers map to specific
|
|
496
|
+
suggestions.
|
|
449
497
|
HomeAssistantConnectionError: Network, timeout, or transport error.
|
|
450
498
|
"""
|
|
451
|
-
|
|
499
|
+
if is_running_in_addon():
|
|
500
|
+
return await self._get_addon_logs_via_supervisor(slug)
|
|
501
|
+
|
|
502
|
+
logger.debug(f"Fetching addon logs for slug={slug} via HA Core proxy")
|
|
452
503
|
response = await self._raw_request(
|
|
453
504
|
"GET",
|
|
454
505
|
f"/hassio/addons/{slug}/logs",
|
|
@@ -456,6 +507,146 @@ class HomeAssistantClient:
|
|
|
456
507
|
)
|
|
457
508
|
return response.text
|
|
458
509
|
|
|
510
|
+
async def _supervisor_logs_get(self, path: str) -> str:
|
|
511
|
+
"""Fetch ``text/plain`` logs from a Supervisor REST endpoint.
|
|
512
|
+
|
|
513
|
+
``path`` is everything between ``http://supervisor/`` and ``/logs``:
|
|
514
|
+
|
|
515
|
+
- ``"addons/<slug>"`` for add-on container logs
|
|
516
|
+
- ``"<service>"`` (where service ∈ {supervisor, host, core, dns, audio,
|
|
517
|
+
multicast, observer}) for system-service logs
|
|
518
|
+
|
|
519
|
+
Bypasses ``HomeAssistantClient.httpx_client`` because the Supervisor
|
|
520
|
+
endpoint uses a different base URL (``http://supervisor``) and a
|
|
521
|
+
different token (``SUPERVISOR_TOKEN``) than HA Core REST. Both
|
|
522
|
+
endpoints require the addon's ``hassio_role`` to be ``manager`` (not
|
|
523
|
+
``default``); a ``default`` role gets a 403 here — see #1116.
|
|
524
|
+
|
|
525
|
+
Raises:
|
|
526
|
+
HomeAssistantAuthError: ``SUPERVISOR_TOKEN`` absent at call time,
|
|
527
|
+
or 401 from Supervisor.
|
|
528
|
+
HomeAssistantAPIError: 403 (role too low — distinct branch with
|
|
529
|
+
role hint), 404, other 4xx/5xx. Tries to parse Supervisor's
|
|
530
|
+
``{"result":"error","message":"..."}`` JSON envelope before
|
|
531
|
+
falling back to text body / reason phrase / placeholder.
|
|
532
|
+
HomeAssistantConnectionError: Timeout or transport error, with
|
|
533
|
+
distinct messages so callers can tell them apart.
|
|
534
|
+
"""
|
|
535
|
+
token = os.environ.get("SUPERVISOR_TOKEN", "")
|
|
536
|
+
if not token:
|
|
537
|
+
# The is_running_in_addon() gate already keys off SUPERVISOR_TOKEN
|
|
538
|
+
# being truthy, so a direct caller landing here without one is a
|
|
539
|
+
# detection/config mismatch — fail-fast with a distinct message
|
|
540
|
+
# so operators don't read it as "token rejected".
|
|
541
|
+
raise HomeAssistantAuthError(
|
|
542
|
+
f"Supervisor token absent at call time for /{path}/logs "
|
|
543
|
+
"(addon-mode gate fired but SUPERVISOR_TOKEN env var not set)"
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
url = f"http://supervisor/{path}/logs"
|
|
547
|
+
logger.debug("Fetching %s via Supervisor direct", url)
|
|
548
|
+
|
|
549
|
+
try:
|
|
550
|
+
async with httpx.AsyncClient(
|
|
551
|
+
timeout=httpx.Timeout(self.timeout),
|
|
552
|
+
# `verify` is a no-op for plain http://supervisor, but kept
|
|
553
|
+
# for symmetry with the other two direct-Supervisor httpx
|
|
554
|
+
# clients (#1128 establishes the 3-site convention).
|
|
555
|
+
verify=self.verify_ssl,
|
|
556
|
+
) as client:
|
|
557
|
+
response = await client.get(
|
|
558
|
+
url,
|
|
559
|
+
headers={
|
|
560
|
+
"Authorization": f"Bearer {token}",
|
|
561
|
+
"Accept": "text/plain",
|
|
562
|
+
},
|
|
563
|
+
)
|
|
564
|
+
except httpx.TimeoutException as e:
|
|
565
|
+
raise HomeAssistantConnectionError(
|
|
566
|
+
f"Timeout fetching /{path}/logs from Supervisor: {e}"
|
|
567
|
+
) from e
|
|
568
|
+
except httpx.HTTPError as e:
|
|
569
|
+
raise HomeAssistantConnectionError(
|
|
570
|
+
f"Transport error fetching /{path}/logs from Supervisor: {e}"
|
|
571
|
+
) from e
|
|
572
|
+
|
|
573
|
+
if response.status_code == 401:
|
|
574
|
+
raise HomeAssistantAuthError(f"Invalid Supervisor token for /{path}/logs")
|
|
575
|
+
if response.status_code == 403:
|
|
576
|
+
# Distinct from 401: token is valid but addon's hassio_role isn't
|
|
577
|
+
# high enough. Most-likely cause for this exact endpoint at the
|
|
578
|
+
# time #1116 surfaced (default → manager bump in addon config.yaml
|
|
579
|
+
# is the same-PR companion fix).
|
|
580
|
+
logger.warning(
|
|
581
|
+
"Supervisor returned 403 for /%s/logs — addon hassio_role may "
|
|
582
|
+
"be too low (need 'manager')",
|
|
583
|
+
path,
|
|
584
|
+
)
|
|
585
|
+
raise HomeAssistantAPIError(
|
|
586
|
+
f"Supervisor forbids /{path}/logs (403) — addon's hassio_role "
|
|
587
|
+
"may be 'default'; need 'manager' or higher",
|
|
588
|
+
status_code=403,
|
|
589
|
+
response_data={"path": path},
|
|
590
|
+
)
|
|
591
|
+
if response.status_code >= 400:
|
|
592
|
+
text_body = response.text
|
|
593
|
+
# Supervisor returns {"result":"error","message":"..."} JSON on
|
|
594
|
+
# some 4xx paths. Try parsing that first so the user sees the
|
|
595
|
+
# human message instead of a JSON blob; then fall back to the
|
|
596
|
+
# text body, then reason_phrase, then a placeholder.
|
|
597
|
+
message = ""
|
|
598
|
+
try:
|
|
599
|
+
envelope = json.loads(text_body) if text_body else None
|
|
600
|
+
if isinstance(envelope, dict):
|
|
601
|
+
msg = envelope.get("message")
|
|
602
|
+
if isinstance(msg, str) and msg:
|
|
603
|
+
message = msg
|
|
604
|
+
except json.JSONDecodeError:
|
|
605
|
+
pass
|
|
606
|
+
if not message:
|
|
607
|
+
message = text_body.strip() or response.reason_phrase or "<empty body>"
|
|
608
|
+
logger.warning(
|
|
609
|
+
"Supervisor returned %s for /%s/logs: %s",
|
|
610
|
+
response.status_code,
|
|
611
|
+
path,
|
|
612
|
+
message,
|
|
613
|
+
)
|
|
614
|
+
raise HomeAssistantAPIError(
|
|
615
|
+
f"API error: {response.status_code} - {message}",
|
|
616
|
+
status_code=response.status_code,
|
|
617
|
+
response_data={"message": text_body, "path": path},
|
|
618
|
+
)
|
|
619
|
+
return response.text
|
|
620
|
+
|
|
621
|
+
async def _get_addon_logs_via_supervisor(self, slug: str) -> str:
|
|
622
|
+
"""Fetch add-on container logs directly from Supervisor's REST API.
|
|
623
|
+
|
|
624
|
+
Distinct from ``tools_bug_report._fetch_addon_logs``: that helper is
|
|
625
|
+
hardcoded to ``/addons/self/logs`` and silently swallows failures
|
|
626
|
+
(it's an aux-data fetch for bug reports, fine to skip on error). This
|
|
627
|
+
helper takes arbitrary slugs and surfaces failures as exceptions
|
|
628
|
+
because callers (``ha_get_logs(source="supervisor", slug=...)``) need
|
|
629
|
+
them. Both endpoints require ``hassio_role: manager``.
|
|
630
|
+
|
|
631
|
+
Delegates to ``_supervisor_logs_get`` so error handling stays in
|
|
632
|
+
lockstep with ``_get_system_service_logs``.
|
|
633
|
+
"""
|
|
634
|
+
return await self._supervisor_logs_get(f"addons/{slug}")
|
|
635
|
+
|
|
636
|
+
async def _get_system_service_logs(self, service: str) -> str:
|
|
637
|
+
"""Fetch HA system-service logs directly from Supervisor's REST API.
|
|
638
|
+
|
|
639
|
+
Hits ``http://supervisor/{service}/logs``. ``service`` must be one of
|
|
640
|
+
the seven Supervisor-managed services: ``supervisor``, ``host``,
|
|
641
|
+
``core``, ``dns``, ``audio``, ``multicast``, ``observer``. Caller is
|
|
642
|
+
responsible for validating ``service`` against the allowed set; this
|
|
643
|
+
helper does no validation and will raise ``HomeAssistantAPIError`` on
|
|
644
|
+
any unknown path (404 from Supervisor).
|
|
645
|
+
|
|
646
|
+
Requires ``hassio_role: manager`` like the addon-logs path.
|
|
647
|
+
"""
|
|
648
|
+
return await self._supervisor_logs_get(service)
|
|
649
|
+
|
|
459
650
|
async def test_connection(self) -> tuple[bool, str | None]:
|
|
460
651
|
"""
|
|
461
652
|
Test connection to Home Assistant.
|
|
@@ -920,7 +1111,9 @@ class HomeAssistantClient:
|
|
|
920
1111
|
await asyncio.sleep(retry_delay)
|
|
921
1112
|
continue
|
|
922
1113
|
else:
|
|
923
|
-
logger.error(
|
|
1114
|
+
logger.error(
|
|
1115
|
+
f"WebSocket 403 error after {max_retries} attempts: {error_str}"
|
|
1116
|
+
)
|
|
924
1117
|
return {
|
|
925
1118
|
"success": False,
|
|
926
1119
|
"error": f"WebSocket request blocked (403 Forbidden): {error_str}",
|
|
@@ -1018,7 +1211,7 @@ class HomeAssistantClient:
|
|
|
1018
1211
|
except Exception:
|
|
1019
1212
|
logger.debug(
|
|
1020
1213
|
f"Entity registry lookup failed for {entity_id}, using bare id: {bare_id}",
|
|
1021
|
-
exc_info=True
|
|
1214
|
+
exc_info=True, # Log full traceback for better debugging
|
|
1022
1215
|
)
|
|
1023
1216
|
return bare_id
|
|
1024
1217
|
|