Skip to content

Commit 84d2686

Browse files
Don't register Home Assistant Cloud LLM platforms if not logged in (home-assistant#157630)
1 parent ae8980c commit 84d2686

File tree

8 files changed

+51
-121
lines changed

8 files changed

+51
-121
lines changed

homeassistant/components/cloud/__init__.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
import asyncio
66
from collections.abc import Awaitable, Callable
7+
from contextlib import suppress
78
from datetime import datetime, timedelta
89
from enum import Enum
910
import logging
1011
from typing import Any, cast
1112

12-
from hass_nabucasa import Cloud
13+
from hass_nabucasa import Cloud, NabuCasaBaseError
1314
import voluptuous as vol
1415

1516
from homeassistant.components import alexa, google_assistant
@@ -78,13 +79,16 @@
7879
DEFAULT_MODE = MODE_PROD
7980

8081
PLATFORMS = [
81-
Platform.AI_TASK,
8282
Platform.BINARY_SENSOR,
83-
Platform.CONVERSATION,
8483
Platform.STT,
8584
Platform.TTS,
8685
]
8786

87+
LLM_PLATFORMS = [
88+
Platform.AI_TASK,
89+
Platform.CONVERSATION,
90+
]
91+
8892
SERVICE_REMOTE_CONNECT = "remote_connect"
8993
SERVICE_REMOTE_DISCONNECT = "remote_disconnect"
9094

@@ -431,7 +435,14 @@ async def on_prefs_updated(prefs: CloudPreferences) -> None:
431435

432436
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
433437
"""Set up a config entry."""
434-
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
438+
platforms = PLATFORMS.copy()
439+
if (cloud := hass.data[DATA_CLOUD]).is_logged_in:
440+
with suppress(NabuCasaBaseError):
441+
await cloud.llm.async_ensure_token()
442+
platforms += LLM_PLATFORMS
443+
444+
await hass.config_entries.async_forward_entry_setups(entry, platforms)
445+
entry.runtime_data = {"platforms": platforms}
435446
stt_tts_entities_added = hass.data[DATA_PLATFORMS_SETUP]["stt_tts_entities_added"]
436447
stt_tts_entities_added.set()
437448

@@ -440,7 +451,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
440451

441452
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
442453
"""Unload a config entry."""
443-
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
454+
return await hass.config_entries.async_unload_platforms(
455+
entry, entry.runtime_data["platforms"]
456+
)
444457

445458

446459
@callback

homeassistant/components/cloud/ai_task.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from json import JSONDecodeError
77
import logging
88

