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.
Files changed (71) hide show
  1. ha_mcp/__init__.py +51 -0
  2. ha_mcp/__main__.py +753 -0
  3. ha_mcp/_pypi_marker +0 -0
  4. ha_mcp/auth/__init__.py +10 -0
  5. ha_mcp/auth/consent_form.py +501 -0
  6. ha_mcp/auth/provider.py +786 -0
  7. ha_mcp/client/__init__.py +10 -0
  8. ha_mcp/client/rest_client.py +924 -0
  9. ha_mcp/client/websocket_client.py +656 -0
  10. ha_mcp/client/websocket_listener.py +340 -0
  11. ha_mcp/config.py +175 -0
  12. ha_mcp/errors.py +399 -0
  13. ha_mcp/py.typed +0 -0
  14. ha_mcp/resources/card_types.json +48 -0
  15. ha_mcp/resources/dashboard_guide.md +573 -0
  16. ha_mcp/server.py +189 -0
  17. ha_mcp/smoke_test.py +108 -0
  18. ha_mcp/tools/__init__.py +11 -0
  19. ha_mcp/tools/backup.py +457 -0
  20. ha_mcp/tools/device_control.py +687 -0
  21. ha_mcp/tools/enhanced.py +191 -0
  22. ha_mcp/tools/helpers.py +184 -0
  23. ha_mcp/tools/registry.py +199 -0
  24. ha_mcp/tools/smart_search.py +797 -0
  25. ha_mcp/tools/tools_addons.py +343 -0
  26. ha_mcp/tools/tools_areas.py +478 -0
  27. ha_mcp/tools/tools_blueprints.py +279 -0
  28. ha_mcp/tools/tools_bug_report.py +570 -0
  29. ha_mcp/tools/tools_calendar.py +391 -0
  30. ha_mcp/tools/tools_camera.py +149 -0
  31. ha_mcp/tools/tools_config_automations.py +508 -0
  32. ha_mcp/tools/tools_config_dashboards.py +1502 -0
  33. ha_mcp/tools/tools_config_entry_flow.py +223 -0
  34. ha_mcp/tools/tools_config_helpers.py +925 -0
  35. ha_mcp/tools/tools_config_info.py +258 -0
  36. ha_mcp/tools/tools_config_scripts.py +267 -0
  37. ha_mcp/tools/tools_entities.py +76 -0
  38. ha_mcp/tools/tools_filesystem.py +589 -0
  39. ha_mcp/tools/tools_groups.py +306 -0
  40. ha_mcp/tools/tools_hacs.py +787 -0
  41. ha_mcp/tools/tools_history.py +714 -0
  42. ha_mcp/tools/tools_integrations.py +297 -0
  43. ha_mcp/tools/tools_labels.py +713 -0
  44. ha_mcp/tools/tools_mcp_component.py +305 -0
  45. ha_mcp/tools/tools_registry.py +1115 -0
  46. ha_mcp/tools/tools_resources.py +622 -0
  47. ha_mcp/tools/tools_search.py +573 -0
  48. ha_mcp/tools/tools_service.py +211 -0
  49. ha_mcp/tools/tools_services.py +352 -0
  50. ha_mcp/tools/tools_system.py +363 -0
  51. ha_mcp/tools/tools_todo.py +530 -0
  52. ha_mcp/tools/tools_traces.py +496 -0
  53. ha_mcp/tools/tools_updates.py +476 -0
  54. ha_mcp/tools/tools_utility.py +519 -0
  55. ha_mcp/tools/tools_voice_assistant.py +345 -0
  56. ha_mcp/tools/tools_zones.py +387 -0
  57. ha_mcp/tools/util_helpers.py +199 -0
  58. ha_mcp/utils/__init__.py +30 -0
  59. ha_mcp/utils/domain_handlers.py +380 -0
  60. ha_mcp/utils/fuzzy_search.py +339 -0
  61. ha_mcp/utils/operation_manager.py +442 -0
  62. ha_mcp/utils/usage_logger.py +290 -0
  63. ha_mcp_dev-6.3.1.dev137.dist-info/METADATA +229 -0
  64. ha_mcp_dev-6.3.1.dev137.dist-info/RECORD +71 -0
  65. ha_mcp_dev-6.3.1.dev137.dist-info/WHEEL +5 -0
  66. ha_mcp_dev-6.3.1.dev137.dist-info/entry_points.txt +7 -0
  67. ha_mcp_dev-6.3.1.dev137.dist-info/licenses/LICENSE +21 -0
  68. ha_mcp_dev-6.3.1.dev137.dist-info/top_level.txt +2 -0
  69. tests/__init__.py +1 -0
  70. tests/test_constants.py +15 -0
  71. 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
+ }