mirror of
https://github.com/home-assistant/core.git
synced 2026-04-02 08:26:41 +01:00
Add coordinator to Anthropic for availability check (#164615)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
@@ -2,19 +2,15 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import anthropic
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
@@ -24,12 +20,11 @@ from .const import (
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
from .coordinator import AnthropicConfigEntry, AnthropicCoordinator
|
||||
|
||||
PLATFORMS = (Platform.AI_TASK, Platform.CONVERSATION)
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
type AnthropicConfigEntry = ConfigEntry[anthropic.AsyncClient]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Anthropic."""
|
||||
@@ -39,29 +34,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) -> bool:
|
||||
"""Set up Anthropic from a config entry."""
|
||||
client = anthropic.AsyncAnthropic(
|
||||
api_key=entry.data[CONF_API_KEY], http_client=get_async_client(hass)
|
||||
)
|
||||
try:
|
||||
await client.models.list(timeout=10.0)
|
||||
except anthropic.AuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_authentication_error",
|
||||
translation_placeholders={"message": err.message},
|
||||
) from err
|
||||
except anthropic.AnthropicError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
translation_placeholders={
|
||||
"message": err.message
|
||||
if isinstance(err, anthropic.APIError)
|
||||
else str(err)
|
||||
},
|
||||
) from err
|
||||
|
||||
entry.runtime_data = client
|
||||
coordinator = AnthropicCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
||||
78
homeassistant/components/anthropic/coordinator.py
Normal file
78
homeassistant/components/anthropic/coordinator.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Coordinator for the Anthropic integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
import anthropic
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
UPDATE_INTERVAL_CONNECTED = timedelta(hours=12)
|
||||
UPDATE_INTERVAL_DISCONNECTED = timedelta(minutes=1)
|
||||
|
||||
type AnthropicConfigEntry = ConfigEntry[AnthropicCoordinator]
|
||||
|
||||
|
||||
class AnthropicCoordinator(DataUpdateCoordinator[None]):
|
||||
"""DataUpdateCoordinator which uses different intervals after successful and unsuccessful updates."""
|
||||
|
||||
client: anthropic.AsyncAnthropic
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: AnthropicConfigEntry) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=config_entry.title,
|
||||
update_interval=UPDATE_INTERVAL_CONNECTED,
|
||||
update_method=self.async_update_data,
|
||||
always_update=False,
|
||||
)
|
||||
self.client = anthropic.AsyncAnthropic(
|
||||
api_key=config_entry.data[CONF_API_KEY], http_client=get_async_client(hass)
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_set_updated_data(self, data: None) -> None:
|
||||
"""Manually update data, notify listeners and update refresh interval."""
|
||||
self.update_interval = UPDATE_INTERVAL_CONNECTED
|
||||
super().async_set_updated_data(data)
|
||||
|
||||
async def async_update_data(self) -> None:
|
||||
"""Fetch data from the API."""
|
||||
try:
|
||||
self.update_interval = UPDATE_INTERVAL_DISCONNECTED
|
||||
await self.client.models.list(timeout=10.0)
|
||||
self.update_interval = UPDATE_INTERVAL_CONNECTED
|
||||
except anthropic.APITimeoutError as err:
|
||||
raise TimeoutError(err.message or str(err)) from err
|
||||
except anthropic.AuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_authentication_error",
|
||||
translation_placeholders={"message": err.message},
|
||||
) from err
|
||||
except anthropic.APIError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
translation_placeholders={"message": err.message},
|
||||
) from err
|
||||
|
||||
def mark_connection_error(self) -> None:
|
||||
"""Mark the connection as having an error and reschedule background check."""
|
||||
self.update_interval = UPDATE_INTERVAL_DISCONNECTED
|
||||
if self.last_update_success:
|
||||
self.last_update_success = False
|
||||
self.async_update_listeners()
|
||||
if self._listeners and not self.hass.is_stopping:
|
||||
self._schedule_refresh()
|
||||
@@ -82,12 +82,11 @@ from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, llm
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util.json import JsonObjectType
|
||||
|
||||
from . import AnthropicConfigEntry
|
||||
from .const import (
|
||||
CONF_CHAT_MODEL,
|
||||
CONF_CODE_EXECUTION,
|
||||
@@ -111,6 +110,7 @@ from .const import (
|
||||
PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS,
|
||||
UNSUPPORTED_STRUCTURED_OUTPUT_MODELS,
|
||||
)
|
||||
from .coordinator import AnthropicConfigEntry, AnthropicCoordinator
|
||||
|
||||
# Max number of back and forth with the LLM to generate a response
|
||||
MAX_TOOL_ITERATIONS = 10
|
||||
@@ -658,7 +658,7 @@ def _create_token_stats(
|
||||
}
|
||||
|
||||
|
||||
class AnthropicBaseLLMEntity(Entity):
|
||||
class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
|
||||
"""Anthropic base LLM entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
@@ -666,6 +666,7 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
|
||||
def __init__(self, entry: AnthropicConfigEntry, subentry: ConfigSubentry) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(entry.runtime_data)
|
||||
self.entry = entry
|
||||
self.subentry = subentry
|
||||
self._attr_unique_id = subentry.subentry_id
|
||||
@@ -877,7 +878,8 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
if tools:
|
||||
model_args["tools"] = tools
|
||||
|
||||
client = self.entry.runtime_data
|
||||
coordinator = self.entry.runtime_data
|
||||
client = coordinator.client
|
||||
|
||||
# To prevent infinite loops, we limit the number of iterations
|
||||
for _iteration in range(max_iterations):
|
||||
@@ -899,13 +901,24 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
)
|
||||
messages.extend(new_messages)
|
||||
except anthropic.AuthenticationError as err:
|
||||
self.entry.async_start_reauth(self.hass)
|
||||
# Trigger coordinator to confirm the auth failure and trigger the reauth flow.
|
||||
await coordinator.async_request_refresh()
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_authentication_error",
|
||||
translation_placeholders={"message": err.message},
|
||||
) from err
|
||||
except anthropic.APIConnectionError as err:
|
||||
LOGGER.info("Connection error while talking to Anthropic: %s", err)
|
||||
coordinator.mark_connection_error()
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
translation_placeholders={"message": err.message},
|
||||
) from err
|
||||
except anthropic.AnthropicError as err:
|
||||
# Non-connection error, mark connection as healthy
|
||||
coordinator.async_set_updated_data(None)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
@@ -917,6 +930,7 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
) from err
|
||||
|
||||
if not chat_log.unresponded_tool_results:
|
||||
coordinator.async_set_updated_data(None)
|
||||
break
|
||||
|
||||
|
||||
|
||||
@@ -35,9 +35,9 @@ rules:
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: todo
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
log-when-unavailable: done
|
||||
parallel-updates:
|
||||
status: exempt
|
||||
comment: |
|
||||
|
||||
@@ -58,7 +58,7 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
|
||||
if entry.entry_id in self._model_list_cache:
|
||||
model_list = self._model_list_cache[entry.entry_id]
|
||||
else:
|
||||
client = entry.runtime_data
|
||||
client = entry.runtime_data.client
|
||||
model_list = [
|
||||
model_option
|
||||
for model_option in await get_model_list(client)
|
||||
|
||||
@@ -4,7 +4,7 @@ import datetime
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from anthropic import AuthenticationError, RateLimitError
|
||||
from anthropic import RateLimitError
|
||||
from anthropic.types import (
|
||||
CitationsWebSearchResultLocation,
|
||||
CitationWebSearchResultLocationParam,
|
||||
@@ -42,10 +42,8 @@ from homeassistant.components.anthropic.const import (
|
||||
CONF_WEB_SEARCH_REGION,
|
||||
CONF_WEB_SEARCH_TIMEZONE,
|
||||
CONF_WEB_SEARCH_USER_LOCATION,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.anthropic.entity import CitationDetails, ContentDetails
|
||||
from homeassistant.config_entries import SOURCE_REAUTH
|
||||
from homeassistant.const import CONF_LLM_HASS_API
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -129,38 +127,6 @@ async def test_error_handling(
|
||||
assert result.response.error_code == "unknown", result
|
||||
|
||||
|
||||
async def test_auth_error_handling(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_init_component,
|
||||
mock_create_stream: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauth after authentication error during conversation."""
|
||||
mock_create_stream.side_effect = AuthenticationError(
|
||||
message="Invalid API key",
|
||||
response=Response(status_code=403, request=Request(method="POST", url=URL())),
|
||||
body=None,
|
||||
)
|
||||
|
||||
result = await conversation.async_converse(
|
||||
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown", result
|
||||
|
||||
await hass.async_block_till_done()
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
|
||||
flow = flows[0]
|
||||
assert flow["step_id"] == "reauth_confirm"
|
||||
assert flow["handler"] == DOMAIN
|
||||
assert "context" in flow
|
||||
assert flow["context"]["source"] == SOURCE_REAUTH
|
||||
assert flow["context"]["entry_id"] == mock_config_entry.entry_id
|
||||
|
||||
|
||||
async def test_template_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
|
||||
264
tests/components/anthropic/test_coordinator.py
Normal file
264
tests/components/anthropic/test_coordinator.py
Normal file
@@ -0,0 +1,264 @@
|
||||
"""Tests for the Anthropic integration."""
|
||||
|
||||
import datetime
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from anthropic import APITimeoutError, AuthenticationError, RateLimitError
|
||||
from freezegun import freeze_time
|
||||
from httpx import URL, Request, Response
|
||||
|
||||
from homeassistant.components import conversation
|
||||
from homeassistant.components.anthropic.const import DOMAIN
|
||||
from homeassistant.components.anthropic.coordinator import (
|
||||
UPDATE_INTERVAL_CONNECTED,
|
||||
UPDATE_INTERVAL_DISCONNECTED,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_REAUTH
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.helpers import intent
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@patch("anthropic.resources.models.AsyncModels.list", new_callable=AsyncMock)
|
||||
async def test_auth_error_handling(
|
||||
mock_model_list: AsyncMock,
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_init_component,
|
||||
mock_create_stream: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauth after authentication error during conversation."""
|
||||
# This is an assumption of the tests, not the main code:
|
||||
assert UPDATE_INTERVAL_DISCONNECTED < UPDATE_INTERVAL_CONNECTED
|
||||
|
||||
mock_create_stream.side_effect = mock_model_list.side_effect = AuthenticationError(
|
||||
message="Invalid API key",
|
||||
response=Response(status_code=403, request=Request(method="POST", url=URL())),
|
||||
body=None,
|
||||
)
|
||||
|
||||
result = await conversation.async_converse(
|
||||
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown", result
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("conversation.claude_conversation")
|
||||
assert state
|
||||
assert state.state == "unavailable"
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
|
||||
flow = flows[0]
|
||||
assert flow["step_id"] == "reauth_confirm"
|
||||
assert flow["handler"] == DOMAIN
|
||||
assert "context" in flow
|
||||
assert flow["context"]["source"] == SOURCE_REAUTH
|
||||
assert flow["context"]["entry_id"] == mock_config_entry.entry_id
|
||||
|
||||
|
||||
@freeze_time("2026-02-27 12:00:00")
|
||||
@patch("anthropic.resources.models.AsyncModels.list", new_callable=AsyncMock)
|
||||
async def test_connection_error_handling(
|
||||
mock_model_list: AsyncMock,
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_init_component,
|
||||
mock_create_stream: AsyncMock,
|
||||
) -> None:
|
||||
"""Test making entity unavailable on connection error."""
|
||||
mock_create_stream.side_effect = APITimeoutError(
|
||||
request=Request(method="POST", url=URL()),
|
||||
)
|
||||
|
||||
# Check initial state
|
||||
state = hass.states.get("conversation.claude_conversation")
|
||||
assert state
|
||||
assert state.state == "unknown"
|
||||
|
||||
# Get timeout
|
||||
result = await conversation.async_converse(
|
||||
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
assert result.response.error_code == "unknown", result
|
||||
|
||||
# Check new state
|
||||
state = hass.states.get("conversation.claude_conversation")
|
||||
assert state
|
||||
assert state.state == "unavailable"
|
||||
|
||||
# Try again
|
||||
await conversation.async_converse(
|
||||
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
|
||||
)
|
||||
|
||||
# Check state is still unavailable
|
||||
state = hass.states.get("conversation.claude_conversation")
|
||||
assert state
|
||||
assert state.state == "unavailable"
|
||||
|
||||
mock_create_stream.side_effect = RateLimitError(
|
||||
message=None,
|
||||
response=Response(status_code=429, request=Request(method="POST", url=URL())),
|
||||
body=None,
|
||||
)
|
||||
|
||||
# Get a different error meaning the connection is restored
|
||||
await conversation.async_converse(
|
||||
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
|
||||
)
|
||||
|
||||
# Check state is back to normal
|
||||
state = hass.states.get("conversation.claude_conversation")
|
||||
assert state
|
||||
assert state.state == "2026-02-27T12:00:00+00:00"
|
||||
|
||||
# Verify the background check period
|
||||
test_time = datetime.datetime.now(datetime.UTC) + UPDATE_INTERVAL_DISCONNECTED
|
||||
async_fire_time_changed(hass, test_time)
|
||||
await hass.async_block_till_done()
|
||||
mock_model_list.assert_not_awaited()
|
||||
|
||||
test_time += UPDATE_INTERVAL_CONNECTED - UPDATE_INTERVAL_DISCONNECTED
|
||||
async_fire_time_changed(hass, test_time)
|
||||
await hass.async_block_till_done()
|
||||
mock_model_list.assert_awaited_once()
|
||||
|
||||
|
||||
@patch("anthropic.resources.models.AsyncModels.list", new_callable=AsyncMock)
|
||||
async def test_connection_check_reauth(
|
||||
mock_model_list: AsyncMock,
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_init_component,
|
||||
) -> None:
|
||||
"""Test authentication error during background availability check."""
|
||||
mock_model_list.side_effect = APITimeoutError(
|
||||
request=Request(method="POST", url=URL()),
|
||||
)
|
||||
|
||||
# Check initial state
|
||||
state = hass.states.get("conversation.claude_conversation")
|
||||
assert state
|
||||
assert state.state == "unknown"
|
||||
|
||||
# Get timeout
|
||||
assert mock_model_list.await_count == 0
|
||||
test_time = datetime.datetime.now(datetime.UTC) + UPDATE_INTERVAL_CONNECTED
|
||||
async_fire_time_changed(hass, test_time)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_model_list.await_count == 1
|
||||
|
||||
# Check new state
|
||||
state = hass.states.get("conversation.claude_conversation")
|
||||
assert state
|
||||
assert state.state == "unavailable"
|
||||
|
||||
mock_model_list.side_effect = AuthenticationError(
|
||||
message="Invalid API key",
|
||||
response=Response(status_code=403, request=Request(method="POST", url=URL())),
|
||||
body=None,
|
||||
)
|
||||
|
||||
# Wait for background check to run and fail
|
||||
test_time += UPDATE_INTERVAL_DISCONNECTED
|
||||
async_fire_time_changed(hass, test_time)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_model_list.await_count == 2
|
||||
|
||||
# Check state is still unavailable
|
||||
state = hass.states.get("conversation.claude_conversation")
|
||||
assert state
|
||||
assert state.state == "unavailable"
|
||||
|
||||
# Verify that the background check is not running anymore
|
||||
test_time += UPDATE_INTERVAL_DISCONNECTED
|
||||
async_fire_time_changed(hass, test_time)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_model_list.await_count == 2
|
||||
|
||||
# Check that a reauth flow has been created
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
|
||||
flow = flows[0]
|
||||
assert flow["step_id"] == "reauth_confirm"
|
||||
assert flow["handler"] == DOMAIN
|
||||
assert "context" in flow
|
||||
assert flow["context"]["source"] == SOURCE_REAUTH
|
||||
assert flow["context"]["entry_id"] == mock_config_entry.entry_id
|
||||
|
||||
|
||||
@patch("anthropic.resources.models.AsyncModels.list", new_callable=AsyncMock)
|
||||
async def test_connection_restore(
|
||||
mock_model_list: AsyncMock,
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_init_component,
|
||||
mock_create_stream: AsyncMock,
|
||||
) -> None:
|
||||
"""Test background availability check restore on non-connectivity error."""
|
||||
mock_create_stream.side_effect = APITimeoutError(
|
||||
request=Request(method="POST", url=URL()),
|
||||
)
|
||||
|
||||
# Check initial state
|
||||
state = hass.states.get("conversation.claude_conversation")
|
||||
assert state
|
||||
assert state.state == "unknown"
|
||||
|
||||
# Get timeout
|
||||
await conversation.async_converse(
|
||||
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
|
||||
)
|
||||
|
||||
# Check new state
|
||||
state = hass.states.get("conversation.claude_conversation")
|
||||
assert state
|
||||
assert state.state == "unavailable"
|
||||
|
||||
mock_model_list.side_effect = APITimeoutError(
|
||||
request=Request(method="POST", url=URL()),
|
||||
)
|
||||
|
||||
# Wait for background check to run and fail
|
||||
assert mock_model_list.await_count == 0
|
||||
test_time = datetime.datetime.now(datetime.UTC) + UPDATE_INTERVAL_DISCONNECTED
|
||||
async_fire_time_changed(hass, test_time)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_model_list.await_count == 1
|
||||
|
||||
# Check state is still unavailable
|
||||
state = hass.states.get("conversation.claude_conversation")
|
||||
assert state
|
||||
assert state.state == "unavailable"
|
||||
|
||||
# Now make the background check succeed
|
||||
mock_model_list.side_effect = None
|
||||
test_time += UPDATE_INTERVAL_DISCONNECTED
|
||||
async_fire_time_changed(hass, test_time)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_model_list.await_count == 2
|
||||
|
||||
# Check that state is back to normal since the error is not connectivity related
|
||||
state = hass.states.get("conversation.claude_conversation")
|
||||
assert state
|
||||
assert state.state != "unavailable"
|
||||
|
||||
# Verify the background check period
|
||||
test_time += UPDATE_INTERVAL_DISCONNECTED
|
||||
async_fire_time_changed(hass, test_time)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_model_list.await_count == 2
|
||||
|
||||
test_time += UPDATE_INTERVAL_CONNECTED - UPDATE_INTERVAL_DISCONNECTED
|
||||
async_fire_time_changed(hass, test_time)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_model_list.await_count == 3
|
||||
Reference in New Issue
Block a user