ha-mcp-dev 7.4.1.dev440__tar.gz → 7.4.1.dev441__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ha_mcp_dev-7.4.1.dev440/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev441}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/pyproject.toml +1 -1
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_entities.py +277 -74
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/setup.cfg +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/tests/test_env_manager.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.4.1.
|
|
7
|
+
version = "7.4.1.dev441"
|
|
8
8
|
description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13,<3.14"
|
|
@@ -38,9 +38,30 @@ def _format_entity_entry(entry: dict[str, Any]) -> dict[str, Any]:
|
|
|
38
38
|
"aliases": entry.get("aliases", []),
|
|
39
39
|
"labels": entry.get("labels", []),
|
|
40
40
|
"categories": entry.get("categories", {}),
|
|
41
|
+
"device_class": entry.get("device_class"),
|
|
42
|
+
"original_device_class": entry.get("original_device_class"),
|
|
43
|
+
"options": entry.get("options", {}),
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
|
|
47
|
+
def _extract_ws_error(result: dict[str, Any]) -> str:
|
|
48
|
+
"""Pull a user-readable message out of a failed WebSocket response.
|
|
49
|
+
|
|
50
|
+
Falls back to a static placeholder + warning log when HA returns an
|
|
51
|
+
empty or malformed error envelope, so the user-facing message never
|
|
52
|
+
degrades to literal "{}".
|
|
53
|
+
"""
|
|
54
|
+
error = result.get("error")
|
|
55
|
+
if isinstance(error, dict):
|
|
56
|
+
msg = error.get("message")
|
|
57
|
+
if isinstance(msg, str) and msg:
|
|
58
|
+
return msg
|
|
59
|
+
elif isinstance(error, str) and error:
|
|
60
|
+
return error
|
|
61
|
+
logger.warning("HA WS response had no usable error detail: %r", result)
|
|
62
|
+
return "no error detail returned by Home Assistant"
|
|
63
|
+
|
|
64
|
+
|
|
44
65
|
def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
45
66
|
"""Register entity management tools with the MCP server."""
|
|
46
67
|
|
|
@@ -52,13 +73,7 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
52
73
|
}
|
|
53
74
|
result = await client.send_websocket_message(get_msg)
|
|
54
75
|
if not result.get("success"):
|
|
55
|
-
|
|
56
|
-
error_msg = (
|
|
57
|
-
error.get("message", str(error))
|
|
58
|
-
if isinstance(error, dict)
|
|
59
|
-
else str(error)
|
|
60
|
-
)
|
|
61
|
-
return None, error_msg
|
|
76
|
+
return None, _extract_ws_error(result)
|
|
62
77
|
return result.get("result", {}).get("labels", []), None
|
|
63
78
|
|
|
64
79
|
async def _update_single_entity(
|
|
@@ -75,6 +90,8 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
75
90
|
parsed_expose_to: dict[str, bool] | None,
|
|
76
91
|
new_entity_id: str | None = None,
|
|
77
92
|
new_device_name: str | None = None,
|
|
93
|
+
device_class: str | None = None,
|
|
94
|
+
parsed_options: dict[str, dict[str, Any]] | None = None,
|
|
78
95
|
) -> dict[str, Any]:
|
|
79
96
|
"""Update a single entity. Returns the response dict."""
|
|
80
97
|
# For add/remove operations, we need to fetch current labels first
|
|
@@ -120,6 +137,17 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
120
137
|
message["icon"] = icon if icon else None
|
|
121
138
|
updates_made.append(f"icon='{icon}'" if icon else "icon cleared")
|
|
122
139
|
|
|
140
|
+
if device_class is not None:
|
|
141
|
+
# Treat whitespace-only as the documented "clear" sentinel so
|
|
142
|
+
# accidental spaces don't reach HA as a literal validation error.
|
|
143
|
+
normalized_device_class = device_class.strip() or None
|
|
144
|
+
message["device_class"] = normalized_device_class
|
|
145
|
+
updates_made.append(
|
|
146
|
+
f"device_class='{normalized_device_class}'"
|
|
147
|
+
if normalized_device_class
|
|
148
|
+
else "device_class cleared"
|
|
149
|
+
)
|
|
150
|
+
|
|
123
151
|
if enabled is not None:
|
|
124
152
|
try:
|
|
125
153
|
enabled_bool = coerce_bool_param(enabled, "enabled")
|
|
@@ -203,13 +231,16 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
203
231
|
if new_device_name is not None:
|
|
204
232
|
updates_made.append(f"device_name -> {new_device_name}")
|
|
205
233
|
|
|
206
|
-
|
|
234
|
+
# parsed_options entries are appended to updates_made AFTER each per-domain
|
|
235
|
+
# WS call succeeds, so the response never falsely claims an unwritten domain
|
|
236
|
+
# was updated. Empty-input check below treats them as "pending" updates.
|
|
237
|
+
if not updates_made and not parsed_options:
|
|
207
238
|
raise_tool_error(
|
|
208
239
|
create_error_response(
|
|
209
240
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
210
241
|
"No updates specified",
|
|
211
242
|
suggestions=[
|
|
212
|
-
"Provide at least one of: area_id, name, icon, enabled, hidden, aliases, categories, labels, expose_to, new_entity_id, or new_device_name"
|
|
243
|
+
"Provide at least one of: area_id, name, icon, device_class, enabled, hidden, aliases, categories, labels, options, expose_to, new_entity_id, or new_device_name"
|
|
213
244
|
],
|
|
214
245
|
)
|
|
215
246
|
)
|
|
@@ -231,25 +262,24 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
231
262
|
result = await client.send_websocket_message(message)
|
|
232
263
|
|
|
233
264
|
if not result.get("success"):
|
|
234
|
-
|
|
235
|
-
error_msg = (
|
|
236
|
-
error.get("message", str(error))
|
|
237
|
-
if isinstance(error, dict)
|
|
238
|
-
else str(error)
|
|
239
|
-
)
|
|
265
|
+
error_msg = _extract_ws_error(result)
|
|
240
266
|
suggestions = [
|
|
241
267
|
"Verify the entity_id exists using ha_search_entities()",
|
|
242
268
|
]
|
|
243
269
|
if new_entity_id is not None:
|
|
244
|
-
suggestions.extend(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
270
|
+
suggestions.extend(
|
|
271
|
+
[
|
|
272
|
+
"Check that the new entity_id doesn't already exist",
|
|
273
|
+
"Ensure the entity has a unique_id (some legacy entities cannot be renamed)",
|
|
274
|
+
]
|
|
275
|
+
)
|
|
248
276
|
else:
|
|
249
|
-
suggestions.extend(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
277
|
+
suggestions.extend(
|
|
278
|
+
[
|
|
279
|
+
"Check that area_id exists if specified",
|
|
280
|
+
"Some entities may not support all update options",
|
|
281
|
+
]
|
|
282
|
+
)
|
|
253
283
|
raise_tool_error(
|
|
254
284
|
create_error_response(
|
|
255
285
|
ErrorCode.SERVICE_CALL_FAILED,
|
|
@@ -265,6 +295,64 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
265
295
|
if new_entity_id:
|
|
266
296
|
entity_id = new_entity_id
|
|
267
297
|
|
|
298
|
+
# Per-domain options updates: HA's WS schema requires `options_domain`
|
|
299
|
+
# and `options` to be sent paired one domain per call (the API takes a
|
|
300
|
+
# single domain's sub-dict). An agent-supplied {domain: {...}, ...} is
|
|
301
|
+
# therefore split into one registry update per domain.
|
|
302
|
+
options_succeeded: dict[str, dict[str, Any]] = {}
|
|
303
|
+
if parsed_options:
|
|
304
|
+
for opts_domain, opts_sub in parsed_options.items():
|
|
305
|
+
opts_msg: dict[str, Any] = {
|
|
306
|
+
"type": "config/entity_registry/update",
|
|
307
|
+
"entity_id": entity_id,
|
|
308
|
+
"options_domain": opts_domain,
|
|
309
|
+
"options": opts_sub,
|
|
310
|
+
}
|
|
311
|
+
opts_result = await client.send_websocket_message(opts_msg)
|
|
312
|
+
if not opts_result.get("success"):
|
|
313
|
+
err_msg = _extract_ws_error(opts_result)
|
|
314
|
+
partial = bool(options_succeeded) or has_registry_updates
|
|
315
|
+
msg_prefix = (
|
|
316
|
+
"Partially updated entity; failed updating options for"
|
|
317
|
+
if partial
|
|
318
|
+
else "Failed to update options for"
|
|
319
|
+
)
|
|
320
|
+
# `options_succeeded` is the structured retriable form
|
|
321
|
+
# (agent can re-feed it minus the failing domain).
|
|
322
|
+
# `updates_applied` is the human-readable prose list
|
|
323
|
+
# including non-options updates (name=, icon=, etc.).
|
|
324
|
+
# Both are surfaced — they serve different consumers.
|
|
325
|
+
options_failure_context: dict[str, Any] = {
|
|
326
|
+
"entity_id": entity_id,
|
|
327
|
+
"options_domain": opts_domain,
|
|
328
|
+
"partial": partial,
|
|
329
|
+
"options_succeeded": options_succeeded,
|
|
330
|
+
"updates_applied": list(updates_made),
|
|
331
|
+
}
|
|
332
|
+
# Only include entity_entry when something actually mutated;
|
|
333
|
+
# _format_entity_entry({}) returns an all-None stub that's
|
|
334
|
+
# indistinguishable from "entity has nothing set". Mirrors
|
|
335
|
+
# the expose_to failure path below.
|
|
336
|
+
if partial:
|
|
337
|
+
options_failure_context["entity_entry"] = _format_entity_entry(
|
|
338
|
+
entity_entry
|
|
339
|
+
)
|
|
340
|
+
raise_tool_error(
|
|
341
|
+
create_error_response(
|
|
342
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
343
|
+
f"{msg_prefix} domain '{opts_domain}': {err_msg}",
|
|
344
|
+
context=options_failure_context,
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
# HA returns the cumulative entity_entry on each per-domain
|
|
348
|
+
# call, so last-call-wins reassignment leaves the final loop
|
|
349
|
+
# iteration carrying the full state.
|
|
350
|
+
entity_entry = opts_result.get("result", {}).get(
|
|
351
|
+
"entity_entry", entity_entry
|
|
352
|
+
)
|
|
353
|
+
options_succeeded[opts_domain] = opts_sub
|
|
354
|
+
updates_made.append(f"options[{opts_domain}]={opts_sub}")
|
|
355
|
+
|
|
268
356
|
# Handle new_device_name — rename the associated device
|
|
269
357
|
# Normalize empty string to None (no-op, don't clear device name)
|
|
270
358
|
if new_device_name is not None and not new_device_name.strip():
|
|
@@ -281,12 +369,18 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
281
369
|
if get_result.get("success"):
|
|
282
370
|
entity_entry = get_result.get("result", {})
|
|
283
371
|
else:
|
|
284
|
-
logger.warning(
|
|
372
|
+
logger.warning(
|
|
373
|
+
"Entity registry lookup failed for %s: %s",
|
|
374
|
+
entity_id,
|
|
375
|
+
_extract_ws_error(get_result),
|
|
376
|
+
)
|
|
285
377
|
device_rename_result = {
|
|
286
378
|
"warning": "Entity registry lookup failed — could not determine device. Retry may succeed.",
|
|
287
379
|
}
|
|
288
380
|
|
|
289
|
-
device_id =
|
|
381
|
+
device_id = (
|
|
382
|
+
entity_entry.get("device_id") if not device_rename_result else None
|
|
383
|
+
)
|
|
290
384
|
if not device_id:
|
|
291
385
|
device_rename_result = {
|
|
292
386
|
"warning": "Entity has no associated device — device rename skipped",
|
|
@@ -302,7 +396,7 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
302
396
|
device_rename_result = {"success": True, "device_id": device_id}
|
|
303
397
|
else:
|
|
304
398
|
device_rename_result = {
|
|
305
|
-
"warning": f"Entity updated but device rename failed: {device_result
|
|
399
|
+
"warning": f"Entity updated but device rename failed: {_extract_ws_error(device_result)}",
|
|
306
400
|
"device_id": device_id,
|
|
307
401
|
}
|
|
308
402
|
|
|
@@ -336,27 +430,43 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
336
430
|
expose_result = await client.send_websocket_message(expose_msg)
|
|
337
431
|
|
|
338
432
|
if not expose_result.get("success"):
|
|
339
|
-
|
|
340
|
-
error_msg = (
|
|
341
|
-
error.get("message", str(error))
|
|
342
|
-
if isinstance(error, dict)
|
|
343
|
-
else str(error)
|
|
344
|
-
)
|
|
433
|
+
error_msg = _extract_ws_error(expose_result)
|
|
345
434
|
failed = dict.fromkeys(assistants, should_expose)
|
|
435
|
+
# `partial` must reflect every prior mutation in the function:
|
|
436
|
+
# main registry update, per-domain options, device rename, and
|
|
437
|
+
# any expose_to batch (e.g. expose_true) that ran before this
|
|
438
|
+
# one (expose_false) failed. Anything truthy in those means
|
|
439
|
+
# the registry already moved.
|
|
440
|
+
prior_mutation = (
|
|
441
|
+
has_registry_updates
|
|
442
|
+
or bool(options_succeeded)
|
|
443
|
+
or bool(succeeded)
|
|
444
|
+
or bool(
|
|
445
|
+
device_rename_result and device_rename_result.get("success")
|
|
446
|
+
)
|
|
447
|
+
)
|
|
346
448
|
context: dict[str, Any] = {
|
|
347
449
|
"entity_id": entity_id,
|
|
348
450
|
"exposure_succeeded": succeeded,
|
|
349
451
|
"exposure_failed": failed,
|
|
350
452
|
}
|
|
351
|
-
if
|
|
453
|
+
if prior_mutation:
|
|
352
454
|
context["partial"] = True
|
|
353
455
|
context["entity_entry"] = _format_entity_entry(entity_entry)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
456
|
+
if options_succeeded:
|
|
457
|
+
context["options_succeeded"] = options_succeeded
|
|
458
|
+
if device_rename_result and device_rename_result.get("success"):
|
|
459
|
+
context["device_rename_succeeded"] = True
|
|
460
|
+
raise_tool_error(
|
|
461
|
+
create_error_response(
|
|
462
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
463
|
+
f"Exposure failed: {error_msg}",
|
|
464
|
+
context=context,
|
|
465
|
+
suggestions=[
|
|
466
|
+
"Check Home Assistant connection and entity availability"
|
|
467
|
+
],
|
|
468
|
+
)
|
|
469
|
+
)
|
|
360
470
|
|
|
361
471
|
# Track successful exposures
|
|
362
472
|
for a in assistants:
|
|
@@ -374,15 +484,20 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
374
484
|
if get_result.get("success"):
|
|
375
485
|
entity_entry = get_result.get("result", {})
|
|
376
486
|
else:
|
|
377
|
-
raise_tool_error(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
487
|
+
raise_tool_error(
|
|
488
|
+
create_error_response(
|
|
489
|
+
ErrorCode.ENTITY_NOT_FOUND,
|
|
490
|
+
f"Entity '{entity_id}' not found in registry after applying exposure changes",
|
|
491
|
+
context={
|
|
492
|
+
"entity_id": entity_id,
|
|
493
|
+
"exposure_succeeded": exposure_result,
|
|
494
|
+
},
|
|
495
|
+
suggestions=[
|
|
496
|
+
"Verify the entity_id exists using ha_search_entities()",
|
|
497
|
+
"The entity's exposure settings were likely changed, but its current state could not be confirmed.",
|
|
498
|
+
],
|
|
499
|
+
)
|
|
500
|
+
)
|
|
386
501
|
|
|
387
502
|
response_data: dict[str, Any] = {
|
|
388
503
|
"success": True,
|
|
@@ -427,7 +542,7 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
427
542
|
entity_id: Annotated[
|
|
428
543
|
str | list[str],
|
|
429
544
|
Field(
|
|
430
|
-
description="Entity ID or list of entity IDs to update. Bulk operations (list) only support labels and
|
|
545
|
+
description="Entity ID or list of entity IDs to update. Bulk operations (list) only support labels, expose_to, and categories parameters."
|
|
431
546
|
),
|
|
432
547
|
],
|
|
433
548
|
area_id: Annotated[
|
|
@@ -451,6 +566,37 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
451
566
|
default=None,
|
|
452
567
|
),
|
|
453
568
|
] = None,
|
|
569
|
+
device_class: Annotated[
|
|
570
|
+
str | None,
|
|
571
|
+
Field(
|
|
572
|
+
description=(
|
|
573
|
+
"Override the entity's display device class — what the HA UI's "
|
|
574
|
+
"'Show As' dropdown writes. Use empty string '' to clear the "
|
|
575
|
+
"override and fall back to the integration default. None (the "
|
|
576
|
+
"default) means 'no change' — pass an explicit '' to clear. "
|
|
577
|
+
"Single entity only. Examples: 'window', 'door', 'motion' for "
|
|
578
|
+
"binary_sensor; 'temperature', 'humidity' for sensor."
|
|
579
|
+
),
|
|
580
|
+
default=None,
|
|
581
|
+
),
|
|
582
|
+
] = None,
|
|
583
|
+
options: Annotated[
|
|
584
|
+
str | dict[str, dict[str, Any]] | None,
|
|
585
|
+
Field(
|
|
586
|
+
description=(
|
|
587
|
+
"Per-domain entity registry options (e.g. sensor 'display_precision', "
|
|
588
|
+
"weather 'forecast_type'). Pass a dict mapping domain to a sub-dict, "
|
|
589
|
+
'e.g. {"sensor": {"display_precision": 2}}. JSON-string form also accepted. '
|
|
590
|
+
"Multiple domains are sent as separate registry updates. "
|
|
591
|
+
"For 'Show As' use the dedicated `device_class` parameter — that is "
|
|
592
|
+
"what the HA UI Show As dropdown writes. Voice-assistant exposure is "
|
|
593
|
+
"stored under `options.<assistant>.should_expose` but must be managed "
|
|
594
|
+
"via the dedicated `expose_to` parameter, not this options dict. "
|
|
595
|
+
"Single entity only."
|
|
596
|
+
),
|
|
597
|
+
default=None,
|
|
598
|
+
),
|
|
599
|
+
] = None,
|
|
454
600
|
enabled: Annotated[
|
|
455
601
|
bool | str | None,
|
|
456
602
|
Field(
|
|
@@ -540,18 +686,30 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
540
686
|
"""Update entity properties in the entity registry.
|
|
541
687
|
|
|
542
688
|
Allows modifying entity metadata such as area assignment, display name,
|
|
543
|
-
icon,
|
|
544
|
-
|
|
689
|
+
icon, "Show As" device class override, per-domain registry options,
|
|
690
|
+
enabled/disabled state, visibility, aliases, labels, voice assistant
|
|
691
|
+
exposure, and entity_id rename in a single call.
|
|
545
692
|
|
|
546
693
|
BULK OPERATIONS:
|
|
547
694
|
When entity_id is a list, only labels, expose_to, and categories parameters are supported.
|
|
548
|
-
Other parameters (area_id, name, icon, enabled, hidden, aliases, new_entity_id, new_device_name) require single entity.
|
|
695
|
+
Other parameters (area_id, name, icon, device_class, options, enabled, hidden, aliases, new_entity_id, new_device_name) require single entity.
|
|
549
696
|
|
|
550
697
|
LABEL OPERATIONS:
|
|
551
698
|
- label_operation="set" (default): Replace all labels with the provided list. Use [] to clear.
|
|
552
699
|
- label_operation="add": Add labels to existing ones without removing any.
|
|
553
700
|
- label_operation="remove": Remove specified labels from the entity.
|
|
554
701
|
|
|
702
|
+
SHOW AS / DEVICE CLASS:
|
|
703
|
+
device_class overrides the entity's display device class — equivalent to the
|
|
704
|
+
HA UI's "Show As" dropdown. Use empty string '' to clear. Applies instantly,
|
|
705
|
+
no reload needed.
|
|
706
|
+
|
|
707
|
+
REGISTRY OPTIONS:
|
|
708
|
+
options carries per-domain registry options (sensor display_precision,
|
|
709
|
+
weather forecast_type, etc). Pass {domain: {key: value}}; multi-domain
|
|
710
|
+
dicts are sent as separate registry updates because HA's WS schema
|
|
711
|
+
requires options_domain + options to be paired one domain at a time.
|
|
712
|
+
|
|
555
713
|
ENTITY ID RENAME:
|
|
556
714
|
Use new_entity_id to change an entity's ID (e.g., sensor.old -> sensor.new).
|
|
557
715
|
Domain must match. Voice exposure settings are preserved automatically.
|
|
@@ -576,6 +734,9 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
576
734
|
Single entity:
|
|
577
735
|
- Assign to area: ha_set_entity("sensor.temp", area_id="living_room")
|
|
578
736
|
- Rename display name: ha_set_entity("sensor.temp", name="Living Room Temperature")
|
|
737
|
+
- Set Show As: ha_set_entity("binary_sensor.zone_10", device_class="window")
|
|
738
|
+
- Clear Show As: ha_set_entity("binary_sensor.zone_10", device_class="")
|
|
739
|
+
- Set sensor precision: ha_set_entity("sensor.power", options={"sensor": {"display_precision": 2}})
|
|
579
740
|
- Rename entity_id: ha_set_entity("light.old_name", new_entity_id="light.new_name")
|
|
580
741
|
- Rename entity and device: ha_set_entity("light.old", new_entity_id="light.new", new_device_name="New Lamp")
|
|
581
742
|
- Rename entity_id with friendly name: ha_set_entity("sensor.old", new_entity_id="sensor.new", name="New Name")
|
|
@@ -638,6 +799,8 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
638
799
|
"area_id": area_id,
|
|
639
800
|
"name": name,
|
|
640
801
|
"icon": icon,
|
|
802
|
+
"device_class": device_class,
|
|
803
|
+
"options": options,
|
|
641
804
|
"enabled": enabled,
|
|
642
805
|
"hidden": hidden,
|
|
643
806
|
"aliases": aliases,
|
|
@@ -655,7 +818,7 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
655
818
|
f"Bulk operations (multiple entity_ids) only support categories, labels, and expose_to. "
|
|
656
819
|
f"Single-entity parameters provided: {non_null_single_params}",
|
|
657
820
|
suggestions=[
|
|
658
|
-
"Use a single entity_id for area_id, name, icon, enabled, hidden, or aliases",
|
|
821
|
+
"Use a single entity_id for area_id, name, icon, device_class, options, enabled, hidden, or aliases",
|
|
659
822
|
"Or remove single-entity parameters to use bulk categories/labels/expose_to",
|
|
660
823
|
],
|
|
661
824
|
)
|
|
@@ -747,6 +910,51 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
747
910
|
)
|
|
748
911
|
)
|
|
749
912
|
|
|
913
|
+
parsed_options: dict[str, dict[str, Any]] | None = None
|
|
914
|
+
if options is not None:
|
|
915
|
+
try:
|
|
916
|
+
parsed_opts = parse_json_param(options, "options")
|
|
917
|
+
except ValueError as e:
|
|
918
|
+
raise_tool_error(
|
|
919
|
+
create_error_response(
|
|
920
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
921
|
+
f"Invalid options parameter: {e}",
|
|
922
|
+
)
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
if not isinstance(parsed_opts, dict):
|
|
926
|
+
raise_tool_error(
|
|
927
|
+
create_error_response(
|
|
928
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
929
|
+
f"options must be a dict mapping domain to a sub-dict "
|
|
930
|
+
f"(got {type(parsed_opts).__name__}), "
|
|
931
|
+
'e.g. {"sensor": {"display_precision": 2}}',
|
|
932
|
+
)
|
|
933
|
+
)
|
|
934
|
+
if not parsed_opts:
|
|
935
|
+
raise_tool_error(
|
|
936
|
+
create_error_response(
|
|
937
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
938
|
+
"options cannot be an empty dict — pass at least one "
|
|
939
|
+
'domain entry, e.g. {"sensor": {"display_precision": 2}}, '
|
|
940
|
+
"or omit the parameter entirely.",
|
|
941
|
+
)
|
|
942
|
+
)
|
|
943
|
+
bad_subs = [
|
|
944
|
+
f"{k!r}: {type(v).__name__}"
|
|
945
|
+
for k, v in parsed_opts.items()
|
|
946
|
+
if not isinstance(v, dict)
|
|
947
|
+
]
|
|
948
|
+
if bad_subs:
|
|
949
|
+
raise_tool_error(
|
|
950
|
+
create_error_response(
|
|
951
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
952
|
+
"options sub-values must be dicts, got non-dict for: "
|
|
953
|
+
f"{', '.join(bad_subs)}",
|
|
954
|
+
)
|
|
955
|
+
)
|
|
956
|
+
parsed_options = parsed_opts
|
|
957
|
+
|
|
750
958
|
# Parse and validate expose_to parameter
|
|
751
959
|
parsed_expose_to: dict[str, bool] | None = None
|
|
752
960
|
if expose_to is not None:
|
|
@@ -819,6 +1027,8 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
819
1027
|
parsed_expose_to,
|
|
820
1028
|
new_entity_id=new_entity_id,
|
|
821
1029
|
new_device_name=new_device_name,
|
|
1030
|
+
device_class=device_class,
|
|
1031
|
+
parsed_options=parsed_options,
|
|
822
1032
|
)
|
|
823
1033
|
|
|
824
1034
|
# Bulk case - process each entity
|
|
@@ -856,7 +1066,10 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
856
1066
|
"error": str(result),
|
|
857
1067
|
}
|
|
858
1068
|
)
|
|
859
|
-
|
|
1069
|
+
else:
|
|
1070
|
+
# _update_single_entity always returns success-shape or
|
|
1071
|
+
# raises ToolError (caught above as BaseException), so the
|
|
1072
|
+
# `result.get("success") is False` branch is unreachable.
|
|
860
1073
|
succeeded.append(
|
|
861
1074
|
{
|
|
862
1075
|
"entity_id": eid,
|
|
@@ -864,13 +1077,6 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
864
1077
|
"updates": result.get("updates"),
|
|
865
1078
|
}
|
|
866
1079
|
)
|
|
867
|
-
else:
|
|
868
|
-
failed.append(
|
|
869
|
-
{
|
|
870
|
-
"entity_id": eid,
|
|
871
|
-
"error": result.get("error", "Unknown error"),
|
|
872
|
-
}
|
|
873
|
-
)
|
|
874
1080
|
|
|
875
1081
|
response: dict[str, Any] = {
|
|
876
1082
|
"success": len(failed) == 0,
|
|
@@ -937,6 +1143,11 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
937
1143
|
- aliases: Voice assistant aliases
|
|
938
1144
|
- labels: Assigned label IDs
|
|
939
1145
|
- categories: Category assignments (dict mapping scope to category_id)
|
|
1146
|
+
- device_class: User "Show As" override (null = use original_device_class)
|
|
1147
|
+
- original_device_class: Default device class from the integration
|
|
1148
|
+
- options: Per-domain registry options (e.g. sensor display_precision).
|
|
1149
|
+
Voice-assistant exposure is also stored here but should be set/cleared
|
|
1150
|
+
via the ha_set_entity(expose_to=...) parameter, not the options dict.
|
|
940
1151
|
- platform: Integration platform (e.g., "hue", "zwave_js")
|
|
941
1152
|
- device_id: Associated device ID (null if standalone)
|
|
942
1153
|
- unique_id: Integration's unique identifier
|
|
@@ -983,13 +1194,7 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
983
1194
|
result = await client.send_websocket_message(message)
|
|
984
1195
|
|
|
985
1196
|
if not result.get("success"):
|
|
986
|
-
|
|
987
|
-
error_msg = (
|
|
988
|
-
error.get("message", str(error))
|
|
989
|
-
if isinstance(error, dict)
|
|
990
|
-
else str(error)
|
|
991
|
-
)
|
|
992
|
-
raise ValueError(error_msg)
|
|
1197
|
+
raise ValueError(_extract_ws_error(result))
|
|
993
1198
|
|
|
994
1199
|
entry = result.get("result", {})
|
|
995
1200
|
return {
|
|
@@ -1005,6 +1210,9 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
1005
1210
|
"aliases": entry.get("aliases", []),
|
|
1006
1211
|
"labels": entry.get("labels", []),
|
|
1007
1212
|
"categories": entry.get("categories", {}),
|
|
1213
|
+
"device_class": entry.get("device_class"),
|
|
1214
|
+
"original_device_class": entry.get("original_device_class"),
|
|
1215
|
+
"options": entry.get("options", {}),
|
|
1008
1216
|
"platform": entry.get("platform"),
|
|
1009
1217
|
"device_id": entry.get("device_id"),
|
|
1010
1218
|
"unique_id": entry.get("unique_id"),
|
|
@@ -1131,12 +1339,7 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
1131
1339
|
)
|
|
1132
1340
|
|
|
1133
1341
|
if not result.get("success"):
|
|
1134
|
-
|
|
1135
|
-
error_msg = (
|
|
1136
|
-
error.get("message", str(error))
|
|
1137
|
-
if isinstance(error, dict)
|
|
1138
|
-
else str(error)
|
|
1139
|
-
)
|
|
1342
|
+
error_msg = _extract_ws_error(result)
|
|
1140
1343
|
if "not found" in error_msg.lower():
|
|
1141
1344
|
raise_tool_error(
|
|
1142
1345
|
create_error_response(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/best_practice_checker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_voice_assistant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/transforms/categorized_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/kill_signal_diagnostics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev440 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|