Skip to content

Commit

Permalink
Merge pull request #204 from valentinfrlch/main
Browse files Browse the repository at this point in the history
Update image-calendar to 1.3.9
  • Loading branch information
valentinfrlch authored Feb 13, 2025
2 parents 9cd7b84 + 505313b commit 102de36
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 56 deletions.
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
</p>
<p align=center>
<img src=https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badg>
<img src=https://img.shields.io/badge/version-1.3.8-blue>
<img src=https://img.shields.io/badge/version-1.3.9-blue>
<a href="https://github.com/valentinfrlch/ha-llmvision/issues">
<img src="https://img.shields.io/maintenance/yes/2025.svg">
<img alt="Issues" src="https://img.shields.io/github/issues/valentinfrlch/ha-llmvision?color=0088ff"/>
Expand Down Expand Up @@ -33,14 +33,11 @@
<br>
<p align="center">
<strong>LLM Vision</strong> is a Home Assistant integration that can analyze images, videos,
live camera feeds and frigate events using the vision capabilities of multimodal LLMs.
Supported providers are OpenAI, Anthropic, Google Gemini, AWS Bedrock, Groq,
<a href="https://github.com/mudler/LocalAI">LocalAI</a>,
<a href="https://ollama.com/">Ollama</a> and any OpenAI compatible API.
live camera feeds and frigate events using the vision capabilities of multimodal LLMs.
</p>

## Features
- Compatible with OpenAI, Anthropic Claude, Google Gemini, AWS Bedrock, Groq, [LocalAI](https://github.com/mudler/LocalAI), [Ollama](https://ollama.com/) and custom OpenAI compatible APIs
- Compatible with OpenAI, Anthropic Claude, Google Gemini, AWS Bedrock, Groq, [LocalAI](https://github.com/mudler/LocalAI), [Ollama](https://ollama.com/), [Open WebUI](https://github.com/open-webui/open-webui) and providers with OpenAI compatible enpoints.
- Analyzes images and video files, live camera feeds and Frigate events
- Remembers Frigate events and camera motion events so you can ask about them later
- Seamlessly updates sensors based on image input
Expand Down
15 changes: 15 additions & 0 deletions custom_components/llmvision/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
CONF_AWS_SECRET_ACCESS_KEY,
CONF_AWS_REGION_NAME,
CONF_AWS_DEFAULT_MODEL,
CONF_OPENWEBUI_IP_ADDRESS,
CONF_OPENWEBUI_PORT,
CONF_OPENWEBUI_HTTPS,
CONF_OPENWEBUI_API_KEY,
CONF_OPENWEBUI_DEFAULT_MODEL,
MESSAGE,
REMEMBER,
MODEL,
Expand Down Expand Up @@ -88,6 +93,11 @@ async def async_setup_entry(hass, entry):
aws_secret_access_key = entry.data.get(CONF_AWS_SECRET_ACCESS_KEY)
aws_region_name = entry.data.get(CONF_AWS_REGION_NAME)
aws_default_model = entry.data.get(CONF_AWS_DEFAULT_MODEL)
openwebui_ip_address = entry.data.get(CONF_OPENWEBUI_IP_ADDRESS)
openwebui_port = entry.data.get(CONF_OPENWEBUI_PORT)
openwebui_https = entry.data.get(CONF_OPENWEBUI_HTTPS)
openwebui_api_key = entry.data.get(CONF_OPENWEBUI_API_KEY)
openwebui_default_model = entry.data.get(CONF_OPENWEBUI_DEFAULT_MODEL)

# Ensure DOMAIN exists in hass.data
if DOMAIN not in hass.data:
Expand Down Expand Up @@ -117,6 +127,11 @@ async def async_setup_entry(hass, entry):
CONF_AWS_SECRET_ACCESS_KEY: aws_secret_access_key,
CONF_AWS_REGION_NAME: aws_region_name,
CONF_AWS_DEFAULT_MODEL: aws_default_model,
CONF_OPENWEBUI_IP_ADDRESS: openwebui_ip_address,
CONF_OPENWEBUI_PORT: openwebui_port,
CONF_OPENWEBUI_HTTPS: openwebui_https,
CONF_OPENWEBUI_API_KEY: openwebui_api_key,
CONF_OPENWEBUI_DEFAULT_MODEL: openwebui_default_model
}

# Filter out None values
Expand Down
103 changes: 83 additions & 20 deletions custom_components/llmvision/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@
CONF_AWS_SECRET_ACCESS_KEY,
CONF_AWS_REGION_NAME,
CONF_AWS_DEFAULT_MODEL,
CONF_OPENWEBUI_IP_ADDRESS,
CONF_OPENWEBUI_PORT,
CONF_OPENWEBUI_HTTPS,
CONF_OPENWEBUI_API_KEY,
CONF_OPENWEBUI_DEFAULT_MODEL,
ENDPOINT_OPENWEBUI,

)
import voluptuous as vol
import logging
Expand All @@ -59,6 +66,7 @@ async def handle_provider(self, provider):
"LocalAI": self.async_step_localai,
"Ollama": self.async_step_ollama,
"OpenAI": self.async_step_openai,
"OpenWebUI": self.async_step_openwebui,
}

