diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 251d586c119148..41ceb1481c541e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -132,8 +132,15 @@ jobs: strategy: fail-fast: false matrix: &matrix-build - abi: ["cp313"] + abi: ["cp313", "cp314"] arch: ${{ fromJson(needs.init.outputs.architectures) }} + exclude: + - abi: "cp314" + arch: "armv7" + - abi: "cp314" + arch: "armhf" + - abi: "cp314" + arch: "i386" steps: - *checkout @@ -163,7 +170,7 @@ jobs: # home-assistant/wheels doesn't support sha pinning - name: Build wheels - uses: &home-assistant-wheels home-assistant/wheels@2025.09.1 + uses: &home-assistant-wheels home-assistant/wheels@2025.10.0 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 diff --git a/.strict-typing b/.strict-typing index e125deb7cac1c1..91aa90df0280d4 100644 --- a/.strict-typing +++ b/.strict-typing @@ -182,7 +182,6 @@ homeassistant.components.efergy.* homeassistant.components.eheimdigital.* homeassistant.components.electrasmart.* homeassistant.components.electric_kiwi.* -homeassistant.components.elevenlabs.* homeassistant.components.elgato.* homeassistant.components.elkm1.* homeassistant.components.emulated_hue.* diff --git a/homeassistant/components/demo/valve.py b/homeassistant/components/demo/valve.py index 03f0123dd962e5..eb415e8475c30b 100644 --- a/homeassistant/components/demo/valve.py +++ b/homeassistant/components/demo/valve.py @@ -3,12 +3,14 @@ from __future__ import annotations import asyncio +from datetime import datetime from typing import Any from homeassistant.components.valve import ValveEntity, ValveEntityFeature, ValveState from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.event import async_track_utc_time_change OPEN_CLOSE_DELAY = 2 # Used to give a realistic open/close experience in frontend @@ -23,6 +25,8 @@ async def async_setup_entry( [ DemoValve("Front Garden", ValveState.OPEN), DemoValve("Orchard", ValveState.CLOSED), + DemoValve("Back Garden", ValveState.CLOSED, position=70), + DemoValve("Trees", ValveState.CLOSED, position=30), ] ) @@ -37,6 +41,7 @@ def __init__( name: str, state: str, moveable: bool = True, + position: int | None = None, ) -> None: """Initialize the valve.""" self._attr_name = name @@ -46,11 +51,23 @@ def __init__( ) self._state = state self._moveable = moveable + self._attr_reports_position = False + self._unsub_listener_valve: CALLBACK_TYPE | None = None + self._set_position: int = 0 + self._position: int = 0 + if position is None: + return + + self._position = self._set_position = position + self._attr_reports_position = True + self._attr_supported_features |= ( + ValveEntityFeature.SET_POSITION | ValveEntityFeature.STOP + ) @property - def is_open(self) -> bool: - """Return true if valve is open.""" - return self._state == ValveState.OPEN + def current_valve_position(self) -> int: + """Return current position of valve.""" + return self._position @property def is_opening(self) -> bool: @@ -67,11 +84,6 @@ def is_closed(self) -> bool: """Return true if valve is closed.""" return self._state == ValveState.CLOSED - @property - def reports_position(self) -> bool: - """Return True if entity reports position, False otherwise.""" - return False - async def async_open_valve(self, **kwargs: Any) -> None: """Open the valve.""" self._state = ValveState.OPENING @@ -87,3 +99,45 @@ async def async_close_valve(self, **kwargs: Any) -> None: await asyncio.sleep(OPEN_CLOSE_DELAY) self._state = ValveState.CLOSED self.async_write_ha_state() + + async def async_stop_valve(self) -> None: + """Stop the valve.""" + self._state = ValveState.OPEN if self._position > 0 else ValveState.CLOSED + if self._unsub_listener_valve is not None: + self._unsub_listener_valve() + self._unsub_listener_valve = None + self.async_write_ha_state() + + async def async_set_valve_position(self, position: int) -> None: + """Move the valve to a specific position.""" + if position == self._position: + return + if position > self._position: + self._state = ValveState.OPENING + else: + self._state = ValveState.CLOSING + + self._set_position = round(position, -1) + self._listen_valve() + self.async_write_ha_state() + + @callback + def _listen_valve(self) -> None: + """Listen for changes in valve.""" + if self._unsub_listener_valve is None: + self._unsub_listener_valve = async_track_utc_time_change( + self.hass, self._time_changed_valve + ) + + async def _time_changed_valve(self, now: datetime) -> None: + """Track time changes.""" + if self._state == ValveState.OPENING: + self._position += 10 + elif self._state == ValveState.CLOSING: + self._position -= 10 + + if self._position in (100, 0, self._set_position): + await self.async_stop_valve() + return + + self.async_write_ha_state() diff --git a/homeassistant/components/elevenlabs/const.py b/homeassistant/components/elevenlabs/const.py index 5d7aab7dbb6025..c424e0a45882c0 100644 --- a/homeassistant/components/elevenlabs/const.py +++ b/homeassistant/components/elevenlabs/const.py @@ -21,6 +21,9 @@ DEFAULT_STYLE = 0 DEFAULT_USE_SPEAKER_BOOST = True +MAX_REQUEST_IDS = 3 +MODELS_PREVIOUS_INFO_NOT_SUPPORTED = ("eleven_v3",) + STT_LANGUAGES = [ "af-ZA", # Afrikaans "am-ET", # Amharic diff --git a/homeassistant/components/elevenlabs/manifest.json b/homeassistant/components/elevenlabs/manifest.json index f36a238357630b..36d5b6aa3aa8e6 100644 --- a/homeassistant/components/elevenlabs/manifest.json +++ b/homeassistant/components/elevenlabs/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["elevenlabs"], - "requirements": ["elevenlabs==2.3.0"] + "requirements": ["elevenlabs==2.3.0", "sentence-stream==1.2.0"] } diff --git a/homeassistant/components/elevenlabs/quality_scale.yaml b/homeassistant/components/elevenlabs/quality_scale.yaml index 94c395310c5bbd..99658e555a813b 100644 --- a/homeassistant/components/elevenlabs/quality_scale.yaml +++ b/homeassistant/components/elevenlabs/quality_scale.yaml @@ -85,4 +85,4 @@ rules: # Platinum async-dependency: done inject-websession: done - strict-typing: done + strict-typing: todo diff --git a/homeassistant/components/elevenlabs/tts.py b/homeassistant/components/elevenlabs/tts.py index 21da81cef6f02f..b1c26093cf9d2e 100644 --- a/homeassistant/components/elevenlabs/tts.py +++ b/homeassistant/components/elevenlabs/tts.py @@ -2,17 +2,23 @@ from __future__ import annotations -from collections.abc import Mapping +import asyncio +from collections import deque +from collections.abc import AsyncGenerator, Mapping +import contextlib import logging from typing import Any from elevenlabs import AsyncElevenLabs from elevenlabs.core import ApiError from elevenlabs.types import Model, Voice as ElevenLabsVoice, VoiceSettings +from sentence_stream import SentenceBoundaryDetector from homeassistant.components.tts import ( ATTR_VOICE, TextToSpeechEntity, + TTSAudioRequest, + TTSAudioResponse, TtsAudioType, Voice, ) @@ -35,10 +41,12 @@ DEFAULT_STYLE, DEFAULT_USE_SPEAKER_BOOST, DOMAIN, + MAX_REQUEST_IDS, + MODELS_PREVIOUS_INFO_NOT_SUPPORTED, ) _LOGGER = logging.getLogger(__name__) -PARALLEL_UPDATES = 0 +PARALLEL_UPDATES = 6 def to_voice_settings(options: Mapping[str, Any]) -> VoiceSettings: @@ -122,7 +130,12 @@ def __init__( self._attr_supported_languages = [ lang.language_id for lang in self._model.languages or [] ] - self._attr_default_language = self.supported_languages[0] + # Use the first supported language as the default if available + self._attr_default_language = ( + self._attr_supported_languages[0] + if self._attr_supported_languages + else "en" + ) def async_get_supported_voices(self, language: str) -> list[Voice]: """Return a list of supported voices for a language.""" @@ -151,3 +164,151 @@ async def async_get_tts_audio( ) raise HomeAssistantError(exc) from exc return "mp3", bytes_combined + + async def async_stream_tts_audio( + self, request: TTSAudioRequest + ) -> TTSAudioResponse: + """Generate speech from an incoming message.""" + _LOGGER.debug( + "Getting TTS audio for language %s and options: %s", + request.language, + request.options, + ) + return TTSAudioResponse("mp3", self._process_tts_stream(request)) + + async def _process_tts_stream( + self, request: TTSAudioRequest + ) -> AsyncGenerator[bytes]: + """Generate speech from an incoming message.""" + text_stream = request.message_gen + boundary_detector = SentenceBoundaryDetector() + sentences: list[str] = [] + sentences_ready = asyncio.Event() + sentences_complete = False + + language_code: str | None = request.language + voice_id = request.options.get(ATTR_VOICE, self._default_voice_id) + model = request.options.get(ATTR_MODEL, self._model.model_id) + + use_request_ids = model not in MODELS_PREVIOUS_INFO_NOT_SUPPORTED + previous_request_ids: deque[str] = deque(maxlen=MAX_REQUEST_IDS) + + base_stream_params = { + "voice_id": voice_id, + "model_id": model, + "output_format": "mp3_44100_128", + "voice_settings": self._voice_settings, + } + if language_code: + base_stream_params["language_code"] = language_code + + _LOGGER.debug("Starting TTS Stream with options: %s", base_stream_params) + + async def _add_sentences() -> None: + nonlocal sentences_complete + + try: + # Text chunks may not be on word or sentence boundaries + async for text_chunk in text_stream: + for sentence in boundary_detector.add_chunk(text_chunk): + if not sentence.strip(): + continue + + sentences.append(sentence) + + if not sentences: + continue + + sentences_ready.set() + + # Final sentence + if text := boundary_detector.finish(): + sentences.append(text) + finally: + sentences_complete = True + sentences_ready.set() + + _add_sentences_task = self.hass.async_create_background_task( + _add_sentences(), name="elevenlabs_tts_add_sentences" + ) + + # Process new sentences as they're available, but synthesize the first + # one immediately. While that's playing, synthesize (up to) the next 3 + # sentences. After that, synthesize all completed sentences as they're + # available. + sentence_schedule = [1, 3] + while True: + await sentences_ready.wait() + + # Don't wait again if no more sentences are coming + if not sentences_complete: + sentences_ready.clear() + + if not sentences: + if sentences_complete: + # Exit TTS loop + _LOGGER.debug("No more sentences to process") + break + + # More sentences may be coming + continue + + new_sentences = sentences[:] + sentences.clear() + + while new_sentences: + if sentence_schedule: + max_sentences = sentence_schedule.pop(0) + sentences_to_process = new_sentences[:max_sentences] + new_sentences = new_sentences[len(sentences_to_process) :] + else: + # Process all available sentences together + sentences_to_process = new_sentences[:] + new_sentences.clear() + + # Combine all new sentences completed to this point + text = " ".join(sentences_to_process).strip() + + if not text: + continue + + # Build kwargs common to both modes + kwargs = base_stream_params | { + "text": text, + } + + # Provide previous_request_ids if supported. + if previous_request_ids: + # Send previous request ids. + kwargs["previous_request_ids"] = list(previous_request_ids) + + # Synthesize audio while text chunks are still being accumulated + _LOGGER.debug("Synthesizing TTS for text: %s", text) + try: + async with self._client.text_to_speech.with_raw_response.stream( + **kwargs + ) as stream: + async for chunk_bytes in stream.data: + yield chunk_bytes + + if use_request_ids: + if (rid := stream.headers.get("request-id")) is not None: + previous_request_ids.append(rid) + else: + _LOGGER.debug( + "No request-id returned from server; clearing previous requests" + ) + previous_request_ids.clear() + except ApiError as exc: + _LOGGER.warning( + "Error during processing of TTS request %s", exc, exc_info=True + ) + _add_sentences_task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await _add_sentences_task + raise HomeAssistantError(exc) from exc + + # Capture and store server request-id for next calls (only when supported) + _LOGGER.debug("Completed TTS stream for text: %s", text) + + _LOGGER.debug("Completed TTS stream") diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 8c4e06031916bc..1de8db61f4ba4f 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -9,6 +9,7 @@ from aioesphomeapi import ( ClimateAction, ClimateFanMode, + ClimateFeature, ClimateInfo, ClimateMode, ClimatePreset, @@ -134,12 +135,16 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = "climate" + _feature_flags = ClimateFeature(0) @callback def _on_static_info_update(self, static_info: EntityInfo) -> None: """Set attrs from static info.""" super()._on_static_info_update(static_info) static_info = self._static_info + self._feature_flags = ClimateFeature( + static_info.supported_feature_flags_compat(self._api_version) + ) self._attr_precision = self._get_precision() self._attr_hvac_modes = [ _CLIMATE_MODES.from_esphome(mode) for mode in static_info.supported_modes @@ -163,11 +168,18 @@ def _on_static_info_update(self, static_info: EntityInfo) -> None: self._attr_max_temp = static_info.visual_max_temperature self._attr_min_humidity = round(static_info.visual_min_humidity) self._attr_max_humidity = round(static_info.visual_max_humidity) - features = ClimateEntityFeature.TARGET_TEMPERATURE - if static_info.supports_two_point_target_temperature: - features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE - if static_info.supports_target_humidity: + features = ClimateEntityFeature(0) + if self._feature_flags & ClimateFeature.SUPPORTS_TARGET_HUMIDITY: features |= ClimateEntityFeature.TARGET_HUMIDITY + if self._feature_flags & ClimateFeature.REQUIRES_TWO_POINT_TARGET_TEMPERATURE: + features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + else: + features |= ClimateEntityFeature.TARGET_TEMPERATURE + if ( + self._feature_flags + & ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE + ): + features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE if self.preset_modes: features |= ClimateEntityFeature.PRESET_MODE if self.fan_modes: @@ -203,7 +215,7 @@ def hvac_mode(self) -> HVACMode | None: def hvac_action(self) -> HVACAction | None: """Return current action.""" # HA has no support feature field for hvac_action - if not self._static_info.supports_action: + if not self._feature_flags & ClimateFeature.SUPPORTS_ACTION: return None return _CLIMATE_ACTIONS.from_esphome(self._state.action) @@ -233,7 +245,7 @@ def swing_mode(self) -> str | None: @esphome_float_state_property def current_temperature(self) -> float | None: """Return the current temperature.""" - if not self._static_info.supports_current_temperature: + if not self._feature_flags & ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE: return None return self._state.current_temperature @@ -242,7 +254,7 @@ def current_temperature(self) -> float | None: def current_humidity(self) -> int | None: """Return the current humidity.""" if ( - not self._static_info.supports_current_humidity + (not self._feature_flags & ClimateFeature.SUPPORTS_CURRENT_HUMIDITY) or (val := self._state.current_humidity) is None or not isfinite(val) ): @@ -254,7 +266,11 @@ def current_humidity(self) -> int | None: def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" if ( - not self._static_info.supports_two_point_target_temperature + not self._feature_flags + & ( + ClimateFeature.REQUIRES_TWO_POINT_TARGET_TEMPERATURE + | ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE + ) and self.hvac_mode != HVACMode.AUTO ): return self._state.target_temperature @@ -295,7 +311,10 @@ async def async_set_temperature(self, **kwargs: Any) -> None: cast(HVACMode, kwargs[ATTR_HVAC_MODE]) ) if ATTR_TEMPERATURE in kwargs: - if not self._static_info.supports_two_point_target_temperature: + if not self._feature_flags & ( + ClimateFeature.REQUIRES_TWO_POINT_TARGET_TEMPERATURE + | ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE + ): data["target_temperature"] = kwargs[ATTR_TEMPERATURE] else: hvac_mode = kwargs.get(ATTR_HVAC_MODE) or self.hvac_mode diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index 7a022683769470..43c7b3f3044c4d 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -171,9 +171,27 @@ async def async_step_cloud( except TooManyAttemptsBannedException: errors["base"] = "too_many_attempts" except UnknownUserException: + # If the user has no supported CozyTouch devices on + # the Overkiz API server. Login will return unknown user. + if user_input[CONF_HUB] in { + Server.ATLANTIC_COZYTOUCH, + Server.SAUTER_COZYTOUCH, + Server.THERMOR_COZYTOUCH, + }: + description_placeholders["unsupported_device"] = "CozyTouch" # Somfy Protect accounts are not supported since they don't use # the Overkiz API server. Login will return unknown user. - description_placeholders["unsupported_device"] = "Somfy Protect" + elif user_input[CONF_HUB] in { + Server.SOMFY_AMERICA, + Server.SOMFY_DEVELOPER_MODE, + Server.SOMFY_EUROPE, + Server.SOMFY_OCEANIA, + }: + description_placeholders["unsupported_device"] = "Somfy Protect" + # Fallback for other unknown devices + else: + description_placeholders["unsupported_device"] = "Unknown" + errors["base"] = "unsupported_hardware" except Exception: # noqa: BLE001 errors["base"] = "unknown" diff --git a/homeassistant/components/overkiz/strings.json b/homeassistant/components/overkiz/strings.json index 6f0001691a6208..ce783c4eb07918 100644 --- a/homeassistant/components/overkiz/strings.json +++ b/homeassistant/components/overkiz/strings.json @@ -55,7 +55,7 @@ "too_many_attempts": "Too many attempts with an invalid token, temporarily banned", "too_many_requests": "Too many requests, try again later", "unknown": "[%key:common::config_flow::error::unknown%]", - "unsupported_hardware": "Your {unsupported_device} hardware is not supported by this integration." + "unsupported_hardware": "Your {unsupported_device} hardware is not using the Overkiz platform and can't be supported by this integration." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 78885d767bdb0f..67e71a712a370c 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import CONF_SLEEP_PERIOD, MODEL_FRANKEVER_WATER_VALVE +from .const import CONF_SLEEP_PERIOD, MODEL_FRANKEVER_WATER_VALVE, ROLE_GENERIC from .coordinator import ShellyConfigEntry, ShellyRpcCoordinator from .entity import ( BlockEntityDescription, @@ -272,7 +272,7 @@ def __init__( removal_condition=lambda config, _status, key: not is_view_for_platform( config, key, BINARY_SENSOR_PLATFORM ), - role="generic", + role=ROLE_GENERIC, ), "boolean_has_power": RpcBinarySensorDescription( key="boolean", diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index f5e0e315fd8af5..e5bbc2104e63f5 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -23,7 +23,13 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, LOGGER, MODEL_FRANKEVER_WATER_VALVE, SHELLY_GAS_MODELS +from .const import ( + DOMAIN, + LOGGER, + MODEL_FRANKEVER_WATER_VALVE, + ROLE_GENERIC, + SHELLY_GAS_MODELS, +) from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .entity import ( RpcEntityDescription, @@ -351,7 +357,7 @@ async def async_press(self) -> None: RPC_BUTTONS = { "button_generic": RpcButtonDescription( key="button", - role="generic", + role=ROLE_GENERIC, ), "button_open": RpcButtonDescription( key="button", diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index ff38a24a15b2e8..935d168253589e 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -328,3 +328,5 @@ class DeprecatedFirmwareInfo(TypedDict): MODEL_LINKEDGO_ST1820_THERMOSTAT = "ST1820" MODEL_TOP_EV_CHARGER_EVE01 = "EVE01" MODEL_FRANKEVER_IRRIGATION_CONTROLLER = "Irrigation" + +ROLE_GENERIC = "generic" diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 07d64fa6eb071e..f9eb51c2ac8d7e 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -560,7 +560,9 @@ def __init__( self.entity_description = description self._attr_unique_id = f"{super().unique_id}-{attribute}" - self._attr_name = get_rpc_entity_name(coordinator.device, key, description.name) + self._attr_name = get_rpc_entity_name( + coordinator.device, key, description.name, description.role + ) self._last_value = None id_key = key.split(":")[-1] self._id = int(id_key) if id_key.isnumeric() else None diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index a7531f47a6b77f..29bd88a719495f 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -32,6 +32,7 @@ MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT, MODEL_TOP_EV_CHARGER_EVE01, + ROLE_GENERIC, VIRTUAL_NUMBER_MODE_MAP, ) from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator @@ -223,7 +224,7 @@ async def async_set_native_value(self, value: float) -> None: step_fn=lambda config: config["meta"]["ui"].get("step"), unit=get_virtual_component_unit, method="number_set", - role="generic", + role=ROLE_GENERIC, ), "number_current_limit": RpcNumberDescription( key="number", diff --git a/homeassistant/components/shelly/select.py b/homeassistant/components/shelly/select.py index 165e9bc21a2351..4274eac9fafb23 100644 --- a/homeassistant/components/shelly/select.py +++ b/homeassistant/components/shelly/select.py @@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from .const import ROLE_GENERIC from .coordinator import ShellyConfigEntry, ShellyRpcCoordinator from .entity import ( RpcEntityDescription, @@ -44,7 +45,7 @@ class RpcSelectDescription(RpcEntityDescription, SelectEntityDescription): removal_condition=lambda config, _status, key: not is_view_for_platform( config, key, SELECT_PLATFORM ), - role="generic", + role=ROLE_GENERIC, ), } diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 5a6592a8f71b0f..ec1abd415ae699 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -41,7 +41,7 @@ from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.typing import StateType -from .const import CONF_SLEEP_PERIOD +from .const import CONF_SLEEP_PERIOD, ROLE_GENERIC from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .entity import ( BlockEntityDescription, @@ -1425,7 +1425,7 @@ def __init__( removal_condition=lambda config, _, key: not is_view_for_platform( config, key, SENSOR_PLATFORM ), - role="generic", + role=ROLE_GENERIC, ), "number_generic": RpcSensorDescription( key="number", @@ -1434,7 +1434,7 @@ def __init__( config, key, SENSOR_PLATFORM ), unit=get_virtual_component_unit, - role="generic", + role=ROLE_GENERIC, ), "enum_generic": RpcSensorDescription( key="enum", @@ -1444,7 +1444,7 @@ def __init__( ), options_fn=lambda config: config["options"], device_class=SensorDeviceClass.ENUM, - role="generic", + role=ROLE_GENERIC, ), "valve_position": RpcSensorDescription( key="blutrv", @@ -1489,6 +1489,7 @@ def __init__( "number_current_humidity": RpcSensorDescription( key="number", sub_key="value", + name="Humidity", native_unit_of_measurement=PERCENTAGE, suggested_display_precision=1, device_class=SensorDeviceClass.HUMIDITY, @@ -1498,6 +1499,7 @@ def __init__( "number_current_temperature": RpcSensorDescription( key="number", sub_key="value", + name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, suggested_display_precision=1, device_class=SensorDeviceClass.TEMPERATURE, @@ -1507,6 +1509,7 @@ def __init__( "number_flow_rate": RpcSensorDescription( key="number", sub_key="value", + name="Water flow rate", native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_MINUTE, device_class=SensorDeviceClass.VOLUME_FLOW_RATE, state_class=SensorStateClass.MEASUREMENT, @@ -1549,8 +1552,7 @@ def __init__( "number_energy_charge": RpcSensorDescription( key="number", sub_key="value", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, @@ -1594,8 +1596,7 @@ def __init__( key="object", sub_key="value", value=lambda status, _: float(status["counter"]["total"]), - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -1606,8 +1607,7 @@ def __init__( sub_key="value", name="Energy", value=lambda status, _: float(status["total_act_energy"]), - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -1618,8 +1618,7 @@ def __init__( sub_key="value", name="Power", value=lambda status, _: float(status["total_power"]), - native_unit_of_measurement=UnitOfPower.WATT, - suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + native_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=2, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 61b8796cf8b368..2f6c76995f15c7 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -27,6 +27,7 @@ MODEL_LINKEDGO_ST1820_THERMOSTAT, MODEL_NEO_WATER_VALVE, MODEL_TOP_EV_CHARGER_EVE01, + ROLE_GENERIC, ) from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .entity import ( @@ -105,7 +106,7 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): method_on="boolean_set", method_off="boolean_set", method_params_fn=lambda id, value: (id, value), - role="generic", + role=ROLE_GENERIC, ), "boolean_anti_freeze": RpcSwitchDescription( key="boolean", diff --git a/homeassistant/components/shelly/text.py b/homeassistant/components/shelly/text.py index 5643c34c7272ad..164445d569a6c6 100644 --- a/homeassistant/components/shelly/text.py +++ b/homeassistant/components/shelly/text.py @@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from .const import ROLE_GENERIC from .coordinator import ShellyConfigEntry from .entity import ( RpcEntityDescription, @@ -44,7 +45,7 @@ class RpcTextDescription(RpcEntityDescription, TextEntityDescription): removal_condition=lambda config, _status, key: not is_view_for_platform( config, key, TEXT_PLATFORM ), - role="generic", + role=ROLE_GENERIC, ), } diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 963494a30f3846..36478a3dc6205c 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -66,6 +66,7 @@ GEN2_RELEASE_URL, LOGGER, MAX_SCRIPT_SIZE, + ROLE_GENERIC, RPC_INPUTS_EVENTS_TYPES, SHAIR_MAX_WORK_HOURS, SHBTN_INPUTS_EVENTS_TYPES, @@ -436,13 +437,15 @@ def get_rpc_sub_device_name( def get_rpc_entity_name( - device: RpcDevice, key: str, description: str | None = None + device: RpcDevice, key: str, name: str | None = None, role: str | None = None ) -> str | None: """Naming for RPC based switch and sensors.""" channel_name = get_rpc_channel_name(device, key) - if description: - return f"{channel_name} {description.lower()}" if channel_name else description + if name: + if role and role != ROLE_GENERIC: + return name + return f"{channel_name} {name.lower()}" if channel_name else name return channel_name @@ -483,7 +486,7 @@ def get_rpc_key_by_role(keys_dict: dict[str, Any], role: str) -> str | None: def get_rpc_role_by_key(keys_dict: dict[str, Any], key: str) -> str: """Return role by key for RPC device from a dict.""" - return cast(str, keys_dict[key].get("role", "generic")) + return cast(str, keys_dict[key].get("role", ROLE_GENERIC)) def id_from_key(key: str) -> int: diff --git a/homeassistant/components/weather/const.py b/homeassistant/components/weather/const.py index f532b891e3ee14..d5d47d27eadc55 100644 --- a/homeassistant/components/weather/const.py +++ b/homeassistant/components/weather/const.py @@ -77,6 +77,7 @@ class WeatherEntityFeature(IntFlag): UnitOfLength.MILES, } VALID_UNITS_WIND_SPEED: set[str] = { + UnitOfSpeed.BEAUFORT, UnitOfSpeed.FEET_PER_SECOND, UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.KNOTS, diff --git a/mypy.ini b/mypy.ini index f7a36041fa91d0..3f987800262d01 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1576,16 +1576,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.elevenlabs.*] -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_subclassing_any = true -disallow_untyped_calls = true -disallow_untyped_decorators = true -disallow_untyped_defs = true -warn_return_any = true -warn_unreachable = true - [mypy-homeassistant.components.elgato.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index fcd51da3f48f54..57fc83704682f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2812,6 +2812,9 @@ sensorpush-ha==1.3.2 # homeassistant.components.sensoterra sensoterra==2.0.1 +# homeassistant.components.elevenlabs +sentence-stream==1.2.0 + # homeassistant.components.sentry sentry-sdk==1.45.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 605a2745bd735b..fde780d62dbd25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2337,6 +2337,9 @@ sensorpush-ha==1.3.2 # homeassistant.components.sensoterra sensoterra==2.0.1 +# homeassistant.components.elevenlabs +sentence-stream==1.2.0 + # homeassistant.components.sentry sentry-sdk==1.45.1 diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index c25e1f3ddfb567..fd3caf5128e8c9 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -234,11 +234,6 @@ # pyopnsense > pbr > setuptools "pbr": {"setuptools"} }, - "opower": { - # https://github.com/arrow-py/arrow/issues/1169 (fixed not yet released) - # opower > arrow > types-python-dateutil - "arrow": {"types-python-dateutil"} - }, "pvpc_hourly_pricing": {"aiopvpc": {"async-timeout"}}, "remote_rpi_gpio": { # https://github.com/waveform80/colorzero/issues/9 diff --git a/tests/components/demo/test_valve.py b/tests/components/demo/test_valve.py index 1057065ce700b5..ea2aca5344ef42 100644 --- a/tests/components/demo/test_valve.py +++ b/tests/components/demo/test_valve.py @@ -1,24 +1,30 @@ """The tests for the Demo valve platform.""" +from datetime import timedelta from unittest.mock import patch import pytest from homeassistant.components.demo import DOMAIN, valve as demo_valve from homeassistant.components.valve import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, DOMAIN as VALVE_DOMAIN, SERVICE_CLOSE_VALVE, SERVICE_OPEN_VALVE, + SERVICE_SET_VALVE_POSITION, ValveState, ) from homeassistant.const import ATTR_ENTITY_ID, EVENT_STATE_CHANGED, Platform from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util -from tests.common import async_capture_events +from tests.common import async_capture_events, async_fire_time_changed FRONT_GARDEN = "valve.front_garden" ORCHARD = "valve.orchard" +BACK_GARDEN = "valve.back_garden" @pytest.fixture @@ -81,3 +87,59 @@ async def test_opening(hass: HomeAssistant) -> None: assert state_changes[1].data["entity_id"] == ORCHARD assert state_changes[1].data["new_state"].state == ValveState.OPEN + + +async def test_set_valve_position(hass: HomeAssistant) -> None: + """Test moving the valve to a specific position.""" + state = hass.states.get(BACK_GARDEN) + assert state.attributes[ATTR_CURRENT_POSITION] == 70 + + # close to 10% + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_SET_VALVE_POSITION, + {ATTR_ENTITY_ID: BACK_GARDEN, ATTR_POSITION: 10}, + blocking=True, + ) + state = hass.states.get(BACK_GARDEN) + assert state.state == ValveState.CLOSING + + for _ in range(6): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(BACK_GARDEN) + assert state.attributes[ATTR_CURRENT_POSITION] == 10 + assert state.state == ValveState.OPEN + + # open to 80% + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_SET_VALVE_POSITION, + {ATTR_ENTITY_ID: BACK_GARDEN, ATTR_POSITION: 80}, + blocking=True, + ) + state = hass.states.get(BACK_GARDEN) + assert state.state == ValveState.OPENING + + for _ in range(7): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(BACK_GARDEN) + assert state.attributes[ATTR_CURRENT_POSITION] == 80 + assert state.state == ValveState.OPEN + + # test valve is at requested position + state_changes = async_capture_events(hass, EVENT_STATE_CHANGED) + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_SET_VALVE_POSITION, + {ATTR_ENTITY_ID: BACK_GARDEN, ATTR_POSITION: 80}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(state_changes) == 0 diff --git a/tests/components/elevenlabs/test_tts.py b/tests/components/elevenlabs/test_tts.py index c5e7529e5a0ce3..08ab1b6fab0ed7 100644 --- a/tests/components/elevenlabs/test_tts.py +++ b/tests/components/elevenlabs/test_tts.py @@ -2,9 +2,12 @@ from __future__ import annotations +import asyncio +from collections import deque +from collections.abc import AsyncIterator, Iterator from http import HTTPStatus from pathlib import Path -from typing import Any +from typing import Any, Self from unittest.mock import AsyncMock, MagicMock from elevenlabs.core import ApiError @@ -28,6 +31,7 @@ DOMAIN as DOMAIN_MP, SERVICE_PLAY_MEDIA, ) +from homeassistant.components.tts import TTSAudioRequest from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core_config import async_process_ha_core_config @@ -37,17 +41,99 @@ from tests.typing import ClientSessionGenerator -class FakeAudioGenerator: - """Mock audio generator for ElevenLabs TTS.""" +class _FakeResponse: + def __init__(self, headers: dict[str, str]) -> None: + self.headers = headers - def __aiter__(self): - """Mock async iterator for audio parts.""" - async def _gen(): - yield b"audio-part-1" - yield b"audio-part-2" +class _AsyncByteStream: + """Async iterator that yields bytes and exposes response headers like ElevenLabs' stream.""" - return _gen() + def __init__(self, chunks: list[bytes], request_id: str | None = None) -> None: + self._chunks = chunks + self._i = 0 + + def __aiter__(self) -> AsyncIterator[bytes]: + return self + + async def __anext__(self) -> bytes: + if self._i >= len(self._chunks): + raise StopAsyncIteration + b = self._chunks[self._i] + self._i += 1 + await asyncio.sleep(0) # let loop breathe; mirrors real async iterator + return b + + +class _AsyncStreamResponse: + """Async context manager that mimics ElevenLabs raw stream responses.""" + + def __init__(self, chunks: list[bytes], request_id: str | None = None) -> None: + self.headers = {"request-id": request_id} if request_id else {} + self.data = _AsyncByteStream(chunks) + + async def __aenter__(self) -> Self: + return self + + async def __aexit__(self, exc_type, exc, tb) -> None: + return None + + +@pytest.fixture +def capture_stream_calls(monkeypatch: pytest.MonkeyPatch): + """Patches AsyncElevenLabs.text_to_speech.with_raw_response.stream and captures each call's kwargs. + + Returns: + calls: list[dict] — kwargs passed into each stream() invocation + set_next_return(chunks, request_id): sets what the NEXT stream() call yields/returns + """ + calls: list[dict] = [] + state = {"chunks": [b"X"], "request_id": "rid-1"} # defaults; override per test + + def set_next_return( + *, chunks: list[bytes], request_id: str | None, error: Exception | None = None + ) -> None: + state["chunks"] = chunks + state["request_id"] = request_id + state["error"] = error + + def patch_stream(tts_entity): + def _mock_stream(**kwargs): + calls.append(kwargs) + if state.get("error") is not None: + raise state["error"] + return _AsyncStreamResponse( + chunks=list(state["chunks"]), + request_id=state["request_id"], + ) + + tts_entity._client.text_to_speech.with_raw_response.stream = _mock_stream + + return calls, set_next_return, patch_stream + + +@pytest.fixture +def stream_sentence_helpers(): + """Return helpers for queue-driven sentence streaming.""" + + def factory(sentence_iter: Iterator[tuple], queue: asyncio.Queue[str | None]): + async def get_next_part() -> tuple[Any, ...]: + try: + return next(sentence_iter) + except StopIteration: + await queue.put(None) + return None, None, None + + async def message_gen() -> AsyncIterator[str]: + while True: + part = await queue.get() + if part is None: + break + yield part + + return get_next_part, message_gen + + return factory @pytest.fixture(autouse=True) @@ -134,15 +220,15 @@ async def test_tts_service_speak( setup: AsyncMock, hass: HomeAssistant, hass_client: ClientSessionGenerator, + capture_stream_calls, calls: list[ServiceCall], tts_service: str, service_data: dict[str, Any], ) -> None: """Test tts service.""" + stream_calls, _, patch_stream = capture_stream_calls tts_entity = hass.data[tts.DOMAIN].get_entity(service_data[ATTR_ENTITY_ID]) - tts_entity._client.text_to_speech.convert = MagicMock( - return_value=FakeAudioGenerator() - ) + patch_stream(tts_entity) assert tts_entity._voice_settings == VoiceSettings( stability=DEFAULT_STABILITY, @@ -158,20 +244,22 @@ async def test_tts_service_speak( blocking=True, ) - assert len(calls) == 1 assert ( await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == HTTPStatus.OK ) + assert len(stream_calls) == 1 voice_id = service_data[tts.ATTR_OPTIONS].get(tts.ATTR_VOICE, "voice1") model_id = service_data[tts.ATTR_OPTIONS].get(ATTR_MODEL, "model1") + language = service_data.get(tts.ATTR_LANGUAGE, tts_entity.default_language) - tts_entity._client.text_to_speech.convert.assert_called_once_with( - text="There is a person at the front door.", - voice_id=voice_id, - model_id=model_id, - voice_settings=tts_entity._voice_settings, - ) + call_kwargs = stream_calls[0] + assert call_kwargs["text"] == "There is a person at the front door." + assert call_kwargs["voice_id"] == voice_id + assert call_kwargs["model_id"] == model_id + assert call_kwargs["voice_settings"] == tts_entity._voice_settings + assert call_kwargs["output_format"] == "mp3_44100_128" + assert call_kwargs["language_code"] == language @pytest.mark.parametrize( @@ -206,15 +294,15 @@ async def test_tts_service_speak_lang_config( setup: AsyncMock, hass: HomeAssistant, hass_client: ClientSessionGenerator, + capture_stream_calls, calls: list[ServiceCall], tts_service: str, service_data: dict[str, Any], ) -> None: """Test service call say with other langcodes in the config.""" + stream_calls, _, patch_stream = capture_stream_calls tts_entity = hass.data[tts.DOMAIN].get_entity(service_data[ATTR_ENTITY_ID]) - tts_entity._client.text_to_speech.convert = MagicMock( - return_value=FakeAudioGenerator() - ) + patch_stream(tts_entity) await hass.services.async_call( tts.DOMAIN, @@ -223,18 +311,20 @@ async def test_tts_service_speak_lang_config( blocking=True, ) - assert len(calls) == 1 assert ( await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == HTTPStatus.OK ) - tts_entity._client.text_to_speech.convert.assert_called_once_with( - text="There is a person at the front door.", - voice_id="voice1", - model_id="model1", - voice_settings=tts_entity._voice_settings, - ) + assert len(stream_calls) == 1 + language = service_data.get(tts.ATTR_LANGUAGE, tts_entity.default_language) + call_kwargs = stream_calls[0] + assert call_kwargs["text"] == "There is a person at the front door." + assert call_kwargs["voice_id"] == "voice1" + assert call_kwargs["model_id"] == "model1" + assert call_kwargs["voice_settings"] == tts_entity._voice_settings + assert call_kwargs["output_format"] == "mp3_44100_128" + assert call_kwargs["language_code"] == language @pytest.mark.parametrize( @@ -257,16 +347,16 @@ async def test_tts_service_speak_error( setup: AsyncMock, hass: HomeAssistant, hass_client: ClientSessionGenerator, + capture_stream_calls, calls: list[ServiceCall], tts_service: str, service_data: dict[str, Any], ) -> None: """Test service call say with http response 400.""" + stream_calls, set_next_return, patch_stream = capture_stream_calls tts_entity = hass.data[tts.DOMAIN].get_entity(service_data[ATTR_ENTITY_ID]) - tts_entity._client.text_to_speech.convert = MagicMock( - return_value=FakeAudioGenerator() - ) - tts_entity._client.text_to_speech.convert.side_effect = ApiError + patch_stream(tts_entity) + set_next_return(chunks=[], request_id=None, error=ApiError()) await hass.services.async_call( tts.DOMAIN, @@ -275,18 +365,20 @@ async def test_tts_service_speak_error( blocking=True, ) - assert len(calls) == 1 assert ( await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == HTTPStatus.INTERNAL_SERVER_ERROR ) - tts_entity._client.text_to_speech.convert.assert_called_once_with( - text="There is a person at the front door.", - voice_id="voice1", - model_id="model1", - voice_settings=tts_entity._voice_settings, - ) + assert len(stream_calls) == 1 + language = service_data.get(tts.ATTR_LANGUAGE, tts_entity.default_language) + call_kwargs = stream_calls[0] + assert call_kwargs["text"] == "There is a person at the front door." + assert call_kwargs["voice_id"] == "voice1" + assert call_kwargs["model_id"] == "model1" + assert call_kwargs["voice_settings"] == tts_entity._voice_settings + assert call_kwargs["output_format"] == "mp3_44100_128" + assert call_kwargs["language_code"] == language @pytest.mark.parametrize( @@ -323,16 +415,17 @@ async def test_tts_service_speak_voice_settings( setup: AsyncMock, hass: HomeAssistant, hass_client: ClientSessionGenerator, + capture_stream_calls, calls: list[ServiceCall], tts_service: str, service_data: dict[str, Any], mock_similarity: float, ) -> None: """Test tts service.""" + stream_calls, _, patch_stream = capture_stream_calls tts_entity = hass.data[tts.DOMAIN].get_entity(service_data[ATTR_ENTITY_ID]) - tts_entity._client.text_to_speech.convert = MagicMock( - return_value=FakeAudioGenerator() - ) + patch_stream(tts_entity) + assert tts_entity._voice_settings == VoiceSettings( stability=DEFAULT_STABILITY, similarity_boost=DEFAULT_SIMILARITY / 2, @@ -347,18 +440,20 @@ async def test_tts_service_speak_voice_settings( blocking=True, ) - assert len(calls) == 1 assert ( await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == HTTPStatus.OK ) - tts_entity._client.text_to_speech.convert.assert_called_once_with( - text="There is a person at the front door.", - voice_id="voice2", - model_id="model1", - voice_settings=tts_entity._voice_settings, - ) + assert len(stream_calls) == 1 + language = service_data.get(tts.ATTR_LANGUAGE, tts_entity.default_language) + call_kwargs = stream_calls[0] + assert call_kwargs["text"] == "There is a person at the front door." + assert call_kwargs["voice_id"] == "voice2" + assert call_kwargs["model_id"] == "model1" + assert call_kwargs["voice_settings"] == tts_entity._voice_settings + assert call_kwargs["output_format"] == "mp3_44100_128" + assert call_kwargs["language_code"] == language @pytest.mark.parametrize( @@ -381,15 +476,15 @@ async def test_tts_service_speak_without_options( setup: AsyncMock, hass: HomeAssistant, hass_client: ClientSessionGenerator, + capture_stream_calls, calls: list[ServiceCall], tts_service: str, service_data: dict[str, Any], ) -> None: """Test service call say with http response 200.""" + stream_calls, _, patch_stream = capture_stream_calls tts_entity = hass.data[tts.DOMAIN].get_entity(service_data[ATTR_ENTITY_ID]) - tts_entity._client.text_to_speech.convert = MagicMock( - return_value=FakeAudioGenerator() - ) + patch_stream(tts_entity) await hass.services.async_call( tts.DOMAIN, @@ -398,17 +493,179 @@ async def test_tts_service_speak_without_options( blocking=True, ) - assert len(calls) == 1 assert ( await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == HTTPStatus.OK ) - tts_entity._client.text_to_speech.convert.assert_called_once_with( - text="There is a person at the front door.", - voice_id="voice1", - voice_settings=VoiceSettings( - stability=0.5, similarity_boost=0.75, style=0.0, use_speaker_boost=True + assert len(stream_calls) == 1 + language = service_data.get(tts.ATTR_LANGUAGE, tts_entity.default_language) + call_kwargs = stream_calls[0] + assert call_kwargs["text"] == "There is a person at the front door." + assert call_kwargs["voice_id"] == "voice1" + assert call_kwargs["model_id"] == "model1" + assert call_kwargs["voice_settings"] == VoiceSettings( + stability=0.5, + similarity_boost=0.75, + style=0.0, + use_speaker_boost=True, + ) + assert call_kwargs["output_format"] == "mp3_44100_128" + assert call_kwargs["language_code"] == language + + +@pytest.mark.parametrize( + ("setup", "model_id"), + [ + ("mock_config_entry_setup", "eleven_multilingual_v2"), + ], + indirect=["setup"], +) +@pytest.mark.parametrize( + ("message", "chunks", "request_ids"), + [ + ( + [ + ["One. ", "Two! ", "Three"], + ["! ", "Four"], + ["? ", "Five"], + ["! ", "Six!"], + ], + [b"\x05\x06", b"\x07\x08", b"\x09\x0a", b"\x0b\x0c"], + ["rid-1", "rid-2", "rid-3", "rid-4"], + ), + ], +) +async def test_stream_tts_with_request_ids( + setup: AsyncMock, + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + capture_stream_calls, + stream_sentence_helpers, + model_id: str, + message: list[list[str]], + chunks: list[bytes], + request_ids: list[str], +) -> None: + """Test streaming TTS with request-id stitching.""" + calls, set_next_return, patch_stream = capture_stream_calls + + # Access the TTS entity as in your existing tests; adjust if you use a fixture instead + tts_entity = hass.data[tts.DOMAIN].get_entity("tts.elevenlabs_text_to_speech") + patch_stream(tts_entity) + + # Use a queue to control when each part is yielded + queue = asyncio.Queue() + prev_request_ids: deque[str] = deque(maxlen=3) # keep last 3 request IDs + sentence_iter = iter(zip(message, chunks, request_ids, strict=False)) + get_next_part, message_gen = stream_sentence_helpers(sentence_iter, queue) + options = {tts.ATTR_VOICE: "voice1", "model": model_id} + req = TTSAudioRequest(message_gen=message_gen(), language="en", options=options) + + resp = await tts_entity.async_stream_tts_audio(req) + assert resp.extension == "mp3" + + item, chunk, request_id = await get_next_part() + if item is not None: + for part in item: + await queue.put(part) + else: + await queue.put(None) + + set_next_return(chunks=[chunk], request_id=request_id) + next_item, next_chunk, next_request_id = await get_next_part() + # Consume bytes; after first chunk, switch next return to emulate second call + async for b in resp.data_gen: + assert b == chunk # each sentence yields its first chunk immediately + assert "previous_text" not in calls[-1] # no previous_text for first sentence + assert "next_text" not in calls[-1] # no next_text for first + assert calls[-1].get("previous_request_ids", []) == ( + [] if len(calls) == 1 else list(prev_request_ids) + ) + if request_id: + prev_request_ids.append(request_id or "") + item, chunk, request_id = next_item, next_chunk, next_request_id + if item is not None: + for part in item: + await queue.put(part) + set_next_return(chunks=[chunk], request_id=request_id) + next_item, next_chunk, next_request_id = await get_next_part() + if item is None: + await queue.put(None) + else: + await queue.put(None) + + # We expect two stream() invocations (one per sentence batch) + assert len(calls) == len(message) + + +@pytest.mark.parametrize( + ("message", "chunks", "request_ids"), + [ + ( + [ + ["This is the first sentence. ", "This is "], + ["the second sentence. "], + ], + [b"\x05\x06", b"\x07\x08"], + ["rid-1", "rid-2"], ), - model_id="model1", + ], +) +async def test_stream_tts_without_previous_info( + setup: AsyncMock, + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + capture_stream_calls, + stream_sentence_helpers, + monkeypatch: pytest.MonkeyPatch, + message: list[list[str]], + chunks: list[bytes], + request_ids: list[str], +) -> None: + """Test streaming TTS without request-id stitching (eleven_v3).""" + calls, set_next_return, patch_stream = capture_stream_calls + tts_entity = hass.data[tts.DOMAIN].get_entity("tts.elevenlabs_text_to_speech") + patch_stream(tts_entity) + monkeypatch.setattr( + "homeassistant.components.elevenlabs.tts.MODELS_PREVIOUS_INFO_NOT_SUPPORTED", + ("model1",), + raising=False, ) + + queue = asyncio.Queue() + sentence_iter = iter(zip(message, chunks, request_ids, strict=False)) + get_next_part, message_gen = stream_sentence_helpers(sentence_iter, queue) + options = {tts.ATTR_VOICE: "voice1", "model": "model1"} + req = TTSAudioRequest(message_gen=message_gen(), language="en", options=options) + + resp = await tts_entity.async_stream_tts_audio(req) + assert resp.extension == "mp3" + + item, chunk, request_id = await get_next_part() + if item is not None: + for part in item: + await queue.put(part) + else: + await queue.put(None) + + set_next_return(chunks=[chunk], request_id=request_id) + next_item, next_chunk, next_request_id = await get_next_part() + # Consume bytes; after first chunk, switch next return to emulate second call + async for b in resp.data_gen: + assert b == chunk # each sentence yields its first chunk immediately + assert "previous_request_ids" not in calls[-1] # no previous_request_ids + + item, chunk, request_id = next_item, next_chunk, next_request_id + if item is not None: + for part in item: + await queue.put(part) + set_next_return(chunks=[chunk], request_id=request_id) + next_item, next_chunk, next_request_id = await get_next_part() + if item is None: + await queue.put(None) + else: + await queue.put(None) + + # We expect two stream() invocations (one per sentence batch) + assert len(calls) == len(message) diff --git a/tests/components/esphome/test_climate.py b/tests/components/esphome/test_climate.py index 216421cd8b0186..2a2c232e2c463c 100644 --- a/tests/components/esphome/test_climate.py +++ b/tests/components/esphome/test_climate.py @@ -7,6 +7,7 @@ APIClient, ClimateAction, ClimateFanMode, + ClimateFeature, ClimateInfo, ClimateMode, ClimatePreset, @@ -107,11 +108,10 @@ async def test_climate_entity_with_step_and_two_point( object_id="myclimate", key=1, name="my climate", - supports_current_temperature=True, - supports_two_point_target_temperature=True, + feature_flags=ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE + | ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE, visual_target_temperature_step=2, visual_current_temperature_step=2, - supports_action=False, visual_min_temperature=10.0, visual_max_temperature=30.0, supported_modes=[ClimateMode.COOL, ClimateMode.HEAT, ClimateMode.AUTO], @@ -346,13 +346,13 @@ async def test_climate_entity_with_humidity( object_id="myclimate", key=1, name="my climate", - supports_current_temperature=True, - supports_two_point_target_temperature=True, - supports_action=True, + feature_flags=ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE + | ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE + | ClimateFeature.SUPPORTS_CURRENT_HUMIDITY + | ClimateFeature.SUPPORTS_TARGET_HUMIDITY + | ClimateFeature.SUPPORTS_ACTION, visual_min_temperature=10.0, visual_max_temperature=30.0, - supports_current_humidity=True, - supports_target_humidity=True, visual_min_humidity=10.1, visual_max_humidity=29.7, ) @@ -407,9 +407,9 @@ async def test_climate_entity_with_heat( object_id="myclimate", key=1, name="my climate", - supports_current_temperature=True, - supports_two_point_target_temperature=True, - supports_action=True, + feature_flags=ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE + | ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE + | ClimateFeature.SUPPORTS_ACTION, visual_min_temperature=10.0, visual_max_temperature=30.0, supported_modes=[ClimateMode.COOL, ClimateMode.HEAT, ClimateMode.AUTO], @@ -456,9 +456,9 @@ async def test_climate_entity_with_heat_cool( object_id="myclimate", key=1, name="my climate", - supports_current_temperature=True, - supports_two_point_target_temperature=True, - supports_action=True, + feature_flags=ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE + | ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE + | ClimateFeature.SUPPORTS_ACTION, visual_min_temperature=10.0, visual_max_temperature=30.0, supported_modes=[ClimateMode.COOL, ClimateMode.HEAT, ClimateMode.HEAT_COOL], @@ -516,7 +516,7 @@ async def test_climate_set_temperature_unsupported_mode( object_id="myclimate", key=1, name="my climate", - supports_two_point_target_temperature=True, + feature_flags=ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE, supported_modes=[ClimateMode.HEAT, ClimateMode.COOL, ClimateMode.AUTO], visual_min_temperature=10.0, visual_max_temperature=30.0, @@ -563,13 +563,13 @@ async def test_climate_entity_with_inf_value( object_id="myclimate", key=1, name="my climate", - supports_current_temperature=True, - supports_two_point_target_temperature=True, - supports_action=True, + feature_flags=ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE + | ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE + | ClimateFeature.SUPPORTS_CURRENT_HUMIDITY + | ClimateFeature.SUPPORTS_TARGET_HUMIDITY + | ClimateFeature.SUPPORTS_ACTION, visual_min_temperature=10.0, visual_max_temperature=30.0, - supports_current_humidity=True, - supports_target_humidity=True, visual_min_humidity=10.1, visual_max_humidity=29.7, ) @@ -616,10 +616,10 @@ async def test_climate_entity_attributes( object_id="myclimate", key=1, name="my climate", - supports_current_temperature=True, + feature_flags=ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE + | ClimateFeature.SUPPORTS_ACTION, visual_target_temperature_step=2, visual_current_temperature_step=2, - supports_action=True, visual_min_temperature=10.0, visual_max_temperature=30.0, supported_fan_modes=[ClimateFanMode.LOW, ClimateFanMode.HIGH], diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index 410c2ebb5f1db0..f2d9b6c76391ab 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -255,6 +255,96 @@ async def test_form_invalid_auth_cloud( assert result["errors"] == {"base": error} +@pytest.mark.parametrize( + ("side_effect", "description_placeholder", "server"), + [ + (UnknownUserException, "CozyTouch", TEST_SERVER_COZYTOUCH), + (UnknownUserException, "Unknown", TEST_SERVER2), + ], +) +async def test_form_invalid_hardware_cloud( + hass: HomeAssistant, + side_effect: Exception, + description_placeholder: str, + server: str, +) -> None: + """Test we handle unsupported hardware (cloud).""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"hub": server}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "cloud" + + with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": TEST_EMAIL, "password": TEST_PASSWORD}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "unsupported_hardware"} + assert result["description_placeholders"] == { + "unsupported_device": description_placeholder + } + + +@pytest.mark.parametrize( + ("side_effect", "description_placeholder", "server"), + [ + (UnknownUserException, "Somfy Protect", TEST_SERVER), + ], +) +async def test_form_invalid_hardware_cloud_local( + hass: HomeAssistant, + side_effect: Exception, + description_placeholder: str, + server: str, +) -> None: + """Test we handle unsupported hardware (cloud and local).""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"hub": server}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"api_type": "cloud"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "cloud" + + with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": TEST_EMAIL, "password": TEST_PASSWORD}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "unsupported_hardware"} + assert result["description_placeholders"] == { + "unsupported_device": description_placeholder + } + + @pytest.mark.parametrize( ("side_effect", "error"), [ diff --git a/tests/components/shelly/snapshots/test_sensor.ambr b/tests/components/shelly/snapshots/test_sensor.ambr index 09dc06c514a710..11892f586a6612 100644 --- a/tests/components/shelly/snapshots/test_sensor.ambr +++ b/tests/components/shelly/snapshots/test_sensor.ambr @@ -507,9 +507,6 @@ 'sensor': dict({ 'suggested_display_precision': 2, }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 00c08cb7868bd2..640d7439eb9f3b 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -1575,7 +1575,7 @@ async def test_rpc_device_virtual_number_sensor_with_device_class( entity_registry: EntityRegistry, ) -> None: """Test a virtual number sensor with device class for RPC device.""" - entity_id = f"{SENSOR_DOMAIN}.test_name_current_humidity" + entity_id = f"{SENSOR_DOMAIN}.test_name_humidity" config = deepcopy(mock_rpc_device.config) config["number:203"] = { "name": "Current humidity", @@ -1587,6 +1587,7 @@ async def test_rpc_device_virtual_number_sensor_with_device_class( monkeypatch.setattr(mock_rpc_device, "config", config) status = deepcopy(mock_rpc_device.status) + status.pop("humidity:0") status["number:203"] = {"value": 34} monkeypatch.setattr(mock_rpc_device, "status", status) @@ -1723,7 +1724,7 @@ async def test_rpc_shelly_ev_sensors( } config["number:201"] = { "name": "Session energy", - "meta": {"ui": {"unit": "Wh", "view": "label"}}, + "meta": {"ui": {"unit": "kWh", "view": "label"}}, "role": "energy_charge", } config["number:202"] = { @@ -1735,7 +1736,7 @@ async def test_rpc_shelly_ev_sensors( status = deepcopy(mock_rpc_device.status) status["number:200"] = {"value": "charger_charging"} - status["number:201"] = {"value": 5000} + status["number:201"] = {"value": 5.0} status["number:202"] = {"value": 60} monkeypatch.setattr(mock_rpc_device, "status", status) diff --git a/tests/components/weather/test_websocket_api.py b/tests/components/weather/test_websocket_api.py index 5c8a785771fb1f..11c5baafa301ac 100644 --- a/tests/components/weather/test_websocket_api.py +++ b/tests/components/weather/test_websocket_api.py @@ -33,7 +33,7 @@ async def test_device_class_units( "pressure_unit": ["hPa", "inHg", "mbar", "mmHg"], "temperature_unit": ["°C", "°F"], "visibility_unit": ["km", "mi"], - "wind_speed_unit": ["ft/s", "km/h", "kn", "m/s", "mph"], + "wind_speed_unit": ["Beaufort", "ft/s", "km/h", "kn", "m/s", "mph"], } } diff --git a/tests/helpers/template/extensions/test_base64.py b/tests/helpers/template/extensions/test_base64.py index b0c1fb35134d05..f1679633431f4a 100644 --- a/tests/helpers/template/extensions/test_base64.py +++ b/tests/helpers/template/extensions/test_base64.py @@ -5,7 +5,8 @@ import pytest from homeassistant.core import HomeAssistant -from homeassistant.helpers import template + +from tests.helpers.template.helpers import render @pytest.mark.parametrize( @@ -18,26 +19,19 @@ ) def test_base64_encode(hass: HomeAssistant, value_template: str, expected: str) -> None: """Test the base64_encode filter.""" - assert template.Template(value_template, hass).async_render() == expected + assert render(hass, value_template) == expected def test_base64_decode(hass: HomeAssistant) -> None: """Test the base64_decode filter.""" assert ( - template.Template( - '{{ "aG9tZWFzc2lzdGFudA==" | base64_decode }}', hass - ).async_render() - == "homeassistant" + render(hass, '{{ "aG9tZWFzc2lzdGFudA==" | base64_decode }}') == "homeassistant" ) assert ( - template.Template( - '{{ "aG9tZWFzc2lzdGFudA==" | base64_decode(None) }}', hass - ).async_render() + render(hass, '{{ "aG9tZWFzc2lzdGFudA==" | base64_decode(None) }}') == b"homeassistant" ) assert ( - template.Template( - '{{ "aG9tZWFzc2lzdGFudA==" | base64_decode("ascii") }}', hass - ).async_render() + render(hass, '{{ "aG9tZWFzc2lzdGFudA==" | base64_decode("ascii") }}') == "homeassistant" ) diff --git a/tests/helpers/template/extensions/test_collection.py b/tests/helpers/template/extensions/test_collection.py index 88cdb00dd194c6..d392d43c63583e 100644 --- a/tests/helpers/template/extensions/test_collection.py +++ b/tests/helpers/template/extensions/test_collection.py @@ -8,7 +8,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template + +from tests.helpers.template.helpers import render @pytest.mark.parametrize( @@ -27,10 +28,7 @@ ) def test_is_list(hass: HomeAssistant, value: Any, expected: bool) -> None: """Test list test.""" - assert ( - template.Template("{{ value is list }}", hass).async_render({"value": value}) - == expected - ) + assert render(hass, "{{ value is list }}", {"value": value}) == expected @pytest.mark.parametrize( @@ -49,10 +47,7 @@ def test_is_list(hass: HomeAssistant, value: Any, expected: bool) -> None: ) def test_is_set(hass: HomeAssistant, value: Any, expected: bool) -> None: """Test set test.""" - assert ( - template.Template("{{ value is set }}", hass).async_render({"value": value}) - == expected - ) + assert render(hass, "{{ value is set }}", {"value": value}) == expected @pytest.mark.parametrize( @@ -71,10 +66,7 @@ def test_is_set(hass: HomeAssistant, value: Any, expected: bool) -> None: ) def test_is_tuple(hass: HomeAssistant, value: Any, expected: bool) -> None: """Test tuple test.""" - assert ( - template.Template("{{ value is tuple }}", hass).async_render({"value": value}) - == expected - ) + assert render(hass, "{{ value is tuple }}", {"value": value}) == expected @pytest.mark.parametrize( @@ -93,8 +85,7 @@ def test_is_tuple(hass: HomeAssistant, value: Any, expected: bool) -> None: def test_set(hass: HomeAssistant, value: Any, expected: bool) -> None: """Test set conversion.""" assert ( - template.Template("{{ set(value) }}", hass).async_render({"value": value}) - == list(expected.values())[0] + render(hass, "{{ set(value) }}", {"value": value}) == list(expected.values())[0] ) @@ -113,9 +104,7 @@ def test_set(hass: HomeAssistant, value: Any, expected: bool) -> None: ) def test_tuple(hass: HomeAssistant, value: Any, expected: bool) -> None: """Test tuple conversion.""" - result = template.Template("{{ tuple(value) }}", hass).async_render( - {"value": value} - ) + result = render(hass, "{{ tuple(value) }}", {"value": value}) expected_value = list(expected.values())[0] if isinstance(value, set): # Sets don't have predictable order assert set(result) == set(expected_value) @@ -133,18 +122,11 @@ def test_tuple(hass: HomeAssistant, value: Any, expected: bool) -> None: ) def test_zip(hass: HomeAssistant, cola, colb, expected) -> None: """Test zip.""" - assert ( - template.Template("{{ zip(cola, colb) | list }}", hass).async_render( - {"cola": cola, "colb": colb} - ) - == expected - ) - assert ( - template.Template( - "[{% for a, b in zip(cola, colb) %}({{a}}, {{b}}), {% endfor %}]", hass - ).async_render({"cola": cola, "colb": colb}) - == expected - ) + for tpl in ( + "{{ zip(cola, colb) | list }}", + "[{% for a, b in zip(cola, colb) %}({{a}}, {{b}}), {% endfor %}]", + ): + assert render(hass, tpl, {"cola": cola, "colb": colb}) == expected @pytest.mark.parametrize( @@ -156,38 +138,27 @@ def test_zip(hass: HomeAssistant, cola, colb, expected) -> None: ) def test_unzip(hass: HomeAssistant, col, expected) -> None: """Test unzipping using zip.""" - assert ( - template.Template("{{ zip(*col) | list }}", hass).async_render({"col": col}) - == expected - ) - assert ( - template.Template( - "{% set a, b = zip(*col) %}[{{a}}, {{b}}]", hass - ).async_render({"col": col}) - == expected - ) + for tpl in ( + "{{ zip(*col) | list }}", + "{% set a, b = zip(*col) %}[{{a}}, {{b}}]", + ): + assert render(hass, tpl, {"col": col}) == expected def test_shuffle(hass: HomeAssistant) -> None: """Test shuffle.""" # Test basic shuffle - result = template.Template("{{ shuffle([1, 2, 3, 4, 5]) }}", hass).async_render() + result = render(hass, "{{ shuffle([1, 2, 3, 4, 5]) }}") assert len(result) == 5 assert set(result) == {1, 2, 3, 4, 5} # Test shuffle with seed - result1 = template.Template( - "{{ shuffle([1, 2, 3, 4, 5], seed=42) }}", hass - ).async_render() - result2 = template.Template( - "{{ shuffle([1, 2, 3, 4, 5], seed=42) }}", hass - ).async_render() + result1 = render(hass, "{{ shuffle([1, 2, 3, 4, 5], seed=42) }}") + result2 = render(hass, "{{ shuffle([1, 2, 3, 4, 5], seed=42) }}") assert result1 == result2 # Same seed should give same result # Test shuffle with different seed - result3 = template.Template( - "{{ shuffle([1, 2, 3, 4, 5], seed=123) }}", hass - ).async_render() + result3 = render(hass, "{{ shuffle([1, 2, 3, 4, 5], seed=123) }}") # Different seeds should usually give different results # (but we can't guarantee it for small lists) assert len(result3) == 5 @@ -197,144 +168,115 @@ def test_shuffle(hass: HomeAssistant) -> None: def test_flatten(hass: HomeAssistant) -> None: """Test flatten.""" # Test basic flattening - assert template.Template( - "{{ flatten([[1, 2], [3, 4]]) }}", hass - ).async_render() == [1, 2, 3, 4] + assert render(hass, "{{ flatten([[1, 2], [3, 4]]) }}") == [1, 2, 3, 4] # Test nested flattening - assert template.Template( - "{{ flatten([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) }}", hass - ).async_render() == [1, 2, 3, 4, 5, 6, 7, 8] + expected = [1, 2, 3, 4, 5, 6, 7, 8] + assert ( + render(hass, "{{ flatten([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) }}") == expected + ) # Test flattening with levels - assert template.Template( - "{{ flatten([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], levels=1) }}", hass - ).async_render() == [[1, 2], [3, 4], [5, 6], [7, 8]] + assert render( + hass, "{{ flatten([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], levels=1) }}" + ) == [[1, 2], [3, 4], [5, 6], [7, 8]] # Test mixed types - assert template.Template( - "{{ flatten([[1, 'a'], [2, 'b']]) }}", hass - ).async_render() == [1, "a", 2, "b"] + assert render(hass, "{{ flatten([[1, 'a'], [2, 'b']]) }}") == [1, "a", 2, "b"] # Test empty list - assert template.Template("{{ flatten([]) }}", hass).async_render() == [] + assert render(hass, "{{ flatten([]) }}") == [] # Test single level - assert template.Template("{{ flatten([1, 2, 3]) }}", hass).async_render() == [ - 1, - 2, - 3, - ] + assert render(hass, "{{ flatten([1, 2, 3]) }}") == [1, 2, 3] def test_intersect(hass: HomeAssistant) -> None: """Test intersect.""" # Test basic intersection - result = template.Template( - "{{ [1, 2, 3, 4] | intersect([3, 4, 5, 6]) | sort }}", hass - ).async_render() + result = render(hass, "{{ [1, 2, 3, 4] | intersect([3, 4, 5, 6]) | sort }}") assert result == [3, 4] # Test no intersection - result = template.Template("{{ [1, 2] | intersect([3, 4]) }}", hass).async_render() + result = render(hass, "{{ [1, 2] | intersect([3, 4]) }}") assert result == [] # Test string intersection - result = template.Template( - "{{ ['a', 'b', 'c'] | intersect(['b', 'c', 'd']) | sort }}", hass - ).async_render() + result = render(hass, "{{ ['a', 'b', 'c'] | intersect(['b', 'c', 'd']) | sort }}") assert result == ["b", "c"] # Test empty list intersection - result = template.Template("{{ [] | intersect([1, 2, 3]) }}", hass).async_render() + result = render(hass, "{{ [] | intersect([1, 2, 3]) }}") assert result == [] def test_difference(hass: HomeAssistant) -> None: """Test difference.""" # Test basic difference - result = template.Template( - "{{ [1, 2, 3, 4] | difference([3, 4, 5, 6]) | sort }}", hass - ).async_render() + result = render(hass, "{{ [1, 2, 3, 4] | difference([3, 4, 5, 6]) | sort }}") assert result == [1, 2] # Test no difference - result = template.Template( - "{{ [1, 2] | difference([1, 2, 3, 4]) }}", hass - ).async_render() + result = render(hass, "{{ [1, 2] | difference([1, 2, 3, 4]) }}") assert result == [] # Test string difference - result = template.Template( - "{{ ['a', 'b', 'c'] | difference(['b', 'c', 'd']) | sort }}", hass - ).async_render() + result = render(hass, "{{ ['a', 'b', 'c'] | difference(['b', 'c', 'd']) | sort }}") assert result == ["a"] # Test empty list difference - result = template.Template("{{ [] | difference([1, 2, 3]) }}", hass).async_render() + result = render(hass, "{{ [] | difference([1, 2, 3]) }}") assert result == [] def test_union(hass: HomeAssistant) -> None: """Test union.""" # Test basic union - result = template.Template( - "{{ [1, 2, 3] | union([3, 4, 5]) | sort }}", hass - ).async_render() + result = render(hass, "{{ [1, 2, 3] | union([3, 4, 5]) | sort }}") assert result == [1, 2, 3, 4, 5] # Test string union - result = template.Template( - "{{ ['a', 'b'] | union(['b', 'c']) | sort }}", hass - ).async_render() + result = render(hass, "{{ ['a', 'b'] | union(['b', 'c']) | sort }}") assert result == ["a", "b", "c"] # Test empty list union - result = template.Template( - "{{ [] | union([1, 2, 3]) | sort }}", hass - ).async_render() + result = render(hass, "{{ [] | union([1, 2, 3]) | sort }}") assert result == [1, 2, 3] # Test duplicate elements - result = template.Template( - "{{ [1, 1, 2, 2] | union([2, 2, 3, 3]) | sort }}", hass - ).async_render() + result = render(hass, "{{ [1, 1, 2, 2] | union([2, 2, 3, 3]) | sort }}") assert result == [1, 2, 3] def test_symmetric_difference(hass: HomeAssistant) -> None: """Test symmetric_difference.""" # Test basic symmetric difference - result = template.Template( - "{{ [1, 2, 3, 4] | symmetric_difference([3, 4, 5, 6]) | sort }}", hass - ).async_render() + result = render( + hass, "{{ [1, 2, 3, 4] | symmetric_difference([3, 4, 5, 6]) | sort }}" + ) assert result == [1, 2, 5, 6] # Test no symmetric difference (identical sets) - result = template.Template( - "{{ [1, 2, 3] | symmetric_difference([1, 2, 3]) }}", hass - ).async_render() + result = render(hass, "{{ [1, 2, 3] | symmetric_difference([1, 2, 3]) }}") assert result == [] # Test string symmetric difference - result = template.Template( - "{{ ['a', 'b', 'c'] | symmetric_difference(['b', 'c', 'd']) | sort }}", hass - ).async_render() + result = render( + hass, "{{ ['a', 'b', 'c'] | symmetric_difference(['b', 'c', 'd']) | sort }}" + ) assert result == ["a", "d"] # Test empty list symmetric difference - result = template.Template( - "{{ [] | symmetric_difference([1, 2, 3]) | sort }}", hass - ).async_render() + result = render(hass, "{{ [] | symmetric_difference([1, 2, 3]) | sort }}") assert result == [1, 2, 3] def test_collection_functions_as_tests(hass: HomeAssistant) -> None: """Test that type checking functions work as tests.""" # Test various type checking functions - assert template.Template("{{ [1,2,3] is list }}", hass).async_render() - assert template.Template("{{ set([1,2,3]) is set }}", hass).async_render() - assert template.Template("{{ (1,2,3) is tuple }}", hass).async_render() + assert render(hass, "{{ [1,2,3] is list }}") + assert render(hass, "{{ set([1,2,3]) is set }}") + assert render(hass, "{{ (1,2,3) is tuple }}") def test_collection_error_handling(hass: HomeAssistant) -> None: @@ -342,16 +284,16 @@ def test_collection_error_handling(hass: HomeAssistant) -> None: # Test flatten with non-iterable with pytest.raises(TemplateError, match="flatten expected a list"): - template.Template("{{ flatten(123) }}", hass).async_render() + render(hass, "{{ flatten(123) }}") # Test intersect with non-iterable with pytest.raises(TemplateError, match="intersect expected a list"): - template.Template("{{ [1, 2] | intersect(123) }}", hass).async_render() + render(hass, "{{ [1, 2] | intersect(123) }}") # Test difference with non-iterable with pytest.raises(TemplateError, match="difference expected a list"): - template.Template("{{ [1, 2] | difference(123) }}", hass).async_render() + render(hass, "{{ [1, 2] | difference(123) }}") # Test shuffle with no arguments with pytest.raises(TemplateError, match="shuffle expected at least 1 argument"): - template.Template("{{ shuffle() }}", hass).async_render() + render(hass, "{{ shuffle() }}") diff --git a/tests/helpers/template/extensions/test_crypto.py b/tests/helpers/template/extensions/test_crypto.py index f1e4c3b39cc2b9..7fe316c497dcdf 100644 --- a/tests/helpers/template/extensions/test_crypto.py +++ b/tests/helpers/template/extensions/test_crypto.py @@ -3,56 +3,33 @@ from __future__ import annotations from homeassistant.core import HomeAssistant -from homeassistant.helpers import template + +from tests.helpers.template.helpers import render def test_md5(hass: HomeAssistant) -> None: """Test the md5 function and filter.""" - assert ( - template.Template("{{ md5('Home Assistant') }}", hass).async_render() - == "3d15e5c102c3413d0337393c3287e006" - ) - - assert ( - template.Template("{{ 'Home Assistant' | md5 }}", hass).async_render() - == "3d15e5c102c3413d0337393c3287e006" - ) + ha_md5 = "3d15e5c102c3413d0337393c3287e006" + assert render(hass, "{{ md5('Home Assistant') }}") == ha_md5 + assert render(hass, "{{ 'Home Assistant' | md5 }}") == ha_md5 def test_sha1(hass: HomeAssistant) -> None: """Test the sha1 function and filter.""" - assert ( - template.Template("{{ sha1('Home Assistant') }}", hass).async_render() - == "c8fd3bb19b94312664faa619af7729bdbf6e9f8a" - ) - - assert ( - template.Template("{{ 'Home Assistant' | sha1 }}", hass).async_render() - == "c8fd3bb19b94312664faa619af7729bdbf6e9f8a" - ) + ha_sha1 = "c8fd3bb19b94312664faa619af7729bdbf6e9f8a" + assert render(hass, "{{ sha1('Home Assistant') }}") == ha_sha1 + assert render(hass, "{{ 'Home Assistant' | sha1 }}") == ha_sha1 def test_sha256(hass: HomeAssistant) -> None: """Test the sha256 function and filter.""" - assert ( - template.Template("{{ sha256('Home Assistant') }}", hass).async_render() - == "2a366abb0cd47f51f3725bf0fb7ebcb4fefa6e20f4971e25fe2bb8da8145ce2b" - ) - - assert ( - template.Template("{{ 'Home Assistant' | sha256 }}", hass).async_render() - == "2a366abb0cd47f51f3725bf0fb7ebcb4fefa6e20f4971e25fe2bb8da8145ce2b" - ) + ha_sha256 = "2a366abb0cd47f51f3725bf0fb7ebcb4fefa6e20f4971e25fe2bb8da8145ce2b" + assert render(hass, "{{ sha256('Home Assistant') }}") == ha_sha256 + assert render(hass, "{{ 'Home Assistant' | sha256 }}") == ha_sha256 def test_sha512(hass: HomeAssistant) -> None: """Test the sha512 function and filter.""" - assert ( - template.Template("{{ sha512('Home Assistant') }}", hass).async_render() - == "9e3c2cdd1fbab0037378d37e1baf8a3a4bf92c54b56ad1d459deee30ccbb2acbebd7a3614552ea08992ad27dedeb7b4c5473525ba90cb73dbe8b9ec5f69295bb" - ) - - assert ( - template.Template("{{ 'Home Assistant' | sha512 }}", hass).async_render() - == "9e3c2cdd1fbab0037378d37e1baf8a3a4bf92c54b56ad1d459deee30ccbb2acbebd7a3614552ea08992ad27dedeb7b4c5473525ba90cb73dbe8b9ec5f69295bb" - ) + ha_sha512 = "9e3c2cdd1fbab0037378d37e1baf8a3a4bf92c54b56ad1d459deee30ccbb2acbebd7a3614552ea08992ad27dedeb7b4c5473525ba90cb73dbe8b9ec5f69295bb" + assert render(hass, "{{ sha512('Home Assistant') }}") == ha_sha512 + assert render(hass, "{{ 'Home Assistant' | sha512 }}") == ha_sha512 diff --git a/tests/helpers/template/extensions/test_math.py b/tests/helpers/template/extensions/test_math.py index 4cf26cdf517769..0ee79f4e0a327d 100644 --- a/tests/helpers/template/extensions/test_math.py +++ b/tests/helpers/template/extensions/test_math.py @@ -8,7 +8,6 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template from tests.helpers.template.helpers import render @@ -29,29 +28,19 @@ def test_logarithm(hass: HomeAssistant) -> None: ] for value, base, expected in tests: - assert ( - template.Template( - f"{{{{ {value} | log({base}) | round(1) }}}}", hass - ).async_render() - == expected - ) + assert render(hass, f"{{{{ {value} | log({base}) | round(1) }}}}") == expected - assert ( - template.Template( - f"{{{{ log({value}, {base}) | round(1) }}}}", hass - ).async_render() - == expected - ) + assert render(hass, f"{{{{ log({value}, {base}) | round(1) }}}}") == expected # Test handling of invalid input with pytest.raises(TemplateError): - template.Template("{{ invalid | log(_) }}", hass).async_render() + render(hass, "{{ invalid | log(_) }}") with pytest.raises(TemplateError): - template.Template("{{ log(invalid, _) }}", hass).async_render() + render(hass, "{{ log(invalid, _) }}") with pytest.raises(TemplateError): - template.Template("{{ 10 | log(invalid) }}", hass).async_render() + render(hass, "{{ 10 | log(invalid) }}") with pytest.raises(TemplateError): - template.Template("{{ log(10, invalid) }}", hass).async_render() + render(hass, "{{ log(10, invalid) }}") # Test handling of default return value assert render(hass, "{{ 'no_number' | log(10, 1) }}") == 1 @@ -73,19 +62,14 @@ def test_sine(hass: HomeAssistant) -> None: ] for value, expected in tests: - assert ( - template.Template( - f"{{{{ {value} | sin | round(3) }}}}", hass - ).async_render() - == expected - ) + assert render(hass, f"{{{{ {value} | sin | round(3) }}}}") == expected assert render(hass, f"{{{{ sin({value}) | round(3) }}}}") == expected # Test handling of invalid input with pytest.raises(TemplateError): - template.Template("{{ 'duck' | sin }}", hass).async_render() + render(hass, "{{ 'duck' | sin }}") with pytest.raises(TemplateError): - template.Template("{{ invalid | sin('duck') }}", hass).async_render() + render(hass, "{{ invalid | sin('duck') }}") # Test handling of default return value assert render(hass, "{{ 'no_number' | sin(1) }}") == 1 @@ -105,17 +89,12 @@ def test_cosine(hass: HomeAssistant) -> None: ] for value, expected in tests: - assert ( - template.Template( - f"{{{{ {value} | cos | round(3) }}}}", hass - ).async_render() - == expected - ) + assert render(hass, f"{{{{ {value} | cos | round(3) }}}}") == expected assert render(hass, f"{{{{ cos({value}) | round(3) }}}}") == expected # Test handling of invalid input with pytest.raises(TemplateError): - template.Template("{{ 'duck' | cos }}", hass).async_render() + render(hass, "{{ 'duck' | cos }}") # Test handling of default return value assert render(hass, "{{ 'no_number' | cos(1) }}") == 1 @@ -134,17 +113,12 @@ def test_tangent(hass: HomeAssistant) -> None: ] for value, expected in tests: - assert ( - template.Template( - f"{{{{ {value} | tan | round(3) }}}}", hass - ).async_render() - == expected - ) + assert render(hass, f"{{{{ {value} | tan | round(3) }}}}") == expected assert render(hass, f"{{{{ tan({value}) | round(3) }}}}") == expected # Test handling of invalid input with pytest.raises(TemplateError): - template.Template("{{ 'duck' | tan }}", hass).async_render() + render(hass, "{{ 'duck' | tan }}") # Test handling of default return value assert render(hass, "{{ 'no_number' | tan(1) }}") == 1 @@ -165,17 +139,14 @@ def test_square_root(hass: HomeAssistant) -> None: ] for value, expected in tests: - assert ( - template.Template(f"{{{{ {value} | sqrt }}}}", hass).async_render() - == expected - ) + assert render(hass, f"{{{{ {value} | sqrt }}}}") == expected assert render(hass, f"{{{{ sqrt({value}) }}}}") == expected # Test handling of invalid input with pytest.raises(TemplateError): - template.Template("{{ 'duck' | sqrt }}", hass).async_render() + render(hass, "{{ 'duck' | sqrt }}") with pytest.raises(TemplateError): - template.Template("{{ -1 | sqrt }}", hass).async_render() + render(hass, "{{ -1 | sqrt }}") # Test handling of default return value assert render(hass, "{{ 'no_number' | sqrt(1) }}") == 1 @@ -217,114 +188,95 @@ def test_arc_functions(hass: HomeAssistant) -> None: def test_average(hass: HomeAssistant) -> None: """Test the average function.""" - assert template.Template("{{ average([1, 2, 3]) }}", hass).async_render() == 2 - assert template.Template("{{ average(1, 2, 3) }}", hass).async_render() == 2 + assert render(hass, "{{ average([1, 2, 3]) }}") == 2 + assert render(hass, "{{ average(1, 2, 3) }}") == 2 # Testing of default values - assert template.Template("{{ average([1, 2, 3], -1) }}", hass).async_render() == 2 - assert template.Template("{{ average([], -1) }}", hass).async_render() == -1 - assert template.Template("{{ average([], default=-1) }}", hass).async_render() == -1 - assert ( - template.Template("{{ average([], 5, default=-1) }}", hass).async_render() == -1 - ) - assert ( - template.Template("{{ average(1, 'a', 3, default=-1) }}", hass).async_render() - == -1 - ) + assert render(hass, "{{ average([1, 2, 3], -1) }}") == 2 + assert render(hass, "{{ average([], -1) }}") == -1 + assert render(hass, "{{ average([], default=-1) }}") == -1 + assert render(hass, "{{ average([], 5, default=-1) }}") == -1 + assert render(hass, "{{ average(1, 'a', 3, default=-1) }}") == -1 with pytest.raises(TemplateError): - template.Template("{{ average() }}", hass).async_render() + render(hass, "{{ average() }}") with pytest.raises(TemplateError): - template.Template("{{ average([]) }}", hass).async_render() + render(hass, "{{ average([]) }}") def test_median(hass: HomeAssistant) -> None: """Test the median function.""" - assert template.Template("{{ median([1, 2, 3]) }}", hass).async_render() == 2 - assert template.Template("{{ median([1, 2, 3, 4]) }}", hass).async_render() == 2.5 - assert template.Template("{{ median(1, 2, 3) }}", hass).async_render() == 2 + assert render(hass, "{{ median([1, 2, 3]) }}") == 2 + assert render(hass, "{{ median([1, 2, 3, 4]) }}") == 2.5 + assert render(hass, "{{ median(1, 2, 3) }}") == 2 # Testing of default values - assert template.Template("{{ median([1, 2, 3], -1) }}", hass).async_render() == 2 - assert template.Template("{{ median([], -1) }}", hass).async_render() == -1 - assert template.Template("{{ median([], default=-1) }}", hass).async_render() == -1 + assert render(hass, "{{ median([1, 2, 3], -1) }}") == 2 + assert render(hass, "{{ median([], -1) }}") == -1 + assert render(hass, "{{ median([], default=-1) }}") == -1 with pytest.raises(TemplateError): - template.Template("{{ median() }}", hass).async_render() + render(hass, "{{ median() }}") with pytest.raises(TemplateError): - template.Template("{{ median([]) }}", hass).async_render() + render(hass, "{{ median([]) }}") def test_statistical_mode(hass: HomeAssistant) -> None: """Test the statistical mode function.""" - assert ( - template.Template("{{ statistical_mode([1, 1, 2, 3]) }}", hass).async_render() - == 1 - ) - assert ( - template.Template("{{ statistical_mode(1, 1, 2, 3) }}", hass).async_render() - == 1 - ) + assert render(hass, "{{ statistical_mode([1, 1, 2, 3]) }}") == 1 + assert render(hass, "{{ statistical_mode(1, 1, 2, 3) }}") == 1 # Testing of default values - assert ( - template.Template("{{ statistical_mode([1, 1, 2], -1) }}", hass).async_render() - == 1 - ) - assert ( - template.Template("{{ statistical_mode([], -1) }}", hass).async_render() == -1 - ) - assert ( - template.Template("{{ statistical_mode([], default=-1) }}", hass).async_render() - == -1 - ) + assert render(hass, "{{ statistical_mode([1, 1, 2], -1) }}") == 1 + assert render(hass, "{{ statistical_mode([], -1) }}") == -1 + assert render(hass, "{{ statistical_mode([], default=-1) }}") == -1 with pytest.raises(TemplateError): - template.Template("{{ statistical_mode() }}", hass).async_render() + render(hass, "{{ statistical_mode() }}") with pytest.raises(TemplateError): - template.Template("{{ statistical_mode([]) }}", hass).async_render() + render(hass, "{{ statistical_mode([]) }}") def test_min_max_functions(hass: HomeAssistant) -> None: """Test min and max functions.""" # Test min function - assert template.Template("{{ min([1, 2, 3]) }}", hass).async_render() == 1 - assert template.Template("{{ min(1, 2, 3) }}", hass).async_render() == 1 + assert render(hass, "{{ min([1, 2, 3]) }}") == 1 + assert render(hass, "{{ min(1, 2, 3) }}") == 1 # Test max function - assert template.Template("{{ max([1, 2, 3]) }}", hass).async_render() == 3 - assert template.Template("{{ max(1, 2, 3) }}", hass).async_render() == 3 + assert render(hass, "{{ max([1, 2, 3]) }}") == 3 + assert render(hass, "{{ max(1, 2, 3) }}") == 3 # Test error handling with pytest.raises(TemplateError): - template.Template("{{ min() }}", hass).async_render() + render(hass, "{{ min() }}") with pytest.raises(TemplateError): - template.Template("{{ max() }}", hass).async_render() + render(hass, "{{ max() }}") def test_bitwise_and(hass: HomeAssistant) -> None: """Test bitwise and.""" - assert template.Template("{{ bitwise_and(8, 2) }}", hass).async_render() == 0 - assert template.Template("{{ bitwise_and(10, 2) }}", hass).async_render() == 2 - assert template.Template("{{ bitwise_and(8, 8) }}", hass).async_render() == 8 + assert render(hass, "{{ bitwise_and(8, 2) }}") == 0 + assert render(hass, "{{ bitwise_and(10, 2) }}") == 2 + assert render(hass, "{{ bitwise_and(8, 8) }}") == 8 def test_bitwise_or(hass: HomeAssistant) -> None: """Test bitwise or.""" - assert template.Template("{{ bitwise_or(8, 2) }}", hass).async_render() == 10 - assert template.Template("{{ bitwise_or(8, 8) }}", hass).async_render() == 8 - assert template.Template("{{ bitwise_or(10, 2) }}", hass).async_render() == 10 + assert render(hass, "{{ bitwise_or(8, 2) }}") == 10 + assert render(hass, "{{ bitwise_or(8, 8) }}") == 8 + assert render(hass, "{{ bitwise_or(10, 2) }}") == 10 def test_bitwise_xor(hass: HomeAssistant) -> None: """Test bitwise xor.""" - assert template.Template("{{ bitwise_xor(8, 2) }}", hass).async_render() == 10 - assert template.Template("{{ bitwise_xor(8, 8) }}", hass).async_render() == 0 - assert template.Template("{{ bitwise_xor(10, 2) }}", hass).async_render() == 8 + assert render(hass, "{{ bitwise_xor(8, 2) }}") == 10 + assert render(hass, "{{ bitwise_xor(8, 8) }}") == 0 + assert render(hass, "{{ bitwise_xor(10, 2) }}") == 8 @pytest.mark.parametrize( @@ -361,30 +313,30 @@ def test_min_max_attribute(hass: HomeAssistant, attribute) -> None: }, ) assert ( - template.Template( - f"{{{{ (state_attr('test.object', 'objects') | min(attribute='{attribute}'))['{attribute}']}}}}", + render( hass, - ).async_render() + f"{{{{ (state_attr('test.object', 'objects') | min(attribute='{attribute}'))['{attribute}']}}}}", + ) == 1 ) assert ( - template.Template( - f"{{{{ (min(state_attr('test.object', 'objects'), attribute='{attribute}'))['{attribute}']}}}}", + render( hass, - ).async_render() + f"{{{{ (min(state_attr('test.object', 'objects'), attribute='{attribute}'))['{attribute}']}}}}", + ) == 1 ) assert ( - template.Template( - f"{{{{ (state_attr('test.object', 'objects') | max(attribute='{attribute}'))['{attribute}']}}}}", + render( hass, - ).async_render() + f"{{{{ (state_attr('test.object', 'objects') | max(attribute='{attribute}'))['{attribute}']}}}}", + ) == 3 ) assert ( - template.Template( - f"{{{{ (max(state_attr('test.object', 'objects'), attribute='{attribute}'))['{attribute}']}}}}", + render( hass, - ).async_render() + f"{{{{ (max(state_attr('test.object', 'objects'), attribute='{attribute}'))['{attribute}']}}}}", + ) == 3 ) diff --git a/tests/helpers/template/extensions/test_regex.py b/tests/helpers/template/extensions/test_regex.py index 290b55bad1fed1..1c508bfb3b1ad4 100644 --- a/tests/helpers/template/extensions/test_regex.py +++ b/tests/helpers/template/extensions/test_regex.py @@ -6,260 +6,169 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template + +from tests.helpers.template.helpers import render def test_regex_match(hass: HomeAssistant) -> None: """Test regex_match method.""" - tpl = template.Template( - r""" -{{ '123-456-7890' | regex_match('(\\d{3})-(\\d{3})-(\\d{4})') }} - """, - hass, - ) - assert tpl.async_render() is True - tpl = template.Template( - """ -{{ 'Home Assistant test' | regex_match('home', True) }} - """, - hass, + result = render( + hass, r"""{{ '123-456-7890' | regex_match('(\\d{3})-(\\d{3})-(\\d{4})') }}""" ) - assert tpl.async_render() is True + assert result is True - tpl = template.Template( - """ - {{ 'Another Home Assistant test' | regex_match('Home') }} - """, - hass, - ) - assert tpl.async_render() is False + result = render(hass, """{{ 'Home Assistant test' | regex_match('home', True) }}""") + assert result is True - tpl = template.Template( - """ -{{ ['Home Assistant test'] | regex_match('.*Assist') }} - """, - hass, - ) - assert tpl.async_render() is True + result = render(hass, """{{ 'Another Home Assistant test'|regex_match('Home') }}""") + assert result is False + + result = render(hass, """{{ ['Home Assistant test'] | regex_match('.*Assist') }}""") + assert result is True def test_match_test(hass: HomeAssistant) -> None: """Test match test.""" - tpl = template.Template( - r""" -{{ '123-456-7890' is match('(\\d{3})-(\\d{3})-(\\d{4})') }} - """, - hass, + + result = render( + hass, r"""{{ '123-456-7890' is match('(\\d{3})-(\\d{3})-(\\d{4})') }}""" ) - assert tpl.async_render() is True + assert result is True def test_regex_search(hass: HomeAssistant) -> None: """Test regex_search method.""" - tpl = template.Template( - r""" -{{ '123-456-7890' | regex_search('(\\d{3})-(\\d{3})-(\\d{4})') }} - """, - hass, - ) - assert tpl.async_render() is True - tpl = template.Template( - """ -{{ 'Home Assistant test' | regex_search('home', True) }} - """, - hass, + result = render( + hass, r"""{{ '123-456-7890' | regex_search('(\\d{3})-(\\d{3})-(\\d{4})') }}""" ) - assert tpl.async_render() is True + assert result is True - tpl = template.Template( - """ - {{ 'Another Home Assistant test' | regex_search('Home') }} - """, - hass, + result = render( + hass, """{{ 'Home Assistant test' | regex_search('home', True) }}""" ) - assert tpl.async_render() is True + assert result is True - tpl = template.Template( - """ -{{ ['Home Assistant test'] | regex_search('Assist') }} - """, - hass, + result = render( + hass, """ {{ 'Another Home Assistant test' | regex_search('Home') }}""" ) - assert tpl.async_render() is True + assert result is True + + result = render(hass, """{{ ['Home Assistant test'] | regex_search('Assist') }}""") + assert result is True def test_search_test(hass: HomeAssistant) -> None: """Test search test.""" - tpl = template.Template( - r""" -{{ '123-456-7890' is search('(\\d{3})-(\\d{3})-(\\d{4})') }} - """, - hass, + + result = render( + hass, r"""{{ '123-456-7890' is search('(\\d{3})-(\\d{3})-(\\d{4})') }}""" ) - assert tpl.async_render() is True + assert result is True def test_regex_replace(hass: HomeAssistant) -> None: """Test regex_replace method.""" - tpl = template.Template( - r""" -{{ 'Hello World' | regex_replace('(Hello\\s)',) }} - """, - hass, - ) - assert tpl.async_render() == "World" - tpl = template.Template( - """ -{{ ['Home hinderant test'] | regex_replace('hinder', 'Assist') }} - """, - hass, + result = render(hass, r"""{{ 'Hello World' | regex_replace('(Hello\\s)',) }}""") + assert result == "World" + + result = render( + hass, """{{ ['Home hinderant test'] | regex_replace('hinder', 'Assist') }}""" ) - assert tpl.async_render() == ["Home Assistant test"] + assert result == ["Home Assistant test"] def test_regex_findall(hass: HomeAssistant) -> None: """Test regex_findall method.""" - tpl = template.Template( - """ -{{ 'Flight from JFK to LHR' | regex_findall('([A-Z]{3})') }} - """, - hass, + + result = render( + hass, """{{ 'Flight from JFK to LHR' | regex_findall('([A-Z]{3})') }}""" ) - assert tpl.async_render() == ["JFK", "LHR"] + assert result == ["JFK", "LHR"] def test_regex_findall_index(hass: HomeAssistant) -> None: """Test regex_findall_index method.""" - tpl = template.Template( - """ -{{ 'Flight from JFK to LHR' | regex_findall_index('([A-Z]{3})', 0) }} - """, + + result = render( hass, + """{{ 'Flight from JFK to LHR' | regex_findall_index('([A-Z]{3})', 0) }}""", ) - assert tpl.async_render() == "JFK" + assert result == "JFK" - tpl = template.Template( - """ -{{ 'Flight from JFK to LHR' | regex_findall_index('([A-Z]{3})', 1) }} - """, + result = render( hass, + """{{ 'Flight from JFK to LHR' | regex_findall_index('([A-Z]{3})', 1) }}""", ) - assert tpl.async_render() == "LHR" + assert result == "LHR" def test_regex_ignorecase_parameter(hass: HomeAssistant) -> None: """Test ignorecase parameter across all regex functions.""" # Test regex_match with ignorecase - tpl = template.Template( - """ -{{ 'TEST' | regex_match('test', True) }} - """, - hass, - ) - assert tpl.async_render() is True + + result = render(hass, """{{ 'TEST' | regex_match('test', True) }}""") + assert result is True # Test regex_search with ignorecase - tpl = template.Template( - """ -{{ 'TEST STRING' | regex_search('test', True) }} - """, - hass, - ) - assert tpl.async_render() is True + + result = render(hass, """{{ 'TEST STRING' | regex_search('test', True) }}""") + assert result is True # Test regex_replace with ignorecase - tpl = template.Template( - """ -{{ 'TEST' | regex_replace('test', 'replaced', True) }} - """, - hass, - ) - assert tpl.async_render() == "replaced" + + result = render(hass, """{{ 'TEST' | regex_replace('test', 'replaced', True) }}""") + assert result == "replaced" # Test regex_findall with ignorecase - tpl = template.Template( - """ -{{ 'TEST test Test' | regex_findall('test', True) }} - """, - hass, - ) - assert tpl.async_render() == ["TEST", "test", "Test"] + + result = render(hass, """{{ 'TEST test Test' | regex_findall('test', True) }}""") + assert result == ["TEST", "test", "Test"] def test_regex_with_non_string_input(hass: HomeAssistant) -> None: """Test regex functions with non-string input (automatic conversion).""" # Test with integer - tpl = template.Template( - r""" -{{ 12345 | regex_match('\\d+') }} - """, - hass, - ) - assert tpl.async_render() is True + + result = render(hass, r"""{{ 12345 | regex_match('\\d+') }}""") + assert result is True # Test with list (string conversion) - tpl = template.Template( - r""" -{{ [1, 2, 3] | regex_search('\\d') }} - """, - hass, - ) - assert tpl.async_render() is True + + result = render(hass, r"""{{ [1, 2, 3] | regex_search('\\d') }}""") + assert result is True def test_regex_edge_cases(hass: HomeAssistant) -> None: """Test regex functions with edge cases.""" # Test with empty string - tpl = template.Template( - """ -{{ '' | regex_match('.*') }} - """, - hass, - ) - assert tpl.async_render() is True + + assert render(hass, """{{ '' | regex_match('.*') }}""") is True # Test regex_findall_index with out of bounds index - tpl = template.Template( - """ -{{ 'test' | regex_findall_index('t', 5) }} - """, - hass, - ) with pytest.raises(TemplateError): - tpl.async_render() + render(hass, """{{ 'test' | regex_findall_index('t', 5) }}""") # Test with invalid regex pattern - tpl = template.Template( - """ -{{ 'test' | regex_match('[') }} - """, - hass, - ) with pytest.raises(TemplateError): # re.error wrapped in TemplateError - tpl.async_render() + render(hass, """{{ 'test' | regex_match('[') }}""") def test_regex_groups_and_replacement_patterns(hass: HomeAssistant) -> None: """Test regex with groups and replacement patterns.""" # Test replacement with groups - tpl = template.Template( - r""" -{{ 'John Doe' | regex_replace('(\\w+) (\\w+)', '\\2, \\1') }} - """, - hass, + + result = render( + hass, r"""{{ 'John Doe' | regex_replace('(\\w+) (\\w+)', '\\2, \\1') }}""" ) - assert tpl.async_render() == "Doe, John" + assert result == "Doe, John" # Test findall with groups - tpl = template.Template( - r""" -{{ 'Email: test@example.com, Phone: 123-456-7890' | regex_findall('(\\w+@\\w+\\.\\w+)|(\\d{3}-\\d{3}-\\d{4})') }} - """, + result = render( hass, + r"""{{ 'Email: test@example.com, Phone: 123-456-7890' | regex_findall('(\\w+@\\w+\\.\\w+)|(\\d{3}-\\d{3}-\\d{4})') }}""", ) - result = tpl.async_render() # The result will contain tuples with empty strings for non-matching groups assert len(result) == 2 diff --git a/tests/helpers/template/extensions/test_string.py b/tests/helpers/template/extensions/test_string.py index 241bf40eef1092..88a7ef758d3444 100644 --- a/tests/helpers/template/extensions/test_string.py +++ b/tests/helpers/template/extensions/test_string.py @@ -3,7 +3,8 @@ from __future__ import annotations from homeassistant.core import HomeAssistant -from homeassistant.helpers import template + +from tests.helpers.template.helpers import render def test_ordinal(hass: HomeAssistant) -> None: @@ -20,63 +21,48 @@ def test_ordinal(hass: HomeAssistant) -> None: ] for value, expected in tests: - assert ( - template.Template(f"{{{{ {value} | ordinal }}}}", hass).async_render() - == expected - ) + assert render(hass, f"{{{{ {value} | ordinal }}}}") == expected def test_slugify(hass: HomeAssistant) -> None: """Test the slugify filter.""" # Test as global function - assert ( - template.Template('{{ slugify("Home Assistant") }}', hass).async_render() - == "home_assistant" - ) + assert render(hass, '{{ slugify("Home Assistant") }}') == "home_assistant" # Test as filter - assert ( - template.Template('{{ "Home Assistant" | slugify }}', hass).async_render() - == "home_assistant" - ) + assert render(hass, '{{ "Home Assistant" | slugify }}') == "home_assistant" # Test with custom separator as global - assert ( - template.Template('{{ slugify("Home Assistant", "-") }}', hass).async_render() - == "home-assistant" - ) + assert render(hass, '{{ slugify("Home Assistant", "-") }}') == "home-assistant" # Test with custom separator as filter - assert ( - template.Template('{{ "Home Assistant" | slugify("-") }}', hass).async_render() - == "home-assistant" - ) + assert render(hass, '{{ "Home Assistant" | slugify("-") }}') == "home-assistant" def test_urlencode(hass: HomeAssistant) -> None: """Test the urlencode method.""" # Test with dictionary - tpl = template.Template( - "{% set dict = {'foo': 'x&y', 'bar': 42} %}{{ dict | urlencode }}", - hass, + + result = render( + hass, "{% set dict = {'foo': 'x&y', 'bar': 42} %}{{ dict | urlencode }}" ) - assert tpl.async_render() == "foo=x%26y&bar=42" + assert result == "foo=x%26y&bar=42" # Test with string - tpl = template.Template( - "{% set string = 'the quick brown fox = true' %}{{ string | urlencode }}", - hass, + + result = render( + hass, "{% set string = 'the quick brown fox = true' %}{{ string | urlencode }}" ) - assert tpl.async_render() == "the%20quick%20brown%20fox%20%3D%20true" + assert result == "the%20quick%20brown%20fox%20%3D%20true" def test_string_functions_with_non_string_input(hass: HomeAssistant) -> None: """Test string functions with non-string input (automatic conversion).""" # Test ordinal with integer - assert template.Template("{{ 42 | ordinal }}", hass).async_render() == "42nd" + assert render(hass, "{{ 42 | ordinal }}") == "42nd" # Test slugify with integer - Note: Jinja2 may return integer for simple cases - result = template.Template("{{ 123 | slugify }}", hass).async_render() + result = render(hass, "{{ 123 | slugify }}") # Accept either string or integer result for simple numeric cases assert result in ["123", 123] @@ -94,10 +80,7 @@ def test_ordinal_edge_cases(hass: HomeAssistant) -> None: ] for value, expected in teens_tests: - assert ( - template.Template(f"{{{{ {value} | ordinal }}}}", hass).async_render() - == expected - ) + assert render(hass, f"{{{{ {value} | ordinal }}}}") == expected # Test other numbers ending in 1, 2, 3 other_tests = [ @@ -110,10 +93,7 @@ def test_ordinal_edge_cases(hass: HomeAssistant) -> None: ] for value, expected in other_tests: - assert ( - template.Template(f"{{{{ {value} | ordinal }}}}", hass).async_render() - == expected - ) + assert render(hass, f"{{{{ {value} | ordinal }}}}") == expected def test_slugify_various_separators(hass: HomeAssistant) -> None: @@ -127,38 +107,27 @@ def test_slugify_various_separators(hass: HomeAssistant) -> None: for text, separator, expected in test_cases: # Test as global function - assert ( - template.Template( - f'{{{{ slugify("{text}", "{separator}") }}}}', hass - ).async_render() - == expected - ) + assert render(hass, f'{{{{ slugify("{text}", "{separator}") }}}}') == expected # Test as filter - assert ( - template.Template( - f'{{{{ "{text}" | slugify("{separator}") }}}}', hass - ).async_render() - == expected - ) + assert render(hass, f'{{{{ "{text}" | slugify("{separator}") }}}}') == expected def test_urlencode_various_types(hass: HomeAssistant) -> None: """Test urlencode with various data types.""" # Test with nested dictionary values - tpl = template.Template( - "{% set data = {'key': 'value with spaces', 'num': 123} %}{{ data | urlencode }}", + result = render( hass, + "{% set data = {'key': 'value with spaces', 'num': 123} %}{{ data | urlencode }}", ) - result = tpl.async_render() # URL encoding can have different order, so check both parts are present # Note: urllib.parse.urlencode uses + for spaces in form data assert "key=value+with+spaces" in result assert "num=123" in result # Test with special characters - tpl = template.Template( - "{% set data = {'special': 'a+b=c&d'} %}{{ data | urlencode }}", - hass, + + result = render( + hass, "{% set data = {'special': 'a+b=c&d'} %}{{ data | urlencode }}" ) - assert tpl.async_render() == "special=a%2Bb%3Dc%26d" + assert result == "special=a%2Bb%3Dc%26d" diff --git a/tests/helpers/template/test_init.py b/tests/helpers/template/test_init.py index c4e7c058a8300c..d7d7b1b5253e1d 100644 --- a/tests/helpers/template/test_init.py +++ b/tests/helpers/template/test_init.py @@ -81,8 +81,7 @@ def _set_up_units(hass: HomeAssistant) -> None: async def test_template_render_missing_hass(hass: HomeAssistant) -> None: """Test template render when hass is not set.""" hass.states.async_set("sensor.test", "23") - template_str = "{{ states('sensor.test') }}" - template_obj = template.Template(template_str, None) + template_obj = template.Template("{{ states('sensor.test') }}", None) template.render_info_cv.set(template.RenderInfo(template_obj)) with pytest.raises(RuntimeError, match="hass not set while rendering"): @@ -96,8 +95,7 @@ async def test_template_render_info_collision(hass: HomeAssistant) -> None: in the wrong thread. """ hass.states.async_set("sensor.test", "23") - template_str = "{{ states('sensor.test') }}" - template_obj = template.Template(template_str, None) + template_obj = template.Template("{{ states('sensor.test') }}", None) template_obj.hass = hass template.render_info_cv.set(template.RenderInfo(template_obj)) @@ -148,29 +146,21 @@ def test_invalid_template(hass: HomeAssistant) -> None: def test_referring_states_by_entity_id(hass: HomeAssistant) -> None: """Test referring states by entity id.""" hass.states.async_set("test.object", "happy") - assert ( - template.Template("{{ states.test.object.state }}", hass).async_render() - == "happy" - ) + assert render(hass, "{{ states.test.object.state }}") == "happy" - assert ( - template.Template('{{ states["test.object"].state }}', hass).async_render() - == "happy" - ) + assert render(hass, '{{ states["test.object"].state }}') == "happy" - assert ( - template.Template('{{ states("test.object") }}', hass).async_render() == "happy" - ) + assert render(hass, '{{ states("test.object") }}') == "happy" def test_invalid_entity_id(hass: HomeAssistant) -> None: """Test referring states by entity id.""" with pytest.raises(TemplateError): - template.Template('{{ states["big.fat..."] }}', hass).async_render() + render(hass, '{{ states["big.fat..."] }}') with pytest.raises(TemplateError): - template.Template('{{ states.test["big.fat..."] }}', hass).async_render() + render(hass, '{{ states.test["big.fat..."] }}') with pytest.raises(TemplateError): - template.Template('{{ states["invalid/domain"] }}', hass).async_render() + render(hass, '{{ states["invalid/domain"] }}') def test_raise_exception_on_error(hass: HomeAssistant) -> None: @@ -246,35 +236,35 @@ async def test_import(hass: HomeAssistant) -> None: assert "test.jinja" in template._get_hass_loader(hass).sources assert "inner/inner_test.jinja" in template._get_hass_loader(hass).sources assert ( - template.Template( - """ - {% import 'test.jinja' as t %} - {{ t.test_macro() }} {{ t.test_variable }} - """, + render( hass, - ).async_render() + """ + {% import 'test.jinja' as t %} + {{ t.test_macro() }} {{ t.test_variable }} + """, + ) == "macro variable" ) assert ( - template.Template( - """ - {% import 'inner/inner_test.jinja' as t %} - {{ t.test_macro() }} {{ t.test_variable }} - """, + render( hass, - ).async_render() + """ + {% import 'inner/inner_test.jinja' as t %} + {{ t.test_macro() }} {{ t.test_variable }} + """, + ) == "inner macro inner variable" ) with pytest.raises(TemplateError): - template.Template( - """ - {% import 'notfound.jinja' as t %} - {{ t.test_macro() }} {{ t.test_variable }} - """, + render( hass, - ).async_render() + """ + {% import 'notfound.jinja' as t %} + {{ t.test_macro() }} {{ t.test_variable }} + """, + ) async def test_import_change(hass: HomeAssistant) -> None: @@ -303,45 +293,30 @@ async def test_import_change(hass: HomeAssistant) -> None: def test_loop_controls(hass: HomeAssistant) -> None: """Test that loop controls are enabled.""" - assert ( - template.Template( - """ - {%- for v in range(10) %} - {%- if v == 1 -%} - {%- continue -%} - {%- elif v == 3 -%} - {%- break -%} - {%- endif -%} - {{ v }} - {%- endfor -%} - """, - hass, - ).async_render() - == "02" - ) + tpl = """ + {%- for v in range(10) %} + {%- if v == 1 -%} + {%- continue -%} + {%- elif v == 3 -%} + {%- break -%} + {%- endif -%} + {{ v }} + {%- endfor -%} + """ + assert render(hass, tpl) == "02" def test_float_function(hass: HomeAssistant) -> None: """Test float function.""" hass.states.async_set("sensor.temperature", "12") - assert ( - template.Template( - "{{ float(states.sensor.temperature.state) }}", hass - ).async_render() - == 12.0 - ) + assert render(hass, "{{ float(states.sensor.temperature.state) }}") == 12.0 - assert ( - template.Template( - "{{ float(states.sensor.temperature.state) > 11 }}", hass - ).async_render() - is True - ) + assert render(hass, "{{ float(states.sensor.temperature.state) > 11 }}") is True # Test handling of invalid input with pytest.raises(TemplateError): - template.Template("{{ float('forgiving') }}", hass).async_render() + render(hass, "{{ float('forgiving') }}") # Test handling of default return value assert render(hass, "{{ float('bad', 1) }}") == 1 @@ -451,31 +426,18 @@ def test_bool_filter(hass: HomeAssistant) -> None: ) def test_isnumber(hass: HomeAssistant, value, expected) -> None: """Test is_number.""" - assert ( - template.Template("{{ is_number(value) }}", hass).async_render({"value": value}) - == expected - ) - assert ( - template.Template("{{ value | is_number }}", hass).async_render( - {"value": value} - ) - == expected - ) - assert ( - template.Template("{{ value is is_number }}", hass).async_render( - {"value": value} - ) - == expected - ) + assert render(hass, "{{ is_number(value) }}", {"value": value}) == expected + assert render(hass, "{{ value | is_number }}", {"value": value}) == expected + assert render(hass, "{{ value is is_number }}", {"value": value}) == expected def test_converting_datetime_to_iterable(hass: HomeAssistant) -> None: """Test converting a datetime to an iterable raises an error.""" dt_ = datetime(2020, 1, 1, 0, 0, 0) with pytest.raises(TemplateError): - template.Template("{{ tuple(value) }}", hass).async_render({"value": dt_}) + render(hass, "{{ tuple(value) }}", {"value": dt_}) with pytest.raises(TemplateError): - template.Template("{{ set(value) }}", hass).async_render({"value": dt_}) + render(hass, "{{ set(value) }}", {"value": dt_}) @pytest.mark.parametrize( @@ -494,51 +456,31 @@ def test_converting_datetime_to_iterable(hass: HomeAssistant) -> None: ) def test_is_datetime(hass: HomeAssistant, value, expected) -> None: """Test is datetime.""" - assert ( - template.Template("{{ value is datetime }}", hass).async_render( - {"value": value} - ) - == expected - ) + assert render(hass, "{{ value is datetime }}", {"value": value}) == expected def test_rounding_value(hass: HomeAssistant) -> None: """Test rounding value.""" hass.states.async_set("sensor.temperature", 12.78) - assert ( - template.Template( - "{{ states.sensor.temperature.state | round(1) }}", hass - ).async_render() - == 12.8 - ) + assert render(hass, "{{ states.sensor.temperature.state | round(1) }}") == 12.8 assert ( - template.Template( - "{{ states.sensor.temperature.state | multiply(10) | round }}", hass - ).async_render() + render(hass, "{{ states.sensor.temperature.state | multiply(10) | round }}") == 128 ) assert ( - template.Template( - '{{ states.sensor.temperature.state | round(1, "floor") }}', hass - ).async_render() + render(hass, '{{ states.sensor.temperature.state | round(1, "floor") }}') == 12.7 ) assert ( - template.Template( - '{{ states.sensor.temperature.state | round(1, "ceil") }}', hass - ).async_render() - == 12.8 + render(hass, '{{ states.sensor.temperature.state | round(1, "ceil") }}') == 12.8 ) assert ( - template.Template( - '{{ states.sensor.temperature.state | round(1, "half") }}', hass - ).async_render() - == 13.0 + render(hass, '{{ states.sensor.temperature.state | round(1, "half") }}') == 13.0 ) @@ -546,10 +488,10 @@ def test_rounding_value_on_error(hass: HomeAssistant) -> None: """Test rounding value handling of error.""" # Test handling of invalid input with pytest.raises(TemplateError): - template.Template("{{ None | round }}", hass).async_render() + render(hass, "{{ None | round }}") with pytest.raises(TemplateError): - template.Template('{{ "no_number" | round }}', hass).async_render() + render(hass, '{{ "no_number" | round }}') # Test handling of default return value assert render(hass, "{{ 'no_number' | round(default=1) }}") == 1 @@ -560,16 +502,11 @@ def test_multiply(hass: HomeAssistant) -> None: tests = {10: 100} for inp, out in tests.items(): - assert ( - template.Template( - f"{{{{ {inp} | multiply(10) | round }}}}", hass - ).async_render() - == out - ) + assert render(hass, f"{{{{ {inp} | multiply(10) | round }}}}") == out # Test handling of invalid input with pytest.raises(TemplateError): - template.Template("{{ abcd | multiply(10) }}", hass).async_render() + render(hass, "{{ abcd | multiply(10) }}") # Test handling of default return value assert render(hass, "{{ 'no_number' | multiply(10, 1) }}") == 1 @@ -581,14 +518,11 @@ def test_add(hass: HomeAssistant) -> None: tests = {10: 42} for inp, out in tests.items(): - assert ( - template.Template(f"{{{{ {inp} | add(32) | round }}}}", hass).async_render() - == out - ) + assert render(hass, f"{{{{ {inp} | add(32) | round }}}}") == out # Test handling of invalid input with pytest.raises(TemplateError): - template.Template("{{ abcd | add(10) }}", hass).async_render() + render(hass, "{{ abcd | add(10) }}") # Test handling of default return value assert render(hass, "{{ 'no_number' | add(10, 1) }}") == 1 @@ -597,101 +531,71 @@ def test_add(hass: HomeAssistant) -> None: def test_apply(hass: HomeAssistant) -> None: """Test apply.""" - assert template.Template( - """ - {%- macro add_foo(arg) -%} - {{arg}}foo - {%- endmacro -%} - {{ ["a", "b", "c"] | map('apply', add_foo) | list }} - """, - hass, - ).async_render() == ["afoo", "bfoo", "cfoo"] + tpl = """ + {%- macro add_foo(arg) -%} + {{arg}}foo + {%- endmacro -%} + {{ ["a", "b", "c"] | map('apply', add_foo) | list }} + """ + assert render(hass, tpl) == ["afoo", "bfoo", "cfoo"] - assert template.Template( - """ - {{ ['1', '2', '3', '4', '5'] | map('apply', int) | list }} - """, - hass, - ).async_render() == [1, 2, 3, 4, 5] + assert render( + hass, "{{ ['1', '2', '3', '4', '5'] | map('apply', int) | list }}" + ) == [1, 2, 3, 4, 5] def test_apply_macro_with_arguments(hass: HomeAssistant) -> None: """Test apply macro with positional, named, and mixed arguments.""" # Test macro with positional arguments - assert ( - template.Template( - """ - {%- macro add_numbers(a, b, c) -%} - {{ a + b + c }} - {%- endmacro -%} - {{ apply(5, add_numbers, 10, 15) }} - """, - hass, - ).async_render() - == 30 - ) + tpl = """ + {%- macro add_numbers(a, b, c) -%} + {{ a + b + c }} + {%- endmacro -%} + {{ apply(5, add_numbers, 10, 15) }} + """ + assert render(hass, tpl) == 30 # Test macro with named arguments - assert ( - template.Template( - """ - {%- macro greet(name, greeting="Hello") -%} - {{ greeting }}, {{ name }}! - {%- endmacro -%} - {{ apply("World", greet, greeting="Hi") }} - """, - hass, - ).async_render() - == "Hi, World!" - ) + tpl = """ + {%- macro greet(name, greeting="Hello") -%} + {{ greeting }}, {{ name }}! + {%- endmacro -%} + {{ apply("World", greet, greeting="Hi") }} + """ + assert render(hass, tpl) == "Hi, World!" # Test macro with mixed arguments - assert ( - template.Template( - """ - {%- macro format_message(prefix, name, suffix="!") -%} - {{ prefix }} {{ name }}{{ suffix }} - {%- endmacro -%} - {{ apply("Welcome", format_message, "John", suffix="...") }} - """, - hass, - ).async_render() - == "Welcome John..." - ) + tpl = """ + {%- macro format_message(prefix, name, suffix="!") -%} + {{ prefix }} {{ name }}{{ suffix }} + {%- endmacro -%} + {{ apply("Welcome", format_message, "John", suffix="...") }} + """ + assert render(hass, tpl) == "Welcome John..." def test_as_function(hass: HomeAssistant) -> None: """Test as_function.""" - assert ( - template.Template( - """ - {%- macro macro_double(num, returns) -%} - {%- do returns(num * 2) -%} - {%- endmacro -%} - {%- set double = macro_double | as_function -%} - {{ double(5) }} - """, - hass, - ).async_render() - == 10 - ) + tpl = """ + {%- macro macro_double(num, returns) -%} + {%- do returns(num * 2) -%} + {%- endmacro -%} + {%- set double = macro_double | as_function -%} + {{ double(5) }} + """ + assert render(hass, tpl) == 10 def test_as_function_no_arguments(hass: HomeAssistant) -> None: """Test as_function with no arguments.""" - assert ( - template.Template( - """ - {%- macro macro_get_hello(returns) -%} - {%- do returns("Hello") -%} - {%- endmacro -%} - {%- set get_hello = macro_get_hello | as_function -%} - {{ get_hello() }} - """, - hass, - ).async_render() - == "Hello" - ) + tpl = """ + {%- macro macro_get_hello(returns) -%} + {%- do returns("Hello") -%} + {%- endmacro -%} + {%- set get_hello = macro_get_hello | as_function -%} + {{ get_hello() }} + """ + assert render(hass, tpl) == "Hello" def test_strptime(hass: HomeAssistant) -> None: @@ -711,7 +615,7 @@ def test_strptime(hass: HomeAssistant) -> None: temp = f"{{{{ strptime('{inp}', '{fmt}') }}}}" - assert template.Template(temp, hass).async_render() == expected + assert render(hass, temp) == expected # Test handling of invalid input invalid_tests = [ @@ -723,7 +627,7 @@ def test_strptime(hass: HomeAssistant) -> None: temp = f"{{{{ strptime('{inp}', '{fmt}') }}}}" with pytest.raises(TemplateError): - template.Template(temp, hass).async_render() + render(hass, temp) # Test handling of default return value assert render(hass, "{{ strptime('invalid', '%Y', 1) }}") == 1 @@ -749,7 +653,7 @@ async def test_timestamp_custom(hass: HomeAssistant) -> None: else: fil = "timestamp_custom" - assert template.Template(f"{{{{ {inp} | {fil} }}}}", hass).async_render() == out + assert render(hass, f"{{{{ {inp} | {fil} }}}}") == out # Test handling of invalid input invalid_tests = [ @@ -765,7 +669,7 @@ async def test_timestamp_custom(hass: HomeAssistant) -> None: fil = "timestamp_custom" with pytest.raises(TemplateError): - template.Template(f"{{{{ {inp} | {fil} }}}}", hass).async_render() + render(hass, f"{{{{ {inp} | {fil} }}}}") # Test handling of default return value assert render(hass, "{{ None | timestamp_custom('invalid', True, 1) }}") == 1 @@ -780,10 +684,7 @@ async def test_timestamp_local(hass: HomeAssistant) -> None: ] for inp, out in tests: - assert ( - template.Template(f"{{{{ {inp} | timestamp_local }}}}", hass).async_render() - == out - ) + assert render(hass, f"{{{{ {inp} | timestamp_local }}}}") == out # Test handling of invalid input invalid_tests = [ @@ -792,7 +693,7 @@ async def test_timestamp_local(hass: HomeAssistant) -> None: for inp in invalid_tests: with pytest.raises(TemplateError): - template.Template(f"{{{{ {inp} | timestamp_local }}}}", hass).async_render() + render(hass, f"{{{{ {inp} | timestamp_local }}}}") # Test handling of default return value assert render(hass, "{{ None | timestamp_local(1) }}") == 1 @@ -815,14 +716,8 @@ def test_as_datetime(hass: HomeAssistant, input) -> None: expected = dt_util.parse_datetime(input) if expected is not None: expected = str(expected) - assert ( - template.Template(f"{{{{ as_datetime('{input}') }}}}", hass).async_render() - == expected - ) - assert ( - template.Template(f"{{{{ '{input}' | as_datetime }}}}", hass).async_render() - == expected - ) + assert render(hass, f"{{{{ as_datetime('{input}') }}}}") == expected + assert render(hass, f"{{{{ '{input}' | as_datetime }}}}") == expected @pytest.mark.parametrize( @@ -839,22 +734,10 @@ def test_as_datetime_from_timestamp( output: str, ) -> None: """Test converting a UNIX timestamp to a date object.""" - assert ( - template.Template(f"{{{{ as_datetime({input}) }}}}", hass).async_render() - == output - ) - assert ( - template.Template(f"{{{{ {input} | as_datetime }}}}", hass).async_render() - == output - ) - assert ( - template.Template(f"{{{{ as_datetime('{input}') }}}}", hass).async_render() - == output - ) - assert ( - template.Template(f"{{{{ '{input}' | as_datetime }}}}", hass).async_render() - == output - ) + assert render(hass, f"{{{{ as_datetime({input}) }}}}") == output + assert render(hass, f"{{{{ {input} | as_datetime }}}}") == output + assert render(hass, f"{{{{ as_datetime('{input}') }}}}") == output + assert render(hass, f"{{{{ '{input}' | as_datetime }}}}") == output @pytest.mark.parametrize( @@ -875,15 +758,9 @@ def test_as_datetime_from_datetime( ) -> None: """Test using datetime.datetime or datetime.date objects as input.""" - assert ( - template.Template(f"{input}{{{{ dt | as_datetime }}}}", hass).async_render() - == output - ) + assert render(hass, f"{input}{{{{ dt | as_datetime }}}}") == output - assert ( - template.Template(f"{input}{{{{ as_datetime(dt) }}}}", hass).async_render() - == output - ) + assert render(hass, f"{input}{{{{ as_datetime(dt) }}}}") == output @pytest.mark.parametrize( @@ -900,18 +777,8 @@ def test_as_datetime_default( ) -> None: """Test invalid input and return default value.""" - assert ( - template.Template( - f"{{{{ as_datetime({input}, default={default}) }}}}", hass - ).async_render() - == output - ) - assert ( - template.Template( - f"{{{{ {input} | as_datetime({default}) }}}}", hass - ).async_render() - == output - ) + assert render(hass, f"{{{{ as_datetime({input}, default={default}) }}}}") == output + assert render(hass, f"{{{{ {input} | as_datetime({default}) }}}}") == output def test_as_local(hass: HomeAssistant) -> None: @@ -919,12 +786,12 @@ def test_as_local(hass: HomeAssistant) -> None: hass.states.async_set("test.object", "available") last_updated = hass.states.get("test.object").last_updated - assert template.Template( - "{{ as_local(states.test.object.last_updated) }}", hass - ).async_render() == str(dt_util.as_local(last_updated)) - assert template.Template( - "{{ states.test.object.last_updated | as_local }}", hass - ).async_render() == str(dt_util.as_local(last_updated)) + assert render(hass, "{{ as_local(states.test.object.last_updated) }}") == str( + dt_util.as_local(last_updated) + ) + assert render(hass, "{{ states.test.object.last_updated | as_local }}") == str( + dt_util.as_local(last_updated) + ) def test_to_json(hass: HomeAssistant) -> None: @@ -933,27 +800,27 @@ def test_to_json(hass: HomeAssistant) -> None: # Note that we're not testing the actual json.loads and json.dumps methods, # only the filters, so we don't need to be exhaustive with our sample JSON. expected_result = {"Foo": "Bar"} - actual_result = template.Template( - "{{ {'Foo': 'Bar'} | to_json }}", hass - ).async_render() + actual_result = render(hass, "{{ {'Foo': 'Bar'} | to_json }}") assert actual_result == expected_result expected_result = orjson.dumps({"Foo": "Bar"}, option=orjson.OPT_INDENT_2).decode() - actual_result = template.Template( - "{{ {'Foo': 'Bar'} | to_json(pretty_print=True) }}", hass - ).async_render(parse_result=False) + actual_result = render( + hass, "{{ {'Foo': 'Bar'} | to_json(pretty_print=True) }}", parse_result=False + ) assert actual_result == expected_result expected_result = orjson.dumps( {"Z": 26, "A": 1, "M": 13}, option=orjson.OPT_SORT_KEYS ).decode() - actual_result = template.Template( - "{{ {'Z': 26, 'A': 1, 'M': 13} | to_json(sort_keys=True) }}", hass - ).async_render(parse_result=False) + actual_result = render( + hass, + "{{ {'Z': 26, 'A': 1, 'M': 13} | to_json(sort_keys=True) }}", + parse_result=False, + ) assert actual_result == expected_result with pytest.raises(TemplateError): - template.Template("{{ {'Foo': now()} | to_json }}", hass).async_render() + render(hass, "{{ {'Foo': now()} | to_json }}") # Test special case where substring class cannot be rendered # See: https://github.com/ijl/orjson/issues/445 @@ -966,9 +833,12 @@ class MyStr(str): MyStr("mykey1"): 11.0, MyStr("mykey3"): ["opt3b", "opt3a"], } - actual_result = template.Template( - "{{ test_dict | to_json(sort_keys=True) }}", hass - ).async_render(parse_result=False, variables={"test_dict": test_dict}) + actual_result = render( + hass, + "{{ test_dict | to_json(sort_keys=True) }}", + {"test_dict": test_dict}, + parse_result=False, + ) assert actual_result == expected_result @@ -977,26 +847,25 @@ def test_to_json_ensure_ascii(hass: HomeAssistant) -> None: # Note that we're not testing the actual json.loads and json.dumps methods, # only the filters, so we don't need to be exhaustive with our sample JSON. - actual_value_ascii = template.Template( - "{{ 'Bar ҝ éèà' | to_json(ensure_ascii=True) }}", hass - ).async_render() + actual_value_ascii = render(hass, "{{ 'Bar ҝ éèà' | to_json(ensure_ascii=True) }}") assert actual_value_ascii == '"Bar \\u049d \\u00e9\\u00e8\\u00e0"' - actual_value = template.Template( - "{{ 'Bar ҝ éèà' | to_json(ensure_ascii=False) }}", hass - ).async_render() + actual_value = render(hass, "{{ 'Bar ҝ éèà' | to_json(ensure_ascii=False) }}") assert actual_value == '"Bar ҝ éèà"' expected_result = json.dumps({"Foo": "Bar"}, indent=2) - actual_result = template.Template( - "{{ {'Foo': 'Bar'} | to_json(pretty_print=True, ensure_ascii=True) }}", hass - ).async_render(parse_result=False) + actual_result = render( + hass, + "{{ {'Foo': 'Bar'} | to_json(pretty_print=True, ensure_ascii=True) }}", + parse_result=False, + ) assert actual_result == expected_result expected_result = json.dumps({"Z": 26, "A": 1, "M": 13}, sort_keys=True) - actual_result = template.Template( - "{{ {'Z': 26, 'A': 1, 'M': 13} | to_json(sort_keys=True, ensure_ascii=True) }}", + actual_result = render( hass, - ).async_render(parse_result=False) + "{{ {'Z': 26, 'A': 1, 'M': 13} | to_json(sort_keys=True, ensure_ascii=True) }}", + parse_result=False, + ) assert actual_result == expected_result @@ -1006,32 +875,25 @@ def test_from_json(hass: HomeAssistant) -> None: # Note that we're not testing the actual json.loads and json.dumps methods, # only the filters, so we don't need to be exhaustive with our sample JSON. expected_result = "Bar" - actual_result = template.Template( - '{{ (\'{"Foo": "Bar"}\' | from_json).Foo }}', hass - ).async_render() + actual_result = render(hass, '{{ (\'{"Foo": "Bar"}\' | from_json).Foo }}') assert actual_result == expected_result info = render_to_info(hass, "{{ 'garbage string' | from_json }}") with pytest.raises(TemplateError, match="no default was specified"): info.result() - actual_result = template.Template( - "{{ 'garbage string' | from_json('Bar') }}", hass - ).async_render() + actual_result = render(hass, "{{ 'garbage string' | from_json('Bar') }}") assert actual_result == expected_result def test_ord(hass: HomeAssistant) -> None: """Test the ord filter.""" - assert template.Template('{{ "d" | ord }}', hass).async_render() == 100 + assert render(hass, '{{ "d" | ord }}') == 100 def test_from_hex(hass: HomeAssistant) -> None: """Test the fromhex filter.""" - assert ( - template.Template("{{ '0F010003' | from_hex }}", hass).async_render() - == b"\x0f\x01\x00\x03" - ) + assert render(hass, "{{ '0F010003' | from_hex }}") == b"\x0f\x01\x00\x03" def test_timestamp_utc(hass: HomeAssistant) -> None: @@ -1043,10 +905,7 @@ def test_timestamp_utc(hass: HomeAssistant) -> None: ] for inp, out in tests: - assert ( - template.Template(f"{{{{ {inp} | timestamp_utc }}}}", hass).async_render() - == out - ) + assert render(hass, f"{{{{ {inp} | timestamp_utc }}}}") == out # Test handling of invalid input invalid_tests = [ @@ -1055,7 +914,7 @@ def test_timestamp_utc(hass: HomeAssistant) -> None: for inp in invalid_tests: with pytest.raises(TemplateError): - template.Template(f"{{{{ {inp} | timestamp_utc }}}}", hass).async_render() + render(hass, f"{{{{ {inp} | timestamp_utc }}}}") # Test handling of default return value assert render(hass, "{{ None | timestamp_utc(1) }}") == 1 @@ -1065,17 +924,17 @@ def test_timestamp_utc(hass: HomeAssistant) -> None: def test_as_timestamp(hass: HomeAssistant) -> None: """Test the as_timestamp function.""" with pytest.raises(TemplateError): - template.Template('{{ as_timestamp("invalid") }}', hass).async_render() + render(hass, '{{ as_timestamp("invalid") }}') hass.states.async_set("test.object", None) with pytest.raises(TemplateError): - template.Template("{{ as_timestamp(states.test.object) }}", hass).async_render() + render(hass, "{{ as_timestamp(states.test.object) }}") tpl = ( '{{ as_timestamp(strptime("2024-02-03T09:10:24+0000", ' '"%Y-%m-%dT%H:%M:%S%z")) }}' ) - assert template.Template(tpl, hass).async_render() == 1706951424.0 + assert render(hass, tpl) == 1706951424.0 # Test handling of default return value assert render(hass, "{{ 'invalid' | as_timestamp(1) }}") == 1 @@ -1096,12 +955,12 @@ def test_random_every_time(test_choice, hass: HomeAssistant) -> None: def test_passing_vars_as_keywords(hass: HomeAssistant) -> None: """Test passing variables as keywords.""" - assert template.Template("{{ hello }}", hass).async_render(hello=127) == 127 + assert render(hass, "{{ hello }}", hello=127) == 127 def test_passing_vars_as_vars(hass: HomeAssistant) -> None: """Test passing variables as variables.""" - assert template.Template("{{ hello }}", hass).async_render({"hello": 127}) == 127 + assert render(hass, "{{ hello }}", {"hello": 127}) == 127 def test_passing_vars_as_list(hass: HomeAssistant) -> None: @@ -1113,29 +972,20 @@ def test_passing_vars_as_list(hass: HomeAssistant) -> None: def test_passing_vars_as_list_element(hass: HomeAssistant) -> None: """Test passing variables as list.""" - assert ( - template.render_complex( - template.Template("{{ hello[1] }}", hass), {"hello": ["foo", "bar"]} - ) - == "bar" - ) + tpl = template.Template("{{ hello[1] }}", hass) + assert template.render_complex(tpl, {"hello": ["foo", "bar"]}) == "bar" def test_passing_vars_as_dict_element(hass: HomeAssistant) -> None: """Test passing variables as list.""" - assert ( - template.render_complex( - template.Template("{{ hello.foo }}", hass), {"hello": {"foo": "bar"}} - ) - == "bar" - ) + tpl = template.Template("{{ hello.foo }}", hass) + assert template.render_complex(tpl, {"hello": {"foo": "bar"}}) == "bar" def test_passing_vars_as_dict(hass: HomeAssistant) -> None: """Test passing variables as list.""" - assert template.render_complex( - template.Template("{{ hello }}", hass), {"hello": {"foo": "bar"}} - ) == {"foo": "bar"} + tpl = template.Template("{{ hello }}", hass) + assert template.render_complex(tpl, {"hello": {"foo": "bar"}}) == {"foo": "bar"} def test_render_with_possible_json_value_with_valid_json(hass: HomeAssistant) -> None: @@ -1194,9 +1044,7 @@ def test_render_with_possible_json_value_undefined_json_error_value( def test_render_with_possible_json_value_non_string_value(hass: HomeAssistant) -> None: """Render with possible JSON value with non-string value.""" tpl = template.Template( - """ -{{ strptime(value~'+0000', '%Y-%m-%d %H:%M:%S%z') }} - """, + """{{ strptime(value~'+0000', '%Y-%m-%d %H:%M:%S%z') }}""", hass, ) value = datetime(2019, 1, 18, 12, 13, 14) @@ -1227,10 +1075,11 @@ def test_render_with_possible_json_value_and_dont_parse_result( def test_if_state_exists(hass: HomeAssistant) -> None: """Test if state exists works.""" hass.states.async_set("test.object", "available") - tpl = template.Template( - "{% if states.test.object %}exists{% else %}not exists{% endif %}", hass + + result = render( + hass, "{% if states.test.object %}exists{% else %}not exists{% endif %}" ) - assert tpl.async_render() == "exists" + assert result == "exists" def test_is_hidden_entity( @@ -1242,116 +1091,80 @@ def test_is_hidden_entity( "sensor", "mock", "hidden", hidden_by=er.RegistryEntryHider.USER ) visible_entity = entity_registry.async_get_or_create("sensor", "mock", "visible") - assert template.Template( - f"{{{{ is_hidden_entity('{hidden_entity.entity_id}') }}}}", - hass, - ).async_render() + assert render(hass, f"{{{{ is_hidden_entity('{hidden_entity.entity_id}') }}}}") - assert not template.Template( - f"{{{{ is_hidden_entity('{visible_entity.entity_id}') }}}}", - hass, - ).async_render() + assert not render(hass, f"{{{{ is_hidden_entity('{visible_entity.entity_id}') }}}}") - assert not template.Template( - f"{{{{ ['{visible_entity.entity_id}'] | select('is_hidden_entity') | first }}}}", + assert not render( hass, - ).async_render() + f"{{{{ ['{visible_entity.entity_id}'] | select('is_hidden_entity') | first }}}}", + ) def test_is_state(hass: HomeAssistant) -> None: """Test is_state method.""" hass.states.async_set("test.object", "available") - tpl = template.Template( - """ -{% if is_state("test.object", "available") %}yes{% else %}no{% endif %} - """, - hass, - ) - assert tpl.async_render() == "yes" - tpl = template.Template( - """ -{{ is_state("test.noobject", "available") }} - """, - hass, + result = render( + hass, '{% if is_state("test.object", "available") %}yes{% else %}no{% endif %}' ) - assert tpl.async_render() is False + assert result == "yes" - tpl = template.Template( - """ -{% if "test.object" is is_state("available") %}yes{% else %}no{% endif %} - """, - hass, - ) - assert tpl.async_render() == "yes" + result = render(hass, """{{ is_state("test.noobject", "available") }}""") + assert result is False - tpl = template.Template( - """ -{{ ['test.object'] | select("is_state", "available") | first | default }} - """, + result = render( hass, + '{% if "test.object" is is_state("available") %}yes{% else %}no{% endif %}', ) - assert tpl.async_render() == "test.object" + assert result == "yes" - tpl = template.Template( - """ -{{ is_state("test.object", ["on", "off", "available"]) }} - """, + result = render( hass, + """{{ ['test.object'] | select("is_state", "available") | first | default }}""", ) - assert tpl.async_render() is True + assert result == "test.object" + + result = render(hass, '{{ is_state("test.object", ["on", "off", "available"]) }}') + assert result is True def test_is_state_attr(hass: HomeAssistant) -> None: """Test is_state_attr method.""" hass.states.async_set("test.object", "available", {"mode": "on", "exists": None}) - tpl = template.Template( - """ -{% if is_state_attr("test.object", "mode", "on") %}yes{% else %}no{% endif %} - """, - hass, - ) - assert tpl.async_render() == "yes" - tpl = template.Template( - """ -{{ is_state_attr("test.noobject", "mode", "on") }} - """, + result = render( hass, + """{% if is_state_attr("test.object", "mode", "on") %}yes{% else %}no{% endif %}""", ) - assert tpl.async_render() is False + assert result == "yes" - tpl = template.Template( - """ -{% if "test.object" is is_state_attr("mode", "on") %}yes{% else %}no{% endif %} - """, + result = render(hass, """{{ is_state_attr("test.noobject", "mode", "on") }}""") + assert result is False + + result = render( hass, + """{% if "test.object" is is_state_attr("mode", "on") %}yes{% else %}no{% endif %}""", ) - assert tpl.async_render() == "yes" + assert result == "yes" - tpl = template.Template( - """ -{{ ['test.object'] | select("is_state_attr", "mode", "on") | first | default }} - """, + result = render( hass, + """{{ ['test.object'] | select("is_state_attr", "mode", "on") | first | default }}""", ) - assert tpl.async_render() == "test.object" + assert result == "test.object" - tpl = template.Template( - """ -{% if is_state_attr("test.object", "exists", None) %}yes{% else %}no{% endif %} - """, + result = render( hass, + """{% if is_state_attr("test.object", "exists", None) %}yes{% else %}no{% endif %}""", ) - assert tpl.async_render() == "yes" + assert result == "yes" - tpl = template.Template( - """ -{% if is_state_attr("test.object", "noexist", None) %}yes{% else %}no{% endif %} - """, + result = render( hass, + """{% if is_state_attr("test.object", "noexist", None) %}yes{% else %}no{% endif %}""", ) - assert tpl.async_render() == "no" + assert result == "no" def test_state_attr(hass: HomeAssistant) -> None: @@ -1359,63 +1172,47 @@ def test_state_attr(hass: HomeAssistant) -> None: hass.states.async_set( "test.object", "available", {"effect": "action", "mode": "on"} ) - tpl = template.Template( - """ -{% if state_attr("test.object", "mode") == "on" %}yes{% else %}no{% endif %} - """, - hass, - ) - assert tpl.async_render() == "yes" - tpl = template.Template( - """ -{{ state_attr("test.noobject", "mode") == None }} - """, + result = render( hass, + """{% if state_attr("test.object", "mode") == "on" %}yes{% else %}no{% endif %}""", ) - assert tpl.async_render() is True + assert result == "yes" - tpl = template.Template( - """ -{% if "test.object" | state_attr("mode") == "on" %}yes{% else %}no{% endif %} - """, + result = render(hass, """{{ state_attr("test.noobject", "mode") == None }}""") + assert result is True + + result = render( hass, + """{% if "test.object" | state_attr("mode") == "on" %}yes{% else %}no{% endif %}""", ) - assert tpl.async_render() == "yes" + assert result == "yes" - tpl = template.Template( - """ -{{ ['test.object'] | map("state_attr", "effect") | first | default }} - """, + result = render( hass, + """{{ ['test.object'] | map("state_attr", "effect") | first | default }}""", ) - assert tpl.async_render() == "action" + assert result == "action" def test_states_function(hass: HomeAssistant) -> None: """Test using states as a function.""" hass.states.async_set("test.object", "available") - tpl = template.Template('{{ states("test.object") }}', hass) - assert tpl.async_render() == "available" - tpl2 = template.Template('{{ states("test.object2") }}', hass) - assert tpl2.async_render() == "unknown" + result = render(hass, '{{ states("test.object") }}') + assert result == "available" - tpl = template.Template( - """ -{% if "test.object" | states == "available" %}yes{% else %}no{% endif %} - """, - hass, - ) - assert tpl.async_render() == "yes" + result = render(hass, '{{ states("test.object2") }}') + assert result == "unknown" - tpl = template.Template( - """ -{{ ['test.object'] | map("states") | first | default }} - """, + result = render( hass, + """{% if "test.object" | states == "available" %}yes{% else %}no{% endif %}""", ) - assert tpl.async_render() == "available" + assert result == "yes" + + result = render(hass, """{{ ['test.object'] | map("states") | first | default }}""") + assert result == "available" async def test_state_translated( @@ -1470,36 +1267,30 @@ async def test_state_translated( ) hass.states.async_set("light.hue_5678", "on", attributes={}) - tpl = template.Template( - '{{ state_translated("switch.without_translations") }}', hass - ) - assert tpl.async_render() == "on" + result = render(hass, '{{ state_translated("switch.without_translations") }}') + assert result == "on" - tp2 = template.Template( - '{{ state_translated("binary_sensor.without_device_class") }}', hass + result = render( + hass, '{{ state_translated("binary_sensor.without_device_class") }}' ) - assert tp2.async_render() == "On" + assert result == "On" - tpl3 = template.Template( - '{{ state_translated("binary_sensor.with_device_class") }}', hass - ) - assert tpl3.async_render() == "Detected" + result = render(hass, '{{ state_translated("binary_sensor.with_device_class") }}') + assert result == "Detected" - tpl4 = template.Template( - '{{ state_translated("binary_sensor.with_unknown_device_class") }}', hass + result = render( + hass, '{{ state_translated("binary_sensor.with_unknown_device_class") }}' ) - assert tpl4.async_render() == "On" + assert result == "On" with pytest.raises(TemplateError): - template.Template( - '{{ state_translated("contextfunction") }}', hass - ).async_render() + render(hass, '{{ state_translated("contextfunction") }}') - tpl6 = template.Template('{{ state_translated("switch.invalid") }}', hass) - assert tpl6.async_render() == "unknown" + result = render(hass, '{{ state_translated("switch.invalid") }}') + assert result == "unknown" with pytest.raises(TemplateError): - template.Template('{{ state_translated("-invalid") }}', hass).async_render() + render(hass, '{{ state_translated("-invalid") }}') def mock_get_cached_translations( _hass: HomeAssistant, @@ -1517,14 +1308,14 @@ def mock_get_cached_translations( "homeassistant.helpers.translation.async_get_cached_translations", side_effect=mock_get_cached_translations, ): - tpl8 = template.Template('{{ state_translated("light.hue_5678") }}', hass) - assert tpl8.async_render() == "state_is_on" + result = render(hass, '{{ state_translated("light.hue_5678") }}') + assert result == "state_is_on" - tpl11 = template.Template('{{ state_translated("domain.is_unavailable") }}', hass) - assert tpl11.async_render() == "unavailable" + result = render(hass, '{{ state_translated("domain.is_unavailable") }}') + assert result == "unavailable" - tpl12 = template.Template('{{ state_translated("domain.is_unknown") }}', hass) - assert tpl12.async_render() == "unknown" + result = render(hass, '{{ state_translated("domain.is_unknown") }}') + assert result == "unknown" def test_has_value(hass: HomeAssistant) -> None: @@ -1532,37 +1323,19 @@ def test_has_value(hass: HomeAssistant) -> None: hass.states.async_set("test.value1", 1) hass.states.async_set("test.unavailable", STATE_UNAVAILABLE) - tpl = template.Template( - """ -{{ has_value("test.value1") }} - """, - hass, - ) - assert tpl.async_render() is True + result = render(hass, """{{ has_value("test.value1") }}""") + assert result is True - tpl = template.Template( - """ -{{ has_value("test.unavailable") }} - """, - hass, - ) - assert tpl.async_render() is False + result = render(hass, """{{ has_value("test.unavailable") }}""") + assert result is False - tpl = template.Template( - """ -{{ has_value("test.unknown") }} - """, - hass, - ) - assert tpl.async_render() is False + result = render(hass, """{{ has_value("test.unknown") }}""") + assert result is False - tpl = template.Template( - """ -{% if "test.value1" is has_value %}yes{% else %}no{% endif %} - """, - hass, + result = render( + hass, """{% if "test.value1" is has_value %}yes{% else %}no{% endif %}""" ) - assert tpl.async_render() == "yes" + assert result == "yes" @patch( @@ -1573,7 +1346,7 @@ def test_now(mock_is_safe, hass: HomeAssistant) -> None: """Test now method.""" now = dt_util.now() with freeze_time(now): - info = template.Template("{{ now().isoformat() }}", hass).async_render_to_info() + info = render_to_info(hass, "{{ now().isoformat() }}") assert now.isoformat() == info.result() assert info.has_time is True @@ -1587,9 +1360,7 @@ def test_utcnow(mock_is_safe, hass: HomeAssistant) -> None: """Test now method.""" utcnow = dt_util.utcnow() with freeze_time(utcnow): - info = template.Template( - "{{ utcnow().isoformat() }}", hass - ).async_render_to_info() + info = render_to_info(hass, "{{ utcnow().isoformat() }}") assert utcnow.isoformat() == info.result() assert info.has_time is True @@ -1627,36 +1398,22 @@ async def test_today_at( await hass.config.async_set_time_zone(timezone_str) - result = template.Template( - "{{ today_at('10:00').isoformat() }}", - hass, - ).async_render() + result = render(hass, "{{ today_at('10:00').isoformat() }}") assert result == expected - result = template.Template( - "{{ today_at('10:00:00').isoformat() }}", - hass, - ).async_render() + result = render(hass, "{{ today_at('10:00:00').isoformat() }}") assert result == expected - result = template.Template( - "{{ ('10:00:00' | today_at).isoformat() }}", - hass, - ).async_render() + result = render(hass, "{{ ('10:00:00' | today_at).isoformat() }}") assert result == expected - result = template.Template( - "{{ today_at().isoformat() }}", - hass, - ).async_render() + result = render(hass, "{{ today_at().isoformat() }}") assert result == expected_midnight with pytest.raises(TemplateError): - template.Template("{{ today_at('bad') }}", hass).async_render() + render(hass, "{{ today_at('bad') }}") - info = template.Template( - "{{ today_at('10:00').isoformat() }}", hass - ).async_render_to_info() + info = render_to_info(hass, "{{ today_at('10:00').isoformat() }}") assert info.has_time is True freezer.stop() @@ -1674,12 +1431,10 @@ async def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: '{{relative_time(strptime("2000-01-01 09:00:00", "%Y-%m-%d %H:%M:%S"))}}' ) with freeze_time(now): - result = template.Template( - relative_time_template, - hass, - ).async_render() + result = render(hass, relative_time_template) assert result == "1 hour" - result = template.Template( + result = render( + hass, ( "{{" " relative_time(" @@ -1690,11 +1445,11 @@ async def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "2 hours" - result = template.Template( + result = render( + hass, ( "{{" " relative_time(" @@ -1705,14 +1460,14 @@ async def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "1 hour" result1 = str( template.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") ) - result2 = template.Template( + result2 = render( + hass, ( "{{" " relative_time(" @@ -1723,18 +1478,15 @@ async def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result1 == result2 - result = template.Template( - '{{relative_time("string")}}', - hass, - ).async_render() + result = render(hass, '{{relative_time("string")}}') assert result == "string" # Test behavior when current time is same as the input time - result = template.Template( + result = render( + hass, ( "{{" " relative_time(" @@ -1745,12 +1497,12 @@ async def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "0 seconds" # Test behavior when the input time is in the future - result = template.Template( + result = render( + hass, ( "{{" " relative_time(" @@ -1761,11 +1513,10 @@ async def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "2000-01-01 11:00:00+00:00" - info = template.Template(relative_time_template, hass).async_render_to_info() + info = render_to_info(hass, relative_time_template) assert info.has_time is True @@ -1781,13 +1532,11 @@ async def test_time_since(mock_is_safe, hass: HomeAssistant) -> None: '{{time_since(strptime("2000-01-01 09:00:00", "%Y-%m-%d %H:%M:%S"))}}' ) with freeze_time(now): - result = template.Template( - time_since_template, - hass, - ).async_render() + result = render(hass, time_since_template) assert result == "1 hour" - result = template.Template( + result = render( + hass, ( "{{" " time_since(" @@ -1798,11 +1547,11 @@ async def test_time_since(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "2 hours" - result = template.Template( + result = render( + hass, ( "{{" " time_since(" @@ -1813,14 +1562,14 @@ async def test_time_since(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "1 hour" result1 = str( template.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") ) - result2 = template.Template( + result2 = render( + hass, ( "{{" " time_since(" @@ -1831,11 +1580,11 @@ async def test_time_since(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result1 == result2 - result = template.Template( + result = render( + hass, ( "{{" " time_since(" @@ -1846,11 +1595,11 @@ async def test_time_since(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "1 hour 55 minutes" - result = template.Template( + result = render( + hass, ( "{{" " time_since(" @@ -1861,10 +1610,10 @@ async def test_time_since(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "1 hour 54 minutes 33 seconds" - result = template.Template( + result = render( + hass, ( "{{" " time_since(" @@ -1874,10 +1623,10 @@ async def test_time_since(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "2 hours" - result = template.Template( + result = render( + hass, ( "{{" " time_since(" @@ -1888,10 +1637,10 @@ async def test_time_since(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "11 months 4 days 1 hour 54 minutes 33 seconds" - result = template.Template( + result = render( + hass, ( "{{" " time_since(" @@ -1901,13 +1650,13 @@ async def test_time_since(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "11 months" result1 = str( template.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") ) - result2 = template.Template( + result2 = render( + hass, ( "{{" " time_since(" @@ -1918,17 +1667,13 @@ async def test_time_since(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result1 == result2 - result = template.Template( - '{{time_since("string")}}', - hass, - ).async_render() + result = render(hass, '{{time_since("string")}}') assert result == "string" - info = template.Template(time_since_template, hass).async_render_to_info() + info = render_to_info(hass, time_since_template) assert info.has_time is True @@ -1944,13 +1689,11 @@ async def test_time_until(mock_is_safe, hass: HomeAssistant) -> None: '{{time_until(strptime("2000-01-01 11:00:00", "%Y-%m-%d %H:%M:%S"))}}' ) with freeze_time(now): - result = template.Template( - time_until_template, - hass, - ).async_render() + result = render(hass, time_until_template) assert result == "1 hour" - result = template.Template( + result = render( + hass, ( "{{" " time_until(" @@ -1961,11 +1704,11 @@ async def test_time_until(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "2 hours" - result = template.Template( + result = render( + hass, ( "{{" " time_until(" @@ -1976,14 +1719,14 @@ async def test_time_until(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "1 hour" result1 = str( template.strptime("2000-01-01 09:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") ) - result2 = template.Template( + result2 = render( + hass, ( "{{" " time_until(" @@ -1994,11 +1737,11 @@ async def test_time_until(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result1 == result2 - result = template.Template( + result = render( + hass, ( "{{" " time_until(" @@ -2009,11 +1752,11 @@ async def test_time_until(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "1 hour 5 minutes" - result = template.Template( + result = render( + hass, ( "{{" " time_until(" @@ -2024,10 +1767,10 @@ async def test_time_until(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "1 hour 54 minutes 33 seconds" - result = template.Template( + result = render( + hass, ( "{{" " time_until(" @@ -2037,10 +1780,10 @@ async def test_time_until(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "2 hours" - result = template.Template( + result = render( + hass, ( "{{" " time_until(" @@ -2051,10 +1794,10 @@ async def test_time_until(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "1 year 1 month 2 days 1 hour 54 minutes 33 seconds" - result = template.Template( + result = render( + hass, ( "{{" " time_until(" @@ -2065,13 +1808,13 @@ async def test_time_until(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result == "1 year 1 month 2 days 2 hours" result1 = str( template.strptime("2000-01-01 09:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") ) - result2 = template.Template( + result2 = render( + hass, ( "{{" " time_until(" @@ -2082,17 +1825,13 @@ async def test_time_until(mock_is_safe, hass: HomeAssistant) -> None: " )" "}}" ), - hass, - ).async_render() + ) assert result1 == result2 - result = template.Template( - '{{time_until("string")}}', - hass, - ).async_render() + result = render(hass, '{{time_until("string")}}') assert result == "string" - info = template.Template(time_until_template, hass).async_render_to_info() + info = render_to_info(hass, time_until_template) assert info.has_time is True @@ -2104,126 +1843,59 @@ def test_timedelta(mock_is_safe, hass: HomeAssistant) -> None: """Test relative_time method.""" now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") with freeze_time(now): - result = template.Template( - "{{timedelta(seconds=120)}}", - hass, - ).async_render() + result = render(hass, "{{timedelta(seconds=120)}}") assert result == "0:02:00" - result = template.Template( - "{{timedelta(seconds=86400)}}", - hass, - ).async_render() + result = render(hass, "{{timedelta(seconds=86400)}}") assert result == "1 day, 0:00:00" - result = template.Template( - "{{timedelta(days=1, hours=4)}}", hass - ).async_render() + result = render(hass, "{{timedelta(days=1, hours=4)}}") assert result == "1 day, 4:00:00" - result = template.Template( - "{{relative_time(now() - timedelta(seconds=3600))}}", - hass, - ).async_render() + result = render(hass, "{{relative_time(now() - timedelta(seconds=3600))}}") assert result == "1 hour" - result = template.Template( - "{{relative_time(now() - timedelta(seconds=86400))}}", - hass, - ).async_render() + result = render(hass, "{{relative_time(now() - timedelta(seconds=86400))}}") assert result == "1 day" - result = template.Template( - "{{relative_time(now() - timedelta(seconds=86401))}}", - hass, - ).async_render() + result = render(hass, "{{relative_time(now() - timedelta(seconds=86401))}}") assert result == "1 day" - result = template.Template( - "{{relative_time(now() - timedelta(weeks=2, days=1))}}", - hass, - ).async_render() + result = render(hass, "{{relative_time(now() - timedelta(weeks=2, days=1))}}") assert result == "15 days" def test_version(hass: HomeAssistant) -> None: """Test version filter and function.""" - filter_result = template.Template( - "{{ '2099.9.9' | version}}", - hass, - ).async_render() - function_result = template.Template( - "{{ version('2099.9.9')}}", - hass, - ).async_render() + filter_result = render(hass, "{{ '2099.9.9' | version}}") + function_result = render(hass, "{{ version('2099.9.9')}}") assert filter_result == function_result == "2099.9.9" - filter_result = template.Template( - "{{ '2099.9.9' | version < '2099.9.10' }}", - hass, - ).async_render() - function_result = template.Template( - "{{ version('2099.9.9') < '2099.9.10' }}", - hass, - ).async_render() + filter_result = render(hass, "{{ '2099.9.9' | version < '2099.9.10' }}") + function_result = render(hass, "{{ version('2099.9.9') < '2099.9.10' }}") assert filter_result is function_result is True - filter_result = template.Template( - "{{ '2099.9.9' | version == '2099.9.9' }}", - hass, - ).async_render() - function_result = template.Template( - "{{ version('2099.9.9') == '2099.9.9' }}", - hass, - ).async_render() + filter_result = render(hass, "{{ '2099.9.9' | version == '2099.9.9' }}") + function_result = render(hass, "{{ version('2099.9.9') == '2099.9.9' }}") assert filter_result is function_result is True with pytest.raises(TemplateError): - template.Template( - "{{ version(None) < '2099.9.10' }}", - hass, - ).async_render() + render(hass, "{{ version(None) < '2099.9.10' }}") def test_pack(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None: """Test struct pack method.""" # render as filter - tpl = template.Template( - """ -{{ value | pack('>I') }} - """, - hass, - ) - variables = { - "value": 0xDEADBEEF, - } - assert tpl.async_render(variables=variables) == b"\xde\xad\xbe\xef" + variables = {"value": 0xDEADBEEF} + assert render(hass, "{{ value | pack('>I') }}", variables) == b"\xde\xad\xbe\xef" # render as function - tpl = template.Template( - """ -{{ pack(value, '>I') }} - """, - hass, - ) - variables = { - "value": 0xDEADBEEF, - } - assert tpl.async_render(variables=variables) == b"\xde\xad\xbe\xef" + assert render(hass, "{{ pack(value, '>I') }}", variables) == b"\xde\xad\xbe\xef" # test with None value - tpl = template.Template( - """ -{{ pack(value, '>I') }} - """, - hass, - ) - variables = { - "value": None, - } # "Template warning: 'pack' unable to pack object with type '%s' and format_string '%s' see https://docs.python.org/3/library/struct.html for more information" - assert tpl.async_render(variables=variables) is None + assert render(hass, "{{ pack(value, '>I') }}", {"value": None}) is None assert ( "Template warning: 'pack' unable to pack object 'None' with type 'NoneType' and" " format_string '>I' see https://docs.python.org/3/library/struct.html for more" @@ -2231,17 +1903,8 @@ def test_pack(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None: ) # test with invalid filter - tpl = template.Template( - """ -{{ pack(value, 'invalid filter') }} - """, - hass, - ) - variables = { - "value": 0xDEADBEEF, - } # "Template warning: 'pack' unable to pack object with type '%s' and format_string '%s' see https://docs.python.org/3/library/struct.html for more information" - assert tpl.async_render(variables=variables) is None + assert render(hass, "{{ pack(value, 'invalid filter') }}", variables) is None assert ( "Template warning: 'pack' unable to pack object '3735928559' with type 'int'" " and format_string 'invalid filter' see" @@ -2253,53 +1916,22 @@ def test_pack(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None: def test_unpack(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None: """Test struct unpack method.""" + variables = {"value": b"\xde\xad\xbe\xef"} + # render as filter - tpl = template.Template( - """ -{{ value | unpack('>I') }} - """, - hass, - ) - variables = { - "value": b"\xde\xad\xbe\xef", - } - assert tpl.async_render(variables=variables) == 0xDEADBEEF + result = render(hass, """{{ value | unpack('>I') }}""", variables) + assert result == 0xDEADBEEF # render as function - tpl = template.Template( - """ -{{ unpack(value, '>I') }} - """, - hass, - ) - variables = { - "value": b"\xde\xad\xbe\xef", - } - assert tpl.async_render(variables=variables) == 0xDEADBEEF + result = render(hass, """{{ unpack(value, '>I') }}""", variables) + assert result == 0xDEADBEEF # unpack with offset - tpl = template.Template( - """ -{{ unpack(value, '>H', offset=2) }} - """, - hass, - ) - variables = { - "value": b"\xde\xad\xbe\xef", - } - assert tpl.async_render(variables=variables) == 0xBEEF + result = render(hass, """{{ unpack(value, '>H', offset=2) }}""", variables) + assert result == 0xBEEF # test with an empty bytes object - tpl = template.Template( - """ -{{ unpack(value, '>I') }} - """, - hass, - ) - variables = { - "value": b"", - } - assert tpl.async_render(variables=variables) is None + assert render(hass, """{{ unpack(value, '>I') }}""", {"value": b""}) is None assert ( "Template warning: 'unpack' unable to unpack object 'b''' with format_string" " '>I' and offset 0 see https://docs.python.org/3/library/struct.html for more" @@ -2307,16 +1939,10 @@ def test_unpack(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None: ) # test with invalid filter - tpl = template.Template( - """ -{{ unpack(value, 'invalid filter') }} - """, - hass, + assert ( + render(hass, """{{ unpack(value, 'invalid filter') }}""", {"value": b""}) + is None ) - variables = { - "value": b"", - } - assert tpl.async_render(variables=variables) is None assert ( "Template warning: 'unpack' unable to unpack object 'b''' with format_string" " 'invalid filter' and offset 0 see" @@ -2331,8 +1957,9 @@ def test_distance_function_with_1_state(hass: HomeAssistant) -> None: hass.states.async_set( "test.object", "happy", {"latitude": 32.87336, "longitude": -117.22943} ) - tpl = template.Template("{{ distance(states.test.object) | round }}", hass) - assert tpl.async_render() == 187 + + result = render(hass, "{{ distance(states.test.object) | round }}") + assert result == 187 def test_distance_function_with_2_states(hass: HomeAssistant) -> None: @@ -2346,29 +1973,26 @@ def test_distance_function_with_2_states(hass: HomeAssistant) -> None: "happy", {"latitude": hass.config.latitude, "longitude": hass.config.longitude}, ) - tpl = template.Template( - "{{ distance(states.test.object, states.test.object_2) | round }}", hass + + result = render( + hass, "{{ distance(states.test.object, states.test.object_2) | round }}" ) - assert tpl.async_render() == 187 + assert result == 187 def test_distance_function_with_1_coord(hass: HomeAssistant) -> None: """Test distance function with 1 coord.""" _set_up_units(hass) - tpl = template.Template('{{ distance("32.87336", "-117.22943") | round }}', hass) - assert tpl.async_render() == 187 + + result = render(hass, '{{ distance("32.87336", "-117.22943") | round }}') + assert result == 187 def test_distance_function_with_2_coords(hass: HomeAssistant) -> None: """Test distance function with 2 coords.""" _set_up_units(hass) - assert ( - template.Template( - f'{{{{ distance("32.87336", "-117.22943", {hass.config.latitude}, {hass.config.longitude}) | round }}}}', - hass, - ).async_render() - == 187 - ) + tpl = f'{{{{ distance("32.87336", "-117.22943", {hass.config.latitude}, {hass.config.longitude}) | round }}}}' + assert render(hass, tpl) == 187 def test_distance_function_with_1_state_1_coord(hass: HomeAssistant) -> None: @@ -2379,42 +2003,39 @@ def test_distance_function_with_1_state_1_coord(hass: HomeAssistant) -> None: "happy", {"latitude": hass.config.latitude, "longitude": hass.config.longitude}, ) - tpl = template.Template( - '{{ distance("32.87336", "-117.22943", states.test.object_2) | round }}', - hass, + + result = render( + hass, '{{ distance("32.87336", "-117.22943", states.test.object_2) | round }}' ) - assert tpl.async_render() == 187 + assert result == 187 - tpl2 = template.Template( - '{{ distance(states.test.object_2, "32.87336", "-117.22943") | round }}', - hass, + result = render( + hass, '{{ distance(states.test.object_2, "32.87336", "-117.22943") | round }}' ) - assert tpl2.async_render() == 187 + assert result == 187 def test_distance_function_return_none_if_invalid_state(hass: HomeAssistant) -> None: """Test distance function return None if invalid state.""" hass.states.async_set("test.object_2", "happy", {"latitude": 10}) - tpl = template.Template("{{ distance(states.test.object_2) | round }}", hass) with pytest.raises(TemplateError): - tpl.async_render() + render(hass, "{{ distance(states.test.object_2) | round }}") def test_distance_function_return_none_if_invalid_coord(hass: HomeAssistant) -> None: """Test distance function return None if invalid coord.""" - assert ( - template.Template('{{ distance("123", "abc") }}', hass).async_render() is None - ) + assert render(hass, '{{ distance("123", "abc") }}') is None - assert template.Template('{{ distance("123") }}', hass).async_render() is None + assert render(hass, '{{ distance("123") }}') is None hass.states.async_set( "test.object_2", "happy", {"latitude": hass.config.latitude, "longitude": hass.config.longitude}, ) - tpl = template.Template('{{ distance("123", states.test_object_2) }}', hass) - assert tpl.async_render() is None + + result = render(hass, '{{ distance("123", states.test_object_2) }}') + assert result is None def test_distance_function_with_2_entity_ids(hass: HomeAssistant) -> None: @@ -2428,10 +2049,9 @@ def test_distance_function_with_2_entity_ids(hass: HomeAssistant) -> None: "happy", {"latitude": hass.config.latitude, "longitude": hass.config.longitude}, ) - tpl = template.Template( - '{{ distance("test.object", "test.object_2") | round }}', hass - ) - assert tpl.async_render() == 187 + + result = render(hass, '{{ distance("test.object", "test.object_2") | round }}') + assert result == 187 def test_distance_function_with_1_entity_1_coord(hass: HomeAssistant) -> None: @@ -2442,10 +2062,11 @@ def test_distance_function_with_1_entity_1_coord(hass: HomeAssistant) -> None: "happy", {"latitude": hass.config.latitude, "longitude": hass.config.longitude}, ) - tpl = template.Template( - '{{ distance("test.object", "32.87336", "-117.22943") | round }}', hass + + result = render( + hass, '{{ distance("test.object", "32.87336", "-117.22943") | round }}' ) - assert tpl.async_render() == 187 + assert result == 187 def test_closest_function_home_vs_domain(hass: HomeAssistant) -> None: @@ -2466,16 +2087,12 @@ def test_closest_function_home_vs_domain(hass: HomeAssistant) -> None: ) assert ( - template.Template( - "{{ closest(states.test_domain).entity_id }}", hass - ).async_render() + render(hass, "{{ closest(states.test_domain).entity_id }}") == "test_domain.object" ) assert ( - template.Template( - "{{ (states.test_domain | closest).entity_id }}", hass - ).async_render() + render(hass, "{{ (states.test_domain | closest).entity_id }}") == "test_domain.object" ) @@ -2497,14 +2114,10 @@ def test_closest_function_home_vs_all_states(hass: HomeAssistant) -> None: {"latitude": hass.config.latitude, "longitude": hass.config.longitude}, ) - assert ( - template.Template("{{ closest(states).entity_id }}", hass).async_render() - == "test_domain_2.and_closer" - ) + assert render(hass, "{{ closest(states).entity_id }}") == "test_domain_2.and_closer" assert ( - template.Template("{{ (states | closest).entity_id }}", hass).async_render() - == "test_domain_2.and_closer" + render(hass, "{{ (states | closest).entity_id }}") == "test_domain_2.and_closer" ) @@ -3204,30 +2817,30 @@ async def test_config_entry_attr(hass: HomeAssistant) -> None: info["state"] = config_entries.ConfigEntryState.NOT_LOADED for key, value in info.items(): - tpl = template.Template( - "{{ config_entry_attr('" + config_entry.entry_id + "', '" + key + "') }}", + assert render( hass, - ) - assert tpl.async_render(parse_result=False) == str(value) + "{{ config_entry_attr('" + config_entry.entry_id + "', '" + key + "') }}", + parse_result=False, + ) == str(value) for config_entry_id, key in ( (config_entry.entry_id, "invalid_key"), (56, "domain"), ): with pytest.raises(TemplateError): - template.Template( + render( + hass, "{{ config_entry_attr(" + json.dumps(config_entry_id) + ", '" + key + "') }}", - hass, - ).async_render() + ) assert ( - template.Template( - "{{ config_entry_attr('invalid_id', 'domain') }}", hass - ).async_render(parse_result=False) + render( + hass, "{{ config_entry_attr('invalid_id', 'domain') }}", parse_result=False + ) == "None" ) @@ -3622,19 +3235,17 @@ def test_closest_function_to_coord(hass: HomeAssistant) -> None: }, ) - tpl = template.Template( - f'{{{{ closest("{hass.config.latitude + 0.3}", {hass.config.longitude + 0.3}, states.test_domain).entity_id }}}}', + result = render( hass, + f'{{{{ closest("{hass.config.latitude + 0.3}", {hass.config.longitude + 0.3}, states.test_domain).entity_id }}}}', ) + assert result == "test_domain.closest_zone" - assert tpl.async_render() == "test_domain.closest_zone" - - tpl = template.Template( - f'{{{{ (states.test_domain | closest("{hass.config.latitude + 0.3}", {hass.config.longitude + 0.3})).entity_id }}}}', + result = render( hass, + f'{{{{ (states.test_domain | closest("{hass.config.latitude + 0.3}", {hass.config.longitude + 0.3})).entity_id }}}}', ) - - assert tpl.async_render() == "test_domain.closest_zone" + assert result == "test_domain.closest_zone" def test_async_render_to_info_with_branching(hass: HomeAssistant) -> None: @@ -3915,9 +3526,9 @@ def test_closest_function_to_state(hass: HomeAssistant) -> None: ) assert ( - template.Template( - "{{ closest(states.zone.far_away, states.test_domain).entity_id }}", hass - ).async_render() + render( + hass, "{{ closest(states.zone.far_away, states.test_domain).entity_id }}" + ) == "test_domain.closest_zone" ) @@ -3934,12 +3545,7 @@ def test_closest_function_invalid_state(hass: HomeAssistant) -> None: ) for state in ("states.zone.non_existing", '"zone.non_existing"'): - assert ( - template.Template( - f"{{{{ closest({state}, states) }}}}", hass - ).async_render() - is None - ) + assert render(hass, f"{{{{ closest({state}, states) }}}}") is None def test_closest_function_state_with_invalid_location(hass: HomeAssistant) -> None: @@ -3951,10 +3557,7 @@ def test_closest_function_state_with_invalid_location(hass: HomeAssistant) -> No ) assert ( - template.Template( - "{{ closest(states.test_domain.closest_home, states) }}", hass - ).async_render() - is None + render(hass, "{{ closest(states.test_domain.closest_home, states) }}") is None ) @@ -3969,25 +3572,13 @@ def test_closest_function_invalid_coordinates(hass: HomeAssistant) -> None: }, ) - assert ( - template.Template( - '{{ closest("invalid", "coord", states) }}', hass - ).async_render() - is None - ) - assert ( - template.Template( - '{{ states | closest("invalid", "coord") }}', hass - ).async_render() - is None - ) + assert render(hass, '{{ closest("invalid", "coord", states) }}') is None + assert render(hass, '{{ states | closest("invalid", "coord") }}') is None def test_closest_function_no_location_states(hass: HomeAssistant) -> None: """Test closest function without location states.""" - assert ( - template.Template("{{ closest(states).entity_id }}", hass).async_render() == "" - ) + assert render(hass, "{{ closest(states).entity_id }}") == "" def test_generate_filter_iterators(hass: HomeAssistant) -> None: @@ -4054,15 +3645,14 @@ def test_generate_select(hass: HomeAssistant) -> None: |join(",", attribute="entity_id") }} """ - tmp = template.Template(template_str, hass) - info = tmp.async_render_to_info() + info = render_to_info(hass, template_str) assert_result_info(info, "", [], []) assert info.domains_lifecycle == {"sensor"} hass.states.async_set("sensor.test_sensor", "off", {"attr": "value"}) hass.states.async_set("sensor.test_sensor_on", "on") - info = tmp.async_render_to_info() + info = render_to_info(hass, template_str) assert_result_info( info, "sensor.test_sensor", @@ -4074,12 +3664,7 @@ def test_generate_select(hass: HomeAssistant) -> None: async def test_async_render_to_info_in_conditional(hass: HomeAssistant) -> None: """Test extract entities function with none entities stuff.""" - template_str = """ -{{ states("sensor.xyz") == "dog" }} - """ - - tmp = template.Template(template_str, hass) - info = tmp.async_render_to_info() + info = render_to_info(hass, '{{ states("sensor.xyz") == "dog" }}') assert_result_info(info, False, ["sensor.xyz"], []) hass.states.async_set("sensor.xyz", "dog") @@ -4094,8 +3679,7 @@ async def test_async_render_to_info_in_conditional(hass: HomeAssistant) -> None: {% endif %} """ - tmp = template.Template(template_str, hass) - info = tmp.async_render_to_info() + info = render_to_info(hass, template_str) assert_result_info(info, True, ["sensor.xyz", "sensor.cow"], []) hass.states.async_set("sensor.xyz", "sheep") @@ -4103,8 +3687,7 @@ async def test_async_render_to_info_in_conditional(hass: HomeAssistant) -> None: await hass.async_block_till_done() - tmp = template.Template(template_str, hass) - info = tmp.async_render_to_info() + info = render_to_info(hass, template_str) assert_result_info(info, "oink", ["sensor.xyz", "sensor.pig"], []) @@ -4131,23 +3714,19 @@ def test_state_with_unit(hass: HomeAssistant) -> None: hass.states.async_set("sensor.test", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"}) hass.states.async_set("sensor.test2", "wow") - tpl = template.Template("{{ states.sensor.test.state_with_unit }}", hass) - - assert tpl.async_render() == "23 beers" + result = render(hass, "{{ states.sensor.test.state_with_unit }}") + assert result == "23 beers" - tpl = template.Template("{{ states.sensor.test2.state_with_unit }}", hass) + result = render(hass, "{{ states.sensor.test2.state_with_unit }}") + assert result == "wow" - assert tpl.async_render() == "wow" - - tpl = template.Template( - "{% for state in states %}{{ state.state_with_unit }} {% endfor %}", hass + result = render( + hass, "{% for state in states %}{{ state.state_with_unit }} {% endfor %}" ) + assert result == "23 beers wow" - assert tpl.async_render() == "23 beers wow" - - tpl = template.Template("{{ states.sensor.non_existing.state_with_unit }}", hass) - - assert tpl.async_render() == "" + result = render(hass, "{{ states.sensor.non_existing.state_with_unit }}") + assert result == "" def test_state_with_unit_and_rounding( @@ -4273,11 +3852,11 @@ def test_length_of_states(hass: HomeAssistant) -> None: hass.states.async_set("sensor.test2", "wow") hass.states.async_set("climate.test2", "cooling") - tpl = template.Template("{{ states | length }}", hass) - assert tpl.async_render() == 3 + result = render(hass, "{{ states | length }}") + assert result == 3 - tpl = template.Template("{{ states.sensor | length }}", hass) - assert tpl.async_render() == 2 + result = render(hass, "{{ states.sensor | length }}") + assert result == 2 def test_render_complex_handling_non_template_values(hass: HomeAssistant) -> None: @@ -4289,46 +3868,48 @@ def test_render_complex_handling_non_template_values(hass: HomeAssistant) -> Non def test_as_timedelta(hass: HomeAssistant) -> None: """Test the as_timedelta function/filter.""" - tpl = template.Template("{{ as_timedelta('PT10M') }}", hass) - assert tpl.async_render() == "0:10:00" - tpl = template.Template("{{ 'PT10M' | as_timedelta }}", hass) - assert tpl.async_render() == "0:10:00" + result = render(hass, "{{ as_timedelta('PT10M') }}") + assert result == "0:10:00" + + result = render(hass, "{{ 'PT10M' | as_timedelta }}") + assert result == "0:10:00" - tpl = template.Template("{{ 'T10M' | as_timedelta }}", hass) - assert tpl.async_render() is None + result = render(hass, "{{ 'T10M' | as_timedelta }}") + assert result is None def test_iif(hass: HomeAssistant) -> None: """Test the immediate if function/filter.""" - tpl = template.Template("{{ (1 == 1) | iif }}", hass) - assert tpl.async_render() is True - tpl = template.Template("{{ (1 == 2) | iif }}", hass) - assert tpl.async_render() is False + result = render(hass, "{{ (1 == 1) | iif }}") + assert result is True - tpl = template.Template("{{ (1 == 1) | iif('yes') }}", hass) - assert tpl.async_render() == "yes" + result = render(hass, "{{ (1 == 2) | iif }}") + assert result is False - tpl = template.Template("{{ (1 == 2) | iif('yes') }}", hass) - assert tpl.async_render() is False + result = render(hass, "{{ (1 == 1) | iif('yes') }}") + assert result == "yes" - tpl = template.Template("{{ (1 == 2) | iif('yes', 'no') }}", hass) - assert tpl.async_render() == "no" + result = render(hass, "{{ (1 == 2) | iif('yes') }}") + assert result is False - tpl = template.Template("{{ not_exists | default(None) | iif('yes', 'no') }}", hass) - assert tpl.async_render() == "no" + result = render(hass, "{{ (1 == 2) | iif('yes', 'no') }}") + assert result == "no" - tpl = template.Template( - "{{ not_exists | default(None) | iif('yes', 'no', 'unknown') }}", hass + result = render(hass, "{{ not_exists | default(None) | iif('yes', 'no') }}") + assert result == "no" + + result = render( + hass, "{{ not_exists | default(None) | iif('yes', 'no', 'unknown') }}" ) - assert tpl.async_render() == "unknown" + assert result == "unknown" - tpl = template.Template("{{ iif(1 == 1) }}", hass) - assert tpl.async_render() is True + result = render(hass, "{{ iif(1 == 1) }}") + assert result is True - tpl = template.Template("{{ iif(1 == 2, 'yes', 'no') }}", hass) - assert tpl.async_render() == "no" + result = render(hass, "{{ iif(1 == 2, 'yes', 'no') }}") + assert result == "no" @pytest.mark.usefixtures("hass") @@ -4366,17 +3947,14 @@ def test_is_template_string() -> None: async def test_protected_blocked(hass: HomeAssistant) -> None: """Test accessing __getattr__ produces a template error.""" - tmp = template.Template('{{ states.__getattr__("any") }}', hass) with pytest.raises(TemplateError): - tmp.async_render() + render(hass, '{{ states.__getattr__("any") }}') - tmp = template.Template('{{ states.sensor.__getattr__("any") }}', hass) with pytest.raises(TemplateError): - tmp.async_render() + render(hass, '{{ states.sensor.__getattr__("any") }}') - tmp = template.Template('{{ states.sensor.any.__getattr__("any") }}', hass) with pytest.raises(TemplateError): - tmp.async_render() + render(hass, '{{ states.sensor.any.__getattr__("any") }}') async def test_demo_template(hass: HomeAssistant) -> None: @@ -4412,9 +3990,7 @@ async def test_demo_template(hass: HomeAssistant) -> None: {{ state.name | lower }} is {{state.state_with_unit}} {%- endfor %}. """ - tmp = template.Template(demo_template_str, hass) - - result = tmp.async_render() + result = render(hass, demo_template_str) assert "The temperature is 25" in result assert "is on" in result assert "sensor0" in result @@ -4426,15 +4002,15 @@ async def test_slice_states(hass: HomeAssistant) -> None: """Test iterating states with a slice.""" hass.states.async_set("sensor.test", "23") - tpl = template.Template( + result = render( + hass, ( "{% for states in states | slice(1) -%}{% set state = states | first %}" "{{ state.entity_id }}" "{%- endfor %}" ), - hass, ) - assert tpl.async_render() == "sensor.test" + assert result == "sensor.test" async def test_lifecycle(hass: HomeAssistant) -> None: @@ -4455,9 +4031,7 @@ async def test_lifecycle(hass: HomeAssistant) -> None: await hass.async_block_till_done() - tmp = template.Template("{{ states | count }}", hass) - - info = tmp.async_render_to_info() + info = render_to_info(hass, "{{ states | count }}") assert info.all_states is False assert info.all_states_lifecycle is True assert info.rate_limit is None @@ -4525,8 +4099,7 @@ async def test_lights(hass: HomeAssistant) -> None: states.append(f"light.sensor{i}") hass.states.async_set(f"light.sensor{i}", "on") - tmp = template.Template(tmpl, hass) - info = tmp.async_render_to_info() + info = render_to_info(hass, tmpl) assert info.entities == set() assert info.domains == {"light"} @@ -4539,64 +4112,42 @@ async def test_template_errors(hass: HomeAssistant) -> None: """Test template rendering wraps exceptions with TemplateError.""" with pytest.raises(TemplateError): - template.Template("{{ now() | rando }}", hass).async_render() + render(hass, "{{ now() | rando }}") with pytest.raises(TemplateError): - template.Template("{{ utcnow() | rando }}", hass).async_render() + render(hass, "{{ utcnow() | rando }}") with pytest.raises(TemplateError): - template.Template("{{ now() | random }}", hass).async_render() + render(hass, "{{ now() | random }}") with pytest.raises(TemplateError): - template.Template("{{ utcnow() | random }}", hass).async_render() + render(hass, "{{ utcnow() | random }}") async def test_state_attributes(hass: HomeAssistant) -> None: """Test state attributes.""" hass.states.async_set("sensor.test", "23") - tpl = template.Template( - "{{ states.sensor.test.last_changed }}", - hass, - ) - assert tpl.async_render() == str(hass.states.get("sensor.test").last_changed) + result = render(hass, "{{ states.sensor.test.last_changed }}") + assert result == str(hass.states.get("sensor.test").last_changed) - tpl = template.Template( - "{{ states.sensor.test.object_id }}", - hass, - ) - assert tpl.async_render() == hass.states.get("sensor.test").object_id + result = render(hass, "{{ states.sensor.test.object_id }}") + assert result == hass.states.get("sensor.test").object_id - tpl = template.Template( - "{{ states.sensor.test.domain }}", - hass, - ) - assert tpl.async_render() == hass.states.get("sensor.test").domain + result = render(hass, "{{ states.sensor.test.domain }}") + assert result == hass.states.get("sensor.test").domain - tpl = template.Template( - "{{ states.sensor.test.context.id }}", - hass, - ) - assert tpl.async_render() == hass.states.get("sensor.test").context.id + result = render(hass, "{{ states.sensor.test.context.id }}") + assert result == hass.states.get("sensor.test").context.id - tpl = template.Template( - "{{ states.sensor.test.state_with_unit }}", - hass, - ) - assert tpl.async_render() == 23 + result = render(hass, "{{ states.sensor.test.state_with_unit }}") + assert result == 23 - tpl = template.Template( - "{{ states.sensor.test.invalid_prop }}", - hass, - ) - assert tpl.async_render() == "" + result = render(hass, "{{ states.sensor.test.invalid_prop }}") + assert result == "" - tpl = template.Template( - "{{ states.sensor.test.invalid_prop.xx }}", - hass, - ) with pytest.raises(TemplateError): - tpl.async_render() + render(hass, "{{ states.sensor.test.invalid_prop.xx }}") async def test_unavailable_states(hass: HomeAssistant) -> None: @@ -4609,25 +4160,25 @@ async def test_unavailable_states(hass: HomeAssistant) -> None: hass.states.async_set("light.unknown", "unknown") hass.states.async_set("light.none", "none") - tpl = template.Template( + result = render( + hass, ( "{{ states | selectattr('state', 'in', ['unavailable','unknown','none']) " "| sort(attribute='entity_id') | map(attribute='entity_id') | list | join(', ') }}" ), - hass, ) - assert tpl.async_render() == "light.none, light.unavailable, light.unknown" + assert result == "light.none, light.unavailable, light.unknown" - tpl = template.Template( + result = render( + hass, ( "{{ states.light " "| selectattr('state', 'in', ['unavailable','unknown','none']) " "| sort(attribute='entity_id') | map(attribute='entity_id') | list " "| join(', ') }}" ), - hass, ) - assert tpl.async_render() == "light.none, light.unavailable, light.unknown" + assert result == "light.none, light.unavailable, light.unknown" async def test_no_result_parsing(hass: HomeAssistant) -> None: @@ -4635,21 +4186,13 @@ async def test_no_result_parsing(hass: HomeAssistant) -> None: hass.states.async_set("sensor.temperature", "12") assert ( - template.Template("{{ states.sensor.temperature.state }}", hass).async_render( - parse_result=False - ) + render(hass, "{{ states.sensor.temperature.state }}", parse_result=False) == "12" ) - assert ( - template.Template("{{ false }}", hass).async_render(parse_result=False) - == "False" - ) + assert render(hass, "{{ false }}", parse_result=False) == "False" - assert ( - template.Template("{{ [1, 2, 3] }}", hass).async_render(parse_result=False) - == "[1, 2, 3]" - ) + assert render(hass, "{{ [1, 2, 3] }}", parse_result=False) == "[1, 2, 3]" async def test_is_static_still_ast_evals(hass: HomeAssistant) -> None: @@ -4667,8 +4210,7 @@ async def test_result_wrappers(hass: HomeAssistant) -> None: ("(1, 2)", (1, 2), tuple, vol.ExactSequence([int, int])), ('{"hello": True}', {"hello": True}, dict, vol.Schema({"hello": bool})), ): - tpl = template.Template(text, hass) - result = tpl.async_render() + result = render(hass, text) assert isinstance(result, orig_type) assert isinstance(result, template.ResultWrapper) assert result == native @@ -4706,7 +4248,7 @@ async def test_parse_result(hass: HomeAssistant) -> None: ("010", "010"), ("0011101.00100001010001", "0011101.00100001010001"), ): - assert template.Template(tpl, hass).async_render() == result + assert render(hass, tpl) == result @pytest.mark.parametrize( @@ -4723,11 +4265,11 @@ async def test_undefined_symbol_warnings( template_string: str, ) -> None: """Test a warning is logged on undefined variables.""" - tpl = template.Template(template_string, hass) - assert tpl.async_render() == "" + + assert render(hass, template_string) == "" assert ( - "Template variable warning: 'no_such_variable' is undefined when rendering " - f"'{template_string}'" in caplog.text + f"Template variable warning: 'no_such_variable' is undefined when rendering '{template_string}'" + in caplog.text ) @@ -4766,15 +4308,11 @@ async def test_template_states_can_serialize(hass: HomeAssistant) -> None: def test_contains(hass: HomeAssistant, seq, value, expected) -> None: """Test contains.""" assert ( - template.Template("{{ seq | contains(value) }}", hass).async_render( - {"seq": seq, "value": value} - ) + render(hass, "{{ seq | contains(value) }}", {"seq": seq, "value": value}) == expected ) assert ( - template.Template("{{ seq is contains(value) }}", hass).async_render( - {"seq": seq, "value": value} - ) + render(hass, "{{ seq is contains(value) }}", {"seq": seq, "value": value}) == expected ) @@ -5529,9 +5067,8 @@ async def test_template_thread_safety_checks(hass: HomeAssistant) -> None: def test_template_output_exceeds_maximum_size(hass: HomeAssistant) -> None: """Test template output exceeds maximum size.""" - tpl = template.Template("{{ 'a' * 1024 * 257 }}", hass) with pytest.raises(TemplateError): - tpl.async_render() + render(hass, "{{ 'a' * 1024 * 257 }}") @pytest.mark.parametrize( @@ -5691,9 +5228,11 @@ async def test_merge_response( _template = "{{ merge_response(" + str(service_response) + ") }}" - tpl = template.Template(_template, hass) assert service_response == snapshot(name="a_response") - assert tpl.async_render() == snapshot(name="b_rendered") + assert render( + hass, + _template, + ) == snapshot(name="b_rendered") async def test_merge_response_with_entity_id_in_response( @@ -5711,7 +5250,7 @@ async def test_merge_response_with_entity_id_in_response( TemplateError, match="ValueError: Response dictionary already contains key 'entity_id'", ): - template.Template(_template, hass).async_render() + render(hass, _template) service_response = { "test.response": { @@ -5730,7 +5269,7 @@ async def test_merge_response_with_entity_id_in_response( TemplateError, match="ValueError: Response dictionary already contains key 'entity_id'", ): - template.Template(_template, hass).async_render() + render(hass, _template) async def test_merge_response_with_empty_response( @@ -5745,9 +5284,8 @@ async def test_merge_response_with_empty_response( "calendar.yap_house_schedules": {"events": []}, } _template = "{{ merge_response(" + str(service_response) + ") }}" - tpl = template.Template(_template, hass) assert service_response == snapshot(name="a_response") - assert tpl.async_render() == snapshot(name="b_rendered") + assert render(hass, _template) == snapshot(name="b_rendered") async def test_response_empty_dict( @@ -5758,8 +5296,9 @@ async def test_response_empty_dict( service_response = {} _template = "{{ merge_response(" + str(service_response) + ") }}" - tpl = template.Template(_template, hass) - assert tpl.async_render() == [] + + result = render(hass, _template) + assert result == [] async def test_response_incorrect_value( @@ -5771,7 +5310,7 @@ async def test_response_incorrect_value( service_response = "incorrect" _template = "{{ merge_response(" + str(service_response) + ") }}" with pytest.raises(TemplateError, match="TypeError: Response is not a dictionary"): - template.Template(_template, hass).async_render() + render(hass, _template) async def test_merge_response_with_incorrect_response(hass: HomeAssistant) -> None: @@ -5779,17 +5318,15 @@ async def test_merge_response_with_incorrect_response(hass: HomeAssistant) -> No service_response = {"calendar.sports": []} _template = "{{ merge_response(" + str(service_response) + ") }}" - tpl = template.Template(_template, hass) with pytest.raises(TemplateError, match="TypeError: Response is not a dictionary"): - tpl.async_render() + render(hass, _template) service_response = { "binary_sensor.workday": [], } _template = "{{ merge_response(" + str(service_response) + ") }}" - tpl = template.Template(_template, hass) with pytest.raises(TemplateError, match="TypeError: Response is not a dictionary"): - tpl.async_render() + render(hass, _template) def test_warn_no_hass(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None: @@ -5823,68 +5360,65 @@ async def test_merge_response_not_mutate_original_object( "{{ merge_response(calendar_response) }}" ) - tpl = template.Template(_template, hass) - assert tpl.async_render() + assert render(hass, _template) def test_typeof(hass: HomeAssistant) -> None: """Test the typeof debug filter/function.""" - assert template.Template("{{ True | typeof }}", hass).async_render() == "bool" - assert template.Template("{{ typeof(True) }}", hass).async_render() == "bool" + assert render(hass, "{{ True | typeof }}") == "bool" + assert render(hass, "{{ typeof(True) }}") == "bool" - assert template.Template("{{ [1, 2, 3] | typeof }}", hass).async_render() == "list" - assert template.Template("{{ typeof([1, 2, 3]) }}", hass).async_render() == "list" + assert render(hass, "{{ [1, 2, 3] | typeof }}") == "list" + assert render(hass, "{{ typeof([1, 2, 3]) }}") == "list" - assert template.Template("{{ 1 | typeof }}", hass).async_render() == "int" - assert template.Template("{{ typeof(1) }}", hass).async_render() == "int" + assert render(hass, "{{ 1 | typeof }}") == "int" + assert render(hass, "{{ typeof(1) }}") == "int" - assert template.Template("{{ 1.1 | typeof }}", hass).async_render() == "float" - assert template.Template("{{ typeof(1.1) }}", hass).async_render() == "float" + assert render(hass, "{{ 1.1 | typeof }}") == "float" + assert render(hass, "{{ typeof(1.1) }}") == "float" - assert template.Template("{{ None | typeof }}", hass).async_render() == "NoneType" - assert template.Template("{{ typeof(None) }}", hass).async_render() == "NoneType" + assert render(hass, "{{ None | typeof }}") == "NoneType" + assert render(hass, "{{ typeof(None) }}") == "NoneType" - assert ( - template.Template("{{ 'Home Assistant' | typeof }}", hass).async_render() - == "str" - ) - assert ( - template.Template("{{ typeof('Home Assistant') }}", hass).async_render() - == "str" - ) + assert render(hass, "{{ 'Home Assistant' | typeof }}") == "str" + assert render(hass, "{{ typeof('Home Assistant') }}") == "str" def test_combine(hass: HomeAssistant) -> None: """Test combine filter and function.""" - assert template.Template( - "{{ {'a': 1, 'b': 2} | combine({'b': 3, 'c': 4}) }}", hass - ).async_render() == {"a": 1, "b": 3, "c": 4} + assert render(hass, "{{ {'a': 1, 'b': 2} | combine({'b': 3, 'c': 4}) }}") == { + "a": 1, + "b": 3, + "c": 4, + } - assert template.Template( - "{{ combine({'a': 1, 'b': 2}, {'b': 3, 'c': 4}) }}", hass - ).async_render() == {"a": 1, "b": 3, "c": 4} + assert render(hass, "{{ combine({'a': 1, 'b': 2}, {'b': 3, 'c': 4}) }}") == { + "a": 1, + "b": 3, + "c": 4, + } - assert template.Template( - "{{ combine({'a': 1, 'b': {'x': 1}}, {'b': {'y': 2}, 'c': 4}, recursive=True) }}", + assert render( hass, - ).async_render() == {"a": 1, "b": {"x": 1, "y": 2}, "c": 4} + "{{ combine({'a': 1, 'b': {'x': 1}}, {'b': {'y': 2}, 'c': 4}, recursive=True) }}", + ) == {"a": 1, "b": {"x": 1, "y": 2}, "c": 4} # Test that recursive=False does not merge nested dictionaries - assert template.Template( - "{{ combine({'a': 1, 'b': {'x': 1}}, {'b': {'y': 2}, 'c': 4}, recursive=False) }}", + assert render( hass, - ).async_render() == {"a": 1, "b": {"y": 2}, "c": 4} + "{{ combine({'a': 1, 'b': {'x': 1}}, {'b': {'y': 2}, 'c': 4}, recursive=False) }}", + ) == {"a": 1, "b": {"y": 2}, "c": 4} # Test that None values are handled correctly in recursive merge - assert template.Template( - "{{ combine({'a': 1, 'b': none}, {'b': {'y': 2}, 'c': 4}, recursive=True) }}", + assert render( hass, - ).async_render() == {"a": 1, "b": {"y": 2}, "c": 4} + "{{ combine({'a': 1, 'b': none}, {'b': {'y': 2}, 'c': 4}, recursive=True) }}", + ) == {"a": 1, "b": {"y": 2}, "c": 4} with pytest.raises( TemplateError, match="combine expected at least 1 argument, got 0" ): - template.Template("{{ combine() }}", hass).async_render() + render(hass, "{{ combine() }}") with pytest.raises(TemplateError, match="combine expected a dict, got str"): - template.Template("{{ {'a': 1} | combine('not a dict') }}", hass).async_render() + render(hass, "{{ {'a': 1} | combine('not a dict') }}") diff --git a/tests/helpers/template/test_render_info.py b/tests/helpers/template/test_render_info.py index 9b746a8461018f..1f638ec94e579c 100644 --- a/tests/helpers/template/test_render_info.py +++ b/tests/helpers/template/test_render_info.py @@ -17,9 +17,14 @@ ) -def test_render_info_initialization(hass: HomeAssistant) -> None: +@pytest.fixture +def template_obj(hass: HomeAssistant) -> template.Template: + """Template object for test_render_info.""" + return template.Template("{{ 1 + 1 }}", hass) + + +def test_render_info_initialization(template_obj: template.Template) -> None: """Test RenderInfo initialization.""" - template_obj = template.Template("{{ 1 + 1 }}", hass) info = RenderInfo(template_obj) assert info.template is template_obj @@ -37,9 +42,8 @@ def test_render_info_initialization(hass: HomeAssistant) -> None: assert info.filter is _true -def test_render_info_repr(hass: HomeAssistant) -> None: +def test_render_info_repr(template_obj: template.Template) -> None: """Test RenderInfo representation.""" - template_obj = template.Template("{{ 1 + 1 }}", hass) info = RenderInfo(template_obj) info.domains.add("sensor") info.entities.add("sensor.test") @@ -50,9 +54,8 @@ def test_render_info_repr(hass: HomeAssistant) -> None: assert "entities={'sensor.test'}" in repr_str -def test_render_info_result(hass: HomeAssistant) -> None: +def test_render_info_result(template_obj: template.Template) -> None: """Test RenderInfo result property.""" - template_obj = template.Template("{{ 1 + 1 }}", hass) info = RenderInfo(template_obj) # Test with no result set - should return None cast as str @@ -68,9 +71,10 @@ def test_render_info_result(hass: HomeAssistant) -> None: info.result() -def test_render_info_filter_domains_and_entities(hass: HomeAssistant) -> None: +def test_render_info_filter_domains_and_entities( + template_obj: template.Template, +) -> None: """Test RenderInfo entity and domain filtering.""" - template_obj = template.Template("{{ 1 + 1 }}", hass) info = RenderInfo(template_obj) # Add domain and entity @@ -85,9 +89,8 @@ def test_render_info_filter_domains_and_entities(hass: HomeAssistant) -> None: assert info._filter_domains_and_entities("switch.kitchen") is False -def test_render_info_filter_entities(hass: HomeAssistant) -> None: +def test_render_info_filter_entities(template_obj: template.Template) -> None: """Test RenderInfo entity-only filtering.""" - template_obj = template.Template("{{ 1 + 1 }}", hass) info = RenderInfo(template_obj) info.entities.add("sensor.test") @@ -96,9 +99,8 @@ def test_render_info_filter_entities(hass: HomeAssistant) -> None: assert info._filter_entities("sensor.other") is False -def test_render_info_filter_lifecycle_domains(hass: HomeAssistant) -> None: +def test_render_info_filter_lifecycle_domains(template_obj: template.Template) -> None: """Test RenderInfo domain lifecycle filtering.""" - template_obj = template.Template("{{ 1 + 1 }}", hass) info = RenderInfo(template_obj) info.domains_lifecycle.add("sensor") @@ -107,9 +109,8 @@ def test_render_info_filter_lifecycle_domains(hass: HomeAssistant) -> None: assert info._filter_lifecycle_domains("light.test") is False -def test_render_info_freeze_static(hass: HomeAssistant) -> None: +def test_render_info_freeze_static(template_obj: template.Template) -> None: """Test RenderInfo static freezing.""" - template_obj = template.Template("{{ 1 + 1 }}", hass) info = RenderInfo(template_obj) info.domains.add("sensor") @@ -124,9 +125,8 @@ def test_render_info_freeze_static(hass: HomeAssistant) -> None: assert isinstance(info.entities, frozenset) -def test_render_info_freeze(hass: HomeAssistant) -> None: +def test_render_info_freeze(template_obj: template.Template) -> None: """Test RenderInfo freezing with rate limits.""" - template_obj = template.Template("{{ 1 + 1 }}", hass) info = RenderInfo(template_obj) # Test all_states rate limit @@ -147,9 +147,8 @@ def test_render_info_freeze(hass: HomeAssistant) -> None: assert info.rate_limit == ALL_STATES_RATE_LIMIT -def test_render_info_freeze_filters(hass: HomeAssistant) -> None: +def test_render_info_freeze_filters(template_obj: template.Template) -> None: """Test RenderInfo filter assignment during freeze.""" - template_obj = template.Template("{{ 1 + 1 }}", hass) # Test lifecycle filter assignment info = RenderInfo(template_obj) @@ -180,13 +179,12 @@ def test_render_info_freeze_filters(hass: HomeAssistant) -> None: assert info.filter is _false -def test_render_info_context_var(hass: HomeAssistant) -> None: +def test_render_info_context_var(template_obj: template.Template) -> None: """Test render_info_cv context variable.""" # Should start as None assert render_info_cv.get() is None # Test setting and getting - template_obj = template.Template("{{ 1 + 1 }}", hass) info = RenderInfo(template_obj) render_info_cv.set(info) assert render_info_cv.get() is info