Skip to content

Commit c9922f7

Browse files
Patch76claude
andcommitted
feat: rename ha_delete_helpers_integrations → ha_remove_helpers_integrations + commit to idempotent contract
Closes #1412. Rename joins the `_remove_*` naming family (12 existing tools) and the tool now ships `idempotentHint: True` per the MCP spec. The behavioral contract is unified across all four routing paths: a confirmed-absent target returns `success=True` with `fallback_used="already_deleted"`; transient connectivity failures and call-shape conflicts still raise. Per-path idempotency: - SIMPLE (Path 1): existing state-machine + entity-registry double-empty short-circuit (now positively asserted by test_helper_delete_nonexistent). - FLOW (Path 2): not_in_registry → idempotent success; bare_id_not_supported / lookup_failed / no_config_entry still raise with their distinct error codes (call-shape, transient, YAML-vs- storage conflict respectively). Step-3 delete now catches HomeAssistantAPIError(status_code=404) for the TOCTOU window between registry resolution and the actual delete. - Direct config entry (Path 3, helper_type=None): catches HomeAssistantAPIError(status_code=404) from the REST DELETE and returns idempotent success. Non-404 API errors still surface via exception_to_structured_error. - Config subentry (Path 4): inspects the WebSocket error code/message for "not_found" / "not found" and returns idempotent success. Other failure modes still raise SERVICE_CALL_FAILED. YAML-configured FLOW helpers (Path 2 no_config_entry) keep raising RESOURCE_NOT_FOUND deliberately — that's a call-shape conflict, not a missing target: the entity exists in HA but cannot be removed via the config-entry delete path. Silently succeeding would mislead callers into thinking the YAML helper was removed. Per-call sites updated: - src/ha_mcp/tools/tools_integrations.py: rename + annotation + four per-path idempotent branches + docstring with full contract. - src/ha_mcp/tools/tools_groups.py: 3 cross-reference docstrings. - ~95 occurrences across 21 test files + cleanup utility mechanically renamed. - 3 unit tests updated to assert the new idempotent contract: test_direct_path_entry_not_found, test_flow_path_entity_not_in_registry, test_flow_path_entry_not_found_at_delete. - 1 new unit test pinning Path 4 idempotency: test_delete_helpers_integrations_subentry_not_found_is_idempotent. - 1 E2E test hardened from soft-coverage if/else to positive assertion on idempotent shape: test_helper_delete_nonexistent. - 1 E2E test flipped to assert idempotent success: test_delete_config_entry_nonexistent_confirmed. - site/src/data/tools.json + README.md + homeassistant-addon/DOCS.md regenerated via scripts/extract_tools.py. CHANGELOG entries referencing the old name (the original #1007 introduction notes) are intentionally untouched — history stays. Verified locally: - ruff check src/ tests/ → All checks passed! - ruff format on changed files - mypy src/ha_mcp/tools/tools_integrations.py → no issues - 3257 unit tests passed, 1 skipped (numpy), 6 deselected (slow) The rename is NOT a breaking change per .gemini/styleguide.md ("Tool consolidation / refactor / rename / param change are NOT breaking."). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 18a8aef commit c9922f7

27 files changed

Lines changed: 785 additions & 596 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ Spend less time configuring, more time enjoying your smart home.
169169
| **Files** | `ha_delete_file` *(beta)*, `ha_list_files` *(beta)*, `ha_read_file` *(beta)*, `ha_write_file` *(beta)* |
170170
| **Groups** | `ha_config_list_groups`, `ha_config_remove_group`, `ha_config_set_group` |
171171
| **HACS** | `ha_hacs_add_repository`, `ha_hacs_download`, `ha_hacs_repository_info`, `ha_hacs_search` |
172-
| **Helper Entities** | `ha_config_list_helpers`, `ha_config_set_helper`, `ha_delete_helpers_integrations` |
172+
| **Helper Entities** | `ha_config_list_helpers`, `ha_config_set_helper`, `ha_remove_helpers_integrations` |
173173
| **History & Statistics** | `ha_get_automation_traces`, `ha_get_history`, `ha_get_logs` |
174174
| **Integrations** | `ha_get_integration`, `ha_get_system_health`, `ha_set_integration_enabled` |
175175
| **Labels & Categories** | `ha_config_get_category`, `ha_config_get_label`, `ha_config_remove_category`, `ha_config_remove_label`, `ha_config_set_category`, `ha_config_set_label` |

