Compare commits

..

1 Commits

Author SHA1 Message Date
epenet
f2361ef5aa Drop ignore-missing-annotations from pylint 2026-04-07 11:25:00 +00:00
72 changed files with 138 additions and 2198 deletions

View File

@@ -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]

View File

@@ -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

View File

@@ -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",
]

View File

@@ -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
}

View File

@@ -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."""

View File

@@ -38,7 +38,6 @@ PLATFORMS: list[Platform] = [
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.TEXT,
Platform.VALVE,
]
LOGGER = logging.getLogger(__name__)

View File

@@ -1,12 +0,0 @@
{
"entity": {
"text": {
"contour_name": {
"default": "mdi:vector-polygon"
},
"position_name": {
"default": "mdi:map-marker-radius"
}
}
}
}

View File

@@ -154,14 +154,6 @@
"state": {
"name": "[%key:common::state::open%]"
}
},
"text": {
"contour_name": {
"name": "Contour {number}"
},
"position_name": {
"name": "Position {number}"
}
}
}
}

View File

@@ -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)

View File

@@ -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"]
}

View File

@@ -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": {

View File

@@ -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."""

View File

@@ -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

View File

@@ -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."

View File

@@ -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."""

View File

@@ -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
)

View File

@@ -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": {

View File

@@ -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"]

View File

@@ -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"

View File

@@ -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"):

View File

@@ -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 {})

View File

@@ -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"

View File

@@ -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

View File

@@ -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": {

View File

@@ -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,

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["pyportainer==1.0.38"]
"requirements": ["pyportainer==1.0.37"]
}

View File

@@ -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",

View File

@@ -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

View File

@@ -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"

View File

@@ -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,

View File

@@ -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."""

View File

@@ -1,10 +1,5 @@
{
"entity": {
"button": {
"light_sensor": {
"default": "mdi:brightness-auto"
}
},
"climate": {
"climate": {
"state_attributes": {

View File

@@ -102,9 +102,6 @@
}
},
"button": {
"light_sensor": {
"name": "Light sensor"
},
"next_image": {
"name": "Next image"
},

View File

@@ -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
)

View File

@@ -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()

View File

@@ -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"]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.94"]
"requirements": ["holidays==0.93"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["zcc-helper==3.8"]
"requirements": ["zcc-helper==3.7"]
}

View File

@@ -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,

View File

@@ -523,7 +523,6 @@ class UnitOfEnergyDistance(StrEnum):
class UnitOfElectricCurrent(StrEnum):
"""Electric current units."""
MICROAMPERE = "μA"
MILLIAMPERE = "mA"
AMPERE = "A"

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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
View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -47,9 +47,9 @@
'type': 'text',
}),
]),
'temperature': 1.0,
'thinking': dict({
'budget_tokens': 1024,
'type': 'enabled',
'type': 'disabled',
}),
})
# ---

View File

@@ -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,

View File

@@ -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(

View File

@@ -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",
}

View File

@@ -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 = [

View File

@@ -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

View File

@@ -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',
})
# ---

View File

@@ -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"

View File

@@ -1847,7 +1847,6 @@
'°',
'°C',
'°F',
'μA',
'μS/cm',
'μV',
'μg',
@@ -2192,7 +2191,6 @@
'°',
'°C',
'°F',
'μA',
'μS/cm',
'μV',
'μg',

View File

@@ -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"]

View File

@@ -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,

View File

@@ -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,

View File

@@ -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',
})
# ---

View File

@@ -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"
)

View File

@@ -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": {}},
}

View File

@@ -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"

View File

@@ -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,

View File

@@ -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,

View File

@@ -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"

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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
""",

View File

@@ -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),