Skip to content

Commit d4c19ec

Browse files
committed
Merge remote-tracking branch 'upstream/master' into issue-966-per-tool-approval
# Conflicts: # src/ha_mcp/settings_ui.py
2 parents 7252b11 + 1631ad1 commit d4c19ec

8 files changed

Lines changed: 2246 additions & 284 deletions

File tree

homeassistant-addon-dev/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: "Home Assistant MCP Server (Dev)"
22
description: "Development channel - AI assistant integration via MCP (unstable)"
3-
version: "7.5.0.dev347"
3+
version: "7.5.0.dev349"
44
slug: "ha_mcp_dev"
55
url: "https://github.com/homeassistant-ai/ha-mcp"
66
stage: experimental

site/src/data/tools.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2500,7 +2500,7 @@
25002500
"default": false
25012501
},
25022502
"fields": {
2503-
"type": "Annotated[str | list[str] | None, Field(default=None, description='Return only the specified top-level response keys to reduce response size (e.g. [\"system_info\", \"domains\"]). None = full response (default). Available keys: success, system_summary, domain_stats, area_analysis, ai_insights, pagination, partial, warnings, device_types, service_availability, system_info, notification_count, notifications, repair_count, repairs, repairs_error, tool_discovery. The ``settings_url`` field (stdio mode only, see tool description) is not subject to this projection — it is always included when the settings-UI sidecar is running.')]",
2503+
"type": "Annotated[str | list[str] | None, Field(default=None, description='Return only the specified top-level response keys to reduce response size (e.g. [\"system_info\", \"domains\"]). None = full response (default). Available keys: success, system_summary, domain_stats, area_analysis, ai_insights, pagination, partial, warnings, device_types, service_availability, system_info, notification_count, notifications, repair_count, dismissed_repair_count, repairs, repairs_error, tool_discovery, settings_url. Note: ``settings_url`` (stdio mode only, see tool description) is emitted regardless of ``fields=`` projection — it is always included when the settings-UI sidecar is running.')]",
25042504
"default": null
25052505
}
25062506
}

src/ha_mcp/settings_ui.py

Lines changed: 738 additions & 142 deletions
Large diffs are not rendered by default.

src/ha_mcp/tools/tools_search.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,11 +1230,12 @@ async def ha_get_overview(
12301230
"Available keys: success, system_summary, domain_stats, "
12311231
"area_analysis, ai_insights, pagination, partial, warnings, "
12321232
"device_types, service_availability, system_info, "
1233-
"notification_count, notifications, repair_count, repairs, "
1234-
"repairs_error, tool_discovery. The ``settings_url`` "
1235-
"field (stdio mode only, see tool description) is not "
1236-
"subject to this projection — it is always included "
1237-
"when the settings-UI sidecar is running."
1233+
"notification_count, notifications, repair_count, "
1234+
"dismissed_repair_count, repairs, repairs_error, "
1235+
"tool_discovery, settings_url. Note: ``settings_url`` "
1236+
"(stdio mode only, see tool description) is emitted "
1237+
"regardless of ``fields=`` projection — it is always "
1238+
"included when the settings-UI sidecar is running."
12381239
),
12391240
),
12401241
] = None,

tests/src/e2e/haos_only/test_addon_lifecycle.py