step_method = provider_steps.get(provider)
Expand All @@ -72,7 +80,8 @@ async def async_step_user(self, user_input=None):
data_schema = vol.Schema({
vol.Required("provider", default="Event Calendar"): selector({
"select": {
"options": ["Anthropic", "AWS Bedrock", "Google", "Groq", "LocalAI", "Ollama", "OpenAI", "Custom OpenAI", "Event Calendar"], # Azure removed until fixed
# Azure removed until fixed
"options": ["Anthropic", "AWS Bedrock", "Google", "Groq", "LocalAI", "Ollama", "OpenAI", "OpenWebUI", "Custom OpenAI", "Event Calendar"],
"mode": "dropdown",
"sort": False,
"custom_value": False
Expand Down Expand Up @@ -101,7 +110,7 @@ async def async_step_localai(self, user_input=None):
if self.source == config_entries.SOURCE_RECONFIGURE:
# load existing configuration and add it to the dialog
self.init_info = self._get_reconfigure_entry().data
data_schema=self.add_suggested_values_to_schema(
data_schema = self.add_suggested_values_to_schema(
data_schema, self.init_info
)

Expand Down Expand Up @@ -148,7 +157,7 @@ async def async_step_ollama(self, user_input=None):
if self.source == config_entries.SOURCE_RECONFIGURE:
# load existing configuration and add it to the dialog
self.init_info = self._get_reconfigure_entry().data
data_schema=self.add_suggested_values_to_schema(
data_schema = self.add_suggested_values_to_schema(
data_schema, self.init_info
)

Expand Down Expand Up @@ -185,6 +194,59 @@ async def async_step_ollama(self, user_input=None):
data_schema=data_schema,
)

async def async_step_openwebui(self, user_input=None):
data_schema = vol.Schema({
vol.Required(CONF_OPENWEBUI_API_KEY): str,
vol.Required(CONF_OPENWEBUI_DEFAULT_MODEL, default="minicpm-v"): str,
vol.Required(CONF_OPENWEBUI_IP_ADDRESS): str,
vol.Required(CONF_OPENWEBUI_PORT, default=3000): int,
vol.Required(CONF_OPENWEBUI_HTTPS, default=False): bool,
})

if self.source == config_entries.SOURCE_RECONFIGURE:
# load existing configuration and add it to the dialog
self.init_info = self._get_reconfigure_entry().data
data_schema = self.add_suggested_values_to_schema(
data_schema, self.init_info
)

if user_input is not None:
# save provider to user_input
user_input["provider"] = self.init_info["provider"]
try:
endpoint = ENDPOINT_OPENWEBUI.format(
ip_address=user_input[CONF_OPENWEBUI_IP_ADDRESS],
port=user_input[CONF_OPENWEBUI_PORT],
protocol="https" if user_input[CONF_OPENWEBUI_HTTPS] else "http"
)
openwebui = OpenAI(hass=self.hass,
api_key=user_input[CONF_OPENWEBUI_API_KEY],
default_model=user_input[CONF_OPENWEBUI_DEFAULT_MODEL],
endpoint={'base_url': endpoint})
await openwebui.validate()
# add the mode to user_input
if self.source == config_entries.SOURCE_RECONFIGURE:
# we're reconfiguring an existing config
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
data_updates=user_input,
)
else:
# New config entry
return self.async_create_entry(title=f"OpenWebUI ({user_input[CONF_OPENWEBUI_IP_ADDRESS]})", data=user_input)
except ServiceValidationError as e:
_LOGGER.error(f"Validation failed: {e}")
return self.async_show_form(
step_id="openwebui",
data_schema=data_schema,
errors={"base": "handshake_failed"}
)

return self.async_show_form(
step_id="openwebui",
data_schema=data_schema,
)

async def async_step_openai(self, user_input=None):
data_schema = vol.Schema({
vol.Required(CONF_OPENAI_API_KEY): str,
Expand All @@ -193,7 +255,7 @@ async def async_step_openai(self, user_input=None):
if self.source == config_entries.SOURCE_RECONFIGURE:
# load existing configuration and add it to the dialog
self.init_info = self._get_reconfigure_entry().data
data_schema=self.add_suggested_values_to_schema(
data_schema = self.add_suggested_values_to_schema(
data_schema, self.init_info
)

Expand Down Expand Up @@ -239,7 +301,7 @@ async def async_step_azure(self, user_input=None):
if self.source == config_entries.SOURCE_RECONFIGURE:
# load existing configuration and add it to the dialog
self.init_info = self._get_reconfigure_entry().data
data_schema=self.add_suggested_values_to_schema(
data_schema = self.add_suggested_values_to_schema(
data_schema, self.init_info
)

Expand Down Expand Up @@ -286,7 +348,7 @@ async def async_step_anthropic(self, user_input=None):
if self.source == config_entries.SOURCE_RECONFIGURE:
# load existing configuration and add it to the dialog
self.init_info = self._get_reconfigure_entry().data
data_schema=self.add_suggested_values_to_schema(
data_schema = self.add_suggested_values_to_schema(
data_schema, self.init_info
)

Expand Down Expand Up @@ -329,7 +391,7 @@ async def async_step_google(self, user_input=None):
if self.source == config_entries.SOURCE_RECONFIGURE:
# load existing configuration and add it to the dialog
self.init_info = self._get_reconfigure_entry().data
data_schema=self.add_suggested_values_to_schema(
data_schema = self.add_suggested_values_to_schema(
data_schema, self.init_info
)

Expand Down Expand Up @@ -372,7 +434,7 @@ async def async_step_groq(self, user_input=None):
if self.source == config_entries.SOURCE_RECONFIGURE:
# load existing configuration and add it to the dialog
self.init_info = self._get_reconfigure_entry().data
data_schema=self.add_suggested_values_to_schema(
data_schema = self.add_suggested_values_to_schema(
data_schema, self.init_info
)

Expand Down Expand Up @@ -416,7 +478,7 @@ async def async_step_custom_openai(self, user_input=None):
if self.source == config_entries.SOURCE_RECONFIGURE:
# load existing configuration and add it to the dialog
self.init_info = self._get_reconfigure_entry().data
data_schema=self.add_suggested_values_to_schema(
data_schema = self.add_suggested_values_to_schema(
data_schema, self.init_info
)

Expand All @@ -425,10 +487,11 @@ async def async_step_custom_openai(self, user_input=None):
user_input["provider"] = self.init_info["provider"]
try:
custom_openai = OpenAI(self.hass,
api_key=user_input[CONF_CUSTOM_OPENAI_API_KEY],
endpoint={'base_url': user_input[CONF_CUSTOM_OPENAI_ENDPOINT]},
default_model=user_input[CONF_CUSTOM_OPENAI_DEFAULT_MODEL],
)
api_key=user_input[CONF_CUSTOM_OPENAI_API_KEY],
endpoint={
'base_url': user_input[CONF_CUSTOM_OPENAI_ENDPOINT]},
default_model=user_input[CONF_CUSTOM_OPENAI_DEFAULT_MODEL],
)
await custom_openai.validate()
# add the mode to user_input
user_input["provider"] = self.init_info["provider"]
Expand Down Expand Up @@ -462,7 +525,7 @@ async def async_step_semantic_index(self, user_input=None):
if self.source == config_entries.SOURCE_RECONFIGURE:
# load existing configuration and add it to the dialog
self.init_info = self._get_reconfigure_entry().data
data_schema=self.add_suggested_values_to_schema(
data_schema = self.add_suggested_values_to_schema(
data_schema, self.init_info
)

Expand Down Expand Up @@ -502,7 +565,7 @@ async def async_step_aws_bedrock(self, user_input=None):
if self.source == config_entries.SOURCE_RECONFIGURE:
# load existing configuration and add it to the dialog
self.init_info = self._get_reconfigure_entry().data
data_schema=self.add_suggested_values_to_schema(
data_schema = self.add_suggested_values_to_schema(
data_schema, self.init_info
)

Expand All @@ -511,11 +574,11 @@ async def async_step_aws_bedrock(self, user_input=None):
user_input["provider"] = self.init_info["provider"]
try:
aws_bedrock = AWSBedrock(self.hass,
aws_access_key_id=user_input[CONF_AWS_ACCESS_KEY_ID],
aws_secret_access_key=user_input[CONF_AWS_SECRET_ACCESS_KEY],
aws_region_name=user_input[CONF_AWS_REGION_NAME],
model=user_input[CONF_AWS_DEFAULT_MODEL],
)
aws_access_key_id=user_input[CONF_AWS_ACCESS_KEY_ID],
aws_secret_access_key=user_input[CONF_AWS_SECRET_ACCESS_KEY],
aws_region_name=user_input[CONF_AWS_REGION_NAME],
model=user_input[CONF_AWS_DEFAULT_MODEL],
)
await aws_bedrock.validate()
# add the mode to user_input
user_input["provider"] = self.init_info["provider"]
Expand Down
6 changes: 6 additions & 0 deletions custom_components/llmvision/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
CONF_AWS_SECRET_ACCESS_KEY = 'aws_secret_access_key'
CONF_AWS_REGION_NAME = 'aws_region_name'
CONF_AWS_DEFAULT_MODEL = 'aws_default_model'
CONF_OPENWEBUI_IP_ADDRESS = 'openwebui_ip'
CONF_OPENWEBUI_PORT = 'openwebui_port'
CONF_OPENWEBUI_HTTPS = 'openwebui_https'
CONF_OPENWEBUI_API_KEY = 'openwebui_api_key'
CONF_OPENWEBUI_DEFAULT_MODEL = 'openwebui_default_model'

# service call constants
MESSAGE = 'message'
Expand Down Expand Up @@ -67,4 +72,5 @@
ENDPOINT_GROQ = "https://api.groq.com/openai/v1/chat/completions"
ENDPOINT_LOCALAI = "{protocol}://{ip_address}:{port}/v1/chat/completions"
ENDPOINT_OLLAMA = "{protocol}://{ip_address}:{port}/api/chat"
ENDPOINT_OPENWEBUI = "{protocol}://{ip_address}:{port}/api/chat/completions"
ENDPOINT_AZURE = "{base_url}openai/deployments/{deployment}/chat/completions?api-version={api_version}"
2 changes: 1 addition & 1 deletion custom_components/llmvision/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
"issue_tracker": "https://github.com/valentinfrlch/ha-llmvision/issues",
"requirements": ["boto3"],
"version": "1.4.0"
}
}
Loading

0 comments on commit 102de36

Please sign in to comment.