ha-mcp-dev 7.4.1.dev449__tar.gz → 7.4.1.dev451__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.
Files changed (108) hide show
  1. {ha_mcp_dev-7.4.1.dev449/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev451}/PKG-INFO +4 -4
  2. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/README.md +3 -3
  3. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/pyproject.toml +1 -1
  4. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_addons.py +10 -7
  5. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_areas.py +179 -251
  6. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_code.py +2 -2
  7. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_config_automations.py +4 -7
  8. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_config_dashboards.py +4 -7
  9. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_config_scripts.py +4 -7
  10. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/transforms/categorized_search.py +2 -2
  11. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/utils/python_sandbox.py +182 -22
  12. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451/src/ha_mcp_dev.egg-info}/PKG-INFO +4 -4
  13. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/LICENSE +0 -0
  14. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/MANIFEST.in +0 -0
  15. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/setup.cfg +0 -0
  16. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/__init__.py +0 -0
  17. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/__main__.py +0 -0
  18. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/_pypi_marker +0 -0
  19. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/_version.py +0 -0
  20. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/auth/__init__.py +0 -0
  21. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/auth/consent_form.py +0 -0
  22. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/auth/provider.py +0 -0
  23. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/client/__init__.py +0 -0
  24. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/client/rest_client.py +0 -0
  25. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/client/websocket_client.py +0 -0
  26. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/client/websocket_listener.py +0 -0
  27. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/config.py +0 -0
  28. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/errors.py +0 -0
  29. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/py.typed +0 -0
  30. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  31. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  32. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  33. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  34. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  35. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  36. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  37. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  38. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  39. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  40. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  41. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  42. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  43. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  44. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  45. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  46. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  47. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  48. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  49. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  50. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/server.py +0 -0
  51. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/settings_ui.py +0 -0
  52. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/smoke_test.py +0 -0
  53. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/__init__.py +0 -0
  54. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/backup.py +0 -0
  55. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  56. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/device_control.py +0 -0
  57. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/enhanced.py +0 -0
  58. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/helpers.py +0 -0
  59. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/reference_validator.py +0 -0
  60. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/registry.py +0 -0
  61. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/smart_search.py +0 -0
  62. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  63. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  64. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_calendar.py +0 -0
  65. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_camera.py +0 -0
  66. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_categories.py +0 -0
  67. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  68. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  69. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_energy.py +0 -0
  70. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_entities.py +0 -0
  71. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  72. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_groups.py +0 -0
  73. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_hacs.py +0 -0
  74. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_history.py +0 -0
  75. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_integrations.py +0 -0
  76. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_labels.py +0 -0
  77. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  78. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_registry.py +0 -0
  79. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_resources.py +0 -0
  80. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_search.py +0 -0
  81. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_service.py +0 -0
  82. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_services.py +0 -0
  83. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_system.py +0 -0
  84. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_todo.py +0 -0
  85. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_traces.py +0 -0
  86. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_updates.py +0 -0
  87. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_utility.py +0 -0
  88. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  89. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  90. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/tools_zones.py +0 -0
  91. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/tools/util_helpers.py +0 -0
  92. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/transforms/__init__.py +0 -0
  93. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/utils/__init__.py +0 -0
  94. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/utils/config_hash.py +0 -0
  95. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/utils/data_paths.py +0 -0
  96. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/utils/domain_handlers.py +0 -0
  97. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  98. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  99. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/utils/operation_manager.py +0 -0
  100. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp/utils/usage_logger.py +0 -0
  101. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  102. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  103. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  104. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  105. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  106. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/tests/__init__.py +0 -0
  107. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/tests/test_constants.py +0 -0
  108. {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev451}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.4.1.dev449
3
+ Version: 7.4.1.dev451
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
@@ -38,7 +38,7 @@ Dynamic: license-file
38
38
  <!-- mcp-name: io.github.homeassistant-ai/ha-mcp -->
39
39
 
40
40
  <p align="center">
41
- <img src="https://img.shields.io/badge/tools-87-blue" alt="95+ Tools">
41
+ <img src="https://img.shields.io/badge/tools-85-blue" alt="95+ Tools">
42
42
  <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>
43
43
  <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>
44
44
  <a href="LICENSE.md"><img src="https://img.shields.io/github/license/homeassistant-ai/ha-mcp.svg" alt="License"></a>
