ha-mcp-dev 6.3.1.dev137__py3-none-any.whl
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/__init__.py +51 -0
- ha_mcp/__main__.py +753 -0
- ha_mcp/_pypi_marker +0 -0
- ha_mcp/auth/__init__.py +10 -0
- ha_mcp/auth/consent_form.py +501 -0
- ha_mcp/auth/provider.py +786 -0
- ha_mcp/client/__init__.py +10 -0
- ha_mcp/client/rest_client.py +924 -0
- ha_mcp/client/websocket_client.py +656 -0
- ha_mcp/client/websocket_listener.py +340 -0
- ha_mcp/config.py +175 -0
- ha_mcp/errors.py +399 -0
- ha_mcp/py.typed +0 -0
- ha_mcp/resources/card_types.json +48 -0
- ha_mcp/resources/dashboard_guide.md +573 -0
- ha_mcp/server.py +189 -0
- ha_mcp/smoke_test.py +108 -0
- ha_mcp/tools/__init__.py +11 -0
- ha_mcp/tools/backup.py +457 -0
- ha_mcp/tools/device_control.py +687 -0
- ha_mcp/tools/enhanced.py +191 -0
- ha_mcp/tools/helpers.py +184 -0
- ha_mcp/tools/registry.py +199 -0
- ha_mcp/tools/smart_search.py +797 -0
- ha_mcp/tools/tools_addons.py +343 -0
- ha_mcp/tools/tools_areas.py +478 -0
- ha_mcp/tools/tools_blueprints.py +279 -0
- ha_mcp/tools/tools_bug_report.py +570 -0
- ha_mcp/tools/tools_calendar.py +391 -0
- ha_mcp/tools/tools_camera.py +149 -0
- ha_mcp/tools/tools_config_automations.py +508 -0
- ha_mcp/tools/tools_config_dashboards.py +1502 -0
- ha_mcp/tools/tools_config_entry_flow.py +223 -0
- ha_mcp/tools/tools_config_helpers.py +925 -0
- ha_mcp/tools/tools_config_info.py +258 -0
- ha_mcp/tools/tools_config_scripts.py +267 -0
- ha_mcp/tools/tools_entities.py +76 -0
- ha_mcp/tools/tools_filesystem.py +589 -0
- ha_mcp/tools/tools_groups.py +306 -0
- ha_mcp/tools/tools_hacs.py +787 -0
- ha_mcp/tools/tools_history.py +714 -0
- ha_mcp/tools/tools_integrations.py +297 -0
- ha_mcp/tools/tools_labels.py +713 -0
- ha_mcp/tools/tools_mcp_component.py +305 -0
- ha_mcp/tools/tools_registry.py +1115 -0
- ha_mcp/tools/tools_resources.py +622 -0
- ha_mcp/tools/tools_search.py +573 -0
- ha_mcp/tools/tools_service.py +211 -0
- ha_mcp/tools/tools_services.py +352 -0
- ha_mcp/tools/tools_system.py +363 -0
- ha_mcp/tools/tools_todo.py +530 -0
- ha_mcp/tools/tools_traces.py +496 -0
- ha_mcp/tools/tools_updates.py +476 -0
- ha_mcp/tools/tools_utility.py +519 -0
- ha_mcp/tools/tools_voice_assistant.py +345 -0
- ha_mcp/tools/tools_zones.py +387 -0
- ha_mcp/tools/util_helpers.py +199 -0
- ha_mcp/utils/__init__.py +30 -0
- ha_mcp/utils/domain_handlers.py +380 -0
- ha_mcp/utils/fuzzy_search.py +339 -0
- ha_mcp/utils/operation_manager.py +442 -0
- ha_mcp/utils/usage_logger.py +290 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/METADATA +229 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/RECORD +71 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/WHEEL +5 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/entry_points.txt +7 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/licenses/LICENSE +21 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_constants.py +15 -0
- tests/test_env_manager.py +336 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Voice Assistant Exposure Management Tools for Home Assistant.
|
|
3
|
+
|
|
4
|
+
This module provides tools for:
|
|
5
|
+
- Managing entity exposure to voice assistants (Alexa, Google Home, Assist)
|
|
6
|
+
- Listing which entities are exposed to which assistants
|
|
7
|
+
- Configuring auto-exposure settings for new entities
|
|
8
|
+
|
|
9
|
+
The exposure system is separate from the entity registry and controls
|
|
10
|
+
which entities are accessible to voice assistants like Alexa, Google Assistant,
|
|
11
|
+
and the built-in Assist pipeline.
|
|
12
|
+
|
|
13
|
+
Known assistant identifiers:
|
|
14
|
+
- "conversation" - Home Assistant Assist (local voice control)
|
|
15
|
+
- "cloud.alexa" - Alexa via Nabu Casa cloud
|
|
16
|
+
- "cloud.google_assistant" - Google Assistant via Nabu Casa cloud
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Annotated, Any
|
|
21
|
+
|
|
22
|
+
from pydantic import Field
|
|
23
|
+
|
|
24
|
+
from .helpers import log_tool_usage
|
|
25
|
+
from .util_helpers import coerce_bool_param, parse_string_list_param
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
# Known voice assistant identifiers in Home Assistant
|
|
30
|
+
KNOWN_ASSISTANTS = ["conversation", "cloud.alexa", "cloud.google_assistant"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def register_voice_assistant_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
34
|
+
"""Register voice assistant exposure management tools."""
|
|
35
|
+
|
|
36
|
+
@mcp.tool(
|
|
37
|
+
annotations={
|
|
38
|
+
"destructiveHint": True,
|
|
39
|
+
"title": "Expose Entity to Voice Assistant",
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
@log_tool_usage
|
|
43
|
+
async def ha_expose_entity(
|
|
44
|
+
entity_ids: Annotated[
|
|
45
|
+
str | list[str],
|
|
46
|
+
Field(
|
|
47
|
+
description="Entity ID(s) to expose/hide. Can be a single entity ID string or a list."
|
|
48
|
+
),
|
|
49
|
+
],
|
|
50
|
+
assistants: Annotated[
|
|
51
|
+
str | list[str],
|
|
52
|
+
Field(
|
|
53
|
+
description=(
|
|
54
|
+
"Voice assistant(s) to modify. Options: "
|
|
55
|
+
"'conversation' (Assist), 'cloud.alexa', 'cloud.google_assistant'. "
|
|
56
|
+
"Can be a single assistant or list."
|
|
57
|
+
)
|
|
58
|
+
),
|
|
59
|
+
],
|
|
60
|
+
should_expose: Annotated[
|
|
61
|
+
bool | str,
|
|
62
|
+
Field(
|
|
63
|
+
description="True to expose entities to the assistants, False to hide them"
|
|
64
|
+
),
|
|
65
|
+
],
|
|
66
|
+
) -> dict[str, Any]:
|
|
67
|
+
"""
|
|
68
|
+
Expose or hide entities from voice assistants (Alexa, Google Home, Assist).
|
|
69
|
+
|
|
70
|
+
This controls which entities are accessible via voice commands through
|
|
71
|
+
Home Assistant's voice assistant integrations.
|
|
72
|
+
|
|
73
|
+
ASSISTANTS:
|
|
74
|
+
- "conversation" - Home Assistant Assist (built-in local voice)
|
|
75
|
+
- "cloud.alexa" - Amazon Alexa (requires Nabu Casa subscription)
|
|
76
|
+
- "cloud.google_assistant" - Google Assistant (requires Nabu Casa subscription)
|
|
77
|
+
|
|
78
|
+
EXAMPLES:
|
|
79
|
+
- Expose light to Alexa: ha_expose_entity("light.living_room", "cloud.alexa", True)
|
|
80
|
+
- Hide from all assistants: ha_expose_entity("switch.secret", ["conversation", "cloud.alexa", "cloud.google_assistant"], False)
|
|
81
|
+
- Expose multiple entities: ha_expose_entity(["light.bedroom", "light.kitchen"], "conversation", True)
|
|
82
|
+
|
|
83
|
+
NOTE: Some entities cannot be exposed to cloud assistants (Alexa/Google) for security reasons,
|
|
84
|
+
including sensitive domains like alarm_control_panel, lock, etc.
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
# Parse entity_ids - handle single string or list/JSON array
|
|
88
|
+
if isinstance(entity_ids, str):
|
|
89
|
+
# Try to parse as JSON first, otherwise treat as single entity_id
|
|
90
|
+
try:
|
|
91
|
+
parsed_entity_ids = parse_string_list_param(
|
|
92
|
+
entity_ids, "entity_ids"
|
|
93
|
+
)
|
|
94
|
+
except ValueError:
|
|
95
|
+
# Not valid JSON, treat as single entity_id
|
|
96
|
+
parsed_entity_ids = [entity_ids]
|
|
97
|
+
elif isinstance(entity_ids, list):
|
|
98
|
+
parsed_entity_ids = entity_ids
|
|
99
|
+
else:
|
|
100
|
+
parsed_entity_ids = None
|
|
101
|
+
|
|
102
|
+
if not parsed_entity_ids:
|
|
103
|
+
return {
|
|
104
|
+
"success": False,
|
|
105
|
+
"error": "entity_ids is required and cannot be empty",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Parse assistants - handle single string or list/JSON array
|
|
109
|
+
if isinstance(assistants, str):
|
|
110
|
+
# Try to parse as JSON first, otherwise treat as single assistant
|
|
111
|
+
try:
|
|
112
|
+
parsed_assistants = parse_string_list_param(
|
|
113
|
+
assistants, "assistants"
|
|
114
|
+
)
|
|
115
|
+
except ValueError:
|
|
116
|
+
# Not valid JSON, treat as single assistant
|
|
117
|
+
parsed_assistants = [assistants]
|
|
118
|
+
elif isinstance(assistants, list):
|
|
119
|
+
parsed_assistants = assistants
|
|
120
|
+
else:
|
|
121
|
+
parsed_assistants = None
|
|
122
|
+
|
|
123
|
+
if not parsed_assistants:
|
|
124
|
+
return {
|
|
125
|
+
"success": False,
|
|
126
|
+
"error": "assistants is required and cannot be empty",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Validate assistants
|
|
130
|
+
invalid_assistants = [
|
|
131
|
+
a for a in parsed_assistants if a not in KNOWN_ASSISTANTS
|
|
132
|
+
]
|
|
133
|
+
if invalid_assistants:
|
|
134
|
+
return {
|
|
135
|
+
"success": False,
|
|
136
|
+
"error": f"Invalid assistant(s): {invalid_assistants}",
|
|
137
|
+
"valid_assistants": KNOWN_ASSISTANTS,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Parse should_expose
|
|
141
|
+
expose = coerce_bool_param(should_expose, "should_expose")
|
|
142
|
+
if expose is None:
|
|
143
|
+
return {
|
|
144
|
+
"success": False,
|
|
145
|
+
"error": "should_expose is required (true/false)",
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Build WebSocket message
|
|
149
|
+
message: dict[str, Any] = {
|
|
150
|
+
"type": "homeassistant/expose_entity",
|
|
151
|
+
"assistants": parsed_assistants,
|
|
152
|
+
"entity_ids": parsed_entity_ids,
|
|
153
|
+
"should_expose": expose,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
action = "Exposing" if expose else "Hiding"
|
|
157
|
+
logger.info(
|
|
158
|
+
f"{action} {len(parsed_entity_ids)} entity(ies) "
|
|
159
|
+
f"{'to' if expose else 'from'} {parsed_assistants}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
result = await client.send_websocket_message(message)
|
|
163
|
+
|
|
164
|
+
if result.get("success"):
|
|
165
|
+
return {
|
|
166
|
+
"success": True,
|
|
167
|
+
"entity_ids": parsed_entity_ids,
|
|
168
|
+
"assistants": parsed_assistants,
|
|
169
|
+
"exposed": expose,
|
|
170
|
+
"message": (
|
|
171
|
+
f"Successfully {'exposed' if expose else 'hidden'} "
|
|
172
|
+
f"{len(parsed_entity_ids)} entity(ies) "
|
|
173
|
+
f"{'to' if expose else 'from'} {len(parsed_assistants)} assistant(s)"
|
|
174
|
+
),
|
|
175
|
+
}
|
|
176
|
+
else:
|
|
177
|
+
error = result.get("error", {})
|
|
178
|
+
error_msg = (
|
|
179
|
+
error.get("message", str(error))
|
|
180
|
+
if isinstance(error, dict)
|
|
181
|
+
else str(error)
|
|
182
|
+
)
|
|
183
|
+
return {
|
|
184
|
+
"success": False,
|
|
185
|
+
"error": f"Failed to update exposure: {error_msg}",
|
|
186
|
+
"entity_ids": parsed_entity_ids,
|
|
187
|
+
"assistants": parsed_assistants,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
except ValueError as e:
|
|
191
|
+
return {"success": False, "error": str(e)}
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.error(f"Error updating entity exposure: {e}")
|
|
194
|
+
return {
|
|
195
|
+
"success": False,
|
|
196
|
+
"error": f"Failed to update entity exposure: {str(e)}",
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@mcp.tool(
|
|
200
|
+
annotations={
|
|
201
|
+
"idempotentHint": True,
|
|
202
|
+
"readOnlyHint": True,
|
|
203
|
+
"title": "Get Entity Exposure",
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
@log_tool_usage
|
|
207
|
+
async def ha_get_entity_exposure(
|
|
208
|
+
entity_id: Annotated[
|
|
209
|
+
str | None,
|
|
210
|
+
Field(
|
|
211
|
+
description="Entity ID to check exposure settings for. "
|
|
212
|
+
"If omitted, lists all entities with exposure settings.",
|
|
213
|
+
default=None,
|
|
214
|
+
),
|
|
215
|
+
] = None,
|
|
216
|
+
assistant: Annotated[
|
|
217
|
+
str | None,
|
|
218
|
+
Field(
|
|
219
|
+
description=(
|
|
220
|
+
"Filter by assistant: 'conversation', 'cloud.alexa', or "
|
|
221
|
+
"'cloud.google_assistant'. If not specified, returns all."
|
|
222
|
+
),
|
|
223
|
+
default=None,
|
|
224
|
+
),
|
|
225
|
+
] = None,
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
"""
|
|
228
|
+
Get entity exposure settings - list all or get settings for a specific entity.
|
|
229
|
+
|
|
230
|
+
Without an entity_id: Lists all entities and their exposure status to
|
|
231
|
+
voice assistants (Alexa, Google Assistant, Assist).
|
|
232
|
+
|
|
233
|
+
With an entity_id: Returns which voice assistants the specific entity
|
|
234
|
+
is exposed to.
|
|
235
|
+
|
|
236
|
+
EXAMPLES:
|
|
237
|
+
- List all exposures: ha_get_entity_exposure()
|
|
238
|
+
- Filter by assistant: ha_get_entity_exposure(assistant="cloud.alexa")
|
|
239
|
+
- Get specific entity: ha_get_entity_exposure(entity_id="light.living_room")
|
|
240
|
+
|
|
241
|
+
RETURNS (when listing):
|
|
242
|
+
- exposed_entities: Dict mapping entity_ids to their exposure status
|
|
243
|
+
- summary: Count of entities exposed to each assistant
|
|
244
|
+
|
|
245
|
+
RETURNS (when getting specific entity):
|
|
246
|
+
- exposed_to: Dict of assistant -> True/False for each assistant
|
|
247
|
+
- is_exposed_anywhere: True if exposed to at least one assistant
|
|
248
|
+
"""
|
|
249
|
+
try:
|
|
250
|
+
# Validate assistant filter if provided
|
|
251
|
+
if assistant and assistant not in KNOWN_ASSISTANTS:
|
|
252
|
+
return {
|
|
253
|
+
"success": False,
|
|
254
|
+
"error": f"Invalid assistant: {assistant}",
|
|
255
|
+
"valid_assistants": KNOWN_ASSISTANTS,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
message: dict[str, Any] = {"type": "homeassistant/expose_entity/list"}
|
|
259
|
+
|
|
260
|
+
result = await client.send_websocket_message(message)
|
|
261
|
+
|
|
262
|
+
if not result.get("success"):
|
|
263
|
+
error = result.get("error", {})
|
|
264
|
+
error_msg = (
|
|
265
|
+
error.get("message", str(error))
|
|
266
|
+
if isinstance(error, dict)
|
|
267
|
+
else str(error)
|
|
268
|
+
)
|
|
269
|
+
return {
|
|
270
|
+
"success": False,
|
|
271
|
+
"error": f"Failed to get exposure settings: {error_msg}",
|
|
272
|
+
"entity_id": entity_id,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
exposed_entities = result.get("result", {}).get("exposed_entities", {})
|
|
276
|
+
|
|
277
|
+
# If entity_id provided, return specific entity exposure
|
|
278
|
+
if entity_id is not None:
|
|
279
|
+
entity_settings = exposed_entities.get(entity_id, {})
|
|
280
|
+
|
|
281
|
+
# Check if entity is exposed to any assistant
|
|
282
|
+
is_exposed = any(entity_settings.get(asst) for asst in KNOWN_ASSISTANTS)
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
"success": True,
|
|
286
|
+
"entity_id": entity_id,
|
|
287
|
+
"exposed_to": {
|
|
288
|
+
asst: entity_settings.get(asst, False)
|
|
289
|
+
for asst in KNOWN_ASSISTANTS
|
|
290
|
+
},
|
|
291
|
+
"is_exposed_anywhere": is_exposed,
|
|
292
|
+
"has_custom_settings": entity_id in exposed_entities,
|
|
293
|
+
"note": (
|
|
294
|
+
"If has_custom_settings is False, the entity uses default exposure settings"
|
|
295
|
+
if entity_id not in exposed_entities
|
|
296
|
+
else None
|
|
297
|
+
),
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# List mode - return all exposed entities with optional assistant filter
|
|
301
|
+
filtered = exposed_entities
|
|
302
|
+
if assistant:
|
|
303
|
+
# Filter to only show entities exposed to this assistant
|
|
304
|
+
filtered = {
|
|
305
|
+
eid: settings
|
|
306
|
+
for eid, settings in filtered.items()
|
|
307
|
+
if settings.get(assistant)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# Build summary
|
|
311
|
+
summary = {
|
|
312
|
+
"conversation": 0,
|
|
313
|
+
"cloud.alexa": 0,
|
|
314
|
+
"cloud.google_assistant": 0,
|
|
315
|
+
}
|
|
316
|
+
for settings in filtered.values():
|
|
317
|
+
for asst in KNOWN_ASSISTANTS:
|
|
318
|
+
if settings.get(asst):
|
|
319
|
+
summary[asst] += 1
|
|
320
|
+
|
|
321
|
+
# Build filters_applied dict
|
|
322
|
+
filters_applied: dict[str, Any] = {}
|
|
323
|
+
if assistant:
|
|
324
|
+
filters_applied["assistant"] = assistant
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
"success": True,
|
|
328
|
+
"exposed_entities": filtered,
|
|
329
|
+
"count": len(filtered),
|
|
330
|
+
"total_entities_with_settings": len(exposed_entities),
|
|
331
|
+
"summary": (
|
|
332
|
+
summary
|
|
333
|
+
if not assistant
|
|
334
|
+
else {assistant: summary.get(assistant, 0)}
|
|
335
|
+
),
|
|
336
|
+
"filters_applied": filters_applied,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.error(f"Error getting entity exposure: {e}")
|
|
341
|
+
return {
|
|
342
|
+
"success": False,
|
|
343
|
+
"error": f"Failed to get entity exposure: {str(e)}",
|
|
344
|
+
"entity_id": entity_id,
|
|
345
|
+
}
|