Skip to content

Commit 4b41c7a

Browse files
author
KNOOP
committed
fix(init): restore fault-tolerant setup after accidental deletion
Re-add __init__.py with isolated try/except for intent sync, intent handlers, services and per-platform forward. Single subsystem failure no longer blocks the entire integration from loading.
1 parent e44eb68 commit 4b41c7a

1 file changed

Lines changed: 300 additions & 0 deletions

File tree

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
"""The AI Hub integration."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
from dataclasses import dataclass, field
7+
from typing import Any, TypeAlias
8+
9+
import aiohttp
10+
11+
try:
12+
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
13+
from homeassistant.const import CONF_API_KEY, Platform
14+
from homeassistant.core import HomeAssistant
15+
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
16+
except ModuleNotFoundError: # pragma: no cover - used only in lightweight test environments
17+
ConfigEntry = Any # type: ignore[assignment]
18+
ConfigSubentry = Any # type: ignore[assignment]
19+
HomeAssistant = Any # type: ignore[assignment]
20+
CONF_API_KEY = "api_key"
21+
22+
class ConfigEntryAuthFailed(Exception):
23+
"""Fallback exception when Home Assistant is not installed."""
24+
25+
class ConfigEntryNotReady(Exception):
26+
"""Fallback exception when Home Assistant is not installed."""
27+
28+
Platform = None # type: ignore[assignment]
29+
30+
from .consts import DOMAIN
31+
32+
_LOGGER = logging.getLogger(__name__)
33+
34+
if Platform is None:
35+
PLATFORMS: list[Any] = []
36+
else:
37+
PLATFORMS = [
38+
Platform.CONVERSATION,
39+
Platform.AI_TASK,
40+
Platform.TTS,
41+
Platform.STT,
42+
Platform.BUTTON,
43+
Platform.SENSOR,
44+
]
45+
46+
AIHubConfigEntry: TypeAlias = ConfigEntry # Store API key
47+
48+
49+
@dataclass
50+
class AIHubData:
51+
"""Runtime data for AI Hub integration.
52+
53+
This class holds all runtime state for the integration,
54+
avoiding global variables and ensuring proper cleanup on reload.
55+
"""
56+
57+
api_key: str | None = None
58+
tts_cache: Any = None
59+
provider_registry: Any = None
60+
diagnostics_collector: Any = None
61+
stats: dict[str, Any] = field(default_factory=dict)
62+
63+
def cleanup(self) -> None:
64+
"""Clean up resources."""
65+
if self.tts_cache is not None:
66+
self.tts_cache.clear()
67+
self.provider_registry = None
68+
69+
70+
def get_ai_hub_data(hass: HomeAssistant) -> AIHubData | None:
71+
"""Get AI Hub runtime data."""
72+
return hass.data.get(DOMAIN)
73+
74+
75+
def get_configured_api_key(entry: ConfigEntry) -> str:
76+
"""Return the configured main API key from options or entry data."""
77+
api_key = entry.options.get(CONF_API_KEY) or entry.data.get(CONF_API_KEY) or ""
78+
return api_key.strip() if isinstance(api_key, str) else str(api_key).strip()
79+
80+
81+
def get_or_create_ai_hub_data(hass: HomeAssistant) -> AIHubData:
82+
"""Get or create AI Hub runtime data."""
83+
if DOMAIN not in hass.data:
84+
hass.data[DOMAIN] = AIHubData()
85+
return hass.data[DOMAIN]
86+
87+
88+
def get_provider_registry(hass: HomeAssistant):
89+
"""Get or create the provider registry for this Home Assistant instance.
90+
91+
This ensures proper lifecycle management - the registry is cleaned up
92+
when the integration is unloaded.
93+
94+
Args:
95+
hass: Home Assistant instance
96+
97+
Returns:
98+
UnifiedProviderRegistry instance
99+
"""
100+
from .providers import get_registry
101+
102+
ai_hub_data = get_or_create_ai_hub_data(hass)
103+
if ai_hub_data.provider_registry is None:
104+
ai_hub_data.provider_registry = get_registry()
105+
return ai_hub_data.provider_registry
106+
107+
108+
async def async_setup_entry(hass: HomeAssistant, entry: AIHubConfigEntry) -> bool:
109+
"""Set up AI Hub from a config entry."""
110+
111+
# Get API key (may be None if not provided)
112+
# No startup validation - each entity validates on actual use
113+
api_key = get_configured_api_key(entry)
114+
115+
# Initialize runtime data in hass.data
116+
ai_hub_data = get_or_create_ai_hub_data(hass)
117+
ai_hub_data.api_key = api_key
118+
119+
# Store in entry.runtime_data
120+
entry.runtime_data = api_key
121+
122+
# Each step is independent - one failure does not block others
123+
try:
124+
from .intents.loader import async_sync_intent_lists
125+
await async_sync_intent_lists(hass)
126+
except Exception as err:
127+
_LOGGER.warning("Intent list sync failed (non-fatal): %s", err)
128+
129+
try:
130+
from .intents import async_setup_intents
131+
await async_setup_intents(hass)
132+
except Exception as err:
133+
_LOGGER.warning("Intent handlers setup failed (non-fatal): %s", err)
134+
135+
try:
136+
from .services import async_setup_services
137+
await async_setup_services(hass, entry)
138+
except Exception as err:
139+
_LOGGER.warning("Services setup failed (non-fatal): %s", err)
140+
141+
# Forward setup to platforms individually - one failure should not block others
142+
for platform in PLATFORMS:
143+
try:
144+
await hass.config_entries.async_forward_entry_setups(entry, [platform])
145+
except Exception as err:
146+
_LOGGER.warning("Platform %s setup failed (others continue): %s", platform, err)
147+
148+
# Listen for options updates
149+
entry.async_on_unload(entry.add_update_listener(async_update_options))
150+
151+
return True
152+
153+
154+
async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
155+
"""Handle options update."""
156+
await hass.config_entries.async_reload(entry.entry_id)
157+
158+
159+
async def async_unload_entry(hass: HomeAssistant, entry: AIHubConfigEntry) -> bool:
160+
"""Unload a config entry."""
161+
remaining_entries = [
162+
config_entry
163+
for config_entry in hass.config_entries.async_entries(DOMAIN)
164+
if config_entry.entry_id != entry.entry_id
165+
]
166+
167+
all_ok = True
168+
for platform in PLATFORMS:
169+
try:
170+
if not await hass.config_entries.async_unload_platforms(entry, [platform]):
171+
_LOGGER.warning("Failed to unload platform %s", platform)
172+
all_ok = False
173+
except Exception as err:
174+
_LOGGER.warning("Error unloading platform %s: %s", platform, err)
175+
all_ok = False
176+
177+
from .services import async_unload_services
178+
await async_unload_services(hass, entry.entry_id)
179+
180+
if not remaining_entries:
181+
ai_hub_data = get_ai_hub_data(hass)
182+
if ai_hub_data is not None:
183+
ai_hub_data.cleanup()
184+
hass.data.pop(DOMAIN, None)
185+
186+
return all_ok
187+
188+
189+
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
190+
"""Migrate old entry."""
191+
_LOGGER.debug("Migrating configuration from version %s.%s", entry.version, entry.minor_version)
192+
193+
if entry.version == 1:
194+
# Migrate from version 1 to version 2
195+
# Version 2 uses subentries for conversation and ai_task
196+
new_data = {**entry.data}
197+
new_options = {**entry.options}
198+
199+
# Create default subentries
200+
from homeassistant.helpers import llm
201+
202+
from .consts import (
203+
CONF_CHAT_MODEL,
204+
CONF_LLM_HASS_API,
205+
CONF_MAX_TOKENS,
206+
CONF_PROMPT,
207+
CONF_RECOMMENDED,
208+
CONF_TEMPERATURE,
209+
CONF_TOP_P,
210+
DEFAULT_AI_TASK_NAME,
211+
DEFAULT_CONVERSATION_NAME,
212+
LEGACY_AI_TASK_TITLES,
213+
LEGACY_CONVERSATION_TITLES,
214+
LLM_API_ASSIST,
215+
RECOMMENDED_AI_TASK_MAX_TOKENS,
216+
RECOMMENDED_AI_TASK_MODEL,
217+
RECOMMENDED_AI_TASK_TEMPERATURE,
218+
RECOMMENDED_AI_TASK_TOP_P,
219+
RECOMMENDED_CHAT_MODEL,
220+
RECOMMENDED_MAX_TOKENS,
221+
RECOMMENDED_TEMPERATURE,
222+
RECOMMENDED_TOP_P,
223+
SUBENTRY_AI_TASK,
224+
SUBENTRY_CONVERSATION,
225+
)
226+
227+
# Create conversation subentry from old options
228+
conversation_data = {
229+
CONF_RECOMMENDED: new_options.get(CONF_RECOMMENDED, True),
230+
CONF_CHAT_MODEL: new_options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL),
231+
CONF_TEMPERATURE: new_options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE),
232+
CONF_TOP_P: new_options.get(CONF_TOP_P, RECOMMENDED_TOP_P),
233+
CONF_MAX_TOKENS: new_options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
234+
CONF_PROMPT: new_options.get(CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT),
235+
CONF_LLM_HASS_API: new_options.get(CONF_LLM_HASS_API, [LLM_API_ASSIST]),
236+
}
237+
238+
# Create AI task subentry with defaults
239+
ai_task_data = {
240+
CONF_RECOMMENDED: True,
241+
CONF_CHAT_URL: AI_HUB_CHAT_URL,
242+
CONF_CHAT_MODEL: RECOMMENDED_AI_TASK_MODEL,
243+
CONF_TEMPERATURE: RECOMMENDED_AI_TASK_TEMPERATURE,
244+
CONF_TOP_P: RECOMMENDED_AI_TASK_TOP_P,
245+
CONF_MAX_TOKENS: RECOMMENDED_AI_TASK_MAX_TOKENS,
246+
}
247+
248+
hass.config_entries.async_update_entry(
249+
entry,
250+
data=new_data,
251+
options={},
252+
version=2,
253+
minor_version=2,
254+
)
255+
256+
# Add subentries
257+
conversation_subentry = ConfigSubentry(
258+
data=conversation_data,
259+
subentry_type=SUBENTRY_CONVERSATION,
260+
title=DEFAULT_CONVERSATION_NAME,
261+
unique_id=None,
262+
)
263+
hass.config_entries.async_add_subentry(entry, conversation_subentry)
264+
265+
ai_task_subentry = ConfigSubentry(
266+
data=ai_task_data,
267+
subentry_type=SUBENTRY_AI_TASK,
268+
title=DEFAULT_AI_TASK_NAME,
269+
unique_id=None,
270+
)
271+
hass.config_entries.async_add_subentry(entry, ai_task_subentry)
272+
273+
_LOGGER.debug("Migration to version %s.%s successful", entry.version, entry.minor_version)
274+
275+
if entry.version == 2 and entry.minor_version == 1:
276+
# Migrate from version 2.1 to 2.2
277+
# Update subentry titles
278+
from .consts import DEFAULT_AI_TASK_NAME, DEFAULT_CONVERSATION_NAME
279+
280+
for subentry in entry.subentries.values():
281+
# Update old titles to new format
282+
if subentry.subentry_type == SUBENTRY_CONVERSATION:
283+
if subentry.title in LEGACY_CONVERSATION_TITLES:
284+
hass.config_entries.async_update_subentry(
285+
entry, subentry.subentry_id, title=DEFAULT_CONVERSATION_NAME
286+
)
287+
elif subentry.subentry_type == SUBENTRY_AI_TASK:
288+
if subentry.title in LEGACY_AI_TASK_TITLES:
289+
hass.config_entries.async_update_subentry(
290+
entry, subentry.subentry_id, title=DEFAULT_AI_TASK_NAME
291+
)
292+
293+
hass.config_entries.async_update_entry(
294+
entry,
295+
minor_version=2,
296+
)
297+
298+
_LOGGER.debug("Migration to version %s.%s successful", entry.version, entry.minor_version)
299+
300+
return True

0 commit comments

Comments
 (0)