ha-mcp-dev 7.5.0.dev525__tar.gz → 7.5.0.dev526__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 (111) hide show
  1. {ha_mcp_dev-7.5.0.dev525/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev526}/PKG-INFO +4 -4
  2. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/README.md +3 -3
  3. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/pyproject.toml +1 -1
  4. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/helpers.py +84 -0
  5. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_addons.py +19 -4
  6. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_areas.py +35 -23
  7. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_calendar.py +13 -0
  8. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_categories.py +35 -0
  9. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_config_automations.py +10 -0
  10. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_config_helpers.py +76 -9
  11. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_config_scripts.py +10 -0
  12. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_energy.py +24 -0
  13. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_entities.py +17 -0
  14. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_groups.py +28 -1
  15. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_hacs.py +16 -0
  16. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_integrations.py +29 -0
  17. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_labels.py +35 -0
  18. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_registry.py +24 -0
  19. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_resources.py +24 -0
  20. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_todo.py +31 -0
  21. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_zones.py +22 -0
  22. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526/src/ha_mcp_dev.egg-info}/PKG-INFO +4 -4
  23. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/LICENSE +0 -0
  24. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/MANIFEST.in +0 -0
  25. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/setup.cfg +0 -0
  26. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/__init__.py +0 -0
  27. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/__main__.py +0 -0
  28. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/_pypi_marker +0 -0
  29. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/_version.py +0 -0
  30. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/auth/__init__.py +0 -0
  31. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/auth/consent_form.py +0 -0
  32. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/auth/provider.py +0 -0
  33. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/client/__init__.py +0 -0
  34. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/client/rest_client.py +0 -0
  35. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/client/supervisor_client.py +0 -0
  36. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/client/websocket_client.py +0 -0
  37. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/client/websocket_listener.py +0 -0
  38. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/config.py +0 -0
  39. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/errors.py +0 -0
  40. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/py.typed +0 -0
  41. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  42. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  43. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  44. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  45. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  46. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  47. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  48. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  49. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  50. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  51. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  52. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  53. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  54. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  55. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  56. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  57. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  58. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  59. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  60. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  61. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/server.py +0 -0
  62. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/settings_ui.py +0 -0
  63. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/smoke_test.py +0 -0
  64. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/__init__.py +0 -0
  65. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/backup.py +0 -0
  66. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  67. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/device_control.py +0 -0
  68. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/enhanced.py +0 -0
  69. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/reference_validator.py +0 -0
  70. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/registry.py +0 -0
  71. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/smart_search.py +0 -0
  72. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  73. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  74. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_camera.py +0 -0
  75. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_code.py +0 -0
  76. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  77. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  78. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  79. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  80. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_history.py +0 -0
  81. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  82. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_search.py +0 -0
  83. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_service.py +0 -0
  84. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_services.py +0 -0
  85. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_system.py +0 -0
  86. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_traces.py +0 -0
  87. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_updates.py +0 -0
  88. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_utility.py +0 -0
  89. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  90. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  91. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/tools/util_helpers.py +0 -0
  92. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/transforms/__init__.py +0 -0
  93. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/transforms/categorized_search.py +0 -0
  94. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  95. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/utils/__init__.py +0 -0
  96. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/utils/config_hash.py +0 -0
  97. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/utils/data_paths.py +0 -0
  98. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/utils/domain_handlers.py +0 -0
  99. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  100. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  101. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/utils/operation_manager.py +0 -0
  102. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/utils/python_sandbox.py +0 -0
  103. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp/utils/usage_logger.py +0 -0
  104. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  105. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  106. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  107. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  108. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  109. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/tests/__init__.py +0 -0
  110. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/tests/test_constants.py +0 -0
  111. {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev526}/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.5.0.dev525
3
+ Version: 7.5.0.dev526
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-89-blue" alt="95+ Tools">
41
+ <img src="https://img.shields.io/badge/tools-88-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,7 +181,7 @@ 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 (89 tools)</b></summary>
184
+ <summary><b>Complete Tool List (88 tools)</b></summary>
185
185
 
