mirror of
https://github.com/home-assistant/core.git
synced 2026-04-15 22:26:12 +02:00
Compare commits
1 Commits
remove_dep
...
drop-ignor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2361ef5aa |
4
.github/workflows/builder.yml
vendored
4
.github/workflows/builder.yml
vendored
@@ -47,6 +47,10 @@ jobs:
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Get information
|
||||
id: info
|
||||
uses: home-assistant/actions/helpers/info@5752577ea7cc5aefb064b0b21432f18fe4d6ba90 # zizmor: ignore[unpinned-uses]
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
uses: home-assistant/actions/helpers/version@master # zizmor: ignore[unpinned-uses]
|
||||
|
||||
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -709,7 +709,7 @@ jobs:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pylint --ignore-missing-annotations=y homeassistant
|
||||
pylint homeassistant
|
||||
- name: Run pylint (partially)
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
shell: bash
|
||||
@@ -718,7 +718,7 @@ jobs:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pylint --ignore-missing-annotations=y $(printf "homeassistant/components/%s " ${INTEGRATIONS_GLOB})
|
||||
pylint $(printf "homeassistant/components/%s " ${INTEGRATIONS_GLOB})
|
||||
|
||||
pylint-tests:
|
||||
name: Check pylint on tests
|
||||
|
||||
@@ -11,21 +11,10 @@ import threading
|
||||
|
||||
from .backup_restore import restore_backup
|
||||
from .const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
||||
from .util.package import is_docker_env, is_virtual_env
|
||||
|
||||
FAULT_LOG_FILENAME = "home-assistant.log.fault"
|
||||
|
||||
|
||||
def validate_environment() -> None:
|
||||
"""Validate that Home Assistant is started from a container or a venv."""
|
||||
if not is_virtual_env() and not is_docker_env():
|
||||
print(
|
||||
"Home Assistant must be run in a Python virtual environment or a container.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def validate_os() -> None:
|
||||
"""Validate that Home Assistant is running in a supported operating system."""
|
||||
if not sys.platform.startswith(("darwin", "linux")):
|
||||
@@ -51,6 +40,8 @@ def ensure_config_path(config_dir: str) -> None:
|
||||
"""Validate the configuration directory."""
|
||||
from . import config as config_util # noqa: PLC0415
|
||||
|
||||
lib_dir = os.path.join(config_dir, "deps")
|
||||
|
||||
# Test if configuration directory exists
|
||||
if not os.path.isdir(config_dir):
|
||||
if config_dir != config_util.get_default_config_dir():
|
||||
@@ -74,6 +65,17 @@ def ensure_config_path(config_dir: str) -> None:
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Test if library directory exists
|
||||
if not os.path.isdir(lib_dir):
|
||||
try:
|
||||
os.mkdir(lib_dir)
|
||||
except OSError as ex:
|
||||
print(
|
||||
f"Fatal Error: Unable to create library directory {lib_dir}: {ex}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_arguments() -> argparse.Namespace:
|
||||
"""Get parsed passed in arguments."""
|
||||
@@ -166,7 +168,6 @@ def check_threads() -> None:
|
||||
def main() -> int:
|
||||
"""Start Home Assistant."""
|
||||
validate_python()
|
||||
validate_environment()
|
||||
|
||||
args = get_arguments()
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ from .setup import (
|
||||
from .util.async_ import create_eager_task
|
||||
from .util.hass_dict import HassKey
|
||||
from .util.logging import async_activate_log_queue_handler
|
||||
from .util.package import is_docker_env
|
||||
from .util.package import async_get_user_site, is_docker_env, is_virtual_env
|
||||
from .util.system_info import is_official_image
|
||||
|
||||
with contextlib.suppress(ImportError):
|
||||
@@ -353,6 +353,9 @@ async def async_setup_hass(
|
||||
err,
|
||||
)
|
||||
else:
|
||||
if not is_virtual_env():
|
||||
await async_mount_local_lib_path(runtime_config.config_dir)
|
||||
|
||||
if hass.config.safe_mode:
|
||||
_LOGGER.info("Starting in safe mode")
|
||||
|
||||
@@ -701,6 +704,17 @@ class _RotatingFileHandlerWithoutShouldRollOver(RotatingFileHandler):
|
||||
return False
|
||||
|
||||
|
||||
async def async_mount_local_lib_path(config_dir: str) -> str:
|
||||
"""Add local library to Python Path.
|
||||
|
||||
This function is a coroutine.
|
||||
"""
|
||||
deps_dir = os.path.join(config_dir, "deps")
|
||||
if (lib_dir := await async_get_user_site(deps_dir)) not in sys.path:
|
||||
sys.path.insert(0, lib_dir)
|
||||
return deps_dir
|
||||
|
||||
|
||||
def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
|
||||
"""Get domains of components to set up."""
|
||||
# The common config section [homeassistant] could be filtered here,
|
||||
|
||||
@@ -36,15 +36,13 @@ class PromptCaching(StrEnum):
|
||||
AUTOMATIC = "automatic"
|
||||
|
||||
|
||||
MIN_THINKING_BUDGET = 1024
|
||||
|
||||
DEFAULT = {
|
||||
CONF_CHAT_MODEL: "claude-haiku-4-5",
|
||||
CONF_CODE_EXECUTION: False,
|
||||
CONF_MAX_TOKENS: 3000,
|
||||
CONF_PROMPT_CACHING: PromptCaching.PROMPT.value,
|
||||
CONF_TEMPERATURE: 1.0,
|
||||
CONF_THINKING_BUDGET: MIN_THINKING_BUDGET,
|
||||
CONF_THINKING_BUDGET: 0,
|
||||
CONF_THINKING_EFFORT: "low",
|
||||
CONF_TOOL_SEARCH: False,
|
||||
CONF_WEB_SEARCH: False,
|
||||
@@ -52,6 +50,8 @@ DEFAULT = {
|
||||
CONF_WEB_SEARCH_MAX_USES: 5,
|
||||
}
|
||||
|
||||
MIN_THINKING_BUDGET = 1024
|
||||
|
||||
NON_THINKING_MODELS = [
|
||||
"claude-3-haiku",
|
||||
]
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["cronsim==2.7", "securetar==2026.4.1"],
|
||||
"requirements": ["cronsim==2.7", "securetar==2026.2.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ from securetar import (
|
||||
SecureTarFile,
|
||||
SecureTarReadError,
|
||||
SecureTarRootKeyContext,
|
||||
get_archive_max_ciphertext_size,
|
||||
)
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -384,12 +383,9 @@ def _encrypt_backup(
|
||||
if prefix not in expected_archives:
|
||||
LOGGER.debug("Unknown inner tar file %s will not be encrypted", obj.name)
|
||||
continue
|
||||
if (fileobj := input_tar.extractfile(obj)) is None:
|
||||
LOGGER.debug(
|
||||
"Non regular inner tar file %s will not be encrypted", obj.name
|
||||
)
|
||||
continue
|
||||
output_archive.import_tar(fileobj, obj, derived_key_id=inner_tar_idx)
|
||||
output_archive.import_tar(
|
||||
input_tar.extractfile(obj), obj, derived_key_id=inner_tar_idx
|
||||
)
|
||||
inner_tar_idx += 1
|
||||
|
||||
|
||||
@@ -423,7 +419,7 @@ class _CipherBackupStreamer:
|
||||
hass: HomeAssistant,
|
||||
backup: AgentBackup,
|
||||
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
||||
password: str,
|
||||
password: str | None,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self._workers: list[_CipherWorkerStatus] = []
|
||||
@@ -435,9 +431,7 @@ class _CipherBackupStreamer:
|
||||
|
||||
def size(self) -> int:
|
||||
"""Return the maximum size of the decrypted or encrypted backup."""
|
||||
return get_archive_max_ciphertext_size(
|
||||
self._backup.size, SECURETAR_CREATE_VERSION, self._num_tar_files()
|
||||
)
|
||||
return self._backup.size + self._num_tar_files() * tarfile.RECORDSIZE
|
||||
|
||||
def _num_tar_files(self) -> int:
|
||||
"""Return the number of inner tar files."""
|
||||
|
||||
@@ -38,7 +38,6 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.TEXT,
|
||||
Platform.VALVE,
|
||||
]
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"entity": {
|
||||
"text": {
|
||||
"contour_name": {
|
||||
"default": "mdi:vector-polygon"
|
||||
},
|
||||
"position_name": {
|
||||
"default": "mdi:map-marker-radius"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,14 +154,6 @@
|
||||
"state": {
|
||||
"name": "[%key:common::state::open%]"
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"contour_name": {
|
||||
"name": "Contour {number}"
|
||||
},
|
||||
"position_name": {
|
||||
"name": "Position {number}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
"""Support for text entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from gardena_bluetooth.const import AquaContourContours, AquaContourPosition
|
||||
from gardena_bluetooth.parse import CharacteristicNullString
|
||||
|
||||
from homeassistant.components.text import TextEntity, TextEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import GardenaBluetoothConfigEntry
|
||||
from .entity import GardenaBluetoothDescriptorEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GardenaBluetoothTextEntityDescription(TextEntityDescription):
|
||||
"""Description of entity."""
|
||||
|
||||
char: CharacteristicNullString
|
||||
|
||||
@property
|
||||
def context(self) -> set[str]:
|
||||
"""Context needed for update coordinator."""
|
||||
return {self.char.uuid}
|
||||
|
||||
|
||||
DESCRIPTIONS = (
|
||||
*(
|
||||
GardenaBluetoothTextEntityDescription(
|
||||
key=f"position_{i}_name",
|
||||
translation_key="position_name",
|
||||
translation_placeholders={"number": str(i)},
|
||||
has_entity_name=True,
|
||||
char=getattr(AquaContourPosition, f"position_name_{i}"),
|
||||
native_max=20,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
for i in range(1, 6)
|
||||
),
|
||||
*(
|
||||
GardenaBluetoothTextEntityDescription(
|
||||
key=f"contour_{i}_name",
|
||||
translation_key="contour_name",
|
||||
translation_placeholders={"number": str(i)},
|
||||
has_entity_name=True,
|
||||
char=getattr(AquaContourContours, f"contour_name_{i}"),
|
||||
native_max=20,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
for i in range(1, 6)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: GardenaBluetoothConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up text based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
entities = [
|
||||
GardenaBluetoothTextEntity(coordinator, description, description.context)
|
||||
for description in DESCRIPTIONS
|
||||
if description.char.unique_id in coordinator.characteristics
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class GardenaBluetoothTextEntity(GardenaBluetoothDescriptorEntity, TextEntity):
|
||||
"""Representation of a text entity."""
|
||||
|
||||
entity_description: GardenaBluetoothTextEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""Return the value reported by the text."""
|
||||
char = self.entity_description.char
|
||||
return self.coordinator.get_cached(char)
|
||||
|
||||
async def async_set_value(self, value: str) -> None:
|
||||
"""Change the text."""
|
||||
char = self.entity_description.char
|
||||
await self.coordinator.write(char, value)
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.94", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.93", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -10,12 +10,10 @@ See https://modelcontextprotocol.io/docs/concepts/architecture#implementation-ex
|
||||
from collections.abc import Callable, Sequence
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from mcp import types
|
||||
from mcp.server import Server
|
||||
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
||||
from pydantic import AnyUrl
|
||||
import voluptuous as vol
|
||||
from voluptuous_openapi import convert
|
||||
|
||||
@@ -27,16 +25,6 @@ from .const import STATELESS_LLM_API
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SNAPSHOT_RESOURCE_URI = "homeassistant://assist/context-snapshot"
|
||||
SNAPSHOT_RESOURCE_URL = AnyUrl(SNAPSHOT_RESOURCE_URI)
|
||||
SNAPSHOT_RESOURCE_MIME_TYPE = "text/plain"
|
||||
LIVE_CONTEXT_TOOL_NAME = "GetLiveContext"
|
||||
|
||||
|
||||
def _has_live_context_tool(llm_api: llm.APIInstance) -> bool:
|
||||
"""Return if the selected API exposes the live context tool."""
|
||||
return any(tool.name == LIVE_CONTEXT_TOOL_NAME for tool in llm_api.tools)
|
||||
|
||||
|
||||
def _format_tool(
|
||||
tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None
|
||||
@@ -102,47 +90,6 @@ async def create_server(
|
||||
],
|
||||
)
|
||||
|
||||
@server.list_resources() # type: ignore[no-untyped-call,untyped-decorator]
|
||||
async def handle_list_resources() -> list[types.Resource]:
|
||||
llm_api = await get_api_instance()
|
||||
if not _has_live_context_tool(llm_api):
|
||||
return []
|
||||
|
||||
return [
|
||||
types.Resource(
|
||||
uri=SNAPSHOT_RESOURCE_URL,
|
||||
name="assist_context_snapshot",
|
||||
title="Assist context snapshot",
|
||||
description=(
|
||||
"A snapshot of the current Assist context, matching the"
|
||||
" existing GetLiveContext tool output."
|
||||
),
|
||||
mimeType=SNAPSHOT_RESOURCE_MIME_TYPE,
|
||||
)
|
||||
]
|
||||
|
||||
@server.read_resource() # type: ignore[no-untyped-call,untyped-decorator]
|
||||
async def handle_read_resource(uri: AnyUrl) -> Sequence[ReadResourceContents]:
|
||||
if str(uri) != SNAPSHOT_RESOURCE_URI:
|
||||
raise ValueError(f"Unknown resource: {uri}")
|
||||
|
||||
llm_api = await get_api_instance()
|
||||
if not _has_live_context_tool(llm_api):
|
||||
raise ValueError(f"Unknown resource: {uri}")
|
||||
|
||||
tool_response = await llm_api.async_call_tool(
|
||||
llm.ToolInput(tool_name=LIVE_CONTEXT_TOOL_NAME, tool_args={})
|
||||
)
|
||||
if not tool_response.get("success"):
|
||||
raise HomeAssistantError(cast(str, tool_response["error"]))
|
||||
|
||||
return [
|
||||
ReadResourceContents(
|
||||
content=cast(str, tool_response["result"]),
|
||||
mime_type=SNAPSHOT_RESOURCE_MIME_TYPE,
|
||||
)
|
||||
]
|
||||
|
||||
@server.list_tools() # type: ignore[no-untyped-call,untyped-decorator]
|
||||
async def list_tools() -> list[types.Tool]:
|
||||
"""List available time tools."""
|
||||
|
||||
@@ -49,11 +49,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BUTTON,
|
||||
Platform.MEDIA_PLAYER,
|
||||
Platform.NUMBER,
|
||||
]
|
||||
PLATFORMS = [Platform.BUTTON, Platform.MEDIA_PLAYER]
|
||||
|
||||
CONNECT_TIMEOUT = 10
|
||||
LISTEN_READY_TIMEOUT = 30
|
||||
|
||||
@@ -80,5 +80,3 @@ ATTR_FANART_IMAGE = "fanart_image"
|
||||
ATTR_CONF_EXPOSE_PLAYER_TO_HA = "expose_player_to_ha"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
PLAYER_OPTIONS_TRANSLATION_KEY_PREFIX = "player_options."
|
||||
|
||||
@@ -6,9 +6,8 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from music_assistant_models.enums import EventType
|
||||
from music_assistant_models.event import MassEvent
|
||||
from music_assistant_models.player import Player, PlayerOption
|
||||
from music_assistant_models.player import Player
|
||||
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
@@ -85,45 +84,3 @@ class MusicAssistantEntity(Entity):
|
||||
|
||||
async def async_on_update(self) -> None:
|
||||
"""Handle player updates."""
|
||||
|
||||
|
||||
class MusicAssistantPlayerOptionEntity(MusicAssistantEntity):
|
||||
"""Base entity for Music Assistant Player Options."""
|
||||
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
|
||||
def __init__(
|
||||
self, mass: MusicAssistantClient, player_id: str, player_option: PlayerOption
|
||||
) -> None:
|
||||
"""Initialize MusicAssistantPlayerOptionEntity."""
|
||||
super().__init__(mass, player_id)
|
||||
|
||||
self.mass_option_key = player_option.key
|
||||
self.mass_type = player_option.type
|
||||
|
||||
self.on_player_option_update(player_option)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
# need callbacks of parent to catch availability
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# main callback for player options
|
||||
self.async_on_remove(
|
||||
self.mass.subscribe(
|
||||
self.__on_mass_player_options_update,
|
||||
EventType.PLAYER_OPTIONS_UPDATED,
|
||||
self.player_id,
|
||||
)
|
||||
)
|
||||
|
||||
def __on_mass_player_options_update(self, event: MassEvent) -> None:
|
||||
"""Call when we receive an event from MusicAssistant."""
|
||||
for option in self.player.options:
|
||||
if option.key == self.mass_option_key:
|
||||
self.on_player_option_update(option)
|
||||
self.async_write_ha_state()
|
||||
break
|
||||
|
||||
def on_player_option_update(self, player_option: PlayerOption) -> None:
|
||||
"""Callback for player option updates."""
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
"""Music Assistant Number platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
from music_assistant_client.client import MusicAssistantClient
|
||||
from music_assistant_models.player import PlayerOption, PlayerOptionType
|
||||
|
||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import MusicAssistantConfigEntry
|
||||
from .const import PLAYER_OPTIONS_TRANSLATION_KEY_PREFIX
|
||||
from .entity import MusicAssistantPlayerOptionEntity
|
||||
from .helpers import catch_musicassistant_error
|
||||
|
||||
PLAYER_OPTIONS_TRANSLATION_KEYS_NUMBER: Final[list[str]] = [
|
||||
"bass",
|
||||
"dialogue_level",
|
||||
"dialogue_lift",
|
||||
"dts_dialogue_control",
|
||||
"equalizer_high",
|
||||
"equalizer_low",
|
||||
"equalizer_mid",
|
||||
"subwoofer_volume",
|
||||
"treble",
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: MusicAssistantConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Music Assistant Number Entities (Player Options) from Config Entry."""
|
||||
mass = entry.runtime_data.mass
|
||||
|
||||
def add_player(player_id: str) -> None:
|
||||
"""Handle add player."""
|
||||
player = mass.players.get(player_id)
|
||||
if player is None:
|
||||
return
|
||||
entities: list[MusicAssistantPlayerConfigNumber] = []
|
||||
for player_option in player.options:
|
||||
if (
|
||||
not player_option.read_only
|
||||
and player_option.type
|
||||
in (
|
||||
PlayerOptionType.INTEGER,
|
||||
PlayerOptionType.FLOAT,
|
||||
)
|
||||
and not player_option.options # these we map to select
|
||||
):
|
||||
# the MA translation key must have the format player_options.<translation key>
|
||||
# we ignore entities with unknown translation keys.
|
||||
if (
|
||||
player_option.translation_key is None
|
||||
or not player_option.translation_key.startswith(
|
||||
PLAYER_OPTIONS_TRANSLATION_KEY_PREFIX
|
||||
)
|
||||
):
|
||||
continue
|
||||
translation_key = player_option.translation_key[
|
||||
len(PLAYER_OPTIONS_TRANSLATION_KEY_PREFIX) :
|
||||
]
|
||||
if translation_key not in PLAYER_OPTIONS_TRANSLATION_KEYS_NUMBER:
|
||||
continue
|
||||
|
||||
entities.append(
|
||||
MusicAssistantPlayerConfigNumber(
|
||||
mass,
|
||||
player_id,
|
||||
player_option=player_option,
|
||||
entity_description=NumberEntityDescription(
|
||||
key=player_option.key,
|
||||
translation_key=translation_key,
|
||||
),
|
||||
)
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
# register callback to add players when they are discovered
|
||||
entry.runtime_data.platform_handlers.setdefault(Platform.NUMBER, add_player)
|
||||
|
||||
|
||||
class MusicAssistantPlayerConfigNumber(MusicAssistantPlayerOptionEntity, NumberEntity):
|
||||
"""Representation of a Number entity to control player provider dependent settings."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mass: MusicAssistantClient,
|
||||
player_id: str,
|
||||
player_option: PlayerOption,
|
||||
entity_description: NumberEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize MusicAssistantPlayerConfigNumber."""
|
||||
super().__init__(mass, player_id, player_option)
|
||||
|
||||
self.entity_description = entity_description
|
||||
|
||||
@catch_musicassistant_error
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set a new value."""
|
||||
_value = round(value) if self.mass_type == PlayerOptionType.INTEGER else value
|
||||
await self.mass.players.set_option(
|
||||
self.player_id,
|
||||
self.mass_option_key,
|
||||
_value,
|
||||
)
|
||||
|
||||
def on_player_option_update(self, player_option: PlayerOption) -> None:
|
||||
"""Update on player option update."""
|
||||
if player_option.min_value is not None:
|
||||
self._attr_native_min_value = player_option.min_value
|
||||
if player_option.max_value is not None:
|
||||
self._attr_native_max_value = player_option.max_value
|
||||
if player_option.step is not None:
|
||||
self._attr_native_step = player_option.step
|
||||
|
||||
self._attr_native_value = (
|
||||
player_option.value
|
||||
if isinstance(player_option.value, (int, float))
|
||||
else None
|
||||
)
|
||||
@@ -53,35 +53,6 @@
|
||||
"favorite_now_playing": {
|
||||
"name": "Favorite current song"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"bass": {
|
||||
"name": "Bass"
|
||||
},
|
||||
"dialogue_level": {
|
||||
"name": "Dialogue level"
|
||||
},
|
||||
"dialogue_lift": {
|
||||
"name": "Dialogue lift"
|
||||
},
|
||||
"dts_dialogue_control": {
|
||||
"name": "DTS dialogue control"
|
||||
},
|
||||
"equalizer_high": {
|
||||
"name": "Equalizer high"
|
||||
},
|
||||
"equalizer_low": {
|
||||
"name": "Equalizer low"
|
||||
},
|
||||
"equalizer_mid": {
|
||||
"name": "Equalizer mid"
|
||||
},
|
||||
"subwoofer_volume": {
|
||||
"name": "Subwoofer volume"
|
||||
},
|
||||
"treble": {
|
||||
"name": "Treble"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
|
||||
@@ -104,8 +104,12 @@ async def async_setup_entry(
|
||||
def _create_entity(device: dict) -> MyNeoSelect:
|
||||
"""Create a select entity for a device."""
|
||||
if device["model"] == "EWS":
|
||||
state = device.get("state") or {}
|
||||
if state.get("deviceType") == 0:
|
||||
# According to the MyNeomitis API, EWS "relais" devices expose a "relayMode"
|
||||
# field in their state, while "pilote" devices do not. We therefore use the
|
||||
# presence of "relayMode" as an explicit heuristic to distinguish relais
|
||||
# from pilote devices. If the upstream API changes this behavior, this
|
||||
# detection logic must be revisited.
|
||||
if "relayMode" in device.get("state", {}):
|
||||
description = SELECT_TYPES["relais"]
|
||||
else:
|
||||
description = SELECT_TYPES["pilote"]
|
||||
|
||||
@@ -168,7 +168,7 @@ class NumberDeviceClass(StrEnum):
|
||||
CURRENT = "current"
|
||||
"""Current.
|
||||
|
||||
Unit of measurement: `A`, `mA`, `μA`
|
||||
Unit of measurement: `A`, `mA`
|
||||
"""
|
||||
|
||||
DATA_RATE = "data_rate"
|
||||
|
||||
@@ -46,7 +46,6 @@ from .const import (
|
||||
CONF_MAX_TOKENS,
|
||||
CONF_PROMPT,
|
||||
CONF_REASONING_EFFORT,
|
||||
CONF_STORE_RESPONSES,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TOP_P,
|
||||
DEFAULT_AI_TASK_NAME,
|
||||
@@ -59,7 +58,6 @@ from .const import (
|
||||
RECOMMENDED_CHAT_MODEL,
|
||||
RECOMMENDED_MAX_TOKENS,
|
||||
RECOMMENDED_REASONING_EFFORT,
|
||||
RECOMMENDED_STORE_RESPONSES,
|
||||
RECOMMENDED_STT_OPTIONS,
|
||||
RECOMMENDED_TEMPERATURE,
|
||||
RECOMMENDED_TOP_P,
|
||||
@@ -210,9 +208,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
|
||||
),
|
||||
"user": call.context.user_id,
|
||||
"store": conversation_subentry.data.get(
|
||||
CONF_STORE_RESPONSES, RECOMMENDED_STORE_RESPONSES
|
||||
),
|
||||
"store": False,
|
||||
}
|
||||
|
||||
if model.startswith("o"):
|
||||
|
||||
@@ -55,7 +55,6 @@ from .const import (
|
||||
CONF_REASONING_SUMMARY,
|
||||
CONF_RECOMMENDED,
|
||||
CONF_SERVICE_TIER,
|
||||
CONF_STORE_RESPONSES,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TOP_P,
|
||||
CONF_TTS_SPEED,
|
||||
@@ -83,7 +82,6 @@ from .const import (
|
||||
RECOMMENDED_REASONING_EFFORT,
|
||||
RECOMMENDED_REASONING_SUMMARY,
|
||||
RECOMMENDED_SERVICE_TIER,
|
||||
RECOMMENDED_STORE_RESPONSES,
|
||||
RECOMMENDED_STT_MODEL,
|
||||
RECOMMENDED_STT_OPTIONS,
|
||||
RECOMMENDED_TEMPERATURE,
|
||||
@@ -359,10 +357,6 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
||||
CONF_TEMPERATURE,
|
||||
default=RECOMMENDED_TEMPERATURE,
|
||||
): NumberSelector(NumberSelectorConfig(min=0, max=2, step=0.05)),
|
||||
vol.Optional(
|
||||
CONF_STORE_RESPONSES,
|
||||
default=RECOMMENDED_STORE_RESPONSES,
|
||||
): bool,
|
||||
}
|
||||
|
||||
if user_input is not None:
|
||||
@@ -647,9 +641,7 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
||||
"strict": False,
|
||||
}
|
||||
},
|
||||
store=self.options.get(
|
||||
CONF_STORE_RESPONSES, RECOMMENDED_STORE_RESPONSES
|
||||
),
|
||||
store=False,
|
||||
)
|
||||
location_data = location_schema(json.loads(response.output_text) or {})
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ CONF_PROMPT = "prompt"
|
||||
CONF_REASONING_EFFORT = "reasoning_effort"
|
||||
CONF_REASONING_SUMMARY = "reasoning_summary"
|
||||
CONF_RECOMMENDED = "recommended"
|
||||
CONF_STORE_RESPONSES = "store_responses"
|
||||
CONF_SERVICE_TIER = "service_tier"
|
||||
CONF_TEMPERATURE = "temperature"
|
||||
CONF_TOP_P = "top_p"
|
||||
@@ -43,7 +42,6 @@ RECOMMENDED_CHAT_MODEL = "gpt-4o-mini"
|
||||
RECOMMENDED_IMAGE_MODEL = "gpt-image-1.5"
|
||||
RECOMMENDED_MAX_TOKENS = 3000
|
||||
RECOMMENDED_REASONING_EFFORT = "low"
|
||||
RECOMMENDED_STORE_RESPONSES = False
|
||||
RECOMMENDED_REASONING_SUMMARY = "auto"
|
||||
RECOMMENDED_SERVICE_TIER = "auto"
|
||||
RECOMMENDED_STT_MODEL = "gpt-4o-mini-transcribe"
|
||||
|
||||
@@ -75,7 +75,6 @@ from .const import (
|
||||
CONF_REASONING_EFFORT,
|
||||
CONF_REASONING_SUMMARY,
|
||||
CONF_SERVICE_TIER,
|
||||
CONF_STORE_RESPONSES,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TOP_P,
|
||||
CONF_VERBOSITY,
|
||||
@@ -95,7 +94,6 @@ from .const import (
|
||||
RECOMMENDED_REASONING_EFFORT,
|
||||
RECOMMENDED_REASONING_SUMMARY,
|
||||
RECOMMENDED_SERVICE_TIER,
|
||||
RECOMMENDED_STORE_RESPONSES,
|
||||
RECOMMENDED_STT_MODEL,
|
||||
RECOMMENDED_TEMPERATURE,
|
||||
RECOMMENDED_TOP_P,
|
||||
@@ -510,7 +508,7 @@ class OpenAIBaseLLMEntity(Entity):
|
||||
max_output_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
|
||||
user=chat_log.conversation_id,
|
||||
service_tier=options.get(CONF_SERVICE_TIER, RECOMMENDED_SERVICE_TIER),
|
||||
store=options.get(CONF_STORE_RESPONSES, RECOMMENDED_STORE_RESPONSES),
|
||||
store=False,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
@@ -613,10 +611,8 @@ class OpenAIBaseLLMEntity(Entity):
|
||||
if image_model != "gpt-image-1-mini":
|
||||
image_tool["input_fidelity"] = "high"
|
||||
tools.append(image_tool)
|
||||
# Keep image state on OpenAI so follow-up prompts can continue by
|
||||
# conversation ID without resending the generated image data.
|
||||
model_args["store"] = True
|
||||
model_args["tool_choice"] = ToolChoiceTypesParam(type="image_generation")
|
||||
model_args["store"] = True # Avoid sending image data back and forth
|
||||
|
||||
if tools:
|
||||
model_args["tools"] = tools
|
||||
|
||||
@@ -51,13 +51,9 @@
|
||||
"data": {
|
||||
"chat_model": "[%key:common::generic::model%]",
|
||||
"max_tokens": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data::max_tokens%]",
|
||||
"store_responses": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data::store_responses%]",
|
||||
"temperature": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data::temperature%]",
|
||||
"top_p": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data::top_p%]"
|
||||
},
|
||||
"data_description": {
|
||||
"store_responses": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data_description::store_responses%]"
|
||||
},
|
||||
"title": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::title%]"
|
||||
},
|
||||
"init": {
|
||||
@@ -113,13 +109,9 @@
|
||||
"data": {
|
||||
"chat_model": "[%key:common::generic::model%]",
|
||||
"max_tokens": "Maximum tokens to return in response",
|
||||
"store_responses": "Store requests and responses in OpenAI",
|
||||
"temperature": "Temperature",
|
||||
"top_p": "Top P"
|
||||
},
|
||||
"data_description": {
|
||||
"store_responses": "If enabled, requests and responses are stored by OpenAI and visible in your OpenAI dashboard logs"
|
||||
},
|
||||
"title": "Advanced settings"
|
||||
},
|
||||
"init": {
|
||||
|
||||
@@ -214,19 +214,19 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
|
||||
else None,
|
||||
)
|
||||
|
||||
# Separately fetch stats for active containers
|
||||
active_containers = [
|
||||
# Separately fetch stats for running containers
|
||||
running_containers = [
|
||||
container
|
||||
for container in containers
|
||||
if container.state
|
||||
in (DockerContainerState.RUNNING, DockerContainerState.PAUSED)
|
||||
]
|
||||
if active_containers:
|
||||
if running_containers:
|
||||
container_stats = dict(
|
||||
zip(
|
||||
(
|
||||
self._get_container_name(container.names[0])
|
||||
for container in active_containers
|
||||
for container in running_containers
|
||||
),
|
||||
await asyncio.gather(
|
||||
*(
|
||||
@@ -234,7 +234,7 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
|
||||
endpoint_id=endpoint.id,
|
||||
container_id=container.id,
|
||||
)
|
||||
for container in active_containers
|
||||
for container in running_containers
|
||||
)
|
||||
),
|
||||
strict=False,
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyportainer==1.0.38"]
|
||||
"requirements": ["pyportainer==1.0.37"]
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
},
|
||||
"services": {
|
||||
"delete_command": {
|
||||
"description": "Deletes a command or a list of commands from a remote's database.",
|
||||
"description": "Deletes a command or a list of commands from the database.",
|
||||
"fields": {
|
||||
"command": {
|
||||
"description": "The single command or the list of commands to be deleted.",
|
||||
@@ -52,10 +52,10 @@
|
||||
"name": "Device"
|
||||
}
|
||||
},
|
||||
"name": "Delete remote command"
|
||||
"name": "Delete command"
|
||||
},
|
||||
"learn_command": {
|
||||
"description": "Teaches a remote a command or list of commands from a device.",
|
||||
"description": "Learns a command or a list of commands from a device.",
|
||||
"fields": {
|
||||
"alternative": {
|
||||
"description": "If code must be stored as an alternative. This is useful for discrete codes. Discrete codes are used for toggles that only perform one function. For example, a code to only turn a device on. If it is on already, sending the code won't change the state.",
|
||||
@@ -78,7 +78,7 @@
|
||||
"name": "Timeout"
|
||||
}
|
||||
},
|
||||
"name": "Learn remote command"
|
||||
"name": "Learn command"
|
||||
},
|
||||
"send_command": {
|
||||
"description": "Sends a command or a list of commands to a device.",
|
||||
@@ -104,15 +104,15 @@
|
||||
"name": "Repeats"
|
||||
}
|
||||
},
|
||||
"name": "Send remote command"
|
||||
"name": "Send command"
|
||||
},
|
||||
"toggle": {
|
||||
"description": "Sends the toggle command.",
|
||||
"name": "Toggle via remote"
|
||||
"name": "[%key:common::action::toggle%]"
|
||||
},
|
||||
"turn_off": {
|
||||
"description": "Sends the turn off command.",
|
||||
"name": "Turn off via remote"
|
||||
"name": "[%key:common::action::turn_off%]"
|
||||
},
|
||||
"turn_on": {
|
||||
"description": "Sends the turn on command.",
|
||||
@@ -122,7 +122,7 @@
|
||||
"name": "Activity"
|
||||
}
|
||||
},
|
||||
"name": "Turn on via remote"
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
}
|
||||
},
|
||||
"title": "Remote",
|
||||
|
||||
@@ -20,7 +20,6 @@ from roborock.data import (
|
||||
ZeoSpin,
|
||||
ZeoTemperature,
|
||||
)
|
||||
from roborock.data.b01_q10.b01_q10_code_mappings import YXCleanType
|
||||
from roborock.devices.traits.b01 import Q7PropertiesApi
|
||||
from roborock.devices.traits.v1 import PropertiesApi
|
||||
from roborock.devices.traits.v1.home import HomeTrait
|
||||
@@ -38,7 +37,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from .const import DOMAIN, MAP_SLEEP
|
||||
from .coordinator import (
|
||||
RoborockB01Q7UpdateCoordinator,
|
||||
RoborockB01Q10UpdateCoordinator,
|
||||
RoborockConfigEntry,
|
||||
RoborockDataUpdateCoordinator,
|
||||
RoborockDataUpdateCoordinatorA01,
|
||||
@@ -46,7 +44,6 @@ from .coordinator import (
|
||||
from .entity import (
|
||||
RoborockCoordinatedEntityA01,
|
||||
RoborockCoordinatedEntityB01Q7,
|
||||
RoborockCoordinatedEntityB01Q10,
|
||||
RoborockCoordinatedEntityV1,
|
||||
)
|
||||
|
||||
@@ -269,10 +266,6 @@ async def async_setup_entry(
|
||||
for description in A01_SELECT_DESCRIPTIONS
|
||||
if description.data_protocol in coordinator.request_protocols
|
||||
)
|
||||
async_add_entities(
|
||||
RoborockQ10CleanModeSelectEntity(coordinator)
|
||||
for coordinator in config_entry.runtime_data.b01_q10
|
||||
)
|
||||
|
||||
|
||||
class RoborockB01SelectEntity(RoborockCoordinatedEntityB01Q7, SelectEntity):
|
||||
@@ -473,59 +466,3 @@ class RoborockSelectEntityA01(RoborockCoordinatedEntityA01, SelectEntity):
|
||||
self.entity_description.key,
|
||||
)
|
||||
return str(current_value)
|
||||
|
||||
|
||||
class RoborockQ10CleanModeSelectEntity(RoborockCoordinatedEntityB01Q10, SelectEntity):
|
||||
"""Select entity for Q10 cleaning mode."""
|
||||
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_translation_key = "cleaning_mode"
|
||||
coordinator: RoborockB01Q10UpdateCoordinator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: RoborockB01Q10UpdateCoordinator,
|
||||
) -> None:
|
||||
"""Create a select entity for Q10 cleaning mode."""
|
||||
super().__init__(
|
||||
f"cleaning_mode_{coordinator.duid_slug}",
|
||||
coordinator,
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register trait listener for push-based status updates."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
self.coordinator.api.status.add_update_listener(self.async_write_ha_state)
|
||||
)
|
||||
|
||||
@property
|
||||
def options(self) -> list[str]:
|
||||
"""Return available cleaning modes."""
|
||||
return [mode.value for mode in YXCleanType if mode != YXCleanType.UNKNOWN]
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Get the current cleaning mode."""
|
||||
clean_mode = self.coordinator.api.status.clean_mode
|
||||
if clean_mode is None or clean_mode == YXCleanType.UNKNOWN:
|
||||
return None
|
||||
return clean_mode.value
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Set the cleaning mode."""
|
||||
try:
|
||||
mode = YXCleanType.from_value(option)
|
||||
except ValueError as err:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="select_option_failed",
|
||||
) from err
|
||||
try:
|
||||
await self.coordinator.api.vacuum.set_clean_mode(mode)
|
||||
except RoborockException as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_failed",
|
||||
translation_placeholders={"command": "cleaning_mode"},
|
||||
) from err
|
||||
|
||||
@@ -180,7 +180,7 @@ class SensorDeviceClass(StrEnum):
|
||||
CURRENT = "current"
|
||||
"""Current.
|
||||
|
||||
Unit of measurement: `A`, `mA`, `μA`
|
||||
Unit of measurement: `A`, `mA`
|
||||
"""
|
||||
|
||||
DATA_RATE = "data_rate"
|
||||
|
||||
@@ -110,26 +110,10 @@ PLATFORMS_BY_TYPE = {
|
||||
Platform.LOCK,
|
||||
Platform.SENSOR,
|
||||
],
|
||||
SupportedModels.AIR_PURIFIER_JP.value: [
|
||||
Platform.FAN,
|
||||
Platform.SENSOR,
|
||||
Platform.BUTTON,
|
||||
],
|
||||
SupportedModels.AIR_PURIFIER_US.value: [
|
||||
Platform.FAN,
|
||||
Platform.SENSOR,
|
||||
Platform.BUTTON,
|
||||
],
|
||||
SupportedModels.AIR_PURIFIER_TABLE_JP.value: [
|
||||
Platform.FAN,
|
||||
Platform.SENSOR,
|
||||
Platform.BUTTON,
|
||||
],
|
||||
SupportedModels.AIR_PURIFIER_TABLE_US.value: [
|
||||
Platform.FAN,
|
||||
Platform.SENSOR,
|
||||
Platform.BUTTON,
|
||||
],
|
||||
SupportedModels.AIR_PURIFIER_JP.value: [Platform.FAN, Platform.SENSOR],
|
||||
SupportedModels.AIR_PURIFIER_US.value: [Platform.FAN, Platform.SENSOR],
|
||||
SupportedModels.AIR_PURIFIER_TABLE_JP.value: [Platform.FAN, Platform.SENSOR],
|
||||
SupportedModels.AIR_PURIFIER_TABLE_US.value: [Platform.FAN, Platform.SENSOR],
|
||||
SupportedModels.EVAPORATIVE_HUMIDIFIER.value: [
|
||||
Platform.HUMIDIFIER,
|
||||
Platform.SENSOR,
|
||||
|
||||
@@ -24,8 +24,6 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Switchbot button platform."""
|
||||
coordinator = entry.runtime_data
|
||||
if isinstance(coordinator.device, switchbot.SwitchbotAirPurifier):
|
||||
async_add_entities([LightSensorButton(coordinator)])
|
||||
|
||||
if isinstance(coordinator.device, switchbot.SwitchbotArtFrame):
|
||||
async_add_entities(
|
||||
@@ -39,24 +37,6 @@ async def async_setup_entry(
|
||||
async_add_entities([SwitchBotMeterProCO2SyncDateTimeButton(coordinator)])
|
||||
|
||||
|
||||
class LightSensorButton(SwitchbotEntity, ButtonEntity):
|
||||
"""Representation of a Switchbot light sensor button."""
|
||||
|
||||
_attr_translation_key = "light_sensor"
|
||||
_device: switchbot.SwitchbotAirPurifier
|
||||
|
||||
def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None:
|
||||
"""Initialize the Switchbot light sensor button."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{coordinator.base_unique_id}_light_sensor"
|
||||
|
||||
@exception_handler
|
||||
async def async_press(self) -> None:
|
||||
"""Handle the button press."""
|
||||
_LOGGER.debug("Toggling light sensor mode for %s", self._address)
|
||||
await self._device.open_light_sensitive_switch()
|
||||
|
||||
|
||||
class SwitchBotArtFrameButtonBase(SwitchbotEntity, ButtonEntity):
|
||||
"""Base class for Art Frame buttons."""
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"entity": {
|
||||
"button": {
|
||||
"light_sensor": {
|
||||
"default": "mdi:brightness-auto"
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"climate": {
|
||||
"state_attributes": {
|
||||
|
||||
@@ -102,9 +102,6 @@
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"light_sensor": {
|
||||
"name": "Light sensor"
|
||||
},
|
||||
"next_image": {
|
||||
"name": "Next image"
|
||||
},
|
||||
|
||||
@@ -246,7 +246,6 @@ class TractiveClient:
|
||||
):
|
||||
self._last_hw_time = event["hardware"]["time"]
|
||||
self._send_hardware_update(event)
|
||||
self._send_switch_update(event)
|
||||
if (
|
||||
"position" in event
|
||||
and self._last_pos_time != event["position"]["time"]
|
||||
@@ -303,10 +302,7 @@ class TractiveClient:
|
||||
for switch, key in SWITCH_KEY_MAP.items():
|
||||
if switch_data := event.get(key):
|
||||
payload[switch] = switch_data["active"]
|
||||
if hardware := event.get("hardware", {}):
|
||||
payload[ATTR_POWER_SAVING] = (
|
||||
hardware.get("power_saving_zone_id") is not None
|
||||
)
|
||||
payload[ATTR_POWER_SAVING] = event.get("tracker_state_reason") == "POWER_SAVING"
|
||||
self._dispatch_tracker_event(
|
||||
TRACKER_SWITCH_STATUS_UPDATED, event["tracker_id"], payload
|
||||
)
|
||||
|
||||
@@ -100,11 +100,13 @@ class TractiveSwitch(TractiveEntity, SwitchEntity):
|
||||
@callback
|
||||
def handle_status_update(self, event: dict[str, Any]) -> None:
|
||||
"""Handle status update."""
|
||||
if ATTR_POWER_SAVING in event:
|
||||
self._attr_available = not event[ATTR_POWER_SAVING]
|
||||
if self.entity_description.key not in event:
|
||||
return
|
||||
|
||||
if self.entity_description.key in event:
|
||||
self._attr_is_on = event[self.entity_description.key]
|
||||
# We received an event, so the service is online and the switch entities should
|
||||
# be available.
|
||||
self._attr_available = not event[ATTR_POWER_SAVING]
|
||||
self._attr_is_on = event[self.entity_description.key]
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["unifi_access_api"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["py-unifi-access==1.1.5"]
|
||||
"requirements": ["py-unifi-access==1.1.3"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["holidays"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["holidays==0.94"]
|
||||
"requirements": ["holidays==0.93"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["zcc-helper==3.8"]
|
||||
"requirements": ["zcc-helper==3.7"]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import logging
|
||||
import operator
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from types import ModuleType
|
||||
from typing import TYPE_CHECKING, Any, Literal, overload
|
||||
|
||||
@@ -31,6 +32,7 @@ from .helpers.typing import ConfigType
|
||||
from .loader import ComponentProtocol, Integration, IntegrationNotFound
|
||||
from .requirements import RequirementsNotFound, async_get_integration_with_requirements
|
||||
from .util.async_ import create_eager_task
|
||||
from .util.package import is_docker_env
|
||||
from .util.yaml import SECRET_YAML, Secrets, YamlTypeError, load_yaml_dict
|
||||
from .util.yaml.objects import NodeStrClass
|
||||
|
||||
@@ -306,6 +308,12 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None:
|
||||
|
||||
version_obj = AwesomeVersion(conf_version)
|
||||
|
||||
if version_obj < AwesomeVersion("0.50"):
|
||||
# 0.50 introduced persistent deps dir.
|
||||
lib_path = hass.config.path("deps")
|
||||
if os.path.isdir(lib_path):
|
||||
shutil.rmtree(lib_path)
|
||||
|
||||
if version_obj < AwesomeVersion("0.92"):
|
||||
# 0.92 moved google/tts.py to google_translate/tts.py
|
||||
config_path = hass.config.path(YAML_CONFIG_FILE)
|
||||
@@ -322,6 +330,13 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None:
|
||||
except OSError:
|
||||
_LOGGER.exception("Migrating to google_translate tts failed")
|
||||
|
||||
if version_obj < AwesomeVersion("0.94") and is_docker_env():
|
||||
# In 0.94 we no longer install packages inside the deps folder when
|
||||
# running inside a Docker container.
|
||||
lib_path = hass.config.path("deps")
|
||||
if os.path.isdir(lib_path):
|
||||
shutil.rmtree(lib_path)
|
||||
|
||||
with open(version_path, "w", encoding="utf8") as outp:
|
||||
outp.write(__version__)
|
||||
|
||||
|
||||
@@ -579,13 +579,6 @@ class ConfigEntry[_DataT = Any]:
|
||||
self.clear_state_cache()
|
||||
self.clear_storage_cache()
|
||||
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
"""Return logger for this config entry."""
|
||||
if self._integration_for_domain:
|
||||
return self._integration_for_domain.logger
|
||||
return _LOGGER
|
||||
|
||||
@property
|
||||
def supports_options(self) -> bool:
|
||||
"""Return if entry supports config options."""
|
||||
@@ -705,9 +698,6 @@ class ConfigEntry[_DataT = Any]:
|
||||
integration = await loader.async_get_integration(hass, self.domain)
|
||||
self._integration_for_domain = integration
|
||||
|
||||
# Log setup to the integration logger so it's visible when debug logs are enabled.
|
||||
logger = self.logger
|
||||
|
||||
# Only store setup result as state if it was not forwarded.
|
||||
if domain_is_integration := self.domain == integration.domain:
|
||||
if self.state in (
|
||||
@@ -736,7 +726,7 @@ class ConfigEntry[_DataT = Any]:
|
||||
try:
|
||||
component = await integration.async_get_component()
|
||||
except ImportError as err:
|
||||
logger.error(
|
||||
_LOGGER.error(
|
||||
"Error importing integration %s to set up %s configuration entry: %s",
|
||||
integration.domain,
|
||||
self.domain,
|
||||
@@ -752,7 +742,7 @@ class ConfigEntry[_DataT = Any]:
|
||||
try:
|
||||
await integration.async_get_platform("config_flow")
|
||||
except ImportError as err:
|
||||
logger.error(
|
||||
_LOGGER.error(
|
||||
(
|
||||
"Error importing platform config_flow from integration %s to"
|
||||
" set up %s configuration entry: %s"
|
||||
@@ -787,7 +777,7 @@ class ConfigEntry[_DataT = Any]:
|
||||
result = await component.async_setup_entry(hass, self)
|
||||
|
||||
if not isinstance(result, bool):
|
||||
logger.error( # type: ignore[unreachable]
|
||||
_LOGGER.error( # type: ignore[unreachable]
|
||||
"%s.async_setup_entry did not return boolean", integration.domain
|
||||
)
|
||||
result = False
|
||||
@@ -795,7 +785,7 @@ class ConfigEntry[_DataT = Any]:
|
||||
error_reason = str(exc) or "Unknown fatal config entry error"
|
||||
error_reason_translation_key = exc.translation_key
|
||||
error_reason_translation_placeholders = exc.translation_placeholders
|
||||
logger.exception(
|
||||
_LOGGER.exception(
|
||||
"Error setting up entry %s for %s: %s",
|
||||
self.title,
|
||||
self.domain,
|
||||
@@ -810,13 +800,13 @@ class ConfigEntry[_DataT = Any]:
|
||||
auth_message = (
|
||||
f"{auth_base_message}: {message}" if message else auth_base_message
|
||||
)
|
||||
logger.warning(
|
||||
_LOGGER.warning(
|
||||
"Config entry '%s' for %s integration %s",
|
||||
self.title,
|
||||
self.domain,
|
||||
auth_message,
|
||||
)
|
||||
logger.debug("Full exception", exc_info=True)
|
||||
_LOGGER.debug("Full exception", exc_info=True)
|
||||
self.async_start_reauth(hass)
|
||||
except ConfigEntryNotReady as exc:
|
||||
message = str(exc)
|
||||
@@ -834,14 +824,14 @@ class ConfigEntry[_DataT = Any]:
|
||||
)
|
||||
self._tries += 1
|
||||
ready_message = f"ready yet: {message}" if message else "ready yet"
|
||||
logger.info(
|
||||
_LOGGER.info(
|
||||
"Config entry '%s' for %s integration not %s; Retrying in %d seconds",
|
||||
self.title,
|
||||
self.domain,
|
||||
ready_message,
|
||||
wait_time,
|
||||
)
|
||||
logger.debug("Full exception", exc_info=True)
|
||||
_LOGGER.debug("Full exception", exc_info=True)
|
||||
|
||||
if hass.state is CoreState.running:
|
||||
self._async_cancel_retry_setup = async_call_later(
|
||||
@@ -864,7 +854,7 @@ class ConfigEntry[_DataT = Any]:
|
||||
except asyncio.CancelledError:
|
||||
# We want to propagate CancelledError if we are being cancelled.
|
||||
if (task := asyncio.current_task()) and task.cancelling() > 0:
|
||||
logger.exception(
|
||||
_LOGGER.exception(
|
||||
"Setup of config entry '%s' for %s integration cancelled",
|
||||
self.title,
|
||||
self.domain,
|
||||
@@ -879,13 +869,13 @@ class ConfigEntry[_DataT = Any]:
|
||||
raise
|
||||
|
||||
# This was not a "real" cancellation, log it and treat as a normal error.
|
||||
logger.exception(
|
||||
_LOGGER.exception(
|
||||
"Error setting up entry %s for %s", self.title, integration.domain
|
||||
)
|
||||
|
||||
# pylint: disable-next=broad-except
|
||||
except SystemExit, Exception:
|
||||
logger.exception(
|
||||
_LOGGER.exception(
|
||||
"Error setting up entry %s for %s", self.title, integration.domain
|
||||
)
|
||||
|
||||
@@ -1039,7 +1029,7 @@ class ConfigEntry[_DataT = Any]:
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
self.logger.exception(
|
||||
_LOGGER.exception(
|
||||
"Error unloading entry %s for %s", self.title, integration.domain
|
||||
)
|
||||
if domain_is_integration:
|
||||
@@ -1082,7 +1072,7 @@ class ConfigEntry[_DataT = Any]:
|
||||
try:
|
||||
await component.async_remove_entry(hass, self)
|
||||
except Exception:
|
||||
self.logger.exception(
|
||||
_LOGGER.exception(
|
||||
"Error calling entry remove callback %s for %s",
|
||||
self.title,
|
||||
integration.domain,
|
||||
@@ -1127,7 +1117,7 @@ class ConfigEntry[_DataT = Any]:
|
||||
Returns True if config entry is up-to-date or has been migrated.
|
||||
"""
|
||||
if (handler := HANDLERS.get(self.domain)) is None:
|
||||
self.logger.error(
|
||||
_LOGGER.error(
|
||||
"Flow handler not found for entry %s for %s", self.title, self.domain
|
||||
)
|
||||
return False
|
||||
@@ -1148,7 +1138,7 @@ class ConfigEntry[_DataT = Any]:
|
||||
if not supports_migrate:
|
||||
if same_major_version:
|
||||
return True
|
||||
self.logger.error(
|
||||
_LOGGER.error(
|
||||
"Migration handler not found for entry %s for %s",
|
||||
self.title,
|
||||
self.domain,
|
||||
@@ -1158,14 +1148,14 @@ class ConfigEntry[_DataT = Any]:
|
||||
try:
|
||||
result = await component.async_migrate_entry(hass, self)
|
||||
if not isinstance(result, bool):
|
||||
self.logger.error( # type: ignore[unreachable]
|
||||
_LOGGER.error( # type: ignore[unreachable]
|
||||
"%s.async_migrate_entry did not return boolean", self.domain
|
||||
)
|
||||
return False
|
||||
if result:
|
||||
hass.config_entries._async_schedule_save() # noqa: SLF001
|
||||
except Exception:
|
||||
self.logger.exception(
|
||||
_LOGGER.exception(
|
||||
"Error migrating entry %s for %s", self.title, self.domain
|
||||
)
|
||||
return False
|
||||
@@ -1228,7 +1218,7 @@ class ConfigEntry[_DataT = Any]:
|
||||
)
|
||||
|
||||
for task in pending:
|
||||
self.logger.warning(
|
||||
_LOGGER.warning(
|
||||
"Unloading %s (%s) config entry. Task %s did not complete in time",
|
||||
self.title,
|
||||
self.domain,
|
||||
@@ -1257,7 +1247,7 @@ class ConfigEntry[_DataT = Any]:
|
||||
try:
|
||||
func()
|
||||
except Exception:
|
||||
self.logger.exception(
|
||||
_LOGGER.exception(
|
||||
"Error calling on_state_change callback for %s (%s)",
|
||||
self.title,
|
||||
self.domain,
|
||||
@@ -1646,7 +1636,7 @@ class ConfigEntriesFlowManager(
|
||||
)
|
||||
}
|
||||
)
|
||||
entry.logger.debug(
|
||||
_LOGGER.debug(
|
||||
"Updating discovery keys for %s entry %s %s -> %s",
|
||||
entry.domain,
|
||||
unique_id,
|
||||
@@ -1879,7 +1869,7 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]):
|
||||
if entry_id in data:
|
||||
# This is likely a bug in a test that is adding the same entry twice.
|
||||
# In the future, once we have fixed the tests, this will raise HomeAssistantError.
|
||||
entry.logger.error("An entry with the id %s already exists", entry_id)
|
||||
_LOGGER.error("An entry with the id %s already exists", entry_id)
|
||||
self._unindex_entry(entry_id)
|
||||
data[entry_id] = entry
|
||||
self._index_entry(entry)
|
||||
@@ -1902,7 +1892,7 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]):
|
||||
report_issue = async_suggest_report_issue(
|
||||
self._hass, integration_domain=entry.domain
|
||||
)
|
||||
entry.logger.error(
|
||||
_LOGGER.error(
|
||||
(
|
||||
"Config entry '%s' from integration %s has an invalid unique_id"
|
||||
" '%s' of type %s when a string is expected, please %s"
|
||||
@@ -2292,7 +2282,7 @@ class ConfigEntries:
|
||||
try:
|
||||
await loader.async_get_integration(self.hass, entry.domain)
|
||||
except loader.IntegrationNotFound:
|
||||
entry.logger.info(
|
||||
_LOGGER.info(
|
||||
"Integration for ignored config entry %s not found. Creating repair issue",
|
||||
entry,
|
||||
)
|
||||
@@ -2524,7 +2514,7 @@ class ConfigEntries:
|
||||
report_issue = async_suggest_report_issue(
|
||||
self.hass, integration_domain=entry.domain
|
||||
)
|
||||
entry.logger.error(
|
||||
_LOGGER.error(
|
||||
(
|
||||
"Unique id of config entry '%s' from integration %s changed to"
|
||||
" '%s' which is already in use, please %s"
|
||||
@@ -4056,7 +4046,7 @@ async def _load_integration(
|
||||
try:
|
||||
await integration.async_get_platform("config_flow")
|
||||
except ImportError as err:
|
||||
integration.logger.error(
|
||||
_LOGGER.error(
|
||||
"Error occurred loading flow for integration %s: %s",
|
||||
domain,
|
||||
err,
|
||||
|
||||
@@ -523,7 +523,6 @@ class UnitOfEnergyDistance(StrEnum):
|
||||
class UnitOfElectricCurrent(StrEnum):
|
||||
"""Electric current units."""
|
||||
|
||||
MICROAMPERE = "μA"
|
||||
MILLIAMPERE = "mA"
|
||||
AMPERE = "A"
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ from homeassistant.requirements import (
|
||||
async_get_integration_with_requirements,
|
||||
)
|
||||
|
||||
from . import condition, config_validation as cv, trigger
|
||||
from . import config_validation as cv
|
||||
from .typing import ConfigType
|
||||
|
||||
|
||||
@@ -93,12 +93,6 @@ async def async_check_ha_config_file( # noqa: C901
|
||||
result = HomeAssistantConfig()
|
||||
async_clear_install_history(hass)
|
||||
|
||||
# Set up condition and trigger helpers needed for config validation.
|
||||
if condition.CONDITIONS not in hass.data:
|
||||
await condition.async_setup(hass)
|
||||
if trigger.TRIGGERS not in hass.data:
|
||||
await trigger.async_setup(hass)
|
||||
|
||||
def _pack_error(
|
||||
hass: HomeAssistant,
|
||||
package: str,
|
||||
|
||||
@@ -767,7 +767,6 @@ class Integration:
|
||||
self.pkg_path = pkg_path
|
||||
self.file_path = file_path
|
||||
self.manifest = manifest
|
||||
self.logger = logging.getLogger(pkg_path)
|
||||
manifest["is_built_in"] = self.is_built_in
|
||||
manifest["overwrites_built_in"] = self.overwrites_built_in
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ python-slugify==8.0.4
|
||||
PyTurboJPEG==1.8.0
|
||||
PyYAML==6.0.3
|
||||
requests==2.33.1
|
||||
securetar==2026.4.1
|
||||
securetar==2026.2.0
|
||||
SQLAlchemy==2.0.41
|
||||
standard-aifc==3.13.0
|
||||
standard-telnetlib==3.13.0
|
||||
|
||||
@@ -94,12 +94,16 @@ def async_clear_install_history(hass: HomeAssistant) -> None:
|
||||
_async_get_manager(hass).install_failure_history.clear()
|
||||
|
||||
|
||||
def pip_kwargs() -> dict[str, Any]:
|
||||
def pip_kwargs(config_dir: str | None) -> dict[str, Any]:
|
||||
"""Return keyword arguments for PIP install."""
|
||||
return {
|
||||
is_docker = pkg_util.is_docker_env()
|
||||
kwargs = {
|
||||
"constraints": os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE),
|
||||
"timeout": PIP_TIMEOUT,
|
||||
}
|
||||
if not (config_dir is None or pkg_util.is_virtual_env()) and not is_docker:
|
||||
kwargs["target"] = os.path.join(config_dir, "deps")
|
||||
return kwargs
|
||||
|
||||
|
||||
def _install_with_retry(requirement: str, kwargs: dict[str, Any]) -> bool:
|
||||
@@ -332,7 +336,7 @@ class RequirementsManager:
|
||||
requirements: list[str],
|
||||
) -> None:
|
||||
"""Install a requirement and save failures."""
|
||||
kwargs = pip_kwargs()
|
||||
kwargs = pip_kwargs(self.hass.config.config_dir)
|
||||
installed, failures = await self.hass.async_add_executor_job(
|
||||
_install_requirements_if_missing, requirements, kwargs
|
||||
)
|
||||
|
||||
@@ -11,9 +11,10 @@ import os
|
||||
import sys
|
||||
|
||||
from homeassistant import runner
|
||||
from homeassistant.bootstrap import async_mount_local_lib_path
|
||||
from homeassistant.config import get_default_config_dir
|
||||
from homeassistant.requirements import pip_kwargs
|
||||
from homeassistant.util.package import install_package, is_installed
|
||||
from homeassistant.util.package import install_package, is_installed, is_virtual_env
|
||||
|
||||
# mypy: allow-untyped-defs, disallow-any-generics, no-warn-return-any
|
||||
|
||||
@@ -43,7 +44,12 @@ def run(args: list[str]) -> int:
|
||||
|
||||
script = importlib.import_module(f"homeassistant.scripts.{args[0]}")
|
||||
|
||||
_pip_kwargs = pip_kwargs()
|
||||
config_dir = extract_config_dir()
|
||||
|
||||
if not is_virtual_env():
|
||||
asyncio.run(async_mount_local_lib_path(config_dir))
|
||||
|
||||
_pip_kwargs = pip_kwargs(config_dir)
|
||||
|
||||
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ def run(script_args: list) -> int:
|
||||
yaml_files = [
|
||||
f
|
||||
for f in glob(os.path.join(config_dir, "**/*.yaml"), recursive=True)
|
||||
if not f.startswith(deps) # Avoid scanning legacy deps folder
|
||||
if not f.startswith(deps)
|
||||
]
|
||||
|
||||
for yfn in sorted(yaml_files):
|
||||
|
||||
@@ -353,7 +353,6 @@ class ElectricCurrentConverter(BaseUnitConverter):
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
UnitOfElectricCurrent.AMPERE: 1,
|
||||
UnitOfElectricCurrent.MILLIAMPERE: 1e3,
|
||||
UnitOfElectricCurrent.MICROAMPERE: 1e6,
|
||||
}
|
||||
VALID_UNITS = set(UnitOfElectricCurrent)
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ dependencies = [
|
||||
"python-slugify==8.0.4",
|
||||
"PyYAML==6.0.3",
|
||||
"requests==2.33.1",
|
||||
"securetar==2026.4.1",
|
||||
"securetar==2026.2.0",
|
||||
"SQLAlchemy==2.0.41",
|
||||
"standard-aifc==3.13.0",
|
||||
"standard-telnetlib==3.13.0",
|
||||
|
||||
2
requirements.txt
generated
2
requirements.txt
generated
@@ -47,7 +47,7 @@ python-slugify==8.0.4
|
||||
PyTurboJPEG==1.8.0
|
||||
PyYAML==6.0.3
|
||||
requests==2.33.1
|
||||
securetar==2026.4.1
|
||||
securetar==2026.2.0
|
||||
SQLAlchemy==2.0.41
|
||||
standard-aifc==3.13.0
|
||||
standard-telnetlib==3.13.0
|
||||
|
||||
10
requirements_all.txt
generated
10
requirements_all.txt
generated
@@ -1229,7 +1229,7 @@ hole==0.9.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.94
|
||||
holidays==0.93
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260325.6
|
||||
@@ -1895,7 +1895,7 @@ py-sucks==0.9.11
|
||||
py-synologydsm-api==2.7.3
|
||||
|
||||
# homeassistant.components.unifi_access
|
||||
py-unifi-access==1.1.5
|
||||
py-unifi-access==1.1.3
|
||||
|
||||
# homeassistant.components.atome
|
||||
pyAtome==0.1.1
|
||||
@@ -2397,7 +2397,7 @@ pyplaato==0.0.19
|
||||
pypoint==3.0.0
|
||||
|
||||
# homeassistant.components.portainer
|
||||
pyportainer==1.0.38
|
||||
pyportainer==1.0.37
|
||||
|
||||
# homeassistant.components.probe_plus
|
||||
pyprobeplus==1.1.2
|
||||
@@ -2895,7 +2895,7 @@ screenlogicpy==0.10.2
|
||||
scsgate==0.1.0
|
||||
|
||||
# homeassistant.components.backup
|
||||
securetar==2026.4.1
|
||||
securetar==2026.2.0
|
||||
|
||||
# homeassistant.components.sendgrid
|
||||
sendgrid==6.8.2
|
||||
@@ -3380,7 +3380,7 @@ zabbix-utils==2.0.3
|
||||
zamg==0.3.6
|
||||
|
||||
# homeassistant.components.zimi
|
||||
zcc-helper==3.8
|
||||
zcc-helper==3.7
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.148.0
|
||||
|
||||
10
requirements_test_all.txt
generated
10
requirements_test_all.txt
generated
@@ -1093,7 +1093,7 @@ hole==0.9.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.94
|
||||
holidays==0.93
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260325.6
|
||||
@@ -1647,7 +1647,7 @@ py-sucks==0.9.11
|
||||
py-synologydsm-api==2.7.3
|
||||
|
||||
# homeassistant.components.unifi_access
|
||||
py-unifi-access==1.1.5
|
||||
py-unifi-access==1.1.3
|
||||
|
||||
# homeassistant.components.hdmi_cec
|
||||
pyCEC==0.5.2
|
||||
@@ -2053,7 +2053,7 @@ pyplaato==0.0.19
|
||||
pypoint==3.0.0
|
||||
|
||||
# homeassistant.components.portainer
|
||||
pyportainer==1.0.38
|
||||
pyportainer==1.0.37
|
||||
|
||||
# homeassistant.components.probe_plus
|
||||
pyprobeplus==1.1.2
|
||||
@@ -2455,7 +2455,7 @@ satel-integra==1.1.0
|
||||
screenlogicpy==0.10.2
|
||||
|
||||
# homeassistant.components.backup
|
||||
securetar==2026.4.1
|
||||
securetar==2026.2.0
|
||||
|
||||
# homeassistant.components.emulated_kasa
|
||||
# homeassistant.components.sense
|
||||
@@ -2862,7 +2862,7 @@ yt-dlp[default]==2026.03.17
|
||||
zamg==0.3.6
|
||||
|
||||
# homeassistant.components.zimi
|
||||
zcc-helper==3.8
|
||||
zcc-helper==3.7
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.148.0
|
||||
|
||||
@@ -47,9 +47,9 @@
|
||||
'type': 'text',
|
||||
}),
|
||||
]),
|
||||
'temperature': 1.0,
|
||||
'thinking': dict({
|
||||
'budget_tokens': 1024,
|
||||
'type': 'enabled',
|
||||
'type': 'disabled',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_disabled_thinking[subentry_data0]
|
||||
# name: test_disabled_thinking
|
||||
list([
|
||||
dict({
|
||||
'content': '''
|
||||
@@ -224,72 +224,7 @@
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_disabled_thinking[subentry_data0].1
|
||||
dict({
|
||||
'container': None,
|
||||
'max_tokens': 3000,
|
||||
'messages': list([
|
||||
dict({
|
||||
'content': 'hello',
|
||||
'role': 'user',
|
||||
}),
|
||||
dict({
|
||||
'content': 'Hello, how can I help you today?',
|
||||
'role': 'assistant',
|
||||
}),
|
||||
]),
|
||||
'model': 'claude-haiku-4-5',
|
||||
'stream': True,
|
||||
'system': list([
|
||||
dict({
|
||||
'cache_control': dict({
|
||||
'type': 'ephemeral',
|
||||
}),
|
||||
'text': '''
|
||||
You are a voice assistant for Home Assistant.
|
||||
Answer questions about the world truthfully.
|
||||
Answer in plain text. Keep it simple and to the point.
|
||||
Only if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant.
|
||||
''',
|
||||
'type': 'text',
|
||||
}),
|
||||
]),
|
||||
'temperature': 1.0,
|
||||
'thinking': dict({
|
||||
'type': 'disabled',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_disabled_thinking[subentry_data1]
|
||||
list([
|
||||
dict({
|
||||
'content': '''
|
||||
You are a voice assistant for Home Assistant.
|
||||
Answer questions about the world truthfully.
|
||||
Answer in plain text. Keep it simple and to the point.
|
||||
Only if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant.
|
||||
''',
|
||||
'created': HAFakeDatetime(2024, 5, 24, 12, 0, tzinfo=datetime.timezone.utc),
|
||||
'role': 'system',
|
||||
}),
|
||||
dict({
|
||||
'attachments': None,
|
||||
'content': 'hello',
|
||||
'created': HAFakeDatetime(2024, 5, 24, 12, 0, tzinfo=datetime.timezone.utc),
|
||||
'role': 'user',
|
||||
}),
|
||||
dict({
|
||||
'agent_id': 'conversation.claude_conversation',
|
||||
'content': 'Hello, how can I help you today?',
|
||||
'created': HAFakeDatetime(2024, 5, 24, 12, 0, tzinfo=datetime.timezone.utc),
|
||||
'native': None,
|
||||
'role': 'assistant',
|
||||
'thinking_content': None,
|
||||
'tool_calls': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_disabled_thinking[subentry_data1].1
|
||||
# name: test_disabled_thinking.1
|
||||
dict({
|
||||
'container': None,
|
||||
'max_tokens': 3000,
|
||||
|
||||
@@ -10,10 +10,7 @@ from syrupy.assertion import SnapshotAssertion
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import ai_task, media_source
|
||||
from homeassistant.components.anthropic.const import (
|
||||
CONF_CHAT_MODEL,
|
||||
CONF_THINKING_BUDGET,
|
||||
)
|
||||
from homeassistant.components.anthropic.const import CONF_CHAT_MODEL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er, selector
|
||||
@@ -130,7 +127,6 @@ async def test_generate_structured_data_legacy(
|
||||
subentry,
|
||||
data={
|
||||
CONF_CHAT_MODEL: "claude-sonnet-4-0",
|
||||
CONF_THINKING_BUDGET: 0,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -187,11 +183,7 @@ async def test_generate_structured_data_legacy_tools(
|
||||
hass.config_entries.async_update_subentry(
|
||||
mock_config_entry,
|
||||
subentry,
|
||||
data={
|
||||
"chat_model": "claude-sonnet-4-0",
|
||||
"web_search": True,
|
||||
"thinking_budget": 0,
|
||||
},
|
||||
data={"chat_model": "claude-sonnet-4-0", "web_search": True},
|
||||
)
|
||||
|
||||
result = await ai_task.async_generate_data(
|
||||
|
||||
@@ -330,7 +330,7 @@ async def test_subentry_web_search_user_location(
|
||||
"recommended": False,
|
||||
"region": "California",
|
||||
"temperature": 1.0,
|
||||
"thinking_budget": 1024,
|
||||
"thinking_budget": 0,
|
||||
"timezone": "America/Los_Angeles",
|
||||
"tool_search": False,
|
||||
"user_location": True,
|
||||
@@ -487,7 +487,7 @@ async def test_model_list_error(
|
||||
CONF_TEMPERATURE: 1.0,
|
||||
CONF_CHAT_MODEL: "claude-haiku-4-5",
|
||||
CONF_MAX_TOKENS: DEFAULT[CONF_MAX_TOKENS],
|
||||
CONF_THINKING_BUDGET: DEFAULT[CONF_THINKING_BUDGET],
|
||||
CONF_THINKING_BUDGET: 0,
|
||||
CONF_WEB_SEARCH: False,
|
||||
CONF_WEB_SEARCH_MAX_USES: 10,
|
||||
CONF_WEB_SEARCH_USER_LOCATION: False,
|
||||
@@ -614,7 +614,7 @@ async def test_model_list_error(
|
||||
CONF_TEMPERATURE: 0.3,
|
||||
CONF_CHAT_MODEL: DEFAULT[CONF_CHAT_MODEL],
|
||||
CONF_MAX_TOKENS: DEFAULT[CONF_MAX_TOKENS],
|
||||
CONF_THINKING_BUDGET: DEFAULT[CONF_THINKING_BUDGET],
|
||||
CONF_THINKING_BUDGET: 0,
|
||||
CONF_WEB_SEARCH: False,
|
||||
CONF_WEB_SEARCH_MAX_USES: 5,
|
||||
CONF_WEB_SEARCH_USER_LOCATION: False,
|
||||
@@ -788,7 +788,7 @@ async def test_creating_ai_task_subentry_advanced(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_CHAT_MODEL: "claude-sonnet-4-5",
|
||||
CONF_MAX_TOKENS: 1200,
|
||||
CONF_MAX_TOKENS: 200,
|
||||
CONF_TEMPERATURE: 0.5,
|
||||
},
|
||||
)
|
||||
@@ -809,13 +809,13 @@ async def test_creating_ai_task_subentry_advanced(
|
||||
assert result4.get("data") == {
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_CHAT_MODEL: "claude-sonnet-4-5",
|
||||
CONF_MAX_TOKENS: 1200,
|
||||
CONF_MAX_TOKENS: 200,
|
||||
CONF_TEMPERATURE: 0.5,
|
||||
CONF_TOOL_SEARCH: False,
|
||||
CONF_WEB_SEARCH: False,
|
||||
CONF_WEB_SEARCH_MAX_USES: 5,
|
||||
CONF_WEB_SEARCH_USER_LOCATION: False,
|
||||
CONF_THINKING_BUDGET: 1024,
|
||||
CONF_THINKING_BUDGET: 0,
|
||||
CONF_CODE_EXECUTION: False,
|
||||
CONF_PROMPT_CACHING: "prompt",
|
||||
}
|
||||
|
||||
@@ -706,21 +706,6 @@ async def test_extended_thinking(
|
||||
assert call_args == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"subentry_data",
|
||||
[
|
||||
{
|
||||
CONF_LLM_HASS_API: "assist",
|
||||
CONF_CHAT_MODEL: "claude-haiku-4-5",
|
||||
CONF_THINKING_BUDGET: 0,
|
||||
},
|
||||
{
|
||||
CONF_LLM_HASS_API: "assist",
|
||||
CONF_CHAT_MODEL: "claude-opus-4-6",
|
||||
CONF_THINKING_EFFORT: "none",
|
||||
},
|
||||
],
|
||||
)
|
||||
@freeze_time("2024-05-24 12:00:00")
|
||||
async def test_disabled_thinking(
|
||||
hass: HomeAssistant,
|
||||
@@ -728,13 +713,16 @@ async def test_disabled_thinking(
|
||||
mock_init_component,
|
||||
mock_create_stream: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
subentry_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test conversation with thinking effort disabled."""
|
||||
hass.config_entries.async_update_subentry(
|
||||
mock_config_entry,
|
||||
next(iter(mock_config_entry.subentries.values())),
|
||||
data=subentry_data,
|
||||
data={
|
||||
CONF_LLM_HASS_API: "assist",
|
||||
CONF_CHAT_MODEL: "claude-opus-4-6",
|
||||
CONF_THINKING_EFFORT: "none",
|
||||
},
|
||||
)
|
||||
|
||||
mock_create_stream.return_value = [
|
||||
|
||||
@@ -282,14 +282,14 @@ def test_validate_password_no_homeassistant(caplog: pytest.LogCaptureFixture) ->
|
||||
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
|
||||
AddonInfo(name="Core 2", slug="core2", version="1.0.0"),
|
||||
],
|
||||
51200, # 5 x 10240 byte of padding
|
||||
40960, # 4 x 10240 byte of padding
|
||||
"test_backups/c0cb53bd.tar.decrypted",
|
||||
),
|
||||
(
|
||||
[
|
||||
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
|
||||
],
|
||||
40960, # 4 x 10240 byte of padding
|
||||
30720, # 3 x 10240 byte of padding
|
||||
"test_backups/c0cb53bd.tar.decrypted_skip_core2",
|
||||
),
|
||||
],
|
||||
@@ -460,14 +460,14 @@ async def test_decrypted_backup_streamer_wrong_password(hass: HomeAssistant) ->
|
||||
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
|
||||
AddonInfo(name="Core 2", slug="core2", version="1.0.0"),
|
||||
],
|
||||
51200, # 5 x 10240 byte of padding
|
||||
40960, # 4 x 10240 byte of padding
|
||||
"test_backups/c0cb53bd.tar.encrypted_v3",
|
||||
),
|
||||
(
|
||||
[
|
||||
AddonInfo(name="Core 1", slug="core1", version="1.0.0"),
|
||||
],
|
||||
40960, # 4 x 10240 byte of padding
|
||||
30720, # 3 x 10240 byte of padding
|
||||
"test_backups/c0cb53bd.tar.encrypted_v3_skip_core2",
|
||||
),
|
||||
],
|
||||
@@ -674,8 +674,8 @@ async def test_encrypted_backup_streamer_random_nonce(hass: HomeAssistant) -> No
|
||||
# Expect the output length to match the stored encrypted backup file, with
|
||||
# additional padding.
|
||||
encrypted_backup_data = encrypted_backup_path.read_bytes()
|
||||
# 5 x 10240 byte of padding
|
||||
assert len(encrypted_output1) == len(encrypted_backup_data) + 51200
|
||||
# 4 x 10240 byte of padding
|
||||
assert len(encrypted_output1) == len(encrypted_backup_data) + 40960
|
||||
assert encrypted_output1[: len(encrypted_backup_data)] != encrypted_backup_data
|
||||
|
||||
|
||||
|
||||
@@ -1,591 +0,0 @@
|
||||
# serializer version: 1
|
||||
# name: test_setup[aqua_contour][text.mock_title_contour_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'text',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'text.mock_title_contour_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Contour 1',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Contour 1',
|
||||
'platform': 'gardena_bluetooth',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'contour_name',
|
||||
'unique_id': '00000000-0000-0000-0000-000000000003-contour_1_name',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_contour_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Title Contour 1',
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'text.mock_title_contour_1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Contour 1',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_contour_2-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'text',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'text.mock_title_contour_2',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Contour 2',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Contour 2',
|
||||
'platform': 'gardena_bluetooth',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'contour_name',
|
||||
'unique_id': '00000000-0000-0000-0000-000000000003-contour_2_name',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_contour_2-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Title Contour 2',
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'text.mock_title_contour_2',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Contour 2',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_contour_3-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'text',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'text.mock_title_contour_3',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Contour 3',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Contour 3',
|
||||
'platform': 'gardena_bluetooth',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'contour_name',
|
||||
'unique_id': '00000000-0000-0000-0000-000000000003-contour_3_name',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_contour_3-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Title Contour 3',
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'text.mock_title_contour_3',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Contour 3',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_contour_4-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'text',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'text.mock_title_contour_4',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Contour 4',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Contour 4',
|
||||
'platform': 'gardena_bluetooth',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'contour_name',
|
||||
'unique_id': '00000000-0000-0000-0000-000000000003-contour_4_name',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_contour_4-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Title Contour 4',
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'text.mock_title_contour_4',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Contour 4',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_contour_5-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'text',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'text.mock_title_contour_5',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Contour 5',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Contour 5',
|
||||
'platform': 'gardena_bluetooth',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'contour_name',
|
||||
'unique_id': '00000000-0000-0000-0000-000000000003-contour_5_name',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_contour_5-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Title Contour 5',
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'text.mock_title_contour_5',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Contour 5',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_position_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'text',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'text.mock_title_position_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Position 1',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Position 1',
|
||||
'platform': 'gardena_bluetooth',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'position_name',
|
||||
'unique_id': '00000000-0000-0000-0000-000000000003-position_1_name',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_position_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Title Position 1',
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'text.mock_title_position_1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Position 1',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_position_2-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'text',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'text.mock_title_position_2',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Position 2',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Position 2',
|
||||
'platform': 'gardena_bluetooth',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'position_name',
|
||||
'unique_id': '00000000-0000-0000-0000-000000000003-position_2_name',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_position_2-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Title Position 2',
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'text.mock_title_position_2',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Position 2',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_position_3-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'text',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'text.mock_title_position_3',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Position 3',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Position 3',
|
||||
'platform': 'gardena_bluetooth',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'position_name',
|
||||
'unique_id': '00000000-0000-0000-0000-000000000003-position_3_name',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_position_3-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Title Position 3',
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'text.mock_title_position_3',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Position 3',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_position_4-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'text',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'text.mock_title_position_4',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Position 4',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Position 4',
|
||||
'platform': 'gardena_bluetooth',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'position_name',
|
||||
'unique_id': '00000000-0000-0000-0000-000000000003-position_4_name',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_position_4-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Title Position 4',
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'text.mock_title_position_4',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Position 4',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_position_5-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'text',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'text.mock_title_position_5',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Position 5',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Position 5',
|
||||
'platform': 'gardena_bluetooth',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'position_name',
|
||||
'unique_id': '00000000-0000-0000-0000-000000000003-position_5_name',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[aqua_contour][text.mock_title_position_5-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Title Position 5',
|
||||
'max': 20,
|
||||
'min': 0,
|
||||
'mode': <TextMode.TEXT: 'text'>,
|
||||
'pattern': None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'text.mock_title_position_5',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Position 5',
|
||||
})
|
||||
# ---
|
||||
@@ -1,87 +0,0 @@
|
||||
"""Test Gardena Bluetooth text entities."""
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
from gardena_bluetooth.const import AquaContourContours, AquaContourPosition
|
||||
from habluetooth import BluetoothServiceInfo
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.text import (
|
||||
ATTR_VALUE,
|
||||
DOMAIN as TEXT_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import AQUA_CONTOUR_SERVICE_INFO, setup_entry
|
||||
|
||||
from tests.common import snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_info", "raw"),
|
||||
[
|
||||
pytest.param(
|
||||
AQUA_CONTOUR_SERVICE_INFO,
|
||||
{
|
||||
AquaContourPosition.position_name_1.uuid: b"Position 1\x00",
|
||||
AquaContourPosition.position_name_2.uuid: b"Position 2\x00",
|
||||
AquaContourPosition.position_name_3.uuid: b"Position 3\x00",
|
||||
AquaContourPosition.position_name_4.uuid: b"Position 4\x00",
|
||||
AquaContourPosition.position_name_5.uuid: b"Position 5\x00",
|
||||
AquaContourContours.contour_name_1.uuid: b"Contour 1\x00",
|
||||
AquaContourContours.contour_name_2.uuid: b"Contour 2\x00",
|
||||
AquaContourContours.contour_name_3.uuid: b"Contour 3\x00",
|
||||
AquaContourContours.contour_name_4.uuid: b"Contour 4\x00",
|
||||
AquaContourContours.contour_name_5.uuid: b"Contour 5\x00",
|
||||
},
|
||||
id="aqua_contour",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
mock_read_char_raw: dict[str, bytes],
|
||||
service_info: BluetoothServiceInfo,
|
||||
raw: dict[str, bytes],
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test text entities."""
|
||||
mock_read_char_raw.update(raw)
|
||||
|
||||
entry = await setup_entry(
|
||||
hass, platforms=[Platform.TEXT], service_info=service_info
|
||||
)
|
||||
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
|
||||
|
||||
|
||||
async def test_text_set_value(
|
||||
hass: HomeAssistant,
|
||||
mock_read_char_raw: dict[str, bytes],
|
||||
mock_client: Mock,
|
||||
) -> None:
|
||||
"""Test setting text value."""
|
||||
mock_read_char_raw[AquaContourPosition.position_name_1.uuid] = b"Position 1\x00"
|
||||
|
||||
await setup_entry(
|
||||
hass, platforms=[Platform.TEXT], service_info=AQUA_CONTOUR_SERVICE_INFO
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
TEXT_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: "text.mock_title_position_1",
|
||||
ATTR_VALUE: "New Position Name",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(mock_client.write_char.mock_calls) == 1
|
||||
args = mock_client.write_char.mock_calls[0].args
|
||||
assert args[0] == AquaContourPosition.position_name_1
|
||||
assert args[1] == "New Position Name"
|
||||
@@ -1847,7 +1847,6 @@
|
||||
'°',
|
||||
'°C',
|
||||
'°F',
|
||||
'μA',
|
||||
'μS/cm',
|
||||
'μV',
|
||||
'μg',
|
||||
@@ -2192,7 +2191,6 @@
|
||||
'°',
|
||||
'°C',
|
||||
'°F',
|
||||
'μA',
|
||||
'μS/cm',
|
||||
'μV',
|
||||
'μg',
|
||||
|
||||
@@ -117,7 +117,7 @@ async def test_setting_level(hass: HomeAssistant) -> None:
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mocks) == 5
|
||||
assert len(mocks) == 4
|
||||
|
||||
assert len(mocks[""].orig_setLevel.mock_calls) == 1
|
||||
assert mocks[""].orig_setLevel.mock_calls[0][1][0] == LOGSEVERITY["WARNING"]
|
||||
@@ -134,8 +134,6 @@ async def test_setting_level(hass: HomeAssistant) -> None:
|
||||
== LOGSEVERITY["WARNING"]
|
||||
)
|
||||
|
||||
assert len(mocks["homeassistant.components.logger"].orig_setLevel.mock_calls) == 0
|
||||
|
||||
# Test set default level
|
||||
with patch("logging.getLogger", mocks.__getitem__):
|
||||
await hass.services.async_call(
|
||||
@@ -152,7 +150,7 @@ async def test_setting_level(hass: HomeAssistant) -> None:
|
||||
{"test.child": "info", "new_logger": "notset"},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(mocks) == 6
|
||||
assert len(mocks) == 5
|
||||
|
||||
assert len(mocks["test.child"].orig_setLevel.mock_calls) == 2
|
||||
assert mocks["test.child"].orig_setLevel.mock_calls[1][1][0] == LOGSEVERITY["INFO"]
|
||||
|
||||
@@ -44,8 +44,6 @@ from tests.typing import ClientSessionGenerator
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TEST_ENTITY = "light.kitchen"
|
||||
SNAPSHOT_RESOURCE_URI = "homeassistant://assist/context-snapshot"
|
||||
TEST_LLM_API_ID = "test-api"
|
||||
INITIALIZE_MESSAGE = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "request-id-1",
|
||||
@@ -68,21 +66,6 @@ EXPECTED_PROMPT_SUFFIX = """
|
||||
"""
|
||||
|
||||
|
||||
class MockLLMAPI(llm.API):
|
||||
"""Test LLM API that does not expose any tools."""
|
||||
|
||||
async def async_get_api_instance(
|
||||
self, llm_context: llm.LLMContext
|
||||
) -> llm.APIInstance:
|
||||
"""Return a test API instance."""
|
||||
return llm.APIInstance(
|
||||
api=self,
|
||||
api_prompt="Test prompt",
|
||||
llm_context=llm_context,
|
||||
tools=[],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Set up the config entry."""
|
||||
@@ -498,104 +481,6 @@ async def test_get_unknown_prompt(
|
||||
await session.get_prompt(name="Unknown")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("llm_hass_api", [llm.LLM_API_ASSIST, STATELESS_LLM_API])
|
||||
async def test_mcp_resources_list(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: None,
|
||||
mcp_url: str,
|
||||
mcp_client: Any,
|
||||
hass_supervisor_access_token: str,
|
||||
) -> None:
|
||||
"""Test the resource list endpoint."""
|
||||
|
||||
async with mcp_client(hass, mcp_url, hass_supervisor_access_token) as session:
|
||||
result = await session.list_resources()
|
||||
|
||||
assert len(result.resources) == 1
|
||||
resource = result.resources[0]
|
||||
assert str(resource.uri) == SNAPSHOT_RESOURCE_URI
|
||||
assert resource.name == "assist_context_snapshot"
|
||||
assert resource.title == "Assist context snapshot"
|
||||
assert resource.description is not None
|
||||
assert resource.mimeType == "text/plain"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("llm_hass_api", [llm.LLM_API_ASSIST, STATELESS_LLM_API])
|
||||
async def test_mcp_resource_read(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: None,
|
||||
mcp_url: str,
|
||||
mcp_client: Any,
|
||||
hass_supervisor_access_token: str,
|
||||
) -> None:
|
||||
"""Test reading an MCP resource."""
|
||||
|
||||
async with mcp_client(hass, mcp_url, hass_supervisor_access_token) as session:
|
||||
resources = await session.list_resources()
|
||||
resource = resources.resources[0]
|
||||
result = await session.read_resource(resource.uri)
|
||||
|
||||
assert len(result.contents) == 1
|
||||
content = result.contents[0]
|
||||
assert content.uri == resource.uri
|
||||
assert content.mimeType == "text/plain"
|
||||
assert content.text == (
|
||||
"Live Context: An overview of the areas and the devices in this smart home:\n"
|
||||
"- names: Kitchen Light\n"
|
||||
" domain: light\n"
|
||||
" state: 'off'\n"
|
||||
" areas: Kitchen\n"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("llm_hass_api", [llm.LLM_API_ASSIST, STATELESS_LLM_API])
|
||||
async def test_mcp_resource_read_unknown_resource(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: None,
|
||||
mcp_url: str,
|
||||
mcp_client: Any,
|
||||
hass_supervisor_access_token: str,
|
||||
) -> None:
|
||||
"""Test reading an unknown MCP resource."""
|
||||
|
||||
unknown_uri = mcp.types.Resource(
|
||||
uri="homeassistant://assist/missing",
|
||||
name="missing",
|
||||
).uri
|
||||
|
||||
async with mcp_client(hass, mcp_url, hass_supervisor_access_token) as session:
|
||||
with pytest.raises(McpError, match="Unknown resource"):
|
||||
await session.read_resource(unknown_uri)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("llm_hass_api", [TEST_LLM_API_ID])
|
||||
async def test_mcp_resources_unavailable_without_live_context_tool(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: None,
|
||||
mcp_url: str,
|
||||
mcp_client: Any,
|
||||
hass_supervisor_access_token: str,
|
||||
) -> None:
|
||||
"""Test resources are unavailable when the selected API exposes no live context."""
|
||||
|
||||
llm.async_register_api(
|
||||
hass, MockLLMAPI(hass=hass, id=TEST_LLM_API_ID, name="Test API")
|
||||
)
|
||||
|
||||
resource_uri = mcp.types.Resource(
|
||||
uri=SNAPSHOT_RESOURCE_URI,
|
||||
name="assist_context_snapshot",
|
||||
).uri
|
||||
|
||||
async with mcp_client(hass, mcp_url, hass_supervisor_access_token) as session:
|
||||
result = await session.list_resources()
|
||||
|
||||
assert result.resources == []
|
||||
|
||||
with pytest.raises(McpError, match="Unknown resource"):
|
||||
await session.read_resource(resource_uri)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("llm_hass_api", [llm.LLM_API_ASSIST])
|
||||
async def test_mcp_tool_call_unicode(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -26,131 +26,6 @@
|
||||
"volume_level": 20,
|
||||
"volume_muted": false,
|
||||
"group_members": [],
|
||||
"options": [
|
||||
{
|
||||
"key": "treble",
|
||||
"name": "Treble",
|
||||
"type": "integer",
|
||||
"translation_key": "player_options.treble",
|
||||
"translation_params": null,
|
||||
"value": -6,
|
||||
"read_only": false,
|
||||
"min_value": -10,
|
||||
"max_value": 10,
|
||||
"step": 1,
|
||||
"options": null
|
||||
},
|
||||
{
|
||||
"key": "bass",
|
||||
"name": "Bass",
|
||||
"type": "float",
|
||||
"translation_key": "player_options.bass",
|
||||
"translation_params": null,
|
||||
"value": -6.0,
|
||||
"read_only": false,
|
||||
"min_value": -10.0,
|
||||
"max_value": 10.0,
|
||||
"step": 1.0,
|
||||
"options": null
|
||||
},
|
||||
{
|
||||
"key": "treble_ro",
|
||||
"name": "Treble RO",
|
||||
"type": "integer",
|
||||
"translation_key": "player_options.treble",
|
||||
"translation_params": null,
|
||||
"value": -6,
|
||||
"read_only": true,
|
||||
"min_value": null,
|
||||
"max_value": null,
|
||||
"step": null,
|
||||
"options": null
|
||||
},
|
||||
{
|
||||
"key": "enhancer",
|
||||
"name": "Enhancer",
|
||||
"type": "boolean",
|
||||
"translation_key": "player_options.enhancer",
|
||||
"translation_params": null,
|
||||
"value": false,
|
||||
"read_only": false,
|
||||
"min_value": null,
|
||||
"max_value": null,
|
||||
"step": null,
|
||||
"options": null
|
||||
},
|
||||
{
|
||||
"key": "enhancer_ro",
|
||||
"name": "Enhancer RO",
|
||||
"type": "boolean",
|
||||
"translation_key": "player_options.enhancer",
|
||||
"translation_params": null,
|
||||
"value": false,
|
||||
"read_only": true,
|
||||
"min_value": null,
|
||||
"max_value": null,
|
||||
"step": null,
|
||||
"options": null
|
||||
},
|
||||
{
|
||||
"key": "network_name",
|
||||
"name": "Network Name",
|
||||
"type": "string",
|
||||
"translation_key": "player_options.network_name",
|
||||
"translation_params": null,
|
||||
"value": "receiver",
|
||||
"read_only": false,
|
||||
"min_value": null,
|
||||
"max_value": null,
|
||||
"step": null,
|
||||
"options": null
|
||||
},
|
||||
{
|
||||
"key": "network_name_ro",
|
||||
"name": "Network Name RO",
|
||||
"type": "string",
|
||||
"translation_key": "player_options.network_name",
|
||||
"translation_params": null,
|
||||
"value": "receiver ro",
|
||||
"read_only": true,
|
||||
"min_value": null,
|
||||
"max_value": null,
|
||||
"step": null,
|
||||
"options": null
|
||||
},
|
||||
{
|
||||
"key": "link_audio_delay",
|
||||
"name": "Link Audio Delay",
|
||||
"type": "string",
|
||||
"translation_key": "player_options.link_audio_delay",
|
||||
"translation_params": null,
|
||||
"value": "lip_sync",
|
||||
"read_only": false,
|
||||
"min_value": null,
|
||||
"max_value": null,
|
||||
"step": null,
|
||||
"options": [
|
||||
{
|
||||
"key": "audio_sync",
|
||||
"name": "audio_sync",
|
||||
"type": "string",
|
||||
"value": "audio_sync"
|
||||
},
|
||||
{
|
||||
"key": "balanced",
|
||||
"name": "balanced",
|
||||
"type": "string",
|
||||
"value": "balanced"
|
||||
},
|
||||
{
|
||||
"key": "lip_sync",
|
||||
"name": "lip_sync",
|
||||
"type": "string",
|
||||
"value": "lip_sync"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"active_source": "00:00:00:00:00:01",
|
||||
"active_group": null,
|
||||
"current_media": null,
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
# serializer version: 1
|
||||
# name: test_number_entities[number.test_player_1_bass-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 10.0,
|
||||
'min': -10.0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.test_player_1_bass',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Bass',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Bass',
|
||||
'platform': 'music_assistant',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'bass',
|
||||
'unique_id': '00:00:00:00:00:01_bass',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_number_entities[number.test_player_1_bass-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Player 1 Bass',
|
||||
'max': 10.0,
|
||||
'min': -10.0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.test_player_1_bass',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '-6.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_number_entities[number.test_player_1_treble-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 10,
|
||||
'min': -10,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.test_player_1_treble',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Treble',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Treble',
|
||||
'platform': 'music_assistant',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'treble',
|
||||
'unique_id': '00:00:00:00:00:01_treble',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_number_entities[number.test_player_1_treble-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Player 1 Treble',
|
||||
'max': 10,
|
||||
'min': -10,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 1,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.test_player_1_treble',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '-6',
|
||||
})
|
||||
# ---
|
||||
@@ -1,153 +0,0 @@
|
||||
"""Test Music Assistant number entities."""
|
||||
|
||||
from unittest.mock import MagicMock, call
|
||||
|
||||
from music_assistant_models.enums import EventType
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.music_assistant.const import DOMAIN
|
||||
from homeassistant.components.music_assistant.number import (
|
||||
PLAYER_OPTIONS_TRANSLATION_KEYS_NUMBER,
|
||||
)
|
||||
from homeassistant.components.number import (
|
||||
ATTR_VALUE,
|
||||
DOMAIN as NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.translation import LOCALE_EN, async_get_translations
|
||||
|
||||
from .common import (
|
||||
setup_integration_from_fixtures,
|
||||
snapshot_music_assistant_entities,
|
||||
trigger_subscription_callback,
|
||||
)
|
||||
|
||||
|
||||
async def test_number_entities(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
music_assistant_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test number entities."""
|
||||
await setup_integration_from_fixtures(hass, music_assistant_client)
|
||||
snapshot_music_assistant_entities(hass, entity_registry, snapshot, Platform.NUMBER)
|
||||
|
||||
|
||||
async def test_number_set_action(
|
||||
hass: HomeAssistant,
|
||||
music_assistant_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test number set action."""
|
||||
mass_player_id = "00:00:00:00:00:01"
|
||||
mass_option_key = "treble"
|
||||
entity_id = "number.test_player_1_treble"
|
||||
|
||||
option_value = 3
|
||||
|
||||
await setup_integration_from_fixtures(hass, music_assistant_client)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
|
||||
# test within range
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_VALUE: option_value,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert music_assistant_client.send_command.call_count == 1
|
||||
assert music_assistant_client.send_command.call_args == call(
|
||||
"players/cmd/set_option",
|
||||
player_id=mass_player_id,
|
||||
option_key=mass_option_key,
|
||||
option_value=option_value,
|
||||
)
|
||||
|
||||
# test out of range
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_VALUE: 20,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_external_update(
|
||||
hass: HomeAssistant,
|
||||
music_assistant_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test external value update."""
|
||||
mass_player_id = "00:00:00:00:00:01"
|
||||
mass_option_key = "treble"
|
||||
entity_id = "number.test_player_1_treble"
|
||||
|
||||
await setup_integration_from_fixtures(hass, music_assistant_client)
|
||||
|
||||
# get current option and remove it
|
||||
number_option = next(
|
||||
option
|
||||
for option in music_assistant_client.players._players[mass_player_id].options
|
||||
if option.key == mass_option_key
|
||||
)
|
||||
music_assistant_client.players._players[mass_player_id].options.remove(
|
||||
number_option
|
||||
)
|
||||
|
||||
# set new value different from previous one
|
||||
previous_value = number_option.value
|
||||
new_value = 5
|
||||
number_option.value = new_value
|
||||
assert previous_value != number_option.value
|
||||
music_assistant_client.players._players[mass_player_id].options.append(
|
||||
number_option
|
||||
)
|
||||
|
||||
await trigger_subscription_callback(
|
||||
hass, music_assistant_client, EventType.PLAYER_OPTIONS_UPDATED, mass_player_id
|
||||
)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert int(float(state.state)) == new_value
|
||||
|
||||
|
||||
async def test_ignored(
|
||||
hass: HomeAssistant,
|
||||
music_assistant_client: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test that non-compatible player options are ignored."""
|
||||
config_entry = await setup_integration_from_fixtures(hass, music_assistant_client)
|
||||
registry_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry_id=config_entry.entry_id
|
||||
)
|
||||
# we only have two non read-only player options, bass and treble
|
||||
assert sum(1 for entry in registry_entries if entry.domain == NUMBER_DOMAIN) == 2
|
||||
|
||||
|
||||
async def test_name_translation_availability(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Verify, that the list of available translation keys is reflected in strings.json."""
|
||||
# verify, that PLAYER_OPTIONS_TRANSLATION_KEYS_NUMBER matches strings.json
|
||||
translations = await async_get_translations(
|
||||
hass, language=LOCALE_EN, category="entity", integrations=[DOMAIN]
|
||||
)
|
||||
prefix = f"component.{DOMAIN}.entity.{Platform.NUMBER.value}."
|
||||
for translation_key in PLAYER_OPTIONS_TRANSLATION_KEYS_NUMBER:
|
||||
assert translations.get(f"{prefix}{translation_key}.name") is not None, (
|
||||
f"{translation_key} is missing in strings.json for platform number"
|
||||
)
|
||||
@@ -14,7 +14,7 @@ RELAIS_DEVICE = {
|
||||
"_id": "relais1",
|
||||
"name": "Relais Device",
|
||||
"model": "EWS",
|
||||
"state": {"deviceType": 0, "targetMode": 2},
|
||||
"state": {"relayMode": 1, "targetMode": 2},
|
||||
"connected": True,
|
||||
"program": {"data": {}},
|
||||
}
|
||||
@@ -23,7 +23,7 @@ PILOTE_DEVICE = {
|
||||
"_id": "pilote1",
|
||||
"name": "Pilote Device",
|
||||
"model": "EWS",
|
||||
"state": {"deviceType": 1, "targetMode": 1},
|
||||
"state": {"targetMode": 1},
|
||||
"connected": True,
|
||||
"program": {"data": {}},
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import ai_task, media_source
|
||||
from homeassistant.components.openai_conversation import DOMAIN
|
||||
from homeassistant.components.openai_conversation.const import CONF_STORE_RESPONSES
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir, selector
|
||||
@@ -21,13 +20,11 @@ from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_init_component")
|
||||
@pytest.mark.parametrize("expected_store", [False, True])
|
||||
async def test_generate_data(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_create_stream: AsyncMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
expected_store: bool,
|
||||
) -> None:
|
||||
"""Test AI Task data generation."""
|
||||
entity_id = "ai_task.openai_ai_task"
|
||||
@@ -41,12 +38,6 @@ async def test_generate_data(
|
||||
if entry.subentry_type == "ai_task_data"
|
||||
)
|
||||
)
|
||||
hass.config_entries.async_update_subentry(
|
||||
mock_config_entry,
|
||||
ai_task_entry,
|
||||
data={**ai_task_entry.data, CONF_STORE_RESPONSES: expected_store},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert entity_entry is not None
|
||||
assert entity_entry.config_entry_id == mock_config_entry.entry_id
|
||||
assert entity_entry.config_subentry_id == ai_task_entry.subentry_id
|
||||
@@ -64,8 +55,6 @@ async def test_generate_data(
|
||||
)
|
||||
|
||||
assert result.data == "The test data"
|
||||
assert mock_create_stream.call_args is not None
|
||||
assert mock_create_stream.call_args.kwargs["store"] is expected_store
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_init_component")
|
||||
@@ -223,7 +212,6 @@ async def test_generate_data_with_attachments(
|
||||
|
||||
@pytest.mark.usefixtures("mock_init_component")
|
||||
@pytest.mark.freeze_time("2025-06-14 22:59:00")
|
||||
@pytest.mark.parametrize("configured_store", [False, True])
|
||||
@pytest.mark.parametrize(
|
||||
"image_model", ["gpt-image-1.5", "gpt-image-1", "gpt-image-1-mini"]
|
||||
)
|
||||
@@ -234,7 +222,6 @@ async def test_generate_image(
|
||||
entity_registry: er.EntityRegistry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
image_model: str,
|
||||
configured_store: bool,
|
||||
) -> None:
|
||||
"""Test AI Task image generation."""
|
||||
entity_id = "ai_task.openai_ai_task"
|
||||
@@ -251,11 +238,7 @@ async def test_generate_image(
|
||||
hass.config_entries.async_update_subentry(
|
||||
mock_config_entry,
|
||||
ai_task_entry,
|
||||
data={
|
||||
**ai_task_entry.data,
|
||||
"image_model": image_model,
|
||||
CONF_STORE_RESPONSES: configured_store,
|
||||
},
|
||||
data={"image_model": image_model},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert entity_entry is not None
|
||||
@@ -294,8 +277,6 @@ async def test_generate_image(
|
||||
assert result["model"] == image_model
|
||||
|
||||
mock_upload_media.assert_called_once()
|
||||
assert mock_create_stream.call_args is not None
|
||||
assert mock_create_stream.call_args.kwargs["store"] is True
|
||||
image_data = mock_upload_media.call_args[0][1]
|
||||
assert image_data.file.getvalue() == b"A"
|
||||
assert image_data.content_type == "image/png"
|
||||
|
||||
@@ -21,7 +21,6 @@ from homeassistant.components.openai_conversation.const import (
|
||||
CONF_REASONING_SUMMARY,
|
||||
CONF_RECOMMENDED,
|
||||
CONF_SERVICE_TIER,
|
||||
CONF_STORE_RESPONSES,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TOP_P,
|
||||
CONF_TTS_SPEED,
|
||||
@@ -540,7 +539,6 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
CONF_CHAT_MODEL: "o1-pro",
|
||||
CONF_TOP_P: RECOMMENDED_TOP_P,
|
||||
CONF_MAX_TOKENS: 10000,
|
||||
CONF_STORE_RESPONSES: False,
|
||||
CONF_REASONING_EFFORT: "high",
|
||||
CONF_CODE_INTERPRETER: True,
|
||||
},
|
||||
@@ -577,7 +575,6 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL,
|
||||
CONF_TOP_P: RECOMMENDED_TOP_P,
|
||||
CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS,
|
||||
CONF_STORE_RESPONSES: False,
|
||||
CONF_SERVICE_TIER: "auto",
|
||||
CONF_WEB_SEARCH: True,
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE: "low",
|
||||
@@ -595,7 +592,6 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
CONF_CHAT_MODEL: "gpt-4o",
|
||||
CONF_TOP_P: 0.9,
|
||||
CONF_MAX_TOKENS: 1000,
|
||||
CONF_STORE_RESPONSES: True,
|
||||
CONF_WEB_SEARCH: True,
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE: "low",
|
||||
CONF_WEB_SEARCH_USER_LOCATION: True,
|
||||
@@ -617,7 +613,6 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
CONF_CHAT_MODEL: "gpt-4o",
|
||||
CONF_TOP_P: 0.9,
|
||||
CONF_MAX_TOKENS: 1000,
|
||||
CONF_STORE_RESPONSES: True,
|
||||
},
|
||||
{
|
||||
CONF_WEB_SEARCH: True,
|
||||
@@ -635,7 +630,6 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
CONF_CHAT_MODEL: "gpt-4o",
|
||||
CONF_TOP_P: 0.9,
|
||||
CONF_MAX_TOKENS: 1000,
|
||||
CONF_STORE_RESPONSES: True,
|
||||
CONF_SERVICE_TIER: "default",
|
||||
CONF_WEB_SEARCH: True,
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE: "low",
|
||||
@@ -692,7 +686,6 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
CONF_CHAT_MODEL: "gpt-5",
|
||||
CONF_TOP_P: 0.9,
|
||||
CONF_MAX_TOKENS: 1000,
|
||||
CONF_STORE_RESPONSES: False,
|
||||
CONF_REASONING_EFFORT: "minimal",
|
||||
CONF_REASONING_SUMMARY: RECOMMENDED_REASONING_SUMMARY,
|
||||
CONF_CODE_INTERPRETER: False,
|
||||
@@ -806,7 +799,6 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
CONF_CHAT_MODEL: "o3-mini",
|
||||
CONF_TOP_P: 0.9,
|
||||
CONF_MAX_TOKENS: 1000,
|
||||
CONF_STORE_RESPONSES: False,
|
||||
CONF_REASONING_EFFORT: "low",
|
||||
CONF_CODE_INTERPRETER: True,
|
||||
},
|
||||
@@ -852,7 +844,6 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
CONF_CHAT_MODEL: "gpt-4o",
|
||||
CONF_TOP_P: 0.9,
|
||||
CONF_MAX_TOKENS: 1000,
|
||||
CONF_STORE_RESPONSES: False,
|
||||
CONF_SERVICE_TIER: "priority",
|
||||
CONF_WEB_SEARCH: True,
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE: "high",
|
||||
@@ -905,7 +896,6 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
CONF_CHAT_MODEL: "gpt-5-pro",
|
||||
CONF_TOP_P: 0.9,
|
||||
CONF_MAX_TOKENS: 1000,
|
||||
CONF_STORE_RESPONSES: False,
|
||||
CONF_REASONING_SUMMARY: RECOMMENDED_REASONING_SUMMARY,
|
||||
CONF_VERBOSITY: "medium",
|
||||
CONF_WEB_SEARCH: True,
|
||||
@@ -925,11 +915,7 @@ async def test_subentry_switching(
|
||||
expected_options,
|
||||
) -> None:
|
||||
"""Test the subentry form."""
|
||||
subentry = next(
|
||||
sub
|
||||
for sub in mock_config_entry.subentries.values()
|
||||
if sub.subentry_type == "conversation"
|
||||
)
|
||||
subentry = next(iter(mock_config_entry.subentries.values()))
|
||||
hass.config_entries.async_update_subentry(
|
||||
mock_config_entry, subentry, data=current_options
|
||||
)
|
||||
@@ -966,19 +952,11 @@ async def test_subentry_switching(
|
||||
assert subentry.data == expected_options
|
||||
|
||||
|
||||
@pytest.mark.parametrize("store_responses", [False, True])
|
||||
async def test_subentry_web_search_user_location(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry,
|
||||
mock_init_component,
|
||||
store_responses: bool,
|
||||
hass: HomeAssistant, mock_config_entry, mock_init_component
|
||||
) -> None:
|
||||
"""Test fetching user location."""
|
||||
subentry = next(
|
||||
sub
|
||||
for sub in mock_config_entry.subentries.values()
|
||||
if sub.subentry_type == "conversation"
|
||||
)
|
||||
subentry = next(iter(mock_config_entry.subentries.values()))
|
||||
subentry_flow = await mock_config_entry.start_subentry_reconfigure_flow(
|
||||
hass, subentry.subentry_id
|
||||
)
|
||||
@@ -1004,7 +982,6 @@ async def test_subentry_web_search_user_location(
|
||||
CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL,
|
||||
CONF_TOP_P: RECOMMENDED_TOP_P,
|
||||
CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS,
|
||||
CONF_STORE_RESPONSES: store_responses,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@@ -1059,7 +1036,6 @@ async def test_subentry_web_search_user_location(
|
||||
mock_create.call_args.kwargs["input"][0]["content"] == "Where are the following"
|
||||
" coordinates located: (37.7749, -122.4194)?"
|
||||
)
|
||||
assert mock_create.call_args.kwargs["store"] is store_responses
|
||||
assert subentry_flow["type"] is FlowResultType.ABORT
|
||||
assert subentry_flow["reason"] == "reconfigure_successful"
|
||||
assert subentry.data == {
|
||||
@@ -1070,7 +1046,6 @@ async def test_subentry_web_search_user_location(
|
||||
CONF_TOP_P: RECOMMENDED_TOP_P,
|
||||
CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS,
|
||||
CONF_SERVICE_TIER: "auto",
|
||||
CONF_STORE_RESPONSES: store_responses,
|
||||
CONF_WEB_SEARCH: True,
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE: "medium",
|
||||
CONF_WEB_SEARCH_USER_LOCATION: True,
|
||||
@@ -1174,7 +1149,6 @@ async def test_creating_ai_task_subentry_advanced(
|
||||
{
|
||||
CONF_CHAT_MODEL: "gpt-4o",
|
||||
CONF_MAX_TOKENS: 200,
|
||||
CONF_STORE_RESPONSES: True,
|
||||
CONF_TEMPERATURE: 0.5,
|
||||
CONF_TOP_P: 0.9,
|
||||
},
|
||||
@@ -1198,7 +1172,6 @@ async def test_creating_ai_task_subentry_advanced(
|
||||
CONF_CHAT_MODEL: "gpt-4o",
|
||||
CONF_IMAGE_MODEL: "gpt-image-1.5",
|
||||
CONF_MAX_TOKENS: 200,
|
||||
CONF_STORE_RESPONSES: True,
|
||||
CONF_TEMPERATURE: 0.5,
|
||||
CONF_TOP_P: 0.9,
|
||||
CONF_CODE_INTERPRETER: False,
|
||||
|
||||
@@ -20,7 +20,6 @@ from homeassistant.components.homeassistant.exposed_entities import async_expose
|
||||
from homeassistant.components.openai_conversation.const import (
|
||||
CONF_CODE_INTERPRETER,
|
||||
CONF_SERVICE_TIER,
|
||||
CONF_STORE_RESPONSES,
|
||||
CONF_WEB_SEARCH,
|
||||
CONF_WEB_SEARCH_CITY,
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE,
|
||||
@@ -473,46 +472,6 @@ async def test_assist_api_tools_conversion(
|
||||
assert tools
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expected_store",
|
||||
[
|
||||
False,
|
||||
True,
|
||||
],
|
||||
)
|
||||
async def test_store_responses_forwarded_for_conversation_agent(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_init_component,
|
||||
mock_create_stream: AsyncMock,
|
||||
expected_store: bool,
|
||||
) -> None:
|
||||
"""Test store_responses is forwarded for the conversation agent."""
|
||||
subentry = next(
|
||||
entry
|
||||
for entry in mock_config_entry.subentries.values()
|
||||
if entry.subentry_type == "conversation"
|
||||
)
|
||||
hass.config_entries.async_update_subentry(
|
||||
mock_config_entry,
|
||||
subentry,
|
||||
data={**subentry.data, CONF_STORE_RESPONSES: expected_store},
|
||||
)
|
||||
await hass.config_entries.async_reload(mock_config_entry.entry_id)
|
||||
|
||||
mock_create_stream.return_value = [
|
||||
create_message_item(id="msg_A", text="Hello!", output_index=0)
|
||||
]
|
||||
|
||||
result = await conversation.async_converse(
|
||||
hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert mock_create_stream.call_args is not None
|
||||
assert mock_create_stream.call_args.kwargs["store"] is expected_store
|
||||
|
||||
|
||||
@pytest.mark.parametrize("inline_citations", [True, False])
|
||||
async def test_web_search(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -19,7 +19,6 @@ from syrupy.filters import props
|
||||
|
||||
from homeassistant.components.openai_conversation import CONF_CHAT_MODEL
|
||||
from homeassistant.components.openai_conversation.const import (
|
||||
CONF_STORE_RESPONSES,
|
||||
DEFAULT_AI_TASK_NAME,
|
||||
DEFAULT_CONVERSATION_NAME,
|
||||
DEFAULT_STT_NAME,
|
||||
@@ -309,7 +308,6 @@ async def test_init_auth_error(
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
@pytest.mark.parametrize("store_responses", [False, True])
|
||||
@pytest.mark.parametrize(
|
||||
("service_data", "expected_args", "number_of_files"),
|
||||
[
|
||||
@@ -406,31 +404,18 @@ async def test_generate_content_service(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_init_component,
|
||||
store_responses: bool,
|
||||
service_data,
|
||||
expected_args,
|
||||
number_of_files,
|
||||
) -> None:
|
||||
"""Test generate content service."""
|
||||
conversation_subentry = next(
|
||||
sub
|
||||
for sub in mock_config_entry.subentries.values()
|
||||
if sub.subentry_type == "conversation"
|
||||
)
|
||||
hass.config_entries.async_update_subentry(
|
||||
mock_config_entry,
|
||||
conversation_subentry,
|
||||
data={**conversation_subentry.data, CONF_STORE_RESPONSES: store_responses},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
service_data["config_entry"] = mock_config_entry.entry_id
|
||||
expected_args["model"] = "gpt-4o-mini"
|
||||
expected_args["max_output_tokens"] = 3000
|
||||
expected_args["top_p"] = 1.0
|
||||
expected_args["temperature"] = 1.0
|
||||
expected_args["user"] = None
|
||||
expected_args["store"] = store_responses
|
||||
expected_args["store"] = False
|
||||
expected_args["input"][0]["type"] = "message"
|
||||
expected_args["input"][0]["role"] = "user"
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from roborock.data import (
|
||||
WaterLevelMapping,
|
||||
ZeoProgram,
|
||||
)
|
||||
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP, YXCleanType
|
||||
from roborock.exceptions import RoborockException
|
||||
from roborock.roborock_message import RoborockZeoProtocol
|
||||
|
||||
@@ -384,98 +383,3 @@ async def test_update_failure_zeo_invalid_option() -> None:
|
||||
await entity.async_select_option("invalid_option")
|
||||
|
||||
coordinator.api.set_value.assert_not_called()
|
||||
|
||||
|
||||
async def test_q10_cleaning_mode_select_current_option(
|
||||
hass: HomeAssistant,
|
||||
setup_entry: MockConfigEntry,
|
||||
fake_q10_vacuum: FakeDevice,
|
||||
) -> None:
|
||||
"""Test Q10 cleaning mode select entity current option."""
|
||||
entity_id = "select.roborock_q10_s5_cleaning_mode"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
options = state.attributes.get("options")
|
||||
assert options is not None
|
||||
assert set(options) == {"vac_and_mop", "vacuum", "mop"}
|
||||
|
||||
assert fake_q10_vacuum.b01_q10_properties
|
||||
fake_q10_vacuum.b01_q10_properties.status.update_from_dps(
|
||||
{B01_Q10_DP.CLEAN_MODE: YXCleanType.VAC_AND_MOP.code}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
updated_state = hass.states.get(entity_id)
|
||||
assert updated_state is not None
|
||||
assert updated_state.state == "vac_and_mop"
|
||||
|
||||
|
||||
async def test_q10_cleaning_mode_select_update_success(
|
||||
hass: HomeAssistant,
|
||||
setup_entry: MockConfigEntry,
|
||||
fake_q10_vacuum: FakeDevice,
|
||||
) -> None:
|
||||
"""Test setting Q10 cleaning mode select option."""
|
||||
entity_id = "select.roborock_q10_s5_cleaning_mode"
|
||||
assert hass.states.get(entity_id) is not None
|
||||
|
||||
# Test setting value
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
SERVICE_SELECT_OPTION,
|
||||
service_data={"option": "vac_and_mop"},
|
||||
blocking=True,
|
||||
target={"entity_id": entity_id},
|
||||
)
|
||||
|
||||
assert fake_q10_vacuum.b01_q10_properties
|
||||
assert fake_q10_vacuum.b01_q10_properties.vacuum.set_clean_mode.call_count == 1
|
||||
fake_q10_vacuum.b01_q10_properties.vacuum.set_clean_mode.assert_called_once_with(
|
||||
YXCleanType.VAC_AND_MOP
|
||||
)
|
||||
|
||||
|
||||
async def test_q10_cleaning_mode_select_update_failure(
|
||||
hass: HomeAssistant,
|
||||
setup_entry: MockConfigEntry,
|
||||
fake_q10_vacuum: FakeDevice,
|
||||
) -> None:
|
||||
"""Test failure when setting Q10 cleaning mode."""
|
||||
assert fake_q10_vacuum.b01_q10_properties
|
||||
fake_q10_vacuum.b01_q10_properties.vacuum.set_clean_mode.side_effect = (
|
||||
RoborockException
|
||||
)
|
||||
entity_id = "select.roborock_q10_s5_cleaning_mode"
|
||||
assert hass.states.get(entity_id) is not None
|
||||
|
||||
with pytest.raises(HomeAssistantError, match="cleaning_mode"):
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
SERVICE_SELECT_OPTION,
|
||||
service_data={"option": "vac_and_mop"},
|
||||
blocking=True,
|
||||
target={"entity_id": entity_id},
|
||||
)
|
||||
|
||||
|
||||
async def test_q10_cleaning_mode_select_invalid_option(
|
||||
hass: HomeAssistant,
|
||||
setup_entry: MockConfigEntry,
|
||||
fake_q10_vacuum: FakeDevice,
|
||||
) -> None:
|
||||
"""Test that an invalid option raises ServiceValidationError and does not call set_clean_mode."""
|
||||
entity_id = "select.roborock_q10_s5_cleaning_mode"
|
||||
assert hass.states.get(entity_id) is not None
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
SERVICE_SELECT_OPTION,
|
||||
service_data={"option": "invalid_option"},
|
||||
blocking=True,
|
||||
target={"entity_id": entity_id},
|
||||
)
|
||||
|
||||
assert fake_q10_vacuum.b01_q10_properties
|
||||
fake_q10_vacuum.b01_q10_properties.vacuum.set_clean_mode.assert_not_called()
|
||||
|
||||
@@ -6,21 +6,12 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import (
|
||||
AIR_PURIFIER_JP_SERVICE_INFO,
|
||||
AIR_PURIFIER_TABLE_JP_SERVICE_INFO,
|
||||
AIR_PURIFIER_TABLE_US_SERVICE_INFO,
|
||||
AIR_PURIFIER_US_SERVICE_INFO,
|
||||
ART_FRAME_INFO,
|
||||
DOMAIN,
|
||||
WOMETERTHPC_SERVICE_INFO,
|
||||
)
|
||||
from . import ART_FRAME_INFO, DOMAIN, WOMETERTHPC_SERVICE_INFO
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.bluetooth import inject_bluetooth_service_info
|
||||
@@ -176,45 +167,3 @@ async def test_meter_pro_co2_sync_datetime_button_with_timezone(
|
||||
utc_offset_hours=expected_utc_offset_hours,
|
||||
utc_offset_minutes=expected_utc_offset_minutes,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_info", "sensor_type"),
|
||||
[
|
||||
(AIR_PURIFIER_JP_SERVICE_INFO, "air_purifier_jp"),
|
||||
(AIR_PURIFIER_TABLE_JP_SERVICE_INFO, "air_purifier_table_jp"),
|
||||
(AIR_PURIFIER_US_SERVICE_INFO, "air_purifier_us"),
|
||||
(AIR_PURIFIER_TABLE_US_SERVICE_INFO, "air_purifier_table_us"),
|
||||
],
|
||||
)
|
||||
async def test_air_purifier_buttons(
|
||||
hass: HomeAssistant,
|
||||
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
|
||||
service_info: BluetoothServiceInfoBleak,
|
||||
sensor_type: str,
|
||||
) -> None:
|
||||
"""Test pressing the air purifier buttons."""
|
||||
inject_bluetooth_service_info(hass, service_info)
|
||||
|
||||
entry = mock_entry_encrypted_factory(sensor_type)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
entity_id = "button.test_name_light_sensor"
|
||||
mock_instance = AsyncMock(return_value=True)
|
||||
|
||||
with patch.multiple(
|
||||
"homeassistant.components.switchbot.button.switchbot.SwitchbotAirPurifier",
|
||||
update=AsyncMock(return_value=None),
|
||||
get_basic_info=AsyncMock(return_value=None),
|
||||
open_light_sensitive_switch=mock_instance,
|
||||
):
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
mock_instance.assert_awaited_once()
|
||||
|
||||
@@ -248,7 +248,10 @@ async def test_switch_unavailable(
|
||||
|
||||
event = {
|
||||
"tracker_id": "device_id_123",
|
||||
"hardware": {"power_saving_zone_id": "zone_id_123"},
|
||||
"buzzer_control": {"active": True},
|
||||
"led_control": {"active": False},
|
||||
"live_tracking": {"active": True},
|
||||
"tracker_state_reason": "POWER_SAVING",
|
||||
}
|
||||
mock_tractive_client.send_switch_event(mock_config_entry, event)
|
||||
await hass.async_block_till_done()
|
||||
@@ -257,9 +260,7 @@ async def test_switch_unavailable(
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
event["hardware"]["power_saving_zone_id"] = None
|
||||
|
||||
mock_tractive_client.send_switch_event(mock_config_entry, event)
|
||||
mock_tractive_client.send_switch_event(mock_config_entry)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
@@ -15,8 +15,6 @@ from homeassistant.helpers.check_config import (
|
||||
HomeAssistantConfig,
|
||||
async_check_ha_config_file,
|
||||
)
|
||||
from homeassistant.helpers.condition import CONDITIONS
|
||||
from homeassistant.helpers.trigger import TRIGGERS
|
||||
from homeassistant.requirements import RequirementsNotFound
|
||||
|
||||
from tests.common import (
|
||||
@@ -495,11 +493,6 @@ async def test_missing_included_file(hass: HomeAssistant) -> None:
|
||||
|
||||
async def test_automation_config_platform(hass: HomeAssistant) -> None:
|
||||
"""Test automation async config."""
|
||||
# Remove keys pre-populated by the test fixture to simulate
|
||||
# the check_config script which doesn't run bootstrap.
|
||||
del hass.data[TRIGGERS]
|
||||
del hass.data[CONDITIONS]
|
||||
|
||||
files = {
|
||||
YAML_CONFIG_FILE: BASE_CONFIG
|
||||
+ """
|
||||
@@ -521,9 +514,6 @@ blueprint:
|
||||
trigger:
|
||||
platform: event
|
||||
event_type: !input trigger_event
|
||||
condition:
|
||||
condition: template
|
||||
value_template: "{{ true }}"
|
||||
action:
|
||||
service: !input service_to_call
|
||||
""",
|
||||
|
||||
@@ -52,6 +52,29 @@ async def test_requirement_installed_in_venv(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def test_requirement_installed_in_deps(hass: HomeAssistant) -> None:
|
||||
"""Test requirement installed in deps directory."""
|
||||
with (
|
||||
patch("os.path.dirname", return_value="ha_package_path"),
|
||||
patch("homeassistant.util.package.is_virtual_env", return_value=False),
|
||||
patch("homeassistant.util.package.is_docker_env", return_value=False),
|
||||
patch(
|
||||
"homeassistant.util.package.install_package", return_value=True
|
||||
) as mock_install,
|
||||
patch.dict(os.environ, env_without_wheel_links(), clear=True),
|
||||
):
|
||||
hass.config.skip_pip = False
|
||||
mock_integration(hass, MockModule("comp", requirements=["package==0.0.1"]))
|
||||
assert await setup.async_setup_component(hass, "comp", {})
|
||||
assert "comp" in hass.config.components
|
||||
assert mock_install.call_args == call(
|
||||
"package==0.0.1",
|
||||
target=hass.config.path("deps"),
|
||||
constraints=os.path.join("ha_package_path", CONSTRAINT_FILE),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
async def test_install_existing_package(hass: HomeAssistant) -> None:
|
||||
"""Test an install attempt on an existing package."""
|
||||
with patch(
|
||||
|
||||
@@ -628,11 +628,7 @@ _CONVERTED_VALUE: dict[
|
||||
],
|
||||
ElectricCurrentConverter: [
|
||||
(5, UnitOfElectricCurrent.AMPERE, 5000, UnitOfElectricCurrent.MILLIAMPERE),
|
||||
(5, UnitOfElectricCurrent.AMPERE, 5e6, UnitOfElectricCurrent.MICROAMPERE),
|
||||
(5, UnitOfElectricCurrent.MILLIAMPERE, 5e-3, UnitOfElectricCurrent.AMPERE),
|
||||
(5, UnitOfElectricCurrent.MILLIAMPERE, 5e3, UnitOfElectricCurrent.MICROAMPERE),
|
||||
(5, UnitOfElectricCurrent.MICROAMPERE, 5e-6, UnitOfElectricCurrent.AMPERE),
|
||||
(5, UnitOfElectricCurrent.MICROAMPERE, 5e-3, UnitOfElectricCurrent.MILLIAMPERE),
|
||||
(5, UnitOfElectricCurrent.MILLIAMPERE, 0.005, UnitOfElectricCurrent.AMPERE),
|
||||
],
|
||||
ElectricPotentialConverter: [
|
||||
(5, UnitOfElectricPotential.VOLT, 5000, UnitOfElectricPotential.MILLIVOLT),
|
||||
|
||||
Reference in New Issue
Block a user