9-
from hass_nabucasa import NabuCasaBaseError
109
from hass_nabucasa.llm import (
1110
LLMAuthenticationError,
1211
LLMError,
@@ -20,7 +19,7 @@
2019
from homeassistant.components import ai_task, conversation
2120
from homeassistant.config_entries import ConfigEntry
2221
from homeassistant.core import HomeAssistant
23-
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
22+
from homeassistant.exceptions import HomeAssistantError
2423
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
2524
from homeassistant.util.json import json_loads
2625

@@ -94,17 +93,11 @@ async def async_setup_entry(
9493
async_add_entities: AddConfigEntryEntitiesCallback,
9594
) -> None:
9695
"""Set up Home Assistant Cloud AI Task entity."""
97-
if not (cloud := hass.data[DATA_CLOUD]).is_logged_in:
98-
return
99-
try:
100-
await cloud.llm.async_ensure_token()
101-
except (LLMError, NabuCasaBaseError):
102-
return
96+
cloud = hass.data[DATA_CLOUD]
97+
async_add_entities([CloudAITaskEntity(cloud, config_entry)])
10398

104-
async_add_entities([CloudLLMTaskEntity(cloud, config_entry)])
10599

106-
107-
class CloudLLMTaskEntity(ai_task.AITaskEntity, BaseCloudLLMEntity):
100+
class CloudAITaskEntity(BaseCloudLLMEntity, ai_task.AITaskEntity):
108101
"""Home Assistant Cloud AI Task entity."""
109102

110103
_attr_has_entity_name = True
@@ -181,7 +174,7 @@ async def _async_generate_image(
181174
attachments=attachments,
182175
)
183176
except LLMAuthenticationError as err:
184-
raise ConfigEntryAuthFailed("Cloud LLM authentication failed") from err
177+
raise HomeAssistantError("Cloud LLM authentication failed") from err
185178
except LLMRateLimitError as err:
186179
raise HomeAssistantError("Cloud LLM is rate limited") from err
187180
except LLMResponseError as err:

homeassistant/components/cloud/conversation.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44

55
from typing import Literal
66

7-
from hass_nabucasa import NabuCasaBaseError
8-
from hass_nabucasa.llm import LLMError
9-
107
from homeassistant.components import conversation
118
from homeassistant.config_entries import ConfigEntry
129
from homeassistant.const import MATCH_ALL
@@ -24,19 +21,13 @@ async def async_setup_entry(
2421
async_add_entities: AddConfigEntryEntitiesCallback,
2522
) -> None:
2623
"""Set up the Home Assistant Cloud conversation entity."""
27-
if not (cloud := hass.data[DATA_CLOUD]).is_logged_in:
28-
return
29-
try:
30-
await cloud.llm.async_ensure_token()
31-
except (LLMError, NabuCasaBaseError):
32-
return
33-
24+
cloud = hass.data[DATA_CLOUD]
3425
async_add_entities([CloudConversationEntity(cloud, config_entry)])
3526

3627

3728
class CloudConversationEntity(
38-
conversation.ConversationEntity,
3929
BaseCloudLLMEntity,
30+
conversation.ConversationEntity,
4031
):
4132
"""Home Assistant Cloud conversation agent."""
4233

homeassistant/components/cloud/entity.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@
88
import re
99
from typing import Any, Literal, cast
1010

11-
from hass_nabucasa import Cloud
11+
from hass_nabucasa import Cloud, NabuCasaBaseError
1212
from hass_nabucasa.llm import (
1313
LLMAuthenticationError,
14-
LLMError,
1514
LLMRateLimitError,
1615
LLMResponseError,
1716
LLMServiceError,
@@ -37,7 +36,7 @@
3736

3837
from homeassistant.components import conversation
3938
from homeassistant.config_entries import ConfigEntry
40-
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
39+
from homeassistant.exceptions import HomeAssistantError
4140
from homeassistant.helpers import llm
4241
from homeassistant.helpers.entity import Entity
4342
from homeassistant.util import slugify
@@ -601,14 +600,14 @@ async def _async_handle_chat_log(
601600
)
602601

603602
except LLMAuthenticationError as err:
604-
raise ConfigEntryAuthFailed("Cloud LLM authentication failed") from err
603+
raise HomeAssistantError("Cloud LLM authentication failed") from err
605604
except LLMRateLimitError as err:
606605
raise HomeAssistantError("Cloud LLM is rate limited") from err
607606
except LLMResponseError as err:
608607
raise HomeAssistantError(str(err)) from err
609608
except LLMServiceError as err:
610609
raise HomeAssistantError("Error talking to Cloud LLM") from err
611-
except LLMError as err:
610+
except NabuCasaBaseError as err:
612611
raise HomeAssistantError(str(err)) from err
613612

614613
if not chat_log.unresponded_tool_results:

tests/components/cloud/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ async def cloud_fixture() -> AsyncGenerator[MagicMock]:
8585
return_value=lambda: "mock-unregister"
8686
),
8787
)
88+
mock_cloud.llm = MagicMock(async_ensure_token=AsyncMock())
8889

