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
3 changes: 1 addition & 2 deletions homeassistant/components/ai_task/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,14 @@
)
from .entity import AITaskEntity
from .http import async_setup as async_setup_http
from .task import GenDataTask, GenDataTaskResult, PlayMediaWithId, async_generate_data
from .task import GenDataTask, GenDataTaskResult, async_generate_data

__all__ = [
"DOMAIN",
"AITaskEntity",
"AITaskEntityFeature",
"GenDataTask",
"GenDataTaskResult",
"PlayMediaWithId",
"async_generate_data",
"async_setup",
"async_setup_entry",
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/ai_task/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ async def _async_get_ai_task_chat_log(
user_llm_prompt=DEFAULT_SYSTEM_PROMPT,
)

chat_log.async_add_user_content(UserContent(task.instructions))
chat_log.async_add_user_content(
UserContent(task.instructions, attachments=task.attachments)
)

yield chat_log

Expand Down
33 changes: 12 additions & 21 deletions homeassistant/components/ai_task/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,18 @@

from __future__ import annotations

from dataclasses import dataclass, fields
from dataclasses import dataclass
from typing import Any

import voluptuous as vol

from homeassistant.components import media_source
from homeassistant.components import conversation, media_source
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError

from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature


@dataclass(slots=True)
class PlayMediaWithId(media_source.PlayMedia):
"""Play media with a media content ID."""

media_content_id: str
"""Media source ID to play."""

def __str__(self) -> str:
"""Return media source ID as a string."""
return f"<PlayMediaWithId {self.media_content_id}>"


async def async_generate_data(
hass: HomeAssistant,
*,
Expand All @@ -52,7 +40,7 @@ async def async_generate_data(
)

# Resolve attachments
resolved_attachments: list[PlayMediaWithId] | None = None
resolved_attachments: list[conversation.Attachment] | None = None

if attachments:
if AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features:
Expand All @@ -66,13 +54,16 @@ async def async_generate_data(
media = await media_source.async_resolve_media(
hass, attachment["media_content_id"], None
)
if media.path is None:
raise HomeAssistantError(
"Only local attachments are currently supported"
)
resolved_attachments.append(
PlayMediaWithId(
**{
field.name: getattr(media, field.name)
for field in fields(media)
},
conversation.Attachment(
media_content_id=attachment["media_content_id"],
url=media.url,
mime_type=media.mime_type,
path=media.path,
)
)

Expand All @@ -99,7 +90,7 @@ class GenDataTask:
structure: vol.Schema | None = None
"""Optional structure for the data to be generated."""

attachments: list[PlayMediaWithId] | None = None
attachments: list[conversation.Attachment] | None = None
"""List of attachments to go along the instructions."""

def __str__(self) -> str:
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/conversation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from .chat_log import (
AssistantContent,
AssistantContentDeltaDict,
Attachment,
ChatLog,
Content,
ConverseError,
Expand Down Expand Up @@ -66,6 +67,7 @@
"HOME_ASSISTANT_AGENT",
"AssistantContent",
"AssistantContentDeltaDict",
"Attachment",
"ChatLog",
"Content",
"ConversationEntity",
Expand Down
19 changes: 19 additions & 0 deletions homeassistant/components/conversation/chat_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from contextvars import ContextVar
from dataclasses import asdict, dataclass, field, replace
import logging
from pathlib import Path
from typing import Any, Literal, TypedDict

import voluptuous as vol
Expand Down Expand Up @@ -136,6 +137,24 @@ class UserContent:

role: Literal["user"] = field(init=False, default="user")
content: str
attachments: list[Attachment] | None = field(default=None)


@dataclass(frozen=True)
class Attachment:
"""Attachment for a chat message."""

media_content_id: str
"""Media content ID of the attachment."""

url: str
"""URL of the attachment."""

mime_type: str
"""MIME type of the attachment."""

path: Path
"""Path to the attachment on disk."""


@dataclass(frozen=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ async def _async_generate_data(
chat_log: conversation.ChatLog,
) -> ai_task.GenDataTaskResult:
"""Handle a generate data task."""
await self._async_handle_chat_log(chat_log, task.structure, task.attachments)
await self._async_handle_chat_log(chat_log, task.structure)

if not isinstance(chat_log.content[-1], conversation.AssistantContent):
LOGGER.error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import voluptuous as vol
from voluptuous_openapi import convert

from homeassistant.components import ai_task, conversation
from homeassistant.components import conversation
from homeassistant.config_entries import ConfigSubentry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
Expand Down Expand Up @@ -338,7 +338,6 @@ async def _async_handle_chat_log(
self,
chat_log: conversation.ChatLog,
structure: vol.Schema | None = None,
attachments: list[ai_task.PlayMediaWithId] | None = None,
) -> None:
"""Generate an answer for the chat log."""
options = self.subentry.data
Expand Down Expand Up @@ -442,15 +441,11 @@ async def _async_handle_chat_log(
user_message = chat_log.content[-1]
assert isinstance(user_message, conversation.UserContent)
chat_request: str | list[Part] = user_message.content
if attachments:
if any(a.path is None for a in attachments):
raise HomeAssistantError(
"Only local attachments are currently supported"
)
if user_message.attachments:
files = await async_prepare_files_for_prompt(
self.hass,
self._genai_client,
[a.path for a in attachments], # type: ignore[misc]
[a.path for a in user_message.attachments],
)
chat_request = [chat_request, *files]

Expand Down
46 changes: 22 additions & 24 deletions homeassistant/components/home_connect/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import (
Expand Down Expand Up @@ -626,39 +626,37 @@ def refreshed_too_often_recently(self, appliance_ha_id: str) -> bool:
"""Check if the appliance data hasn't been refreshed too often recently."""

now = self.hass.loop.time()
if len(self._execution_tracker[appliance_ha_id]) >= MAX_EXECUTIONS:
return True

execution_tracker = self._execution_tracker[appliance_ha_id]
initial_len = len(execution_tracker)

execution_tracker = self._execution_tracker[appliance_ha_id] = [
timestamp
for timestamp in self._execution_tracker[appliance_ha_id]
for timestamp in execution_tracker
if now - timestamp < MAX_EXECUTIONS_TIME_WINDOW
]

execution_tracker.append(now)

if len(execution_tracker) >= MAX_EXECUTIONS:
ir.async_create_issue(
self.hass,
DOMAIN,
f"home_connect_too_many_connected_paired_events_{appliance_ha_id}",
is_fixable=True,
is_persistent=True,
severity=ir.IssueSeverity.ERROR,
translation_key="home_connect_too_many_connected_paired_events",
data={
"entry_id": self.config_entry.entry_id,
"appliance_ha_id": appliance_ha_id,
},
translation_placeholders={
"appliance_name": self.data[appliance_ha_id].info.name,
"times": str(MAX_EXECUTIONS),
"time_window": str(MAX_EXECUTIONS_TIME_WINDOW // 60),
"home_connect_resource_url": "https://www.home-connect.com/global/help-support/error-codes#/Togglebox=15362315-13320636-1/",
"home_assistant_core_issue_url": "https://github.com/home-assistant/core/issues/147299",
},
)
if initial_len < MAX_EXECUTIONS:
_LOGGER.warning(
'Too many connected/paired events for appliance "%s" '
"(%s times in less than %s minutes), updates have been disabled "
"and they will be enabled again whenever the connection stabilizes. "
"Consider trying to unplug the appliance "
"for a while to perform a soft reset",
self.data[appliance_ha_id].info.name,
MAX_EXECUTIONS,
MAX_EXECUTIONS_TIME_WINDOW // 60,
)
return True
if initial_len >= MAX_EXECUTIONS:
_LOGGER.info(
'Connected/paired events from the appliance "%s" have stabilized,'
" updates have been re-enabled",
self.data[appliance_ha_id].info.name,
)

return False

Expand Down
11 changes: 0 additions & 11 deletions homeassistant/components/home_connect/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,6 @@
}
},
"issues": {
"home_connect_too_many_connected_paired_events": {
"title": "{appliance_name} sent too many connected or paired events",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::home_connect_too_many_connected_paired_events::title%]",
"description": "The appliance \"{appliance_name}\" has been reported as connected or paired {times} times in less than {time_window} minutes, so refreshes on connected or paired events has been disabled to avoid exceeding the API rate limit.\n\nPlease refer to the [Home Connect Wi-Fi requirements and recommendations]({home_connect_resource_url}). If everything seems right with your network configuration, restart the appliance.\n\nClick \"submit\" to re-enable the updates.\nIf the issue persists, please see the following issue in the [Home Assistant core repository]({home_assistant_core_issue_url})."
}
}
}
},
"deprecated_time_alarm_clock_in_automations_scripts": {
"title": "Deprecated alarm clock entity detected in some automations or scripts",
"fix_flow": {
Expand Down
32 changes: 2 additions & 30 deletions homeassistant/components/keymitt_ble/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,14 @@

from __future__ import annotations

from collections.abc import Generator
from contextlib import contextmanager

import bleak
from microbot import MicroBotApiClient

from homeassistant.components import bluetooth
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady


@contextmanager
def patch_unused_bleak_discover_import() -> Generator[None]:
"""Patch bleak.discover import in microbot. It is unused and was removed in bleak 1.0.0."""

def getattr_bleak(name: str) -> object:
if name == "discover":
return None
raise AttributeError

original_func = bleak.__dict__.get("__getattr__")
bleak.__dict__["__getattr__"] = getattr_bleak
try:
yield
finally:
if original_func is not None:
bleak.__dict__["__getattr__"] = original_func


with patch_unused_bleak_discover_import():
from microbot import MicroBotApiClient

from .coordinator import ( # noqa: E402
MicroBotConfigEntry,
MicroBotDataUpdateCoordinator,
)
from .coordinator import MicroBotConfigEntry, MicroBotDataUpdateCoordinator

PLATFORMS: list[str] = [Platform.SWITCH]

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/keymitt_ble/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@
"integration_type": "hub",
"iot_class": "assumed_state",
"loggers": ["keymitt_ble"],
"requirements": ["PyMicroBot==0.0.17"]
"requirements": ["PyMicroBot==0.0.23"]
}
5 changes: 4 additions & 1 deletion homeassistant/components/openai_conversation/ai_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ class OpenAITaskEntity(
):
"""OpenAI AI Task entity."""

_attr_supported_features = ai_task.AITaskEntityFeature.GENERATE_DATA
_attr_supported_features = (
ai_task.AITaskEntityFeature.GENERATE_DATA
| ai_task.AITaskEntityFeature.SUPPORT_ATTACHMENTS
)

async def _async_generate_data(
self,
Expand Down
Loading
Loading