186
186
  | Category | Tools |
187
187
  |----------|-------|
@@ -198,7 +198,7 @@ Spend less time configuring, more time enjoying your smart home.
198
198
  | **Files** | `ha_delete_file` *(beta)*, `ha_list_files` *(beta)*, `ha_read_file` *(beta)*, `ha_write_file` *(beta)* |
199
199
  | **Groups** | `ha_config_list_groups`, `ha_config_remove_group`, `ha_config_set_group` |
200
200
  | **HACS** | `ha_hacs_add_repository`, `ha_hacs_download`, `ha_hacs_repository_info`, `ha_hacs_search` |
201
- | **Helper Entities** | `ha_config_list_helpers`, `ha_config_set_helper`, `ha_delete_helpers_integrations`, `ha_get_helper_schema` |
201
+ | **Helper Entities** | `ha_config_list_helpers`, `ha_config_set_helper`, `ha_delete_helpers_integrations` |
202
202
  | **History & Statistics** | `ha_get_automation_traces`, `ha_get_history`, `ha_get_logs` |
203
203
  | **Integrations** | `ha_get_integration`, `ha_set_integration_enabled` |
204
204
  | **Labels & Categories** | `ha_config_get_category`, `ha_config_get_label`, `ha_config_remove_category`, `ha_config_remove_label`, `ha_config_set_category`, `ha_config_set_label` |
@@ -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-89-blue" alt="95+ Tools">
11
+ <img src="https://img.shields.io/badge/tools-88-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,7 +151,7 @@ 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 (89 tools)</b></summary>
154
+ <summary><b>Complete Tool List (88 tools)</b></summary>
155
155
 
156
156
  | Category | Tools |
157
157
  |----------|-------|
@@ -168,7 +168,7 @@ Spend less time configuring, more time enjoying your smart home.
168
168
  | **Files** | `ha_delete_file` *(beta)*, `ha_list_files` *(beta)*, `ha_read_file` *(beta)*, `ha_write_file` *(beta)* |
169
169
  | **Groups** | `ha_config_list_groups`, `ha_config_remove_group`, `ha_config_set_group` |
170
170
  | **HACS** | `ha_hacs_add_repository`, `ha_hacs_download`, `ha_hacs_repository_info`, `ha_hacs_search` |
171
- | **Helper Entities** | `ha_config_list_helpers`, `ha_config_set_helper`, `ha_delete_helpers_integrations`, `ha_get_helper_schema` |
171
+ | **Helper Entities** | `ha_config_list_helpers`, `ha_config_set_helper`, `ha_delete_helpers_integrations` |
172
172
  | **History & Statistics** | `ha_get_automation_traces`, `ha_get_history`, `ha_get_logs` |
173
173
  | **Integrations** | `ha_get_integration`, `ha_set_integration_enabled` |
174
174
  | **Labels & Categories** | `ha_config_get_category`, `ha_config_get_label`, `ha_config_remove_category`, `ha_config_remove_label`, `ha_config_set_category`, `ha_config_set_label` |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.5.0.dev525"
7
+ version = "7.5.0.dev526"
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"
@@ -78,6 +78,90 @@ def extract_tool_error_message(te: ToolError) -> str:
78
78
  return str(te)
79
79
 
80
80
 
81
+ def validate_identifier_not_empty(
82
+ value: str | None,
83
+ param_name: str,
84
+ *,
85
+ message: str | None = None,
86
+ suggestions: list[str] | None = None,
87
+ context: dict[str, Any] | None = None,
88
+ ) -> str:
89
+ """Reject ``None``, empty, or whitespace-only identifier values.
90
+
91
+ Surfaces a structured ``VALIDATION_INVALID_PARAMETER`` response so
92
+ CRUD-style tools fail loudly on missing identifiers instead of silently
93
+ routing on Python falsy semantics or relying on Home Assistant to
94
+ translate a missing identifier into a generic ``RESOURCE_NOT_FOUND``.
95
+
96
+ The destructive class this protects against:
97
+ ``action = "update" if label_id else "create"`` — passing ``""`` as
98
+ ``label_id`` silently routes to ``create`` when the caller intended
99
+ ``update``. The whitespace class this protects against: ``" "`` is truthy
100
+ in Python so ``if not value:`` lets it through, but Home Assistant has
101
+ no entry with id ``" "``.
102
+
103
+ The value is checked but not normalised: ``" abc "`` (a real id wrapped
104
+ in spaces) is accepted as-is and returned untouched. Only purely empty or
105
+ purely whitespace strings are rejected.
106
+
107
+ ``None`` is also rejected by this helper. Callers for whom ``None`` is a
108
+ documented sentinel (e.g. ``label_id=None`` meaning "list all" or
109
+ "create new") must gate the call themselves with
110
+ ``if value is not None: validate_identifier_not_empty(value, ...)``.
111
+
112
+ Returning ``str`` (rather than ``None``) lets call sites use the helper
113
+ in narrowing position — ``name = validate_identifier_not_empty(name, …)``
114
+ re-binds ``name`` from ``str | None`` to ``str`` so mypy can prove later
115
+ uses are safe without a duplicate inline check.
116
+
117
+ Args:
118
+ value: Identifier string supplied by the caller. ``None`` is
119
+ rejected — see the ``None``-sentinel note above for the caller
120
+ pattern that permits the sentinel.
121
+ param_name: Name of the parameter, used in the structured error
122
+ response's ``context`` and the human-readable message.
123
+ message: Optional override for the human-readable error message.
124
+ Defaults to ``"{param_name} must be a non-empty, non-whitespace
125
+ string"`` when omitted.
126
+ suggestions: Optional list of guidance strings for the response's
127
+ ``suggestions`` field. Defaults to a generic
128
+ "provide a non-empty value" hint.
129
+ context: Optional additional context fields merged into the error
130
+ response (e.g. ``{"action": "update"}``). The keys ``parameter``
131
+ and ``value`` are always set and take precedence.
132
+
133
+ Returns:
134
+ The validated ``value`` unchanged (typed as ``str``).
135
+
136
+ Raises:
137
+ ToolError: When ``value`` is ``None``, empty, or whitespace-only —
138
+ carrying a structured ``VALIDATION_INVALID_PARAMETER`` response
139
+ with the parameter name, the offending value (for diagnostics),
140
+ and the suggestions.
141
+
142
+ Example:
143
+ >>> validate_identifier_not_empty(label_id, "label_id",
144
+ ... suggestions=["Omit label_id to create a new label"])
145
+ """
146
+ if value is not None and value.strip():
147
+ return value
148
+
149
+ final_context: dict[str, Any] = {}
150
+ if context:
151
+ final_context.update(context)
152
+ final_context["parameter"] = param_name
153
+ final_context["value"] = value
154
+ raise_tool_error(
155
+ create_error_response(
156
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
157
+ message or f"{param_name} must be a non-empty, non-whitespace string",
158
+ suggestions=suggestions
159
+ or [f"Provide a valid non-empty value for {param_name}"],
160
+ context=final_context,
161
+ )
162
+ )
163
+
164
+
81
165
  async def get_connected_ws_client(
82
166
  base_url: str, token: str, verify_ssl: bool | None = None
83
167
  ) -> tuple[HomeAssistantWebSocketClient | None, dict[str, Any] | None]:
@@ -38,6 +38,7 @@ from .helpers import (
38
38
  get_connected_ws_client,
39
39
  log_tool_usage,
40
40
  raise_tool_error,
41
+ validate_identifier_not_empty,
41
42
  )
42
43
  from .util_helpers import ANSI_ESCAPE_RE
43
44
 
@@ -1208,14 +1209,16 @@ def _apply_array_ops(
1208
1209
  )
1209
1210
  )
1210
1211
  new_id = new_item[id_field]
1211
- if new_id is None or new_id == "":
1212
+ if new_id is None or (isinstance(new_id, str) and not new_id.strip()):
1212
1213
  # Items missing the id field have `dict.get(id_field) == None`
1213
- # by default, so allowing None/"" ids would let later patch /
1214
- # delete ops match unrelated items.
1214
+ # by default, so allowing None/""/whitespace-only ids would
1215
+ # let later patch / delete ops match unrelated items.
1216
+ # Non-string ids (e.g. integers) stay valid by design — see
1217
+ # ``test_add_with_integer_zero_id_is_accepted``.
1215
1218
  raise_tool_error(
1216
1219
  create_validation_error(
1217
1220
  f"array_patch add op #{index} item {id_field!r} cannot be "
1218
- "None or an empty string",
1221
+ "None, empty, or whitespace-only",
1219
1222
  parameter=f"array_patch.operations[{index}].item.{id_field}",
1220
1223
  )
1221
1224
  )
@@ -1954,6 +1957,18 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
1954
1957
  ha_manage_addon(slug="...", path="/api/state",
1955
1958
  request_headers={"Accept": "text/plain"})
1956
1959
  """
1960
+ # Empty/whitespace slug would propagate to every dispatch arm
1961
+ # (Supervisor API, ingress proxy, websocket bridge) and surface as a
1962
+ # misleading "addon not found" or 404 from the Supervisor. Reject
1963
+ # up-front so the caller learns the slug was unusable before any
1964
+ # backend call.
1965
+ validate_identifier_not_empty(
1966
+ slug,
1967
+ "slug",
1968
+ suggestions=[
1969
+ "Use ha_get_addon() to discover installed add-on slugs",
1970
+ ],
1971
+ )
1957
1972
  # Build config payload from provided config parameters
1958
1973
  config_data: dict[str, Any] = {}
1959
1974
  if options:
@@ -18,6 +18,7 @@ from .helpers import (
18
18
  log_tool_usage,
19
19
  raise_tool_error,
20
20
  register_tool_methods,
21
+ validate_identifier_not_empty,
21
22
  )
22
23
  from .util_helpers import parse_string_list_param
23
24
 
@@ -465,19 +466,19 @@ class AreaTools:
465
466
  ],
466
467
  ))
467
468
 
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},
469
+ # ``None`` stays the documented "create-new" sentinel; explicit
470
+ # empty/whitespace would silently route to the ``if id:`` create
471
+ # branch below and lose update intent.
472
+ if id is not None:
473
+ validate_identifier_not_empty(
474
+ id,
475
+ "id",
476
476
  suggestions=[
477
477
  "Omit id entirely to create a new entry",
478
478
  "Pass a real area_id/floor_id to update an existing entry",
479
479
  ],
480
- ))
480
+ context={"kind": kind},
481
+ )
481
482
 
482
483
  if kind == "area":
483
484
  if id:
@@ -486,13 +487,15 @@ class AreaTools:
486
487
  )
487
488
  operation = "update"
488
489
  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
- ))
490
+ # Reassignment narrows ``name`` from ``str | None`` to
491
+ # ``str`` for the build-message call below.
492
+ name = validate_identifier_not_empty(
493
+ name,
494
+ "name",
495
+ message="name is required when creating a new area",
496
+ context={"operation": "create_area"},
497
+ suggestions=["Provide a non-empty name for the new area"],
498
+ )
496
499
  message = self._build_area_create_message(
497
500
  name, floor_id, icon, parsed_aliases, picture,
498
501
  )
@@ -506,13 +509,13 @@ class AreaTools:
506
509
  )
507
510
  operation = "update"
508
511
  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
- ))
512
+ name = validate_identifier_not_empty(
513
+ name,
514
+ "name",
515
+ message="name is required when creating a new floor",
516
+ context={"operation": "create_floor"},
517
+ suggestions=["Provide a non-empty name for the new floor"],
518
+ )
516
519
  message = self._build_floor_create_message(
517
520
  name, level, icon, parsed_aliases,
518
521
  )
@@ -590,6 +593,15 @@ class AreaTools:
590
593
  registry = "area_registry" if kind == "area" else "floor_registry"
591
594
  id_key = "area_id" if kind == "area" else "floor_id"
592
595
  try:
596
+ # Empty/whitespace would surface as a misleading HA delete-failure.
597
+ validate_identifier_not_empty(
598
+ id,
599
+ "id",
600
+ suggestions=[
601
+ f"Pass a valid {id_key} (use ha_list_floors_areas() to list)",
602
+ ],
603
+ context={"action": "remove", "kind": kind},
604
+ )
593
605
  message: dict[str, Any] = {
594
606
  "type": f"config/{registry}/delete",
595
607
  id_key: id,
@@ -21,6 +21,7 @@ from .helpers import (
21
21
  log_tool_usage,
22
22
  raise_tool_error,
23
23
  register_tool_methods,
24
+ validate_identifier_not_empty,
24
25
  )
25
26
 
26
27
  logger = logging.getLogger(__name__)
@@ -354,6 +355,18 @@ class CalendarTools:
354
355
  ],
355
356
  ))
356
357
 
358
+ # entity_id format-check above does not cover the ``uid`` parameter.
359
+ # Empty/whitespace uid would flow through to ``calendar.delete_event``
360
+ # and HA returns a misleading "event not found".
361
+ validate_identifier_not_empty(
362
+ uid,
363
+ "uid",
364
+ suggestions=[
365
+ "Use ha_config_get_calendar_events() to list events and obtain valid UIDs",
366
+ ],
367
+ context={"entity_id": entity_id},
368
+ )
369
+
357
370
  # Build service data
358
371
  service_data: dict[str, Any] = {
359
372
  "entity_id": entity_id,
@@ -21,6 +21,7 @@ from .helpers import (
21
21
  log_tool_usage,
22
22
  raise_tool_error,
23
23
  register_tool_methods,
24
+ validate_identifier_not_empty,
24
25
  )
25
26
 
26
27
  logger = logging.getLogger(__name__)
@@ -76,6 +77,18 @@ class CategoryTools:
76
77
  Use ha_set_entity(categories={"automation": "category_id"}) to assign categories to entities.
77
78
  """
78
79
  try:
80
+ # ``None`` stays the documented "list-all" sentinel; explicit
81
+ # empty/whitespace is rejected by ``validate_identifier_not_empty``.
82
+ if category_id is not None:
83
+ validate_identifier_not_empty(
84
+ category_id,
85
+ "category_id",
86
+ suggestions=[
87
+ "Omit category_id to list all categories for the scope",
88
+ "Pass a valid non-empty category_id",
89
+ ],
90
+ context={"action": "get", "scope": scope},
91
+ )
79
92
  message: dict[str, Any] = {
80
93
  "type": "config/category_registry/list",
81
94
  "scope": scope,
@@ -194,6 +207,19 @@ class CategoryTools:
194
207
  After creating a category, use ha_set_entity(categories={"automation": "category_id"}) to assign it.
195
208
  """
196
209
  try:
210
+ # ``None`` stays the documented "create-new" sentinel; explicit
211
+ # empty/whitespace is rejected so the create/update discriminator
212
+ # below cannot silently route an intended update to create.
213
+ if category_id is not None:
214
+ validate_identifier_not_empty(
215
+ category_id,
216
+ "category_id",
217
+ suggestions=[
218
+ "Omit category_id entirely to create a new category",
219
+ "Pass a valid category_id to update an existing category",
220
+ ],
221
+ context={"action": "set", "scope": scope, "name": name},
222
+ )
197
223
  action = "update" if category_id else "create"
198
224
 
199
225
  message: dict[str, Any] = {
@@ -282,6 +308,15 @@ class CategoryTools:
282
308
  This action cannot be undone.
283
309
  """
284
310
  try:
311
+ # Empty/whitespace would surface as a misleading HA delete-failure.
312
+ validate_identifier_not_empty(
313
+ category_id,
314
+ "category_id",
315
+ suggestions=[
316
+ "Pass a valid category_id (use ha_config_get_category() to list)",
317
+ ],
318
+ context={"action": "remove", "scope": scope},
319
+ )
285
320
  message: dict[str, Any] = {
286
321
  "type": "config/category_registry/delete",
287
322
  "scope": scope,
@@ -33,6 +33,7 @@ from .helpers import (
33
33
  log_tool_usage,
34
34
  raise_tool_error,
35
35
  register_tool_methods,
36
+ validate_identifier_not_empty,
36
37
  )
37
38
  from .reference_validator import validate_config_references
38
39
  from .util_helpers import (
@@ -965,6 +966,15 @@ class AutomationConfigTools:
965
966
  **WARNING:** Deleting an automation removes it permanently from your Home Assistant configuration.
966
967
  """
967
968
  try:
969
+ # Empty/whitespace would surface as a misleading HA delete-failure.
970
+ validate_identifier_not_empty(
971
+ identifier,
972
+ "identifier",
973
+ suggestions=[
974
+ "Use ha_search_entities(domain_filter='automation') to find existing automations"
975
+ ],
976
+ context={"operation": "remove_automation"},
977
+ )
968
978
  # Resolve entity_id for wait verification (identifier may be a unique_id)
969
979
  entity_id_for_wait = await self._resolve_automation_entity_id(identifier)
970
980
  if not entity_id_for_wait:
@@ -16,7 +16,12 @@ from pydantic import AliasChoices, Field
16
16
 
17
17
  from ..client.rest_client import HomeAssistantAPIError
18
18
  from ..errors import ErrorCode, create_error_response
19
- from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
19
+ from .helpers import (
20
+ exception_to_structured_error,
21
+ log_tool_usage,
22
+ raise_tool_error,
23
+ validate_identifier_not_empty,
24
+ )
20
25
  from .tools_config_entry_flow import (
21
26
  FLOW_HELPER_TYPES,
22
27
  create_flow_helper,
@@ -1223,11 +1228,12 @@ async def _check_name_collision(
1223
1228
  duplicate entity instead of updating the original. Detect and reject before
1224
1229
  we send the create message, pointing the caller at the existing helper_id.
1225
1230
 
1226
- Empty / missing `name` is left to the existing name-required check downstream
1227
- so the user sees the standard "name is required" error rather than a
1228
- spurious collision miss.
1231
+ Empty / missing / whitespace-only `name` is left to the existing
1232
+ name-required check downstream so the user sees the standard "name is
1233
+ required" error rather than a spurious collision miss (or a wasted WS
1234
+ round-trip on a name the validator is about to reject anyway).
1229
1235
  """
1230
- if not name:
1236
+ if not name or not name.strip():
1231
1237
  return
1232
1238
 
1233
1239
  target_slug = _slugify_helper_name(name)
@@ -1563,6 +1569,22 @@ async def _handle_flow_helper(
1563
1569
  is consistent has already happened upstream in ha_config_set_helper.
1564
1570
  """
1565
1571
  if action is None:
1572
+ # Defence in depth: when reached via the legacy implicit-action
1573
+ # path, an empty/whitespace ``helper_id`` would otherwise be falsy
1574
+ # and silently route to ``create`` — same destructive intent-loss
1575
+ # class as the registry-metadata twins. ``None`` stays the
1576
+ # documented "create-new" sentinel.
1577
+ if helper_id is not None:
1578
+ validate_identifier_not_empty(
1579
+ helper_id,
1580
+ "helper_id",
1581
+ suggestions=[
1582
+ "Omit helper_id entirely to create a new flow helper",
1583
+ "Pass a valid helper_id to update an existing one",
1584
+ "Or pass action='create' / action='update' explicitly",
1585
+ ],
1586
+ context={"helper_type": helper_type},
1587
+ )
1566
1588
  action = "update" if helper_id else "create"
1567
1589
 
1568
1590
  # Normalize empty string to None, matching ha_config_set_helper's treatment
@@ -1618,7 +1640,7 @@ async def _handle_flow_helper(
1618
1640
  # caller-supplied `name` (you cannot rename a flow helper through its
1619
1641
  # options flow). Strip `name` from config_dict and emit a warning so the
1620
1642
  # caller learns their attempted rename was a no-op.
1621
- if action == "create" and name and "name" not in config_dict:
1643
+ if action == "create" and name and name.strip() and "name" not in config_dict:
1622
1644
  schema_fields = await get_user_step_field_names(client, helper_type)
1623
1645
  if schema_fields is None or "name" in schema_fields:
1624
1646
  config_dict["name"] = name
@@ -1661,7 +1683,12 @@ async def _handle_flow_helper(
1661
1683
  # Some helpers (switch_as_x) deliberately don't have `name` injected into
1662
1684
  # config_dict because their schema rejects it — but the tool still
1663
1685
  # requires `name` to be supplied so callers fail fast and consistently.
1664
- if not (name or config_dict.get("name")):
1686
+ # Reject whitespace-only too (``" "`` is truthy in Python) so the
1687
+ # flow-helper create gate is coherent with the simple-helper twin.
1688
+ config_name = config_dict.get("name")
1689
+ config_name_ok = isinstance(config_name, str) and bool(config_name.strip())
1690
+ name_ok = name is not None and bool(name.strip())
1691
+ if not (name_ok or config_name_ok):
1665
1692
  raise_tool_error(
1666
1693
  create_error_response(
1667
1694
  ErrorCode.VALIDATION_INVALID_PARAMETER,
@@ -2373,9 +2400,45 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2373
2400
  ],
2374
2401
  )
2375
2402
  )
2403
+ # Explicit-action update path: helper_id was confirmed non-None
2404
+ # above; close the whitespace gap before the FLOW dispatch
2405
+ # below (``if helper_type in FLOW_HELPER_TYPES: return await
2406
+ # _handle_flow_helper(...)``). The simple-path whitespace twin
2407
+ # inside the ``elif action == "update":`` branch fires after
2408
+ # the FLOW dispatch returns and so is unreachable for flow
2409
+ # helpers; without this guard, ``helper_id=" "`` would reach
2410
+ # ``update_flow_helper`` and surface as a misleading "entry
2411
+ # not found" from HA.
2412
+ if action == "update" and helper_id is not None:
2413
+ validate_identifier_not_empty(
2414
+ helper_id,
2415
+ "helper_id",
2416
+ suggestions=[
2417
+ "Pass a valid helper_id to identify the helper to update",
2418
+ "Or omit helper_id and pass action='create' to create a new helper",
2419
+ ],
2420
+ context={"helper_type": helper_type, "action": action},
2421
+ )
2376
2422
  else:
2377
2423
  # Implicit discriminator (back-compat). Pass action='create'
2378
2424
  # or action='update' explicitly to avoid the inference.
2425
+ # Reject empty/whitespace helper_id up front: ``bool("")`` is
2426
+ # False, so without this gate ``helper_id=""`` silently routes
2427
+ # to ``create`` even when the caller's intent was ``update``,
2428
+ # producing a destructive intent-loss class identical to the
2429
+ # one closed for labels/categories. ``None`` stays the
2430
+ # documented "create-new" sentinel.
2431
+ if helper_id is not None:
2432
+ validate_identifier_not_empty(
2433
+ helper_id,
2434
+ "helper_id",
2435
+ suggestions=[
2436
+ "Omit helper_id entirely to create a new helper",
2437
+ "Pass a valid helper_id to update an existing helper",
2438
+ "Or pass action='create' / action='update' explicitly to declare intent",
2439
+ ],
2440
+ context={"helper_type": helper_type},
2441
+ )
2379
2442
  action = "update" if helper_id else "create"
2380
2443
 
2381
2444
  # Bug 4b/7c/10/14 (issue #1150): reject typed params that don't apply
@@ -2496,7 +2559,9 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2496
2559
  )
2497
2560
 
2498
2561
  if action == "create":
2499
- if not name:
2562
+ # ``bool(" ")`` is True, so plain ``if not name`` would let
2563
+ # whitespace-only names through to a downstream HA error.
2564
+ if not name or not name.strip():
2500
2565
  raise_tool_error(
2501
2566
  create_error_response(
2502
2567
  ErrorCode.VALIDATION_INVALID_PARAMETER,
@@ -2839,7 +2904,9 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2839
2904
  )
2840
2905
 
2841
2906
  elif action == "update":
2842
- if not helper_id:
2907
+ # Same whitespace-upgrade rationale as the create-name check
2908
+ # above — ``if not helper_id`` would let ``" "`` through.
2909
+ if not helper_id or not helper_id.strip():
2843
2910
  raise_tool_error(
2844
2911
  create_error_response(
2845
2912
  ErrorCode.VALIDATION_INVALID_PARAMETER,
@@ -28,6 +28,7 @@ from .helpers import (
28
28
  log_tool_usage,
29
29
  raise_tool_error,
30
30
  register_tool_methods,
31
+ validate_identifier_not_empty,
31
32
  )
32
33
  from .reference_validator import validate_config_references
33
34
  from .util_helpers import (
@@ -619,6 +620,15 @@ class ConfigScriptTools:
619
620
  **WARNING:** Deleting a script that is used by automations may cause those automations to fail.
620
621
  """
621
622
  try:
623
+ # Empty/whitespace would surface as a misleading HA delete-failure.
624
+ validate_identifier_not_empty(
625
+ script_id,
626
+ "script_id",
627
+ suggestions=[
628
+ "Use ha_search_entities(domain_filter='script') to find existing script_ids"
629
+ ],
630
+ context={"operation": "remove_script"},
631
+ )
622
632
  result = await self._client.delete_script_config(script_id)
623
633
 
624
634
  # Wait for script to be removed
@@ -41,6 +41,7 @@ from .helpers import (
41
41
  log_tool_usage,
42
42
  raise_tool_error,
43
43
  register_tool_methods,
44
+ validate_identifier_not_empty,
44
45
  )
45
46
 
46
47
  logger = logging.getLogger(__name__)
@@ -1011,6 +1012,18 @@ class EnergyTools:
1011
1012
  ],
1012
1013
  )
1013
1014
  )
1015
+ # Empty/whitespace stat_consumption would write a ``{"stat_consumption": ""}``
1016
+ # entry to energy prefs storage — a phantom row keyed on an empty
1017
+ # sensor reference. Same multi-modal-destructive class as
1018
+ # ``ha_manage_addon`` slug guard.
1019
+ validate_identifier_not_empty(
1020
+ stat_consumption,
1021
+ "stat_consumption",
1022
+ suggestions=[
1023
+ "Pass stat_consumption='sensor.<your_device_energy>'",
1024
+ ],
1025
+ context={"mode": "add_device"},
1026
+ )
1014
1027
 
1015
1028
  target_key = "device_consumption_water" if water else "device_consumption"
1016
1029
 
@@ -1049,6 +1062,17 @@ class EnergyTools:
1049
1062
  ],
1050
1063
  )
1051
1064
  )
1065
+ # Empty/whitespace stat_consumption would search the prefs storage for
1066
+ # an empty match (always missing) and surface as a misleading
1067
+ # "Device with stat_consumption='' not found".
1068
+ validate_identifier_not_empty(
1069
+ stat_consumption,
1070
+ "stat_consumption",
1071
+ suggestions=[
1072
+ "Pass stat_consumption='sensor.<existing_device_energy>'",
1073
+ ],
1074
+ context={"mode": "remove_device"},
1075
+ )
1052
1076
 
1053
1077
  target_key = "device_consumption_water" if water else "device_consumption"
1054
1078