new version

This commit is contained in:
Sergey Makarov
2024-11-18 00:43:28 +03:00
parent a7a945e6e1
commit 9cda11ad40
19 changed files with 603 additions and 892 deletions
+144 -150
View File
@@ -1,156 +1,150 @@
"""The HA Text AI Integration."""
from __future__ import annotations
"""The HA text AI integration."""
import logging
from typing import Any
import asyncio
import logging
from typing import Any
import voluptuous as vol
from openai import AsyncOpenAI
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant, ServiceCall
import homeassistant.helpers.config_validation as cv
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers import config_validation as cv
from homeassistant.components import input_text
from .const import (
DOMAIN,
PLATFORMS,
SERVICE_ASK_QUESTION,
SERVICE_CLEAR_HISTORY,
SERVICE_GET_HISTORY,
SERVICE_SET_SYSTEM_PROMPT,
CONF_MODEL,
CONF_TEMPERATURE,
CONF_MAX_TOKENS,
CONF_API_ENDPOINT,
CONF_REQUEST_INTERVAL,
)
from .coordinator import HATextAICoordinator
from .const import (
DOMAIN,
CONF_API_BASE,
CONF_REQUEST_INTERVAL,
DEFAULT_API_BASE,
DEFAULT_REQUEST_INTERVAL,
TEXT_HELPER_PREFIX,
TEXT_HELPER_MAX_LENGTH,
)
_LOGGER = logging.getLogger(__name__)
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = []
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up the HA Text AI component."""
hass.data.setdefault(DOMAIN, {})
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up HA Text AI from a config entry."""
client = AsyncOpenAI(
api_key=entry.data[CONF_API_KEY],
base_url=entry.data.get(CONF_API_BASE, DEFAULT_API_BASE)
)
hass.data[DOMAIN][entry.entry_id] = {
"client": client,
CONF_REQUEST_INTERVAL: entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
"queue": [],
"processing": False,
}
async def create_text_helper(name: str) -> str:
"""Create a Text Helper if it doesn't exist."""
object_id = f"{TEXT_HELPER_PREFIX}{name}"
entity_id = f"input_text.{object_id}"
entity_registry = er.async_get(hass)
if entity_registry.async_get(entity_id) is None:
await hass.services.async_call(
"input_text",
"create",
{
"name": f"AI Response {name}",
"id": object_id,
"max": TEXT_HELPER_MAX_LENGTH,
"initial": "",
},
)
return entity_id
async def handle_text_ai_call(call: ServiceCall) -> None:
"""Handle the text AI service call."""
entry_id = list(hass.data[DOMAIN].keys())[0] # Используем первую настроенную интеграцию
response_id = call.data.get("response_id", "default")
entity_id = await create_text_helper(response_id)
request_data = {
"prompt": call.data["prompt"],
"model": call.data.get("model", "gpt-3.5-turbo"),
"temperature": call.data.get("temperature", 0.7),
"max_tokens": call.data.get("max_tokens", 150),
"top_p": call.data.get("top_p", 1.0),
"frequency_penalty": call.data.get("frequency_penalty", 0.0),
"presence_penalty": call.data.get("presence_penalty", 0.0),
"entity_id": entity_id,
}
hass.data[DOMAIN][entry_id]["queue"].append(request_data)
if not hass.data[DOMAIN][entry_id]["processing"]:
asyncio.create_task(process_queue(hass, entry_id))
async def process_queue(hass: HomeAssistant, entry_id: str) -> None:
"""Process the queue of requests."""
if hass.data[DOMAIN][entry_id]["processing"]:
return
hass.data[DOMAIN][entry_id]["processing"] = True
client = hass.data[DOMAIN][entry_id]["client"]
while hass.data[DOMAIN][entry_id]["queue"]:
request = hass.data[DOMAIN][entry_id]["queue"].pop(0)
try:
response = await client.chat.completions.create(
model=request["model"],
messages=[{"role": "user", "content": request["prompt"]}],
temperature=request["temperature"],
max_tokens=request["max_tokens"],
top_p=request["top_p"],
frequency_penalty=request["frequency_penalty"],
presence_penalty=request["presence_penalty"],
)
response_text = response.choices[0].message.content
await hass.services.async_call(
"input_text",
"set_value",
{
"entity_id": request["entity_id"],
"value": response_text
},
)
except Exception as e:
_LOGGER.error("Error processing request: %s", str(e))
await asyncio.sleep(hass.data[DOMAIN][entry_id][CONF_REQUEST_INTERVAL])
hass.data[DOMAIN][entry_id]["processing"] = False
hass.services.async_register(
DOMAIN,
"text_ai_call",
handle_text_ai_call,
schema=vol.Schema({
vol.Required("prompt"): cv.string,
vol.Optional("response_id", default="default"): cv.string,
vol.Optional("model", default="gpt-3.5-turbo"): cv.string,
vol.Optional("temperature", default=0.7): vol.Coerce(float),
vol.Optional("max_tokens", default=150): vol.Coerce(int),
vol.Optional("top_p", default=1.0): vol.Coerce(float),
vol.Optional("frequency_penalty", default=0.0): vol.Coerce(float),
vol.Optional("presence_penalty", default=0.0): vol.Coerce(float),
})
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
hass.data[DOMAIN].pop(entry.entry_id)
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up the HA text AI component from configuration.yaml."""
hass.data.setdefault(DOMAIN, {})
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up HA text AI from a config entry."""
coordinator = HATextAICoordinator(
hass,
api_key=entry.data[CONF_API_KEY],
endpoint=entry.data.get(CONF_API_ENDPOINT),
model=entry.data.get(CONF_MODEL),
temperature=entry.data.get(CONF_TEMPERATURE),
max_tokens=entry.data.get(CONF_MAX_TOKENS),
request_interval=entry.data.get(CONF_REQUEST_INTERVAL),
)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def async_ask_question(call: ServiceCall) -> None:
"""Handle the ask_question service call."""
question = call.data["question"]
model = call.data.get("model", coordinator.model)
temperature = call.data.get("temperature", coordinator.temperature)
max_tokens = call.data.get("max_tokens", coordinator.max_tokens)
# Temporarily update parameters if they were overridden
original_model = coordinator.model
original_temperature = coordinator.temperature
original_max_tokens = coordinator.max_tokens
try:
coordinator.model = model
coordinator.temperature = temperature
coordinator.max_tokens = max_tokens
await coordinator.async_ask_question(question)
finally:
# Restore original parameters
coordinator.model = original_model
coordinator.temperature = original_temperature
coordinator.max_tokens = original_max_tokens
async def async_clear_history(call: ServiceCall) -> None:
"""Handle the clear_history service call."""
coordinator._responses.clear()
await coordinator.async_refresh()
async def async_get_history(call: ServiceCall) -> None:
"""Handle the get_history service call."""
limit = call.data.get("limit", 10)
history = list(coordinator._responses.items())[-limit:]
return {
"history": [
{"question": q, "response": r} for q, r in history
]
}
async def async_set_system_prompt(call: ServiceCall) -> None:
"""Handle the set_system_prompt service call."""
prompt = call.data["prompt"]
coordinator.system_prompt = prompt
# Register all services
hass.services.async_register(
DOMAIN,
SERVICE_ASK_QUESTION,
async_ask_question,
schema=vol.Schema({
vol.Required("question"): cv.string,
vol.Optional("model"): cv.string,
vol.Optional("temperature"): vol.Coerce(float),
vol.Optional("max_tokens"): vol.Coerce(int),
})
)
hass.services.async_register(
DOMAIN,
SERVICE_CLEAR_HISTORY,
async_clear_history,
schema=vol.Schema({})
)
hass.services.async_register(
DOMAIN,
SERVICE_GET_HISTORY,
async_get_history,
schema=vol.Schema({
vol.Optional("limit"): vol.Coerce(int),
})
)
hass.services.async_register(
DOMAIN,
SERVICE_SET_SYSTEM_PROMPT,
async_set_system_prompt,
schema=vol.Schema({
vol.Required("prompt"): cv.string,
})
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
# Unregister services
for service in [
SERVICE_ASK_QUESTION,
SERVICE_CLEAR_HISTORY,
SERVICE_GET_HISTORY,
SERVICE_SET_SYSTEM_PROMPT
]:
hass.services.async_remove(DOMAIN, service)
return unload_ok
+77 -118
View File
@@ -1,130 +1,89 @@
"""Config flow for HA Text AI Integration."""
from __future__ import annotations
import logging
from typing import Any
#### `config_flow.py`
```python
"""Config flow for HA text AI integration."""
import voluptuous as vol
from homeassistant import config_entries
import homeassistant.helpers.config_validation as cv
from homeassistant.core import callback
import voluptuous as vol
from openai import AsyncOpenAI
from openai import OpenAIError, AuthenticationError, APIConnectionError
from .const import (
DOMAIN,
CONF_MODEL,
CONF_TEMPERATURE,
CONF_MAX_TOKENS,
CONF_API_ENDPOINT,
CONF_REQUEST_INTERVAL,
DEFAULT_MODEL,
DEFAULT_TEMPERATURE,
DEFAULT_MAX_TOKENS,
DEFAULT_API_ENDPOINT,
DEFAULT_REQUEST_INTERVAL,
)
from homeassistant import config_entries
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for HA text AI."""
from .const import (
DOMAIN,
CONF_API_KEY,
CONF_API_BASE,
CONF_REQUEST_INTERVAL,
DEFAULT_API_BASE,
DEFAULT_REQUEST_INTERVAL,
)
VERSION = 1
_LOGGER = logging.getLogger(__name__)
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
class TextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for HA Text AI Integration."""
if user_input is not None:
return self.async_create_entry(title="HA text AI", data=user_input)
VERSION = 1
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({
vol.Required("api_key"): str,
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): str,
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.Coerce(float),
vol.Optional(CONF_MAX_TOKENS, default=DEFAULT_MAX_TOKENS): vol.Coerce(int),
vol.Optional(CONF_API_ENDPOINT, default=DEFAULT_API_ENDPOINT): str,
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.Coerce(float),
}),
errors=errors,
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors = {}
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
if user_input is not None:
try:
await self._test_api_key(user_input[CONF_API_KEY], user_input.get(CONF_API_BASE))
return self.async_create_entry(
title="HA Text AI",
data=user_input,
)
except ApiKeyError:
errors["base"] = "invalid_api_key"
except ApiConnectionError:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options flow for HA text AI."""
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_API_KEY): str,
vol.Optional(CONF_API_BASE, default=DEFAULT_API_BASE): str,
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): int,
}
),
errors=errors,
)
def __init__(self, config_entry):
"""Initialize options flow."""
self.config_entry = config_entry
@staticmethod
async def _test_api_key(api_key: str, api_base: str | None) -> None:
"""Test if the API key is valid."""
try:
client = AsyncOpenAI(
api_key=api_key,
base_url=api_base if api_base else DEFAULT_API_BASE
)
models = await client.models.list()
if not models.data:
raise ApiKeyError
except AuthenticationError as err:
raise ApiKeyError from err
except APIConnectionError as err:
raise ApiConnectionError from err
except OpenAIError as err:
if getattr(err, 'status_code', None) == 401:
raise ApiKeyError from err
raise ApiConnectionError from err
async def async_step_init(self, user_input=None):
"""Handle options flow."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> TextAIOptionsFlow:
"""Get the options flow for this handler."""
return TextAIOptionsFlow(config_entry)
class TextAIOptionsFlow(config_entries.OptionsFlow):
"""Handle options flow for HA Text AI Integration."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
CONF_REQUEST_INTERVAL,
default=self.config_entry.options.get(
CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL
),
): int,
}
),
)
class ApiKeyError(HomeAssistantError):
"""Error to indicate there is an invalid API key."""
class ApiConnectionError(HomeAssistantError):
"""Error to indicate there is a connection error."""
return self.async_show_form(
step_id="init",
data_schema=vol.Schema({
vol.Optional(
CONF_TEMPERATURE,
default=self.config_entry.options.get(
CONF_TEMPERATURE, DEFAULT_TEMPERATURE
),
): vol.Coerce(float),
vol.Optional(
CONF_MAX_TOKENS,
default=self.config_entry.options.get(
CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS
),
): vol.Coerce(int),
vol.Optional(
CONF_REQUEST_INTERVAL,
default=self.config_entry.options.get(
CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL
),
): vol.Coerce(float),
}),
)
+27 -10
View File
@@ -1,13 +1,30 @@
"""Constants for the HA Text AI Integration."""
DOMAIN = "ha_text_ai"
"""Constants for the HA text AI integration."""
from homeassistant.const import Platform
CONF_API_KEY = "api_key"
CONF_API_BASE = "api_base"
CONF_REQUEST_INTERVAL = "request_interval"
DOMAIN = "ha_text_ai"
PLATFORMS = [Platform.SENSOR]
DEFAULT_API_BASE = "https://api.openai.com/v1"
DEFAULT_REQUEST_INTERVAL = 5 # 5 seconds
# Configuration
CONF_MODEL = "model"
CONF_TEMPERATURE = "temperature"
CONF_MAX_TOKENS = "max_tokens"
CONF_API_ENDPOINT = "api_endpoint"
CONF_REQUEST_INTERVAL = "request_interval"
# Text Helper related
TEXT_HELPER_MAX_LENGTH = 65536
TEXT_HELPER_PREFIX = "text_ai_response_"
# Defaults
DEFAULT_MODEL = "gpt-3.5-turbo"
DEFAULT_TEMPERATURE = 0.7
DEFAULT_MAX_TOKENS = 1000
DEFAULT_API_ENDPOINT = "https://api.openai.com/v1"
DEFAULT_REQUEST_INTERVAL = 1.0
# Attributes
ATTR_QUESTION = "question"
ATTR_RESPONSE = "response"
ATTR_LAST_UPDATED = "last_updated"
# Services
SERVICE_ASK_QUESTION = "ask_question
SERVICE_CLEAR_HISTORY = "clear_history"
SERVICE_GET_HISTORY = "get_history"
SERVICE_SET_SYSTEM_PROMPT = "set_system_prompt"
@@ -0,0 +1,88 @@
"""Data coordinator for HA text AI."""
import asyncio
import logging
from datetime import timedelta
from typing import Any, Dict
import openai
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.exceptions import ConfigEntryAuthFailed
from .const import (
DOMAIN,
DEFAULT_REQUEST_INTERVAL,
)
_LOGGER = logging.getLogger(__name__)
class HATextAICoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the API."""
def __init__(
self,
hass: HomeAssistant,
api_key: str,
endpoint: str,
model: str,
temperature: float,
max_tokens: int,
request_interval: float,
) -> None:
"""Initialize."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=request_interval),
)
self.api_key = api_key
self.endpoint = endpoint
self.model = model
self.temperature = temperature
self.max_tokens = max_tokens
self._question_queue = asyncio.Queue()
self._responses: Dict[str, Any] = {}
openai.api_key = self.api_key
if endpoint != "https://api.openai.com/v1":
openai.api_base = endpoint
async def _async_update_data(self) -> Dict[str, Any]:
"""Update data via OpenAI API."""
if self._question_queue.empty():
return self._responses
try:
question = await self._question_queue.get()
response = await self.hass.async_add_executor_job(
self._make_api_call, question
)
self._responses[question] = response
return self._responses
except openai.error.AuthenticationError as err:
raise ConfigEntryAuthFailed from err
except Exception as err:
_LOGGER.error("Error communicating with API: %s", err)
return self._responses
def _make_api_call(self, question: str) -> str:
"""Make API call to OpenAI."""
try:
completion = openai.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": question}],
temperature=self.temperature,
max_tokens=self.max_tokens,
)
return completion.choices[0].message.content
except Exception as err:
_LOGGER.error("Error in API call: %s", err)
raise
async def async_ask_question(self, question: str) -> None:
"""Add question to queue."""
await self._question_queue.put(question)
await self.async_refresh()
+14 -16
View File
@@ -1,17 +1,15 @@
{
"domain": "ha_text_ai",
"name": "HA Text AI",
"version": "1.0.0",
"config_flow": true,
"documentation": "https://github.com/smkrv/ha-text-ai",
"issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
"requirements": ["openai>=1.0.0"],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
"codeowners": ["@smkrv"],
"iot_class": "cloud_polling",
"min_ha_version": "2023.8.0",
"icon": "images/icon.svg"
{
"domain": "ha_text_ai",
"name": "HA text AI",
"config_flow": true,
"documentation": "https://github.com/smkrv/ha_text_ai",
"issue_tracker": "https://github.com/smkrv/ha_text_ai/issues",
"requirements": ["openai>=1.0.0"],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
"codeowners": ["@smkrv"],
"version": "1.0.1",
"iot_class": "cloud_polling"
}
+44
View File
@@ -0,0 +1,44 @@
"""Sensor platform for HA text AI."""
from typing import Any, Callable, Dict, Optional
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, ATTR_QUESTION, ATTR_RESPONSE, ATTR_LAST_UPDATED
from .coordinator import HATextAICoordinator
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the HA text AI sensor."""
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([HATextAISensor(coordinator, entry)], True)
class HATextAISensor(CoordinatorEntity, SensorEntity):
"""HA text AI Sensor."""
def __init__(
self,
coordinator: HATextAICoordinator,
config_entry: ConfigEntry,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._config_entry = config_entry
self._attr_unique_id = f"{config_entry.entry_id}"
self._attr_name = "HA text AI"
@property
def extra_state_attributes(self) -> Optional[Dict[str, Any]]:
"""Return entity specific state attributes."""
return {
ATTR_QUESTION: list(self.coordinator.data.keys())[-1] if self.coordinator.data else None,
ATTR_RESPONSE: list(self.coordinator.data.values())[-1] if self.coordinator.data else None,
ATTR_LAST_UPDATED: self.coordinator.last_update_success_time,
}
@@ -1,34 +1,38 @@
{
"config": {
"abort": {
"already_configured": "Service is already configured"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_api_key": "Invalid API key",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"api_key": "API Key",
"api_base": "API Base URL",
"request_interval": "Request interval (seconds)"
},
"description": "Enter your OpenAI API credentials",
"title": "OpenAI API"
}
}
},
"options": {
"step": {
"init": {
"data": {
"request_interval": "Request interval (seconds)"
},
"description": "Configure HA Text AI options",
"title": "HA Text AI Options"
}
}
}
{
"config": {
"step": {
"user": {
"title": "Set up HA text AI",
"description": "Set up your OpenAI integration",
"data": {
"api_key": "API Key",
"model": "Model",
"temperature": "Temperature",
"max_tokens": "Max Tokens",
"api_endpoint": "API Endpoint",
"request_interval": "Request Interval (seconds)"
}
}
},
"error": {
"auth": "API key is invalid.",
"cannot_connect": "Failed to connect to API.",
"unknown": "Unexpected error occurred."
},
"abort": {
"already_configured": "Device is already configured"
}
},
"options": {
"step": {
"init": {
"title": "HA text AI Options",
"data": {
"temperature": "Temperature",
"max_tokens": "Max Tokens",
"request_interval": "Request Interval (seconds)"
}
}
}
}
}
@@ -1,34 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Сервис уже настроен"
},
"error": {
"cannot_connect": "Ошибка подключения",
"invalid_api_key": "Неверный API ключ",
"unknown": "Неожиданная ошибка"
},
"step": {
"user": {
"data": {
"api_key": "API ключ",
"api_base": "Базовый URL API",
"request_interval": "Интервал запросов (секунды)"
},
"description": "Введите учетные данные OpenAI API",
"title": "OpenAI API"
}
}
},
"options": {
"step": {
"init": {
"data": {
"request_interval": "Интервал запросов (секунды)"
},
"description": "Настройка параметров HA Text AI",
"title": "Настройки HA Text AI"
}
}
}
}