Skip to content

Commit a2386e0

Browse files
committed
fix(backup): use typed client helpers for fetch/restore
HomeAssistantClient doesn't expose a generic client.get(path) method — the convention is to call the typed helpers (get_automation_config, upsert_script_config, etc.). My initial fetcher path tried client.get(...) which raised AttributeError on every call; the decorator's best-effort exception handler swallowed the error and returned None, so no snapshot was written. Switch automation/script/scene fetchers to the existing typed helpers (get_automation_config, get_script_config, get_scene_config) which handle id resolution and unwrap the response envelope identically to how ha_config_set_<domain> itself fetches state. Restore similarly routes through upsert_automation_config / upsert_script_config / upsert_scene_config — note the script/scene signatures take (config, id) not (id, config). Other domains (helpers, dashboards, labels, etc.) use WebSocket so were unaffected; only the REST-fetched domains needed this fix. The remaining _rest_get_or_none / _rest_post helpers are kept for domains without typed wrappers; they now call client._request directly (public 'get' doesn't exist).
1 parent b4c645c commit a2386e0

1 file changed

Lines changed: 41 additions & 25 deletions

File tree

src/ha_mcp/backup_manager.py

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -463,19 +463,25 @@ def get_backup_manager(client: Any, settings: Any) -> BackupManager:
463463

464464

465465
async def _rest_get_or_none(client: Any, path: str) -> Any:
466-
"""Fetch a REST path; return None on 404, propagate other errors."""
466+
"""Fetch via the client's internal ``_request``; return None on 404.
467+
468+
The client doesn't expose a public ``get(path)`` — the convention is
469+
to call the typed wrappers (``get_automation_config``,
470+
``get_states``, etc.) which internally call ``_request``. Domains
471+
without a typed wrapper use this helper.
472+
"""
467473
try:
468-
return await client.get(path)
474+
return await client._request("GET", path)
469475
except Exception as err:
470-
# Detect 404 from the HomeAssistantAPIError shape used by rest_client.
471476
status = getattr(err, "status_code", None)
472477
if status == 404:
473478
return None
474479
raise
475480

476481

477482
async def _rest_post(client: Any, path: str, payload: Any) -> Any:
478-
return await client.post(path, json_data=payload)
483+
"""POST via the client's internal ``_request`` helper."""
484+
return await client._request("POST", path, json=payload)
479485

480486

481487
async def _ws_send(client: Any, message: dict[str, Any]) -> Any:
@@ -508,45 +514,55 @@ async def _ws_send(client: Any, message: dict[str, Any]) -> Any:
508514
return result
509515

510516

511-
# Automation / Script / Scene share the /api/config/<domain>/config/<id> shape.
517+
# Automation / Script / Scene — reuse the typed client helpers, which
518+
# handle id-resolution (entity_id ↔ unique_id) and unwrap response envelopes
519+
# identically to how ``ha_config_set_<domain>`` itself fetches state for
520+
# the existing optimistic-locking flow. Going through these helpers
521+
# guarantees the snapshot's ``config`` shape matches what the restorer
522+
# will re-POST.
512523

513524

514525
async def _fetch_automation(client: Any, entity_id: str) -> Any:
515-
eid = (
516-
entity_id[len("automation.") :]
517-
if entity_id.startswith("automation.")
518-
else entity_id
519-
)
520-
return await _rest_get_or_none(client, f"/api/config/automation/config/{eid}")
526+
try:
527+
return await client.get_automation_config(entity_id)
528+
except Exception as err:
529+
if getattr(err, "status_code", None) == 404:
530+
return None
531+
raise
521532

522533

523534
async def _restore_automation(client: Any, entity_id: str, config: Any) -> Any:
524-
eid = (
525-
entity_id[len("automation.") :]
526-
if entity_id.startswith("automation.")
527-
else entity_id
528-
)
529-
return await _rest_post(client, f"/api/config/automation/config/{eid}", config)
535+
return await client.upsert_automation_config(config, identifier=entity_id)
530536

531537

532538
async def _fetch_script(client: Any, entity_id: str) -> Any:
533-
sid = entity_id[len("script.") :] if entity_id.startswith("script.") else entity_id
534-
return await _rest_get_or_none(client, f"/api/config/script/config/{sid}")
539+
try:
540+
result = await client.get_script_config(entity_id)
541+
except Exception as err:
542+
if getattr(err, "status_code", None) == 404:
543+
return None
544+
raise
545+
# get_script_config returns a wrapper {"config": <body>, "script_id": ...};
546+
# the inner body is what upsert_script_config takes.
547+
return result.get("config", result) if isinstance(result, dict) else result
535548

536549

537550
async def _restore_script(client: Any, entity_id: str, config: Any) -> Any:
538-
sid = entity_id[len("script.") :] if entity_id.startswith("script.") else entity_id
539-
return await _rest_post(client, f"/api/config/script/config/{sid}", config)
551+
return await client.upsert_script_config(config, entity_id)
540552

541553

542554
async def _fetch_scene(client: Any, entity_id: str) -> Any:
543-
sid = entity_id[len("scene.") :] if entity_id.startswith("scene.") else entity_id
544-
return await _rest_get_or_none(client, f"/api/config/scene/config/{sid}")
555+
try:
556+
result = await client.get_scene_config(entity_id)
557+
except Exception as err:
558+
if getattr(err, "status_code", None) == 404:
559+
return None
560+
raise
561+
return result.get("config", result) if isinstance(result, dict) else result
545562

546563

547564
async def _restore_scene(client: Any, entity_id: str, config: Any) -> Any:
548-
sid = entity_id[len("scene.") :] if entity_id.startswith("scene.") else entity_id
549-
return await _rest_post(client, f"/api/config/scene/config/{sid}", config)
565+
return await client.upsert_scene_config(config, entity_id)
550566

551567

552568
# Dashboards — WS lovelace/config (fetch) and lovelace/config/save (restore).

0 commit comments

Comments
 (0)