@@ -181,12 +181,12 @@ Spend less time configuring, more time enjoying your smart home.
181
181
  <details>
182
182
  <!-- TOOLS_TABLE_START -->
183
183
 
184
- <summary><b>Complete Tool List (87 tools)</b></summary>
184
+ <summary><b>Complete Tool List (85 tools)</b></summary>
185
185
 
186
186
  | Category | Tools |
187
187
  |----------|-------|
188
188
  | **Add-ons** | `ha_get_addon`, `ha_manage_addon` |
189
- | **Areas & Floors** | `ha_config_list_areas`, `ha_config_list_floors`, `ha_config_remove_area`, `ha_config_remove_floor`, `ha_config_set_area`, `ha_config_set_floor`, `ha_list_floors_areas` |
189
+ | **Areas & Floors** | `ha_config_list_areas`, `ha_config_list_floors`, `ha_list_floors_areas`, `ha_remove_area_or_floor`, `ha_set_area_or_floor` |
190
190
  | **Automations** | `ha_config_get_automation`, `ha_config_remove_automation`, `ha_config_set_automation` |
191
191
  | **Blueprints** | `ha_get_blueprint`, `ha_import_blueprint` |
192
192
  | **Calendar** | `ha_config_get_calendar_events`, `ha_config_remove_calendar_event`, `ha_config_set_calendar_event` |
@@ -8,7 +8,7 @@
8
8
  <!-- mcp-name: io.github.homeassistant-ai/ha-mcp -->
9
9
 
10
10
  <p align="center">
11
- <img src="https://img.shields.io/badge/tools-87-blue" alt="95+ Tools">
11
+ <img src="https://img.shields.io/badge/tools-85-blue" alt="95+ Tools">
12
12
  <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>
13
13
  <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>
14
14
  <a href="LICENSE.md"><img src="https://img.shields.io/github/license/homeassistant-ai/ha-mcp.svg" alt="License"></a>
@@ -151,12 +151,12 @@ Spend less time configuring, more time enjoying your smart home.
151
151
  <details>
152
152
  <!-- TOOLS_TABLE_START -->
153
153
 
154
- <summary><b>Complete Tool List (87 tools)</b></summary>
154
+ <summary><b>Complete Tool List (85 tools)</b></summary>
155
155
 
156
156
  | Category | Tools |
157
157
  |----------|-------|
158
158
  | **Add-ons** | `ha_get_addon`, `ha_manage_addon` |
159
- | **Areas & Floors** | `ha_config_list_areas`, `ha_config_list_floors`, `ha_config_remove_area`, `ha_config_remove_floor`, `ha_config_set_area`, `ha_config_set_floor`, `ha_list_floors_areas` |
159
+ | **Areas & Floors** | `ha_config_list_areas`, `ha_config_list_floors`, `ha_list_floors_areas`, `ha_remove_area_or_floor`, `ha_set_area_or_floor` |
160
160
  | **Automations** | `ha_config_get_automation`, `ha_config_remove_automation`, `ha_config_set_automation` |
161
161
  | **Blueprints** | `ha_get_blueprint`, `ha_import_blueprint` |
162
162
  | **Calendar** | `ha_config_get_calendar_events`, `ha_config_remove_calendar_event`, `ha_config_set_calendar_event` |
@@ -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.dev449"
7
+ version = "7.4.1.dev451"
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"
@@ -28,7 +28,11 @@ from ..errors import (
28
28
  create_error_response,
29
29
  create_validation_error,
30
30
  )
