ha-mcp-dev 7.4.1.dev427__tar.gz → 7.4.1.dev428__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.dev427/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev428}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/pyproject.toml +1 -1
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_energy.py +108 -13
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/setup.cfg +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/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.dev428"
|
|
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"
|
|
@@ -96,7 +96,7 @@ def _compute_per_key_hashes(prefs: dict[str, Any]) -> dict[_PrefsKey, str]:
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
def _is_no_prefs_error(error_msg: str) -> bool:
|
|
99
|
-
"""True if an error string from send_websocket_message indicates
|
|
99
|
+
"""Return True if an error string from send_websocket_message indicates
|
|
100
100
|
``ERR_NOT_FOUND "No prefs"`` from HA Core's energy/get_prefs handler.
|
|
101
101
|
|
|
102
102
|
HA Core wraps the error as ``f"Command failed: {message}"``; the
|
|
@@ -143,13 +143,30 @@ def _flatten_validation_errors(raw: Any) -> list[dict[str, str]]:
|
|
|
143
143
|
return errors
|
|
144
144
|
|
|
145
145
|
|
|
146
|
-
def _shape_check(
|
|
147
|
-
|
|
146
|
+
def _shape_check(
|
|
147
|
+
config: dict[str, Any],
|
|
148
|
+
validate_only: dict[str, set[int]] | None = None,
|
|
149
|
+
) -> list[dict[str, str]]:
|
|
150
|
+
"""Validate config shape locally before sending to the server.
|
|
148
151
|
|
|
149
152
|
Validates that top-level keys have the expected list-of-dicts shape and
|
|
150
153
|
that required identifying fields are present. Does NOT validate semantic
|
|
151
154
|
correctness (stat IDs existing, units matching, etc.) — that's surfaced
|
|
152
155
|
by the post-save server-side ``energy/validate`` call.
|
|
156
|
+
|
|
157
|
+
``validate_only`` scopes the per-entry check. ``None`` (default) validates
|
|
158
|
+
every entry under every present top-level key — the original contract.
|
|
159
|
+
A dict scopes the check to the listed keys and, within each, only the
|
|
160
|
+
listed indices: top-level keys absent from the dict are skipped entirely,
|
|
161
|
+
indices outside each key's set are skipped per-entry. The "must be a
|
|
162
|
+
list" structural check still fires for any present-and-listed key with a
|
|
163
|
+
non-list value, so ``validate_only`` cannot be used to bypass structural
|
|
164
|
+
sanity. A dict with an empty set for a key (``{key: set()}``) skips the
|
|
165
|
+
per-entry pass for that key while preserving the structural check —
|
|
166
|
+
this is what convenience-mode write paths pass for remove operations
|
|
167
|
+
(no new entries to validate, but the list shape is still checked). An
|
|
168
|
+
empty dict (``{}``) skips all keys entirely. See issue #1086 for the
|
|
169
|
+
asymmetric over-validation problem this addresses.
|
|
153
170
|
"""
|
|
154
171
|
errors: list[dict[str, str]] = []
|
|
155
172
|
|
|
@@ -159,11 +176,18 @@ def _shape_check(config: dict[str, Any]) -> list[dict[str, str]]:
|
|
|
159
176
|
for key in _PREFS_TOP_LEVEL_KEYS:
|
|
160
177
|
if key not in config:
|
|
161
178
|
continue
|
|
179
|
+
if validate_only is not None and key not in validate_only:
|
|
180
|
+
continue
|
|
162
181
|
value = config[key]
|
|
163
182
|
if not isinstance(value, list):
|
|
164
183
|
errors.append({"path": key, "message": "must be a list"})
|
|
165
184
|
continue
|
|
185
|
+
allowed_indices: set[int] | None = (
|
|
186
|
+
validate_only[key] if validate_only is not None else None
|
|
187
|
+
)
|
|
166
188
|
for idx, entry in enumerate(value):
|
|
189
|
+
if allowed_indices is not None and idx not in allowed_indices:
|
|
190
|
+
continue
|
|
167
191
|
if not isinstance(entry, dict):
|
|
168
192
|
errors.append(
|
|
169
193
|
{
|
|
@@ -217,6 +241,47 @@ def _shape_check(config: dict[str, Any]) -> list[dict[str, str]]:
|
|
|
217
241
|
return errors
|
|
218
242
|
|
|
219
243
|
|
|
244
|
+
def _appended_tail_indices(existing: list[Any], new: list[Any]) -> set[int]:
|
|
245
|
+
"""Return the indices in ``new`` that lie past the end of ``existing``.
|
|
246
|
+
|
|
247
|
+
Per issue #1086, this builds the ``validate_only`` index set scoped to the
|
|
248
|
+
appended tail of an append-only / shrink-only mutation, so pre-existing
|
|
249
|
+
HA-validated siblings are not re-checked on every add/remove:
|
|
250
|
+
|
|
251
|
+
- Append-only mutators (``_add_*``): returns indices of the new entries.
|
|
252
|
+
- Shrink-only mutators (``_remove_*``): returns an empty set — nothing
|
|
253
|
+
new to validate, and the surviving entries already passed HA validation.
|
|
254
|
+
|
|
255
|
+
Refuses in-place mutators where ``len(new) == len(existing)`` but the
|
|
256
|
+
contents differ — the appended-tail formula would yield an empty index
|
|
257
|
+
set on a list whose entries actually changed, silently bypassing
|
|
258
|
+
per-entry validation. Add explicit handling (e.g. an
|
|
259
|
+
``_indices_of_modified_entries`` helper) before introducing such a
|
|
260
|
+
mutator; do not extend this one.
|
|
261
|
+
"""
|
|
262
|
+
if len(new) == len(existing) and new != existing:
|
|
263
|
+
raise_tool_error(
|
|
264
|
+
create_error_response(
|
|
265
|
+
ErrorCode.INTERNAL_ERROR,
|
|
266
|
+
"_appended_tail_indices: in-place mutation detected "
|
|
267
|
+
"(same length, different content) — the appended-tail "
|
|
268
|
+
"validation heuristic only covers append-only / shrink-only "
|
|
269
|
+
"mutators. Add explicit per-entry validation handling for "
|
|
270
|
+
"in-place mutators before reusing this helper.",
|
|
271
|
+
context={
|
|
272
|
+
"existing_len": len(existing),
|
|
273
|
+
"new_len": len(new),
|
|
274
|
+
},
|
|
275
|
+
suggestions=[
|
|
276
|
+
"If introducing a _replace_* / _update_* mutator, "
|
|
277
|
+
"compute the indices of modified entries explicitly "
|
|
278
|
+
"and pass those to _shape_check via validate_only.",
|
|
279
|
+
],
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
return set(range(len(existing), len(new)))
|
|
283
|
+
|
|
284
|
+
|
|
220
285
|
class EnergyTools:
|
|
221
286
|
"""Energy Dashboard preference management tools for Home Assistant."""
|
|
222
287
|
|
|
@@ -629,6 +694,7 @@ class EnergyTools:
|
|
|
629
694
|
config_hash: str | dict[_PrefsKey, str],
|
|
630
695
|
*,
|
|
631
696
|
current_prefs: dict[str, Any] | None = None,
|
|
697
|
+
validate_only: dict[str, set[int]] | None = None,
|
|
632
698
|
) -> dict[str, Any]:
|
|
633
699
|
"""Shape-check → hash-check → save → post-save validate.
|
|
634
700
|
|
|
@@ -650,11 +716,18 @@ class EnergyTools:
|
|
|
650
716
|
path uses this to avoid a second ``energy/get_prefs`` round trip
|
|
651
717
|
per attempt (the snapshot was already fetched by ``_mutate_atomic``).
|
|
652
718
|
Convenience modes always pass a ``str`` hash; the dict form is
|
|
653
|
-
only reachable via direct mode='set' callers.
|
|
719
|
+
only reachable via direct mode='set' callers. The hash check still
|
|
720
|
+
runs against the provided snapshot as a defensive guard.
|
|
721
|
+
|
|
722
|
+
``validate_only`` is forwarded to ``_shape_check`` and lets a caller
|
|
723
|
+
scope the per-entry check to specific top-level keys / indices.
|
|
724
|
+
Convenience-mode writes pass the appended tail indices so
|
|
725
|
+
pre-existing (HA-validated) entries are not re-validated against the
|
|
726
|
+
local schema — see issue #1086.
|
|
654
727
|
"""
|
|
655
728
|
try:
|
|
656
729
|
# 1. Shape check (fast local, fail closed)
|
|
657
|
-
shape_errors = _shape_check(config)
|
|
730
|
+
shape_errors = _shape_check(config, validate_only=validate_only)
|
|
658
731
|
if shape_errors:
|
|
659
732
|
raise_tool_error(
|
|
660
733
|
create_error_response(
|
|
@@ -1176,7 +1249,7 @@ class EnergyTools:
|
|
|
1176
1249
|
dry_run: bool,
|
|
1177
1250
|
preview_payload: dict[str, Any],
|
|
1178
1251
|
) -> dict[str, Any]:
|
|
1179
|
-
"""
|
|
1252
|
+
"""Run convenience-mode read-modify-write with dry-run backstop and hash-conflict retry.
|
|
1180
1253
|
|
|
1181
1254
|
Atomicity is with respect to the *entire* prefs snapshot, not just
|
|
1182
1255
|
``target_key``: ``_set_prefs`` validates the full ``config_hash``, so
|
|
@@ -1205,9 +1278,14 @@ class EnergyTools:
|
|
|
1205
1278
|
new_list = mutator(existing_list)
|
|
1206
1279
|
|
|
1207
1280
|
# Backstop shape-check, mirroring the real-run path through
|
|
1208
|
-
# ``_set_prefs`` — keeps dry_run/real-run shape-equivalent
|
|
1209
|
-
#
|
|
1210
|
-
|
|
1281
|
+
# ``_set_prefs`` — keeps dry_run/real-run shape-equivalent if
|
|
1282
|
+
# the entry-construction logic ever changes. See
|
|
1283
|
+
# ``_appended_tail_indices`` for the validate_only contract.
|
|
1284
|
+
appended_indices = _appended_tail_indices(existing_list, new_list)
|
|
1285
|
+
shape_errors = _shape_check(
|
|
1286
|
+
{target_key: new_list},
|
|
1287
|
+
validate_only={target_key: appended_indices},
|
|
1288
|
+
)
|
|
1211
1289
|
if shape_errors:
|
|
1212
1290
|
raise_tool_error(
|
|
1213
1291
|
create_error_response(
|
|
@@ -1241,11 +1319,17 @@ class EnergyTools:
|
|
|
1241
1319
|
new_list = mutator(existing_list)
|
|
1242
1320
|
|
|
1243
1321
|
partial_config = {target_key: new_list}
|
|
1322
|
+
# Per issue #1086: validate only the appended tail so a
|
|
1323
|
+
# pre-existing HA-validated entry cannot block an unrelated
|
|
1324
|
+
# add/remove. See ``_appended_tail_indices`` for the
|
|
1325
|
+
# validate_only contract.
|
|
1326
|
+
appended_indices = _appended_tail_indices(existing_list, new_list)
|
|
1244
1327
|
try:
|
|
1245
1328
|
set_result = await self._set_prefs(
|
|
1246
1329
|
partial_config,
|
|
1247
1330
|
current_hash,
|
|
1248
1331
|
current_prefs=current_config,
|
|
1332
|
+
validate_only={target_key: appended_indices},
|
|
1249
1333
|
)
|
|
1250
1334
|
except ToolError as exc:
|
|
1251
1335
|
# _set_prefs raises ToolError(RESOURCE_LOCKED) on hash mismatch.
|
|
@@ -1289,10 +1373,21 @@ class EnergyTools:
|
|
|
1289
1373
|
# Unreachable as long as every iteration either returns or raises:
|
|
1290
1374
|
# the only ``continue`` is gated on ``attempt + 1 < max_attempts``,
|
|
1291
1375
|
# which is False on the final iteration — so the bare ``raise``
|
|
1292
|
-
# in the except block always fires there.
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1376
|
+
# in the except block always fires there. Surface as an actionable
|
|
1377
|
+
# structured error rather than a bare AssertionError that would
|
|
1378
|
+
# otherwise fall through to ``except Exception`` and lose context.
|
|
1379
|
+
raise_tool_error(
|
|
1380
|
+
create_error_response(
|
|
1381
|
+
ErrorCode.INTERNAL_ERROR,
|
|
1382
|
+
f"_mutate_atomic({mode}, {target_key}): retry loop exited "
|
|
1383
|
+
"without a return or raise",
|
|
1384
|
+
context={"mode": mode, "target_key": target_key},
|
|
1385
|
+
suggestions=[
|
|
1386
|
+
"This indicates a bug in the optimistic-concurrency "
|
|
1387
|
+
"loop logic — please file an issue with the mode and "
|
|
1388
|
+
"target_key from the context.",
|
|
1389
|
+
],
|
|
1390
|
+
)
|
|
1296
1391
|
)
|
|
1297
1392
|
|
|
1298
1393
|
except ToolError:
|
|
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.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/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.dev427 → ha_mcp_dev-7.4.1.dev428}/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.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/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.dev427 → ha_mcp_dev-7.4.1.dev428}/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.dev427 → ha_mcp_dev-7.4.1.dev428}/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.dev427 → ha_mcp_dev-7.4.1.dev428}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev427 → ha_mcp_dev-7.4.1.dev428}/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
|