8990
def set_up_mock_cloud(
9091
cloud_client: CloudClient, mode: str, **kwargs: Any

tests/components/cloud/snapshots/test_http_api.ambr

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
## Active Integrations
2323

24-
Built-in integrations: 19
24+
Built-in integrations: 21
2525
Custom integrations: 1
2626

2727
<details><summary>Built-in integrations</summary>
@@ -32,7 +32,9 @@
3232
auth | Auth
3333
binary_sensor | Binary Sensor
3434
cloud | Home Assistant Cloud
35+
cloud.ai_task | Unknown
3536
cloud.binary_sensor | Unknown
37+
cloud.conversation | Unknown
3638
cloud.stt | Unknown
3739
cloud.tts | Unknown
3840
conversation | Conversation
@@ -120,7 +122,7 @@
120122

121123
## Active Integrations
122124

123-
Built-in integrations: 19
125+
Built-in integrations: 21
124126
Custom integrations: 0
125127

126128
<details><summary>Built-in integrations</summary>
@@ -131,7 +133,9 @@
131133
auth | Auth
132134
binary_sensor | Binary Sensor
133135
cloud | Home Assistant Cloud
136+
cloud.ai_task | Unknown
134137
cloud.binary_sensor | Unknown
138+
cloud.conversation | Unknown
135139
cloud.stt | Unknown
136140
cloud.tts | Unknown
137141
conversation | Conversation

tests/components/cloud/test_ai_task.py

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,18 @@
1919

2020
from homeassistant.components import ai_task, conversation
2121
from homeassistant.components.cloud.ai_task import (
22-
CloudLLMTaskEntity,
22+
CloudAITaskEntity,
2323
async_prepare_image_generation_attachments,
24-
async_setup_entry,
2524
)
26-
from homeassistant.components.cloud.const import DATA_CLOUD
2725
from homeassistant.core import HomeAssistant
28-
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
26+
from homeassistant.exceptions import HomeAssistantError
2927

3028
from tests.common import MockConfigEntry
3129

3230

3331
@pytest.fixture
34-
def mock_cloud_ai_task_entity(hass: HomeAssistant) -> CloudLLMTaskEntity:
35-
"""Return a CloudLLMTaskEntity with a mocked cloud LLM."""
32+
def mock_cloud_ai_task_entity(hass: HomeAssistant) -> CloudAITaskEntity:
33+
"""Return a CloudAITaskEntity with a mocked cloud LLM."""
3634
cloud = MagicMock()
3735
cloud.llm = MagicMock(
3836
async_generate_image=AsyncMock(),
@@ -42,32 +40,17 @@ def mock_cloud_ai_task_entity(hass: HomeAssistant) -> CloudLLMTaskEntity:
4240
cloud.valid_subscription = True
4341
entry = MockConfigEntry(domain="cloud")
4442
entry.add_to_hass(hass)
45-
entity = CloudLLMTaskEntity(cloud, entry)
43+
entity = CloudAITaskEntity(cloud, entry)
4644
entity.entity_id = "ai_task.cloud_ai_task"
4745
entity.hass = hass
4846
return entity
4947

5048

51-
async def test_setup_entry_skips_when_not_logged_in(
52-
hass: HomeAssistant,
53-
) -> None:
54-
"""Test setup_entry exits early when not logged in."""
55-
cloud = MagicMock()
56-
cloud.is_logged_in = False
57-
entry = MockConfigEntry(domain="cloud")
58-
entry.add_to_hass(hass)
59-
hass.data[DATA_CLOUD] = cloud
60-
61-
async_add_entities = AsyncMock()
62-
await async_setup_entry(hass, entry, async_add_entities)
63-
async_add_entities.assert_not_called()
64-
65-
6649
@pytest.fixture(name="mock_handle_chat_log")
6750
def mock_handle_chat_log_fixture() -> AsyncMock:
6851
"""Patch the chat log handler."""
6952
with patch(
70-
"homeassistant.components.cloud.ai_task.CloudLLMTaskEntity._async_handle_chat_log",
53+
"homeassistant.components.cloud.ai_task.CloudAITaskEntity._async_handle_chat_log",
7154
AsyncMock(),
7255
) as mock:
7356
yield mock
@@ -171,7 +154,7 @@ async def test_prepare_image_generation_attachments_processing_error(
171154

172155
async def test_generate_data_returns_text(
173156
hass: HomeAssistant,
174-
mock_cloud_ai_task_entity: CloudLLMTaskEntity,
157+
mock_cloud_ai_task_entity: CloudAITaskEntity,
175158
mock_handle_chat_log: AsyncMock,
176159
) -> None:
177160
"""Test generating plain text data."""
@@ -200,7 +183,7 @@ async def fake_handle(chat_type, log, task_name, structure):
200183

201184
async def test_generate_data_returns_json(
202185
hass: HomeAssistant,
203-
mock_cloud_ai_task_entity: CloudLLMTaskEntity,
186+
mock_cloud_ai_task_entity: CloudAITaskEntity,
204187
mock_handle_chat_log: AsyncMock,
205188
) -> None:
206189
"""Test generating structured data."""
@@ -228,7 +211,7 @@ async def fake_handle(chat_type, log, task_name, structure):
228211

229212
async def test_generate_data_invalid_json(
230213
hass: HomeAssistant,
231-
mock_cloud_ai_task_entity: CloudLLMTaskEntity,
214+
mock_cloud_ai_task_entity: CloudAITaskEntity,
232215
mock_handle_chat_log: AsyncMock,
233216
) -> None:
234217
"""Test invalid JSON responses raise an error."""
@@ -256,7 +239,7 @@ async def fake_handle(chat_type, log, task_name, structure):
256239

257240

258241
async def test_generate_image_no_attachments(
259-
hass: HomeAssistant, mock_cloud_ai_task_entity: CloudLLMTaskEntity
242+
hass: HomeAssistant, mock_cloud_ai_task_entity: CloudAITaskEntity
260243
) -> None:
261244
"""Test generating an image without attachments."""
262245
mock_cloud_ai_task_entity._cloud.llm.async_generate_image.return_value = {
@@ -281,7 +264,7 @@ async def test_generate_image_no_attachments(
281264

282265
async def test_generate_image_with_attachments(
283266
hass: HomeAssistant,
284-
mock_cloud_ai_task_entity: CloudLLMTaskEntity,
267+
mock_cloud_ai_task_entity: CloudAITaskEntity,
285268
mock_prepare_generation_attachments: AsyncMock,
286269
) -> None:
287270
"""Test generating an edited image when attachments are provided."""
@@ -319,7 +302,7 @@ async def test_generate_image_with_attachments(
319302
[
320303
(
321304
LLMAuthenticationError("auth"),
322-
ConfigEntryAuthFailed,
305+
HomeAssistantError,
323306
"Cloud LLM authentication failed",
324307
),
325308
(
@@ -346,7 +329,7 @@ async def test_generate_image_with_attachments(
346329
)
347330
async def test_generate_image_error_handling(
348331
hass: HomeAssistant,
349-
mock_cloud_ai_task_entity: CloudLLMTaskEntity,
332+
mock_cloud_ai_task_entity: CloudAITaskEntity,
350333
err: Exception,
351334
expected_exception: type[Exception],
352335
message: str,

0 commit comments

Comments
 (0)