31
- from ..utils.python_sandbox import PythonSandboxError, safe_execute_expression
31
+ from ..utils.python_sandbox import (
32
+ PythonSandboxError,
33
+ format_sandbox_error,
34
+ safe_execute_expression,
35
+ )
32
36
  from .helpers import (
33
37
  exception_to_structured_error,
34
38
  get_connected_ws_client,
@@ -179,16 +183,15 @@ def _apply_response_transform(response: Any, expr: str) -> Any:
179
183
  try:
180
184
  return safe_execute_expression(expr, {"response": response}, "response")
181
185
  except PythonSandboxError as e:
186
+ message, suggestions = format_sandbox_error(
187
+ e, expr, variable_name="response"
188
+ )
182
189
  raise_tool_error(
183
190
  create_error_response(
184
191
  ErrorCode.VALIDATION_FAILED,
185
- f"python_transform failed: {e!s}",
192
+ message,
186
193
  context={"expression_preview": expr[:200]},
187
- suggestions=[
188
- "Operate on the `response` variable (in-place or reassign)",
189
- "Allowed: dict/list access, assignment, loops, "
190
- "comprehensions, whitelisted str/list/dict methods",
191
- ],
194
+ suggestions=suggestions,
192
195
  )
193
196
  )
194
197
 
@@ -6,7 +6,7 @@ Home Assistant areas and floors - essential organizational features for smart ho
6
6
  """
7
7
 
8
8
  import logging
9
- from typing import Annotated, Any
9
+ from typing import Annotated, Any, Literal
10
10
 
11
11
  from fastmcp.exceptions import ToolError
12
12
  from fastmcp.tools import tool
@@ -168,186 +168,6 @@ class AreaTools:
168
168
  "Verify WebSocket connection is active",
169
169
  ])
170
170
 
171
- @tool(
172
- name="ha_config_set_area",
173
- tags={"Areas & Floors"},
174
- annotations={"destructiveHint": True, "title": "Create or Update Area"},
175
- )
176
- @log_tool_usage
177
- async def ha_config_set_area(
178
- self,
179
- name: Annotated[
180
- str | None,
181
- Field(
182
- description="Name for the area (required for create, optional for update, e.g., 'Living Room', 'Kitchen')",
183
- default=None,
184
- ),
185
- ] = None,
186
- area_id: Annotated[
187
- str | None,
188
- Field(
189
- description="Area ID to update (omit to create new area, use ha_config_list_areas to find IDs)",
190
- default=None,
191
- ),
192
- ] = None,
193
- floor_id: Annotated[
194
- str | None,
195
- Field(
196
- description="Floor ID to assign this area to (use ha_config_list_floors to find IDs, empty string to remove)",
197
- default=None,
198
- ),
199
- ] = None,
200
- icon: Annotated[
201
- str | None,
202
- Field(
203
- description="Material Design Icon (e.g., 'mdi:sofa', 'mdi:bed', empty string to remove)",
204
- default=None,
205
- ),
206
- ] = None,
207
- aliases: Annotated[
208
- str | list[str] | None,
209
- Field(
210
- description="Alternative names for voice assistant recognition (e.g., ['lounge', 'family room'], empty list to clear)",
211
- default=None,
212
- ),
213
- ] = None,
214
- picture: Annotated[
215
- str | None,
216
- Field(
217
- description="URL to a picture representing the area (empty string to remove)",
218
- default=None,
219
- ),
220
- ] = None,
221
- ) -> dict[str, Any]:
222
- """
223
- Create or update a Home Assistant area (room).
224
-
225
- Areas organize entities by physical location for room-based control.
226
-
227
- Create: provide name only.
228
- Update: provide area_id (from ha_config_list_areas) plus any fields to change.
229
-
230
- EXAMPLES:
231
- ha_config_set_area(name="Kitchen")
232
- ha_config_set_area(name="Living Room", icon="mdi:sofa")
233
- ha_config_set_area(area_id="kitchen", name="Kitchen Renamed", floor_id="ground_floor")
234
- """
235
- try:
236
- # Parse aliases if provided as string
237
- try:
238
- parsed_aliases = parse_string_list_param(aliases, "aliases")
239
- except ValueError as e:
240
- raise_tool_error(create_error_response(
241
- ErrorCode.VALIDATION_INVALID_PARAMETER,
242
- f"Invalid aliases parameter: {e}",
243
- ))
244
-
245
- # Determine if this is a create or update operation
246
- if area_id:
247
- message = self._build_area_update_message(
248
- area_id, name, floor_id, icon, parsed_aliases, picture,
249
- )
250
- operation = "update"
251
- else:
252
- if not name:
253
- raise_tool_error(create_error_response(
254
- ErrorCode.VALIDATION_MISSING_PARAMETER,
255
- "name is required when creating a new area",
256
- context={"operation": "create_area"},
257
- suggestions=["Provide a name for the new area"],
258
- ))
259
- message = self._build_area_create_message(
260
- name, floor_id, icon, parsed_aliases, picture,
261
- )
262
- operation = "create"
263
-
264
- result = await self._client.send_websocket_message(message)
265
-
266
- if result.get("success"):
267
- area_data = result.get("result", {})
268
- area_name = name or area_data.get("name", area_id)
269
- return {
270
- "success": True,
271
- "area": area_data,
272
- "area_id": area_data.get("area_id", area_id),
273
- "message": f"Successfully {operation}d area: {area_name}",
274
- }
275
-
276
- error = result.get("error", {})
277
- error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
278
- ctx: dict[str, Any] = {"operation": operation}
279
- if name:
280
- ctx["name"] = name
281
- if area_id:
282
- ctx["area_id"] = area_id
283
- raise_tool_error(create_error_response(
284
- ErrorCode.SERVICE_CALL_FAILED,
285
- f"Failed to {operation} area: {error_msg}",
286
- context=ctx,
287
- ))
288
-
289
- except ToolError:
290
- raise
291
- except Exception as e:
292
- logger.error(f"Error {operation} area {name!r}: {e}")
293
- exception_to_structured_error(e, context={"operation": operation, "name": name, "area_id": area_id}, suggestions=[
294
- "Check Home Assistant connection",
295
- "For create: Verify the name is unique",
296
- "For update: Verify the area_id exists using ha_config_list_areas()",
297
- "If assigning to a floor, verify floor_id exists",
298
- ])
299
-
300
- @tool(
301
- name="ha_config_remove_area",
302
- tags={"Areas & Floors"},
303
- annotations={"destructiveHint": True, "idempotentHint": True, "title": "Remove Area"},
304
- )
305
- @log_tool_usage
306
- async def ha_config_remove_area(
307
- self,
308
- area_id: Annotated[
309
- str,
310
- Field(description="Area ID to delete (use ha_config_list_areas to find IDs)"),
311
- ],
312
- ) -> dict[str, Any]:
313
- """
314
- Delete a Home Assistant area.
315
-
316
- Entities and devices in the area are not deleted, just unassigned.
317
- May break automations referencing this area.
318
- """
319
- try:
320
- message: dict[str, Any] = {
321
- "type": "config/area_registry/delete",
322
- "area_id": area_id,
323
- }
324
-
325
- result = await self._client.send_websocket_message(message)
326
-
327
- if result.get("success"):
328
- return {
329
- "success": True,
330
- "area_id": area_id,
331
- "message": f"Successfully deleted area: {area_id}",
332
- }
333
- else:
334
- error = result.get("error", {})
335
- error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
336
- raise_tool_error(create_error_response(
337
- ErrorCode.SERVICE_CALL_FAILED,
338
- f"Failed to delete area: {error_msg}",
339
- context={"area_id": area_id},
340
- ))
341
-
342
- except ToolError:
343
- raise
344
- except Exception as e:
345
- logger.error(f"Error removing area {area_id!r}: {e}")
346
- exception_to_structured_error(e, context={"area_id": area_id}, suggestions=[
347
- "Check Home Assistant connection",
348
- "Verify the area_id exists using ha_config_list_areas()",
349
- ])
350
-
351
171
  # ============================================================
352
172
  # FLOOR TOOLS
353
173
  # ============================================================
@@ -533,58 +353,88 @@ class AreaTools:
533
353
  ],
534
354
  )
535
355
 
356
+ # ============================================================
357
+ # COMBINED SET / REMOVE
358
+ # ============================================================
359
+
536
360
  @tool(
537
- name="ha_config_set_floor",
361
+ name="ha_set_area_or_floor",
538
362
  tags={"Areas & Floors"},
539
- annotations={"destructiveHint": True, "title": "Create or Update Floor"},
363
+ annotations={"destructiveHint": True, "title": "Create or Update Area or Floor"},
540
364
  )
541
365
  @log_tool_usage
542
- async def ha_config_set_floor(
366
+ async def ha_set_area_or_floor(
543
367
  self,
368
+ kind: Annotated[
369
+ Literal["area", "floor"],
370
+ Field(
371
+ description="Which registry to operate on: 'area' for rooms, 'floor' for building levels",
372
+ ),
373
+ ],
544
374
  name: Annotated[
545
375
  str | None,
546
376
  Field(
547
- description="Name for the floor (required for create, optional for update, e.g., 'Ground Floor', 'Basement')",
377
+ description="Name (required when creating; optional when updating, e.g., 'Living Room', 'Ground Floor')",
378
+ default=None,
379
+ ),
380
+ ] = None,
381
+ id: Annotated[ # noqa: A002
382
+ str | None,
383
+ Field(
384
+ description="Existing area_id or floor_id to update (omit to create a new entry; use ha_list_floors_areas to find IDs)",
548
385
  default=None,
549
386
  ),
550
387
  ] = None,
551
388
  floor_id: Annotated[
552
389
  str | None,
553
390
  Field(
554
- description="Floor ID to update (omit to create new floor, use ha_config_list_floors to find IDs)",
391
+ description="Floor assignment when kind='area' (use empty string to clear). Only valid when kind='area'.",
555
392
  default=None,
556
393
  ),
557
394
  ] = None,
558
395
  level: Annotated[
559
396
  int | None,
560
397
  Field(
561
- description="Numeric level for ordering (0=ground, 1=first, -1=basement, etc.)",
398
+ description="Numeric level when kind='floor' (0=ground, 1=first, -1=basement). Only valid when kind='floor'.",
562
399
  default=None,
563
400
  ),
564
401
  ] = None,
565
402
  icon: Annotated[
566
403
  str | None,
567
404
  Field(
568
- description="Material Design Icon (e.g., 'mdi:home-floor-1', 'mdi:home-floor-b', empty string to remove)",
405
+ description="Material Design Icon (e.g., 'mdi:sofa', 'mdi:home-floor-1', empty string to remove)",
569
406
  default=None,
570
407
  ),
571
408
  ] = None,
572
409
  aliases: Annotated[
573
410
  str | list[str] | None,
574
411
  Field(
575
- description="Alternative names for voice assistant recognition (e.g., ['downstairs', 'main level'], empty list to clear)",
412
+ description="Alternative names for voice assistant recognition (e.g., ['lounge'], empty list to clear)",
413
+ default=None,
414
+ ),
415
+ ] = None,
416
+ picture: Annotated[
417
+ str | None,
418
+ Field(
419
+ description="Picture URL when kind='area' (empty string to remove). Only valid when kind='area'.",
576
420
  default=None,
577
421
  ),
578
422
  ] = None,
579
423
  ) -> dict[str, Any]:
580
- """
581
- Create or update a Home Assistant floor.
424
+ """Create or update a Home Assistant area or floor.
425
+
426
+ Pass kind='area' (with optional floor_id, picture) or kind='floor' (with optional level).
427
+ Provide name only to create a new entry; provide id to update an existing one.
428
+ Cross-kind parameters (e.g., picture under kind='floor') are rejected with VALIDATION_INVALID_PARAMETER.
582
429
 
583
- Provide name only to create a new floor. Provide floor_id to update existing.
584
- Floors organize areas into vertical levels for building-wide control.
430
+ EXAMPLES:
431
+ ha_set_area_or_floor(kind="area", name="Kitchen")
432
+ ha_set_area_or_floor(kind="area", id="kitchen", floor_id="ground_floor")
433
+ ha_set_area_or_floor(kind="floor", name="Basement", level=-1)
434
+ ha_set_area_or_floor(kind="floor", id="ground_floor", level=0)
585
435
  """
436
+ operation = "create"
586
437
  try:
587
- # Parse aliases if provided as string
588
438
  try:
589
439
  parsed_aliases = parse_string_list_param(aliases, "aliases")
590
440
  except ValueError as e:
@@ -593,83 +443,156 @@ class AreaTools:
593
443
  f"Invalid aliases parameter: {e}",
594
444
  ))
