Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,7 @@ homeassistant.components.timer.*
homeassistant.components.tod.*
homeassistant.components.todo.*
homeassistant.components.tolo.*
homeassistant.components.tonewinner.*
homeassistant.components.tplink.*
homeassistant.components.tplink_omada.*
homeassistant.components.trace.*
Expand Down
4 changes: 3 additions & 1 deletion CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions homeassistant/components/anthropic/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -933,9 +933,9 @@ async def _get_model_args( # noqa: C901
options: dict[str, Any] = DEFAULT | self.subentry.data

preloaded_tools = [
"HassTurnOn",
"HassTurnOff",
"GetLiveContext",
"intent__HassTurnOn",
"intent__HassTurnOff",
"homeassistant__GetLiveContext",
"code_execution",
"web_search",
"web_fetch",
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/assist_satellite/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from homeassistant.helpers import intent
from homeassistant.helpers.llm import LLM_API_ASSIST, IntentTool, LLMContext, Tool

from .const import DOMAIN


@callback
def async_get_tools(
Expand All @@ -17,7 +19,7 @@ def async_get_tools(
# assist_satellite registers the broadcast intent when it is set up, and
# this platform is only queried once that has happened.
tools: list[Tool] = [
IntentTool(handler.intent_type, handler)
IntentTool(f"{DOMAIN}__{handler.intent_type}", handler)
for handler in intent.async_get(hass)
if handler.intent_type == intent.INTENT_BROADCAST
]
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/calendar/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
class CalendarGetEventsTool(Tool):
"""LLM Tool allowing querying a calendar."""

name = "calendar_get_events"
name = "calendar__get_events"
description = (
"Get events from a calendar. "
"When asked if something happens, search the whole week. "
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/climate/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def async_get_tools(
return None

tools: list[Tool] = [
IntentTool(handler.intent_type, handler)
IntentTool(f"{DOMAIN}__{handler.intent_type}", handler)
for handler in intent.async_get(hass)
if handler.intent_type in LLM_INTENTS
]
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/fan/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def async_get_tools(
return None

tools: list[Tool] = [
IntentTool(handler.intent_type, handler)
IntentTool(f"{DOMAIN}__{handler.intent_type}", handler)
for handler in intent.async_get(hass)
if handler.intent_type in LLM_INTENTS
]
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/google/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ def _get_entity_descriptions(
event_type=EventTypeEnum.BIRTHDAY,
name=None,
entity_id=None,
ignore_availability=True,
)
)
# Create an optional disabled by default entity for Work Location
Expand All @@ -191,6 +192,7 @@ def _get_entity_descriptions(
name=None,
entity_id=None,
entity_registry_enabled_default=False,
ignore_availability=True,
)
)
return entity_descriptions
Expand Down
12 changes: 8 additions & 4 deletions homeassistant/components/google_health/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
from google_health_api.const import HealthApiScope
from google_health_api.exceptions import (
GoogleHealthApiError,
HealthApiForbiddenException,
HealthApiScopeInsufficientException,
HealthApiServiceDisabledException,
)

from homeassistant.config_entries import (
Expand Down Expand Up @@ -81,19 +82,22 @@ async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResu

try:
identity = await api.get_identity()
except HealthApiForbiddenException as err:
except HealthApiServiceDisabledException as err:
_LOGGER.error("Error getting Google Health identity: %s", err)
return self.async_abort(
reason="api_not_enabled",
description_placeholders={"url": API_CONSOLE_URL},
)
except HealthApiScopeInsufficientException as err:
_LOGGER.error("Error getting Google Health identity: %s", err)
return self.async_abort(reason="missing_profile_scope")
except GoogleHealthApiError as err:
_LOGGER.error("Error getting Google Health identity: %s", err)
return self.async_abort(reason="cannot_connect")

if not identity.health_user_id:
_LOGGER.error("Google Health identity has no health_user_id")
return self.async_abort(reason="cannot_connect")
return self.async_abort(reason="missing_profile_scope")

await self.async_set_unique_id(identity.health_user_id)
if self.source in (SOURCE_REAUTH, SOURCE_RECONFIGURE):
Expand All @@ -110,7 +114,7 @@ async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResu
try:
userinfo = await api.get_user_info()
display_name = userinfo.given_name or userinfo.name
except Exception as err: # pylint: disable=broad-except # noqa: BLE001
except GoogleHealthApiError as err:
_LOGGER.warning("Error fetching user profile name: %s", err)

return self.async_create_entry(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/google_health/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"cannot_connect": "Failed to connect.",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"missing_profile_scope": "Missing required Google Health profile read permission.",
"missing_profile_scope": "Missing required Google Health profile read permission. Please try again and select the right permission.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
Expand Down
6 changes: 3 additions & 3 deletions homeassistant/components/homeassistant/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
DYNAMIC_CONTEXT_PROMPT = (
"You ARE equipped to answer questions about the"
" current state of\n"
"the home using the `GetLiveContext` tool."
"the home using the `homeassistant__GetLiveContext` tool."
" This is a primary function."
" Do not state you lack the\n"
"functionality if the question requires live data.\n"
Expand All @@ -50,7 +50,7 @@
' "What mode is the thermostat in?",'
' "What is the temperature outside?"):\n'
" 1. Recognize this requires live data.\n"
" 2. You MUST call `GetLiveContext`."
" 2. You MUST call `homeassistant__GetLiveContext`."
" This tool will provide the needed real-time"
" information (like temperature from the local"
" weather, lock status, etc.).\n"
Expand Down Expand Up @@ -198,7 +198,7 @@ class GetLiveContextTool(Tool):
returns state for entities based on intent parameters.
"""

name = "GetLiveContext"
name = "homeassistant__GetLiveContext"
description = (
"Provides real-time information about the"
" CURRENT state, value, or mode of devices,"
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/humidifier/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def async_get_tools(
return None

tools: list[Tool] = [
IntentTool(handler.intent_type, handler)
IntentTool(f"{DOMAIN}__{handler.intent_type}", handler)
for handler in intent.async_get(hass)
if handler.intent_type in LLM_INTENTS
]
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/iammeter/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"domain": "iammeter",
"name": "IamMeter",
"codeowners": ["@lewei50"],
"codeowners": ["@lewei50", "@IAMMETER", "@miwyf"],
"documentation": "https://www.home-assistant.io/integrations/iammeter",
"iot_class": "local_polling",
"loggers": ["iammeter"],
Expand Down
5 changes: 3 additions & 2 deletions homeassistant/components/intent/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
)
from homeassistant.helpers.llm import LLM_API_ASSIST, IntentTool, LLMContext, Tool

from .const import DOMAIN
from .timers import async_device_supports_timers

# Generic intents exposed as LLM tools regardless of a timer-capable device.
Expand All @@ -40,7 +41,7 @@

DEVICE_CONTROL_TOOL_USAGE_PROMPT = (
"When controlling Home Assistant always call the intent tools. "
"Use HassTurnOn to lock and HassTurnOff to unlock a lock. "
"Use intent__HassTurnOn to lock and intent__HassTurnOff to unlock a lock. "
"When controlling a device, prefer passing just name and domain. "
"When controlling an area, prefer passing just area name and domain."
)
Expand Down Expand Up @@ -75,7 +76,7 @@ def async_get_tools(
]

tools: list[Tool] = [
IntentTool(handler.intent_type, handler) for handler in handlers
IntentTool(f"{DOMAIN}__{handler.intent_type}", handler) for handler in handlers
]
if not tools:
return None
Expand Down
5 changes: 3 additions & 2 deletions homeassistant/components/intent_script/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from homeassistant.helpers import intent
from homeassistant.helpers.llm import LLM_API_ASSIST, IntentTool, LLMContext, Tool

from . import ScriptIntentHandler
from . import DOMAIN, ScriptIntentHandler


@callback
Expand Down Expand Up @@ -43,7 +43,8 @@ def async_get_tools(
# valid tool names.
tools: list[Tool] = [
IntentTool(
unicode_slug.slugify(handler.intent_type, separator="_", lowercase=False),
f"{DOMAIN}__"
+ unicode_slug.slugify(handler.intent_type, separator="_", lowercase=False),
handler,
)
for handler in handlers
Expand Down
25 changes: 20 additions & 5 deletions homeassistant/components/knx/expose.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,18 @@ def async_register(self) -> None:

@callback
def _init_expose_state(self) -> None:
"""Initialize state of all exposures."""
init_state = self.hass.states.get(self.entity_id)
"""Initialize state of all exposures from the current HA state."""
state = self.hass.states.get(self.entity_id)
for option, xknx_expose in self._exposures:
state_value = self._get_expose_value(init_state, option)
expose_value = self._get_expose_value(state, option)
if expose_value is None:
continue
try:
xknx_expose.sensor_value.value = state_value
xknx_expose.initialize_value(expose_value)
except ConversionError:
_LOGGER.exception(
"Error setting value %s for expose sensor %s",
state_value,
expose_value,
xknx_expose.name,
)

Expand Down Expand Up @@ -280,11 +282,24 @@ def _get_expose_value(
async def _async_entity_changed(self, event: Event[EventStateChangedData]) -> None:
"""Handle entity change for all options."""
new_state = event.data["new_state"]

async with TaskGroup() as tg:
for option, xknx_expose in self._exposures:
expose_value = self._get_expose_value(new_state, option)
if expose_value is None:
continue

if xknx_expose.sensor_value.value is None:
try:
xknx_expose.initialize_value(expose_value)
except ConversionError:
_LOGGER.exception(
"Error setting value %s for expose sensor %s",
expose_value,
xknx_expose.name,
)
continue

tg.create_task(self._async_set_knx_value(xknx_expose, expose_value))

async def _async_set_knx_value(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/lawn_mower/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def async_get_tools(
return None

tools: list[Tool] = [
IntentTool(handler.intent_type, handler)
IntentTool(f"{DOMAIN}__{handler.intent_type}", handler)
for handler in intent.async_get(hass)
if handler.intent_type in LLM_INTENTS
]
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/light/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def async_get_tools(
return None

tools: list[Tool] = [
IntentTool(handler.intent_type, handler)
IntentTool(f"{DOMAIN}__{handler.intent_type}", handler)
for handler in intent.async_get(hass)
if handler.intent_type in LLM_INTENTS
]
Expand Down
9 changes: 5 additions & 4 deletions homeassistant/components/llama_cpp/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, llm
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.json import json_dumps

from .api import api_error_handler
from .const import (
Expand Down Expand Up @@ -102,7 +103,7 @@ def _convert_content_to_chat_message(
return ChatCompletionToolMessageParam(
role="tool",
tool_call_id=content.tool_call_id,
content=json.dumps(content.tool_result),
content=json_dumps(content.tool_result),
)

role: Literal["user", "assistant", "system"] = content.role
Expand All @@ -123,7 +124,7 @@ def _convert_content_to_chat_message(
type="function",
id=tool_call.id,
function=Function(
arguments=json.dumps(tool_call.tool_args),
arguments=json_dumps(tool_call.tool_args),
name=tool_call.tool_name,
),
)
Expand Down Expand Up @@ -175,7 +176,7 @@ def _convert_content_to_param(
return ChatCompletionToolMessageParam(
role="tool",
tool_call_id=content.tool_call_id,
content=json.dumps(content.tool_result),
content=json_dumps(content.tool_result),
)
if not isinstance(content, conversation.AssistantContent) or not content.tool_calls:
if isinstance(content, conversation.SystemContent):
Expand All @@ -195,7 +196,7 @@ def _convert_content_to_param(
ChatCompletionMessageToolCallParam(
id=tool_call.id,
function=Function(
arguments=json.dumps(tool_call.tool_args),
arguments=json_dumps(tool_call.tool_args),
name=tool_call.tool_name,
),
type="function",
Expand Down
Loading
Loading