Lines changed: 16 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,17 @@
77
Two coverage modes:
88
99
1. **Running addons** (Node-RED, ESPHome Device Builder, Matter Server,
10-
AppDaemon — all ``start=True`` in the bake): full lifecycle round-trip
11-
plus options-get / options-set persistence and a log-shape check.
12-
Each test leaves the addon in ``started`` state via an explicit
13-
cleanup call so later tests in the same session find it running.
10+
AppDaemon — all ``start=True`` in the bake): options-get / options-set
11+
persistence and a log-shape check. Each test leaves the addon in
12+
``started`` state via an explicit cleanup call so later tests in the
13+
same session find it running.
14+
15+
The full ``hassio.addon_stop`` → ``addon_start`` → ``addon_restart``
16+
lifecycle round-trip is exercised by **Matter Server only**. All four
17+
roundtrip variants used the same ``_addon_action`` helper hitting the
18+
same MCP wire path, so one is sufficient signal; Matter Server is the
19+
lightest addon to cycle (consistently <13s) and Node-RED's roundtrip
20+
produced a recurring Supervisor 500 flake — see #1414.
1421
1522
2. **Stopped addons** (Mosquitto, MQTT IO — stay ``start=False`` because
1623
their schemas require config they don't have in the bake): exercise
@@ -87,9 +94,7 @@ async def _resolve_slug(mcp_client: Any, display_name: str) -> str:
8794
for entry in payload.get("addons", []):
8895
if entry.get("name") == display_name:
8996
slug = entry.get("slug")
90-
assert slug, (
91-
f"Addon {display_name!r} listed but has no slug field: {entry}"
92-
)
97+
assert slug, f"Addon {display_name!r} listed but has no slug field: {entry}"
9398
return str(slug)
9499

95100
# Filter ``None`` names so a future Supervisor addition with a
@@ -116,9 +121,7 @@ async def _get_addon_detail(mcp_client: Any, slug: str) -> dict[str, Any]:
116121
return detail
117122

118123

119-
async def _addon_action(
120-
mcp_client: Any, slug: str, action: str
121-
) -> dict[str, Any]:
124+
async def _addon_action(mcp_client: Any, slug: str, action: str) -> dict[str, Any]:
122125
"""Invoke ``hassio.addon_{action}`` via ``ha_call_service``.
123126
124127
Returns the parsed result (success or failure). Caller decides how
@@ -209,36 +212,6 @@ async def _ensure_started(mcp_client: Any, slug: str) -> None:
209212
# ---------------------------------------------------------------------------
210213

211214

212-
async def test_nodered_start_stop_restart_roundtrip(mcp_client: Any) -> None:
213-
"""Stop → start → restart cycles Node-RED via real Supervisor.
214-
215-
Each transition is verified by re-reading ``ha_get_addon(slug=...)``
216-
and asserting the state moved as expected. Final assertion plus the
217-
cleanup hook leave the addon ``started`` for downstream tests.
218-
"""
219-
slug = await _resolve_slug(mcp_client, NODERED_NAME)
220-
try:
221-
stop_result = await _addon_action(mcp_client, slug, "stop")
222-
assert stop_result.get("success"), (
223-
f"hassio.addon_stop({slug}) failed: {stop_result}"
224-
)
225-
await _wait_for_state(mcp_client, slug, STOPPED_STATES)
226-
227-
start_result = await _addon_action(mcp_client, slug, "start")
228-
assert start_result.get("success"), (
229-
f"hassio.addon_start({slug}) failed: {start_result}"
230-
)
231-
await _wait_for_state(mcp_client, slug, "started")
232-
233-
restart_result = await _addon_action(mcp_client, slug, "restart")
234-
assert restart_result.get("success"), (
235-
f"hassio.addon_restart({slug}) failed: {restart_result}"
236-
)
237-
await _wait_for_state(mcp_client, slug, "started")
238-
finally:
239-
await _ensure_started(mcp_client, slug)
240-
241-
242215
async def test_nodered_options_get_returns_dict(mcp_client: Any) -> None:
243216
"""`ha_get_addon(slug=...)` exposes the addon's options as a dict."""
244217
slug = await _resolve_slug(mcp_client, NODERED_NAME)
@@ -295,9 +268,7 @@ async def test_nodered_options_set_persists(mcp_client: Any) -> None:
295268
ok = write_payload.get("success") is True or (
296269
write_payload.get("status") == "pending_restart"
297270
)
298-
assert ok, (
299-
f"ha_manage_addon options probe write failed: {write_payload}"
300-
)
271+
assert ok, f"ha_manage_addon options probe write failed: {write_payload}"
301272

302273
detail_after = await _get_addon_detail(mcp_client, slug)
303274
options_after = detail_after.get("options") or {}
@@ -344,31 +315,6 @@ async def test_nodered_logs_fetch_shape(mcp_client: Any) -> None:
344315
# ---------------------------------------------------------------------------
345316

346317

347-
async def test_esphome_start_stop_restart_roundtrip(mcp_client: Any) -> None:
348-
"""Stop → start → restart cycles ESPHome via real Supervisor."""
349-
slug = await _resolve_slug(mcp_client, ESPHOME_NAME)
350-
try:
351-
stop_result = await _addon_action(mcp_client, slug, "stop")
352-
assert stop_result.get("success"), (
353-
f"hassio.addon_stop({slug}) failed: {stop_result}"
354-
)
355-
await _wait_for_state(mcp_client, slug, STOPPED_STATES)
356-
357-
start_result = await _addon_action(mcp_client, slug, "start")
358-
assert start_result.get("success"), (
359-
f"hassio.addon_start({slug}) failed: {start_result}"
360-
)
361-
await _wait_for_state(mcp_client, slug, "started")
362-
363-
restart_result = await _addon_action(mcp_client, slug, "restart")
364-
assert restart_result.get("success"), (
365-
f"hassio.addon_restart({slug}) failed: {restart_result}"
366-
)
367-
await _wait_for_state(mcp_client, slug, "started")
368-
finally:
369-
await _ensure_started(mcp_client, slug)
370-
371-
372318
async def test_esphome_options_get_returns_dict(mcp_client: Any) -> None:
373319
"""`ha_get_addon(slug=...)` exposes ESPHome's options as a dict."""
374320
slug = await _resolve_slug(mcp_client, ESPHOME_NAME)
@@ -417,9 +363,7 @@ async def test_esphome_options_set_persists(mcp_client: Any) -> None:
417363
ok = write_payload.get("success") is True or (
418364
write_payload.get("status") == "pending_restart"
419365
)
420-
assert ok, (
421-
f"ha_manage_addon options probe write failed: {write_payload}"
422-
)
366+
assert ok, f"ha_manage_addon options probe write failed: {write_payload}"
423367

424368
detail_after = await _get_addon_detail(mcp_client, slug)
425369
options_after = detail_after.get("options") or {}
@@ -540,9 +484,7 @@ async def test_matter_server_options_set_persists(mcp_client: Any) -> None:
540484
ok = write_payload.get("success") is True or (
541485
write_payload.get("status") == "pending_restart"
542486
)
543-
assert ok, (
544-
f"ha_manage_addon options probe write failed: {write_payload}"
545-
)
487+
assert ok, f"ha_manage_addon options probe write failed: {write_payload}"
546488

547489
detail_after = await _get_addon_detail(mcp_client, slug)
548490
options_after = detail_after.get("options") or {}
@@ -582,31 +524,6 @@ async def test_matter_server_logs_fetch_shape(mcp_client: Any) -> None:
582524
# ---------------------------------------------------------------------------
583525

584526

585-
async def test_appdaemon_start_stop_restart_roundtrip(mcp_client: Any) -> None:
586-
"""Stop → start → restart cycles AppDaemon via real Supervisor."""
587-
slug = await _resolve_slug(mcp_client, APPDAEMON_NAME)
588-
try:
589-
stop_result = await _addon_action(mcp_client, slug, "stop")
590-
assert stop_result.get("success"), (
591-
f"hassio.addon_stop({slug}) failed: {stop_result}"
592-
)
593-
await _wait_for_state(mcp_client, slug, STOPPED_STATES)
594-
595-
start_result = await _addon_action(mcp_client, slug, "start")
596-
assert start_result.get("success"), (
597-
f"hassio.addon_start({slug}) failed: {start_result}"
598-
)
599-
await _wait_for_state(mcp_client, slug, "started")
600-
601-
restart_result = await _addon_action(mcp_client, slug, "restart")
602-
assert restart_result.get("success"), (
603-
f"hassio.addon_restart({slug}) failed: {restart_result}"
604-
)
605-
await _wait_for_state(mcp_client, slug, "started")
606-
finally:
607-
await _ensure_started(mcp_client, slug)
608-
609-
610527
async def test_appdaemon_webui_no_ingress_shape(mcp_client: Any) -> None:
611528
"""`ha_get_addon(slug=...)` reports ``webui`` set + ``ingress=false`` for AppDaemon.
612529

0 commit comments

Comments
 (0)