mirror of
https://github.com/home-assistant/core.git
synced 2025-07-31 01:07:10 +00:00
Merge branch 'dev' into ai-task-structured-data
This commit is contained in:
commit
e42038742a
@ -16,6 +16,7 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import (
|
||||||
SOURCE_REAUTH,
|
SOURCE_REAUTH,
|
||||||
|
SOURCE_RECONFIGURE,
|
||||||
ConfigFlow,
|
ConfigFlow,
|
||||||
ConfigFlowResult,
|
ConfigFlowResult,
|
||||||
OptionsFlow,
|
OptionsFlow,
|
||||||
@ -40,12 +41,6 @@ APPS_NEW_ID = "NewApp"
|
|||||||
CONF_APP_DELETE = "app_delete"
|
CONF_APP_DELETE = "app_delete"
|
||||||
CONF_APP_ID = "app_id"
|
CONF_APP_ID = "app_id"
|
||||||
|
|
||||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required("host"): str,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
STEP_PAIR_DATA_SCHEMA = vol.Schema(
|
STEP_PAIR_DATA_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required("pin"): str,
|
vol.Required("pin"): str,
|
||||||
@ -66,7 +61,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle the initial step."""
|
"""Handle the initial and reconfigure step."""
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
self.host = user_input[CONF_HOST]
|
self.host = user_input[CONF_HOST]
|
||||||
@ -75,15 +70,32 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
await api.async_generate_cert_if_missing()
|
await api.async_generate_cert_if_missing()
|
||||||
self.name, self.mac = await api.async_get_name_and_mac()
|
self.name, self.mac = await api.async_get_name_and_mac()
|
||||||
await self.async_set_unique_id(format_mac(self.mac))
|
await self.async_set_unique_id(format_mac(self.mac))
|
||||||
|
if self.source == SOURCE_RECONFIGURE:
|
||||||
|
self._abort_if_unique_id_mismatch()
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
self._get_reconfigure_entry(),
|
||||||
|
data={
|
||||||
|
CONF_HOST: self.host,
|
||||||
|
CONF_NAME: self.name,
|
||||||
|
CONF_MAC: self.mac,
|
||||||
|
},
|
||||||
|
)
|
||||||
self._abort_if_unique_id_configured(updates={CONF_HOST: self.host})
|
self._abort_if_unique_id_configured(updates={CONF_HOST: self.host})
|
||||||
return await self._async_start_pair()
|
return await self._async_start_pair()
|
||||||
except (CannotConnect, ConnectionClosed):
|
except (CannotConnect, ConnectionClosed):
|
||||||
# Likely invalid IP address or device is network unreachable. Stay
|
# Likely invalid IP address or device is network unreachable. Stay
|
||||||
# in the user step allowing the user to enter a different host.
|
# in the user step allowing the user to enter a different host.
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
|
else:
|
||||||
|
user_input = {}
|
||||||
|
default_host = user_input.get(CONF_HOST, vol.UNDEFINED)
|
||||||
|
if self.source == SOURCE_RECONFIGURE:
|
||||||
|
default_host = self._get_reconfigure_entry().data[CONF_HOST]
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="reconfigure" if self.source == SOURCE_RECONFIGURE else "user",
|
||||||
data_schema=STEP_USER_DATA_SCHEMA,
|
data_schema=vol.Schema(
|
||||||
|
{vol.Required(CONF_HOST, default=default_host): str}
|
||||||
|
),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -216,6 +228,12 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_reconfigure(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle reconfiguration."""
|
||||||
|
return await self.async_step_user(user_input)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(
|
||||||
|
@ -11,6 +11,15 @@
|
|||||||
"host": "The hostname or IP address of the Android TV device."
|
"host": "The hostname or IP address of the Android TV device."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"reconfigure": {
|
||||||
|
"description": "Update the IP address of this previously configured Android TV device.",
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"host": "The hostname or IP address of the Android TV device."
|
||||||
|
}
|
||||||
|
},
|
||||||
"zeroconf_confirm": {
|
"zeroconf_confirm": {
|
||||||
"title": "Discovered Android TV",
|
"title": "Discovered Android TV",
|
||||||
"description": "Do you want to add the Android TV ({name}) to Home Assistant? It will turn on and a pairing code will be displayed on it that you will need to enter in the next screen."
|
"description": "Do you want to add the Android TV ({name}) to Home Assistant? It will turn on and a pairing code will be displayed on it that you will need to enter in the next screen."
|
||||||
@ -38,7 +47,9 @@
|
|||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
|
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||||
|
"unique_id_mismatch": "Please ensure you reconfigure against the same device."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
@ -63,6 +63,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) ->
|
|||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
coordinator.async_cancel_token_refresh()
|
coordinator.async_cancel_token_refresh()
|
||||||
coordinator.async_cancel_firmware_refresh()
|
coordinator.async_cancel_firmware_refresh()
|
||||||
|
coordinator.async_cancel_mac_verification()
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pyenphase"],
|
"loggers": ["pyenphase"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pyenphase==2.1.0"],
|
"requirements": ["pyenphase==2.2.0"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"type": "_enphase-envoy._tcp.local."
|
"type": "_enphase-envoy._tcp.local."
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from functools import partial
|
||||||
import mimetypes
|
import mimetypes
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
@ -37,11 +38,13 @@ from homeassistant.helpers.typing import ConfigType
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_PROMPT,
|
CONF_PROMPT,
|
||||||
|
DEFAULT_AI_TASK_NAME,
|
||||||
DEFAULT_TITLE,
|
DEFAULT_TITLE,
|
||||||
DEFAULT_TTS_NAME,
|
DEFAULT_TTS_NAME,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
FILE_POLLING_INTERVAL_SECONDS,
|
FILE_POLLING_INTERVAL_SECONDS,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
|
RECOMMENDED_AI_TASK_OPTIONS,
|
||||||
RECOMMENDED_CHAT_MODEL,
|
RECOMMENDED_CHAT_MODEL,
|
||||||
RECOMMENDED_TTS_OPTIONS,
|
RECOMMENDED_TTS_OPTIONS,
|
||||||
TIMEOUT_MILLIS,
|
TIMEOUT_MILLIS,
|
||||||
@ -53,6 +56,7 @@ CONF_FILENAMES = "filenames"
|
|||||||
|
|
||||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
PLATFORMS = (
|
PLATFORMS = (
|
||||||
|
Platform.AI_TASK,
|
||||||
Platform.CONVERSATION,
|
Platform.CONVERSATION,
|
||||||
Platform.TTS,
|
Platform.TTS,
|
||||||
)
|
)
|
||||||
@ -187,11 +191,9 @@ async def async_setup_entry(
|
|||||||
"""Set up Google Generative AI Conversation from a config entry."""
|
"""Set up Google Generative AI Conversation from a config entry."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
client = await hass.async_add_executor_job(
|
||||||
def _init_client() -> Client:
|
partial(Client, api_key=entry.data[CONF_API_KEY])
|
||||||
return Client(api_key=entry.data[CONF_API_KEY])
|
)
|
||||||
|
|
||||||
client = await hass.async_add_executor_job(_init_client)
|
|
||||||
await client.aio.models.get(
|
await client.aio.models.get(
|
||||||
model=RECOMMENDED_CHAT_MODEL,
|
model=RECOMMENDED_CHAT_MODEL,
|
||||||
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
|
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
|
||||||
@ -350,6 +352,19 @@ async def async_migrate_entry(
|
|||||||
|
|
||||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||||
|
|
||||||
|
if entry.version == 2 and entry.minor_version == 2:
|
||||||
|
# Add AI Task subentry with default options
|
||||||
|
hass.config_entries.async_add_subentry(
|
||||||
|
entry,
|
||||||
|
ConfigSubentry(
|
||||||
|
data=MappingProxyType(RECOMMENDED_AI_TASK_OPTIONS),
|
||||||
|
subentry_type="ai_task_data",
|
||||||
|
title=DEFAULT_AI_TASK_NAME,
|
||||||
|
unique_id=None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
hass.config_entries.async_update_entry(entry, minor_version=3)
|
||||||
|
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
"""AI Task integration for Google Generative AI Conversation."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.components import ai_task, conversation
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .const import LOGGER
|
||||||
|
from .entity import ERROR_GETTING_RESPONSE, GoogleGenerativeAILLMBaseEntity
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up AI Task entities."""
|
||||||
|
for subentry in config_entry.subentries.values():
|
||||||
|
if subentry.subentry_type != "ai_task_data":
|
||||||
|
continue
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
[GoogleGenerativeAITaskEntity(config_entry, subentry)],
|
||||||
|
config_subentry_id=subentry.subentry_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleGenerativeAITaskEntity(
|
||||||
|
ai_task.AITaskEntity,
|
||||||
|
GoogleGenerativeAILLMBaseEntity,
|
||||||
|
):
|
||||||
|
"""Google Generative AI AI Task entity."""
|
||||||
|
|
||||||
|
_attr_supported_features = ai_task.AITaskEntityFeature.GENERATE_DATA
|
||||||
|
|
||||||
|
async def _async_generate_data(
|
||||||
|
self,
|
||||||
|
task: ai_task.GenDataTask,
|
||||||
|
chat_log: conversation.ChatLog,
|
||||||
|
) -> ai_task.GenDataTaskResult:
|
||||||
|
"""Handle a generate data task."""
|
||||||
|
await self._async_handle_chat_log(chat_log)
|
||||||
|
|
||||||
|
if not isinstance(chat_log.content[-1], conversation.AssistantContent):
|
||||||
|
LOGGER.error(
|
||||||
|
"Last content in chat log is not an AssistantContent: %s. This could be due to the model not returning a valid response",
|
||||||
|
chat_log.content[-1],
|
||||||
|
)
|
||||||
|
raise HomeAssistantError(ERROR_GETTING_RESPONSE)
|
||||||
|
|
||||||
|
return ai_task.GenDataTaskResult(
|
||||||
|
conversation_id=chat_log.conversation_id,
|
||||||
|
data=chat_log.content[-1].content or "",
|
||||||
|
)
|
@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
@ -46,10 +47,12 @@ from .const import (
|
|||||||
CONF_TOP_K,
|
CONF_TOP_K,
|
||||||
CONF_TOP_P,
|
CONF_TOP_P,
|
||||||
CONF_USE_GOOGLE_SEARCH_TOOL,
|
CONF_USE_GOOGLE_SEARCH_TOOL,
|
||||||
|
DEFAULT_AI_TASK_NAME,
|
||||||
DEFAULT_CONVERSATION_NAME,
|
DEFAULT_CONVERSATION_NAME,
|
||||||
DEFAULT_TITLE,
|
DEFAULT_TITLE,
|
||||||
DEFAULT_TTS_NAME,
|
DEFAULT_TTS_NAME,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
RECOMMENDED_AI_TASK_OPTIONS,
|
||||||
RECOMMENDED_CHAT_MODEL,
|
RECOMMENDED_CHAT_MODEL,
|
||||||
RECOMMENDED_CONVERSATION_OPTIONS,
|
RECOMMENDED_CONVERSATION_OPTIONS,
|
||||||
RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||||
@ -72,12 +75,14 @@ STEP_API_DATA_SCHEMA = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def validate_input(data: dict[str, Any]) -> None:
|
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
||||||
"""Validate the user input allows us to connect.
|
"""Validate the user input allows us to connect.
|
||||||
|
|
||||||
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||||
"""
|
"""
|
||||||
client = genai.Client(api_key=data[CONF_API_KEY])
|
client = await hass.async_add_executor_job(
|
||||||
|
partial(genai.Client, api_key=data[CONF_API_KEY])
|
||||||
|
)
|
||||||
await client.aio.models.list(
|
await client.aio.models.list(
|
||||||
config={
|
config={
|
||||||
"http_options": {
|
"http_options": {
|
||||||
@ -92,7 +97,7 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle a config flow for Google Generative AI Conversation."""
|
"""Handle a config flow for Google Generative AI Conversation."""
|
||||||
|
|
||||||
VERSION = 2
|
VERSION = 2
|
||||||
MINOR_VERSION = 2
|
MINOR_VERSION = 3
|
||||||
|
|
||||||
async def async_step_api(
|
async def async_step_api(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
@ -102,7 +107,7 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
self._async_abort_entries_match(user_input)
|
self._async_abort_entries_match(user_input)
|
||||||
try:
|
try:
|
||||||
await validate_input(user_input)
|
await validate_input(self.hass, user_input)
|
||||||
except (APIError, Timeout) as err:
|
except (APIError, Timeout) as err:
|
||||||
if isinstance(err, ClientError) and "API_KEY_INVALID" in str(err):
|
if isinstance(err, ClientError) and "API_KEY_INVALID" in str(err):
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
@ -133,6 +138,12 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"title": DEFAULT_TTS_NAME,
|
"title": DEFAULT_TTS_NAME,
|
||||||
"unique_id": None,
|
"unique_id": None,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"subentry_type": "ai_task_data",
|
||||||
|
"data": RECOMMENDED_AI_TASK_OPTIONS,
|
||||||
|
"title": DEFAULT_AI_TASK_NAME,
|
||||||
|
"unique_id": None,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
@ -181,6 +192,7 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
return {
|
return {
|
||||||
"conversation": LLMSubentryFlowHandler,
|
"conversation": LLMSubentryFlowHandler,
|
||||||
"tts": LLMSubentryFlowHandler,
|
"tts": LLMSubentryFlowHandler,
|
||||||
|
"ai_task_data": LLMSubentryFlowHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -214,6 +226,8 @@ class LLMSubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
options: dict[str, Any]
|
options: dict[str, Any]
|
||||||
if self._subentry_type == "tts":
|
if self._subentry_type == "tts":
|
||||||
options = RECOMMENDED_TTS_OPTIONS.copy()
|
options = RECOMMENDED_TTS_OPTIONS.copy()
|
||||||
|
elif self._subentry_type == "ai_task_data":
|
||||||
|
options = RECOMMENDED_AI_TASK_OPTIONS.copy()
|
||||||
else:
|
else:
|
||||||
options = RECOMMENDED_CONVERSATION_OPTIONS.copy()
|
options = RECOMMENDED_CONVERSATION_OPTIONS.copy()
|
||||||
else:
|
else:
|
||||||
@ -288,6 +302,8 @@ async def google_generative_ai_config_option_schema(
|
|||||||
default_name = options[CONF_NAME]
|
default_name = options[CONF_NAME]
|
||||||
elif subentry_type == "tts":
|
elif subentry_type == "tts":
|
||||||
default_name = DEFAULT_TTS_NAME
|
default_name = DEFAULT_TTS_NAME
|
||||||
|
elif subentry_type == "ai_task_data":
|
||||||
|
default_name = DEFAULT_AI_TASK_NAME
|
||||||
else:
|
else:
|
||||||
default_name = DEFAULT_CONVERSATION_NAME
|
default_name = DEFAULT_CONVERSATION_NAME
|
||||||
schema: dict[vol.Required | vol.Optional, Any] = {
|
schema: dict[vol.Required | vol.Optional, Any] = {
|
||||||
@ -315,6 +331,7 @@ async def google_generative_ai_config_option_schema(
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
schema.update(
|
schema.update(
|
||||||
{
|
{
|
||||||
vol.Required(
|
vol.Required(
|
||||||
@ -443,4 +460,5 @@ async def google_generative_ai_config_option_schema(
|
|||||||
): bool,
|
): bool,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return schema
|
return schema
|
||||||
|
@ -12,6 +12,7 @@ CONF_PROMPT = "prompt"
|
|||||||
|
|
||||||
DEFAULT_CONVERSATION_NAME = "Google AI Conversation"
|
DEFAULT_CONVERSATION_NAME = "Google AI Conversation"
|
||||||
DEFAULT_TTS_NAME = "Google AI TTS"
|
DEFAULT_TTS_NAME = "Google AI TTS"
|
||||||
|
DEFAULT_AI_TASK_NAME = "Google AI Task"
|
||||||
|
|
||||||
CONF_RECOMMENDED = "recommended"
|
CONF_RECOMMENDED = "recommended"
|
||||||
CONF_CHAT_MODEL = "chat_model"
|
CONF_CHAT_MODEL = "chat_model"
|
||||||
@ -35,6 +36,7 @@ RECOMMENDED_USE_GOOGLE_SEARCH_TOOL = False
|
|||||||
|
|
||||||
TIMEOUT_MILLIS = 10000
|
TIMEOUT_MILLIS = 10000
|
||||||
FILE_POLLING_INTERVAL_SECONDS = 0.05
|
FILE_POLLING_INTERVAL_SECONDS = 0.05
|
||||||
|
|
||||||
RECOMMENDED_CONVERSATION_OPTIONS = {
|
RECOMMENDED_CONVERSATION_OPTIONS = {
|
||||||
CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT,
|
CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT,
|
||||||
CONF_LLM_HASS_API: [llm.LLM_API_ASSIST],
|
CONF_LLM_HASS_API: [llm.LLM_API_ASSIST],
|
||||||
@ -44,3 +46,7 @@ RECOMMENDED_CONVERSATION_OPTIONS = {
|
|||||||
RECOMMENDED_TTS_OPTIONS = {
|
RECOMMENDED_TTS_OPTIONS = {
|
||||||
CONF_RECOMMENDED: True,
|
CONF_RECOMMENDED: True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RECOMMENDED_AI_TASK_OPTIONS = {
|
||||||
|
CONF_RECOMMENDED: True,
|
||||||
|
}
|
||||||
|
@ -88,6 +88,34 @@
|
|||||||
"entry_not_loaded": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::abort::entry_not_loaded%]",
|
"entry_not_loaded": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::abort::entry_not_loaded%]",
|
||||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ai_task_data": {
|
||||||
|
"initiate_flow": {
|
||||||
|
"user": "Add Generate data with AI service",
|
||||||
|
"reconfigure": "Reconfigure Generate data with AI service"
|
||||||
|
},
|
||||||
|
"entry_type": "Generate data with AI service",
|
||||||
|
"step": {
|
||||||
|
"set_options": {
|
||||||
|
"data": {
|
||||||
|
"name": "[%key:common::config_flow::data::name%]",
|
||||||
|
"recommended": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::recommended%]",
|
||||||
|
"chat_model": "[%key:common::generic::model%]",
|
||||||
|
"temperature": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::temperature%]",
|
||||||
|
"top_p": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::top_p%]",
|
||||||
|
"top_k": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::top_k%]",
|
||||||
|
"max_tokens": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::max_tokens%]",
|
||||||
|
"harassment_block_threshold": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::harassment_block_threshold%]",
|
||||||
|
"hate_block_threshold": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::hate_block_threshold%]",
|
||||||
|
"sexual_block_threshold": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::sexual_block_threshold%]",
|
||||||
|
"dangerous_block_threshold": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::dangerous_block_threshold%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"entry_not_loaded": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::abort::entry_not_loaded%]",
|
||||||
|
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from phone_modem import PhoneModem
|
from phone_modem import PhoneModem
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorEntity
|
from homeassistant.components.sensor import RestoreSensor
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_IDLE
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_IDLE
|
||||||
from homeassistant.core import Event, HomeAssistant, callback
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
@ -40,7 +40,7 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModemCalleridSensor(SensorEntity):
|
class ModemCalleridSensor(RestoreSensor):
|
||||||
"""Implementation of USB modem caller ID sensor."""
|
"""Implementation of USB modem caller ID sensor."""
|
||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
@ -62,9 +62,21 @@ class ModemCalleridSensor(SensorEntity):
|
|||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Call when the modem sensor is added to Home Assistant."""
|
"""Call when the modem sensor is added to Home Assistant."""
|
||||||
self.api.registercallback(self._async_incoming_call)
|
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
|
if (last_state := await self.async_get_last_state()) is not None:
|
||||||
|
self._attr_extra_state_attributes[CID.CID_NAME] = last_state.attributes.get(
|
||||||
|
CID.CID_NAME, ""
|
||||||
|
)
|
||||||
|
self._attr_extra_state_attributes[CID.CID_NUMBER] = (
|
||||||
|
last_state.attributes.get(CID.CID_NUMBER, "")
|
||||||
|
)
|
||||||
|
self._attr_extra_state_attributes[CID.CID_TIME] = last_state.attributes.get(
|
||||||
|
CID.CID_TIME, 0
|
||||||
|
)
|
||||||
|
|
||||||
|
self.api.registercallback(self._async_incoming_call)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_incoming_call(self, new_state: str) -> None:
|
def _async_incoming_call(self, new_state: str) -> None:
|
||||||
"""Handle new states."""
|
"""Handle new states."""
|
||||||
|
@ -24,6 +24,8 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import OpowerConfigEntry, OpowerCoordinator
|
from .coordinator import OpowerConfigEntry, OpowerCoordinator
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class OpowerEntityDescription(SensorEntityDescription):
|
class OpowerEntityDescription(SensorEntityDescription):
|
||||||
@ -38,7 +40,7 @@ class OpowerEntityDescription(SensorEntityDescription):
|
|||||||
ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="elec_usage_to_date",
|
key="elec_usage_to_date",
|
||||||
name="Current bill electric usage to date",
|
translation_key="elec_usage_to_date",
|
||||||
device_class=SensorDeviceClass.ENERGY,
|
device_class=SensorDeviceClass.ENERGY,
|
||||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
# Not TOTAL_INCREASING because it can decrease for accounts with solar
|
# Not TOTAL_INCREASING because it can decrease for accounts with solar
|
||||||
@ -48,7 +50,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="elec_forecasted_usage",
|
key="elec_forecasted_usage",
|
||||||
name="Current bill electric forecasted usage",
|
translation_key="elec_forecasted_usage",
|
||||||
device_class=SensorDeviceClass.ENERGY,
|
device_class=SensorDeviceClass.ENERGY,
|
||||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
@ -57,7 +59,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="elec_typical_usage",
|
key="elec_typical_usage",
|
||||||
name="Typical monthly electric usage",
|
translation_key="elec_typical_usage",
|
||||||
device_class=SensorDeviceClass.ENERGY,
|
device_class=SensorDeviceClass.ENERGY,
|
||||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
@ -66,7 +68,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="elec_cost_to_date",
|
key="elec_cost_to_date",
|
||||||
name="Current bill electric cost to date",
|
translation_key="elec_cost_to_date",
|
||||||
device_class=SensorDeviceClass.MONETARY,
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
native_unit_of_measurement="USD",
|
native_unit_of_measurement="USD",
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
@ -75,7 +77,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="elec_forecasted_cost",
|
key="elec_forecasted_cost",
|
||||||
name="Current bill electric forecasted cost",
|
translation_key="elec_forecasted_cost",
|
||||||
device_class=SensorDeviceClass.MONETARY,
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
native_unit_of_measurement="USD",
|
native_unit_of_measurement="USD",
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
@ -84,7 +86,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="elec_typical_cost",
|
key="elec_typical_cost",
|
||||||
name="Typical monthly electric cost",
|
translation_key="elec_typical_cost",
|
||||||
device_class=SensorDeviceClass.MONETARY,
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
native_unit_of_measurement="USD",
|
native_unit_of_measurement="USD",
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
@ -93,7 +95,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="elec_start_date",
|
key="elec_start_date",
|
||||||
name="Current bill electric start date",
|
translation_key="elec_start_date",
|
||||||
device_class=SensorDeviceClass.DATE,
|
device_class=SensorDeviceClass.DATE,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
@ -101,7 +103,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="elec_end_date",
|
key="elec_end_date",
|
||||||
name="Current bill electric end date",
|
translation_key="elec_end_date",
|
||||||
device_class=SensorDeviceClass.DATE,
|
device_class=SensorDeviceClass.DATE,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
@ -111,7 +113,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="gas_usage_to_date",
|
key="gas_usage_to_date",
|
||||||
name="Current bill gas usage to date",
|
translation_key="gas_usage_to_date",
|
||||||
device_class=SensorDeviceClass.GAS,
|
device_class=SensorDeviceClass.GAS,
|
||||||
native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET,
|
native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
@ -120,7 +122,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="gas_forecasted_usage",
|
key="gas_forecasted_usage",
|
||||||
name="Current bill gas forecasted usage",
|
translation_key="gas_forecasted_usage",
|
||||||
device_class=SensorDeviceClass.GAS,
|
device_class=SensorDeviceClass.GAS,
|
||||||
native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET,
|
native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
@ -129,7 +131,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="gas_typical_usage",
|
key="gas_typical_usage",
|
||||||
name="Typical monthly gas usage",
|
translation_key="gas_typical_usage",
|
||||||
device_class=SensorDeviceClass.GAS,
|
device_class=SensorDeviceClass.GAS,
|
||||||
native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET,
|
native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
@ -138,7 +140,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="gas_cost_to_date",
|
key="gas_cost_to_date",
|
||||||
name="Current bill gas cost to date",
|
translation_key="gas_cost_to_date",
|
||||||
device_class=SensorDeviceClass.MONETARY,
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
native_unit_of_measurement="USD",
|
native_unit_of_measurement="USD",
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
@ -147,7 +149,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="gas_forecasted_cost",
|
key="gas_forecasted_cost",
|
||||||
name="Current bill gas forecasted cost",
|
translation_key="gas_forecasted_cost",
|
||||||
device_class=SensorDeviceClass.MONETARY,
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
native_unit_of_measurement="USD",
|
native_unit_of_measurement="USD",
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
@ -156,7 +158,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="gas_typical_cost",
|
key="gas_typical_cost",
|
||||||
name="Typical monthly gas cost",
|
translation_key="gas_typical_cost",
|
||||||
device_class=SensorDeviceClass.MONETARY,
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
native_unit_of_measurement="USD",
|
native_unit_of_measurement="USD",
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
@ -165,7 +167,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="gas_start_date",
|
key="gas_start_date",
|
||||||
name="Current bill gas start date",
|
translation_key="gas_start_date",
|
||||||
device_class=SensorDeviceClass.DATE,
|
device_class=SensorDeviceClass.DATE,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
@ -173,7 +175,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
OpowerEntityDescription(
|
OpowerEntityDescription(
|
||||||
key="gas_end_date",
|
key="gas_end_date",
|
||||||
name="Current bill gas end date",
|
translation_key="gas_end_date",
|
||||||
device_class=SensorDeviceClass.DATE,
|
device_class=SensorDeviceClass.DATE,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
@ -229,6 +231,7 @@ async def async_setup_entry(
|
|||||||
class OpowerSensor(CoordinatorEntity[OpowerCoordinator], SensorEntity):
|
class OpowerSensor(CoordinatorEntity[OpowerCoordinator], SensorEntity):
|
||||||
"""Representation of an Opower sensor."""
|
"""Representation of an Opower sensor."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
entity_description: OpowerEntityDescription
|
entity_description: OpowerEntityDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -249,8 +252,6 @@ class OpowerSensor(CoordinatorEntity[OpowerCoordinator], SensorEntity):
|
|||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType | date:
|
def native_value(self) -> StateType | date:
|
||||||
"""Return the state."""
|
"""Return the state."""
|
||||||
if self.coordinator.data is not None:
|
return self.entity_description.value_fn(
|
||||||
return self.entity_description.value_fn(
|
self.coordinator.data[self.utility_account_id]
|
||||||
self.coordinator.data[self.utility_account_id]
|
)
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
@ -37,5 +37,57 @@
|
|||||||
"title": "Return to grid statistics for account: {utility_account_id}",
|
"title": "Return to grid statistics for account: {utility_account_id}",
|
||||||
"description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those in separate return statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Return to grid** section:\n\n{target_ids}\n\nOnce you have added them, ignore this issue."
|
"description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those in separate return statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Return to grid** section:\n\n{target_ids}\n\nOnce you have added them, ignore this issue."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"elec_usage_to_date": {
|
||||||
|
"name": "Current bill electric usage to date"
|
||||||
|
},
|
||||||
|
"elec_forecasted_usage": {
|
||||||
|
"name": "Current bill electric forecasted usage"
|
||||||
|
},
|
||||||
|
"elec_typical_usage": {
|
||||||
|
"name": "Typical monthly electric usage"
|
||||||
|
},
|
||||||
|
"elec_cost_to_date": {
|
||||||
|
"name": "Current bill electric cost to date"
|
||||||
|
},
|
||||||
|
"elec_forecasted_cost": {
|
||||||
|
"name": "Current bill electric forecasted cost"
|
||||||
|
},
|
||||||
|
"elec_typical_cost": {
|
||||||
|
"name": "Typical monthly electric cost"
|
||||||
|
},
|
||||||
|
"elec_start_date": {
|
||||||
|
"name": "Current bill electric start date"
|
||||||
|
},
|
||||||
|
"elec_end_date": {
|
||||||
|
"name": "Current bill electric end date"
|
||||||
|
},
|
||||||
|
"gas_usage_to_date": {
|
||||||
|
"name": "Current bill gas usage to date"
|
||||||
|
},
|
||||||
|
"gas_forecasted_usage": {
|
||||||
|
"name": "Current bill gas forecasted usage"
|
||||||
|
},
|
||||||
|
"gas_typical_usage": {
|
||||||
|
"name": "Typical monthly gas usage"
|
||||||
|
},
|
||||||
|
"gas_cost_to_date": {
|
||||||
|
"name": "Current bill gas cost to date"
|
||||||
|
},
|
||||||
|
"gas_forecasted_cost": {
|
||||||
|
"name": "Current bill gas forecasted cost"
|
||||||
|
},
|
||||||
|
"gas_typical_cost": {
|
||||||
|
"name": "Typical monthly gas cost"
|
||||||
|
},
|
||||||
|
"gas_start_date": {
|
||||||
|
"name": "Current bill gas start date"
|
||||||
|
},
|
||||||
|
"gas_end_date": {
|
||||||
|
"name": "Current bill gas end date"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -777,7 +777,23 @@ def _selector_serializer(schema: Any) -> Any: # noqa: C901
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
if isinstance(schema, selector.ObjectSelector):
|
if isinstance(schema, selector.ObjectSelector):
|
||||||
return {"type": "object", "additionalProperties": True}
|
result = {"type": "object"}
|
||||||
|
if fields := schema.config.get("fields"):
|
||||||
|
result["properties"] = {
|
||||||
|
field: convert(
|
||||||
|
selector.selector(field_schema["selector"]),
|
||||||
|
custom_serializer=_selector_serializer,
|
||||||
|
)
|
||||||
|
for field, field_schema in fields.items()
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
result["additionalProperties"] = True
|
||||||
|
if schema.config.get("multiple"):
|
||||||
|
result = {
|
||||||
|
"type": "array",
|
||||||
|
"items": result,
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
if isinstance(schema, selector.SelectSelector):
|
if isinstance(schema, selector.SelectSelector):
|
||||||
options = [
|
options = [
|
||||||
|
@ -1789,6 +1789,13 @@ def async_get_issue_tracker(
|
|||||||
# If we know nothing about the integration, suggest opening an issue on HA core
|
# If we know nothing about the integration, suggest opening an issue on HA core
|
||||||
return issue_tracker
|
return issue_tracker
|
||||||
|
|
||||||
|
if module and not integration_domain:
|
||||||
|
# If we only have a module, we can try to get the integration domain from it
|
||||||
|
if module.startswith("custom_components."):
|
||||||
|
integration_domain = module.split(".")[1]
|
||||||
|
elif module.startswith("homeassistant.components."):
|
||||||
|
integration_domain = module.split(".")[2]
|
||||||
|
|
||||||
if not integration:
|
if not integration:
|
||||||
integration = async_get_issue_integration(hass, integration_domain)
|
integration = async_get_issue_integration(hass, integration_domain)
|
||||||
|
|
||||||
|
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@ -1962,7 +1962,7 @@ pyeiscp==0.0.7
|
|||||||
pyemoncms==0.1.1
|
pyemoncms==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.enphase_envoy
|
# homeassistant.components.enphase_envoy
|
||||||
pyenphase==2.1.0
|
pyenphase==2.2.0
|
||||||
|
|
||||||
# homeassistant.components.envisalink
|
# homeassistant.components.envisalink
|
||||||
pyenvisalink==4.7
|
pyenvisalink==4.7
|
||||||
|
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@ -1637,7 +1637,7 @@ pyeiscp==0.0.7
|
|||||||
pyemoncms==0.1.1
|
pyemoncms==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.enphase_envoy
|
# homeassistant.components.enphase_envoy
|
||||||
pyenphase==2.1.0
|
pyenphase==2.2.0
|
||||||
|
|
||||||
# homeassistant.components.everlights
|
# homeassistant.components.everlights
|
||||||
pyeverlights==0.1.0
|
pyeverlights==0.1.0
|
||||||
|
@ -1069,3 +1069,100 @@ async def test_options_flow(
|
|||||||
)
|
)
|
||||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
assert mock_config_entry.options == {CONF_ENABLE_IME: True}
|
assert mock_config_entry.options == {CONF_ENABLE_IME: True}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reconfigure_flow_success(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
mock_api: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test the full reconfigure flow from start to finish without any exceptions."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "reconfigure"
|
||||||
|
assert not result["errors"]
|
||||||
|
assert "host" in result["data_schema"].schema
|
||||||
|
# Form should have as default value the existing host
|
||||||
|
host_key = next(k for k in result["data_schema"].schema if k.schema == "host")
|
||||||
|
assert host_key.default() == mock_config_entry.data["host"]
|
||||||
|
|
||||||
|
mock_api.async_generate_cert_if_missing = AsyncMock(return_value=True)
|
||||||
|
mock_api.async_get_name_and_mac = AsyncMock(
|
||||||
|
return_value=(mock_config_entry.data["name"], mock_config_entry.data["mac"])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Simulate user input with a new host
|
||||||
|
new_host = "4.3.2.1"
|
||||||
|
assert new_host != mock_config_entry.data["host"]
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], {"host": new_host}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "reconfigure_successful"
|
||||||
|
assert mock_config_entry.data["host"] == new_host
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reconfigure_flow_cannot_connect(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
mock_api: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test reconfigure flow with CannotConnect exception."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "reconfigure"
|
||||||
|
|
||||||
|
mock_api.async_generate_cert_if_missing = AsyncMock(return_value=True)
|
||||||
|
mock_api.async_get_name_and_mac = AsyncMock(side_effect=CannotConnect())
|
||||||
|
|
||||||
|
new_host = "4.3.2.1"
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], {"host": new_host}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "reconfigure"
|
||||||
|
assert result["errors"] == {"base": "cannot_connect"}
|
||||||
|
assert mock_config_entry.data["host"] == "1.2.3.4"
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reconfigure_flow_unique_id_mismatch(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
mock_api: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test reconfigure flow with a different device (unique_id mismatch)."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "reconfigure"
|
||||||
|
|
||||||
|
mock_api.async_generate_cert_if_missing = AsyncMock(return_value=True)
|
||||||
|
# The new host corresponds to a device with a different MAC/unique_id
|
||||||
|
new_mac = "FF:EE:DD:CC:BB:AA"
|
||||||
|
assert new_mac != mock_config_entry.data["mac"]
|
||||||
|
mock_api.async_get_name_and_mac = AsyncMock(return_value=("name", new_mac))
|
||||||
|
|
||||||
|
new_host = "4.3.2.1"
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], {"host": new_host}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "unique_id_mismatch"
|
||||||
|
assert mock_config_entry.data["host"] == "1.2.3.4"
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 0
|
||||||
|
@ -129,6 +129,7 @@ async def test_async_step_reauth(
|
|||||||
CONF_PASSWORD: "test-password",
|
CONF_PASSWORD: "test-password",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "reauth_successful"
|
assert result["reason"] == "reauth_successful"
|
||||||
@ -639,6 +640,7 @@ async def test_reauth_errors(
|
|||||||
CONF_PASSWORD: "test-password",
|
CONF_PASSWORD: "test-password",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "reauth_successful"
|
assert result["reason"] == "reauth_successful"
|
||||||
|
@ -7,6 +7,7 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant.components.google_generative_ai_conversation.const import (
|
from homeassistant.components.google_generative_ai_conversation.const import (
|
||||||
CONF_USE_GOOGLE_SEARCH_TOOL,
|
CONF_USE_GOOGLE_SEARCH_TOOL,
|
||||||
|
DEFAULT_AI_TASK_NAME,
|
||||||
DEFAULT_CONVERSATION_NAME,
|
DEFAULT_CONVERSATION_NAME,
|
||||||
DEFAULT_TTS_NAME,
|
DEFAULT_TTS_NAME,
|
||||||
)
|
)
|
||||||
@ -29,6 +30,7 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
|||||||
"api_key": "bla",
|
"api_key": "bla",
|
||||||
},
|
},
|
||||||
version=2,
|
version=2,
|
||||||
|
minor_version=3,
|
||||||
subentries_data=[
|
subentries_data=[
|
||||||
{
|
{
|
||||||
"data": {},
|
"data": {},
|
||||||
@ -44,6 +46,13 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
|||||||
"subentry_id": "ulid-tts",
|
"subentry_id": "ulid-tts",
|
||||||
"unique_id": None,
|
"unique_id": None,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"data": {},
|
||||||
|
"subentry_type": "ai_task_data",
|
||||||
|
"title": DEFAULT_AI_TASK_NAME,
|
||||||
|
"subentry_id": "ulid-ai-task",
|
||||||
|
"unique_id": None,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
entry.runtime_data = Mock()
|
entry.runtime_data = Mock()
|
||||||
|
@ -7,6 +7,14 @@
|
|||||||
'options': dict({
|
'options': dict({
|
||||||
}),
|
}),
|
||||||
'subentries': dict({
|
'subentries': dict({
|
||||||
|
'ulid-ai-task': dict({
|
||||||
|
'data': dict({
|
||||||
|
}),
|
||||||
|
'subentry_id': 'ulid-ai-task',
|
||||||
|
'subentry_type': 'ai_task_data',
|
||||||
|
'title': 'Google AI Task',
|
||||||
|
'unique_id': None,
|
||||||
|
}),
|
||||||
'ulid-conversation': dict({
|
'ulid-conversation': dict({
|
||||||
'data': dict({
|
'data': dict({
|
||||||
'chat_model': 'models/gemini-2.5-flash',
|
'chat_model': 'models/gemini-2.5-flash',
|
||||||
|
@ -32,6 +32,37 @@
|
|||||||
'sw_version': None,
|
'sw_version': None,
|
||||||
'via_device_id': None,
|
'via_device_id': None,
|
||||||
}),
|
}),
|
||||||
|
DeviceRegistryEntrySnapshot({
|
||||||
|
'area_id': None,
|
||||||
|
'config_entries': <ANY>,
|
||||||
|
'config_entries_subentries': <ANY>,
|
||||||
|
'configuration_url': None,
|
||||||
|
'connections': set({
|
||||||
|
}),
|
||||||
|
'disabled_by': None,
|
||||||
|
'entry_type': <DeviceEntryType.SERVICE: 'service'>,
|
||||||
|
'hw_version': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'identifiers': set({
|
||||||
|
tuple(
|
||||||
|
'google_generative_ai_conversation',
|
||||||
|
'ulid-ai-task',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
'is_new': False,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'manufacturer': 'Google',
|
||||||
|
'model': 'gemini-2.5-flash',
|
||||||
|
'model_id': None,
|
||||||
|
'name': 'Google AI Task',
|
||||||
|
'name_by_user': None,
|
||||||
|
'primary_config_entry': <ANY>,
|
||||||
|
'serial_number': None,
|
||||||
|
'suggested_area': None,
|
||||||
|
'sw_version': None,
|
||||||
|
'via_device_id': None,
|
||||||
|
}),
|
||||||
DeviceRegistryEntrySnapshot({
|
DeviceRegistryEntrySnapshot({
|
||||||
'area_id': None,
|
'area_id': None,
|
||||||
'config_entries': <ANY>,
|
'config_entries': <ANY>,
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
"""Test AI Task platform of Google Generative AI Conversation integration."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from google.genai.types import GenerateContentResponse
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components import ai_task
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
from tests.components.conversation import (
|
||||||
|
MockChatLog,
|
||||||
|
mock_chat_log, # noqa: F401
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_init_component")
|
||||||
|
async def test_run_task(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_chat_log: MockChatLog, # noqa: F811
|
||||||
|
mock_send_message_stream: AsyncMock,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test empty response."""
|
||||||
|
entity_id = "ai_task.google_ai_task"
|
||||||
|
|
||||||
|
# Ensure it's linked to the subentry
|
||||||
|
entity_entry = entity_registry.async_get(entity_id)
|
||||||
|
ai_task_entry = next(
|
||||||
|
iter(
|
||||||
|
entry
|
||||||
|
for entry in mock_config_entry.subentries.values()
|
||||||
|
if entry.subentry_type == "ai_task_data"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert entity_entry.config_entry_id == mock_config_entry.entry_id
|
||||||
|
assert entity_entry.config_subentry_id == ai_task_entry.subentry_id
|
||||||
|
|
||||||
|
mock_send_message_stream.return_value = [
|
||||||
|
[
|
||||||
|
GenerateContentResponse(
|
||||||
|
candidates=[
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"parts": [{"text": "Hi there!"}],
|
||||||
|
"role": "model",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
result = await ai_task.async_generate_data(
|
||||||
|
hass,
|
||||||
|
task_name="Test Task",
|
||||||
|
entity_id=entity_id,
|
||||||
|
instructions="Test prompt",
|
||||||
|
)
|
||||||
|
assert result.data == "Hi there!"
|
@ -19,9 +19,11 @@ from homeassistant.components.google_generative_ai_conversation.const import (
|
|||||||
CONF_TOP_K,
|
CONF_TOP_K,
|
||||||
CONF_TOP_P,
|
CONF_TOP_P,
|
||||||
CONF_USE_GOOGLE_SEARCH_TOOL,
|
CONF_USE_GOOGLE_SEARCH_TOOL,
|
||||||
|
DEFAULT_AI_TASK_NAME,
|
||||||
DEFAULT_CONVERSATION_NAME,
|
DEFAULT_CONVERSATION_NAME,
|
||||||
DEFAULT_TTS_NAME,
|
DEFAULT_TTS_NAME,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
RECOMMENDED_AI_TASK_OPTIONS,
|
||||||
RECOMMENDED_CHAT_MODEL,
|
RECOMMENDED_CHAT_MODEL,
|
||||||
RECOMMENDED_CONVERSATION_OPTIONS,
|
RECOMMENDED_CONVERSATION_OPTIONS,
|
||||||
RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||||
@ -121,6 +123,12 @@ async def test_form(hass: HomeAssistant) -> None:
|
|||||||
"title": DEFAULT_TTS_NAME,
|
"title": DEFAULT_TTS_NAME,
|
||||||
"unique_id": None,
|
"unique_id": None,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"subentry_type": "ai_task_data",
|
||||||
|
"data": RECOMMENDED_AI_TASK_OPTIONS,
|
||||||
|
"title": DEFAULT_AI_TASK_NAME,
|
||||||
|
"unique_id": None,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
@ -222,7 +230,7 @@ async def test_creating_tts_subentry(
|
|||||||
assert result2["title"] == "Mock TTS"
|
assert result2["title"] == "Mock TTS"
|
||||||
assert result2["data"] == RECOMMENDED_TTS_OPTIONS
|
assert result2["data"] == RECOMMENDED_TTS_OPTIONS
|
||||||
|
|
||||||
assert len(mock_config_entry.subentries) == 3
|
assert len(mock_config_entry.subentries) == 4
|
||||||
|
|
||||||
new_subentry_id = list(set(mock_config_entry.subentries) - old_subentries)[0]
|
new_subentry_id = list(set(mock_config_entry.subentries) - old_subentries)[0]
|
||||||
new_subentry = mock_config_entry.subentries[new_subentry_id]
|
new_subentry = mock_config_entry.subentries[new_subentry_id]
|
||||||
@ -232,13 +240,59 @@ async def test_creating_tts_subentry(
|
|||||||
assert new_subentry.title == "Mock TTS"
|
assert new_subentry.title == "Mock TTS"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_creating_ai_task_subentry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_init_component: None,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test creating an AI task subentry."""
|
||||||
|
with patch(
|
||||||
|
"google.genai.models.AsyncModels.list",
|
||||||
|
return_value=get_models_pager(),
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.subentries.async_init(
|
||||||
|
(mock_config_entry.entry_id, "ai_task_data"),
|
||||||
|
context={"source": config_entries.SOURCE_USER},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM, result
|
||||||
|
assert result["step_id"] == "set_options"
|
||||||
|
assert not result["errors"]
|
||||||
|
|
||||||
|
old_subentries = set(mock_config_entry.subentries)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"google.genai.models.AsyncModels.list",
|
||||||
|
return_value=get_models_pager(),
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.subentries.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{CONF_NAME: "Mock AI Task", **RECOMMENDED_AI_TASK_OPTIONS},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result2["title"] == "Mock AI Task"
|
||||||
|
assert result2["data"] == RECOMMENDED_AI_TASK_OPTIONS
|
||||||
|
|
||||||
|
assert len(mock_config_entry.subentries) == 4
|
||||||
|
|
||||||
|
new_subentry_id = list(set(mock_config_entry.subentries) - old_subentries)[0]
|
||||||
|
new_subentry = mock_config_entry.subentries[new_subentry_id]
|
||||||
|
|
||||||
|
assert new_subentry.subentry_type == "ai_task_data"
|
||||||
|
assert new_subentry.data == RECOMMENDED_AI_TASK_OPTIONS
|
||||||
|
assert new_subentry.title == "Mock AI Task"
|
||||||
|
|
||||||
|
|
||||||
async def test_creating_conversation_subentry_not_loaded(
|
async def test_creating_conversation_subentry_not_loaded(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_init_component: None,
|
mock_init_component: None,
|
||||||
mock_config_entry: MockConfigEntry,
|
mock_config_entry: MockConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test creating a conversation subentry."""
|
"""Test that subentry fails to init if entry not loaded."""
|
||||||
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"google.genai.models.AsyncModels.list",
|
"google.genai.models.AsyncModels.list",
|
||||||
return_value=get_models_pager(),
|
return_value=get_models_pager(),
|
||||||
|
@ -8,9 +8,13 @@ from requests.exceptions import Timeout
|
|||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.components.google_generative_ai_conversation.const import (
|
from homeassistant.components.google_generative_ai_conversation.const import (
|
||||||
|
DEFAULT_AI_TASK_NAME,
|
||||||
|
DEFAULT_CONVERSATION_NAME,
|
||||||
DEFAULT_TITLE,
|
DEFAULT_TITLE,
|
||||||
DEFAULT_TTS_NAME,
|
DEFAULT_TTS_NAME,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
RECOMMENDED_AI_TASK_OPTIONS,
|
||||||
|
RECOMMENDED_CONVERSATION_OPTIONS,
|
||||||
RECOMMENDED_TTS_OPTIONS,
|
RECOMMENDED_TTS_OPTIONS,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntryState, ConfigSubentryData
|
from homeassistant.config_entries import ConfigEntryState, ConfigSubentryData
|
||||||
@ -397,7 +401,7 @@ async def test_load_entry_with_unloaded_entries(
|
|||||||
assert [tuple(mock_call) for mock_call in mock_generate.mock_calls] == snapshot
|
assert [tuple(mock_call) for mock_call in mock_generate.mock_calls] == snapshot
|
||||||
|
|
||||||
|
|
||||||
async def test_migration_from_v1_to_v2(
|
async def test_migration_from_v1(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
device_registry: dr.DeviceRegistry,
|
device_registry: dr.DeviceRegistry,
|
||||||
entity_registry: er.EntityRegistry,
|
entity_registry: er.EntityRegistry,
|
||||||
@ -473,10 +477,10 @@ async def test_migration_from_v1_to_v2(
|
|||||||
assert len(entries) == 1
|
assert len(entries) == 1
|
||||||
entry = entries[0]
|
entry = entries[0]
|
||||||
assert entry.version == 2
|
assert entry.version == 2
|
||||||
assert entry.minor_version == 2
|
assert entry.minor_version == 3
|
||||||
assert not entry.options
|
assert not entry.options
|
||||||
assert entry.title == DEFAULT_TITLE
|
assert entry.title == DEFAULT_TITLE
|
||||||
assert len(entry.subentries) == 3
|
assert len(entry.subentries) == 4
|
||||||
conversation_subentries = [
|
conversation_subentries = [
|
||||||
subentry
|
subentry
|
||||||
for subentry in entry.subentries.values()
|
for subentry in entry.subentries.values()
|
||||||
@ -495,6 +499,14 @@ async def test_migration_from_v1_to_v2(
|
|||||||
assert len(tts_subentries) == 1
|
assert len(tts_subentries) == 1
|
||||||
assert tts_subentries[0].data == RECOMMENDED_TTS_OPTIONS
|
assert tts_subentries[0].data == RECOMMENDED_TTS_OPTIONS
|
||||||
assert tts_subentries[0].title == DEFAULT_TTS_NAME
|
assert tts_subentries[0].title == DEFAULT_TTS_NAME
|
||||||
|
ai_task_subentries = [
|
||||||
|
subentry
|
||||||
|
for subentry in entry.subentries.values()
|
||||||
|
if subentry.subentry_type == "ai_task_data"
|
||||||
|
]
|
||||||
|
assert len(ai_task_subentries) == 1
|
||||||
|
assert ai_task_subentries[0].data == RECOMMENDED_AI_TASK_OPTIONS
|
||||||
|
assert ai_task_subentries[0].title == DEFAULT_AI_TASK_NAME
|
||||||
|
|
||||||
subentry = conversation_subentries[0]
|
subentry = conversation_subentries[0]
|
||||||
|
|
||||||
@ -542,7 +554,7 @@ async def test_migration_from_v1_to_v2(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_migration_from_v1_to_v2_with_multiple_keys(
|
async def test_migration_from_v1_with_multiple_keys(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
device_registry: dr.DeviceRegistry,
|
device_registry: dr.DeviceRegistry,
|
||||||
entity_registry: er.EntityRegistry,
|
entity_registry: er.EntityRegistry,
|
||||||
@ -619,10 +631,10 @@ async def test_migration_from_v1_to_v2_with_multiple_keys(
|
|||||||
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
assert entry.version == 2
|
assert entry.version == 2
|
||||||
assert entry.minor_version == 2
|
assert entry.minor_version == 3
|
||||||
assert not entry.options
|
assert not entry.options
|
||||||
assert entry.title == DEFAULT_TITLE
|
assert entry.title == DEFAULT_TITLE
|
||||||
assert len(entry.subentries) == 2
|
assert len(entry.subentries) == 3
|
||||||
subentry = list(entry.subentries.values())[0]
|
subentry = list(entry.subentries.values())[0]
|
||||||
assert subentry.subentry_type == "conversation"
|
assert subentry.subentry_type == "conversation"
|
||||||
assert subentry.data == options
|
assert subentry.data == options
|
||||||
@ -631,6 +643,10 @@ async def test_migration_from_v1_to_v2_with_multiple_keys(
|
|||||||
assert subentry.subentry_type == "tts"
|
assert subentry.subentry_type == "tts"
|
||||||
assert subentry.data == RECOMMENDED_TTS_OPTIONS
|
assert subentry.data == RECOMMENDED_TTS_OPTIONS
|
||||||
assert subentry.title == DEFAULT_TTS_NAME
|
assert subentry.title == DEFAULT_TTS_NAME
|
||||||
|
subentry = list(entry.subentries.values())[2]
|
||||||
|
assert subentry.subentry_type == "ai_task_data"
|
||||||
|
assert subentry.data == RECOMMENDED_AI_TASK_OPTIONS
|
||||||
|
assert subentry.title == DEFAULT_AI_TASK_NAME
|
||||||
|
|
||||||
dev = device_registry.async_get_device(
|
dev = device_registry.async_get_device(
|
||||||
identifiers={(DOMAIN, list(entry.subentries.values())[0].subentry_id)}
|
identifiers={(DOMAIN, list(entry.subentries.values())[0].subentry_id)}
|
||||||
@ -642,7 +658,7 @@ async def test_migration_from_v1_to_v2_with_multiple_keys(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_migration_from_v1_to_v2_with_same_keys(
|
async def test_migration_from_v1_with_same_keys(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
device_registry: dr.DeviceRegistry,
|
device_registry: dr.DeviceRegistry,
|
||||||
entity_registry: er.EntityRegistry,
|
entity_registry: er.EntityRegistry,
|
||||||
@ -718,10 +734,10 @@ async def test_migration_from_v1_to_v2_with_same_keys(
|
|||||||
assert len(entries) == 1
|
assert len(entries) == 1
|
||||||
entry = entries[0]
|
entry = entries[0]
|
||||||
assert entry.version == 2
|
assert entry.version == 2
|
||||||
assert entry.minor_version == 2
|
assert entry.minor_version == 3
|
||||||
assert not entry.options
|
assert not entry.options
|
||||||
assert entry.title == DEFAULT_TITLE
|
assert entry.title == DEFAULT_TITLE
|
||||||
assert len(entry.subentries) == 3
|
assert len(entry.subentries) == 4
|
||||||
conversation_subentries = [
|
conversation_subentries = [
|
||||||
subentry
|
subentry
|
||||||
for subentry in entry.subentries.values()
|
for subentry in entry.subentries.values()
|
||||||
@ -740,6 +756,14 @@ async def test_migration_from_v1_to_v2_with_same_keys(
|
|||||||
assert len(tts_subentries) == 1
|
assert len(tts_subentries) == 1
|
||||||
assert tts_subentries[0].data == RECOMMENDED_TTS_OPTIONS
|
assert tts_subentries[0].data == RECOMMENDED_TTS_OPTIONS
|
||||||
assert tts_subentries[0].title == DEFAULT_TTS_NAME
|
assert tts_subentries[0].title == DEFAULT_TTS_NAME
|
||||||
|
ai_task_subentries = [
|
||||||
|
subentry
|
||||||
|
for subentry in entry.subentries.values()
|
||||||
|
if subentry.subentry_type == "ai_task_data"
|
||||||
|
]
|
||||||
|
assert len(ai_task_subentries) == 1
|
||||||
|
assert ai_task_subentries[0].data == RECOMMENDED_AI_TASK_OPTIONS
|
||||||
|
assert ai_task_subentries[0].title == DEFAULT_AI_TASK_NAME
|
||||||
|
|
||||||
subentry = conversation_subentries[0]
|
subentry = conversation_subentries[0]
|
||||||
|
|
||||||
@ -829,7 +853,7 @@ async def test_migration_from_v1_to_v2_with_same_keys(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_migration_from_v2_1_to_v2_2(
|
async def test_migration_from_v2_1(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
device_registry: dr.DeviceRegistry,
|
device_registry: dr.DeviceRegistry,
|
||||||
entity_registry: er.EntityRegistry,
|
entity_registry: er.EntityRegistry,
|
||||||
@ -837,12 +861,13 @@ async def test_migration_from_v2_1_to_v2_2(
|
|||||||
extra_subentries: list[ConfigSubentryData],
|
extra_subentries: list[ConfigSubentryData],
|
||||||
expected_device_subentries: dict[str, set[str | None]],
|
expected_device_subentries: dict[str, set[str | None]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test migration from version 2.1 to version 2.2.
|
"""Test migration from version 2.1.
|
||||||
|
|
||||||
This tests we clean up the broken migration in Home Assistant Core
|
This tests we clean up the broken migration in Home Assistant Core
|
||||||
2025.7.0b0-2025.7.0b1:
|
2025.7.0b0-2025.7.0b1 and add AI Task subentry:
|
||||||
- Fix device registry (Fixed in Home Assistant Core 2025.7.0b2)
|
- Fix device registry (Fixed in Home Assistant Core 2025.7.0b2)
|
||||||
- Add TTS subentry (Added in Home Assistant Core 2025.7.0b1)
|
- Add TTS subentry (Added in Home Assistant Core 2025.7.0b1)
|
||||||
|
- Add AI Task subentry (Added in version 2.3)
|
||||||
"""
|
"""
|
||||||
# Create a v2.1 config entry with 2 subentries, devices and entities
|
# Create a v2.1 config entry with 2 subentries, devices and entities
|
||||||
options = {
|
options = {
|
||||||
@ -930,10 +955,10 @@ async def test_migration_from_v2_1_to_v2_2(
|
|||||||
assert len(entries) == 1
|
assert len(entries) == 1
|
||||||
entry = entries[0]
|
entry = entries[0]
|
||||||
assert entry.version == 2
|
assert entry.version == 2
|
||||||
assert entry.minor_version == 2
|
assert entry.minor_version == 3
|
||||||
assert not entry.options
|
assert not entry.options
|
||||||
assert entry.title == DEFAULT_TITLE
|
assert entry.title == DEFAULT_TITLE
|
||||||
assert len(entry.subentries) == 3
|
assert len(entry.subentries) == 4
|
||||||
conversation_subentries = [
|
conversation_subentries = [
|
||||||
subentry
|
subentry
|
||||||
for subentry in entry.subentries.values()
|
for subentry in entry.subentries.values()
|
||||||
@ -952,6 +977,14 @@ async def test_migration_from_v2_1_to_v2_2(
|
|||||||
assert len(tts_subentries) == 1
|
assert len(tts_subentries) == 1
|
||||||
assert tts_subentries[0].data == RECOMMENDED_TTS_OPTIONS
|
assert tts_subentries[0].data == RECOMMENDED_TTS_OPTIONS
|
||||||
assert tts_subentries[0].title == DEFAULT_TTS_NAME
|
assert tts_subentries[0].title == DEFAULT_TTS_NAME
|
||||||
|
ai_task_subentries = [
|
||||||
|
subentry
|
||||||
|
for subentry in entry.subentries.values()
|
||||||
|
if subentry.subentry_type == "ai_task_data"
|
||||||
|
]
|
||||||
|
assert len(ai_task_subentries) == 1
|
||||||
|
assert ai_task_subentries[0].data == RECOMMENDED_AI_TASK_OPTIONS
|
||||||
|
assert ai_task_subentries[0].title == DEFAULT_AI_TASK_NAME
|
||||||
|
|
||||||
subentry = conversation_subentries[0]
|
subentry = conversation_subentries[0]
|
||||||
|
|
||||||
@ -1011,3 +1044,80 @@ async def test_devices(
|
|||||||
device_registry, mock_config_entry.entry_id
|
device_registry, mock_config_entry.entry_id
|
||||||
)
|
)
|
||||||
assert devices == snapshot
|
assert devices == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
async def test_migrate_entry_from_v2_2(hass: HomeAssistant) -> None:
|
||||||
|
"""Test migration from version 2.2."""
|
||||||
|
# Create a v2.2 config entry with conversation and TTS subentries
|
||||||
|
mock_config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={CONF_API_KEY: "test-api-key"},
|
||||||
|
version=2,
|
||||||
|
minor_version=2,
|
||||||
|
subentries_data=[
|
||||||
|
{
|
||||||
|
"data": RECOMMENDED_CONVERSATION_OPTIONS,
|
||||||
|
"subentry_type": "conversation",
|
||||||
|
"title": DEFAULT_CONVERSATION_NAME,
|
||||||
|
"unique_id": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": RECOMMENDED_TTS_OPTIONS,
|
||||||
|
"subentry_type": "tts",
|
||||||
|
"title": DEFAULT_TTS_NAME,
|
||||||
|
"unique_id": None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
# Verify initial state
|
||||||
|
assert mock_config_entry.version == 2
|
||||||
|
assert mock_config_entry.minor_version == 2
|
||||||
|
assert len(mock_config_entry.subentries) == 2
|
||||||
|
|
||||||
|
# Run setup to trigger migration
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.google_generative_ai_conversation.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
assert result is True
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Verify migration completed
|
||||||
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
assert len(entries) == 1
|
||||||
|
entry = entries[0]
|
||||||
|
|
||||||
|
# Check version and subversion were updated
|
||||||
|
assert entry.version == 2
|
||||||
|
assert entry.minor_version == 3
|
||||||
|
|
||||||
|
# Check we now have conversation, tts and ai_task_data subentries
|
||||||
|
assert len(entry.subentries) == 3
|
||||||
|
|
||||||
|
subentries = {
|
||||||
|
subentry.subentry_type: subentry for subentry in entry.subentries.values()
|
||||||
|
}
|
||||||
|
assert "conversation" in subentries
|
||||||
|
assert "tts" in subentries
|
||||||
|
assert "ai_task_data" in subentries
|
||||||
|
|
||||||
|
# Find and verify the ai_task_data subentry
|
||||||
|
ai_task_subentry = subentries["ai_task_data"]
|
||||||
|
assert ai_task_subentry is not None
|
||||||
|
assert ai_task_subentry.title == DEFAULT_AI_TASK_NAME
|
||||||
|
assert ai_task_subentry.data == RECOMMENDED_AI_TASK_OPTIONS
|
||||||
|
|
||||||
|
# Verify conversation subentry is still there and unchanged
|
||||||
|
conversation_subentry = subentries["conversation"]
|
||||||
|
assert conversation_subentry is not None
|
||||||
|
assert conversation_subentry.title == DEFAULT_CONVERSATION_NAME
|
||||||
|
assert conversation_subentry.data == RECOMMENDED_CONVERSATION_OPTIONS
|
||||||
|
|
||||||
|
# Verify TTS subentry is still there and unchanged
|
||||||
|
tts_subentry = subentries["tts"]
|
||||||
|
assert tts_subentry is not None
|
||||||
|
assert tts_subentry.title == DEFAULT_TTS_NAME
|
||||||
|
assert tts_subentry.data == RECOMMENDED_TTS_OPTIONS
|
||||||
|
@ -25,36 +25,48 @@ async def test_sensors(
|
|||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
# Check electric sensors
|
# Check electric sensors
|
||||||
entry = entity_registry.async_get("sensor.current_bill_electric_usage_to_date")
|
entry = entity_registry.async_get(
|
||||||
|
"sensor.elec_account_111111_current_bill_electric_usage_to_date"
|
||||||
|
)
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "pge_111111_elec_usage_to_date"
|
assert entry.unique_id == "pge_111111_elec_usage_to_date"
|
||||||
state = hass.states.get("sensor.current_bill_electric_usage_to_date")
|
state = hass.states.get(
|
||||||
|
"sensor.elec_account_111111_current_bill_electric_usage_to_date"
|
||||||
|
)
|
||||||
assert state
|
assert state
|
||||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR
|
||||||
assert state.state == "100"
|
assert state.state == "100"
|
||||||
|
|
||||||
entry = entity_registry.async_get("sensor.current_bill_electric_cost_to_date")
|
entry = entity_registry.async_get(
|
||||||
|
"sensor.elec_account_111111_current_bill_electric_cost_to_date"
|
||||||
|
)
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "pge_111111_elec_cost_to_date"
|
assert entry.unique_id == "pge_111111_elec_cost_to_date"
|
||||||
state = hass.states.get("sensor.current_bill_electric_cost_to_date")
|
state = hass.states.get(
|
||||||
|
"sensor.elec_account_111111_current_bill_electric_cost_to_date"
|
||||||
|
)
|
||||||
assert state
|
assert state
|
||||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "USD"
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "USD"
|
||||||
assert state.state == "20.0"
|
assert state.state == "20.0"
|
||||||
|
|
||||||
# Check gas sensors
|
# Check gas sensors
|
||||||
entry = entity_registry.async_get("sensor.current_bill_gas_usage_to_date")
|
entry = entity_registry.async_get(
|
||||||
|
"sensor.gas_account_222222_current_bill_gas_usage_to_date"
|
||||||
|
)
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "pge_222222_gas_usage_to_date"
|
assert entry.unique_id == "pge_222222_gas_usage_to_date"
|
||||||
state = hass.states.get("sensor.current_bill_gas_usage_to_date")
|
state = hass.states.get("sensor.gas_account_222222_current_bill_gas_usage_to_date")
|
||||||
assert state
|
assert state
|
||||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS
|
||||||
# Convert 50 CCF to m³
|
# Convert 50 CCF to m³
|
||||||
assert float(state.state) == pytest.approx(50 * 2.83168, abs=1e-3)
|
assert float(state.state) == pytest.approx(50 * 2.83168, abs=1e-3)
|
||||||
|
|
||||||
entry = entity_registry.async_get("sensor.current_bill_gas_cost_to_date")
|
entry = entity_registry.async_get(
|
||||||
|
"sensor.gas_account_222222_current_bill_gas_cost_to_date"
|
||||||
|
)
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "pge_222222_gas_cost_to_date"
|
assert entry.unique_id == "pge_222222_gas_cost_to_date"
|
||||||
state = hass.states.get("sensor.current_bill_gas_cost_to_date")
|
state = hass.states.get("sensor.gas_account_222222_current_bill_gas_cost_to_date")
|
||||||
assert state
|
assert state
|
||||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "USD"
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "USD"
|
||||||
assert state.state == "15.0"
|
assert state.state == "15.0"
|
||||||
|
@ -1139,6 +1139,59 @@ async def test_selector_serializer(
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": True,
|
"additionalProperties": True,
|
||||||
}
|
}
|
||||||
|
assert selector_serializer(
|
||||||
|
selector.ObjectSelector(
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"name": {
|
||||||
|
"required": True,
|
||||||
|
"selector": {"text": {}},
|
||||||
|
},
|
||||||
|
"percentage": {
|
||||||
|
"selector": {"number": {"min": 30, "max": 100}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"multiple": False,
|
||||||
|
"label_field": "name",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
) == {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"percentage": {"type": "number", "minimum": 30, "maximum": 100},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert selector_serializer(
|
||||||
|
selector.ObjectSelector(
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"name": {
|
||||||
|
"required": True,
|
||||||
|
"selector": {"text": {}},
|
||||||
|
},
|
||||||
|
"percentage": {
|
||||||
|
"selector": {"number": {"min": 30, "max": 100}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"multiple": True,
|
||||||
|
"label_field": "name",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
) == {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"percentage": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 30,
|
||||||
|
"maximum": 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
assert selector_serializer(
|
assert selector_serializer(
|
||||||
selector.SelectSelector(
|
selector.SelectSelector(
|
||||||
{
|
{
|
||||||
|
@ -1143,10 +1143,10 @@ CUSTOM_ISSUE_TRACKER = "https://blablabla.com"
|
|||||||
("hue", "homeassistant.components.hue.sensor", CORE_ISSUE_TRACKER_HUE),
|
("hue", "homeassistant.components.hue.sensor", CORE_ISSUE_TRACKER_HUE),
|
||||||
("hue", None, CORE_ISSUE_TRACKER_HUE),
|
("hue", None, CORE_ISSUE_TRACKER_HUE),
|
||||||
("bla_built_in", None, CORE_ISSUE_TRACKER_BUILT_IN),
|
("bla_built_in", None, CORE_ISSUE_TRACKER_BUILT_IN),
|
||||||
# Integration domain is not currently deduced from module
|
(None, "homeassistant.components.hue.sensor", CORE_ISSUE_TRACKER_HUE),
|
||||||
(None, "homeassistant.components.hue.sensor", CORE_ISSUE_TRACKER),
|
|
||||||
("hue", "homeassistant.components.mqtt.sensor", CORE_ISSUE_TRACKER_HUE),
|
("hue", "homeassistant.components.mqtt.sensor", CORE_ISSUE_TRACKER_HUE),
|
||||||
# Loaded custom integration with known issue tracker
|
# Loaded custom integration with known issue tracker
|
||||||
|
(None, "custom_components.bla_custom.sensor", CUSTOM_ISSUE_TRACKER),
|
||||||
("bla_custom", "custom_components.bla_custom.sensor", CUSTOM_ISSUE_TRACKER),
|
("bla_custom", "custom_components.bla_custom.sensor", CUSTOM_ISSUE_TRACKER),
|
||||||
("bla_custom", None, CUSTOM_ISSUE_TRACKER),
|
("bla_custom", None, CUSTOM_ISSUE_TRACKER),
|
||||||
# Loaded custom integration without known issue tracker
|
# Loaded custom integration without known issue tracker
|
||||||
@ -1155,6 +1155,7 @@ CUSTOM_ISSUE_TRACKER = "https://blablabla.com"
|
|||||||
("bla_custom_no_tracker", None, None),
|
("bla_custom_no_tracker", None, None),
|
||||||
("hue", "custom_components.bla.sensor", None),
|
("hue", "custom_components.bla.sensor", None),
|
||||||
# Unloaded custom integration with known issue tracker
|
# Unloaded custom integration with known issue tracker
|
||||||
|
(None, "custom_components.bla_custom_not_loaded.sensor", CUSTOM_ISSUE_TRACKER),
|
||||||
("bla_custom_not_loaded", None, CUSTOM_ISSUE_TRACKER),
|
("bla_custom_not_loaded", None, CUSTOM_ISSUE_TRACKER),
|
||||||
# Unloaded custom integration without known issue tracker
|
# Unloaded custom integration without known issue tracker
|
||||||
("bla_custom_not_loaded_no_tracker", None, None),
|
("bla_custom_not_loaded_no_tracker", None, None),
|
||||||
@ -1218,8 +1219,7 @@ async def test_async_get_issue_tracker(
|
|||||||
("hue", "homeassistant.components.hue.sensor", CORE_ISSUE_TRACKER_HUE),
|
("hue", "homeassistant.components.hue.sensor", CORE_ISSUE_TRACKER_HUE),
|
||||||
("hue", None, CORE_ISSUE_TRACKER_HUE),
|
("hue", None, CORE_ISSUE_TRACKER_HUE),
|
||||||
("bla_built_in", None, CORE_ISSUE_TRACKER_BUILT_IN),
|
("bla_built_in", None, CORE_ISSUE_TRACKER_BUILT_IN),
|
||||||
# Integration domain is not currently deduced from module
|
(None, "homeassistant.components.hue.sensor", CORE_ISSUE_TRACKER_HUE),
|
||||||
(None, "homeassistant.components.hue.sensor", CORE_ISSUE_TRACKER),
|
|
||||||
("hue", "homeassistant.components.mqtt.sensor", CORE_ISSUE_TRACKER_HUE),
|
("hue", "homeassistant.components.mqtt.sensor", CORE_ISSUE_TRACKER_HUE),
|
||||||
# Custom integration with known issue tracker - can't find it without hass
|
# Custom integration with known issue tracker - can't find it without hass
|
||||||
("bla_custom", "custom_components.bla_custom.sensor", None),
|
("bla_custom", "custom_components.bla_custom.sensor", None),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user