ha-mcp-dev 7.3.0.dev392__tar.gz → 7.3.0.dev394__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.
- {ha_mcp_dev-7.3.0.dev392/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.3.0.dev394}/PKG-INFO +4 -3
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/README.md +3 -2
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/pyproject.toml +1 -1
- ha_mcp_dev-7.3.0.dev394/src/ha_mcp/tools/tools_energy.py +626 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394/src/ha_mcp_dev.egg-info}/PKG-INFO +4 -3
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/LICENSE +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/setup.cfg +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/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.3.0.
|
|
3
|
+
Version: 7.3.0.dev394
|
|
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-
|
|
40
|
+
<img src="https://img.shields.io/badge/tools-87-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>
|
|
@@ -180,7 +180,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
180
180
|
<details>
|
|
181
181
|
<!-- TOOLS_TABLE_START -->
|
|
182
182
|
|
|
183
|
-
<summary><b>Complete Tool List (
|
|
183
|
+
<summary><b>Complete Tool List (87 tools)</b></summary>
|
|
184
184
|
|
|
185
185
|
| Category | Tools |
|
|
186
186
|
|----------|-------|
|
|
@@ -192,6 +192,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
192
192
|
| **Camera** | `ha_get_camera_image` |
|
|
193
193
|
| **Dashboards** | `ha_config_delete_dashboard_resource`, `ha_config_delete_dashboard`, `ha_config_get_dashboard`, `ha_config_list_dashboard_resources`, `ha_config_set_dashboard_resource`, `ha_config_set_dashboard` |
|
|
194
194
|
| **Device Registry** | `ha_get_device`, `ha_remove_device`, `ha_update_device` |
|
|
195
|
+
| **Energy** | `ha_manage_energy_prefs` |
|
|
195
196
|
| **Entity Registry** | `ha_get_entity_exposure`, `ha_get_entity`, `ha_remove_entity`, `ha_set_entity` |
|
|
196
197
|
| **Files** | `ha_delete_file` *(beta)*, `ha_list_files` *(beta)*, `ha_read_file` *(beta)*, `ha_write_file` *(beta)* |
|
|
197
198
|
| **Groups** | `ha_config_list_groups`, `ha_config_remove_group`, `ha_config_set_group` |
|
|
@@ -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-
|
|
11
|
+
<img src="https://img.shields.io/badge/tools-87-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 (
|
|
154
|
+
<summary><b>Complete Tool List (87 tools)</b></summary>
|
|
155
155
|
|
|
156
156
|
| Category | Tools |
|
|
157
157
|
|----------|-------|
|
|
@@ -163,6 +163,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
163
163
|
| **Camera** | `ha_get_camera_image` |
|
|
164
164
|
| **Dashboards** | `ha_config_delete_dashboard_resource`, `ha_config_delete_dashboard`, `ha_config_get_dashboard`, `ha_config_list_dashboard_resources`, `ha_config_set_dashboard_resource`, `ha_config_set_dashboard` |
|
|
165
165
|
| **Device Registry** | `ha_get_device`, `ha_remove_device`, `ha_update_device` |
|
|
166
|
+
| **Energy** | `ha_manage_energy_prefs` |
|
|
166
167
|
| **Entity Registry** | `ha_get_entity_exposure`, `ha_get_entity`, `ha_remove_entity`, `ha_set_entity` |
|
|
167
168
|
| **Files** | `ha_delete_file` *(beta)*, `ha_list_files` *(beta)*, `ha_read_file` *(beta)*, `ha_write_file` *(beta)* |
|
|
168
169
|
| **Groups** | `ha_config_list_groups`, `ha_config_remove_group`, `ha_config_set_group` |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.3.0.
|
|
7
|
+
version = "7.3.0.dev394"
|
|
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"
|
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Energy Dashboard preference management tools for Home Assistant.
|
|
3
|
+
|
|
4
|
+
This module provides a single tool to read and write Home Assistant's Energy
|
|
5
|
+
Dashboard configuration through the ``energy/get_prefs`` / ``energy/save_prefs``
|
|
6
|
+
WebSocket commands. The underlying API has destructive full-replace semantics
|
|
7
|
+
per top-level key (``energy_sources``, ``device_consumption``,
|
|
8
|
+
``device_consumption_water``) — sending a key with a partial list silently
|
|
9
|
+
deletes everything else the user had configured. Optimistic locking via
|
|
10
|
+
``config_hash`` prevents concurrent-modification data loss; a local shape
|
|
11
|
+
check catches the most common agent-side errors; and a server-side
|
|
12
|
+
``energy/validate`` call after every write surfaces residual issues
|
|
13
|
+
(missing stats, wrong unit classes, etc.) in the response.
|
|
14
|
+
|
|
15
|
+
Note: ``energy/validate`` in Home Assistant Core takes no payload — it
|
|
16
|
+
validates the currently-persisted config. Pre-write validation of an
|
|
17
|
+
unsubmitted payload is therefore not possible; this tool validates the
|
|
18
|
+
post-save state instead.
|
|
19
|
+
|
|
20
|
+
Note: On a fresh Home Assistant instance that has never had the Energy
|
|
21
|
+
Dashboard configured, ``energy/get_prefs`` returns
|
|
22
|
+
``ERR_NOT_FOUND "No prefs"`` rather than an empty default. The tool
|
|
23
|
+
transparently maps that case to the documented default preferences
|
|
24
|
+
structure (all three top-level keys present, empty lists) so agents
|
|
25
|
+
get uniform behavior on fresh and configured instances alike.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
from typing import Annotated, Any, Literal
|
|
30
|
+
|
|
31
|
+
from fastmcp.exceptions import ToolError
|
|
32
|
+
from fastmcp.tools import tool
|
|
33
|
+
from pydantic import Field
|
|
34
|
+
|
|
35
|
+
from ..errors import ErrorCode, create_error_response
|
|
36
|
+
from ..utils.config_hash import compute_config_hash
|
|
37
|
+
from .helpers import (
|
|
38
|
+
exception_to_structured_error,
|
|
39
|
+
log_tool_usage,
|
|
40
|
+
raise_tool_error,
|
|
41
|
+
register_tool_methods,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Top-level keys in the energy prefs payload. Each is an independent
|
|
48
|
+
# full-replace slot in ``energy/save_prefs``.
|
|
49
|
+
_PREFS_TOP_LEVEL_KEYS = (
|
|
50
|
+
"energy_sources",
|
|
51
|
+
"device_consumption",
|
|
52
|
+
"device_consumption_water",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _default_prefs() -> dict[str, Any]:
|
|
57
|
+
"""Return the default empty prefs structure used by HA Core.
|
|
58
|
+
|
|
59
|
+
Mirrors ``EnergyManager.default_preferences()`` in
|
|
60
|
+
``homeassistant/components/energy/data.py``. A Home Assistant instance
|
|
61
|
+
that has never had the Energy Dashboard configured returns
|
|
62
|
+
``ERR_NOT_FOUND "No prefs"`` from ``energy/get_prefs``; this helper
|
|
63
|
+
provides the canonical empty structure so the tool can transparently
|
|
64
|
+
treat the two cases (never-configured vs. configured-but-empty) the
|
|
65
|
+
same way.
|
|
66
|
+
"""
|
|
67
|
+
return {
|
|
68
|
+
"energy_sources": [],
|
|
69
|
+
"device_consumption": [],
|
|
70
|
+
"device_consumption_water": [],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _is_no_prefs_error(error_msg: str) -> bool:
|
|
75
|
+
"""True if an error string from send_websocket_message indicates
|
|
76
|
+
``ERR_NOT_FOUND "No prefs"`` from HA Core's energy/get_prefs handler.
|
|
77
|
+
|
|
78
|
+
HA Core wraps the error as ``f"Command failed: {message}"``; the
|
|
79
|
+
underlying sentinel we key on is the literal ``"No prefs"`` message
|
|
80
|
+
emitted by ``ws_get_prefs`` when ``manager.data is None``.
|
|
81
|
+
"""
|
|
82
|
+
return error_msg.endswith("No prefs")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _flatten_validation_errors(raw: Any) -> list[dict[str, str]]:
|
|
86
|
+
"""Convert the raw ``energy/validate`` response into a flat error list.
|
|
87
|
+
|
|
88
|
+
The raw response mirrors the prefs structure: a dict with the three
|
|
89
|
+
top-level keys, each mapping to a list of per-entry error lists (empty
|
|
90
|
+
inner list = that entry is valid). This function walks that structure and
|
|
91
|
+
returns a flat list of ``{"path", "message"}`` dicts, suitable for agent
|
|
92
|
+
consumption.
|
|
93
|
+
|
|
94
|
+
A successful validation returns an empty list.
|
|
95
|
+
"""
|
|
96
|
+
if not isinstance(raw, dict):
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
errors: list[dict[str, str]] = []
|
|
100
|
+
for key in _PREFS_TOP_LEVEL_KEYS:
|
|
101
|
+
entries = raw.get(key, [])
|
|
102
|
+
if not isinstance(entries, list):
|
|
103
|
+
continue
|
|
104
|
+
for idx, entry_errors in enumerate(entries):
|
|
105
|
+
if not entry_errors:
|
|
106
|
+
continue
|
|
107
|
+
if isinstance(entry_errors, list):
|
|
108
|
+
errors.extend(
|
|
109
|
+
{"path": f"{key}[{idx}]", "message": str(msg)}
|
|
110
|
+
for msg in entry_errors
|
|
111
|
+
)
|
|
112
|
+
elif isinstance(entry_errors, dict):
|
|
113
|
+
for field, msgs in entry_errors.items():
|
|
114
|
+
msg_list = msgs if isinstance(msgs, list) else [msgs]
|
|
115
|
+
errors.extend(
|
|
116
|
+
{"path": f"{key}[{idx}].{field}", "message": str(msg)}
|
|
117
|
+
for msg in msg_list
|
|
118
|
+
)
|
|
119
|
+
return errors
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _shape_check(config: dict[str, Any]) -> list[dict[str, str]]:
|
|
123
|
+
"""Cheap local shape check before sending to the server.
|
|
124
|
+
|
|
125
|
+
Validates that top-level keys have the expected list-of-dicts shape and
|
|
126
|
+
that required identifying fields are present. Does NOT validate semantic
|
|
127
|
+
correctness (stat IDs existing, units matching, etc.) — that's surfaced
|
|
128
|
+
by the post-save server-side ``energy/validate`` call.
|
|
129
|
+
"""
|
|
130
|
+
errors: list[dict[str, str]] = []
|
|
131
|
+
|
|
132
|
+
if not isinstance(config, dict):
|
|
133
|
+
return [{"path": "config", "message": "must be a dict"}]
|
|
134
|
+
|
|
135
|
+
for key in _PREFS_TOP_LEVEL_KEYS:
|
|
136
|
+
if key not in config:
|
|
137
|
+
continue
|
|
138
|
+
value = config[key]
|
|
139
|
+
if not isinstance(value, list):
|
|
140
|
+
errors.append({"path": key, "message": "must be a list"})
|
|
141
|
+
continue
|
|
142
|
+
for idx, entry in enumerate(value):
|
|
143
|
+
if not isinstance(entry, dict):
|
|
144
|
+
errors.append(
|
|
145
|
+
{
|
|
146
|
+
"path": f"{key}[{idx}]",
|
|
147
|
+
"message": "entry must be a dict",
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
continue
|
|
151
|
+
if key == "energy_sources":
|
|
152
|
+
valid_types = {"grid", "solar", "battery", "gas"}
|
|
153
|
+
requires_stat_from = {"solar", "battery", "gas"}
|
|
154
|
+
entry_type = entry.get("type")
|
|
155
|
+
if entry_type is None:
|
|
156
|
+
errors.append(
|
|
157
|
+
{
|
|
158
|
+
"path": f"{key}[{idx}]",
|
|
159
|
+
"message": "energy_sources entries require 'type' (grid|solar|battery|gas)",
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
elif entry_type not in valid_types:
|
|
163
|
+
errors.append(
|
|
164
|
+
{
|
|
165
|
+
"path": f"{key}[{idx}].type",
|
|
166
|
+
"message": f"invalid type '{entry_type}' (must be one of grid|solar|battery|gas)",
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
elif (
|
|
170
|
+
entry_type in requires_stat_from and "stat_energy_from" not in entry
|
|
171
|
+
):
|
|
172
|
+
errors.append(
|
|
173
|
+
{
|
|
174
|
+
"path": f"{key}[{idx}]",
|
|
175
|
+
"message": f"{entry_type} entries require 'stat_energy_from'",
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
if key == "device_consumption" and "stat_consumption" not in entry:
|
|
179
|
+
errors.append(
|
|
180
|
+
{
|
|
181
|
+
"path": f"{key}[{idx}]",
|
|
182
|
+
"message": "device_consumption entries require 'stat_consumption'",
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
if key == "device_consumption_water" and "stat_consumption" not in entry:
|
|
186
|
+
errors.append(
|
|
187
|
+
{
|
|
188
|
+
"path": f"{key}[{idx}]",
|
|
189
|
+
"message": "device_consumption_water entries require 'stat_consumption'",
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return errors
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class EnergyTools:
|
|
197
|
+
"""Energy Dashboard preference management tools for Home Assistant."""
|
|
198
|
+
|
|
199
|
+
def __init__(self, client: Any) -> None:
|
|
200
|
+
self._client = client
|
|
201
|
+
|
|
202
|
+
@tool(
|
|
203
|
+
name="ha_manage_energy_prefs",
|
|
204
|
+
tags={"Energy"},
|
|
205
|
+
annotations={
|
|
206
|
+
"destructiveHint": True,
|
|
207
|
+
"idempotentHint": False,
|
|
208
|
+
"title": "Manage Energy Dashboard Preferences",
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
@log_tool_usage
|
|
212
|
+
async def ha_manage_energy_prefs(
|
|
213
|
+
self,
|
|
214
|
+
mode: Annotated[
|
|
215
|
+
Literal["get", "set"],
|
|
216
|
+
Field(
|
|
217
|
+
description="Operation mode: 'get' reads the current prefs; 'set' writes a new prefs payload."
|
|
218
|
+
),
|
|
219
|
+
],
|
|
220
|
+
config: Annotated[
|
|
221
|
+
dict[str, Any] | None,
|
|
222
|
+
Field(
|
|
223
|
+
description=(
|
|
224
|
+
"Full prefs payload for mode='set'. Must contain the "
|
|
225
|
+
"top-level keys you intend to replace: 'energy_sources', "
|
|
226
|
+
"'device_consumption', 'device_consumption_water'. Any "
|
|
227
|
+
"top-level key present in this payload REPLACES the "
|
|
228
|
+
"existing list entirely; any omitted key is preserved. "
|
|
229
|
+
"Call with mode='get' first, mutate the returned config, "
|
|
230
|
+
"then pass the whole object back."
|
|
231
|
+
),
|
|
232
|
+
default=None,
|
|
233
|
+
),
|
|
234
|
+
] = None,
|
|
235
|
+
config_hash: Annotated[
|
|
236
|
+
str | None,
|
|
237
|
+
Field(
|
|
238
|
+
description=(
|
|
239
|
+
"Hash returned by the previous mode='get' call. REQUIRED "
|
|
240
|
+
"for mode='set' unless dry_run=True. Rejected if the "
|
|
241
|
+
"server-side config has changed since that read — re-read "
|
|
242
|
+
"and retry."
|
|
243
|
+
),
|
|
244
|
+
default=None,
|
|
245
|
+
),
|
|
246
|
+
] = None,
|
|
247
|
+
dry_run: Annotated[
|
|
248
|
+
bool,
|
|
249
|
+
Field(
|
|
250
|
+
description=(
|
|
251
|
+
"For mode='set' only. If True, runs a local shape check "
|
|
252
|
+
"on the proposed config AND calls the server's "
|
|
253
|
+
"energy/validate against the CURRENT persisted state "
|
|
254
|
+
"(Home Assistant's validate endpoint cannot validate "
|
|
255
|
+
"an unsubmitted payload). Returns both error lists "
|
|
256
|
+
"without writing. Default False."
|
|
257
|
+
),
|
|
258
|
+
default=False,
|
|
259
|
+
),
|
|
260
|
+
] = False,
|
|
261
|
+
) -> dict[str, Any]:
|
|
262
|
+
"""
|
|
263
|
+
Manage the Home Assistant Energy Dashboard preferences.
|
|
264
|
+
|
|
265
|
+
The Energy Dashboard configuration (grid/solar/battery/gas sources,
|
|
266
|
+
individual device consumption sensors, cost tariffs, water) is stored
|
|
267
|
+
in ``.storage/energy`` and not otherwise reachable via REST, services,
|
|
268
|
+
or helper flows — this tool is the only way for agents to inspect or
|
|
269
|
+
modify it.
|
|
270
|
+
|
|
271
|
+
WHEN TO USE:
|
|
272
|
+
- To inspect or modify the Energy Dashboard config programmatically.
|
|
273
|
+
|
|
274
|
+
WHEN NOT TO USE:
|
|
275
|
+
- To create the underlying statistics themselves — they must already
|
|
276
|
+
exist as HA entities before being referenced here; create them via
|
|
277
|
+
the relevant integration's config flow first.
|
|
278
|
+
|
|
279
|
+
CAVEATS:
|
|
280
|
+
- ``energy/save_prefs`` has per-key FULL-REPLACE semantics. Passing
|
|
281
|
+
``{"device_consumption": [<one entry>]}`` deletes every other device
|
|
282
|
+
the user had configured — silently, with no error. Always call
|
|
283
|
+
mode='get' first, mutate the returned config, pass the whole object
|
|
284
|
+
back, and include the returned ``config_hash`` so the tool can
|
|
285
|
+
reject concurrent modifications.
|
|
286
|
+
- A local shape check runs before every write; malformed payloads
|
|
287
|
+
are rejected with a ``shape_errors`` list.
|
|
288
|
+
- After a successful write, the tool calls ``energy/validate`` and
|
|
289
|
+
returns any residual issues as ``post_save_validation_errors`` in
|
|
290
|
+
the response. These reflect semantic problems (missing stats, unit
|
|
291
|
+
mismatches) that shape checks can't catch; the save persists
|
|
292
|
+
regardless — correct the config and write again if needed.
|
|
293
|
+
- The underlying save endpoint is admin-only. Non-admin tokens will
|
|
294
|
+
receive an authorization error from Home Assistant.
|
|
295
|
+
"""
|
|
296
|
+
if mode == "get":
|
|
297
|
+
return await self._get_prefs()
|
|
298
|
+
|
|
299
|
+
# mode == "set"
|
|
300
|
+
if config is None:
|
|
301
|
+
raise_tool_error(
|
|
302
|
+
create_error_response(
|
|
303
|
+
ErrorCode.VALIDATION_MISSING_PARAMETER,
|
|
304
|
+
"'config' is required when mode='set'",
|
|
305
|
+
context={"mode": mode},
|
|
306
|
+
suggestions=[
|
|
307
|
+
"Call ha_manage_energy_prefs(mode='get') first, mutate the returned config, pass it back",
|
|
308
|
+
],
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if dry_run:
|
|
313
|
+
return await self._dry_run(config)
|
|
314
|
+
|
|
315
|
+
if config_hash is None:
|
|
316
|
+
raise_tool_error(
|
|
317
|
+
create_error_response(
|
|
318
|
+
ErrorCode.VALIDATION_MISSING_PARAMETER,
|
|
319
|
+
"'config_hash' is required when mode='set' and dry_run=False",
|
|
320
|
+
context={"mode": mode},
|
|
321
|
+
suggestions=[
|
|
322
|
+
"Call ha_manage_energy_prefs(mode='get') to obtain a fresh config_hash",
|
|
323
|
+
"Or call again with dry_run=True to validate without a hash",
|
|
324
|
+
],
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
return await self._set_prefs(config, config_hash)
|
|
329
|
+
|
|
330
|
+
# ------------------------------------------------------------------
|
|
331
|
+
# Internal handlers
|
|
332
|
+
# ------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
async def _get_prefs(self) -> dict[str, Any]:
|
|
335
|
+
"""Fetch current prefs and return them with a config_hash.
|
|
336
|
+
|
|
337
|
+
On a Home Assistant instance that has never had the Energy Dashboard
|
|
338
|
+
configured, ``energy/get_prefs`` returns ``ERR_NOT_FOUND "No prefs"``
|
|
339
|
+
rather than an empty default. This method maps that case to the
|
|
340
|
+
documented default preferences structure so the tool works uniformly
|
|
341
|
+
on fresh installations.
|
|
342
|
+
"""
|
|
343
|
+
try:
|
|
344
|
+
result = await self._client.send_websocket_message(
|
|
345
|
+
{
|
|
346
|
+
"type": "energy/get_prefs",
|
|
347
|
+
}
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
if not result.get("success"):
|
|
351
|
+
error_msg = str(result.get("error", ""))
|
|
352
|
+
if _is_no_prefs_error(error_msg):
|
|
353
|
+
prefs = _default_prefs()
|
|
354
|
+
return {
|
|
355
|
+
"success": True,
|
|
356
|
+
"mode": "get",
|
|
357
|
+
"config": prefs,
|
|
358
|
+
"config_hash": compute_config_hash(prefs),
|
|
359
|
+
"note": (
|
|
360
|
+
"Energy Dashboard has never been configured on "
|
|
361
|
+
"this instance; returning empty default."
|
|
362
|
+
),
|
|
363
|
+
}
|
|
364
|
+
raise_tool_error(
|
|
365
|
+
create_error_response(
|
|
366
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
367
|
+
f"Failed to get energy prefs: {result.get('error', 'Unknown error')}",
|
|
368
|
+
context={"mode": "get"},
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
prefs = result.get("result") or _default_prefs()
|
|
373
|
+
return {
|
|
374
|
+
"success": True,
|
|
375
|
+
"mode": "get",
|
|
376
|
+
"config": prefs,
|
|
377
|
+
"config_hash": compute_config_hash(prefs),
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
except ToolError:
|
|
381
|
+
raise
|
|
382
|
+
except Exception as e:
|
|
383
|
+
logger.error(f"Error getting energy prefs: {e}")
|
|
384
|
+
exception_to_structured_error(
|
|
385
|
+
e,
|
|
386
|
+
context={"mode": "get"},
|
|
387
|
+
suggestions=[
|
|
388
|
+
"Check Home Assistant connection",
|
|
389
|
+
"Verify WebSocket connection is active",
|
|
390
|
+
],
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
async def _dry_run(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
394
|
+
"""Shape-check the proposed config and fetch current-state validate.
|
|
395
|
+
|
|
396
|
+
Returns both error lists clearly labelled so agents can distinguish
|
|
397
|
+
problems they're about to introduce (shape_errors) from pre-existing
|
|
398
|
+
issues in the persisted state (current_state_validation_errors).
|
|
399
|
+
"""
|
|
400
|
+
try:
|
|
401
|
+
shape_errors = _shape_check(config)
|
|
402
|
+
|
|
403
|
+
validate_result = await self._client.send_websocket_message(
|
|
404
|
+
{
|
|
405
|
+
"type": "energy/validate",
|
|
406
|
+
}
|
|
407
|
+
)
|
|
408
|
+
validate_warning: str | None = None
|
|
409
|
+
if validate_result.get("success"):
|
|
410
|
+
current_state_errors = _flatten_validation_errors(
|
|
411
|
+
validate_result.get("result", {})
|
|
412
|
+
)
|
|
413
|
+
else:
|
|
414
|
+
validate_error = validate_result.get("error") or "unknown error"
|
|
415
|
+
logger.warning(
|
|
416
|
+
f"energy/validate (current state) failed: {validate_error}"
|
|
417
|
+
)
|
|
418
|
+
current_state_errors = []
|
|
419
|
+
validate_warning = (
|
|
420
|
+
f"energy/validate failed: {validate_error} — "
|
|
421
|
+
"current-state validation skipped"
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
response: dict[str, Any] = {
|
|
425
|
+
"success": len(shape_errors) == 0,
|
|
426
|
+
"mode": "set",
|
|
427
|
+
"dry_run": True,
|
|
428
|
+
"shape_errors": shape_errors,
|
|
429
|
+
"current_state_validation_errors": current_state_errors,
|
|
430
|
+
"message": (
|
|
431
|
+
"Shape OK. Note: HA's energy/validate cannot validate an "
|
|
432
|
+
"unsubmitted payload — current_state_validation_errors "
|
|
433
|
+
"reflects the CURRENT persisted config, not your proposal. "
|
|
434
|
+
"Semantic issues in the proposed config (missing stats, "
|
|
435
|
+
"wrong units) will surface in post_save_validation_errors "
|
|
436
|
+
"after an actual mode='set' write."
|
|
437
|
+
if not shape_errors
|
|
438
|
+
else f"{len(shape_errors)} shape error(s) — fix before writing."
|
|
439
|
+
),
|
|
440
|
+
}
|
|
441
|
+
if validate_warning is not None:
|
|
442
|
+
response["partial"] = True
|
|
443
|
+
response["warning"] = validate_warning
|
|
444
|
+
return response
|
|
445
|
+
|
|
446
|
+
except ToolError:
|
|
447
|
+
raise
|
|
448
|
+
except Exception as e:
|
|
449
|
+
logger.error(f"Error in energy prefs dry_run: {e}")
|
|
450
|
+
exception_to_structured_error(
|
|
451
|
+
e,
|
|
452
|
+
context={"mode": "set", "dry_run": True},
|
|
453
|
+
suggestions=[
|
|
454
|
+
"Check Home Assistant connection",
|
|
455
|
+
"Verify config shape matches energy/get_prefs response",
|
|
456
|
+
],
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
async def _set_prefs(
|
|
460
|
+
self,
|
|
461
|
+
config: dict[str, Any],
|
|
462
|
+
config_hash: str,
|
|
463
|
+
) -> dict[str, Any]:
|
|
464
|
+
"""Shape-check → hash-check → save → post-save validate.
|
|
465
|
+
|
|
466
|
+
Shape errors and hash mismatch fail closed. Post-save validation
|
|
467
|
+
errors are reported in the response as a non-fatal warning; the
|
|
468
|
+
save already succeeded.
|
|
469
|
+
"""
|
|
470
|
+
try:
|
|
471
|
+
# 1. Shape check (fast local, fail closed)
|
|
472
|
+
shape_errors = _shape_check(config)
|
|
473
|
+
if shape_errors:
|
|
474
|
+
raise_tool_error(
|
|
475
|
+
create_error_response(
|
|
476
|
+
ErrorCode.VALIDATION_FAILED,
|
|
477
|
+
f"Config shape invalid: {len(shape_errors)} error(s)",
|
|
478
|
+
context={
|
|
479
|
+
"mode": "set",
|
|
480
|
+
"shape_errors": shape_errors,
|
|
481
|
+
},
|
|
482
|
+
suggestions=[
|
|
483
|
+
"Fix the listed errors and retry",
|
|
484
|
+
"Call with dry_run=True to re-check without writing",
|
|
485
|
+
],
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# 2. Fresh read for hash comparison. Map "No prefs" (never
|
|
490
|
+
# configured) to empty default so the hash-check works on
|
|
491
|
+
# fresh installations too.
|
|
492
|
+
current_result = await self._client.send_websocket_message(
|
|
493
|
+
{
|
|
494
|
+
"type": "energy/get_prefs",
|
|
495
|
+
}
|
|
496
|
+
)
|
|
497
|
+
if current_result.get("success"):
|
|
498
|
+
current_prefs: dict[str, Any] = (
|
|
499
|
+
current_result.get("result") or _default_prefs()
|
|
500
|
+
)
|
|
501
|
+
else:
|
|
502
|
+
error = current_result.get("error") or "Unknown error"
|
|
503
|
+
if _is_no_prefs_error(str(error)):
|
|
504
|
+
current_prefs = _default_prefs()
|
|
505
|
+
else:
|
|
506
|
+
raise_tool_error(
|
|
507
|
+
create_error_response(
|
|
508
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
509
|
+
f"Failed to re-read prefs for hash check: {error}",
|
|
510
|
+
context={"mode": "set"},
|
|
511
|
+
)
|
|
512
|
+
)
|
|
513
|
+
# unreachable; appeases type checkers
|
|
514
|
+
current_prefs = {}
|
|
515
|
+
|
|
516
|
+
current_hash = compute_config_hash(current_prefs)
|
|
517
|
+
|
|
518
|
+
if current_hash != config_hash:
|
|
519
|
+
raise_tool_error(
|
|
520
|
+
create_error_response(
|
|
521
|
+
ErrorCode.RESOURCE_LOCKED,
|
|
522
|
+
"Energy prefs modified since last read (conflict)",
|
|
523
|
+
context={"mode": "set"},
|
|
524
|
+
suggestions=[
|
|
525
|
+
"Call ha_manage_energy_prefs(mode='get') again",
|
|
526
|
+
"Re-apply your changes to the fresh config",
|
|
527
|
+
"Pass the new config_hash back in",
|
|
528
|
+
],
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# 3. Save
|
|
533
|
+
save_payload: dict[str, Any] = {"type": "energy/save_prefs"}
|
|
534
|
+
for key in _PREFS_TOP_LEVEL_KEYS:
|
|
535
|
+
if key in config:
|
|
536
|
+
save_payload[key] = config[key]
|
|
537
|
+
|
|
538
|
+
save_result = await self._client.send_websocket_message(save_payload)
|
|
539
|
+
if not save_result.get("success"):
|
|
540
|
+
raise_tool_error(
|
|
541
|
+
create_error_response(
|
|
542
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
543
|
+
f"Failed to save energy prefs: {save_result.get('error', 'Unknown error')}",
|
|
544
|
+
context={"mode": "set"},
|
|
545
|
+
suggestions=[
|
|
546
|
+
"Verify the token has admin privileges (energy/save_prefs is admin-only)",
|
|
547
|
+
"Check config shape against the energy/get_prefs response",
|
|
548
|
+
],
|
|
549
|
+
)
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
# 4. Post-save validation against the newly-persisted state
|
|
553
|
+
post_save_errors: list[dict[str, str]] = []
|
|
554
|
+
post_save_validate_error: str | None = None
|
|
555
|
+
try:
|
|
556
|
+
validate_result = await self._client.send_websocket_message(
|
|
557
|
+
{
|
|
558
|
+
"type": "energy/validate",
|
|
559
|
+
}
|
|
560
|
+
)
|
|
561
|
+
if validate_result.get("success"):
|
|
562
|
+
post_save_errors = _flatten_validation_errors(
|
|
563
|
+
validate_result.get("result", {})
|
|
564
|
+
)
|
|
565
|
+
else:
|
|
566
|
+
post_save_validate_error = (
|
|
567
|
+
validate_result.get("error") or "unknown error"
|
|
568
|
+
)
|
|
569
|
+
logger.warning(
|
|
570
|
+
f"energy/validate (post-save) failed: {post_save_validate_error}"
|
|
571
|
+
)
|
|
572
|
+
except Exception as e:
|
|
573
|
+
# Post-save validate failure is non-fatal — the save itself
|
|
574
|
+
# succeeded. Log and continue.
|
|
575
|
+
logger.warning(f"Post-save energy/validate failed: {e}")
|
|
576
|
+
post_save_validate_error = str(e)
|
|
577
|
+
|
|
578
|
+
# 5. Compute new hash from the effective new state (current
|
|
579
|
+
# merged with the submitted keys; save_prefs does not echo it
|
|
580
|
+
# back).
|
|
581
|
+
new_prefs = {**current_prefs}
|
|
582
|
+
for key in _PREFS_TOP_LEVEL_KEYS:
|
|
583
|
+
if key in config:
|
|
584
|
+
new_prefs[key] = config[key]
|
|
585
|
+
new_hash = compute_config_hash(new_prefs)
|
|
586
|
+
|
|
587
|
+
response: dict[str, Any] = {
|
|
588
|
+
"success": True,
|
|
589
|
+
"mode": "set",
|
|
590
|
+
"config_hash": new_hash,
|
|
591
|
+
"message": "Energy prefs updated.",
|
|
592
|
+
}
|
|
593
|
+
if post_save_errors:
|
|
594
|
+
response["post_save_validation_errors"] = post_save_errors
|
|
595
|
+
response["warning"] = (
|
|
596
|
+
f"Save succeeded, but the persisted config has "
|
|
597
|
+
f"{len(post_save_errors)} validation error(s). Review "
|
|
598
|
+
"and re-write if any relate to this change."
|
|
599
|
+
)
|
|
600
|
+
elif post_save_validate_error is not None:
|
|
601
|
+
response["partial"] = True
|
|
602
|
+
response["warning"] = (
|
|
603
|
+
f"Save succeeded, but post-save energy/validate "
|
|
604
|
+
f"failed: {post_save_validate_error}. The persisted "
|
|
605
|
+
"config has not been re-validated."
|
|
606
|
+
)
|
|
607
|
+
return response
|
|
608
|
+
|
|
609
|
+
except ToolError:
|
|
610
|
+
raise
|
|
611
|
+
except Exception as e:
|
|
612
|
+
logger.error(f"Error setting energy prefs: {e}")
|
|
613
|
+
exception_to_structured_error(
|
|
614
|
+
e,
|
|
615
|
+
context={"mode": "set"},
|
|
616
|
+
suggestions=[
|
|
617
|
+
"Check Home Assistant connection",
|
|
618
|
+
"Verify token has admin privileges",
|
|
619
|
+
"Re-read prefs and retry with a fresh config_hash",
|
|
620
|
+
],
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def register_energy_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
625
|
+
"""Register Home Assistant energy preference management tools."""
|
|
626
|
+
register_tool_methods(mcp, EnergyTools(client))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ha-mcp-dev
|
|
3
|
-
Version: 7.3.0.
|
|
3
|
+
Version: 7.3.0.dev394
|
|
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-
|
|
40
|
+
<img src="https://img.shields.io/badge/tools-87-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>
|
|
@@ -180,7 +180,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
180
180
|
<details>
|
|
181
181
|
<!-- TOOLS_TABLE_START -->
|
|
182
182
|
|
|
183
|
-
<summary><b>Complete Tool List (
|
|
183
|
+
<summary><b>Complete Tool List (87 tools)</b></summary>
|
|
184
184
|
|
|
185
185
|
| Category | Tools |
|
|
186
186
|
|----------|-------|
|
|
@@ -192,6 +192,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
192
192
|
| **Camera** | `ha_get_camera_image` |
|
|
193
193
|
| **Dashboards** | `ha_config_delete_dashboard_resource`, `ha_config_delete_dashboard`, `ha_config_get_dashboard`, `ha_config_list_dashboard_resources`, `ha_config_set_dashboard_resource`, `ha_config_set_dashboard` |
|
|
194
194
|
| **Device Registry** | `ha_get_device`, `ha_remove_device`, `ha_update_device` |
|
|
195
|
+
| **Energy** | `ha_manage_energy_prefs` |
|
|
195
196
|
| **Entity Registry** | `ha_get_entity_exposure`, `ha_get_entity`, `ha_remove_entity`, `ha_set_entity` |
|
|
196
197
|
| **Files** | `ha_delete_file` *(beta)*, `ha_list_files` *(beta)*, `ha_read_file` *(beta)*, `ha_write_file` *(beta)* |
|
|
197
198
|
| **Groups** | `ha_config_list_groups`, `ha_config_remove_group`, `ha_config_set_group` |
|
|
@@ -57,6 +57,7 @@ src/ha_mcp/tools/tools_config_dashboards.py
|
|
|
57
57
|
src/ha_mcp/tools/tools_config_entry_flow.py
|
|
58
58
|
src/ha_mcp/tools/tools_config_helpers.py
|
|
59
59
|
src/ha_mcp/tools/tools_config_scripts.py
|
|
60
|
+
src/ha_mcp/tools/tools_energy.py
|
|
60
61
|
src/ha_mcp/tools/tools_entities.py
|
|
61
62
|
src/ha_mcp/tools/tools_filesystem.py
|
|
62
63
|
src/ha_mcp/tools/tools_groups.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/resources/skills-vendor/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/best_practice_checker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/tools/tools_voice_assistant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp/transforms/categorized_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.3.0.dev392 → ha_mcp_dev-7.3.0.dev394}/src/ha_mcp_dev.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|