ha-mcp-dev 7.2.0.dev331__py3-none-any.whl → 7.2.0.dev332__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/tools/backup.py CHANGED
@@ -51,7 +51,7 @@ def _get_backup_hint_text() -> str:
51
51
 
52
52
  async def _get_backup_password(
53
53
  ws_client: HomeAssistantWebSocketClient,
54
- ) -> tuple[str | None, dict[str, Any] | None]:
54
+ ) -> str:
55
55
  """
56
56
  Retrieve default backup password from Home Assistant configuration.
57
57
 
@@ -59,27 +59,30 @@ async def _get_backup_password(
59
59
  ws_client: Connected WebSocket client
60
60
 
61
61
  Returns:
62
- Tuple of (password, error_dict). If retrieval fails, password is None.
62
+ The backup password string.
63
+
64
+ Raises:
65
+ ToolError: If backup config cannot be retrieved or no password is configured.
63
66
  """
64
67
  backup_config = await ws_client.send_command("backup/config/info")
65
68
  if not backup_config.get("success"):
66
- return None, {
67
- "success": False,
68
- "error": "Failed to retrieve backup configuration",
69
- "details": backup_config,
70
- }
69
+ raise_tool_error(create_error_response(
70
+ ErrorCode.SERVICE_CALL_FAILED,
71
+ "Failed to retrieve backup configuration",
72
+ context={"details": backup_config},
73
+ ))
71
74
 
72
75
  config_data = backup_config.get("result", {}).get("config", {})
73
76
  default_password = config_data.get("create_backup", {}).get("password")
74
77
 
75
78
  if not default_password:
76
- return None, {
77
- "success": False,
78
- "error": "No default backup password configured in Home Assistant",
79
- "suggestion": "Configure automatic backups in Home Assistant settings to set a default password",
80
- }
79
+ raise_tool_error(create_error_response(
80
+ ErrorCode.SERVICE_CALL_FAILED,
81
+ "No default backup password configured in Home Assistant",
82
+ suggestions=["Configure automatic backups in Home Assistant settings to set a default password"],
83
+ ))
81
84
 
82
- return default_password, None
85
+ return cast(str, default_password)
83
86
 
84
87
 
85
88
  async def create_backup(
@@ -107,13 +110,8 @@ async def create_backup(
107
110
  ))
108
111
  ws_client = cast(HomeAssistantWebSocketClient, ws_client)
109
112
 
110
- # Get backup password
111
- password, error = await _get_backup_password(ws_client)
112
- if error:
113
- raise_tool_error(create_error_response(
114
- ErrorCode.SERVICE_CALL_FAILED,
115
- error.get("error", "Failed to retrieve backup password"),
116
- ))
113
+ # Get backup password (raises ToolError on failure)
114
+ password = await _get_backup_password(ws_client)
117
115
 
118
116
  # Generate backup name if not provided
119
117
  if not name:
@@ -283,12 +281,15 @@ async def restore_backup(
283
281
  safety_backup_name = f"PreRestore_Safety_{now.strftime('%Y-%m-%d_%H:%M:%S')}"
284
282
 
285
283
  # Get backup password
286
- password, error = await _get_backup_password(ws_client)
287
- if error:
284
+ try:
285
+ password = await _get_backup_password(ws_client)
286
+ except ToolError:
288
287
  # Password error - log warning but continue (restore might still work)
289
288
  logger.warning("No default password - proceeding without safety backup")
289
+ password = None
290
290
  safety_backup_id = None
291
- else:
291
+
292
+ if password is not None:
292
293
  safety_backup = await ws_client.send_command(
293
294
  "backup/generate",
294
295
  name=safety_backup_name,
@@ -421,38 +421,40 @@ class DeviceControlTools:
421
421
  }
422
422
 
423
423
  elif operation.status.value == "failed":
424
- return {
425
- "operation_id": operation_id,
426
- "status": "failed",
427
- "success": False,
428
- "entity_id": operation.entity_id,
429
- "action": operation.action,
430
- "error": operation.error_message,
431
- "duration_ms": operation.duration_ms,
432
- "suggestions": [
424
+ raise_tool_error(create_error_response(
425
+ ErrorCode.SERVICE_CALL_FAILED,
426
+ operation.error_message or "Device operation failed",
427
+ context={
428
+ "operation_id": operation_id,
429
+ "entity_id": operation.entity_id,
430
+ "action": operation.action,
431
+ "duration_ms": operation.duration_ms,
432
+ },
433
+ suggestions=[
433
434
  "Check if device is available and responding",
434
435
  "Verify device supports the requested action",
435
436
  "Check Home Assistant logs for error details",
436
437
  "Try a simpler action like toggle",
437
438
  ],
438
- }
439
+ ))
439
440
 
440
441
  elif operation.status.value == "timeout":
441
- return {
442
- "operation_id": operation_id,
443
- "status": "timeout",
444
- "success": False,
445
- "entity_id": operation.entity_id,
446
- "action": operation.action,
447
- "error": f"Operation timed out after {operation.timeout_ms}ms",
448
- "elapsed_ms": operation.elapsed_ms,
449
- "suggestions": [
442
+ raise_tool_error(create_error_response(
443
+ ErrorCode.TIMEOUT_OPERATION,
444
+ f"Operation timed out after {operation.timeout_ms}ms",
445
+ context={
446
+ "operation_id": operation_id,
447
+ "entity_id": operation.entity_id,
448
+ "action": operation.action,
449
+ "elapsed_ms": operation.elapsed_ms,
450
+ },
451
+ suggestions=[
450
452
  "Device may be slow to respond or offline",
451
453
  "Check device connectivity",
452
454
  "Try increasing timeout for slow devices",
453
455
  "Verify device is powered on",
454
456
  ],
455
- }
457
+ ))
456
458
 
457
459
  else: # pending
458
460
  return {
@@ -240,21 +240,20 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
240
240
  else str(error)
241
241
  )
242
242
  failed = dict.fromkeys(assistants, should_expose)
243
- response: dict[str, Any] = {
244
- "success": False,
245
- "error": {
246
- "code": ErrorCode.SERVICE_CALL_FAILED.value,
247
- "message": f"Exposure failed: {error_msg}",
248
- "suggestion": "Check Home Assistant connection and entity availability",
249
- },
243
+ context: dict[str, Any] = {
250
244
  "entity_id": entity_id,
251
245
  "exposure_succeeded": succeeded,
252
246
  "exposure_failed": failed,
253
247
  }
254
248
  if has_registry_updates:
255
- response["partial"] = True
256
- response["entity_entry"] = _format_entity_entry(entity_entry)
257
- return response
249
+ context["partial"] = True
250
+ context["entity_entry"] = _format_entity_entry(entity_entry)
251
+ raise_tool_error(create_error_response(
252
+ ErrorCode.SERVICE_CALL_FAILED,
253
+ f"Exposure failed: {error_msg}",
254
+ context=context,
255
+ suggestions=["Check Home Assistant connection and entity availability"],
256
+ ))
258
257
 
259
258
  # Track successful exposures
260
259
  for a in assistants:
@@ -272,21 +271,15 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
272
271
  if get_result.get("success"):
273
272
  entity_entry = get_result.get("result", {})
274
273
  else:
275
- # Return plain dict so caller can inspect exposure_succeeded
276
- return {
277
- "success": False,
278
- "error": {
279
- "code": ErrorCode.ENTITY_NOT_FOUND.value,
280
- "message": f"Entity '{entity_id}' not found in registry after applying exposure changes",
281
- "suggestion": "Use ha_search_entities() to verify the entity exists",
282
- },
283
- "entity_id": entity_id,
284
- "suggestions": [
274
+ raise_tool_error(create_error_response(
275
+ ErrorCode.ENTITY_NOT_FOUND,
276
+ f"Entity '{entity_id}' not found in registry after applying exposure changes",
277
+ context={"entity_id": entity_id, "exposure_succeeded": exposure_result},
278
+ suggestions=[
285
279
  "Verify the entity_id exists using ha_search_entities()",
286
280
  "The entity's exposure settings were likely changed, but its current state could not be confirmed.",
287
281
  ],
288
- "exposure_succeeded": exposure_result,
289
- }
282
+ ))
290
283
 
291
284
  response_data: dict[str, Any] = {
292
285
  "success": True,
@@ -831,15 +824,10 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
831
824
  if isinstance(error, dict)
832
825
  else str(error)
833
826
  )
834
- return {
835
- "success": False,
836
- "entity_id": eid,
837
- "error": error_msg,
838
- }
827
+ raise ValueError(error_msg)
839
828
 
840
829
  entry = result.get("result", {})
841
830
  return {
842
- "success": True,
843
831
  "entity_id": entry.get("entity_id"),
844
832
  "name": entry.get("name"),
845
833
  "original_name": entry.get("original_name"),
@@ -861,21 +849,13 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
861
849
  if not is_bulk:
862
850
  eid = entity_ids[0]
863
851
  logger.info(f"Getting entity registry entry for {eid}")
864
- result = await _fetch_entity(eid)
865
-
866
- if result.get("success"):
867
- return {
868
- "success": True,
869
- "entity_id": eid,
870
- "entity_entry": {
871
- k: v for k, v in result.items() if k not in ("success",)
872
- },
873
- }
874
- else:
852
+ try:
853
+ result = await _fetch_entity(eid)
854
+ except ValueError as e:
875
855
  raise_tool_error(
876
856
  create_error_response(
877
857
  ErrorCode.SERVICE_CALL_FAILED,
878
- f"Entity not found: {result.get('error', 'Unknown error')}",
858
+ f"Entity not found: {e}",
879
859
  context={"entity_id": eid},
880
860
  suggestions=[
881
861
  "Use ha_search_entities() to find valid entity IDs",
@@ -883,6 +863,11 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
883
863
  ],
884
864
  )
885
865
  )
866
+ return {
867
+ "success": True,
868
+ "entity_id": eid,
869
+ "entity_entry": result,
870
+ }
886
871
 
887
872
  # Bulk case - fetch all entities
888
873
  logger.info(
@@ -904,18 +889,8 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
904
889
  "error": str(fetch_result),
905
890
  }
906
891
  )
907
- continue
908
- if fetch_result.get("success"):
909
- entity_entries.append(
910
- {k: v for k, v in fetch_result.items() if k not in ("success",)}
911
- )
912
892
  else:
913
- errors.append(
914
- {
915
- "entity_id": eid,
916
- "error": fetch_result.get("error", "Unknown error"),
917
- }
918
- )
893
+ entity_entries.append(fetch_result)
919
894
 
920
895
  response: dict[str, Any] = {
921
896
  "success": True,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.2.0.dev331
3
+ Version: 7.2.0.dev332
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -37,7 +37,7 @@ Dynamic: license-file
37
37
  <!-- mcp-name: io.github.homeassistant-ai/ha-mcp -->
38
38
 
39
39
  <p align="center">
40
- <img src="https://img.shields.io/badge/tools-93-blue" alt="95+ Tools">
40
+ <img src="https://img.shields.io/badge/tools-91-blue" alt="95+ Tools">
41
41
  <a href="https://github.com/homeassistant-ai/ha-mcp/releases"><img src="https://img.shields.io/github/v/release/homeassistant-ai/ha-mcp" alt="Release"></a>
42
42
  <a href="https://github.com/homeassistant-ai/ha-mcp/actions/workflows/e2e-tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/homeassistant-ai/ha-mcp/e2e-tests.yml?branch=master&label=E2E%20Tests" alt="E2E Tests"></a>
43
43
  <a href="LICENSE.md"><img src="https://img.shields.io/github/license/homeassistant-ai/ha-mcp.svg" alt="License"></a>
@@ -160,7 +160,7 @@ Spend less time configuring, more time enjoying your smart home.
160
160
  <details>
161
161
  <!-- TOOLS_TABLE_START -->
162
162
 
163
- <summary><b>Complete Tool List (93 tools)</b></summary>
163
+ <summary><b>Complete Tool List (91 tools)</b></summary>
164
164
 
165
165
  | Category | Tools |
166
166
  |----------|-------|
@@ -34,9 +34,9 @@ ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/h
34
34
  ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md,sha256=RVkY-J-ZrBDDD3INHwbbkkPocukDgNg8eOuApswRlqk,8014
35
35
  ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md,sha256=rrcM8KrJ0hZw1jd4PPsVnE4rTFnsA1uA7SgltVjewkw,17856
36
36
  ha_mcp/tools/__init__.py,sha256=79ML0aicKZ5WJQn47eTPaeQntJZe1Mzt30E31_v6tXU,334
37
- ha_mcp/tools/backup.py,sha256=uFtzT9qM3FXt07egRbxs31uTB7-ZtfAVWPacoy-9FyQ,18395
37
+ ha_mcp/tools/backup.py,sha256=QN0ZhLke1ItivS0ykJV74WAtsiirPduZfhIMXW4pfJc,18372
38
38
  ha_mcp/tools/best_practice_checker.py,sha256=wyeeB5L3wPFG11VX6acg7F0fz6ISbgK-5wrUhxntIPE,14576
39
- ha_mcp/tools/device_control.py,sha256=4PoeMyV7L1C1DirDpVxgpgjyCcaI-I2bsJJCGFzgGWA,28444
39
+ ha_mcp/tools/device_control.py,sha256=8vI7LqcbNeJi-j4pA0GCUMOKPy8mD90IDYbEAy38_ok,28586
40
40
  ha_mcp/tools/enhanced.py,sha256=plvrTJmuAJ-55M0yznEq1Vv5TFDl_2FgegTYK7RaLC8,6668
41
41
  ha_mcp/tools/helpers.py,sha256=GveFEr0UPmnGwv18gHTkzPenrOOYtr8PT8EF8b4mmus,9504
42
42
  ha_mcp/tools/registry.py,sha256=LDU17zgJCgrlsv-yoBHm6TxAvkfnGoIdYsWfzLTmhGs,7718
@@ -53,7 +53,7 @@ ha_mcp/tools/tools_config_dashboards.py,sha256=3eQKQI-x7ESEBpVdIztp6YCZhWGl26Plj
53
53
  ha_mcp/tools/tools_config_entry_flow.py,sha256=AlDGCQxCWZPRTEyee3ak-KeDzlQM83AFNYAi-D7tS4I,21065
54
54
  ha_mcp/tools/tools_config_helpers.py,sha256=7CHDyCBsKkArZRDBf9hsNlszTGcW3bqIBOyyucBXahg,55094
55
55
  ha_mcp/tools/tools_config_scripts.py,sha256=92CJmlx5u2IQCrIPXnqIjpvDLvAbmAmWQ4EY3oGLA_A,16218
56
- ha_mcp/tools/tools_entities.py,sha256=aPDqXCO5EidqyIU6OTtF-X7aqNyVimwvhWm1oOpHL28,43542
56
+ ha_mcp/tools/tools_entities.py,sha256=EgnR95p8SZatCKW4oFyhbDiIVvMte81qycZuJi_sVl4,42469
57
57
  ha_mcp/tools/tools_filesystem.py,sha256=-ZEOPD7Q4_4ULoxg2Cye8vJkdCUESHUhn1-bUHi_1_g,17846
58
58
  ha_mcp/tools/tools_groups.py,sha256=OQe3BHD8L86IAov-B6tDBkEJZIFq5p8_VmpkbfYKJ0k,14283
59
59
  ha_mcp/tools/tools_hacs.py,sha256=YHDwb2YzBbJLajNrfsQC-UpRjGNbQ9jOmd2lzB0aGjk,27168
@@ -83,12 +83,12 @@ ha_mcp/utils/fuzzy_search.py,sha256=bvT1wnGVVb2q2a4GtAnXK4uSLRU8wfGZBeGVf6CQhR0,
83
83
  ha_mcp/utils/operation_manager.py,sha256=1ETI_L2TFNhnJUUJwtuH4R0s6ZP3_rscIOfdehYSmkU,14266
84
84
  ha_mcp/utils/python_sandbox.py,sha256=mVBrBR1caQksXso3voUw2YlqY2OQJDXkt3EAZpasE0M,7488
85
85
  ha_mcp/utils/usage_logger.py,sha256=ZXbr3vHV2WT7IozrEnuNCulKt3wLXDUJI1dxaBVq0kQ,9294
86
- ha_mcp_dev-7.2.0.dev331.dist-info/licenses/LICENSE,sha256=7rJXXKBJWgJF8595wk-YTxwVTEi1kQaIqyy9dh5o_oY,1062
86
+ ha_mcp_dev-7.2.0.dev332.dist-info/licenses/LICENSE,sha256=7rJXXKBJWgJF8595wk-YTxwVTEi1kQaIqyy9dh5o_oY,1062
87
87
  tests/__init__.py,sha256=YRpec-ZFYCJ48oD_7ZcNY7dB8avoTWOrZICjaM-BYJ0,39
88
88
  tests/test_constants.py,sha256=F14Pf5QMzG77RhsecaNWWaEL-B_8ykHJLIvVMcJxT8M,609
89
89
  tests/test_env_manager.py,sha256=wEYSfwmkga9IPanzVkSo03fsY77KVw71zJG5S7Kkdr8,12045
90
- ha_mcp_dev-7.2.0.dev331.dist-info/METADATA,sha256=dd9VPXxete5DamDBJGKgfc99YaDXgTw13h-PfPT-V3k,18573
91
- ha_mcp_dev-7.2.0.dev331.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
92
- ha_mcp_dev-7.2.0.dev331.dist-info/entry_points.txt,sha256=ckO8PIrfV4-YQEyjqgO8wIzcQiMFTTJNWKZLyRtFpms,292
93
- ha_mcp_dev-7.2.0.dev331.dist-info/top_level.txt,sha256=cqJLEmgh4gQBKg_vBqj0ahS4DCg4J0qBXYgZCDQ2IWs,13
94
- ha_mcp_dev-7.2.0.dev331.dist-info/RECORD,,
90
+ ha_mcp_dev-7.2.0.dev332.dist-info/METADATA,sha256=IapaC2pCw7BgQNDcf5JIyR65aCI3yHH1rcAVAvFMOSM,18573
91
+ ha_mcp_dev-7.2.0.dev332.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
92
+ ha_mcp_dev-7.2.0.dev332.dist-info/entry_points.txt,sha256=ckO8PIrfV4-YQEyjqgO8wIzcQiMFTTJNWKZLyRtFpms,292
93
+ ha_mcp_dev-7.2.0.dev332.dist-info/top_level.txt,sha256=cqJLEmgh4gQBKg_vBqj0ahS4DCg4J0qBXYgZCDQ2IWs,13
94
+ ha_mcp_dev-7.2.0.dev332.dist-info/RECORD,,