595
445
 
596
- # Determine if this is a create or update operation
597
- if floor_id:
598
- message = self._build_floor_update_message(
599
- floor_id, name, level, icon, parsed_aliases,
600
- )
601
- operation = "update"
602
- else:
603
- if not name:
604
- raise_tool_error(create_error_response(
605
- ErrorCode.VALIDATION_MISSING_PARAMETER,
606
- "name is required when creating a new floor",
607
- context={"operation": "create_floor"},
608
- suggestions=["Provide a name for the new floor"],
609
- ))
610
- message = self._build_floor_create_message(
611
- name, level, icon, parsed_aliases,
612
- )
613
- operation = "create"
446
+ # Reject cross-kind params loudly so silent intent loss can't happen
447
+ # (e.g., kind='floor' with picture='...' previously dropped the picture
448
+ # without a diagnostic).
449
+ cross_kind_params: list[str] = []
450
+ if kind == "area" and level is not None:
451
+ cross_kind_params.append("level")
452
+ elif kind == "floor":
453
+ if floor_id is not None:
454
+ cross_kind_params.append("floor_id")
455
+ if picture is not None:
456
+ cross_kind_params.append("picture")
457
+ if cross_kind_params:
458
+ raise_tool_error(create_error_response(
459
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
460
+ f"Parameter(s) {cross_kind_params} are not valid for kind={kind!r}",
461
+ context={"kind": kind, "invalid_parameters": cross_kind_params},
462
+ suggestions=[
463
+ "For kind='area' use: name, id, floor_id, icon, aliases, picture",
464
+ "For kind='floor' use: name, id, level, icon, aliases",
465
+ ],
466
+ ))
467
+
468
+ # Reject empty-string id explicitly. `if id:` below treats it as
469
+ # falsy and would silently route to the create branch — destructive
470
+ # if the caller intended an update.
471
+ if id == "":
472
+ raise_tool_error(create_error_response(
473
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
474
+ "id must be a non-empty string when provided (omit to create)",
475
+ context={"kind": kind},
476
+ suggestions=[
477
+ "Omit id entirely to create a new entry",
478
+ "Pass a real area_id/floor_id to update an existing entry",
479
+ ],
480
+ ))
481
+
482
+ if kind == "area":
483
+ if id:
484
+ message = self._build_area_update_message(
485
+ id, name, floor_id, icon, parsed_aliases, picture,
486
+ )
487
+ operation = "update"
488
+ else:
489
+ if not name:
490
+ raise_tool_error(create_error_response(
491
+ ErrorCode.VALIDATION_MISSING_PARAMETER,
492
+ "name is required when creating a new area",
493
+ context={"operation": "create_area"},
494
+ suggestions=["Provide a name for the new area"],
495
+ ))
496
+ message = self._build_area_create_message(
497
+ name, floor_id, icon, parsed_aliases, picture,
498
+ )
499
+ operation = "create"
500
+ result_key = "area"
501
+ id_key = "area_id"
502
+ else: # kind == "floor"
503
+ if id:
504
+ message = self._build_floor_update_message(
505
+ id, name, level, icon, parsed_aliases,
506
+ )
507
+ operation = "update"
508
+ else:
509
+ if not name:
510
+ raise_tool_error(create_error_response(
511
+ ErrorCode.VALIDATION_MISSING_PARAMETER,
512
+ "name is required when creating a new floor",
513
+ context={"operation": "create_floor"},
514
+ suggestions=["Provide a name for the new floor"],
515
+ ))
516
+ message = self._build_floor_create_message(
517
+ name, level, icon, parsed_aliases,
518
+ )
519
+ operation = "create"
520
+ result_key = "floor"
521
+ id_key = "floor_id"
614
522
 
