ha-mcp-dev 7.4.1.dev470__tar.gz → 7.4.1.dev472__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.dev470/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev472}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/pyproject.toml +1 -1
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/client/rest_client.py +10 -11
- ha_mcp_dev-7.4.1.dev472/src/ha_mcp/client/supervisor_client.py +88 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/server.py +13 -6
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/settings_ui.py +5 -8
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/smart_search.py +192 -32
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_bug_report.py +4 -8
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_search.py +386 -152
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/util_helpers.py +47 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/utils/fuzzy_search.py +162 -19
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/setup.cfg +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.4.1.dev470 → ha_mcp_dev-7.4.1.dev472}/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.dev472"
|
|
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"
|
|
@@ -13,6 +13,7 @@ import httpx
|
|
|
13
13
|
|
|
14
14
|
from .._version import get_supervisor_base_url, is_running_in_addon
|
|
15
15
|
from ..config import get_global_settings
|
|
16
|
+
from .supervisor_client import make_supervisor_httpx_client
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def _is_ssl_error(exc: BaseException) -> bool:
|
|
@@ -555,23 +556,21 @@ class HomeAssistantClient:
|
|
|
555
556
|
"(addon-mode gate fired but SUPERVISOR_TOKEN env var not set)"
|
|
556
557
|
)
|
|
557
558
|
|
|
558
|
-
|
|
559
|
-
logger.debug(
|
|
559
|
+
relative_path = f"/{path}/logs"
|
|
560
|
+
logger.debug(
|
|
561
|
+
"Fetching %s%s via Supervisor direct",
|
|
562
|
+
get_supervisor_base_url(),
|
|
563
|
+
relative_path,
|
|
564
|
+
)
|
|
560
565
|
|
|
561
566
|
try:
|
|
562
|
-
async with
|
|
567
|
+
async with make_supervisor_httpx_client(
|
|
563
568
|
timeout=httpx.Timeout(self.timeout),
|
|
564
|
-
# `verify` is a no-op for plain http://supervisor, but kept
|
|
565
|
-
# for symmetry with the other two direct-Supervisor httpx
|
|
566
|
-
# clients (#1128 establishes the 3-site convention).
|
|
567
569
|
verify=self.verify_ssl,
|
|
568
570
|
) as client:
|
|
569
571
|
response = await client.get(
|
|
570
|
-
|
|
571
|
-
headers={
|
|
572
|
-
"Authorization": f"Bearer {token}",
|
|
573
|
-
"Accept": "text/plain",
|
|
574
|
-
},
|
|
572
|
+
relative_path,
|
|
573
|
+
headers={"Accept": "text/plain"},
|
|
575
574
|
)
|
|
576
575
|
except httpx.TimeoutException as e:
|
|
577
576
|
raise HomeAssistantConnectionError(
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Shared factory for direct-Supervisor httpx clients.
|
|
2
|
+
|
|
3
|
+
Three call sites in the codebase talk directly to the Home Assistant
|
|
4
|
+
Supervisor REST API at ``http://supervisor`` rather than through
|
|
5
|
+
``HomeAssistantClient.httpx_client`` (which is bound to HA Core, not the
|
|
6
|
+
Supervisor — different base URL, different token, different role gate):
|
|
7
|
+
|
|
8
|
+
- :meth:`ha_mcp.client.rest_client.HomeAssistantClient._supervisor_logs_get`
|
|
9
|
+
— fetches addon and system-service logs
|
|
10
|
+
- :func:`ha_mcp.tools.tools_bug_report._fetch_addon_logs` — bundles ha-mcp's
|
|
11
|
+
own addon logs into a bug-report payload
|
|
12
|
+
- :func:`ha_mcp.settings_ui._restart_addon` — POSTs ``/addons/self/restart``
|
|
13
|
+
from the settings UI
|
|
14
|
+
|
|
15
|
+
All three share the same boilerplate (base URL, ``Authorization: Bearer
|
|
16
|
+
${SUPERVISOR_TOKEN}`` header), so this module supplies a single factory and
|
|
17
|
+
keeps the three sites consistent.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import ssl
|
|
24
|
+
|
|
25
|
+
import httpx
|
|
26
|
+
|
|
27
|
+
from .._version import get_supervisor_base_url
|
|
28
|
+
|
|
29
|
+
__all__ = ["make_supervisor_httpx_client"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def make_supervisor_httpx_client(
|
|
33
|
+
*,
|
|
34
|
+
timeout: float | httpx.Timeout,
|
|
35
|
+
verify: bool | str | ssl.SSLContext,
|
|
36
|
+
) -> httpx.AsyncClient:
|
|
37
|
+
"""Construct an ``httpx.AsyncClient`` pre-configured for the Supervisor REST API.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
timeout: Per-request timeout. Accepts either a plain ``float``
|
|
41
|
+
(seconds, applied to all phases) or a full :class:`httpx.Timeout`
|
|
42
|
+
for finer-grained control.
|
|
43
|
+
verify: TLS verify policy. A no-op for the default
|
|
44
|
+
``http://supervisor`` base URL (plain HTTP — no TLS to verify),
|
|
45
|
+
but kept as a parameter because :func:`get_supervisor_base_url`
|
|
46
|
+
honours ``SUPERVISOR_BASE_URL`` env-var overrides that may be
|
|
47
|
+
HTTPS in non-add-on test rigs. The full httpx ``verify`` surface
|
|
48
|
+
(``bool``, CA-bundle path, or :class:`ssl.SSLContext`) is
|
|
49
|
+
accepted and forwarded verbatim.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
A new :class:`httpx.AsyncClient` bound to the Supervisor base URL
|
|
53
|
+
with ``Authorization: Bearer ${SUPERVISOR_TOKEN}`` preset. Callers
|
|
54
|
+
pass relative paths (``/addons/self/logs``) to ``client.get/post``;
|
|
55
|
+
``base_url`` joins them onto the Supervisor host.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
RuntimeError: ``SUPERVISOR_TOKEN`` is unset or empty in the
|
|
59
|
+
environment. Each call site has its own absent-token policy
|
|
60
|
+
(a rich :class:`HomeAssistantAuthError`, a silent ``""``
|
|
61
|
+
return, or a 400 ``JSONResponse``) that does not share a
|
|
62
|
+
common shape, so the factory cannot translate. Detecting the
|
|
63
|
+
absence at construction time prevents a malformed
|
|
64
|
+
``Authorization: Bearer `` header from being read as a token
|
|
65
|
+
rejection by Supervisor, which would mask the missing-env-var
|
|
66
|
+
root cause.
|
|
67
|
+
|
|
68
|
+
Note:
|
|
69
|
+
``SUPERVISOR_TOKEN`` is read from env at construction time and
|
|
70
|
+
baked into the constructed client's ``Authorization`` header.
|
|
71
|
+
Reusing a single client across token rotations would not pick up
|
|
72
|
+
the new value — short-lived ``async with`` callers are unaffected,
|
|
73
|
+
but a future long-lived caller would need to discard and re-create.
|
|
74
|
+
"""
|
|
75
|
+
token = os.environ.get("SUPERVISOR_TOKEN", "")
|
|
76
|
+
if not token:
|
|
77
|
+
raise RuntimeError(
|
|
78
|
+
"SUPERVISOR_TOKEN is not set; "
|
|
79
|
+
"make_supervisor_httpx_client cannot construct an "
|
|
80
|
+
"authenticated client. Callers must verify the token is "
|
|
81
|
+
"present before invoking the factory."
|
|
82
|
+
)
|
|
83
|
+
return httpx.AsyncClient(
|
|
84
|
+
base_url=get_supervisor_base_url(),
|
|
85
|
+
timeout=timeout,
|
|
86
|
+
verify=verify,
|
|
87
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
88
|
+
)
|
|
@@ -21,6 +21,7 @@ from mcp.types import Icon
|
|
|
21
21
|
|
|
22
22
|
from .config import _PACKAGE_VERSION, get_global_settings
|
|
23
23
|
from .tools.enhanced import EnhancedToolsMixin
|
|
24
|
+
from .tools.util_helpers import strip_internal_fields
|
|
24
25
|
from .transforms import DEFAULT_PINNED_TOOLS
|
|
25
26
|
|
|
26
27
|
if TYPE_CHECKING:
|
|
@@ -930,13 +931,19 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
|
|
|
930
931
|
return await self.client.call_service(domain, service, service_data)
|
|
931
932
|
|
|
932
933
|
async def get_entities_by_area(self, area_name: str) -> dict[str, Any]:
|
|
933
|
-
"""Bridge method to existing area functionality.
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
934
|
+
"""Bridge method to existing area functionality.
|
|
935
|
+
|
|
936
|
+
``smart_tools.get_entities_by_area`` enriches per-entity dicts
|
|
937
|
+
with leading-underscore internals (``_hidden_by`` etc.) so
|
|
938
|
+
downstream search branches can apply the score penalty without
|
|
939
|
+
a second registry lookup. Strip them here so this public bridge
|
|
940
|
+
doesn't leak internals to MCP clients.
|
|
941
|
+
"""
|
|
942
|
+
result = await self.smart_tools.get_entities_by_area(
|
|
943
|
+
area_query=area_name, group_by_domain=True
|
|
939
944
|
)
|
|
945
|
+
strip_internal_fields(result)
|
|
946
|
+
return cast(dict[str, Any], result)
|
|
940
947
|
|
|
941
948
|
async def start(self) -> None:
|
|
942
949
|
"""Start the Smart MCP server with async compatibility."""
|
|
@@ -19,7 +19,8 @@ import httpx
|
|
|
19
19
|
from starlette.requests import Request
|
|
20
20
|
from starlette.responses import HTMLResponse, JSONResponse
|
|
21
21
|
|
|
22
|
-
from ._version import
|
|
22
|
+
from ._version import is_running_in_addon
|
|
23
|
+
from .client.supervisor_client import make_supervisor_httpx_client
|
|
23
24
|
from .errors import ErrorCode, create_error_response
|
|
24
25
|
from .transforms import DEFAULT_PINNED_TOOLS
|
|
25
26
|
from .utils.data_paths import get_data_dir
|
|
@@ -893,8 +894,7 @@ def register_settings_routes(
|
|
|
893
894
|
)
|
|
894
895
|
|
|
895
896
|
async def _restart_addon(_: Request) -> JSONResponse:
|
|
896
|
-
|
|
897
|
-
if not token:
|
|
897
|
+
if not os.environ.get("SUPERVISOR_TOKEN"):
|
|
898
898
|
return JSONResponse(
|
|
899
899
|
create_error_response(
|
|
900
900
|
ErrorCode.CONFIG_VALIDATION_FAILED,
|
|
@@ -906,13 +906,10 @@ def register_settings_routes(
|
|
|
906
906
|
# Short timeout — the supervisor kills our process during restart so
|
|
907
907
|
# the connection will drop. A connection drop is actually success.
|
|
908
908
|
try:
|
|
909
|
-
async with
|
|
909
|
+
async with make_supervisor_httpx_client(
|
|
910
910
|
timeout=5.0, verify=server.settings.verify_ssl
|
|
911
911
|
) as client:
|
|
912
|
-
resp = await client.post(
|
|
913
|
-
f"{get_supervisor_base_url()}/addons/self/restart",
|
|
914
|
-
headers={"Authorization": f"Bearer {token}"},
|
|
915
|
-
)
|
|
912
|
+
resp = await client.post("/addons/self/restart")
|
|
916
913
|
except (httpx.ReadError, httpx.RemoteProtocolError):
|
|
917
914
|
# Connection dropped mid-request — restart is happening.
|
|
918
915
|
# `ConnectError` is deliberately NOT in this tuple: it fires
|
|
@@ -113,9 +113,10 @@ class SmartSearchTools:
|
|
|
113
113
|
offset: int = 0,
|
|
114
114
|
include_attributes: bool = False,
|
|
115
115
|
domain_filter: str | None = None,
|
|
116
|
+
include_hidden: bool = True,
|
|
116
117
|
) -> dict[str, Any]:
|
|
117
118
|
"""
|
|
118
|
-
|
|
119
|
+
Search entities with fuzzy matching and typo tolerance.
|
|
119
120
|
|
|
120
121
|
Args:
|
|
121
122
|
query: Search query (can be partial, with typos)
|
|
@@ -123,16 +124,120 @@ class SmartSearchTools:
|
|
|
123
124
|
offset: Number of results to skip for pagination
|
|
124
125
|
include_attributes: Whether to include full entity attributes
|
|
125
126
|
domain_filter: Optional domain to filter entities before search (e.g., "light", "sensor")
|
|
127
|
+
include_hidden: When True (default), entities with ``hidden_by``
|
|
128
|
+
set in the entity registry are still returned but receive
|
|
129
|
+
a score penalty so they sort below comparable visible
|
|
130
|
+
matches. Pass False to filter them out entirely.
|
|
126
131
|
|
|
127
132
|
Returns:
|
|
128
133
|
Dictionary with search results and metadata
|
|
129
134
|
"""
|
|
130
135
|
try:
|
|
131
|
-
#
|
|
132
|
-
|
|
136
|
+
# HA domains are canonically lowercase and unpadded; defend
|
|
137
|
+
# the service layer so internal callers get the same
|
|
138
|
+
# normalization the tool layer applies (strip + lowercase
|
|
139
|
+
# before the prefix match downstream).
|
|
140
|
+
if domain_filter:
|
|
141
|
+
domain_filter = domain_filter.strip().lower()
|
|
142
|
+
# Fetch states + entity registry list in parallel. The slim
|
|
143
|
+
# ``list`` view gives us ``hidden_by`` (used to filter
|
|
144
|
+
# UI-hidden entities by default) and the entity_ids we need
|
|
145
|
+
# to feed into ``get_entries`` for the full-fidelity data
|
|
146
|
+
# (aliases live only in get_entries, not the slim list).
|
|
147
|
+
entities_task = self.client.get_states()
|
|
148
|
+
entity_registry_task = self.client.send_websocket_message(
|
|
149
|
+
{"type": "config/entity_registry/list"}
|
|
150
|
+
)
|
|
151
|
+
results = await asyncio.gather(
|
|
152
|
+
entities_task, entity_registry_task, return_exceptions=True
|
|
153
|
+
)
|
|
154
|
+
# States-fetch failure is fatal — auth/connection errors must
|
|
155
|
+
# propagate so the caller sees the real cause instead of a
|
|
156
|
+
# bogus "zero matches" with success=True.
|
|
157
|
+
if isinstance(results[0], BaseException):
|
|
158
|
+
raise results[0]
|
|
159
|
+
# CancelledError on the registry task must propagate too;
|
|
160
|
+
# gather captures it like any other exception when
|
|
161
|
+
# return_exceptions=True.
|
|
162
|
+
if isinstance(results[1], asyncio.CancelledError):
|
|
163
|
+
raise results[1]
|
|
164
|
+
entities = results[0]
|
|
133
165
|
|
|
134
|
-
#
|
|
135
|
-
#
|
|
166
|
+
# Build entity_id -> slim registry entry map. Registry-list
|
|
167
|
+
# failure is tolerated: search continues without alias /
|
|
168
|
+
# hidden awareness rather than failing the whole call.
|
|
169
|
+
registry_slim: dict[str, dict[str, Any]] = {}
|
|
170
|
+
if isinstance(results[1], dict) and results[1].get("success"):
|
|
171
|
+
for entry in results[1].get("result", []):
|
|
172
|
+
eid = entry.get("entity_id")
|
|
173
|
+
if eid:
|
|
174
|
+
registry_slim[eid] = entry
|
|
175
|
+
|
|
176
|
+
# First pass: hidden filter + collect entity_ids for the
|
|
177
|
+
# alias batch fetch. Pre-filtering shrinks the get_entries
|
|
178
|
+
# payload on installations with thousands of entities.
|
|
179
|
+
survivor_ids: list[str] = []
|
|
180
|
+
survivor_states: list[dict[str, Any]] = []
|
|
181
|
+
for entity in entities:
|
|
182
|
+
eid = entity.get("entity_id", "")
|
|
183
|
+
if not eid:
|
|
184
|
+
continue
|
|
185
|
+
slim = registry_slim.get(eid, {})
|
|
186
|
+
hidden_by = slim.get("hidden_by")
|
|
187
|
+
if hidden_by is not None and not include_hidden:
|
|
188
|
+
continue
|
|
189
|
+
survivor_ids.append(eid)
|
|
190
|
+
survivor_states.append(entity)
|
|
191
|
+
|
|
192
|
+
# Second pass: batch-fetch full registry entries for aliases.
|
|
193
|
+
# ``config/entity_registry/list`` deliberately omits
|
|
194
|
+
# ``aliases``; ``get_entries`` includes them. One extra
|
|
195
|
+
# round-trip enriches the survivor set without N+1 fan-out.
|
|
196
|
+
aliases_map: dict[str, list[str]] = {}
|
|
197
|
+
if survivor_ids:
|
|
198
|
+
try:
|
|
199
|
+
entries_resp = await self.client.send_websocket_message({
|
|
200
|
+
"type": "config/entity_registry/get_entries",
|
|
201
|
+
"entity_ids": survivor_ids,
|
|
202
|
+
})
|
|
203
|
+
if (
|
|
204
|
+
isinstance(entries_resp, dict)
|
|
205
|
+
and entries_resp.get("success")
|
|
206
|
+
):
|
|
207
|
+
for eid, entry in (
|
|
208
|
+
entries_resp.get("result", {}) or {}
|
|
209
|
+
).items():
|
|
210
|
+
if isinstance(entry, dict):
|
|
211
|
+
aliases_map[eid] = entry.get("aliases", []) or []
|
|
212
|
+
else:
|
|
213
|
+
logger.warning(
|
|
214
|
+
"alias_enrichment_failed: get_entries returned "
|
|
215
|
+
"non-success for %d entities (resp=%r)",
|
|
216
|
+
len(survivor_ids),
|
|
217
|
+
entries_resp,
|
|
218
|
+
)
|
|
219
|
+
except (KeyError, TypeError, AttributeError) as alias_err:
|
|
220
|
+
logger.warning(
|
|
221
|
+
"alias_enrichment_failed: malformed payload for "
|
|
222
|
+
"%d entities (err=%r)",
|
|
223
|
+
len(survivor_ids),
|
|
224
|
+
alias_err,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Enrich entities with aliases + hidden_by for the fuzzy layer.
|
|
228
|
+
enriched: list[dict[str, Any]] = []
|
|
229
|
+
for entity, eid in zip(survivor_states, survivor_ids, strict=True):
|
|
230
|
+
slim = registry_slim.get(eid, {})
|
|
231
|
+
# Shallow copy + private-prefixed keys so downstream
|
|
232
|
+
# consumers that round-trip these dicts don't ship
|
|
233
|
+
# internal fields back to clients.
|
|
234
|
+
enriched.append({
|
|
235
|
+
**entity,
|
|
236
|
+
"_aliases": aliases_map.get(eid, []),
|
|
237
|
+
"_hidden_by": slim.get("hidden_by"),
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
entities = enriched
|
|
136
241
|
if domain_filter:
|
|
137
242
|
entities = [
|
|
138
243
|
e
|
|
@@ -159,19 +264,12 @@ class SmartSearchTools:
|
|
|
159
264
|
|
|
160
265
|
if include_attributes:
|
|
161
266
|
result["attributes"] = match["attributes"]
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
"device_class",
|
|
169
|
-
"icon",
|
|
170
|
-
"area_id",
|
|
171
|
-
]:
|
|
172
|
-
if key in attrs:
|
|
173
|
-
essential_attrs[key] = attrs[key]
|
|
174
|
-
result["essential_attributes"] = essential_attrs
|
|
267
|
+
# No ``essential_attributes`` fallback — the other four
|
|
268
|
+
# search-type branches (exact_match, area_only,
|
|
269
|
+
# area_filtered_query, domain_listing) never emit it, so
|
|
270
|
+
# surfacing it only from fuzzy_search was a shape
|
|
271
|
+
# asymmetry. Callers needing full state should follow
|
|
272
|
+
# up with ``ha_get_state``.
|
|
175
273
|
|
|
176
274
|
results.append(result)
|
|
177
275
|
|
|
@@ -213,18 +311,25 @@ class SmartSearchTools:
|
|
|
213
311
|
)
|
|
214
312
|
|
|
215
313
|
async def get_entities_by_area(
|
|
216
|
-
self,
|
|
314
|
+
self,
|
|
315
|
+
area_query: str,
|
|
316
|
+
group_by_domain: bool = True,
|
|
317
|
+
include_hidden: bool = True,
|
|
217
318
|
) -> dict[str, Any]:
|
|
218
319
|
"""
|
|
219
320
|
Get entities grouped by area/room using the HA registries for accurate area resolution.
|
|
220
321
|
|
|
221
322
|
Uses entity registry, device registry, and area registry to determine
|
|
222
323
|
which area each entity belongs to. Fuzzy matches the query against
|
|
223
|
-
area names
|
|
324
|
+
area names, IDs, and area-registry aliases to find the target area(s).
|
|
224
325
|
|
|
225
326
|
Args:
|
|
226
|
-
area_query: Area/room name to search for
|
|
327
|
+
area_query: Area/room name (or alias) to search for
|
|
227
328
|
group_by_domain: Whether to group results by domain within each area
|
|
329
|
+
include_hidden: When True (default), entities with ``hidden_by``
|
|
330
|
+
set in the entity registry are still grouped under their
|
|
331
|
+
area but receive a score penalty when ranked. Pass False
|
|
332
|
+
to filter them out entirely.
|
|
228
333
|
|
|
229
334
|
Returns:
|
|
230
335
|
Dictionary with area-grouped entities
|
|
@@ -260,7 +365,7 @@ class SmartSearchTools:
|
|
|
260
365
|
if area_id:
|
|
261
366
|
area_registry[area_id] = area
|
|
262
367
|
|
|
263
|
-
# Parse entity registry: entity_id -> {area_id, device_id}
|
|
368
|
+
# Parse entity registry: entity_id -> {area_id, device_id, hidden_by}
|
|
264
369
|
entity_reg_map: dict[str, dict[str, str | None]] = {}
|
|
265
370
|
if isinstance(results[2], dict) and results[2].get("success"):
|
|
266
371
|
for entry in results[2].get("result", []):
|
|
@@ -269,6 +374,7 @@ class SmartSearchTools:
|
|
|
269
374
|
entity_reg_map[entity_id] = {
|
|
270
375
|
"area_id": entry.get("area_id"),
|
|
271
376
|
"device_id": entry.get("device_id"),
|
|
377
|
+
"hidden_by": entry.get("hidden_by"),
|
|
272
378
|
}
|
|
273
379
|
|
|
274
380
|
# Parse device registry: device_id -> area_id
|
|
@@ -279,27 +385,54 @@ class SmartSearchTools:
|
|
|
279
385
|
if device_id:
|
|
280
386
|
device_area_map[device_id] = device.get("area_id")
|
|
281
387
|
|
|
282
|
-
#
|
|
388
|
+
# Two-pass area resolution. Pass 1 collects exact id / name /
|
|
389
|
+
# alias matches; if any are found, fuzzy aggregation is
|
|
390
|
+
# skipped entirely. This makes ``area_filter`` honor a
|
|
391
|
+
# literal area_id from ``ha_config_list_areas`` — pre-fix a
|
|
392
|
+
# query like ``"bedroom_kids"`` would also fuzzy-match its
|
|
393
|
+
# parent ``"bedroom"`` (partial_ratio=100) and aggregate
|
|
394
|
+
# sibling areas' entities. Aliases (per-area registry, used
|
|
395
|
+
# by HA voice config) mirror the entity-side enrichment in
|
|
396
|
+
# smart_entity_search.
|
|
283
397
|
area_query_lower = area_query.lower().strip()
|
|
284
|
-
|
|
398
|
+
exact_area_ids: set[str] = set()
|
|
399
|
+
fuzzy_area_ids: set[str] = set()
|
|
285
400
|
|
|
286
401
|
for area_id, area_info in area_registry.items():
|
|
287
402
|
area_name = area_info.get("name", "")
|
|
288
|
-
|
|
403
|
+
area_aliases = area_info.get("aliases", []) or []
|
|
404
|
+
# Exact match on area_id, name, or any alias (case-insensitive)
|
|
289
405
|
if (
|
|
290
406
|
area_query_lower == area_id.lower()
|
|
291
407
|
or area_query_lower == area_name.lower()
|
|
408
|
+
or any(
|
|
409
|
+
area_query_lower == a.lower()
|
|
410
|
+
for a in area_aliases
|
|
411
|
+
if isinstance(a, str)
|
|
412
|
+
)
|
|
292
413
|
):
|
|
293
|
-
|
|
414
|
+
exact_area_ids.add(area_id)
|
|
294
415
|
continue
|
|
295
|
-
# Fuzzy match on area name
|
|
416
|
+
# Fuzzy match on area name, id, or any alias
|
|
296
417
|
name_score = calculate_partial_ratio(
|
|
297
418
|
area_query_lower, area_name.lower()
|
|
298
419
|
)
|
|
299
420
|
id_score = calculate_partial_ratio(area_query_lower, area_id.lower())
|
|
300
|
-
|
|
421
|
+
alias_score = max(
|
|
422
|
+
(
|
|
423
|
+
calculate_partial_ratio(area_query_lower, a.lower())
|
|
424
|
+
for a in area_aliases
|
|
425
|
+
if isinstance(a, str)
|
|
426
|
+
),
|
|
427
|
+
default=0,
|
|
428
|
+
)
|
|
429
|
+
best_score = max(name_score, id_score, alias_score)
|
|
301
430
|
if best_score >= 80:
|
|
302
|
-
|
|
431
|
+
fuzzy_area_ids.add(area_id)
|
|
432
|
+
|
|
433
|
+
# Exact matches win — fuzzy aggregation only runs when no
|
|
434
|
+
# area_query_lower is itself an area_id / name / alias.
|
|
435
|
+
matched_area_ids = exact_area_ids or fuzzy_area_ids
|
|
303
436
|
|
|
304
437
|
if not matched_area_ids:
|
|
305
438
|
return {
|
|
@@ -313,10 +446,19 @@ class SmartSearchTools:
|
|
|
313
446
|
],
|
|
314
447
|
}
|
|
315
448
|
|
|
316
|
-
# Build entity_id -> resolved area_id mapping
|
|
317
|
-
# Priority: entity direct area_id > device area_id
|
|
449
|
+
# Build entity_id -> resolved area_id mapping.
|
|
450
|
+
# Priority: entity direct area_id > device area_id.
|
|
451
|
+
# Hidden entities are filtered only when include_hidden is
|
|
452
|
+
# False; otherwise they pass through and downstream applies
|
|
453
|
+
# the score penalty so they sort below visible matches.
|
|
318
454
|
entity_area_resolved: dict[str, str] = {}
|
|
455
|
+
hidden_entity_ids: set[str] = set()
|
|
319
456
|
for entity_id, reg_info in entity_reg_map.items():
|
|
457
|
+
is_hidden = reg_info.get("hidden_by") is not None
|
|
458
|
+
if is_hidden and not include_hidden:
|
|
459
|
+
continue
|
|
460
|
+
if is_hidden:
|
|
461
|
+
hidden_entity_ids.add(entity_id)
|
|
320
462
|
area_id = reg_info.get("area_id")
|
|
321
463
|
device_id = reg_info.get("device_id")
|
|
322
464
|
if not area_id and device_id:
|
|
@@ -331,7 +473,12 @@ class SmartSearchTools:
|
|
|
331
473
|
if eid:
|
|
332
474
|
state_map[eid] = entity
|
|
333
475
|
|
|
334
|
-
# Collect entities belonging to matched areas
|
|
476
|
+
# Collect entities belonging to matched areas. Alias data is
|
|
477
|
+
# NOT enriched here — exposing private `_aliases` on a public
|
|
478
|
+
# method would leak through any caller that round-trips this
|
|
479
|
+
# response (e.g. server.py:get_entities_by_area). The
|
|
480
|
+
# area+query consumer in tools_search.py fetches aliases on
|
|
481
|
+
# its own when needed.
|
|
335
482
|
formatted_areas: dict[str, dict[str, Any]] = {}
|
|
336
483
|
total_entities = 0
|
|
337
484
|
|
|
@@ -360,6 +507,9 @@ class SmartSearchTools:
|
|
|
360
507
|
state_info = state_map.get(entity_id, {})
|
|
361
508
|
if domain not in domains:
|
|
362
509
|
domains[domain] = []
|
|
510
|
+
# Carry ``_hidden_by`` as a sentinel ("hidden" or
|
|
511
|
+
# None) so downstream branches can apply the
|
|
512
|
+
# score penalty without a second registry lookup.
|
|
363
513
|
domains[domain].append(
|
|
364
514
|
{
|
|
365
515
|
"entity_id": entity_id,
|
|
@@ -367,6 +517,11 @@ class SmartSearchTools:
|
|
|
367
517
|
"friendly_name", entity_id
|
|
368
518
|
),
|
|
369
519
|
"state": state_info.get("state", "unknown"),
|
|
520
|
+
"_hidden_by": (
|
|
521
|
+
"hidden"
|
|
522
|
+
if entity_id in hidden_entity_ids
|
|
523
|
+
else None
|
|
524
|
+
),
|
|
370
525
|
}
|
|
371
526
|
)
|
|
372
527
|
area_data["entities"] = domains
|
|
@@ -381,6 +536,11 @@ class SmartSearchTools:
|
|
|
381
536
|
.get("friendly_name", entity_id),
|
|
382
537
|
"domain": entity_id.split(".")[0],
|
|
383
538
|
"state": state_info.get("state", "unknown"),
|
|
539
|
+
"_hidden_by": (
|
|
540
|
+
"hidden"
|
|
541
|
+
if entity_id in hidden_entity_ids
|
|
542
|
+
else None
|
|
543
|
+
),
|
|
384
544
|
}
|
|
385
545
|
for entity_id in area_entities
|
|
386
546
|
]
|
|
@@ -20,7 +20,7 @@ from pydantic import Field
|
|
|
20
20
|
|
|
21
21
|
from ha_mcp import __version__
|
|
22
22
|
|
|
23
|
-
from ..
|
|
23
|
+
from ..client.supervisor_client import make_supervisor_httpx_client
|
|
24
24
|
from ..config import Settings, get_global_settings
|
|
25
25
|
from ..utils.usage_logger import (
|
|
26
26
|
AVG_LOG_ENTRIES_PER_TOOL,
|
|
@@ -349,18 +349,14 @@ async def _fetch_addon_logs() -> str:
|
|
|
349
349
|
"""
|
|
350
350
|
# Redundant with the caller's `install_method == "addon"` gate, but kept
|
|
351
351
|
# as a defensive guard for any direct callers added later.
|
|
352
|
-
|
|
353
|
-
if not token:
|
|
352
|
+
if not os.environ.get("SUPERVISOR_TOKEN"):
|
|
354
353
|
return ""
|
|
355
354
|
|
|
356
355
|
try:
|
|
357
|
-
async with
|
|
356
|
+
async with make_supervisor_httpx_client(
|
|
358
357
|
timeout=10.0, verify=get_global_settings().verify_ssl
|
|
359
358
|
) as http_client:
|
|
360
|
-
resp = await http_client.get(
|
|
361
|
-
f"{get_supervisor_base_url()}/addons/self/logs",
|
|
362
|
-
headers={"Authorization": f"Bearer {token}"},
|
|
363
|
-
)
|
|
359
|
+
resp = await http_client.get("/addons/self/logs")
|
|
364
360
|
if resp.status_code != 200:
|
|
365
361
|
logger.info("Addon log fetch returned HTTP %s", resp.status_code)
|
|
366
362
|
return ""
|