ha-mcp-dev 7.5.0.dev595__tar.gz → 7.5.0.dev596__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ha_mcp_dev-7.5.0.dev595/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev596}/PKG-INFO +1 -1
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/pyproject.toml +1 -1
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_integrations.py +12 -22
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_search.py +5 -81
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_service.py +143 -14
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_system.py +78 -60
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/util_helpers.py +157 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/LICENSE +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/README.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/setup.cfg +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.5.0.dev595 → ha_mcp_dev-7.5.0.dev596}/tests/test_env_manager.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.5.0.
|
|
7
|
+
version = "7.5.0.dev596"
|
|
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"
|
|
@@ -7,7 +7,7 @@ integrations (config entries) via the REST and WebSocket APIs.
|
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import logging
|
|
10
|
-
from typing import Annotated, Any, Literal,
|
|
10
|
+
from typing import Annotated, Any, Literal, get_args
|
|
11
11
|
|
|
12
12
|
from fastmcp.exceptions import ToolError
|
|
13
13
|
from fastmcp.tools import tool
|
|
@@ -459,27 +459,18 @@ class IntegrationTools:
|
|
|
459
459
|
include_diagnostics_bool = coerce_bool_param(
|
|
460
460
|
include_diagnostics, "include_diagnostics", default=False
|
|
461
461
|
)
|
|
462
|
-
include_subentries_bool =
|
|
463
|
-
|
|
464
|
-
coerce_bool_param(
|
|
465
|
-
include_subentries, "include_subentries", default=False
|
|
466
|
-
),
|
|
462
|
+
include_subentries_bool = coerce_bool_param(
|
|
463
|
+
include_subentries, "include_subentries", default=False
|
|
467
464
|
)
|
|
468
|
-
include_subentry_schema_bool =
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
"include_subentry_schema",
|
|
473
|
-
default=False,
|
|
474
|
-
),
|
|
465
|
+
include_subentry_schema_bool = coerce_bool_param(
|
|
466
|
+
include_subentry_schema,
|
|
467
|
+
"include_subentry_schema",
|
|
468
|
+
default=False,
|
|
475
469
|
)
|
|
476
|
-
show_advanced_options_bool =
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
"show_advanced_options",
|
|
481
|
-
default=False,
|
|
482
|
-
),
|
|
470
|
+
show_advanced_options_bool = coerce_bool_param(
|
|
471
|
+
show_advanced_options,
|
|
472
|
+
"show_advanced_options",
|
|
473
|
+
default=False,
|
|
483
474
|
)
|
|
484
475
|
exact_match_bool = coerce_bool_param(
|
|
485
476
|
exact_match, "exact_match", default=True
|
|
@@ -1316,8 +1307,7 @@ class IntegrationTools:
|
|
|
1316
1307
|
context={"helper_type": helper_type},
|
|
1317
1308
|
)
|
|
1318
1309
|
|
|
1319
|
-
|
|
1320
|
-
wait_bool = cast(bool, coerce_bool_param(wait, "wait", default=True))
|
|
1310
|
+
wait_bool = coerce_bool_param(wait, "wait", default=True)
|
|
1321
1311
|
warnings: list[str] = []
|
|
1322
1312
|
|
|
1323
1313
|
# === Routing dispatch ===
|
|
@@ -30,6 +30,9 @@ from .util_helpers import (
|
|
|
30
30
|
public_fields,
|
|
31
31
|
result_fields_warning,
|
|
32
32
|
)
|
|
33
|
+
from .util_helpers import (
|
|
34
|
+
project_entity_record as _project_entity,
|
|
35
|
+
)
|
|
33
36
|
|
|
34
37
|
logger = logging.getLogger(__name__)
|
|
35
38
|
|
|
@@ -166,85 +169,6 @@ async def _exact_match_search(
|
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
|
|
169
|
-
def _project_entity(
|
|
170
|
-
record: dict[str, Any],
|
|
171
|
-
fields: list[str] | None,
|
|
172
|
-
attribute_keys: list[str] | None,
|
|
173
|
-
) -> tuple[dict[str, Any], str | None]:
|
|
174
|
-
"""Apply optional field projection to a HA entity record.
|
|
175
|
-
|
|
176
|
-
``fields`` filters which top-level keys to keep (e.g. ["state", "attributes"]).
|
|
177
|
-
``attribute_keys`` further filters the ``attributes`` sub-dict.
|
|
178
|
-
Both default None = full payload (no-op).
|
|
179
|
-
|
|
180
|
-
Returns ``(projected_record, warning_string | None)``. *warning_string* is
|
|
181
|
-
non-None when ``attribute_keys`` was specified, the original ``attributes``
|
|
182
|
-
dict was non-empty, and the filter produced an empty result — i.e. the caller
|
|
183
|
-
supplied only unknown attribute keys (typo guard). Callers should append the
|
|
184
|
-
warning to the response ``warnings`` list so the user receives a diagnostic
|
|
185
|
-
rather than a silently empty ``attributes: {}``.
|
|
186
|
-
|
|
187
|
-
Both parameters are already parsed into ``list[str] | None`` — string/CSV inputs
|
|
188
|
-
must be normalised at the call site via ``parse_string_list_param`` (see
|
|
189
|
-
``ha_get_state`` which parses once before the bulk loop to avoid re-parsing per
|
|
190
|
-
entity record).
|
|
191
|
-
|
|
192
|
-
Unlike ``project_fields``, this helper does not auto-retain ``success`` — entity
|
|
193
|
-
records have no ``success`` field, so the asymmetry is intentional.
|
|
194
|
-
|
|
195
|
-
Non-dict ``attributes`` handling: when ``attribute_keys`` is set but the
|
|
196
|
-
record's ``attributes`` value is not a dict (``None``, a string, a list —
|
|
197
|
-
rare from HA's state API but possible from malformed records, partial
|
|
198
|
-
error payloads, or mocked fixtures), the key-set filter cannot be
|
|
199
|
-
applied and the ``attributes`` value is returned unchanged. A
|
|
200
|
-
``warning``-level log line records the short-circuit so it is visible
|
|
201
|
-
at default log levels. The bulk path shares this helper, so
|
|
202
|
-
both single- and bulk-entity calls behave identically here. This is
|
|
203
|
-
deliberately silent (no caller-facing warning) because malformed
|
|
204
|
-
``attributes`` is rare and the call still produces a usable record.
|
|
205
|
-
"""
|
|
206
|
-
if not isinstance(record, dict):
|
|
207
|
-
return (
|
|
208
|
-
record,
|
|
209
|
-
None,
|
|
210
|
-
) # non-dict (e.g. error path returning None) — skip projection
|
|
211
|
-
if fields is not None:
|
|
212
|
-
keep = set(fields)
|
|
213
|
-
record = {k: v for k, v in record.items() if k in keep}
|
|
214
|
-
attr_warn: str | None = None
|
|
215
|
-
if attribute_keys is not None:
|
|
216
|
-
attrs = record.get("attributes")
|
|
217
|
-
if isinstance(attrs, dict):
|
|
218
|
-
attr_keep = set(attribute_keys)
|
|
219
|
-
filtered_attrs = {k: v for k, v in attrs.items() if k in attr_keep}
|
|
220
|
-
# Typo guard: if the original had keys AND the caller specified at least
|
|
221
|
-
# one key AND the filter yielded nothing, the caller likely mistyped an
|
|
222
|
-
# attribute name. Return a diagnostic so the agent gets
|
|
223
|
-
# "attributes came out empty — available: [...]" instead of silently
|
|
224
|
-
# receiving ``attributes: {}``.
|
|
225
|
-
# Exclude the attribute_keys=[] case — an empty list is an explicit
|
|
226
|
-
# "keep nothing" request, not a typo.
|
|
227
|
-
if attrs and attribute_keys and not filtered_attrs:
|
|
228
|
-
available = sorted(attrs.keys())
|
|
229
|
-
attr_warn = (
|
|
230
|
-
f"attribute_keys {sorted(attribute_keys)!r} matched no attribute "
|
|
231
|
-
f"keys — attributes came out empty. "
|
|
232
|
-
f"Available keys: {available!r}"
|
|
233
|
-
)
|
|
234
|
-
record = {**record, "attributes": filtered_attrs}
|
|
235
|
-
elif "attributes" in record:
|
|
236
|
-
# ``attributes`` is present but not a dict — filter cannot apply.
|
|
237
|
-
# Log at warning so the no-op is visible at default log levels
|
|
238
|
-
# (this branch is exercised rarely; see docstring for rationale).
|
|
239
|
-
logger.warning(
|
|
240
|
-
"_project_entity: attribute_keys filter skipped — "
|
|
241
|
-
"'attributes' is %s (expected dict) for record keys=%r",
|
|
242
|
-
type(attrs).__name__,
|
|
243
|
-
list(record.keys()),
|
|
244
|
-
)
|
|
245
|
-
return record, attr_warn
|
|
246
|
-
|
|
247
|
-
|
|
248
172
|
def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
249
173
|
"""Register search and discovery tools with the MCP server."""
|
|
250
174
|
smart_tools = kwargs.get("smart_tools")
|
|
@@ -1564,8 +1488,8 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
1564
1488
|
"""
|
|
1565
1489
|
# Parse search_types to handle JSON string input from MCP clients
|
|
1566
1490
|
parsed_search_types = parse_string_list_param(search_types, "search_types")
|
|
1567
|
-
include_config_bool = (
|
|
1568
|
-
|
|
1491
|
+
include_config_bool = coerce_bool_param(
|
|
1492
|
+
include_config, "include_config", default=False
|
|
1569
1493
|
)
|
|
1570
1494
|
exact_match_bool = coerce_bool_param(exact_match, "exact_match", default=True)
|
|
1571
1495
|
try:
|
|
@@ -23,7 +23,14 @@ from .helpers import (
|
|
|
23
23
|
raise_tool_error,
|
|
24
24
|
register_tool_methods,
|
|
25
25
|
)
|
|
26
|
-
from .util_helpers import
|
|
26
|
+
from .util_helpers import (
|
|
27
|
+
coerce_bool_param,
|
|
28
|
+
compact_service_result,
|
|
29
|
+
parse_json_param,
|
|
30
|
+
parse_string_list_param,
|
|
31
|
+
project_entity_record,
|
|
32
|
+
wait_for_state_change,
|
|
33
|
+
)
|
|
27
34
|
|
|
28
35
|
|
|
29
36
|
def _parse_json_dict_param(
|
|
@@ -215,6 +222,62 @@ class ServiceTools:
|
|
|
215
222
|
f"Service executed but state verification failed: {e}"
|
|
216
223
|
)
|
|
217
224
|
|
|
225
|
+
@staticmethod
|
|
226
|
+
def _project_service_result(
|
|
227
|
+
result: Any,
|
|
228
|
+
*,
|
|
229
|
+
entity_id: str | None,
|
|
230
|
+
verbose: bool,
|
|
231
|
+
fields: list[str] | None,
|
|
232
|
+
attribute_keys: list[str] | None,
|
|
233
|
+
) -> tuple[Any, list[str]]:
|
|
234
|
+
"""Apply compact / explicit projection to a service-call ``result``.
|
|
235
|
+
|
|
236
|
+
Issue #1446. Precedence:
|
|
237
|
+
|
|
238
|
+
- ``verbose=True``: bypass every transformation; return ``result`` as-is.
|
|
239
|
+
- Explicit ``fields`` or ``attribute_keys``: apply per-record projection
|
|
240
|
+
via ``project_entity_record`` to every record. No compaction; this is
|
|
241
|
+
the power-user path.
|
|
242
|
+
- Default: apply ``compact_service_result`` (filter to ``entity_id``
|
|
243
|
+
record when single string, drop top-level metadata + heavy lists).
|
|
244
|
+
|
|
245
|
+
Returns ``(projected, warnings)``. ``warnings`` collects per-record
|
|
246
|
+
typo-guard diagnostics from ``project_entity_record`` (e.g. all-empty
|
|
247
|
+
``attribute_keys`` filter) — deduplicated so an N-record list with the
|
|
248
|
+
same typo doesn't emit N copies of the same warning.
|
|
249
|
+
"""
|
|
250
|
+
if verbose:
|
|
251
|
+
return result, []
|
|
252
|
+
if fields is None and attribute_keys is None:
|
|
253
|
+
return compact_service_result(result, entity_id), []
|
|
254
|
+
if not isinstance(result, list):
|
|
255
|
+
return result, []
|
|
256
|
+
warnings: list[str] = []
|
|
257
|
+
# ``result_attribute_keys`` only takes effect when ``attributes`` is in
|
|
258
|
+
# the projected ``result_fields`` (or ``result_fields`` is None). Surface
|
|
259
|
+
# a warning rather than silently ignoring the parameter — mirrors
|
|
260
|
+
# ha_get_state's attribute_keys_no_effect handling.
|
|
261
|
+
if (
|
|
262
|
+
attribute_keys is not None
|
|
263
|
+
and fields is not None
|
|
264
|
+
and "attributes" not in fields
|
|
265
|
+
):
|
|
266
|
+
warnings.append(
|
|
267
|
+
"result_attribute_keys was ignored because 'attributes' is not "
|
|
268
|
+
"in result_fields. Add 'attributes' to result_fields (or omit "
|
|
269
|
+
"result_fields) to apply result_attribute_keys."
|
|
270
|
+
)
|
|
271
|
+
projected: list[Any] = []
|
|
272
|
+
seen_warnings: set[str] = set()
|
|
273
|
+
for record in result:
|
|
274
|
+
new_record, warn = project_entity_record(record, fields, attribute_keys)
|
|
275
|
+
projected.append(new_record)
|
|
276
|
+
if warn and warn not in seen_warnings:
|
|
277
|
+
seen_warnings.add(warn)
|
|
278
|
+
warnings.append(warn)
|
|
279
|
+
return projected, warnings
|
|
280
|
+
|
|
218
281
|
@tool(
|
|
219
282
|
name="ha_call_service",
|
|
220
283
|
tags={"Service & Device Control"},
|
|
@@ -229,6 +292,43 @@ class ServiceTools:
|
|
|
229
292
|
data: str | dict[str, Any] | None = None,
|
|
230
293
|
return_response: bool | str = False,
|
|
231
294
|
wait: bool | str = True,
|
|
295
|
+
verbose: Annotated[
|
|
296
|
+
bool | str,
|
|
297
|
+
Field(
|
|
298
|
+
description=(
|
|
299
|
+
"Return HA's raw service response unchanged (default: False). "
|
|
300
|
+
"Use as an escape hatch when you need the full propagation "
|
|
301
|
+
"chain or raw attribute payload (debug / inspection). "
|
|
302
|
+
"WARNING: brings back token-bloat for nested-group targets — "
|
|
303
|
+
"prefer result_fields / result_attribute_keys for targeted control."
|
|
304
|
+
),
|
|
305
|
+
),
|
|
306
|
+
] = False,
|
|
307
|
+
result_fields: Annotated[
|
|
308
|
+
str | list[str] | None,
|
|
309
|
+
Field(
|
|
310
|
+
default=None,
|
|
311
|
+
description=(
|
|
312
|
+
"Project each record in 'result' to only these top-level keys "
|
|
313
|
+
"(e.g. ['entity_id', 'state']). Mirrors ha_get_state's fields=. "
|
|
314
|
+
"Setting this DISABLES default compaction — no entity-id filter, "
|
|
315
|
+
"no metadata strip — and applies the explicit projection instead."
|
|
316
|
+
),
|
|
317
|
+
),
|
|
318
|
+
] = None,
|
|
319
|
+
result_attribute_keys: Annotated[
|
|
320
|
+
str | list[str] | None,
|
|
321
|
+
Field(
|
|
322
|
+
default=None,
|
|
323
|
+
description=(
|
|
324
|
+
"Project each record's 'attributes' dict to only these keys "
|
|
325
|
+
"(e.g. ['brightness', 'rgb_color']). Mirrors ha_get_state's "
|
|
326
|
+
"attribute_keys=. Setting this DISABLES default compaction. "
|
|
327
|
+
"Requires 'attributes' to be present in result_fields (or "
|
|
328
|
+
"result_fields=None)."
|
|
329
|
+
),
|
|
330
|
+
),
|
|
331
|
+
] = None,
|
|
232
332
|
) -> dict[str, Any]:
|
|
233
333
|
"""
|
|
234
334
|
Execute Home Assistant services to control entities and trigger automations.
|
|
@@ -252,15 +352,15 @@ class ServiceTools:
|
|
|
252
352
|
ha_call_service("homeassistant", "toggle", entity_id="switch.porch_light")
|
|
253
353
|
```
|
|
254
354
|
|
|
255
|
-
**
|
|
256
|
-
- **
|
|
257
|
-
|
|
258
|
-
- **
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
355
|
+
**Key behavior:**
|
|
356
|
+
- **wait** (default True): wait for the entity state to change before
|
|
357
|
+
returning. Only applies to state-changing services on a single entity.
|
|
358
|
+
- **Result compaction (issue #1446, default ON)**: ``result`` is trimmed
|
|
359
|
+
to the targeted entity's record (drops parent-group propagation) and
|
|
360
|
+
stripped of ``context`` / ``last_*`` metadata and heavy attribute
|
|
361
|
+
lists (``effect_list``, ``hue_scenes``). Escape hatches: ``verbose=True``
|
|
362
|
+
for the raw HA response, or ``result_fields`` / ``result_attribute_keys``
|
|
363
|
+
for explicit per-record projection (mirrors ``ha_get_state``).
|
|
264
364
|
|
|
265
365
|
**For detailed service documentation, use ha_get_skill_guide.**
|
|
266
366
|
|
|
@@ -271,11 +371,30 @@ class ServiceTools:
|
|
|
271
371
|
service_data = self._parse_service_data(data, entity_id)
|
|
272
372
|
|
|
273
373
|
# Coerce return_response boolean parameter
|
|
274
|
-
return_response_bool = (
|
|
275
|
-
|
|
276
|
-
or False
|
|
374
|
+
return_response_bool = coerce_bool_param(
|
|
375
|
+
return_response, "return_response", default=False
|
|
277
376
|
)
|
|
278
377
|
wait_bool = coerce_bool_param(wait, "wait", default=True)
|
|
378
|
+
try:
|
|
379
|
+
verbose_bool = coerce_bool_param(verbose, "verbose", default=False)
|
|
380
|
+
except ValueError as e:
|
|
381
|
+
raise_tool_error(create_validation_error(str(e), parameter="verbose"))
|
|
382
|
+
try:
|
|
383
|
+
parsed_result_fields = parse_string_list_param(
|
|
384
|
+
result_fields, "result_fields", allow_csv=True
|
|
385
|
+
)
|
|
386
|
+
except ValueError as e:
|
|
387
|
+
raise_tool_error(
|
|
388
|
+
create_validation_error(str(e), parameter="result_fields")
|
|
389
|
+
)
|
|
390
|
+
try:
|
|
391
|
+
parsed_result_attribute_keys = parse_string_list_param(
|
|
392
|
+
result_attribute_keys, "result_attribute_keys", allow_csv=True
|
|
393
|
+
)
|
|
394
|
+
except ValueError as e:
|
|
395
|
+
raise_tool_error(
|
|
396
|
+
create_validation_error(str(e), parameter="result_attribute_keys")
|
|
397
|
+
)
|
|
279
398
|
|
|
280
399
|
# Determine if we should wait for state change:
|
|
281
400
|
# Only for state-changing services on a single entity, not for
|
|
@@ -296,15 +415,25 @@ class ServiceTools:
|
|
|
296
415
|
domain, service, service_data, return_response=return_response_bool
|
|
297
416
|
)
|
|
298
417
|
|
|
418
|
+
projected_result, projection_warnings = self._project_service_result(
|
|
419
|
+
result,
|
|
420
|
+
entity_id=entity_id,
|
|
421
|
+
verbose=verbose_bool,
|
|
422
|
+
fields=parsed_result_fields,
|
|
423
|
+
attribute_keys=parsed_result_attribute_keys,
|
|
424
|
+
)
|
|
425
|
+
|
|
299
426
|
response: dict[str, Any] = {
|
|
300
427
|
"success": True,
|
|
301
428
|
"domain": domain,
|
|
302
429
|
"service": service,
|
|
303
430
|
"entity_id": entity_id,
|
|
304
431
|
"parameters": data,
|
|
305
|
-
"result":
|
|
432
|
+
"result": projected_result,
|
|
306
433
|
"message": f"Successfully executed {domain}.{service}",
|
|
307
434
|
}
|
|
435
|
+
if projection_warnings:
|
|
436
|
+
response.setdefault("warnings", []).extend(projection_warnings)
|
|
308
437
|
|
|
309
438
|
# If return_response was requested, include the service_response key prominently
|
|
310
439
|
if return_response_bool and isinstance(result, dict):
|
|
@@ -65,7 +65,11 @@ class SystemTools:
|
|
|
65
65
|
@tool(
|
|
66
66
|
name="ha_check_config",
|
|
67
67
|
tags={"System"},
|
|
68
|
-
annotations={
|
|
68
|
+
annotations={
|
|
69
|
+
"idempotentHint": True,
|
|
70
|
+
"readOnlyHint": True,
|
|
71
|
+
"title": "Check Configuration",
|
|
72
|
+
},
|
|
69
73
|
)
|
|
70
74
|
@log_tool_usage
|
|
71
75
|
async def ha_check_config(self) -> dict[str, Any]:
|
|
@@ -142,22 +146,24 @@ class SystemTools:
|
|
|
142
146
|
instead, which reloads specific components without a full restart.
|
|
143
147
|
"""
|
|
144
148
|
# Coerce boolean parameter that may come as string from XML-style calls
|
|
145
|
-
confirm_bool = coerce_bool_param(confirm, "confirm", default=False)
|
|
149
|
+
confirm_bool = coerce_bool_param(confirm, "confirm", default=False)
|
|
146
150
|
|
|
147
151
|
if not confirm_bool:
|
|
148
|
-
raise_tool_error(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
152
|
+
raise_tool_error(
|
|
153
|
+
create_error_response(
|
|
154
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
155
|
+
"Restart not confirmed",
|
|
156
|
+
details=(
|
|
157
|
+
"You must set confirm=True to restart Home Assistant. "
|
|
158
|
+
"This is a safety measure to prevent accidental restarts."
|
|
159
|
+
),
|
|
160
|
+
suggestions=[
|
|
161
|
+
"Run ha_check_config() first to validate configuration",
|
|
162
|
+
"Call ha_restart(confirm=True) to proceed with restart",
|
|
163
|
+
"Consider using ha_reload_core() for config-only changes",
|
|
164
|
+
],
|
|
165
|
+
)
|
|
166
|
+
)
|
|
161
167
|
|
|
162
168
|
restart_initiated = False
|
|
163
169
|
try:
|
|
@@ -165,15 +171,17 @@ class SystemTools:
|
|
|
165
171
|
config_result = await self._client.check_config()
|
|
166
172
|
if config_result.get("result") != "valid":
|
|
167
173
|
errors = config_result.get("errors") or []
|
|
168
|
-
raise_tool_error(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
raise_tool_error(
|
|
175
|
+
create_error_response(
|
|
176
|
+
ErrorCode.CONFIG_INVALID,
|
|
177
|
+
"Configuration is invalid - restart aborted",
|
|
178
|
+
details=(
|
|
179
|
+
"Home Assistant configuration has errors. "
|
|
180
|
+
"Fix the errors before restarting."
|
|
181
|
+
),
|
|
182
|
+
context={"config_errors": errors},
|
|
183
|
+
)
|
|
184
|
+
)
|
|
177
185
|
|
|
178
186
|
# Call the restart service - mark as initiated before the call
|
|
179
187
|
# as the connection may be closed before we get a response
|
|
@@ -199,8 +207,7 @@ class SystemTools:
|
|
|
199
207
|
# Connection errors after restart initiated are expected
|
|
200
208
|
# (HA closes connections during restart)
|
|
201
209
|
if restart_initiated and any(
|
|
202
|
-
pattern in error_msg.lower()
|
|
203
|
-
for pattern in ("connect", "closed", "504")
|
|
210
|
+
pattern in error_msg.lower() for pattern in ("connect", "closed", "504")
|
|
204
211
|
):
|
|
205
212
|
return {
|
|
206
213
|
"success": True,
|
|
@@ -272,12 +279,17 @@ class SystemTools:
|
|
|
272
279
|
target = target.lower().strip()
|
|
273
280
|
|
|
274
281
|
if target not in RELOAD_TARGETS:
|
|
275
|
-
raise_tool_error(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
282
|
+
raise_tool_error(
|
|
283
|
+
create_error_response(
|
|
284
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
285
|
+
f"Invalid reload target: {target}",
|
|
286
|
+
context={
|
|
287
|
+
"target": target,
|
|
288
|
+
"valid_targets": list(RELOAD_TARGETS.keys()),
|
|
289
|
+
},
|
|
290
|
+
suggestions=[f"Use one of: {', '.join(RELOAD_TARGETS.keys())}"],
|
|
291
|
+
)
|
|
292
|
+
)
|
|
281
293
|
|
|
282
294
|
try:
|
|
283
295
|
if target == "all":
|
|
@@ -313,11 +325,13 @@ class SystemTools:
|
|
|
313
325
|
service_info = RELOAD_TARGETS[target]
|
|
314
326
|
if service_info is None:
|
|
315
327
|
# This shouldn't happen as we check for "all" above
|
|
316
|
-
raise_tool_error(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
328
|
+
raise_tool_error(
|
|
329
|
+
create_error_response(
|
|
330
|
+
ErrorCode.INTERNAL_ERROR,
|
|
331
|
+
f"Invalid target configuration for: {target}",
|
|
332
|
+
context={"target": target},
|
|
333
|
+
)
|
|
334
|
+
)
|
|
321
335
|
domain, service = service_info
|
|
322
336
|
await self._client.call_service(domain, service, {})
|
|
323
337
|
|
|
@@ -343,7 +357,11 @@ class SystemTools:
|
|
|
343
357
|
@tool(
|
|
344
358
|
name="ha_get_system_health",
|
|
345
359
|
tags={"System", "Zigbee", "Z-Wave", "Integrations"},
|
|
346
|
-
annotations={
|
|
360
|
+
annotations={
|
|
361
|
+
"idempotentHint": True,
|
|
362
|
+
"readOnlyHint": True,
|
|
363
|
+
"title": "Get System Health (incl. ZHA/Z-Wave/integration diagnostics)",
|
|
364
|
+
},
|
|
347
365
|
)
|
|
348
366
|
@log_tool_usage
|
|
349
367
|
async def ha_get_system_health(
|
|
@@ -474,9 +492,7 @@ class SystemTools:
|
|
|
474
492
|
("zha_network", self._fetch_zha_network(ws_client, full=zha_full))
|
|
475
493
|
)
|
|
476
494
|
if want_zwave:
|
|
477
|
-
sections.append(
|
|
478
|
-
("zwave_network", self._fetch_zwave_network(ws_client))
|
|
479
|
-
)
|
|
495
|
+
sections.append(("zwave_network", self._fetch_zwave_network(ws_client)))
|
|
480
496
|
|
|
481
497
|
if sections:
|
|
482
498
|
gathered = await asyncio.gather(
|
|
@@ -641,25 +657,32 @@ class SystemTools:
|
|
|
641
657
|
verify_ssl=self._client.verify_ssl,
|
|
642
658
|
)
|
|
643
659
|
if error or ws_client is None:
|
|
644
|
-
raise_tool_error(
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
660
|
+
raise_tool_error(
|
|
661
|
+
error
|
|
662
|
+
or create_error_response(
|
|
663
|
+
ErrorCode.CONNECTION_FAILED,
|
|
664
|
+
"Failed to connect to Home Assistant WebSocket",
|
|
665
|
+
)
|
|
666
|
+
)
|
|
648
667
|
|
|
649
668
|
try:
|
|
650
669
|
_, event_response = await ws_client.send_command_with_event(
|
|
651
670
|
"system_health/info", wait_timeout=10.0
|
|
652
671
|
)
|
|
653
672
|
except TimeoutError:
|
|
654
|
-
raise_tool_error(
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
673
|
+
raise_tool_error(
|
|
674
|
+
create_error_response(
|
|
675
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
676
|
+
"Timeout waiting for system health data",
|
|
677
|
+
)
|
|
678
|
+
)
|
|
658
679
|
except Exception as e:
|
|
659
|
-
raise_tool_error(
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
680
|
+
raise_tool_error(
|
|
681
|
+
create_error_response(
|
|
682
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
683
|
+
str(e),
|
|
684
|
+
)
|
|
685
|
+
)
|
|
663
686
|
|
|
664
687
|
health_info = event_response.get("event", {})
|
|
665
688
|
component_count = len(health_info) if isinstance(health_info, dict) else 0
|
|
@@ -673,7 +696,6 @@ class SystemTools:
|
|
|
673
696
|
|
|
674
697
|
return ws_client, result
|
|
675
698
|
|
|
676
|
-
|
|
677
699
|
@staticmethod
|
|
678
700
|
async def _fetch_repairs(
|
|
679
701
|
ws_client: Any, *, include_dismissed: bool = False
|
|
@@ -703,9 +725,7 @@ class SystemTools:
|
|
|
703
725
|
else:
|
|
704
726
|
err = repairs_result.get("error") or {}
|
|
705
727
|
err_msg = (
|
|
706
|
-
err.get("message")
|
|
707
|
-
if isinstance(err, dict)
|
|
708
|
-
else str(err)
|
|
728
|
+
err.get("message") if isinstance(err, dict) else str(err)
|
|
709
729
|
) or "unknown error"
|
|
710
730
|
logger.warning(
|
|
711
731
|
"repairs/list_issues returned success=false: %s", err_msg
|
|
@@ -717,9 +737,7 @@ class SystemTools:
|
|
|
717
737
|
return repairs
|
|
718
738
|
|
|
719
739
|
@staticmethod
|
|
720
|
-
async def _fetch_zha_network(
|
|
721
|
-
ws_client: Any, *, full: bool
|
|
722
|
-
) -> dict[str, Any]:
|
|
740
|
+
async def _fetch_zha_network(ws_client: Any, *, full: bool) -> dict[str, Any]:
|
|
723
741
|
"""Fetch ZHA Zigbee network device data."""
|
|
724
742
|
ZHA_SUMMARY_LIMIT = 50
|
|
725
743
|
ZHA_FULL_LIMIT = 25
|