615
523
  result = await self._client.send_websocket_message(message)
616
524
 
617
525
  if result.get("success"):
618
- floor_data = result.get("result", {})
619
- floor_name = name or floor_data.get("name", floor_id)
526
+ data = result.get("result", {})
527
+ returned_id = data.get(id_key, id)
528
+ display_name = name or data.get("name", returned_id)
620
529
  return {
621
530
  "success": True,
622
- "floor": floor_data,
623
- "floor_id": floor_data.get("floor_id", floor_id),
624
- "message": f"Successfully {operation}d floor: {floor_name}",
531
+ result_key: data,
532
+ id_key: returned_id,
533
+ "kind": kind,
534
+ "message": f"Successfully {operation}d {kind}: {display_name}",
625
535
  }
626
536
 
627
537
  error = result.get("error", {})
628
538
  error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
629
- ctx: dict[str, Any] = {"operation": operation}
539
+ ctx: dict[str, Any] = {"operation": operation, "kind": kind}
630
540
  if name:
631
541
  ctx["name"] = name
632
- if floor_id:
633
- ctx["floor_id"] = floor_id
542
+ if id:
543
+ ctx[id_key] = id
634
544
  raise_tool_error(create_error_response(
635
545
  ErrorCode.SERVICE_CALL_FAILED,
636
- f"Failed to {operation} floor: {error_msg}",
546
+ f"Failed to {operation} {kind}: {error_msg}",
637
547
  context=ctx,
638
548
  ))
