mirror of
https://github.com/home-assistant/core.git
synced 2026-04-14 21:56:16 +02:00
Compare commits
1 Commits
adjust_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
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]"
|
||||
},
|
||||
"deprecated_method": {
|
||||
"description": "This system is using the {installation_type} installation type, which has been unsupported since Home Assistant 2025.12. To continue receiving updates and support, migrate to a supported installation method.",
|
||||
"description": "This system is using the {installation_type} installation type, which has been deprecated and will become unsupported following the release of Home Assistant 2025.12. While you can continue using your current setup after that point, we strongly recommend migrating to a supported installation method.",
|
||||
"title": "Deprecation notice: Installation method"
|
||||
},
|
||||
"deprecated_method_architecture": {
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
""",
|
||||
|
||||
@@ -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