Refactor Groq Cloud API integration: update setup methods, enhance error handling, and improve configuration options

This commit is contained in:
2025-12-23 17:40:44 +01:00
parent f87aecaefa
commit 4fa1ddae19
7 changed files with 177 additions and 178 deletions
+19 -22
View File
@@ -1,58 +1,55 @@
"""Groq Cloud API integration."""
from __future__ import annotations
import groq
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.typing import ConfigType
from .const import LOGGER
from .const import DOMAIN, LOGGER
PLATFORMS = (Platform.CONVERSATION,)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
type GroqConfigEntry = ConfigEntry[groq.AsyncClient]
async def async_setup_entry(hass: HomeAssistant, entry: GroqConfigEntry) -> bool:
"""Load entry."""
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Groq Cloud API."""
return True
LOGGER.info("Setting up %s", entry)
async def async_setup_entry(hass: HomeAssistant, entry: GroqConfigEntry) -> bool:
"""Set up Groq Cloud API from a config entry."""
LOGGER.debug("Setting up %s", entry)
client = groq.AsyncGroq(
api_key=entry.data[CONF_API_KEY],
http_client=get_async_client(hass)
http_client=get_async_client(hass),
)
# TODO: I get async errors when trying to call the list function. But this would be the proper way to validate the API key.
# try:
# await hass.async_add_executor_job(client.with_options(timeout=10.0).models.list)
# except groq.AuthenticationError as err:
# LOGGER.error("Invalid API key: %s", err)
# return False
# except groq.GroqError as err:
# raise ConfigEntryNotReady(err) from err
entry.runtime_data = client
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_update_listener))
entry.async_on_unload(entry.add_update_listener(async_update_options))
return True
async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Update entry."""
async def async_update_options(hass: HomeAssistant, entry: GroqConfigEntry) -> None:
"""Update options."""
await hass.config_entries.async_reload(entry.entry_id)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: GroqConfigEntry) -> bool:
"""Unload a config entry."""
LOGGER.info("Unloading %s", entry)
LOGGER.debug("Unloading %s", entry)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+53 -72
View File
@@ -1,27 +1,25 @@
"""Config flow for GroqCloud Whisper integration."""
"""Config flow for Groq Cloud API integration."""
from __future__ import annotations
import asyncio
from typing import Any
from types import MappingProxyType
from typing import Any
import groq
import requests
import voluptuous as vol
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant import exceptions
from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_LLM_HASS_API, CONF_MODEL
from homeassistant.const import CONF_API_KEY, CONF_LLM_HASS_API
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import llm
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.selector import (
NumberSelector,
NumberSelectorConfig,
@@ -31,59 +29,48 @@ from homeassistant.helpers.selector import (
TemplateSelector,
)
from . import LOGGER
from .const import (
CONF_PROMPT,
CONF_TEMPERATURE,
DEFAULT_NAME,
CONF_CHAT_MODEL,
CONF_MAX_TOKENS,
CONF_PROMPT,
CONF_RECOMMENDED,
CONF_TEMPERATURE,
CONF_TOP_P,
DEFAULT_NAME,
DOMAIN,
LOGGER,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_OPTIONS,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_TOP_P,
)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_API_KEY): str,
}
)
RECOMMENDED_OPTIONS = {
CONF_RECOMMENDED: True,
CONF_LLM_HASS_API: llm.LLM_API_ASSIST,
CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT,
}
async def validate_input(hass: HomeAssistant, data: dict[str, Any]):
"""Validate the user input."""
obscured_api_key = data.get(CONF_API_KEY)
data[CONF_API_KEY] = "<api_key>"
LOGGER.debug("User validation got: %s", data)
data[CONF_API_KEY] = obscured_api_key
if data.get(CONF_TEMPERATURE) is None:
data[CONF_CHAT_MODEL] = RECOMMENDED_CHAT_MODEL
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
"""Validate the user input allows us to connect."""
response = await asyncio.to_thread(
requests.get,
url="https://api.groq.com/openai/v1/models",
headers={
"Authorization": f"Bearer {data.get(CONF_API_KEY)}",
"Content-Type": "application/json"
"Content-Type": "application/json",
},
timeout=10,
)
LOGGER.debug("Models request took %f s and returned %d - %s", response.elapsed.seconds, response.status_code, response.reason)
LOGGER.debug(
"Models request took %f s and returned %d - %s",
response.elapsed.total_seconds(),
response.status_code,
response.reason,
)
if response.status_code == 401:
raise InvalidAPIKey
@@ -94,56 +81,55 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]):
if response.status_code != 200:
raise UnknownError
for model in response.json().get("data", []):
if model.get("id") == data.get(CONF_CHAT_MODEL):
break
if model == response.json().get("data")[-1]:
raise ModelNotFound
LOGGER.debug("User validation successful")
class GroqConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle UI config flow."""
"""Handle UI config flow for Groq Cloud API."""
VERSION = 1
MINOR_VERSION = 0
MINOR_VERSION = 1
async def async_step_user(
self,
user_input: dict[str, Any] | None = None,
errors: dict[str, str] | None = None,
) -> ConfigFlowResult:
"""Handle initial step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
)
errors = {}
errors: dict[str, str] = {}
self._async_abort_entries_match(user_input)
try:
await validate_input(self.hass, user_input)
except groq.APIConnectionError:
errors["base"] = "cannot_connect"
except groq.AuthenticationError:
errors["base"] = "invalid_auth"
except InvalidAPIKey:
errors["base"] = "invalid_auth"
except UnauthorizedError:
errors["base"] = "unauthorized"
except Exception:
LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(
title="GroqCloud",
title=DEFAULT_NAME,
data=user_input,
options=RECOMMENDED_OPTIONS,
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
)
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> OptionsFlow:
@@ -153,25 +139,25 @@ class GroqConfigFlow(ConfigFlow, domain=DOMAIN):
class GroqOptionsFlow(OptionsFlow):
"""Groq Cloud config flow options handler."""
"""Groq Cloud API options flow handler."""
def __init__(self) -> None:
"""Initialize options flow."""
self.last_rendered_recommended = False
last_rendered_recommended: bool = False
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the options."""
options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options
options: dict[str, Any] | MappingProxyType[str, Any] = (
self.config_entry.options
)
if user_input is None:
self.last_rendered_recommended = options.get(CONF_RECOMMENDED, False)
if user_input is not None:
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
if user_input[CONF_LLM_HASS_API] == "none":
user_input.pop(CONF_LLM_HASS_API)
if not user_input.get(CONF_LLM_HASS_API):
user_input.pop(CONF_LLM_HASS_API, None)
return self.async_create_entry(title="", data=user_input)
# Re-render the options again, now with the recommended options shown/hidden
@@ -179,8 +165,10 @@ class GroqOptionsFlow(OptionsFlow):
options = {
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
CONF_PROMPT: user_input[CONF_PROMPT],
CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API],
CONF_PROMPT: user_input.get(
CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT
),
CONF_LLM_HASS_API: user_input.get(CONF_LLM_HASS_API),
}
schema = groq_config_option_schema(self.hass, options)
@@ -196,20 +184,14 @@ def groq_config_option_schema(
) -> dict:
"""Return a schema for Groq Cloud completion options."""
hass_apis: list[SelectOptionDict] = [
SelectOptionDict(
label="No control",
value="none",
)
]
hass_apis.extend(
SelectOptionDict(
label=api.name,
value=api.id,
)
for api in llm.async_get_apis(hass)
)
]
schema = {
schema: dict = {
vol.Optional(
CONF_PROMPT,
description={
@@ -221,7 +203,6 @@ def groq_config_option_schema(
vol.Optional(
CONF_LLM_HASS_API,
description={"suggested_value": options.get(CONF_LLM_HASS_API)},
default="none",
): SelectSelector(SelectSelectorConfig(options=hass_apis)),
vol.Required(
CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False)
@@ -258,17 +239,17 @@ def groq_config_option_schema(
return schema
class UnknownError(exceptions.HomeAssistantError):
class UnknownError(HomeAssistantError):
"""Unknown error."""
class UnauthorizedError(exceptions.HomeAssistantError):
"""API key valid but doesn't have the rights to use Whisper."""
class UnauthorizedError(HomeAssistantError):
"""API key valid but doesn't have the rights."""
class InvalidAPIKey(exceptions.HomeAssistantError):
class InvalidAPIKey(HomeAssistantError):
"""Invalid api_key error."""
class ModelNotFound(exceptions.HomeAssistantError):
"""Model can't be found in the GroqCloud model's list."""
class ModelNotFound(HomeAssistantError):
"""Model can't be found in the Groq Cloud model's list."""
+11 -1
View File
@@ -2,18 +2,28 @@
import logging
from homeassistant.const import CONF_LLM_HASS_API
from homeassistant.helpers import llm
DOMAIN = "groq_cloud_api"
LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Groq Cloud API"
DEFAULT_CONVERSATION_NAME = "Groq Conversation"
CONF_RECOMMENDED = "recommended"
CONF_PROMPT = "prompt"
CONF_CHAT_MODEL = "chat_model"
RECOMMENDED_CHAT_MODEL = "llama-3.3-70b-versatile"
CONF_MAX_TOKENS = "max_tokens"
RECOMMENDED_MAX_TOKENS = 150
RECOMMENDED_MAX_TOKENS = 3000
CONF_TOP_P = "top_p"
RECOMMENDED_TOP_P = 1.0
CONF_TEMPERATURE = "temperature"
RECOMMENDED_TEMPERATURE = 1.0
RECOMMENDED_OPTIONS = {
CONF_RECOMMENDED: True,
CONF_LLM_HASS_API: llm.LLM_API_ASSIST,
CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT,
}
@@ -1,5 +1,7 @@
"""Conversation support for Groq Cloud."""
from __future__ import annotations
from collections.abc import Callable
import json
from typing import Any, Literal
@@ -32,7 +34,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, intent, llm
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.json import json_dumps
from . import GroqConfigEntry
@@ -57,7 +59,7 @@ MAX_TOOL_ITERATIONS = 10
async def async_setup_entry(
hass: HomeAssistant,
config_entry: GroqConfigEntry,
async_add_entities: AddEntitiesCallback,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up conversation entities."""
agent = GroqConversationEntity(config_entry)
@@ -198,12 +200,10 @@ class GroqConversationEntity(
try:
await chat_log.async_provide_llm_data(
llm_context=user_input.as_llm_context(DOMAIN),
user_llm_hass_api=options.get(CONF_LLM_HASS_API),
user_llm_prompt=options.get(
CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT
),
user_extra_system_prompt=user_input.extra_system_prompt,
user_input.as_llm_context(DOMAIN),
options.get(CONF_LLM_HASS_API),
options.get(CONF_PROMPT),
user_input.extra_system_prompt,
)
except ConverseError as err:
return err.as_conversation_result()
@@ -1,19 +1,18 @@
{
"domain": "groq_cloud_api",
"name": "Groq Cloud API",
"version": "0.1.3",
"version": "0.1.5",
"codeowners": [
"@HunorLaczko"
],
"config_flow": true,
"dependencies": [],
"after_dependencies": ["assist_pipeline", "intent"],
"dependencies": ["conversation"],
"documentation": "https://github.com/HunorLaczko/ha-groq-cloud-api",
"issue_tracker": "https://github.com/HunorLaczko/ha-groq-cloud-api/issues",
"homekit": {},
"iot_class": "cloud_push",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": [
"groq==0.18.0"
],
"ssdp": [],
"zeroconf": []
]
}
@@ -1,39 +1,45 @@
{
"config": {
"step": {
"user": {
"data": {
"api_key": "API key"
}
}
},
"error": {
"cannot_connect": "Cannot connect to service",
"invalid_auth": "Invalid Authentification",
"unknown": "Unknown Error"
"config": {
"abort": {
"already_configured": "This API key is already configured"
},
"step": {
"user": {
"data": {
"api_key": "API key"
},
"description": "Enter your Groq Cloud API key to get started."
}
},
"options": {
"step": {
"init": {
"data": {
"prompt": "Instructions",
"chat_model": "Chat model",
"max_tokens": "Maximum tokens to return in response",
"temperature": "Temperature",
"top_p": "Top P",
"llm_hass_api": "Home Assistant LLM API",
"recommended": "Recommended model settings"
},
"data_description": {
"prompt": "Instruct how the LLM should respond. This can be a template."
}
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid API key",
"unauthorized": "API key does not have required permissions",
"unknown": "Unexpected error"
}
},
"options": {
"step": {
"init": {
"data": {
"prompt": "Instructions",
"chat_model": "Model",
"max_tokens": "Maximum tokens to return in response",
"temperature": "Temperature",
"top_p": "Top P",
"llm_hass_api": "Control Home Assistant",
"recommended": "Recommended model settings"
},
"data_description": {
"prompt": "Instruct how the LLM should respond. This can be a template.",
"llm_hass_api": "Allow the model to interact with Home Assistant. Requires specific permissions."
}
}
},
"exceptions": {
"invalid_config_entry": {
"message": "Invalid config entry provided. Got {config_entry}"
}
}
},
"exceptions": {
"invalid_config_entry": {
"message": "Invalid config entry provided. Got {config_entry}"
}
}
}
@@ -1,39 +1,45 @@
{
"config": {
"step": {
"user": {
"data": {
"api_key": "API kulcs"
}
}
},
"error": {
"cannot_connect": "Nem lehet csatlakozni a szolgáltatáshoz",
"invalid_auth": "Érvénytelen hitelesítés",
"unknown": "Ismeretlen hiba"
"config": {
"abort": {
"already_configured": "Ez az API kulcs már konfigurálva van"
},
"step": {
"user": {
"data": {
"api_key": "API kulcs"
},
"description": "Add meg a Groq Cloud API kulcsot a kezdéshez."
}
},
"options": {
"step": {
"init": {
"data": {
"prompt": "Utasítások",
"chat_model": "Chat modell",
"max_tokens": "Válaszban visszatérítendő maximális tokenek száma",
"temperature": "Hőmérséklet",
"top_p": "Top P",
"llm_hass_api": "Home Assistant LLM API",
"recommended": "Ajánlott modellbeállítások"
},
"data_description": {
"prompt": "Add meg, hogy LLM hogyan reagáljon. Ez egy sablon is lehet."
}
"error": {
"cannot_connect": "Nem lehet csatlakozni a szolgáltatáshoz",
"invalid_auth": "Érvénytelen API kulcs",
"unauthorized": "Az API kulcsnak nincs megfelelő jogosultsága",
"unknown": "Váratlan hiba"
}
},
"options": {
"step": {
"init": {
"data": {
"prompt": "Utasítások",
"chat_model": "Modell",
"max_tokens": "Válaszban visszatérítendő maximális tokenek száma",
"temperature": "Hőmérséklet",
"top_p": "Top P",
"llm_hass_api": "Home Assistant irányítás",
"recommended": "Ajánlott modellbeállítások"
},
"data_description": {
"prompt": "Add meg, hogy az LLM hogyan reagáljon. Ez egy sablon is lehet.",
"llm_hass_api": "Engedélyezd a modell számára a Home Assistant irányítását. Speciális jogosultságok szükségesek."
}
}
},
"exceptions": {
"invalid_config_entry": {
"message": "Érvénytelen beállítás megadva. Ezt kaptam: {config_entry}"
}
}
},
"exceptions": {
"invalid_config_entry": {
"message": "Érvénytelen beállítás megadva. Ezt kaptam: {config_entry}"
}
}
}