homeassistant-addon/DOCS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ The add-on provides 88+ MCP tools for controlling Home Assistant:
445445
### Helper Entities
446446
- `ha_config_list_helpers` — List all Home Assistant helpers of a specific type with their configurations.
447447
- `ha_config_set_helper` — Create or update Home Assistant helper entities and config subentries
448-
- `ha_delete_helpers_integrations` Delete a Home Assistant helper or integration config entry.
448+
- `ha_remove_helpers_integrations` Remove a Home Assistant helper or integration config entry.
449449

450450
### History & Statistics
451451
- `ha_get_automation_traces` — Retrieve execution traces for automations and scripts to debug issues.

site/src/data/tools.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,7 +1276,7 @@
12761276
{
12771277
"name": "ha_config_remove_group",
12781278
"title": "Remove Group",
1279-
"description": "Remove a service-based Home Assistant entity group via the group.remove service.\n\n**When NOT to use:** for groups created through `ha_config_set_helper(helper_type=\"group\", ...)`,\nuse `ha_delete_helpers_integrations`. Those config-entry-backed groups are not reachable via the\ngroup.remove service.\n\n**When to use:** removing groups created with `ha_config_set_group` or defined in YAML\nvia `group:` configuration. Config-entry-backed deletion tools cannot find these.\n\nEXAMPLES:\n- Remove group: ha_config_remove_group(\"living_room_lights\")\n\nUse ha_config_list_groups() to find existing groups.\n\n**WARNING:**\n- Removing a group used in automations may cause those automations to fail.\n- Groups defined in YAML can be removed at runtime but will reappear after restart.\n- This only removes old-style groups, not platform-specific groups.",
1279+
"description": "Remove a service-based Home Assistant entity group via the group.remove service.\n\n**When NOT to use:** for groups created through `ha_config_set_helper(helper_type=\"group\", ...)`,\nuse `ha_remove_helpers_integrations`. Those config-entry-backed groups are not reachable via the\ngroup.remove service.\n\n**When to use:** removing groups created with `ha_config_set_group` or defined in YAML\nvia `group:` configuration. Config-entry-backed deletion tools cannot find these.\n\nEXAMPLES:\n- Remove group: ha_config_remove_group(\"living_room_lights\")\n\nUse ha_config_list_groups() to find existing groups.\n\n**WARNING:**\n- Removing a group used in automations may cause those automations to fail.\n- Groups defined in YAML can be removed at runtime but will reappear after restart.\n- This only removes old-style groups, not platform-specific groups.",
12801280
"inputSchema": {
12811281
"properties": {
12821282
"object_id": {
@@ -1303,7 +1303,7 @@
13031303
{
13041304
"name": "ha_config_set_group",
13051305
"title": "Create or Update Group",
1306-
"description": "Create or update a service-based Home Assistant entity group via the group.set service.\n\n**When NOT to use:** for typical \"combine these entities into one controllable group\"\nrequests, prefer `ha_config_set_helper(helper_type=\"group\", ...)`. Config-entry-backed\ngroups are registered in the entity registry, so `ha_set_entity` can assign them to\nareas and they are deletable via `ha_delete_helpers_integrations`.\n\n**When to use:** compatibility with existing groups already configured via group.set\nor YAML, or the rare case where entity-registry membership is explicitly unwanted.\nGroups created here are only removable via `ha_config_remove_group` —\n`ha_delete_helpers_integrations` will not find them.\n\n**For NEW groups:** Provide object_id and entities (required).\n**For EXISTING groups:** Provide object_id and any fields to update.\n\nEXAMPLES:\n- Create group: ha_config_set_group(\"bedroom_lights\", entities=[\"light.lamp\", \"light.ceiling\"])\n- Create with name: ha_config_set_group(\"sensors\", entities=[\"sensor.temp\"], name=\"All Sensors\")\n- Update name: ha_config_set_group(\"lights\", name=\"Living Room Lights\")\n- Add entities: ha_config_set_group(\"lights\", add_entities=[\"light.extra\"])\n- Remove entities: ha_config_set_group(\"lights\", remove_entities=[\"light.old\"])\n- Replace all entities: ha_config_set_group(\"lights\", entities=[\"light.new1\", \"light.new2\"])\n\n**NOTE:** entities, add_entities, and remove_entities are mutually exclusive.",
1306+
"description": "Create or update a service-based Home Assistant entity group via the group.set service.\n\n**When NOT to use:** for typical \"combine these entities into one controllable group\"\nrequests, prefer `ha_config_set_helper(helper_type=\"group\", ...)`. Config-entry-backed\ngroups are registered in the entity registry, so `ha_set_entity` can assign them to\nareas and they are deletable via `ha_remove_helpers_integrations`.\n\n**When to use:** compatibility with existing groups already configured via group.set\nor YAML, or the rare case where entity-registry membership is explicitly unwanted.\nGroups created here are only removable via `ha_config_remove_group` —\n`ha_remove_helpers_integrations` will not find them.\n\n**For NEW groups:** Provide object_id and entities (required).\n**For EXISTING groups:** Provide object_id and any fields to update.\n\nEXAMPLES:\n- Create group: ha_config_set_group(\"bedroom_lights\", entities=[\"light.lamp\", \"light.ceiling\"])\n- Create with name: ha_config_set_group(\"sensors\", entities=[\"sensor.temp\"], name=\"All Sensors\")\n- Update name: ha_config_set_group(\"lights\", name=\"Living Room Lights\")\n- Add entities: ha_config_set_group(\"lights\", add_entities=[\"light.extra\"])\n- Remove entities: ha_config_set_group(\"lights\", remove_entities=[\"light.old\"])\n- Replace all entities: ha_config_set_group(\"lights\", entities=[\"light.new1\", \"light.new2\"])\n\n**NOTE:** entities, add_entities, and remove_entities are mutually exclusive.",
13071307
"inputSchema": {
13081308
"properties": {
13091309
"object_id": {
@@ -1668,9 +1668,9 @@
16681668
"source_file": "tools_config_helpers.py"
16691669
},
16701670
{
1671-
"name": "ha_delete_helpers_integrations",
1672-
"title": "Delete Helper or Integration",
1673-
"description": "Delete a Home Assistant helper or integration config entry.\n\nCombines simple-helper websocket deletion, config-entry deletion, and\nconfig-subentry deletion under one entry point with four routing paths\ndriven by helper_type.\n\nWHEN NOT TO USE:\n- Removing only an entity (without deleting its underlying helper or\n config entry) — use `ha_remove_entity` instead.\n- YAML-configured helpers — they have no storage backend. Edit the\n YAML file and reload the relevant integration.\n\nSUPPORTED HELPER TYPES:\n- SIMPLE (12, websocket-delete): input_button, input_boolean,\n input_select, input_number, input_text, input_datetime, counter,\n timer, schedule, zone, person, tag.\n- FLOW (15, config-entry-delete via entity lookup): template, group,\n utility_meter, derivative, min_max, threshold, integration,\n statistics, trend, random, filter, tod, generic_thermostat,\n switch_as_x, generic_hygrostat.\n\nROUTING:\n- SIMPLE helper_type + bare helper_id or entity_id → websocket delete.\n- FLOW helper_type + entity_id → resolve entity_id to config_entry_id\n via entity_registry, then delete the config entry. All sub-entities\n (e.g. utility_meter tariffs) are removed together.\n- helper_type=None + entry_id → direct config entry delete (any\n integration).\n- helper_type=\"config_subentry\" + parent entry_id + subentry_id →\n delete one config subentry.\n\nEXAMPLES:\n- Delete SIMPLE button:\n ha_delete_helpers_integrations(\n target=\"my_button\", helper_type=\"input_button\", confirm=True\n )\n- Delete FLOW utility_meter (any sub-entity works):\n ha_delete_helpers_integrations(\n target=\"sensor.energy_peak\",\n helper_type=\"utility_meter\",\n confirm=True,\n )\n- Delete any integration by entry_id:\n ha_delete_helpers_integrations(\n target=\"01HXYZ...\", confirm=True\n )\n- Delete a config subentry:\n ha_delete_helpers_integrations(\n target=\"01HXYZ...\", helper_type=\"config_subentry\",\n subentry_id=\"subentry-123\", confirm=True\n )\n\n**WARNING:** Deleting a helper or integration that is referenced by\nautomations, scripts, or other integrations may cause those to fail.\nUse ha_search_entities() / ha_get_integration() to verify before\ndeletion. Cannot be undone.",
1671+
"name": "ha_remove_helpers_integrations",
1672+
"title": "Remove Helper or Integration",
1673+
"description": "Remove a Home Assistant helper or integration config entry.\n\nCombines simple-helper websocket deletion, config-entry deletion, and\nconfig-subentry deletion under one entry point with four routing paths\ndriven by helper_type.\n\nWHEN NOT TO USE:\n- Removing only an entity (without deleting its underlying helper or\n config entry) — use `ha_remove_entity` instead.\n- YAML-configured helpers — they have no storage backend. Edit the\n YAML file and reload the relevant integration.\n\nSUPPORTED HELPER TYPES:\n- SIMPLE (12, websocket-delete): input_button, input_boolean,\n input_select, input_number, input_text, input_datetime, counter,\n timer, schedule, zone, person, tag.\n- FLOW (15, config-entry-delete via entity lookup): template, group,\n utility_meter, derivative, min_max, threshold, integration,\n statistics, trend, random, filter, tod, generic_thermostat,\n switch_as_x, generic_hygrostat.\n\nROUTING:\n- SIMPLE helper_type + bare helper_id or entity_id → websocket delete.\n- FLOW helper_type + entity_id → resolve entity_id to config_entry_id\n via entity_registry, then delete the config entry. All sub-entities\n (e.g. utility_meter tariffs) are removed together.\n- helper_type=None + entry_id → direct config entry delete (any\n integration).\n- helper_type=\"config_subentry\" + parent entry_id + subentry_id →\n delete one config subentry.\n\nIDEMPOTENT-SUCCESS CONTRACT:\nA target that is *confirmed absent* returns ``success=True`` with\n``fallback_used=\"already_deleted\"``. This applies per-path:\n- SIMPLE: state-machine empty AND entity registry empty.\n- FLOW: entity_id not in registry (target was never there or\n already removed). YAML-configured helpers (no config entry\n backing) still raise ``RESOURCE_NOT_FOUND`` — that's a\n call-shape conflict, not a missing target.\n- Direct config entry (helper_type=None): backend returns HTTP 404.\n- Config subentry: backend returns a \"not_found\" error.\n\nTransient connectivity failures (WebSocket disconnected, network\ntimeouts) still raise so callers can retry. Idempotency applies\nonly when the target is *known* not to exist.\n\nEXAMPLES:\n- Delete SIMPLE button:\n ha_remove_helpers_integrations(\n target=\"my_button\", helper_type=\"input_button\", confirm=True\n )\n- Delete FLOW utility_meter (any sub-entity works):\n ha_remove_helpers_integrations(\n target=\"sensor.energy_peak\",\n helper_type=\"utility_meter\",\n confirm=True,\n )\n- Delete any integration by entry_id:\n ha_remove_helpers_integrations(\n target=\"01HXYZ...\", confirm=True\n )\n- Delete a config subentry:\n ha_remove_helpers_integrations(\n target=\"01HXYZ...\", helper_type=\"config_subentry\",\n subentry_id=\"subentry-123\", confirm=True\n )\n\n**WARNING:** Deleting a helper or integration that is referenced by\nautomations, scripts, or other integrations may cause those to fail.\nUse ha_search_entities() / ha_get_integration() to verify before\ndeletion. Cannot be undone.",
16741674
"inputSchema": {
16751675
"properties": {
16761676
"target": {
@@ -1698,7 +1698,8 @@
16981698
]
16991699
},
17001700
"annotations": {
1701-
"destructiveHint": true
1701+
"destructiveHint": true,
1702+
"idempotentHint": true
17021703
},
17031704
"tags": [
17041705
"Helper Entities",

src/ha_mcp/tools/tools_groups.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,12 @@ async def ha_config_set_group(
277277
**When NOT to use:** for typical "combine these entities into one controllable group"
278278
requests, prefer `ha_config_set_helper(helper_type="group", ...)`. Config-entry-backed
279279
groups are registered in the entity registry, so `ha_set_entity` can assign them to
280-
areas and they are deletable via `ha_delete_helpers_integrations`.
280+
areas and they are deletable via `ha_remove_helpers_integrations`.
281281
282282
**When to use:** compatibility with existing groups already configured via group.set
283283
or YAML, or the rare case where entity-registry membership is explicitly unwanted.
284284
Groups created here are only removable via `ha_config_remove_group` —
285-
`ha_delete_helpers_integrations` will not find them.
285+
`ha_remove_helpers_integrations` will not find them.
286286
287287
**For NEW groups:** Provide object_id and entities (required).
288288
**For EXISTING groups:** Provide object_id and any fields to update.
@@ -414,7 +414,7 @@ async def ha_config_remove_group(
414414
Remove a service-based Home Assistant entity group via the group.remove service.
415415
416416
**When NOT to use:** for groups created through `ha_config_set_helper(helper_type="group", ...)`,
417-
use `ha_delete_helpers_integrations`. Those config-entry-backed groups are not reachable via the
417+
use `ha_remove_helpers_integrations`. Those config-entry-backed groups are not reachable via the
418418
group.remove service.
419419
420420
**When to use:** removing groups created with `ha_config_set_group` or defined in YAML

0 commit comments

Comments
 (0)