639
549
 
640
550
  except ToolError:
641
551
  raise
642
552
  except Exception as e:
643
- logger.error(f"Error {operation} floor {name!r}: {e}")
644
- exception_to_structured_error(e, context={"operation": operation, "name": name, "floor_id": floor_id}, suggestions=[
553
+ logger.error(f"Error {operation} {kind} {name!r}: {e}")
554
+ suggestions = [
645
555
  "Check Home Assistant connection",
646
556
  "For create: Verify the name is unique",
647
- "For update: Verify the floor_id exists using ha_config_list_floors()",
648
- ])
557
+ f"For update: Verify the {kind} id exists using ha_list_floors_areas()",
558
+ ]
559
+ if kind == "area":
560
+ suggestions.append("If assigning to a floor, verify floor_id exists")
561
+ exception_to_structured_error(
562
+ e,
563
+ context={"operation": operation, "kind": kind, "name": name, "id": id},
564
+ suggestions=suggestions,
565
+ )
649
566
 
650
567
  @tool(
651
- name="ha_config_remove_floor",
568
+ name="ha_remove_area_or_floor",
652
569
  tags={"Areas & Floors"},
653
- annotations={"destructiveHint": True, "idempotentHint": True, "title": "Remove Floor"},
570
+ annotations={"destructiveHint": True, "idempotentHint": True, "title": "Remove Area or Floor"},
654
571
  )
