Skip to content

Commit 32ea392

Browse files
docs(internal): refresh ha_manage_addon ESPHome dashboard guidance (#1656)
* docs(internal): refresh ha_manage_addon ESPHome dashboard guidance The ESPHome Device Builder dashboard was rewritten (aiohttp SPA, server 0.1.0b100). Its old surface — GET /edit?configuration=, and the /compile /validate /logs WebSocket paths with {"type": "spawn"} bodies — is gone and now returns the SPA shell. The ha_manage_addon docstring, the websocket/wait_for_close field descriptions, and the WS examples still described that old protocol, sending agents to dead ends. Update them to the current API: HTTP GET /devices for the device list, and the WebSocket /ws command channel ({command, message_id, args}) with devices/get_config, devices/update_config, devices/logs, devices/validate, firmware/compile. Note the one-shot pattern (wait_for_close=False + message_limit) and that direct port= does not reach this dashboard. Also refresh the matching stale /compile example in read_only.py. Verified against a live ESPHome 2026.5.3 dashboard: the old /logs WS path now returns HTTP 200 (SPA); /ws devices/get_config and devices/logs work. Docs-only (docstrings + one comment); no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(internal): tighten ESPHome WS guidance per review Address pr-review-toolkit comment-analyzer findings: (1) separate wire-confirmed commands (devices/get_config, devices/logs) from frontend-only ones (devices/update_config, devices/validate, firmware/compile) so confidence is not overstated; (2) split the legacy-removal note precisely — GET /edit returns the SPA, the old WS paths reject the upgrade (HTTP 200) — instead of lumping both as "return the SPA". Also note body doubles as the WS initial message in its Field description. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: kingpanther13 <kingpanther13@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d0c0e21 commit 32ea392

2 files changed

Lines changed: 41 additions & 14 deletions

File tree

src/ha_mcp/read_only.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ def _addon_write(args: dict[str, Any]) -> str | None:
9393
return "array_patch modification"
9494
if args.get("websocket"):
9595
# A WebSocket session's initial message can command mutations
96-
# (e.g. ESPHome /compile), so it is not statically classifiable
97-
# as a read — fail closed.
96+
# (e.g. an ESPHome firmware/compile or devices/update_config command),
97+
# so it is not statically classifiable as a read — fail closed.
9898
return "WebSocket proxy session"
9999
method = str(args.get("method") or "GET").strip().upper()
100100
if method != "GET":

src/ha_mcp/tools/tools_addons.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -958,7 +958,7 @@ async def _call_addon_ws(
958958
Args:
959959
client: Home Assistant REST client
960960
slug: Add-on slug (e.g., "<prefix>_esphome")
961-
path: WebSocket endpoint path (e.g., "/compile", "/validate")
961+
path: WebSocket endpoint path (e.g., "/ws" for the ESPHome dashboard's command channel)
962962
body: Message to send after connecting (JSON-encoded if dict, raw if string)
963963
timeout: Max seconds to wait for messages (default 60)
964964
debug: Include diagnostic info
@@ -2622,7 +2622,7 @@ async def ha_manage_addon(
26222622
body: Annotated[
26232623
dict[str, Any] | str | None,
26242624
Field(
2625-
description="Proxy mode only. Request body for POST/PUT/PATCH. Pass a JSON object or JSON string.",
2625+
description="Proxy mode only. Request body for POST/PUT/PATCH — or, with websocket=True, the initial WebSocket message. Pass a JSON object or JSON string.",
26262626
default=None,
26272627
),
26282628
] = None,
@@ -2658,17 +2658,20 @@ async def ha_manage_addon(
26582658
websocket: Annotated[
26592659
bool,
26602660
Field(
2661-
description="Proxy mode only. Use WebSocket instead of HTTP. For streaming endpoints "
2662-
"(e.g., ESPHome /compile, /validate). Sends 'body' as initial message, "
2663-
"collects responses. Default: false.",
2661+
description="Proxy mode only. Use WebSocket instead of HTTP — for an add-on's "
2662+
"WebSocket API (e.g. the ESPHome dashboard's '/ws' command channel; see the "
2663+
"docstring's ESPHome section). Sends 'body' as the initial message, collects "
2664+
"responses. Default: false.",
26642665
default=False,
26652666
),
26662667
] = False,
26672668
wait_for_close: Annotated[
26682669
bool,
26692670
Field(
2670-
description="Proxy mode only. WebSocket: True: wait for server to close (for compile/validate). "
2671-
"False: return after first response batch (for quick commands). Default: true.",
2671+
description="Proxy mode only. WebSocket: True: wait for the server to close the stream "
2672+
"(run-to-completion ops like an ESPHome compile/validate). False: return after the first "
2673+
"response batch — use for a one-shot command/response or a bounded log capture on a channel "
2674+
"that stays open (e.g. ESPHome '/ws'). Default: true.",
26722675
default=True,
26732676
),
26742677
] = True,
@@ -2839,6 +2842,29 @@ async def ha_manage_addon(
28392842
share Home Assistant's container network (i.e. only the HAOS addon).
28402843
Use ha_get_addon(slug="...") to discover available ports and endpoints.
28412844
2845+
**ESPHome Device Builder dashboard (current rewrite):** config and log
2846+
access is a WebSocket JSON-command API, NOT REST. The legacy endpoints
2847+
are gone — `GET /edit?configuration=` now returns the dashboard SPA, and
2848+
the old `/compile` `/validate` `/logs` WebSocket paths (which took
2849+
`{"type": "spawn", ...}` bodies) reject the upgrade (HTTP 200). Use
2850+
instead:
2851+
- HTTP `GET /devices` → JSON list of configured devices; each entry's
2852+
`configuration` field is the YAML filename to pass below.
2853+
- WebSocket `path="/ws"` with body
2854+
`{"command": "<cmd>", "message_id": "1", "args": {...}}`. The server
2855+
sends a `server_info` message first, then one reply per `message_id`.
2856+
Wire-confirmed commands: `devices/get_config` `{configuration}` → raw
2857+
YAML (in the reply's `result`); `devices/logs` (stream)
2858+
`{configuration, port: "OTA"}` → live device logs. Also exposed by the
2859+
dashboard frontend (command/arg names not wire-tested here):
2860+
`devices/update_config` `{configuration, content}` → save,
2861+
`devices/validate`, `firmware/compile`.
2862+
- The `/ws` channel stays open, so for a one-shot read or a bounded log
2863+
capture pass `wait_for_close=False` with `message_limit` (and
2864+
`message_offset` to skip the server_info / config-banner preamble).
2865+
Reach the dashboard through Ingress — omit `port`; direct `port=` does
2866+
not route to it.
2867+
28422868
**Array-patch mode** (when path AND array_patch are provided):
28432869
Atomic "GET array, mutate, POST array" workflow for addon APIs whose write
28442870
contract is "send the whole resource collection back". Operations are applied
@@ -2847,8 +2873,8 @@ async def ha_manage_addon(
28472873
full array. Designed for Node-RED /flows and similar endpoints.
28482874
28492875
**Response shaping (proxy mode):**
2850-
- WebSocket streams can be noisy (ESPHome /validate often emits hundreds of
2851-
config-dump lines). By default, `summarize=True` collapses long runs of
2876+
- WebSocket streams can be noisy (e.g. the ESPHome dashboard's devices/logs
2877+
dumps the device's full config banner on connect). By default, `summarize=True` collapses long runs of
28522878
non-signal messages into short elision markers; INFO/WARNING/ERROR/exit
28532879
lines always pass through. Pagination via `message_offset` / `message_limit`
28542880
works on the raw collected list before summarize runs.
@@ -2881,9 +2907,10 @@ async def ha_manage_addon(
28812907
- Set boot mode: ha_manage_addon(slug="...", boot="manual")
28822908
- Call HTTP API: ha_manage_addon(slug="...", path="/api/events")
28832909
- Direct port: ha_manage_addon(slug="...", path="/flows", port=1880)
2884-
- WebSocket: ha_manage_addon(slug="...", path="/validate", port=6052, websocket=True, body={"type": "spawn", "configuration": "device.yaml"})
2885-
- Quick WS health check (50 msgs, raw): ha_manage_addon(slug="...", path="/logs", websocket=True, message_limit=50, summarize=False)
2886-
- Filter WS errors only: ha_manage_addon(slug="...", path="/validate", websocket=True, python_transform="response = [m for m in response if 'ERROR' in str(m) or 'WARN' in str(m)]")
2910+
- ESPHome list devices (HTTP): ha_manage_addon(slug="<prefix>_esphome", path="/devices")
2911+
- ESPHome read a device's YAML (WS one-shot): ha_manage_addon(slug="<prefix>_esphome", path="/ws", websocket=True, wait_for_close=False, message_limit=2, body={"command": "devices/get_config", "message_id": "1", "args": {"configuration": "device.yaml"}})
2912+
- ESPHome live logs (WS, bounded): ha_manage_addon(slug="<prefix>_esphome", path="/ws", websocket=True, wait_for_close=False, message_limit=60, body={"command": "devices/logs", "message_id": "1", "args": {"configuration": "device.yaml", "port": "OTA"}})
2913+
- Filter WS errors only: ha_manage_addon(slug="...", path="/ws", websocket=True, python_transform="response = [m for m in response if 'ERROR' in str(m) or 'WARN' in str(m)]")
28872914
- HTTP subset: ha_manage_addon(slug="...", path="/flows", python_transform="response = [f['id'] for f in response]")
28882915
- Array-patch (Node-RED, rename a node):
28892916
ha_manage_addon(

0 commit comments

Comments
 (0)