ha-mcp-dev 7.4.1.dev418__tar.gz → 7.4.1.dev420__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.4.1.dev418/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev420}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/pyproject.toml +1 -1
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_energy.py +177 -25
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/setup.cfg +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/tests/test_env_manager.py +0 -0
|
@@ -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.
|
|
7
|
+
version = "7.4.1.dev420"
|
|
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,7 @@ get uniform behavior on fresh and configured instances alike.
|
|
|
28
28
|
import json
|
|
29
29
|
import logging
|
|
30
30
|
from collections.abc import Callable
|
|
31
|
-
from typing import Annotated, Any, Literal
|
|
31
|
+
from typing import Annotated, Any, Literal, cast
|
|
32
32
|
|
|
33
33
|
from fastmcp.exceptions import ToolError
|
|
34
34
|
from fastmcp.tools import tool
|
|
@@ -47,8 +47,14 @@ logger = logging.getLogger(__name__)
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
# Top-level keys in the energy prefs payload. Each is an independent
|
|
50
|
-
# full-replace slot in ``energy/save_prefs``.
|
|
51
|
-
|
|
50
|
+
# full-replace slot in ``energy/save_prefs``. ``_PrefsKey`` is the
|
|
51
|
+
# corresponding ``Literal`` alias so MCP-wire callers (Pydantic-validated)
|
|
52
|
+
# get typo-rejection at the boundary; runtime guards in `_set_prefs` cover
|
|
53
|
+
# the unit-test path that bypasses Pydantic.
|
|
54
|
+
_PrefsKey = Literal[
|
|
55
|
+
"energy_sources", "device_consumption", "device_consumption_water"
|
|
56
|
+
]
|
|
57
|
+
_PREFS_TOP_LEVEL_KEYS: tuple[_PrefsKey, ...] = (
|
|
52
58
|
"energy_sources",
|
|
53
59
|
"device_consumption",
|
|
54
60
|
"device_consumption_water",
|
|
@@ -73,6 +79,22 @@ def _default_prefs() -> dict[str, Any]:
|
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
|
|
82
|
+
def _compute_per_key_hashes(prefs: dict[str, Any]) -> dict[_PrefsKey, str]:
|
|
83
|
+
"""Per-top-level-key hashes for partial-update optimistic locking.
|
|
84
|
+
|
|
85
|
+
Each top-level key is wrapped in its own single-key dict before hashing,
|
|
86
|
+
so the per-key hash captures both the key name and its value — an agent
|
|
87
|
+
cannot accidentally use, say, an ``energy_sources`` hash to authorise a
|
|
88
|
+
``device_consumption`` write. ``prefs.get(key, [])`` mirrors the
|
|
89
|
+
"missing top-level key = empty list" semantics codified by
|
|
90
|
+
``_default_prefs``.
|
|
91
|
+
"""
|
|
92
|
+
return {
|
|
93
|
+
key: compute_config_hash({key: prefs.get(key, [])})
|
|
94
|
+
for key in _PREFS_TOP_LEVEL_KEYS
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
76
98
|
def _is_no_prefs_error(error_msg: str) -> bool:
|
|
77
99
|
"""True if an error string from send_websocket_message indicates
|
|
78
100
|
``ERR_NOT_FOUND "No prefs"`` from HA Core's energy/get_prefs handler.
|
|
@@ -243,14 +265,15 @@ class EnergyTools:
|
|
|
243
265
|
),
|
|
244
266
|
] = None,
|
|
245
267
|
config_hash: Annotated[
|
|
246
|
-
str | None,
|
|
268
|
+
str | dict[_PrefsKey, str] | None,
|
|
247
269
|
Field(
|
|
248
270
|
description=(
|
|
249
|
-
"Hash
|
|
250
|
-
"
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
"
|
|
271
|
+
"Hash from a previous mode='get' call. REQUIRED for "
|
|
272
|
+
"mode='set' unless dry_run=True. Two forms: str (full-"
|
|
273
|
+
"blob lock) or dict (per-key lock, taken from the "
|
|
274
|
+
"config_hash_per_key field of mode='get'). See the tool "
|
|
275
|
+
"docstring for fail-closed semantics. Ignored by "
|
|
276
|
+
"convenience modes."
|
|
254
277
|
),
|
|
255
278
|
default=None,
|
|
256
279
|
),
|
|
@@ -375,6 +398,23 @@ class EnergyTools:
|
|
|
375
398
|
the user had configured — silently, with no error. mode='set'
|
|
376
399
|
requires a fresh ``config_hash`` for optimistic locking; convenience
|
|
377
400
|
modes hide this entirely.
|
|
401
|
+
- ``config_hash`` accepts both a single ``str`` (full-blob lock) and
|
|
402
|
+
a ``dict[_PrefsKey, str]`` keyed by top-level keys (per-key lock,
|
|
403
|
+
taken from the ``config_hash_per_key`` field of the mode='get'
|
|
404
|
+
response). The per-key form lets an agent submit only the top-
|
|
405
|
+
level key it wants to change — set-equality between ``config``
|
|
406
|
+
keys and dict keys is enforced, and any key outside the canonical
|
|
407
|
+
set (typo, etc.) on either side is rejected with
|
|
408
|
+
``VALIDATION_FAILED`` rather than silently dropped (so an empty
|
|
409
|
+
submission cannot succeed as a no-op). A per-key submission
|
|
410
|
+
still fully replaces that key's value as the save endpoint
|
|
411
|
+
requires. Mismatch on any locked key returns ``RESOURCE_LOCKED``
|
|
412
|
+
with the offending keys in the response's top-level
|
|
413
|
+
``mismatched_keys`` (``create_error_response`` flattens the
|
|
414
|
+
``context`` dict onto the response root).
|
|
415
|
+
- ``dry_run=True`` skips the hash check entirely for both forms;
|
|
416
|
+
the per-key form is therefore silently accepted on dry runs even
|
|
417
|
+
if its keys would mismatch the current state.
|
|
378
418
|
- A local shape check runs before every write; malformed payloads
|
|
379
419
|
are rejected with a ``shape_errors`` list.
|
|
380
420
|
- After a successful write, the tool calls ``energy/validate`` and
|
|
@@ -481,6 +521,7 @@ class EnergyTools:
|
|
|
481
521
|
"mode": "get",
|
|
482
522
|
"config": prefs,
|
|
483
523
|
"config_hash": compute_config_hash(prefs),
|
|
524
|
+
"config_hash_per_key": _compute_per_key_hashes(prefs),
|
|
484
525
|
"note": (
|
|
485
526
|
"Energy Dashboard has never been configured on "
|
|
486
527
|
"this instance; returning empty default."
|
|
@@ -500,6 +541,7 @@ class EnergyTools:
|
|
|
500
541
|
"mode": "get",
|
|
501
542
|
"config": prefs,
|
|
502
543
|
"config_hash": compute_config_hash(prefs),
|
|
544
|
+
"config_hash_per_key": _compute_per_key_hashes(prefs),
|
|
503
545
|
}
|
|
504
546
|
|
|
505
547
|
except ToolError:
|
|
@@ -584,7 +626,7 @@ class EnergyTools:
|
|
|
584
626
|
async def _set_prefs(
|
|
585
627
|
self,
|
|
586
628
|
config: dict[str, Any],
|
|
587
|
-
config_hash: str,
|
|
629
|
+
config_hash: str | dict[_PrefsKey, str],
|
|
588
630
|
*,
|
|
589
631
|
current_prefs: dict[str, Any] | None = None,
|
|
590
632
|
) -> dict[str, Any]:
|
|
@@ -594,12 +636,21 @@ class EnergyTools:
|
|
|
594
636
|
errors are reported in the response as a non-fatal warning; the
|
|
595
637
|
save already succeeded.
|
|
596
638
|
|
|
639
|
+
``config_hash`` accepts two forms. A ``str`` locks against the
|
|
640
|
+
full prefs blob (the original optimistic-locking contract). A
|
|
641
|
+
``dict[_PrefsKey, str]`` keyed by top-level keys locks each
|
|
642
|
+
submitted key individually — set-equality between ``config`` and
|
|
643
|
+
dict keys is enforced, and unknown keys on either side are
|
|
644
|
+
rejected (``VALIDATION_FAILED``) so an empty submission cannot
|
|
645
|
+
coincide as a no-op success. See the tool docstring for the full
|
|
646
|
+
agent-facing contract.
|
|
647
|
+
|
|
597
648
|
``current_prefs`` is an optional caller-supplied snapshot. When
|
|
598
649
|
provided, the internal re-read is skipped — the convenience-mode
|
|
599
650
|
path uses this to avoid a second ``energy/get_prefs`` round trip
|
|
600
651
|
per attempt (the snapshot was already fetched by ``_mutate_atomic``).
|
|
601
|
-
|
|
602
|
-
|
|
652
|
+
Convenience modes always pass a ``str`` hash; the dict form is
|
|
653
|
+
only reachable via direct mode='set' callers.
|
|
603
654
|
"""
|
|
604
655
|
try:
|
|
605
656
|
# 1. Shape check (fast local, fail closed)
|
|
@@ -648,21 +699,121 @@ class EnergyTools:
|
|
|
648
699
|
# unreachable; appeases type checkers
|
|
649
700
|
current_prefs = {}
|
|
650
701
|
|
|
651
|
-
|
|
702
|
+
if isinstance(config_hash, dict):
|
|
703
|
+
# Per-key form: validate (and hence allow saving) only the
|
|
704
|
+
# top-level keys whose hashes were supplied. Fail-closed on
|
|
705
|
+
# unknown keys (no silent-drop) so a typo in both 'config'
|
|
706
|
+
# and 'config_hash' cannot coincide as an empty no-op
|
|
707
|
+
# success at the save endpoint.
|
|
708
|
+
_valid = set(_PREFS_TOP_LEVEL_KEYS)
|
|
709
|
+
invalid_config_keys = sorted(set(config) - _valid)
|
|
710
|
+
invalid_hash_keys = sorted(set(config_hash) - _valid)
|
|
711
|
+
if invalid_config_keys or invalid_hash_keys:
|
|
712
|
+
raise_tool_error(
|
|
713
|
+
create_error_response(
|
|
714
|
+
ErrorCode.VALIDATION_FAILED,
|
|
715
|
+
"Unknown top-level key(s) in 'config' or "
|
|
716
|
+
"'config_hash' (per-key form)",
|
|
717
|
+
context={
|
|
718
|
+
"mode": "set",
|
|
719
|
+
"invalid_config_keys": invalid_config_keys,
|
|
720
|
+
"invalid_hash_keys": invalid_hash_keys,
|
|
721
|
+
"valid_keys": list(_PREFS_TOP_LEVEL_KEYS),
|
|
722
|
+
},
|
|
723
|
+
suggestions=[
|
|
724
|
+
"Use only 'energy_sources', "
|
|
725
|
+
"'device_consumption', or "
|
|
726
|
+
"'device_consumption_water'",
|
|
727
|
+
],
|
|
728
|
+
)
|
|
729
|
+
)
|
|
652
730
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
"
|
|
661
|
-
"
|
|
662
|
-
|
|
663
|
-
|
|
731
|
+
submitted_keys = set(config)
|
|
732
|
+
hashed_keys = set(config_hash)
|
|
733
|
+
if not submitted_keys:
|
|
734
|
+
raise_tool_error(
|
|
735
|
+
create_error_response(
|
|
736
|
+
ErrorCode.VALIDATION_FAILED,
|
|
737
|
+
"'config' must include at least one top-level "
|
|
738
|
+
"key when using per-key config_hash",
|
|
739
|
+
context={"mode": "set"},
|
|
740
|
+
suggestions=[
|
|
741
|
+
"Include the top-level key(s) you want to "
|
|
742
|
+
"save in 'config' alongside their per-key "
|
|
743
|
+
"hashes in 'config_hash'",
|
|
744
|
+
],
|
|
745
|
+
)
|
|
746
|
+
)
|
|
747
|
+
if submitted_keys != hashed_keys:
|
|
748
|
+
raise_tool_error(
|
|
749
|
+
create_error_response(
|
|
750
|
+
ErrorCode.VALIDATION_FAILED,
|
|
751
|
+
"Per-key config_hash keys must match the "
|
|
752
|
+
"top-level keys submitted in 'config'",
|
|
753
|
+
context={
|
|
754
|
+
"mode": "set",
|
|
755
|
+
"submitted_keys": sorted(submitted_keys),
|
|
756
|
+
"hashed_keys": sorted(hashed_keys),
|
|
757
|
+
"missing_in_hash": sorted(
|
|
758
|
+
submitted_keys - hashed_keys
|
|
759
|
+
),
|
|
760
|
+
"extra_in_hash": sorted(
|
|
761
|
+
hashed_keys - submitted_keys
|
|
762
|
+
),
|
|
763
|
+
},
|
|
764
|
+
suggestions=[
|
|
765
|
+
"Pass exactly one config_hash_per_key entry "
|
|
766
|
+
"per top-level key in 'config'",
|
|
767
|
+
"Use the str form of config_hash to lock "
|
|
768
|
+
"the full prefs blob instead",
|
|
769
|
+
],
|
|
770
|
+
)
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
# ``submitted_keys`` was validated against
|
|
774
|
+
# ``_PREFS_TOP_LEVEL_KEYS`` above, so each ``key`` is in
|
|
775
|
+
# fact a ``_PrefsKey``. Mypy can't narrow ``str`` from
|
|
776
|
+
# ``sorted(set[str])`` automatically, hence the explicit
|
|
777
|
+
# ``cast`` at the dict subscript.
|
|
778
|
+
mismatched_keys = [
|
|
779
|
+
key
|
|
780
|
+
for key in sorted(submitted_keys)
|
|
781
|
+
if config_hash[cast(_PrefsKey, key)]
|
|
782
|
+
!= compute_config_hash({key: current_prefs.get(key, [])})
|
|
783
|
+
]
|
|
784
|
+
if mismatched_keys:
|
|
785
|
+
raise_tool_error(
|
|
786
|
+
create_error_response(
|
|
787
|
+
ErrorCode.RESOURCE_LOCKED,
|
|
788
|
+
"Energy prefs modified since last read on "
|
|
789
|
+
f"top-level key(s): {', '.join(mismatched_keys)}"
|
|
790
|
+
" (conflict)",
|
|
791
|
+
context={
|
|
792
|
+
"mode": "set",
|
|
793
|
+
"mismatched_keys": mismatched_keys,
|
|
794
|
+
},
|
|
795
|
+
suggestions=[
|
|
796
|
+
"Call ha_manage_energy_prefs(mode='get') again",
|
|
797
|
+
"Re-apply your changes to the fresh config",
|
|
798
|
+
"Pass the new config_hash_per_key back in",
|
|
799
|
+
],
|
|
800
|
+
)
|
|
801
|
+
)
|
|
802
|
+
else:
|
|
803
|
+
current_hash = compute_config_hash(current_prefs)
|
|
804
|
+
if current_hash != config_hash:
|
|
805
|
+
raise_tool_error(
|
|
806
|
+
create_error_response(
|
|
807
|
+
ErrorCode.RESOURCE_LOCKED,
|
|
808
|
+
"Energy prefs modified since last read (conflict)",
|
|
809
|
+
context={"mode": "set"},
|
|
810
|
+
suggestions=[
|
|
811
|
+
"Call ha_manage_energy_prefs(mode='get') again",
|
|
812
|
+
"Re-apply your changes to the fresh config",
|
|
813
|
+
"Pass the new config_hash back in",
|
|
814
|
+
],
|
|
815
|
+
)
|
|
664
816
|
)
|
|
665
|
-
)
|
|
666
817
|
|
|
667
818
|
# 3. Save
|
|
668
819
|
save_payload: dict[str, Any] = {"type": "energy/save_prefs"}
|
|
@@ -723,6 +874,7 @@ class EnergyTools:
|
|
|
723
874
|
"success": True,
|
|
724
875
|
"mode": "set",
|
|
725
876
|
"config_hash": new_hash,
|
|
877
|
+
"config_hash_per_key": _compute_per_key_hashes(new_prefs),
|
|
726
878
|
"message": "Energy prefs updated.",
|
|
727
879
|
}
|
|
728
880
|
if post_save_errors:
|
|
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
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/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
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/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
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/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.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/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.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/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
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev418 → ha_mcp_dev-7.4.1.dev420}/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
|