655
572
  @log_tool_usage
656
- async def ha_config_remove_floor(
573
+ async def ha_remove_area_or_floor(
657
574
  self,
658
- floor_id: Annotated[
575
+ kind: Annotated[
576
+ Literal["area", "floor"],
577
+ Field(description="Which registry to delete from: 'area' or 'floor'"),
578
+ ],
579
+ id: Annotated[ # noqa: A002
659
580
  str,
660
- Field(description="Floor ID to delete (use ha_config_list_floors to find IDs)"),
581
+ Field(description="Area ID or floor ID to delete (use ha_list_floors_areas to find IDs)"),
661
582
  ],
662
583
  ) -> dict[str, Any]:
663
- """
664
- Delete a Home Assistant floor.
584
+ """Remove a Home Assistant area or floor.
665
585
 
666
- Areas on this floor are not deleted, just unassigned.
667
- May break automations referencing this floor.
586
+ Removing an area unassigns its entities and devices (the entities and
587
+ devices themselves are not removed). Removing a floor unassigns its
588
+ areas. May break automations referencing the removed area/floor.
668
589
  """
590
+ registry = "area_registry" if kind == "area" else "floor_registry"
591
+ id_key = "area_id" if kind == "area" else "floor_id"
669
592
  try:
670
593
  message: dict[str, Any] = {
671
- "type": "config/floor_registry/delete",
672
- "floor_id": floor_id,
594
+ "type": f"config/{registry}/delete",
595
+ id_key: id,
673
596
  }
674
597
 
675
598
  result = await self._client.send_websocket_message(message)
@@ -677,26 +600,31 @@ class AreaTools:
677
600
  if result.get("success"):
678
601
  return {
679
602
  "success": True,
680
- "floor_id": floor_id,
681
- "message": f"Successfully deleted floor: {floor_id}",
603
+ id_key: id,
604
+ "kind": kind,
605
+ "message": f"Successfully removed {kind}: {id}",
682
606
  }
683
- else:
684
- error = result.get("error", {})
685
- error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
686
- raise_tool_error(create_error_response(
687
- ErrorCode.SERVICE_CALL_FAILED,
688
- f"Failed to delete floor: {error_msg}",
689
- context={"floor_id": floor_id},
690
- ))
607
+
608
+ error = result.get("error", {})
609
+ error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
610
+ raise_tool_error(create_error_response(
611
+ ErrorCode.SERVICE_CALL_FAILED,
612
+ f"Failed to remove {kind}: {error_msg}",
613
+ context={"kind": kind, id_key: id},
614
+ ))
691
615
 
692
616
  except ToolError:
693
617
  raise
694
618
  except Exception as e:
695
- logger.error(f"Error removing floor {floor_id!r}: {e}")
696
- exception_to_structured_error(e, context={"floor_id": floor_id}, suggestions=[
697
- "Check Home Assistant connection",
698
- "Verify the floor_id exists using ha_config_list_floors()",
699
- ])
619
+ logger.error(f"Error removing {kind} {id!r}: {e}")
620
+ exception_to_structured_error(
621
+ e,
622
+ context={"kind": kind, id_key: id},
623
+ suggestions=[
624
+ "Check Home Assistant connection",
625
+ f"Verify the {kind} id exists using ha_list_floors_areas()",
626
+ ],
627
+ )
700
628
 
701
629
 
702
630
  def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None: