Compare commits

...
12 Commits
Author SHA1 Message Date
SMKRV d899144149 Release v1.0.3 2024-11-19 14:00:33 +03:00
SMKRV 5d49b4a40b Release v1.0.2 2024-11-19 13:16:14 +03:00
SMKRV 1a84727cf1 Release v1.0.2 2024-11-19 13:14:00 +03:00
SMKRV d3c7e25202 Release v1.0.2 2024-11-19 13:12:41 +03:00
SMKRV 5e82b9669c Release v1.0.2 2024-11-19 13:10:18 +03:00
SMKRV 103f9d59e9 Release v1.0.1c 2024-11-19 12:46:25 +03:00
SMKRV 6ad67a5acf Release v1.0.1c 2024-11-19 12:45:26 +03:00
SMKRV 12e5778a1c Release v1.0.1b 2024-11-19 01:18:10 +03:00
SMKRV 5c85cba606 Release v1.0.1b 2024-11-19 00:59:35 +03:00
SMKRV 63d28e12d6 Release v1.0.1b 2024-11-19 00:59:15 +03:00
SMKRV 605c73c6c4 Release v1.0.1b 2024-11-19 00:56:56 +03:00
SMKRV 9d54f88520 Release v1.0.1b 2024-11-19 00:56:33 +03:00
15 changed files with 772 additions and 440 deletions
+32 -3
View File
@@ -2,13 +2,42 @@ name: Validate with hassfest
on:
push:
branches:
- main
- dev
paths:
- 'custom_components/**'
- '.github/workflows/**'
pull_request:
branches:
- main
paths:
- 'custom_components/**'
- '.github/workflows/**'
schedule:
- cron: "0 0 * * *"
- cron: "0 0 * * *" # Run daily at midnight UTC
workflow_dispatch: # Allow manual trigger
jobs:
validate:
name: Validate with hassfest
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- uses: home-assistant/actions/hassfest@master
- name: ⤵️ Check out code from GitHub
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🚀 Run hassfest validation
uses: home-assistant/actions/hassfest@master
- name: ️ Print hassfest version
if: always()
run: |
echo "Hassfest version: $(hassfest --version)"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
-24
View File
@@ -1,24 +0,0 @@
name: Validate
on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: |
pytest
+110 -90
View File
@@ -1,145 +1,165 @@
# 🤖 HA text AI Integration for Home Assistant
# 🤖 HA Text AI for Home Assistant
<div align="center">
![GitHub release](https://img.shields.io/github/release/smkrv/ha-text-ai.svg)
![GitHub release](https://img.shields.io/github/release/smkrv/ha-text-ai.svg?style=flat-square)
![GitHub downloads](https://img.shields.io/github/downloads/smkrv/ha-text-ai/total.svg?style=flat-square)
![GitHub stars](https://img.shields.io/github/stars/smkrv/ha-text-ai.svg?style=social)
![GitHub forks](https://img.shields.io/github/forks/smkrv/ha-text-ai.svg?style=social)
![GitHub issues](https://img.shields.io/github/issues/smkrv/ha-text-ai.svg)
[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
![GitHub last commit](https://img.shields.io/github/last-commit/smkrv/ha-text-ai.svg?style=flat-square)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
</div>
<p align="center">
Powerful OpenAI integration for Home Assistant enabling natural language interaction with your smart home
Transform your smart home experience with powerful AI assistance powered by OpenAI's GPT models
</p>
---
## 📋 Table of Contents
- [Features](#-features)
- [Installation](#-installation)
- [Configuration](#-configuration)
- [Services](#-services)
- [Advanced Usage](#-advanced-usage)
- [Troubleshooting](#-troubleshooting)
- [Contributing](#-contributing)
## 🌟 Features
## ✨ Features
- 🧠 **Advanced AI Integration**: Leverage OpenAI's powerful models (GPT-3.5, GPT-4) for smart home interactions
- 💬 **Natural Language Control**: Control your home and get information using everyday language
- 📝 **Conversation Memory**: Maintain context with conversation history tracking
-**Real-time Responses**: Get quick, contextual responses to your queries
- 🎯 **Customizable Behavior**: Fine-tune AI responses with adjustable parameters
- 🔒 **Secure Integration**: Your API key and data are handled securely
- 🎨 **Flexible Configuration**: Easy setup with multiple configuration options
- 🔄 **Automation Ready**: Integrate AI responses into your automations
- 🔄 **Real-time AI Interaction**: Seamless communication with OpenAI's latest models
- 📝 **Conversation History**: Track and manage your AI interactions
- ⚙️ **Customizable Settings**: Fine-tune AI behavior with adjustable parameters
- 🔌 **Easy Integration**: Simple setup process through HACS or manual installation
- 🎯 **System Prompts**: Set context for more relevant AI responses
## 📋 Prerequisites
## 🚀 Installation
- Home Assistant installation (Core, OS, Container, or Supervised)
- OpenAI API key ([Get one here](https://platform.openai.com/account/api-keys))
- Python 3.9 or newer
### HACS Installation (Recommended)
1. Ensure [HACS](https://hacs.xyz/) is installed
2. Search for "HA text AI" in HACS
3. Click Install
4. Restart Home Assistant
## ⚡ Quick Start
<details>
<summary>Manual Installation Steps</summary>
```bash
# 1. Navigate to your Home Assistant configuration directory
cd ~/.homeassistant
# 2. Create custom_components directory if it doesn't exist
mkdir -p custom_components
# 3. Clone the repository
git clone https://github.com/smkrv/ha-text-ai.git custom_components/ha_text_ai
# 4. Restart Home Assistant
```
</details>
## ⚙️ Configuration
### Basic Configuration
### Manual Installation
1. Download the repository
2. Copy `custom_components/ha_text_ai` to your `custom_components` directory
3. Restart Home Assistant
4. Add configuration to `configuration.yaml`:
```yaml
ha_text_ai:
api_key: your_openai_api_key
model: gpt-3.5-turbo
api_key: !secret openai_api_key
```
### Advanced Configuration
```yaml
ha_text_ai:
api_key: your_openai_api_key
model: gpt-4
temperature: 0.8
max_tokens: 2000
api_endpoint: https://custom-endpoint.com/v1
request_interval: 2.0
```
## ⚙️ Configuration Options
## 🛠 Services
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `api_key` | string | Required | Your OpenAI API key |
| `model` | string | `gpt-3.5-turbo` | AI model to use |
| `temperature` | float | `0.7` | Response creativity (0-2) |
| `max_tokens` | integer | `1000` | Maximum response length |
| `request_interval` | float | `1.0` | Minimum seconds between requests |
| `api_endpoint` | string | OpenAI default | Custom API endpoint URL |
### Ask Question
## 🛠️ Available Services
### ask_question
Ask the AI assistant a question:
```yaml
service: ha_text_ai.ask_question
data:
question: "What's the weather like today?"
question: "What's the optimal temperature for sleeping?"
model: "gpt-4" # optional
temperature: 0.7 # optional
temperature: 0.5 # optional
max_tokens: 500 # optional
```
### More Services
- `ha_text_ai.clear_history`: Reset conversation history
- `ha_text_ai.get_history`: Retrieve past interactions
- `ha_text_ai.set_system_prompt`: Configure AI behavior
### set_system_prompt
Configure AI behavior:
```yaml
service: ha_text_ai.set_system_prompt
data:
prompt: "You are a home automation expert focused on energy efficiency"
```
## 🔍 Advanced Usage
### clear_history
Reset conversation history:
```yaml
service: ha_text_ai.clear_history
```
### Automation Example
### get_history
Retrieve conversation history:
```yaml
service: ha_text_ai.get_history
data:
limit: 5 # optional
```
## 🔧 Practical Examples
### Smart Temperature Management
```yaml
automation:
trigger:
platform: time_pattern
hours: "/1"
action:
service: ha_text_ai.ask_question
data:
question: >
Current temperature is {{ states('sensor.living_room_temperature') }}°C.
Should I adjust the thermostat for optimal comfort and energy savings?
```
### Smart Lighting Assistant
```yaml
automation:
trigger:
platform: state
entity_id: binary_sensor.motion
entity_id: binary_sensor.living_room_motion
to: 'on'
condition:
condition: template
value_template: "{{ states('sensor.illuminance') | float < 10 }}"
action:
service: ha_text_ai.ask_question
data:
question: "What should I do when motion is detected?"
question: >
Motion detected in living room with low light levels.
What's the best lighting scene to set based on the time of day?
```
## 🔧 Troubleshooting
## ❗ Common Issues
<details>
<summary>Common Issues and Solutions</summary>
### API Rate Limits
- Increase `request_interval` if hitting rate limits
- Consider upgrading your OpenAI plan
- Use caching for frequent queries
### API Key Issues
- Verify API key format
- Check API key permissions
- Ensure proper configuration in secrets.yaml
### High Token Usage
- Reduce `max_tokens` parameter
- Clear conversation history regularly
- Use focused system prompts
### Connection Problems
- Verify internet connection
- Check API endpoint accessibility
- Review Home Assistant logs
</details>
### Connection Issues
- Check internet connectivity
- Verify API key validity
- Ensure endpoint accessibility
## 👥 Contributing
## 🤝 Contributing
We welcome contributions! Here's how you can help:
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create a feature branch
3. Commit your changes
4. Push to the branch
5. Submit a pull request
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
## 📝 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
---
<div align="center">
**[Documentation](https://github.com/smkrv/ha-text-ai/wiki)** | **[Report Bug](https://github.com/smkrv/ha-text-ai/issues)** | **[Request Feature](https://github.com/smkrv/ha-text-ai/issues)**
Made with ❤️ for the Home Assistant Community
</div>
+92 -90
View File
@@ -5,10 +5,10 @@ from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant, ServiceCall
import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady
from .const import (
DOMAIN,
@@ -23,100 +23,101 @@ from .const import (
CONF_API_ENDPOINT,
CONF_REQUEST_INTERVAL,
)
from .coordinator import HATextAICoordinator
_LOGGER = logging.getLogger(__name__)
def get_coordinator():
"""Get coordinator class with lazy import to avoid circular deps."""
from .coordinator import HATextAICoordinator
return HATextAICoordinator
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_MODEL, default="gpt-3.5-turbo"): cv.string,
vol.Optional(CONF_TEMPERATURE, default=0.7): vol.Coerce(float),
vol.Optional(CONF_MAX_TOKENS, default=1000): vol.Coerce(int),
vol.Optional(CONF_REQUEST_INTERVAL, default=1.0): vol.Coerce(float),
vol.Optional(CONF_API_ENDPOINT): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up the HA text AI component from configuration.yaml."""
"""Set up the HA text AI component."""
hass.data.setdefault(DOMAIN, {})
async def async_ask_question(call: ServiceCall) -> None:
"""Handle the ask_question service call.
"""Handle the ask_question service call."""
if not hass.data[DOMAIN]:
raise HomeAssistantError("No AI Text integration configured")
coordinator = next(iter(hass.data[DOMAIN].values()))
question = call.data["question"]
original_params = {
"model": coordinator.model,
"temperature": coordinator.temperature,
"max_tokens": coordinator.max_tokens
}
Args:
call: Service call containing question and optional parameters.
"""
try:
# Get the coordinator from the first config entry
if not hass.data[DOMAIN]:
raise HomeAssistantError("No AI Text integration configured")
if "model" in call.data:
coordinator.model = call.data["model"]
if "temperature" in call.data:
coordinator.temperature = call.data["temperature"]
if "max_tokens" in call.data:
coordinator.max_tokens = call.data["max_tokens"]
coordinator = next(iter(hass.data[DOMAIN].values()))
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
await coordinator.async_ask_question(question)
except Exception as ex:
_LOGGER.error("Error asking question: %s", str(ex))
raise HomeAssistantError(f"Failed to ask question: {str(ex)}")
raise HomeAssistantError(f"Failed to ask question: {str(ex)}") from ex
finally:
coordinator.model = original_params["model"]
coordinator.temperature = original_params["temperature"]
coordinator.max_tokens = original_params["max_tokens"]
async def async_clear_history(call: ServiceCall) -> None:
"""Handle the clear_history service call."""
try:
if not hass.data[DOMAIN]:
raise HomeAssistantError("No AI Text integration configured")
if not hass.data[DOMAIN]:
raise HomeAssistantError("No AI Text integration configured")
coordinator = next(iter(hass.data[DOMAIN].values()))
coordinator._responses.clear()
await coordinator.async_refresh()
except Exception as ex:
_LOGGER.error("Error clearing history: %s", str(ex))
raise HomeAssistantError(f"Failed to clear history: {str(ex)}")
coordinator = next(iter(hass.data[DOMAIN].values()))
coordinator._responses.clear()
await coordinator.async_refresh()
async def async_get_history(call: ServiceCall) -> dict[str, list]:
"""Handle the get_history service call.
"""Handle the get_history service call."""
if not hass.data[DOMAIN]:
raise HomeAssistantError("No AI Text integration configured")
Returns:
Dictionary containing chat history.
"""
try:
if not hass.data[DOMAIN]:
raise HomeAssistantError("No AI Text integration configured")
coordinator = next(iter(hass.data[DOMAIN].values()))
if not coordinator._responses:
return {"history": []}
coordinator = next(iter(hass.data[DOMAIN].values()))
limit = call.data.get("limit", 10)
history = list(coordinator._responses.items())[-limit:]
return {
"history": [
{"question": q, "response": r} for q, r in history
]
}
except Exception as ex:
_LOGGER.error("Error getting history: %s", str(ex))
raise HomeAssistantError(f"Failed to get history: {str(ex)}")
limit = call.data.get("limit", 10)
history = list(coordinator._responses.items())
limited_history = history[-limit:] if len(history) > limit else history
return {
"history": [
{"question": q, "response": r} for q, r in limited_history
]
}
async def async_set_system_prompt(call: ServiceCall) -> None:
"""Handle the set_system_prompt service call."""
try:
if not hass.data[DOMAIN]:
raise HomeAssistantError("No AI Text integration configured")
if not hass.data[DOMAIN]:
raise HomeAssistantError("No AI Text integration configured")
coordinator = next(iter(hass.data[DOMAIN].values()))
prompt = call.data["prompt"]
coordinator.system_prompt = prompt
except Exception as ex:
_LOGGER.error("Error setting system prompt: %s", str(ex))
raise HomeAssistantError(f"Failed to set system prompt: {str(ex)}")
coordinator = next(iter(hass.data[DOMAIN].values()))
coordinator.system_prompt = call.data["prompt"]
# Register services
hass.services.async_register(
DOMAIN,
SERVICE_ASK_QUESTION,
@@ -164,6 +165,7 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up HA text AI from a config entry."""
HATextAICoordinator = get_coordinator()
try:
coordinator = HATextAICoordinator(
hass,
@@ -176,32 +178,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = coordinator
return await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
except Exception as ex:
_LOGGER.error("Error setting up entry: %s", str(ex))
raise ConfigEntryNotReady from ex
raise ConfigEntryNotReady(f"Failed to setup entry: {str(ex)}") from ex
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
try:
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
if entry.entry_id not in hass.data.get(DOMAIN, {}):
return True
# Only remove services if this is the last entry
if not hass.data[DOMAIN]:
for service in [
SERVICE_ASK_QUESTION,
SERVICE_CLEAR_HISTORY,
SERVICE_GET_HISTORY,
SERVICE_SET_SYSTEM_PROMPT
]:
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
# Remove services only if this was the last instance
if not hass.data[DOMAIN]:
services = [
SERVICE_ASK_QUESTION,
SERVICE_CLEAR_HISTORY,
SERVICE_GET_HISTORY,
SERVICE_SET_SYSTEM_PROMPT
]
for service in services:
if DOMAIN in hass.services.async_services() and \
service in hass.services.async_services()[DOMAIN]:
hass.services.async_remove(DOMAIN, service)
return unload_ok
except Exception as ex:
_LOGGER.error("Error unloading entry: %s", str(ex))
return False
return unload_ok
+105 -36
View File
@@ -1,8 +1,12 @@
"""Config flow for HA text AI integration."""
from typing import Any, Dict, Optional
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY
import homeassistant.helpers.config_validation as cv
from homeassistant.core import callback
import openai
from .const import (
DOMAIN,
@@ -18,70 +22,135 @@ from .const import (
DEFAULT_REQUEST_INTERVAL,
)
class HATextAIConfigFlow(config_entries.ConfigFlow):
import logging
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema({
vol.Required(CONF_API_KEY): str,
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): str,
vol.Optional(
CONF_TEMPERATURE,
default=DEFAULT_TEMPERATURE
): vol.All(vol.Coerce(float), vol.Range(min=0, max=2)),
vol.Optional(
CONF_MAX_TOKENS,
default=DEFAULT_MAX_TOKENS
): vol.All(vol.Coerce(int), vol.Range(min=1, max=4096)),
vol.Optional(CONF_API_ENDPOINT, default=DEFAULT_API_ENDPOINT): str,
vol.Optional(
CONF_REQUEST_INTERVAL,
default=DEFAULT_REQUEST_INTERVAL
): vol.All(vol.Coerce(float), vol.Range(min=0.1)),
})
class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for HA text AI."""
VERSION = 1
DOMAIN = DOMAIN # Define the domain as a class variable
async def async_step_user(self, user_input=None):
async def async_step_user(
self,
user_input: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Handle the initial step."""
errors = {}
errors: Dict[str, str] = {}
if user_input is not None:
return self.async_create_entry(title="HA text AI", data=user_input)
try:
# Create OpenAI client
client = openai.OpenAI(
api_key=user_input[CONF_API_KEY],
base_url=user_input.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT)
)
# Verify API connection and model availability
models = await self.hass.async_add_executor_job(client.models.list)
model_ids = [model.id for model in models.data]
if user_input[CONF_MODEL] not in model_ids:
_LOGGER.warning(
"Selected model %s not found in available models: %s",
user_input[CONF_MODEL],
", ".join(model_ids)
)
errors["base"] = "invalid_model"
else:
await self.async_set_unique_id(user_input[CONF_API_KEY])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title="HA text AI",
data=user_input
)
except openai.AuthenticationError as err:
_LOGGER.error("Authentication failed: %s", str(err))
errors["base"] = "invalid_auth"
except openai.APIError as err:
_LOGGER.error("API connection failed: %s", str(err))
errors["base"] = "cannot_connect"
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Unexpected error: %s", str(err))
errors["base"] = "unknown"
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),
}),
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
description_placeholders={
"default_model": DEFAULT_MODEL,
"default_endpoint": DEFAULT_API_ENDPOINT,
}
)
@staticmethod
@callback
def async_get_options_flow(config_entry):
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options flow for HA text AI."""
def __init__(self, config_entry):
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=None):
async def async_step_init(
self,
user_input: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Handle options flow."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
options_schema = vol.Schema({
vol.Optional(
CONF_TEMPERATURE,
default=self.config_entry.options.get(
CONF_TEMPERATURE, DEFAULT_TEMPERATURE
),
description={"suggested_value": DEFAULT_TEMPERATURE},
): vol.All(vol.Coerce(float), vol.Range(min=0, max=2)),
vol.Optional(
CONF_MAX_TOKENS,
default=self.config_entry.options.get(
CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS
),
description={"suggested_value": DEFAULT_MAX_TOKENS},
): vol.All(vol.Coerce(int), vol.Range(min=1, max=4096)),
vol.Optional(
CONF_REQUEST_INTERVAL,
default=self.config_entry.options.get(
CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL
),
description={"suggested_value": DEFAULT_REQUEST_INTERVAL},
): vol.All(vol.Coerce(float), vol.Range(min=0.1)),
})
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),
}),
data_schema=options_schema,
)
+85 -23
View File
@@ -1,30 +1,92 @@
"""Constants for the HA text AI integration."""
from typing import Final
from homeassistant.const import Platform
DOMAIN = "ha_text_ai"
PLATFORMS = [Platform.SENSOR]
# Domain
DOMAIN: Final = "ha_text_ai"
PLATFORMS: Final = [Platform.SENSOR]
# Configuration
CONF_MODEL = "model"
CONF_TEMPERATURE = "temperature"
CONF_MAX_TOKENS = "max_tokens"
CONF_API_ENDPOINT = "api_endpoint"
CONF_REQUEST_INTERVAL = "request_interval"
# Configuration constants
CONF_MODEL: Final = "model"
CONF_TEMPERATURE: Final = "temperature"
CONF_MAX_TOKENS: Final = "max_tokens"
CONF_API_ENDPOINT: Final = "api_endpoint"
CONF_REQUEST_INTERVAL: Final = "request_interval"
# 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
# Default values
DEFAULT_MODEL: Final = "gpt-3.5-turbo"
DEFAULT_TEMPERATURE: Final = 0.7
DEFAULT_MAX_TOKENS: Final = 1000
DEFAULT_API_ENDPOINT: Final = "https://api.openai.com/v1"
DEFAULT_REQUEST_INTERVAL: Final = 1.0
# Services
SERVICE_ASK_QUESTION = "ask_question"
SERVICE_CLEAR_HISTORY = "clear_history"
SERVICE_GET_HISTORY = "get_history"
SERVICE_SET_SYSTEM_PROMPT = "set_system_prompt"
# Parameter constraints
MIN_TEMPERATURE: Final = 0.0
MAX_TEMPERATURE: Final = 2.0
MIN_MAX_TOKENS: Final = 1
MAX_MAX_TOKENS: Final = 4096
MIN_REQUEST_INTERVAL: Final = 0.1
# Attributes
ATTR_QUESTION = "question"
ATTR_RESPONSE = "response"
ATTR_LAST_UPDATED = "last_updated"
# Service names
SERVICE_ASK_QUESTION: Final = "ask_question"
SERVICE_CLEAR_HISTORY: Final = "clear_history"
SERVICE_GET_HISTORY: Final = "get_history"
SERVICE_SET_SYSTEM_PROMPT: Final = "set_system_prompt"
# Service descriptions
SERVICE_ASK_QUESTION_DESCRIPTION: Final = "Ask a question to the AI model"
SERVICE_CLEAR_HISTORY_DESCRIPTION: Final = "Clear conversation history"
SERVICE_GET_HISTORY_DESCRIPTION: Final = "Get conversation history"
SERVICE_SET_SYSTEM_PROMPT_DESCRIPTION: Final = "Set system prompt for AI model"
# Attribute keys
ATTR_QUESTION: Final = "question"
ATTR_RESPONSE: Final = "response"
ATTR_LAST_UPDATED: Final = "last_updated"
ATTR_MODEL: Final = "model"
ATTR_TEMPERATURE: Final = "temperature"
ATTR_MAX_TOKENS: Final = "max_tokens"
ATTR_TOTAL_RESPONSES: Final = "total_responses"
ATTR_SYSTEM_PROMPT: Final = "system_prompt"
ATTR_RESPONSE_TIME: Final = "response_time"
# Error messages
ERROR_INVALID_API_KEY: Final = "invalid_api_key"
ERROR_CANNOT_CONNECT: Final = "cannot_connect"
ERROR_UNKNOWN: Final = "unknown_error"
ERROR_INVALID_MODEL: Final = "invalid_model"
ERROR_RATE_LIMIT: Final = "rate_limit_exceeded"
ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded"
ERROR_API_ERROR: Final = "api_error"
# Configuration descriptions
CONF_MODEL_DESCRIPTION: Final = "AI model to use for responses"
CONF_TEMPERATURE_DESCRIPTION: Final = "Temperature for response generation (0-2)"
CONF_MAX_TOKENS_DESCRIPTION: Final = "Maximum tokens in response (1-4096)"
CONF_API_ENDPOINT_DESCRIPTION: Final = "API endpoint URL"
CONF_REQUEST_INTERVAL_DESCRIPTION: Final = "Minimum time between API requests (seconds)"
# Entity attributes descriptions
ATTR_QUESTION_DESCRIPTION: Final = "Last question asked"
ATTR_RESPONSE_DESCRIPTION: Final = "Last response received"
ATTR_LAST_UPDATED_DESCRIPTION: Final = "Time of last update"
ATTR_MODEL_DESCRIPTION: Final = "Current AI model in use"
ATTR_TEMPERATURE_DESCRIPTION: Final = "Current temperature setting"
ATTR_MAX_TOKENS_DESCRIPTION: Final = "Current max tokens setting"
ATTR_TOTAL_RESPONSES_DESCRIPTION: Final = "Total number of responses"
ATTR_SYSTEM_PROMPT_DESCRIPTION: Final = "Current system prompt"
ATTR_RESPONSE_TIME_DESCRIPTION: Final = "Time taken for last response"
# Entity attributes
ENTITY_NAME: Final = "HA Text AI"
ENTITY_ICON: Final = "mdi:robot"
# Translation keys
TRANSLATION_KEY_CONFIG: Final = "config"
TRANSLATION_KEY_OPTIONS: Final = "options"
TRANSLATION_KEY_ERROR: Final = "error"
# State attributes
STATE_READY: Final = "ready"
STATE_PROCESSING: Final = "processing"
STATE_ERROR: Final = "error"
+60 -41
View File
@@ -1,21 +1,50 @@
"""The HA Text AI integration."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN, PLATFORMS
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up HA Text AI from a config entry."""
try:
coordinator = HATextAICoordinator(
hass,
api_key=entry.data["api_key"],
endpoint=entry.data.get("api_endpoint", "https://api.openai.com/v1"),
model=entry.data.get("model", "gpt-3.5-turbo"),
temperature=entry.data.get("temperature", 0.7),
max_tokens=entry.data.get("max_tokens", 1000),
request_interval=entry.data.get("request_interval", 1.0),
)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
return await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
except Exception as ex:
raise ConfigEntryNotReady(f"Failed to setup entry: {str(ex)}") from ex
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)
return unload_ok
"""Data coordinator for HA text AI."""
import asyncio
import logging
from datetime import timedelta
from typing import Any, Dict
from typing import Any, Dict, Optional
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,
CONF_MODEL,
CONF_TEMPERATURE,
CONF_MAX_TOKENS,
)
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -40,18 +69,26 @@ class HATextAICoordinator(DataUpdateCoordinator):
update_interval=timedelta(seconds=request_interval),
)
if not api_key:
raise ValueError("API key is required")
if not isinstance(temperature, (int, float)) or not 0 <= temperature <= 2:
raise ValueError("Temperature must be between 0 and 2")
if not isinstance(max_tokens, int) or max_tokens < 1:
raise ValueError("Max tokens must be a positive integer")
self.api_key = api_key
self.endpoint = endpoint
self.model = model
self.temperature = temperature
self.max_tokens = max_tokens
self.endpoint = endpoint or "https://api.openai.com/v1"
self.model = model or "gpt-3.5-turbo"
self.temperature = float(temperature)
self.max_tokens = int(max_tokens)
self._question_queue = asyncio.Queue()
self._responses: Dict[str, Any] = {}
self.system_prompt: Optional[str] = None
openai.api_key = self.api_key
if endpoint != "https://api.openai.com/v1":
openai.api_base = endpoint
self.client = openai.OpenAI(
api_key=self.api_key,
base_url=self.endpoint
)
async def _async_update_data(self) -> Dict[str, Any]:
"""Update data via OpenAI API."""
@@ -63,15 +100,14 @@ class HATextAICoordinator(DataUpdateCoordinator):
response_content = await self.hass.async_add_executor_job(
self._make_api_call, question
)
response = {
self._responses[question] = {
"question": question,
"response": response_content
}
self._responses[question] = response
_LOGGER.debug(f"Response from API: {response}")
_LOGGER.debug("Response from API: %s", response_content)
return self._responses
except openai.error.AuthenticationError as err:
except openai.AuthenticationError as err:
raise ConfigEntryAuthFailed from err
except Exception as err:
_LOGGER.error("Error communicating with API: %s", err)
@@ -80,9 +116,12 @@ class HATextAICoordinator(DataUpdateCoordinator):
def _make_api_call(self, question: str) -> str:
"""Make API call to OpenAI."""
try:
messages = [{"role": "system", "content": self.system_prompt}] if self.system_prompt else []
messages = []
if self.system_prompt:
messages.append({"role": "system", "content": self.system_prompt})
messages.append({"role": "user", "content": question})
completion = openai.chat.completions.create(
completion = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=self.temperature,
@@ -92,23 +131,3 @@ class HATextAICoordinator(DataUpdateCoordinator):
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)
_LOGGER.debug(f"Question added to queue: {question}")
await self.async_refresh()
def clear_history(self) -> None:
"""Clear the stored question and response history."""
self._responses.clear()
_LOGGER.info("History cleared.")
def get_history(self, limit: int = 10) -> Dict[str, Any]:
"""Get the history of questions and responses."""
return {"history": list(self._responses.values())[-limit:]}
def set_system_prompt(self, prompt: str) -> None:
"""Set a system prompt that will be used for all future questions."""
self.system_prompt = prompt
_LOGGER.info(f"System prompt set: {prompt}")
+8 -9
View File
@@ -1,15 +1,14 @@
{
"domain": "ha-text-ai",
"name": "HA text AI",
"domain": "ha_text_ai",
"name": "HA Text AI",
"codeowners": ["@smkrv"],
"config_flow": true,
"documentation": "https://github.com/smkrv/ha-text-ai",
"dependencies": [],
"documentation": "https://github.com/smkrv/ha-text-ai/wiki",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
"requirements": ["openai>=1.0.0"],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
"codeowners": ["@smkrv"],
"version": "1.0.1b",
"iot_class": "cloud_polling"
"version": "1.0.3",
"zeroconf": []
}
+97 -17
View File
@@ -1,16 +1,34 @@
"""Sensor platform for HA text AI."""
from datetime import datetime
import logging
from typing import Any, Callable, Dict, Optional
from homeassistant.components.sensor import SensorEntity, SensorStateClass
from homeassistant.components.sensor import (
SensorEntity,
SensorStateClass,
SensorDeviceClass,
)
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 homeassistant.util import dt as dt_util
from .const import DOMAIN, ATTR_QUESTION, ATTR_RESPONSE, ATTR_LAST_UPDATED
from .const import (
DOMAIN,
ATTR_QUESTION,
ATTR_RESPONSE,
ATTR_LAST_UPDATED,
ATTR_MODEL,
ATTR_TEMPERATURE,
ATTR_MAX_TOKENS,
ATTR_TOTAL_RESPONSES,
)
from .coordinator import HATextAICoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
@@ -23,6 +41,11 @@ async def async_setup_entry(
class HATextAISensor(CoordinatorEntity, SensorEntity):
"""HA text AI Sensor."""
_attr_has_entity_name = True
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_device_class = SensorDeviceClass.TIMESTAMP
_attr_icon = "mdi:robot"
def __init__(
self,
coordinator: HATextAICoordinator,
@@ -32,27 +55,84 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
super().__init__(coordinator)
self._config_entry = config_entry
self._attr_unique_id = f"{config_entry.entry_id}"
self._attr_name = "HA text AI"
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_name = "Last Response"
self._attr_suggested_display_precision = 0
@property
def state(self) -> StateType:
"""Return the state of the sensor."""
if self.coordinator.data:
return "Ready" # Assuming "Ready" is a valid state, you might want to return something meaningful, like the last response time.
return "Not Ready"
if not self.coordinator.data or not self.coordinator.last_update_success_time:
return None
# Convert to local time
if isinstance(self.coordinator.last_update_success_time, datetime):
return dt_util.as_local(self.coordinator.last_update_success_time)
return self.coordinator.last_update_success_time
@property
def extra_state_attributes(self) -> Optional[Dict[str, Any]]:
"""Return entity specific state attributes."""
if not self.coordinator.data:
return None
keys = list(self.coordinator.data.keys())
values = list(self.coordinator.data.values())
last_question = keys[-1]
last_response = values[-1]
return {
ATTR_QUESTION: last_question,
ATTR_RESPONSE: last_response,
ATTR_LAST_UPDATED: self.coordinator.last_update_success_time,
}
return {
ATTR_TOTAL_RESPONSES: 0,
ATTR_MODEL: self.coordinator.model,
ATTR_TEMPERATURE: self.coordinator.temperature,
ATTR_MAX_TOKENS: self.coordinator.max_tokens,
}
try:
history = list(self.coordinator.data.items())
if not history:
return {
ATTR_TOTAL_RESPONSES: 0,
ATTR_MODEL: self.coordinator.model,
ATTR_TEMPERATURE: self.coordinator.temperature,
ATTR_MAX_TOKENS: self.coordinator.max_tokens,
}
last_question, last_data = history[-1]
# Handle different response formats
if isinstance(last_data, dict):
last_response = last_data.get("response", "")
last_updated = last_data.get("timestamp", self.coordinator.last_update_success_time)
else:
last_response = str(last_data)
last_updated = self.coordinator.last_update_success_time
# Convert timestamp to local time if needed
if isinstance(last_updated, datetime):
last_updated = dt_util.as_local(last_updated)
return {
ATTR_QUESTION: last_question,
ATTR_RESPONSE: last_response,
ATTR_LAST_UPDATED: last_updated,
ATTR_TOTAL_RESPONSES: len(history),
ATTR_MODEL: self.coordinator.model,
ATTR_TEMPERATURE: self.coordinator.temperature,
ATTR_MAX_TOKENS: self.coordinator.max_tokens,
}
except Exception as err:
_LOGGER.error("Error getting attributes: %s", err, exc_info=True)
return {
ATTR_TOTAL_RESPONSES: 0,
ATTR_MODEL: self.coordinator.model,
ATTR_TEMPERATURE: self.coordinator.temperature,
ATTR_MAX_TOKENS: self.coordinator.max_tokens,
}
@property
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.last_update_success
@property
def should_poll(self) -> bool:
"""No need to poll. Coordinator notifies entity of updates."""
return False
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self._handle_coordinator_update()
+61 -18
View File
@@ -1,63 +1,94 @@
ask_question:
name: Ask Question
description: Send a question to the AI and get a response
description: >-
Send a question to the AI model and receive a detailed response.
The response will be stored in the conversation history and can be retrieved later.
fields:
question:
name: Question
description: The question or prompt to send to the AI
description: >-
Your question or prompt for the AI assistant. Be specific and clear for better results.
You can ask about home automation, technical advice, or general questions.
required: true
example: "What is the weather like today?"
example: |
What automations would you recommend for a smart kitchen?
Consider energy efficiency and convenience.
selector:
text:
multiline: true
type: text
model:
name: Model
description: Override the default model for this question (optional)
description: >-
Select an AI model to use (optional, overrides default setting).
Different models have different capabilities and token limits.
required: false
example: "gpt-4"
example: "gpt-3.5-turbo"
default: "gpt-3.5-turbo"
selector:
select:
options:
- "gpt-3.5-turbo"
- "gpt-4"
- "gpt-4-32k"
- label: "GPT-3.5 Turbo (Fast & Efficient)"
value: "gpt-3.5-turbo"
- label: "GPT-4 (Most Capable)"
value: "gpt-4"
- label: "GPT-4 32K (Extended Context)"
value: "gpt-4-32k"
mode: dropdown
temperature:
name: Temperature
description: Control randomness in the response (0.0-1.0, lower is more focused)
description: >-
Controls response creativity (0-2):
0.0-0.3: Focused, consistent responses
0.4-0.7: Balanced responses
0.8-2.0: More creative, varied responses
required: false
default: 0.7
selector:
number:
min: 0.0
max: 1.0
max: 2.0
step: 0.1
mode: slider
unit_of_measurement: ""
max_tokens:
name: Max Tokens
description: Maximum length of the response
description: >-
Maximum length of the response. Higher values allow longer responses but use more API tokens.
Recommended ranges:
- Short responses: 256-512
- Medium responses: 512-1024
- Long responses: 1024-4096
required: false
default: 1000
selector:
number:
min: 1
max: 4000
max: 4096
step: 1
mode: box
clear_history:
name: Clear History
description: Clear the stored question and response history
description: >-
Delete all stored questions and responses from the conversation history.
This action cannot be undone.
fields: {}
get_history:
name: Get History
description: Get the history of questions and responses
description: >-
Retrieve recent conversation history, including questions, responses, and timestamps.
Results are ordered from newest to oldest.
fields:
limit:
name: Limit
description: Maximum number of history items to return
description: >-
Number of most recent conversations to return.
Higher values return more history but may take longer to process.
required: false
default: 10
selector:
@@ -65,16 +96,28 @@ get_history:
min: 1
max: 100
step: 1
mode: box
set_system_prompt:
name: Set System Prompt
description: Set a system prompt that will be used for all future questions
description: >-
Configure the AI's behavior by setting a system prompt.
This affects how the AI interprets and responds to all future questions.
fields:
prompt:
name: System Prompt
description: The system prompt to set
description: >-
Instructions that define how the AI should behave and respond.
Be specific about the desired expertise, tone, and format of responses.
required: true
example: "You are a helpful assistant specializing in home automation"
example: |
You are a home automation expert assistant. Focus on:
1. Practical and efficient solutions
2. Energy-saving recommendations
3. Integration with popular smart home platforms
4. Security and privacy considerations
Provide detailed but concise responses with clear steps when applicable.
selector:
text:
multiline: true
type: text
+118 -14
View File
@@ -3,36 +3,140 @@
"step": {
"user": {
"title": "Set up HA text AI",
"description": "Set up your OpenAI integration",
"description": "Configure your OpenAI integration for smart home interactions. You'll need an OpenAI API key to proceed.",
"data": {
"api_key": "API Key",
"model": "Model",
"temperature": "Temperature",
"max_tokens": "Max Tokens",
"api_endpoint": "API Endpoint",
"request_interval": "Request Interval (seconds)"
"api_key": {
"name": "OpenAI API Key",
"description": "Your OpenAI API key from platform.openai.com"
},
"model": {
"name": "AI Model",
"description": "Select the AI model to use. GPT-3.5-Turbo is recommended for most uses."
},
"temperature": {
"name": "Temperature",
"description": "Controls response creativity (0-2). Lower values for focused responses, higher for creative ones."
},
"max_tokens": {
"name": "Max Tokens",
"description": "Maximum length of responses. Higher values allow longer responses but use more API tokens."
},
"api_endpoint": {
"name": "API Endpoint",
"description": "OpenAI API endpoint URL. Leave default unless using a custom endpoint."
},
"request_interval": {
"name": "Request Interval",
"description": "Minimum time between API requests in seconds. Increase if hitting rate limits."
}
}
}
},
"error": {
"auth": "API key is invalid.",
"cannot_connect": "Failed to connect to API.",
"unknown": "Unexpected error occurred."
"invalid_auth": "Invalid API key. Please check your OpenAI API key and try again.",
"cannot_connect": "Failed to connect to API. Please check your internet connection and API endpoint.",
"unknown": "Unexpected error occurred. Please check the logs for more details.",
"already_exists": "This API key is already configured in another integration.",
"invalid_model": "Selected model is not available. Please choose a different model.",
"rate_limit": "API rate limit exceeded. Please try again later or increase the request interval.",
"context_length": "Input too long for selected model. Try reducing max tokens or using a model with larger context.",
"api_error": "OpenAI API error. Please check the logs for details."
},
"abort": {
"already_configured": "Device is already configured"
"already_configured": "This OpenAI integration is already configured",
"auth_failed": "Authentication failed. Please verify your API key.",
"invalid_endpoint": "Invalid API endpoint URL provided"
}
},
"options": {
"step": {
"init": {
"title": "HA text AI Options",
"description": "Adjust your OpenAI integration settings. Changes will apply to future requests.",
"data": {
"temperature": "Temperature",
"max_tokens": "Max Tokens",
"request_interval": "Request Interval (seconds)"
"temperature": {
"name": "Temperature",
"description": "Controls response creativity (0-2). Lower values for focused responses, higher for creative ones."
},
"max_tokens": {
"name": "Max Tokens",
"description": "Maximum length of responses. Higher values allow longer responses but use more API tokens."
},
"request_interval": {
"name": "Request Interval",
"description": "Minimum time between API requests in seconds. Increase if hitting rate limits."
}
}
}
}
},
"entity": {
"sensor": {
"last_response": {
"name": "Last Response",
"state_attributes": {
"last_updated": {
"name": "Last Updated",
"description": "Time of the last AI response"
},
"question": {
"name": "Last Question",
"description": "Most recent question asked"
},
"response": {
"name": "AI Response",
"description": "Latest response from the AI"
},
"model": {
"name": "Current Model",
"description": "AI model currently in use"
},
"temperature": {
"name": "Temperature Setting",
"description": "Current temperature parameter"
},
"max_tokens": {
"name": "Max Tokens Setting",
"description": "Current maximum tokens limit"
},
"total_responses": {
"name": "Total Responses",
"description": "Number of responses since last reset"
},
"system_prompt": {
"name": "System Prompt",
"description": "Current system instructions for the AI"
},
"response_time": {
"name": "Response Time",
"description": "Time taken to generate last response"
}
}
}
}
},
"services": {
"ask_question": {
"name": "Ask Question",
"description": "Send a question to the AI model",
"fields": {
"question": {
"name": "Question",
"description": "Your question for the AI"
}
}
},
"clear_history": {
"name": "Clear History",
"description": "Clear conversation history"
},
"get_history": {
"name": "Get History",
"description": "Retrieve conversation history"
},
"set_system_prompt": {
"name": "Set System Prompt",
"description": "Set AI behavior instructions"
}
}
}
BIN
View File
Binary file not shown.
+4 -2
View File
@@ -2,6 +2,8 @@
"name": "HA text AI",
"render_readme": true,
"domains": ["sensor"],
"homeassistant": "2023.8.0",
"icon": "mdi:brain"
"homeassistant": "2024.11.0",
"icon": "mdi:brain",
"version": "1.0.3",
"documentation": "https://github.com/smkrv/ha-text-ai"
}
-8
View File
@@ -1,8 +0,0 @@
"""Common fixtures for tests."""
import pytest
from homeassistant.core import HomeAssistant
@pytest.fixture
def hass() -> HomeAssistant:
"""Return a Home Assistant instance for testing."""
return HomeAssistant()
-65
View File
@@ -1,65 +0,0 @@
"""Tests for the HA text AI integration."""
from unittest.mock import AsyncMock, patch
import pytest
from custom_components.ha_text_ai.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@pytest.fixture
def mock_setup_entry() -> AsyncMock:
"""Override async_setup_entry."""
with patch(
"custom_components.ha_text_ai.async_setup_entry",
return_value=True,
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_coordinator() -> AsyncMock:
"""Override coordinator."""
with patch(
"custom_components.ha_text_ai.coordinator.HATextAICoordinator",
return_value=AsyncMock(),
) as mock_coordinator:
yield mock_coordinator
async def test_async_setup(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
"""Test the initial setup."""
assert await async_setup_component(hass, DOMAIN, {
DOMAIN: {
"api_key": "test_key",
"model": "gpt-3.5-turbo",
"temperature": 0.7,
"max_tokens": 1000,
"api_endpoint": "https://api.openai.com/v1",
"request_interval": 1.0
}
})
await hass.async_block_till_done()
assert DOMAIN in hass.data
async def test_async_setup_entry(
hass: HomeAssistant,
mock_coordinator: AsyncMock
) -> None:
"""Test setup entry."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
"api_key": "test_key",
"model": "gpt-3.5-turbo",
"temperature": 0.7,
"max_tokens": 1000,
"api_endpoint": "https://api.openai.com/v1",
"request_interval": 1.0
},
)
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert len(mock_coordinator.mock_calls) == 1
assert DOMAIN in hass.data
assert entry.entry_id in hass.data[DOMAIN]