ha-mcp-dev 7.5.0.dev565__tar.gz → 7.5.0.dev566__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.dev565/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev566}/PKG-INFO +1 -1
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/pyproject.toml +1 -1
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_areas.py +197 -69
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_history.py +267 -147
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_search.py +561 -44
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_services.py +79 -5
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/util_helpers.py +99 -4
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/LICENSE +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/README.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/setup.cfg +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.5.0.dev565 → ha_mcp_dev-7.5.0.dev566}/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.dev566"
|
|
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"
|
|
@@ -12,7 +12,7 @@ from fastmcp.exceptions import ToolError
|
|
|
12
12
|
from fastmcp.tools import tool
|
|
13
13
|
from pydantic import Field
|
|
14
14
|
|
|
15
|
-
from ..errors import ErrorCode, create_error_response
|
|
15
|
+
from ..errors import ErrorCode, create_error_response, create_validation_error
|
|
16
16
|
from .helpers import (
|
|
17
17
|
exception_to_structured_error,
|
|
18
18
|
log_tool_usage,
|
|
@@ -20,7 +20,12 @@ from .helpers import (
|
|
|
20
20
|
register_tool_methods,
|
|
21
21
|
validate_identifier_not_empty,
|
|
22
22
|
)
|
|
23
|
-
from .util_helpers import
|
|
23
|
+
from .util_helpers import (
|
|
24
|
+
parse_string_list_param,
|
|
25
|
+
project_fields,
|
|
26
|
+
project_records,
|
|
27
|
+
result_fields_warning,
|
|
28
|
+
)
|
|
24
29
|
|
|
25
30
|
logger = logging.getLogger(__name__)
|
|
26
31
|
|
|
@@ -130,15 +135,65 @@ class AreaTools:
|
|
|
130
135
|
@tool(
|
|
131
136
|
name="ha_config_list_areas",
|
|
132
137
|
tags={"Areas & Floors"},
|
|
133
|
-
annotations={
|
|
138
|
+
annotations={
|
|
139
|
+
"idempotentHint": True,
|
|
140
|
+
"readOnlyHint": True,
|
|
141
|
+
"title": "List Areas",
|
|
142
|
+
},
|
|
134
143
|
)
|
|
135
144
|
@log_tool_usage
|
|
136
|
-
async def ha_config_list_areas(
|
|
145
|
+
async def ha_config_list_areas(
|
|
146
|
+
self,
|
|
147
|
+
fields: Annotated[
|
|
148
|
+
str | list[str] | None,
|
|
149
|
+
Field(
|
|
150
|
+
default=None,
|
|
151
|
+
description=(
|
|
152
|
+
"Return only the specified top-level response keys to reduce "
|
|
153
|
+
'response size (e.g. ["areas"]). '
|
|
154
|
+
"None = full response (default). "
|
|
155
|
+
"Available keys: success, count, areas, message."
|
|
156
|
+
),
|
|
157
|
+
),
|
|
158
|
+
] = None,
|
|
159
|
+
area_fields: Annotated[
|
|
160
|
+
str | list[str] | None,
|
|
161
|
+
Field(
|
|
162
|
+
default=None,
|
|
163
|
+
description=(
|
|
164
|
+
"Project each area record to only the specified keys. "
|
|
165
|
+
'E.g. ["area_id", "name"] returns slim area records. '
|
|
166
|
+
"None = full records (default). Unknown keys yield empty records. "
|
|
167
|
+
"Available keys: area_id, name, icon, floor_id, aliases, picture, labels."
|
|
168
|
+
),
|
|
169
|
+
),
|
|
170
|
+
] = None,
|
|
171
|
+
) -> dict[str, Any]:
|
|
137
172
|
"""
|
|
138
173
|
List all Home Assistant areas (rooms).
|
|
139
174
|
|
|
140
175
|
Returns area ID, name, icon, floor assignment, aliases, and picture URL.
|
|
141
176
|
"""
|
|
177
|
+
parsed_fields: list[str] | None = None
|
|
178
|
+
if fields is not None:
|
|
179
|
+
try:
|
|
180
|
+
parsed_fields = parse_string_list_param(
|
|
181
|
+
fields, "fields", allow_csv=True
|
|
182
|
+
)
|
|
183
|
+
except ValueError as exc:
|
|
184
|
+
raise_tool_error(create_validation_error(str(exc), parameter="fields"))
|
|
185
|
+
parsed_area_fields: list[str] | None = None
|
|
186
|
+
if area_fields is not None:
|
|
187
|
+
try:
|
|
188
|
+
parsed_area_fields = parse_string_list_param(
|
|
189
|
+
area_fields, "area_fields", allow_csv=True
|
|
190
|
+
)
|
|
191
|
+
if parsed_area_fields is not None and len(parsed_area_fields) == 0:
|
|
192
|
+
raise ValueError("area_fields must contain at least one key")
|
|
193
|
+
except ValueError as exc:
|
|
194
|
+
raise_tool_error(
|
|
195
|
+
create_validation_error(str(exc), parameter="area_fields")
|
|
196
|
+
)
|
|
142
197
|
try:
|
|
143
198
|
message: dict[str, Any] = {
|
|
144
199
|
"type": "config/area_registry/list",
|
|
@@ -148,26 +203,42 @@ class AreaTools:
|
|
|
148
203
|
|
|
149
204
|
if result.get("success"):
|
|
150
205
|
areas = result.get("result", [])
|
|
151
|
-
|
|
206
|
+
_orig_areas = areas
|
|
207
|
+
if parsed_area_fields is not None:
|
|
208
|
+
areas = project_records(areas, parsed_area_fields)
|
|
209
|
+
response: dict[str, Any] = {
|
|
152
210
|
"success": True,
|
|
153
211
|
"count": len(areas),
|
|
154
212
|
"areas": areas,
|
|
155
213
|
"message": f"Found {len(areas)} area(s)",
|
|
156
214
|
}
|
|
215
|
+
if parsed_area_fields is not None:
|
|
216
|
+
_warn = result_fields_warning(
|
|
217
|
+
_orig_areas, areas, parsed_area_fields, param_name="area_fields"
|
|
218
|
+
)
|
|
219
|
+
if _warn:
|
|
220
|
+
response.setdefault("warnings", []).append(_warn)
|
|
221
|
+
return project_fields(response, parsed_fields)
|
|
157
222
|
else:
|
|
158
|
-
raise_tool_error(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
223
|
+
raise_tool_error(
|
|
224
|
+
create_error_response(
|
|
225
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
226
|
+
result.get("error", "Failed to list areas"),
|
|
227
|
+
)
|
|
228
|
+
)
|
|
162
229
|
|
|
163
230
|
except ToolError:
|
|
164
231
|
raise
|
|
165
232
|
except Exception as e:
|
|
166
233
|
logger.error(f"Error listing areas: {e}")
|
|
167
|
-
exception_to_structured_error(
|
|
168
|
-
|
|
169
|
-
"
|
|
170
|
-
|
|
234
|
+
exception_to_structured_error(
|
|
235
|
+
e,
|
|
236
|
+
context={"operation": "list_areas"},
|
|
237
|
+
suggestions=[
|
|
238
|
+
"Check Home Assistant connection",
|
|
239
|
+
"Verify WebSocket connection is active",
|
|
240
|
+
],
|
|
241
|
+
)
|
|
171
242
|
|
|
172
243
|
# ============================================================
|
|
173
244
|
# FLOOR TOOLS
|
|
@@ -176,7 +247,11 @@ class AreaTools:
|
|
|
176
247
|
@tool(
|
|
177
248
|
name="ha_config_list_floors",
|
|
178
249
|
tags={"Areas & Floors"},
|
|
179
|
-
annotations={
|
|
250
|
+
annotations={
|
|
251
|
+
"idempotentHint": True,
|
|
252
|
+
"readOnlyHint": True,
|
|
253
|
+
"title": "List Floors",
|
|
254
|
+
},
|
|
180
255
|
)
|
|
181
256
|
@log_tool_usage
|
|
182
257
|
async def ha_config_list_floors(self) -> dict[str, Any]:
|
|
@@ -201,24 +276,34 @@ class AreaTools:
|
|
|
201
276
|
"message": f"Found {len(floors)} floor(s)",
|
|
202
277
|
}
|
|
203
278
|
else:
|
|
204
|
-
raise_tool_error(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
279
|
+
raise_tool_error(
|
|
280
|
+
create_error_response(
|
|
281
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
282
|
+
result.get("error", "Failed to list floors"),
|
|
283
|
+
)
|
|
284
|
+
)
|
|
208
285
|
|
|
209
286
|
except ToolError:
|
|
210
287
|
raise
|
|
211
288
|
except Exception as e:
|
|
212
289
|
logger.error(f"Error listing floors: {e}")
|
|
213
|
-
exception_to_structured_error(
|
|
214
|
-
|
|
215
|
-
"
|
|
216
|
-
|
|
290
|
+
exception_to_structured_error(
|
|
291
|
+
e,
|
|
292
|
+
context={"operation": "list_floors"},
|
|
293
|
+
suggestions=[
|
|
294
|
+
"Check Home Assistant connection",
|
|
295
|
+
"Verify WebSocket connection is active",
|
|
296
|
+
],
|
|
297
|
+
)
|
|
217
298
|
|
|
218
299
|
@tool(
|
|
219
300
|
name="ha_list_floors_areas",
|
|
220
301
|
tags={"Areas & Floors"},
|
|
221
|
-
annotations={
|
|
302
|
+
annotations={
|
|
303
|
+
"idempotentHint": True,
|
|
304
|
+
"readOnlyHint": True,
|
|
305
|
+
"title": "List Floors and Areas",
|
|
306
|
+
},
|
|
222
307
|
)
|
|
223
308
|
@log_tool_usage
|
|
224
309
|
async def ha_list_floors_areas(self) -> dict[str, Any]:
|
|
@@ -251,20 +336,22 @@ class AreaTools:
|
|
|
251
336
|
areas_ok = areas_result.get("success") and "result" in areas_result
|
|
252
337
|
floors_ok = floors_result.get("success") and "result" in floors_result
|
|
253
338
|
if not (areas_ok and floors_ok):
|
|
254
|
-
raise_tool_error(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
339
|
+
raise_tool_error(
|
|
340
|
+
create_error_response(
|
|
341
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
342
|
+
"Failed to retrieve area or floor registry",
|
|
343
|
+
context={
|
|
344
|
+
"areas_success": areas_result.get("success"),
|
|
345
|
+
"floors_success": floors_result.get("success"),
|
|
346
|
+
"areas_response_keys": sorted(areas_result.keys()),
|
|
347
|
+
"floors_response_keys": sorted(floors_result.keys()),
|
|
348
|
+
},
|
|
349
|
+
suggestions=[
|
|
350
|
+
"Check Home Assistant connection",
|
|
351
|
+
"Verify WebSocket connection is active",
|
|
352
|
+
],
|
|
353
|
+
)
|
|
354
|
+
)
|
|
268
355
|
|
|
269
356
|
areas = areas_result["result"]
|
|
270
357
|
floors = floors_result["result"]
|
|
@@ -361,7 +448,10 @@ class AreaTools:
|
|
|
361
448
|
@tool(
|
|
362
449
|
name="ha_set_area_or_floor",
|
|
363
450
|
tags={"Areas & Floors"},
|
|
364
|
-
annotations={
|
|
451
|
+
annotations={
|
|
452
|
+
"destructiveHint": True,
|
|
453
|
+
"title": "Create or Update Area or Floor",
|
|
454
|
+
},
|
|
365
455
|
)
|
|
366
456
|
@log_tool_usage
|
|
367
457
|
async def ha_set_area_or_floor(
|
|
@@ -439,10 +529,12 @@ class AreaTools:
|
|
|
439
529
|
try:
|
|
440
530
|
parsed_aliases = parse_string_list_param(aliases, "aliases")
|
|
441
531
|
except ValueError as e:
|
|
442
|
-
raise_tool_error(
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
532
|
+
raise_tool_error(
|
|
533
|
+
create_error_response(
|
|
534
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
535
|
+
f"Invalid aliases parameter: {e}",
|
|
536
|
+
)
|
|
537
|
+
)
|
|
446
538
|
|
|
447
539
|
# Reject cross-kind params loudly so silent intent loss can't happen
|
|
448
540
|
# (e.g., kind='floor' with picture='...' previously dropped the picture
|
|
@@ -456,15 +548,17 @@ class AreaTools:
|
|
|
456
548
|
if picture is not None:
|
|
457
549
|
cross_kind_params.append("picture")
|
|
458
550
|
if cross_kind_params:
|
|
459
|
-
raise_tool_error(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
551
|
+
raise_tool_error(
|
|
552
|
+
create_error_response(
|
|
553
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
554
|
+
f"Parameter(s) {cross_kind_params} are not valid for kind={kind!r}",
|
|
555
|
+
context={"kind": kind, "invalid_parameters": cross_kind_params},
|
|
556
|
+
suggestions=[
|
|
557
|
+
"For kind='area' use: name, id, floor_id, icon, aliases, picture",
|
|
558
|
+
"For kind='floor' use: name, id, level, icon, aliases",
|
|
559
|
+
],
|
|
560
|
+
)
|
|
561
|
+
)
|
|
468
562
|
|
|
469
563
|
# ``None`` stays the documented "create-new" sentinel; explicit
|
|
470
564
|
# empty/whitespace would silently route to the ``if id:`` create
|
|
@@ -483,7 +577,12 @@ class AreaTools:
|
|
|
483
577
|
if kind == "area":
|
|
484
578
|
if id:
|
|
485
579
|
message = self._build_area_update_message(
|
|
486
|
-
id,
|
|
580
|
+
id,
|
|
581
|
+
name,
|
|
582
|
+
floor_id,
|
|
583
|
+
icon,
|
|
584
|
+
parsed_aliases,
|
|
585
|
+
picture,
|
|
487
586
|
)
|
|
488
587
|
operation = "update"
|
|
489
588
|
else:
|
|
@@ -497,7 +596,11 @@ class AreaTools:
|
|
|
497
596
|
suggestions=["Provide a non-empty name for the new area"],
|
|
498
597
|
)
|
|
499
598
|
message = self._build_area_create_message(
|
|
500
|
-
name,
|
|
599
|
+
name,
|
|
600
|
+
floor_id,
|
|
601
|
+
icon,
|
|
602
|
+
parsed_aliases,
|
|
603
|
+
picture,
|
|
501
604
|
)
|
|
502
605
|
operation = "create"
|
|
503
606
|
result_key = "area"
|
|
@@ -505,7 +608,11 @@ class AreaTools:
|
|
|
505
608
|
else: # kind == "floor"
|
|
506
609
|
if id:
|
|
507
610
|
message = self._build_floor_update_message(
|
|
508
|
-
id,
|
|
611
|
+
id,
|
|
612
|
+
name,
|
|
613
|
+
level,
|
|
614
|
+
icon,
|
|
615
|
+
parsed_aliases,
|
|
509
616
|
)
|
|
510
617
|
operation = "update"
|
|
511
618
|
else:
|
|
@@ -517,7 +624,10 @@ class AreaTools:
|
|
|
517
624
|
suggestions=["Provide a non-empty name for the new floor"],
|
|
518
625
|
)
|
|
519
626
|
message = self._build_floor_create_message(
|
|
520
|
-
name,
|
|
627
|
+
name,
|
|
628
|
+
level,
|
|
629
|
+
icon,
|
|
630
|
+
parsed_aliases,
|
|
521
631
|
)
|
|
522
632
|
operation = "create"
|
|
523
633
|
result_key = "floor"
|
|
@@ -538,17 +648,23 @@ class AreaTools:
|
|
|
538
648
|
}
|
|
539
649
|
|
|
540
650
|
error = result.get("error", {})
|
|
541
|
-
error_msg =
|
|
651
|
+
error_msg = (
|
|
652
|
+
error.get("message", str(error))
|
|
653
|
+
if isinstance(error, dict)
|
|
654
|
+
else str(error)
|
|
655
|
+
)
|
|
542
656
|
ctx: dict[str, Any] = {"operation": operation, "kind": kind}
|
|
543
657
|
if name:
|
|
544
658
|
ctx["name"] = name
|
|
545
659
|
if id:
|
|
546
660
|
ctx[id_key] = id
|
|
547
|
-
raise_tool_error(
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
661
|
+
raise_tool_error(
|
|
662
|
+
create_error_response(
|
|
663
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
664
|
+
f"Failed to {operation} {kind}: {error_msg}",
|
|
665
|
+
context=ctx,
|
|
666
|
+
)
|
|
667
|
+
)
|
|
552
668
|
|
|
553
669
|
except ToolError:
|
|
554
670
|
raise
|
|
@@ -570,7 +686,11 @@ class AreaTools:
|
|
|
570
686
|
@tool(
|
|
571
687
|
name="ha_remove_area_or_floor",
|
|
572
688
|
tags={"Areas & Floors"},
|
|
573
|
-
annotations={
|
|
689
|
+
annotations={
|
|
690
|
+
"destructiveHint": True,
|
|
691
|
+
"idempotentHint": True,
|
|
692
|
+
"title": "Remove Area or Floor",
|
|
693
|
+
},
|
|
574
694
|
)
|
|
575
695
|
@log_tool_usage
|
|
576
696
|
async def ha_remove_area_or_floor(
|
|
@@ -581,7 +701,9 @@ class AreaTools:
|
|
|
581
701
|
],
|
|
582
702
|
id: Annotated[ # noqa: A002
|
|
583
703
|
str,
|
|
584
|
-
Field(
|
|
704
|
+
Field(
|
|
705
|
+
description="Area ID or floor ID to delete (use ha_list_floors_areas to find IDs)"
|
|
706
|
+
),
|
|
585
707
|
],
|
|
586
708
|
) -> dict[str, Any]:
|
|
587
709
|
"""Remove a Home Assistant area or floor.
|
|
@@ -618,12 +740,18 @@ class AreaTools:
|
|
|
618
740
|
}
|
|
619
741
|
|
|
620
742
|
error = result.get("error", {})
|
|
621
|
-
error_msg =
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
743
|
+
error_msg = (
|
|
744
|
+
error.get("message", str(error))
|
|
745
|
+
if isinstance(error, dict)
|
|
746
|
+
else str(error)
|
|
747
|
+
)
|
|
748
|
+
raise_tool_error(
|
|
749
|
+
create_error_response(
|
|
750
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
751
|
+
f"Failed to remove {kind}: {error_msg}",
|
|
752
|
+
context={"kind": kind, id_key: id},
|
|
753
|
+
)
|
|
754
|
+
)
|
|
627
755
|
|
|
628
756
|
except ToolError:
|
|
629
757
|
raise
|