ha-mcp-dev 7.4.1.dev439__tar.gz → 7.4.1.dev440__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.dev440}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/pyproject.toml +1 -1
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/client/rest_client.py +217 -24
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_utility.py +228 -33
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/setup.cfg +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/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.dev440}/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.dev440}/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.dev440}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/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.dev440}/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.dev440}/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.dev440}/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.dev440}/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.dev440}/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.dev440}/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.dev440}/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.dev440}/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.dev440}/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.dev440}/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.dev440}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/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.dev440"
|
|
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
|
|
|
@@ -26,7 +26,24 @@ logger = logging.getLogger(__name__)
|
|
|
26
26
|
|
|
27
27
|
# Fields to keep in compact logbook mode (strips attribute dictionaries
|
|
28
28
|
# and other bulky fields that can cause context exhaustion — see #683)
|
|
29
|
-
COMPACT_LOGBOOK_FIELDS = {
|
|
29
|
+
COMPACT_LOGBOOK_FIELDS = {
|
|
30
|
+
"when",
|
|
31
|
+
"entity_id",
|
|
32
|
+
"state",
|
|
33
|
+
"name",
|
|
34
|
+
"message",
|
|
35
|
+
"domain",
|
|
36
|
+
"context_id",
|
|
37
|
+
"source",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Supervisor-managed system services exposed via /<slug>/logs. Stable set
|
|
42
|
+
# in HA Core; if Supervisor adds e.g. /cli/logs in a future release, extend
|
|
43
|
+
# here. See #1116.
|
|
44
|
+
SYSTEM_SERVICE_SLUGS = frozenset(
|
|
45
|
+
{"supervisor", "host", "core", "dns", "audio", "multicast", "observer"}
|
|
46
|
+
)
|
|
30
47
|
|
|
31
48
|
|
|
32
49
|
def _compact_logbook_entries(entries: list[Any]) -> list[dict[str, Any]]:
|
|
@@ -57,15 +74,24 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
57
74
|
) -> int:
|
|
58
75
|
"""Coerce and validate a limit parameter, raising a structured tool error on failure."""
|
|
59
76
|
try:
|
|
60
|
-
return coerce_int_param(
|
|
77
|
+
return coerce_int_param(
|
|
78
|
+
limit,
|
|
79
|
+
param_name="limit",
|
|
80
|
+
default=default,
|
|
81
|
+
min_value=1,
|
|
82
|
+
max_value=MAX_LIMIT,
|
|
83
|
+
)
|
|
61
84
|
except ValueError as e:
|
|
62
85
|
raise_tool_error(
|
|
63
86
|
create_error_response(
|
|
64
87
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
65
88
|
str(e),
|
|
66
|
-
suggestions=[
|
|
89
|
+
suggestions=[
|
|
90
|
+
f"Provide limit as an integer (e.g., {suggestion_example})"
|
|
91
|
+
],
|
|
67
92
|
)
|
|
68
93
|
)
|
|
94
|
+
|
|
69
95
|
# Regex to match log level at the start of a log line
|
|
70
96
|
_LOG_LEVEL_RE = re.compile(
|
|
71
97
|
r"(?:^|\s)(DEBUG|INFO|WARNING|ERROR|CRITICAL)(?:\s|:|\])", re.IGNORECASE
|
|
@@ -80,11 +106,18 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
80
106
|
"idempotentHint": True,
|
|
81
107
|
"readOnlyHint": True,
|
|
82
108
|
"title": "Get Logs",
|
|
83
|
-
}
|
|
109
|
+
},
|
|
84
110
|
)
|
|
85
111
|
@log_tool_usage
|
|
86
112
|
async def ha_get_logs(
|
|
87
|
-
source: Literal[
|
|
113
|
+
source: Literal[
|
|
114
|
+
"logbook",
|
|
115
|
+
"system",
|
|
116
|
+
"error_log",
|
|
117
|
+
"supervisor",
|
|
118
|
+
"system_service",
|
|
119
|
+
"logger",
|
|
120
|
+
] = "logbook",
|
|
88
121
|
# Shared parameters
|
|
89
122
|
limit: int | str | None = None,
|
|
90
123
|
search: str | None = None,
|
|
@@ -96,7 +129,7 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
96
129
|
compact: bool | str = True,
|
|
97
130
|
# System/error_log-specific
|
|
98
131
|
level: str | None = None,
|
|
99
|
-
# Supervisor-specific
|
|
132
|
+
# Supervisor + system_service-specific (different namespaces — see below)
|
|
100
133
|
slug: str | None = None,
|
|
101
134
|
) -> dict[str, Any]:
|
|
102
135
|
"""
|
|
@@ -106,13 +139,19 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
106
139
|
- "logbook" (default): Entity state change history with pagination
|
|
107
140
|
- "system": Structured system log entries (errors, warnings) via system_log/list
|
|
108
141
|
- "error_log": Raw home-assistant.log text
|
|
109
|
-
- "supervisor": Add-on container logs (requires slug
|
|
142
|
+
- "supervisor": Add-on container logs (requires slug = add-on slug)
|
|
143
|
+
- "system_service": HA-Supervisor-managed system service logs (requires
|
|
144
|
+
slug ∈ {supervisor, host, core, dns, audio, multicast, observer})
|
|
110
145
|
- "logger": Effective log level per integration via logger/log_info (confirms logger.set_level changes took effect)
|
|
111
146
|
|
|
112
147
|
**Shared params:** limit, search (keyword filter on entries/lines; matches integration domain for source='logger')
|
|
113
148
|
**Logbook params:** hours_back, entity_id, end_time, offset, compact (default True — strips attribute dicts to save context)
|
|
114
149
|
**System/error_log params:** level (ERROR, WARNING, INFO, DEBUG)
|
|
115
|
-
**Supervisor params:** slug
|
|
150
|
+
**Supervisor params:** slug = add-on slug, e.g. "core_mosquitto" (use
|
|
151
|
+
ha_get_addon() to list installed slugs)
|
|
152
|
+
**System-service params:** slug = service name. The slug "supervisor"
|
|
153
|
+
here means the Supervisor service's own logs, NOT an add-on with
|
|
154
|
+
that name — the source param disambiguates.
|
|
116
155
|
"""
|
|
117
156
|
|
|
118
157
|
# Validate level if provided
|
|
@@ -131,16 +170,28 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
131
170
|
# Collect warnings about source-incompatible parameters
|
|
132
171
|
warnings: list[str] = []
|
|
133
172
|
if source != "logbook" and any(p is not None for p in [entity_id, end_time]):
|
|
134
|
-
ignored = [
|
|
173
|
+
ignored = [
|
|
174
|
+
p
|
|
175
|
+
for p, v in [("entity_id", entity_id), ("end_time", end_time)]
|
|
176
|
+
if v is not None
|
|
177
|
+
]
|
|
135
178
|
warnings.append(
|
|
136
179
|
f"Parameters {', '.join(ignored)} only apply to source='logbook'; "
|
|
137
180
|
f"ignored for source='{source}'"
|
|
138
181
|
)
|
|
139
|
-
if
|
|
182
|
+
if (
|
|
183
|
+
source in ("logbook", "logger", "supervisor", "system_service")
|
|
184
|
+
and level is not None
|
|
185
|
+
):
|
|
140
186
|
warnings.append(
|
|
141
187
|
"Parameter 'level' only applies to source='system' or 'error_log'; "
|
|
142
188
|
f"ignored for source='{source}'"
|
|
143
189
|
)
|
|
190
|
+
if source not in ("supervisor", "system_service") and slug is not None:
|
|
191
|
+
warnings.append(
|
|
192
|
+
"Parameter 'slug' only applies to source='supervisor' or "
|
|
193
|
+
f"'system_service'; ignored for source='{source}'"
|
|
194
|
+
)
|
|
144
195
|
|
|
145
196
|
# --- source="logbook" ---
|
|
146
197
|
if source == "logbook":
|
|
@@ -186,6 +237,41 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
186
237
|
result["warnings"] = warnings
|
|
187
238
|
return result
|
|
188
239
|
|
|
240
|
+
# --- source="system_service" ---
|
|
241
|
+
if source == "system_service":
|
|
242
|
+
if not slug:
|
|
243
|
+
raise_tool_error(
|
|
244
|
+
create_error_response(
|
|
245
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
246
|
+
"The 'slug' parameter is required for source='system_service'",
|
|
247
|
+
suggestions=[
|
|
248
|
+
"Provide a service name, e.g. slug='supervisor' "
|
|
249
|
+
f"(allowed: {', '.join(sorted(SYSTEM_SERVICE_SLUGS))})",
|
|
250
|
+
],
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
if slug not in SYSTEM_SERVICE_SLUGS:
|
|
254
|
+
raise_tool_error(
|
|
255
|
+
create_error_response(
|
|
256
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
257
|
+
f"Invalid system_service slug '{slug}'. Must be one of: "
|
|
258
|
+
f"{', '.join(sorted(SYSTEM_SERVICE_SLUGS))}",
|
|
259
|
+
suggestions=[
|
|
260
|
+
"Pick a valid service name (e.g. 'supervisor', 'host')",
|
|
261
|
+
"For add-on container logs use source='supervisor' with "
|
|
262
|
+
"the add-on slug instead",
|
|
263
|
+
],
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
result = await _get_system_service_log(
|
|
267
|
+
service=slug,
|
|
268
|
+
limit=limit,
|
|
269
|
+
search=search,
|
|
270
|
+
)
|
|
271
|
+
if warnings:
|
|
272
|
+
result["warnings"] = warnings
|
|
273
|
+
return result
|
|
274
|
+
|
|
189
275
|
# --- source="supervisor" ---
|
|
190
276
|
# source == "supervisor" (Literal type guarantees this)
|
|
191
277
|
if not slug:
|
|
@@ -462,7 +548,9 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
462
548
|
level: str | None = None,
|
|
463
549
|
) -> dict[str, Any]:
|
|
464
550
|
"""Fetch raw error log text from home-assistant.log."""
|
|
465
|
-
effective_limit = _coerce_limit(
|
|
551
|
+
effective_limit = _coerce_limit(
|
|
552
|
+
limit, default=DEFAULT_LOG_LIMIT, suggestion_example="100"
|
|
553
|
+
)
|
|
466
554
|
|
|
467
555
|
try:
|
|
468
556
|
raw_log = await client.get_error_log()
|
|
@@ -572,7 +660,8 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
572
660
|
if search:
|
|
573
661
|
search_lower = search.lower()
|
|
574
662
|
loggers = [
|
|
575
|
-
entry
|
|
663
|
+
entry
|
|
664
|
+
for entry in loggers
|
|
576
665
|
if search_lower in entry["domain"].lower()
|
|
577
666
|
]
|
|
578
667
|
filters_applied["search"] = search
|
|
@@ -619,14 +708,18 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
619
708
|
limit: int | str | None = None,
|
|
620
709
|
search: str | None = None,
|
|
621
710
|
) -> dict[str, Any]:
|
|
622
|
-
"""Fetch add-on container logs
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
711
|
+
"""Fetch add-on container logs.
|
|
712
|
+
|
|
713
|
+
Delegates to ``HomeAssistantClient.get_addon_logs`` which branches on
|
|
714
|
+
``is_running_in_addon()``: inside the add-on container hits Supervisor
|
|
715
|
+
directly at ``http://supervisor/addons/<slug>/logs`` (the HA-Core
|
|
716
|
+
proxy at ``/api/hassio/addons/<slug>/logs`` rejects the Supervisor
|
|
717
|
+
token there — see #1116); on non-addon installs falls back to the
|
|
718
|
+
HA-Core proxy. Both paths return ``text/plain``.
|
|
628
719
|
"""
|
|
629
|
-
effective_limit = _coerce_limit(
|
|
720
|
+
effective_limit = _coerce_limit(
|
|
721
|
+
limit, default=DEFAULT_LOG_LIMIT, suggestion_example="100"
|
|
722
|
+
)
|
|
630
723
|
|
|
631
724
|
try:
|
|
632
725
|
log_text = await client.get_addon_logs(slug)
|
|
@@ -713,13 +806,111 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
713
806
|
],
|
|
714
807
|
)
|
|
715
808
|
|
|
809
|
+
# ---- System-service log source ----
|
|
810
|
+
|
|
811
|
+
async def _get_system_service_log(
|
|
812
|
+
service: str,
|
|
813
|
+
limit: int | str | None = None,
|
|
814
|
+
search: str | None = None,
|
|
815
|
+
) -> dict[str, Any]:
|
|
816
|
+
"""Fetch HA system-service logs from Supervisor's per-service endpoint.
|
|
817
|
+
|
|
818
|
+
``service`` ∈ {supervisor, host, core, dns, audio, multicast, observer}.
|
|
819
|
+
Caller (``ha_get_logs(source='system_service')``) validates against
|
|
820
|
+
``SYSTEM_SERVICE_SLUGS`` before dispatch. Hits
|
|
821
|
+
``http://supervisor/<service>/logs`` directly via
|
|
822
|
+
``HomeAssistantClient._get_system_service_logs`` — same direct-Supervisor
|
|
823
|
+
path #1116's add-on fix uses, just with a different URL prefix.
|
|
824
|
+
Requires ``hassio_role: manager`` in the addon manifest.
|
|
825
|
+
"""
|
|
826
|
+
effective_limit = _coerce_limit(
|
|
827
|
+
limit, default=DEFAULT_LOG_LIMIT, suggestion_example="100"
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
try:
|
|
831
|
+
log_text = await client._get_system_service_logs(service)
|
|
832
|
+
|
|
833
|
+
lines = log_text.splitlines() if log_text else []
|
|
834
|
+
|
|
835
|
+
filters_applied: dict[str, str] = {}
|
|
836
|
+
if search:
|
|
837
|
+
search_lower = search.lower()
|
|
838
|
+
lines = [ln for ln in lines if search_lower in ln.lower()]
|
|
839
|
+
filters_applied["search"] = search
|
|
840
|
+
|
|
841
|
+
total_lines = len(lines)
|
|
842
|
+
lines = lines[-effective_limit:]
|
|
843
|
+
|
|
844
|
+
data: dict[str, Any] = {
|
|
845
|
+
"success": True,
|
|
846
|
+
"source": "system_service",
|
|
847
|
+
"slug": service,
|
|
848
|
+
"log": "\n".join(lines),
|
|
849
|
+
"total_lines": total_lines,
|
|
850
|
+
"returned_lines": len(lines),
|
|
851
|
+
"limit": effective_limit,
|
|
852
|
+
}
|
|
853
|
+
if filters_applied:
|
|
854
|
+
data["filters_applied"] = filters_applied
|
|
855
|
+
|
|
856
|
+
return data
|
|
857
|
+
|
|
858
|
+
except ToolError:
|
|
859
|
+
raise
|
|
860
|
+
except HomeAssistantAPIError as e:
|
|
861
|
+
status = getattr(e, "status_code", None)
|
|
862
|
+
if status == 403:
|
|
863
|
+
# Same role-too-low cause as the addon-logs branch.
|
|
864
|
+
exception_to_structured_error(
|
|
865
|
+
e,
|
|
866
|
+
context={"source": "system_service", "slug": service},
|
|
867
|
+
suggestions=[
|
|
868
|
+
"Addon's hassio_role must be 'manager' or higher to "
|
|
869
|
+
"read /<service>/logs",
|
|
870
|
+
"Verify the addon was reinstalled after the role bump "
|
|
871
|
+
"took effect",
|
|
872
|
+
],
|
|
873
|
+
)
|
|
874
|
+
if status == 404:
|
|
875
|
+
exception_to_structured_error(
|
|
876
|
+
e,
|
|
877
|
+
context={"source": "system_service", "slug": service},
|
|
878
|
+
suggestions=[
|
|
879
|
+
f"Service '{service}' not found at "
|
|
880
|
+
f"http://supervisor/{service}/logs — Supervisor may "
|
|
881
|
+
"not expose it on this HA OS version",
|
|
882
|
+
f"Allowed services: {', '.join(sorted(SYSTEM_SERVICE_SLUGS))}",
|
|
883
|
+
],
|
|
884
|
+
)
|
|
885
|
+
exception_to_structured_error(
|
|
886
|
+
e,
|
|
887
|
+
context={"source": "system_service", "slug": service},
|
|
888
|
+
suggestions=[
|
|
889
|
+
f"Supervisor returned an error for /{service}/logs",
|
|
890
|
+
"Ensure Supervisor is available (HA OS or Supervised install)",
|
|
891
|
+
],
|
|
892
|
+
)
|
|
893
|
+
except (
|
|
894
|
+
HomeAssistantConnectionError,
|
|
895
|
+
TimeoutError,
|
|
896
|
+
OSError,
|
|
897
|
+
) as e:
|
|
898
|
+
exception_to_structured_error(
|
|
899
|
+
e,
|
|
900
|
+
context={"source": "system_service", "slug": service},
|
|
901
|
+
suggestions=[
|
|
902
|
+
"Check Home Assistant connection",
|
|
903
|
+
"Ensure Supervisor is available (HA OS or Supervised install)",
|
|
904
|
+
],
|
|
905
|
+
)
|
|
906
|
+
|
|
716
907
|
@mcp.tool(
|
|
717
908
|
tags={"Utilities"},
|
|
718
909
|
annotations={
|
|
719
910
|
"idempotentHint": True,
|
|
720
911
|
"readOnlyHint": True,
|
|
721
|
-
"title": "Evaluate Template"
|
|
722
|
-
}
|
|
912
|
+
"title": "Evaluate Template",
|
|
913
|
+
},
|
|
723
914
|
)
|
|
724
915
|
@log_tool_usage
|
|
725
916
|
async def ha_eval_template(
|
|
@@ -901,18 +1092,22 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
901
1092
|
}
|
|
902
1093
|
else:
|
|
903
1094
|
error_info = result.get("error", "Unknown error occurred")
|
|
904
|
-
raise_tool_error(
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
"
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1095
|
+
raise_tool_error(
|
|
1096
|
+
create_error_response(
|
|
1097
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
1098
|
+
str(error_info)
|
|
1099
|
+
if not isinstance(error_info, str)
|
|
1100
|
+
else error_info,
|
|
1101
|
+
context={"template": template, "request_id": request_id},
|
|
1102
|
+
suggestions=[
|
|
1103
|
+
"Check template syntax - ensure proper Jinja2 formatting",
|
|
1104
|
+
"Verify entity_ids exist using ha_get_state()",
|
|
1105
|
+
"Use default values: {{ states('sensor.temp') | float(0) }}",
|
|
1106
|
+
"Check for typos in function names and entity references",
|
|
1107
|
+
"Test simpler templates first to isolate issues",
|
|
1108
|
+
],
|
|
1109
|
+
)
|
|
1110
|
+
)
|
|
916
1111
|
|
|
917
1112
|
except ToolError:
|
|
918
1113
|
raise
|
|
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.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/resources/skills-vendor/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/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
|
{ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/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
|
{ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/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.dev439 → ha_mcp_dev-7.4.1.dev440}/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
|
{ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/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.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev440}/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
|