mirror of
https://github.com/home-assistant/core.git
synced 2025-08-31 10:21:30 +02:00
2025.8.2 (#150718)
This commit is contained in:
2
Dockerfile
generated
2
Dockerfile
generated
@@ -31,7 +31,7 @@ RUN \
|
||||
&& go2rtc --version
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.7.1
|
||||
RUN pip3 install uv==0.8.9
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["airos==0.2.7"]
|
||||
"requirements": ["airos==0.2.11"]
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==1.0.1",
|
||||
"bleak-retry-connector==4.0.0",
|
||||
"bleak-retry-connector==4.0.1",
|
||||
"bluetooth-adapters==2.0.0",
|
||||
"bluetooth-auto-recovery==1.5.2",
|
||||
"bluetooth-data-tools==1.28.2",
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["cookidoo_api"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["cookidoo-api==0.12.2"]
|
||||
"requirements": ["cookidoo-api==0.14.0"]
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@
|
||||
],
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"aiodhcpwatcher==1.2.0",
|
||||
"aiodhcpwatcher==1.2.1",
|
||||
"aiodiscover==2.7.1",
|
||||
"cached-ipaddress==0.10.0"
|
||||
]
|
||||
|
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pyenphase import Envoy
|
||||
|
||||
from homeassistant.const import CONF_HOST
|
||||
@@ -42,6 +44,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> b
|
||||
},
|
||||
)
|
||||
|
||||
# register envoy before via_device is used
|
||||
device_registry = dr.async_get(hass)
|
||||
if TYPE_CHECKING:
|
||||
assert envoy.serial_number
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, envoy.serial_number)},
|
||||
manufacturer="Enphase",
|
||||
name=coordinator.name,
|
||||
model=envoy.envoy_model,
|
||||
sw_version=str(envoy.firmware),
|
||||
hw_version=envoy.part_number,
|
||||
serial_number=envoy.serial_number,
|
||||
)
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["habiticalib"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["habiticalib==0.4.1"]
|
||||
"requirements": ["habiticalib==0.4.2"]
|
||||
}
|
||||
|
@@ -1330,4 +1330,5 @@ class PlatePowerStep(MieleEnum):
|
||||
plate_step_17 = 17
|
||||
plate_step_18 = 18
|
||||
plate_step_boost = 117, 118, 218
|
||||
plate_step_boost_2 = 217
|
||||
missing2none = -9999
|
||||
|
@@ -76,7 +76,8 @@
|
||||
"plate_step_16": "mdi:circle-slice-7",
|
||||
"plate_step_17": "mdi:circle-slice-8",
|
||||
"plate_step_18": "mdi:circle-slice-8",
|
||||
"plate_step_boost": "mdi:alpha-b-circle-outline"
|
||||
"plate_step_boost": "mdi:alpha-b-circle-outline",
|
||||
"plate_step_boost_2": "mdi:alpha-b-circle"
|
||||
}
|
||||
},
|
||||
"program_type": {
|
||||
|
@@ -8,7 +8,7 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pymiele"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pymiele==0.5.2"],
|
||||
"requirements": ["pymiele==0.5.4"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_mieleathome._tcp.local."]
|
||||
}
|
||||
|
@@ -223,7 +223,8 @@
|
||||
"plate_step_16": "8\u2022",
|
||||
"plate_step_17": "9",
|
||||
"plate_step_18": "9\u2022",
|
||||
"plate_step_boost": "Boost"
|
||||
"plate_step_boost": "Boost",
|
||||
"plate_step_boost_2": "Boost 2"
|
||||
}
|
||||
},
|
||||
"drying_step": {
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/onvif",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["onvif", "wsdiscovery", "zeep"],
|
||||
"requirements": ["onvif-zeep-async==4.0.2", "WSDiscovery==2.1.2"]
|
||||
"requirements": ["onvif-zeep-async==4.0.3", "WSDiscovery==2.1.2"]
|
||||
}
|
||||
|
@@ -82,7 +82,7 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
await hub.getSystem()
|
||||
await hub.setTransport(hub.secured_transport)
|
||||
await hub.setTransport(hub.secured_transport, hub.api_version_detected)
|
||||
|
||||
if not hub.system or not hub.name:
|
||||
raise ConnectionFailure("System data or name is empty")
|
||||
|
@@ -217,6 +217,13 @@ async def determine_api_version(
|
||||
_LOGGER.debug(
|
||||
"Connection to %s failed: %s, trying API version 5", holeV6.base_url, ex_v6
|
||||
)
|
||||
else:
|
||||
# It seems that occasionally the auth can succeed unexpectedly when there is a valid session
|
||||
_LOGGER.warning(
|
||||
"Authenticated with %s through v6 API, but succeeded with an incorrect password. This is a known bug",
|
||||
holeV6.base_url,
|
||||
)
|
||||
return 6
|
||||
holeV5 = api_by_version(hass, entry, 5, password="wrong_token")
|
||||
try:
|
||||
await holeV5.get_data()
|
||||
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from powerfox import Powerfox, PowerfoxConnectionError
|
||||
from powerfox import DeviceType, Powerfox, PowerfoxConnectionError
|
||||
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -31,7 +31,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: PowerfoxConfigEntry) ->
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
coordinators: list[PowerfoxDataUpdateCoordinator] = [
|
||||
PowerfoxDataUpdateCoordinator(hass, entry, client, device) for device in devices
|
||||
PowerfoxDataUpdateCoordinator(hass, entry, client, device)
|
||||
for device in devices
|
||||
# Filter out gas meter devices (Powerfox FLOW adapters) as they are not yet supported and cause integration failures
|
||||
if device.type != DeviceType.GAS_METER
|
||||
]
|
||||
|
||||
await asyncio.gather(
|
||||
|
@@ -45,6 +45,7 @@ class RestData:
|
||||
self._method = method
|
||||
self._resource = resource
|
||||
self._encoding = encoding
|
||||
self._force_use_set_encoding = False
|
||||
|
||||
# Convert auth tuple to aiohttp.BasicAuth if needed
|
||||
if isinstance(auth, tuple) and len(auth) == 2:
|
||||
@@ -152,10 +153,19 @@ class RestData:
|
||||
# Read the response
|
||||
# Only use configured encoding if no charset in Content-Type header
|
||||
# If charset is present in Content-Type, let aiohttp use it
|
||||
if response.charset:
|
||||
if self._force_use_set_encoding is False and response.charset:
|
||||
# Let aiohttp use the charset from Content-Type header
|
||||
self.data = await response.text()
|
||||
else:
|
||||
try:
|
||||
self.data = await response.text()
|
||||
except UnicodeDecodeError as ex:
|
||||
self._force_use_set_encoding = True
|
||||
_LOGGER.debug(
|
||||
"Response charset came back as %s but could not be decoded, continue with configured encoding %s. %s",
|
||||
response.charset,
|
||||
self._encoding,
|
||||
ex,
|
||||
)
|
||||
if self._force_use_set_encoding or not response.charset:
|
||||
# Use configured encoding as fallback
|
||||
self.data = await response.text(encoding=self._encoding)
|
||||
self.headers = response.headers
|
||||
|
@@ -40,7 +40,7 @@ class SnooCoordinator(DataUpdateCoordinator[SnooData]):
|
||||
|
||||
async def setup(self) -> None:
|
||||
"""Perform setup needed on every coordintaor creation."""
|
||||
await self.snoo.subscribe(self.device, self.async_set_updated_data)
|
||||
self.snoo.start_subscribe(self.device, self.async_set_updated_data)
|
||||
# After we subscribe - get the status so that we have something to start with.
|
||||
# We only need to do this once. The device will auto update otherwise.
|
||||
await self.snoo.get_status(self.device)
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["snoo"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["python-snoo==0.6.6"]
|
||||
"requirements": ["python-snoo==0.8.3"]
|
||||
}
|
||||
|
@@ -157,7 +157,7 @@ class BrowseData:
|
||||
|
||||
cmd = ["apps", 0, browse_limit]
|
||||
result = await player.async_query(*cmd)
|
||||
if result["appss_loop"]:
|
||||
if result and result.get("appss_loop"):
|
||||
for app in result["appss_loop"]:
|
||||
app_cmd = "app-" + app["cmd"]
|
||||
if app_cmd not in self.known_apps_radios:
|
||||
@@ -169,7 +169,7 @@ class BrowseData:
|
||||
)
|
||||
cmd = ["radios", 0, browse_limit]
|
||||
result = await player.async_query(*cmd)
|
||||
if result["radioss_loop"]:
|
||||
if result and result.get("radioss_loop"):
|
||||
for app in result["radioss_loop"]:
|
||||
app_cmd = "app-" + app["cmd"]
|
||||
if app_cmd not in self.known_apps_radios:
|
||||
|
@@ -34,16 +34,20 @@ class AbstractTemplateEntity(Entity):
|
||||
self._action_scripts: dict[str, Script] = {}
|
||||
|
||||
if self._optimistic_entity:
|
||||
optimistic = config.get(CONF_OPTIMISTIC)
|
||||
|
||||
self._template = config.get(CONF_STATE)
|
||||
|
||||
optimistic = self._template is None
|
||||
assumed_optimistic = self._template is None
|
||||
if self._extra_optimistic_options:
|
||||
optimistic = optimistic and all(
|
||||
assumed_optimistic = assumed_optimistic and all(
|
||||
config.get(option) is None
|
||||
for option in self._extra_optimistic_options
|
||||
)
|
||||
|
||||
self._attr_assumed_state = optimistic or config.get(CONF_OPTIMISTIC, False)
|
||||
self._attr_assumed_state = optimistic or (
|
||||
optimistic is None and assumed_optimistic
|
||||
)
|
||||
|
||||
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
|
||||
self.entity_id = async_generate_entity_id(
|
||||
|
@@ -102,7 +102,7 @@ TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA = vol.Schema(
|
||||
|
||||
|
||||
TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA = {
|
||||
vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
|
||||
vol.Optional(CONF_OPTIMISTIC): cv.boolean,
|
||||
}
|
||||
|
||||
|
||||
|
@@ -976,11 +976,15 @@ class SpeechManager:
|
||||
if engine_instance.name is None or engine_instance.name is UNDEFINED:
|
||||
raise HomeAssistantError("TTS engine name is not set.")
|
||||
|
||||
if isinstance(engine_instance, Provider):
|
||||
if isinstance(engine_instance, Provider) or (
|
||||
not engine_instance.async_supports_streaming_input()
|
||||
):
|
||||
# Non-streaming
|
||||
if isinstance(message_or_stream, str):
|
||||
message = message_or_stream
|
||||
else:
|
||||
message = "".join([chunk async for chunk in message_or_stream])
|
||||
|
||||
extension, data = await engine_instance.async_internal_get_tts_audio(
|
||||
message, language, options
|
||||
)
|
||||
@@ -996,6 +1000,7 @@ class SpeechManager:
|
||||
data_gen = make_data_generator(data)
|
||||
|
||||
else:
|
||||
# Streaming
|
||||
if isinstance(message_or_stream, str):
|
||||
|
||||
async def gen_stream() -> AsyncGenerator[str]:
|
||||
|
@@ -191,6 +191,18 @@ class TextToSpeechEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH
|
||||
"""Load tts audio file from the engine."""
|
||||
raise NotImplementedError
|
||||
|
||||
@final
|
||||
async def async_internal_get_tts_audio(
|
||||
self, message: str, language: str, options: dict[str, Any]
|
||||
) -> TtsAudioType:
|
||||
"""Load tts audio file from the engine and update state.
|
||||
|
||||
Return a tuple of file extension and data as bytes.
|
||||
"""
|
||||
self.__last_tts_loaded = dt_util.utcnow().isoformat()
|
||||
self.async_write_ha_state()
|
||||
return await self.async_get_tts_audio(message, language, options=options)
|
||||
|
||||
async def async_get_tts_audio(
|
||||
self, message: str, language: str, options: dict[str, Any]
|
||||
) -> TtsAudioType:
|
||||
|
@@ -665,8 +665,11 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
|
||||
},
|
||||
]
|
||||
|
||||
elif ATTR_BRIGHTNESS in kwargs and self._brightness:
|
||||
brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
elif self._brightness and (ATTR_BRIGHTNESS in kwargs or ATTR_WHITE in kwargs):
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
else:
|
||||
brightness = kwargs[ATTR_WHITE]
|
||||
|
||||
# If there is a min/max value, the brightness is actually limited.
|
||||
# Meaning it is actually not on a 0-255 scale.
|
||||
|
@@ -40,7 +40,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect", "unifi_discovery"],
|
||||
"requirements": ["uiprotect==7.20.0", "unifi-discovery==1.2.0"],
|
||||
"requirements": ["uiprotect==7.21.1", "unifi-discovery==1.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
@@ -69,7 +69,7 @@ class VolvoOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
||||
"""Create an entry for the flow."""
|
||||
self._config_data |= data
|
||||
self._config_data |= (self.init_data or {}) | data
|
||||
return await self.async_step_api_key()
|
||||
|
||||
async def async_step_reauth(self, _: Mapping[str, Any]) -> ConfigFlowResult:
|
||||
@@ -77,7 +77,7 @@ class VolvoOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, _: dict[str, Any] | None = None
|
||||
self, data: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Reconfigure the entry."""
|
||||
return await self.async_step_api_key()
|
||||
@@ -121,7 +121,7 @@ class VolvoOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
|
||||
if user_input is None:
|
||||
if self.source == SOURCE_REAUTH:
|
||||
user_input = self._config_data = dict(self._get_reauth_entry().data)
|
||||
user_input = self._config_data
|
||||
api = _create_volvo_cars_api(
|
||||
self.hass,
|
||||
self._config_data[CONF_TOKEN][CONF_ACCESS_TOKEN],
|
||||
|
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/webostv",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiowebostv"],
|
||||
"requirements": ["aiowebostv==0.7.4"],
|
||||
"requirements": ["aiowebostv==0.7.5"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:lge-com:service:webos-second-screen:1"
|
||||
|
@@ -86,6 +86,9 @@ def add_province_and_language_to_schema(
|
||||
SelectOptionDict(value=k, label=", ".join(v))
|
||||
for k, v in subdiv_aliases.items()
|
||||
]
|
||||
for option in province_options:
|
||||
if option["label"] == "":
|
||||
option["label"] = option["value"]
|
||||
else:
|
||||
province_options = provinces
|
||||
province_schema = {
|
||||
|
@@ -47,6 +47,9 @@
|
||||
"exceptions": {
|
||||
"invalid_config_entry": {
|
||||
"message": "Config entry not found or not loaded!"
|
||||
},
|
||||
"valve_inoperable_currently": {
|
||||
"message": "The Valve cannot be operated currently."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
@@ -21,6 +21,7 @@ from homeassistant.components.valve import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DEV_MODEL_WATER_METER_YS5007, DOMAIN
|
||||
@@ -130,6 +131,13 @@ class YoLinkValveEntity(YoLinkEntity, ValveEntity):
|
||||
|
||||
async def _async_invoke_device(self, state: str) -> None:
|
||||
"""Call setState api to change valve state."""
|
||||
if (
|
||||
self.coordinator.device.is_support_mode_switching()
|
||||
and self.coordinator.dev_net_type == ATTR_DEVICE_MODEL_A
|
||||
):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="valve_inoperable_currently"
|
||||
)
|
||||
if (
|
||||
self.coordinator.device.device_type
|
||||
== ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER
|
||||
@@ -155,10 +163,4 @@ class YoLinkValveEntity(YoLinkEntity, ValveEntity):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return true is device is available."""
|
||||
if (
|
||||
self.coordinator.device.is_support_mode_switching()
|
||||
and self.coordinator.dev_net_type is not None
|
||||
):
|
||||
# When the device operates in Class A mode, it cannot be controlled.
|
||||
return self.coordinator.dev_net_type != ATTR_DEVICE_MODEL_A
|
||||
return super().available
|
||||
|
@@ -88,11 +88,16 @@ ADDON_USER_INPUT_MAP = {
|
||||
CONF_ADDON_LR_S2_AUTHENTICATED_KEY: CONF_LR_S2_AUTHENTICATED_KEY,
|
||||
}
|
||||
|
||||
EXAMPLE_SERVER_URL = "ws://localhost:3000"
|
||||
ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool})
|
||||
MIN_MIGRATION_SDK_VERSION = AwesomeVersion("6.61")
|
||||
|
||||
NETWORK_TYPE_NEW = "new"
|
||||
NETWORK_TYPE_EXISTING = "existing"
|
||||
ZWAVE_JS_SERVER_INSTRUCTIONS = (
|
||||
"https://www.home-assistant.io/integrations/zwave_js/"
|
||||
"#advanced-installation-instructions"
|
||||
)
|
||||
ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS = (
|
||||
"https://www.home-assistant.io/integrations/zwave_js/"
|
||||
"#how-to-migrate-from-one-adapter-to-a-new-adapter-using-z-wave-js-ui"
|
||||
@@ -529,7 +534,12 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a manual configuration."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="manual", data_schema=get_manual_schema({})
|
||||
step_id="manual",
|
||||
data_schema=get_manual_schema({}),
|
||||
description_placeholders={
|
||||
"example_server_url": EXAMPLE_SERVER_URL,
|
||||
"server_instructions": ZWAVE_JS_SERVER_INSTRUCTIONS,
|
||||
},
|
||||
)
|
||||
|
||||
errors = {}
|
||||
@@ -558,7 +568,13 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self._async_create_entry_from_vars()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="manual", data_schema=get_manual_schema(user_input), errors=errors
|
||||
step_id="manual",
|
||||
data_schema=get_manual_schema(user_input),
|
||||
description_placeholders={
|
||||
"example_server_url": EXAMPLE_SERVER_URL,
|
||||
"server_instructions": ZWAVE_JS_SERVER_INSTRUCTIONS,
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_hassio(
|
||||
@@ -1016,6 +1032,10 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="manual_reconfigure",
|
||||
data_schema=get_manual_schema({CONF_URL: config_entry.data[CONF_URL]}),
|
||||
description_placeholders={
|
||||
"example_server_url": EXAMPLE_SERVER_URL,
|
||||
"server_instructions": ZWAVE_JS_SERVER_INSTRUCTIONS,
|
||||
},
|
||||
)
|
||||
|
||||
errors = {}
|
||||
@@ -1046,6 +1066,10 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="manual_reconfigure",
|
||||
data_schema=get_manual_schema(user_input),
|
||||
description_placeholders={
|
||||
"example_server_url": EXAMPLE_SERVER_URL,
|
||||
"server_instructions": ZWAVE_JS_SERVER_INSTRUCTIONS,
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
@@ -82,13 +82,21 @@
|
||||
"title": "Installing add-on"
|
||||
},
|
||||
"manual": {
|
||||
"description": "The Z-Wave integration requires a running Z-Wave Server. If you don't already have that set up, please read the [instructions]({server_instructions}) in our documentation.\n\nWhen you have a Z-Wave Server running, enter its URL below to allow the integration to connect.",
|
||||
"data": {
|
||||
"url": "[%key:common::config_flow::data::url%]"
|
||||
},
|
||||
"data_description": {
|
||||
"url": "The URL of the Z-Wave Server WebSocket API, e.g. {example_server_url}"
|
||||
}
|
||||
},
|
||||
"manual_reconfigure": {
|
||||
"description": "[%key:component::zwave_js::config::step::manual::description%]",
|
||||
"data": {
|
||||
"url": "[%key:common::config_flow::data::url%]"
|
||||
},
|
||||
"data_description": {
|
||||
"url": "[%key:component::zwave_js::config::step::manual::data_description::url%]"
|
||||
}
|
||||
},
|
||||
"on_supervisor": {
|
||||
|
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2025
|
||||
MINOR_VERSION: Final = 8
|
||||
PATCH_VERSION: Final = "1"
|
||||
PATCH_VERSION: Final = "2"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Automatically generated by gen_requirements_all.py, do not edit
|
||||
|
||||
aiodhcpwatcher==1.2.0
|
||||
aiodhcpwatcher==1.2.1
|
||||
aiodiscover==2.7.1
|
||||
aiodns==3.5.0
|
||||
aiohasupervisor==0.3.1
|
||||
@@ -20,7 +20,7 @@ audioop-lts==0.2.1
|
||||
av==13.1.0
|
||||
awesomeversion==25.5.0
|
||||
bcrypt==4.3.0
|
||||
bleak-retry-connector==4.0.0
|
||||
bleak-retry-connector==4.0.1
|
||||
bleak==1.0.1
|
||||
bluetooth-adapters==2.0.0
|
||||
bluetooth-auto-recovery==1.5.2
|
||||
@@ -68,7 +68,7 @@ standard-telnetlib==3.13.0
|
||||
typing-extensions>=4.14.0,<5.0
|
||||
ulid-transform==1.4.0
|
||||
urllib3>=2.0
|
||||
uv==0.7.1
|
||||
uv==0.8.9
|
||||
voluptuous-openapi==0.1.0
|
||||
voluptuous-serialize==2.6.0
|
||||
voluptuous==0.15.2
|
||||
@@ -216,3 +216,8 @@ rpds-py==0.24.0
|
||||
|
||||
# Constraint num2words to 0.5.14 as 0.5.15 and 0.5.16 were removed from PyPI
|
||||
num2words==0.5.14
|
||||
|
||||
# pymodbus does not follow SemVer, and it keeps getting
|
||||
# downgraded or upgraded by custom components
|
||||
# This ensures all use the same version
|
||||
pymodbus==3.9.2
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2025.8.1"
|
||||
version = "2025.8.2"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
@@ -74,7 +74,7 @@ dependencies = [
|
||||
"typing-extensions>=4.14.0,<5.0",
|
||||
"ulid-transform==1.4.0",
|
||||
"urllib3>=2.0",
|
||||
"uv==0.7.1",
|
||||
"uv==0.8.9",
|
||||
"voluptuous==0.15.2",
|
||||
"voluptuous-serialize==2.6.0",
|
||||
"voluptuous-openapi==0.1.0",
|
||||
|
2
requirements.txt
generated
2
requirements.txt
generated
@@ -46,7 +46,7 @@ standard-telnetlib==3.13.0
|
||||
typing-extensions>=4.14.0,<5.0
|
||||
ulid-transform==1.4.0
|
||||
urllib3>=2.0
|
||||
uv==0.7.1
|
||||
uv==0.8.9
|
||||
voluptuous==0.15.2
|
||||
voluptuous-serialize==2.6.0
|
||||
voluptuous-openapi==0.1.0
|
||||
|
20
requirements_all.txt
generated
20
requirements_all.txt
generated
@@ -220,7 +220,7 @@ aiobotocore==2.21.1
|
||||
aiocomelit==0.12.3
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodhcpwatcher==1.2.0
|
||||
aiodhcpwatcher==1.2.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==2.7.1
|
||||
@@ -438,7 +438,7 @@ aiowatttime==0.1.1
|
||||
aiowebdav2==0.4.6
|
||||
|
||||
# homeassistant.components.webostv
|
||||
aiowebostv==0.7.4
|
||||
aiowebostv==0.7.5
|
||||
|
||||
# homeassistant.components.withings
|
||||
aiowithings==3.1.6
|
||||
@@ -453,7 +453,7 @@ airgradient==0.9.2
|
||||
airly==1.1.0
|
||||
|
||||
# homeassistant.components.airos
|
||||
airos==0.2.7
|
||||
airos==0.2.11
|
||||
|
||||
# homeassistant.components.airthings_ble
|
||||
airthings-ble==0.9.2
|
||||
@@ -625,7 +625,7 @@ bizkaibus==0.1.1
|
||||
bleak-esphome==3.1.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak-retry-connector==4.0.0
|
||||
bleak-retry-connector==4.0.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak==1.0.1
|
||||
@@ -743,7 +743,7 @@ connect-box==0.3.1
|
||||
construct==2.10.68
|
||||
|
||||
# homeassistant.components.cookidoo
|
||||
cookidoo-api==0.12.2
|
||||
cookidoo-api==0.14.0
|
||||
|
||||
# homeassistant.components.backup
|
||||
# homeassistant.components.utility_meter
|
||||
@@ -1127,7 +1127,7 @@ ha-philipsjs==3.2.2
|
||||
ha-silabs-firmware-client==0.2.0
|
||||
|
||||
# homeassistant.components.habitica
|
||||
habiticalib==0.4.1
|
||||
habiticalib==0.4.2
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==4.0.2
|
||||
@@ -1594,7 +1594,7 @@ ondilo==0.5.0
|
||||
onedrive-personal-sdk==0.0.14
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==4.0.2
|
||||
onvif-zeep-async==4.0.3
|
||||
|
||||
# homeassistant.components.opengarage
|
||||
open-garage==0.2.0
|
||||
@@ -2146,7 +2146,7 @@ pymeteoclimatic==0.1.0
|
||||
pymicro-vad==1.0.1
|
||||
|
||||
# homeassistant.components.miele
|
||||
pymiele==0.5.2
|
||||
pymiele==0.5.4
|
||||
|
||||
# homeassistant.components.xiaomi_tv
|
||||
pymitv==1.4.3
|
||||
@@ -2512,7 +2512,7 @@ python-roborock==2.18.2
|
||||
python-smarttub==0.0.44
|
||||
|
||||
# homeassistant.components.snoo
|
||||
python-snoo==0.6.6
|
||||
python-snoo==0.8.3
|
||||
|
||||
# homeassistant.components.songpal
|
||||
python-songpal==0.16.2
|
||||
@@ -3004,7 +3004,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==7.20.0
|
||||
uiprotect==7.21.1
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
|
20
requirements_test_all.txt
generated
20
requirements_test_all.txt
generated
@@ -208,7 +208,7 @@ aiobotocore==2.21.1
|
||||
aiocomelit==0.12.3
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodhcpwatcher==1.2.0
|
||||
aiodhcpwatcher==1.2.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==2.7.1
|
||||
@@ -420,7 +420,7 @@ aiowatttime==0.1.1
|
||||
aiowebdav2==0.4.6
|
||||
|
||||
# homeassistant.components.webostv
|
||||
aiowebostv==0.7.4
|
||||
aiowebostv==0.7.5
|
||||
|
||||
# homeassistant.components.withings
|
||||
aiowithings==3.1.6
|
||||
@@ -435,7 +435,7 @@ airgradient==0.9.2
|
||||
airly==1.1.0
|
||||
|
||||
# homeassistant.components.airos
|
||||
airos==0.2.7
|
||||
airos==0.2.11
|
||||
|
||||
# homeassistant.components.airthings_ble
|
||||
airthings-ble==0.9.2
|
||||
@@ -559,7 +559,7 @@ bimmer-connected[china]==0.17.2
|
||||
bleak-esphome==3.1.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak-retry-connector==4.0.0
|
||||
bleak-retry-connector==4.0.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak==1.0.1
|
||||
@@ -646,7 +646,7 @@ colorthief==0.2.1
|
||||
construct==2.10.68
|
||||
|
||||
# homeassistant.components.cookidoo
|
||||
cookidoo-api==0.12.2
|
||||
cookidoo-api==0.14.0
|
||||
|
||||
# homeassistant.components.backup
|
||||
# homeassistant.components.utility_meter
|
||||
@@ -988,7 +988,7 @@ ha-philipsjs==3.2.2
|
||||
ha-silabs-firmware-client==0.2.0
|
||||
|
||||
# homeassistant.components.habitica
|
||||
habiticalib==0.4.1
|
||||
habiticalib==0.4.2
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==4.0.2
|
||||
@@ -1362,7 +1362,7 @@ ondilo==0.5.0
|
||||
onedrive-personal-sdk==0.0.14
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==4.0.2
|
||||
onvif-zeep-async==4.0.3
|
||||
|
||||
# homeassistant.components.opengarage
|
||||
open-garage==0.2.0
|
||||
@@ -1788,7 +1788,7 @@ pymeteoclimatic==0.1.0
|
||||
pymicro-vad==1.0.1
|
||||
|
||||
# homeassistant.components.miele
|
||||
pymiele==0.5.2
|
||||
pymiele==0.5.4
|
||||
|
||||
# homeassistant.components.mochad
|
||||
pymochad==0.2.0
|
||||
@@ -2082,7 +2082,7 @@ python-roborock==2.18.2
|
||||
python-smarttub==0.0.44
|
||||
|
||||
# homeassistant.components.snoo
|
||||
python-snoo==0.6.6
|
||||
python-snoo==0.8.3
|
||||
|
||||
# homeassistant.components.songpal
|
||||
python-songpal==0.16.2
|
||||
@@ -2478,7 +2478,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==7.20.0
|
||||
uiprotect==7.21.1
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
|
@@ -242,6 +242,11 @@ rpds-py==0.24.0
|
||||
|
||||
# Constraint num2words to 0.5.14 as 0.5.15 and 0.5.16 were removed from PyPI
|
||||
num2words==0.5.14
|
||||
|
||||
# pymodbus does not follow SemVer, and it keeps getting
|
||||
# downgraded or upgraded by custom components
|
||||
# This ensures all use the same version
|
||||
pymodbus==3.9.2
|
||||
"""
|
||||
|
||||
GENERATED_MESSAGE = (
|
||||
|
2
script/hassfest/docker/Dockerfile
generated
2
script/hassfest/docker/Dockerfile
generated
@@ -14,7 +14,7 @@ WORKDIR "/github/workspace"
|
||||
COPY . /usr/src/homeassistant
|
||||
|
||||
# Uv is only needed during build
|
||||
RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \
|
||||
RUN --mount=from=ghcr.io/astral-sh/uv:0.8.9,source=/uv,target=/bin/uv \
|
||||
# Uv creates a lock file in /tmp
|
||||
--mount=type=tmpfs,target=/tmp \
|
||||
# Required for PyTurboJPEG
|
||||
|
@@ -28,9 +28,14 @@
|
||||
}),
|
||||
'genuine': '/images/genuine.png',
|
||||
'gps': dict({
|
||||
'alt': None,
|
||||
'dim': None,
|
||||
'dop': None,
|
||||
'fix': 0,
|
||||
'lat': '**REDACTED**',
|
||||
'lon': '**REDACTED**',
|
||||
'sats': None,
|
||||
'time_synced': None,
|
||||
}),
|
||||
'host': dict({
|
||||
'cpuload': 10.10101,
|
||||
|
@@ -208,6 +208,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -266,6 +267,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -304,6 +306,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -362,6 +365,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -400,6 +404,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -458,6 +463,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -496,6 +502,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -554,6 +561,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -592,6 +600,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -650,6 +659,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -688,6 +698,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -746,6 +757,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -784,6 +796,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -842,6 +855,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -880,6 +894,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -938,6 +953,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -976,6 +992,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -1034,6 +1051,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -1457,6 +1475,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -1515,6 +1534,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -1553,6 +1573,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -1611,6 +1632,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -1649,6 +1671,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -1707,6 +1730,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -1745,6 +1769,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -1803,6 +1828,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -1841,6 +1867,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
@@ -1899,6 +1926,7 @@
|
||||
'plate_step_8',
|
||||
'plate_step_9',
|
||||
'plate_step_boost',
|
||||
'plate_step_boost_2',
|
||||
'plate_step_warming',
|
||||
]),
|
||||
}),
|
||||
|
@@ -125,7 +125,7 @@ async def test_pairing(hass: HomeAssistant, mock_tv_pairable, mock_setup_entry)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
mock_tv.setTransport.assert_called_with(True)
|
||||
mock_tv.setTransport.assert_called_with(True, ANY)
|
||||
mock_tv.pairRequest.assert_called()
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
@@ -204,7 +204,7 @@ async def test_pair_grant_failed(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
mock_tv.setTransport.assert_called_with(True)
|
||||
mock_tv.setTransport.assert_called_with(True, ANY)
|
||||
mock_tv.pairRequest.assert_called()
|
||||
|
||||
# Test with invalid pin
|
||||
@@ -266,6 +266,7 @@ async def test_zeroconf_discovery(
|
||||
"""Test we can setup from zeroconf discovery."""
|
||||
|
||||
mock_tv_pairable.secured_transport = secured_transport
|
||||
mock_tv_pairable.api_version_detected = 6
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
@@ -291,7 +292,7 @@ async def test_zeroconf_discovery(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
mock_tv_pairable.setTransport.assert_called_with(secured_transport)
|
||||
mock_tv_pairable.setTransport.assert_called_with(secured_transport, 6)
|
||||
mock_tv_pairable.pairRequest.assert_called()
|
||||
|
||||
|
||||
|
@@ -221,12 +221,16 @@ def _create_mocked_hole(
|
||||
if wrong_host:
|
||||
raise HoleConnectionError("Cannot authenticate with Pi-hole: err")
|
||||
password = getattr(mocked_hole, "password", None)
|
||||
|
||||
if (
|
||||
raise_exception
|
||||
or incorrect_app_password
|
||||
or api_version == 5
|
||||
or (api_version == 6 and password not in ["newkey", "apikey"])
|
||||
):
|
||||
if api_version == 6:
|
||||
if api_version == 6 and (
|
||||
incorrect_app_password or password not in ["newkey", "apikey"]
|
||||
):
|
||||
raise HoleError("Authentication failed: Invalid password")
|
||||
raise HoleConnectionError
|
||||
|
||||
|
@@ -1,13 +1,17 @@
|
||||
"""Test REST data module logging improvements."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.rest import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
@@ -89,6 +93,59 @@ async def test_rest_data_no_warning_on_200_with_wrong_content_type(
|
||||
)
|
||||
|
||||
|
||||
async def test_rest_data_with_incorrect_charset_in_header(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test that we can handle sites which provides an incorrect charset."""
|
||||
aioclient_mock.get(
|
||||
"http://example.com/api",
|
||||
status=200,
|
||||
text="<p>Some html</p>",
|
||||
headers={"Content-Type": "text/html; charset=utf-8"},
|
||||
)
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{
|
||||
DOMAIN: {
|
||||
"resource": "http://example.com/api",
|
||||
"method": "GET",
|
||||
"encoding": "windows-1250",
|
||||
"sensor": [
|
||||
{
|
||||
"name": "test_sensor",
|
||||
"value_template": "{{ value }}",
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"tests.test_util.aiohttp.AiohttpClientMockResponse.text",
|
||||
side_effect=UnicodeDecodeError("utf-8", b"", 1, 0, ""),
|
||||
):
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
log_text = "Response charset came back as utf-8 but could not be decoded, continue with configured encoding windows-1250."
|
||||
assert log_text in caplog.text
|
||||
|
||||
caplog.clear()
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Only log once as we only try once with automatic decoding
|
||||
assert log_text not in caplog.text
|
||||
|
||||
|
||||
async def test_rest_data_no_warning_on_success_json(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
|
@@ -48,7 +48,7 @@ def find_update_callback(
|
||||
mock: AsyncMock, serial_number: str
|
||||
) -> Callable[[SnooData], Awaitable[None]]:
|
||||
"""Find the update callback for a specific identifier."""
|
||||
for call in mock.subscribe.call_args_list:
|
||||
for call in mock.start_subscribe.call_args_list:
|
||||
if call[0][0].serialNumber == serial_number:
|
||||
return call[0][1]
|
||||
pytest.fail(f"Callback for identifier {serial_number} not found")
|
||||
|
@@ -31,7 +31,12 @@ MOCK_SNOO_DEVICES = [
|
||||
"name": "Test Snoo",
|
||||
"presence": {},
|
||||
"presenceIoT": {},
|
||||
"awsIoT": {},
|
||||
"awsIoT": {
|
||||
"awsRegion": "us-east-1",
|
||||
"clientEndpoint": "z00023244d7fia4appr4b-ats.iot.us-east-1.amazonaws.com",
|
||||
"clientReady": True,
|
||||
"thingName": "676cbbe74529f85038b2e623_5831231335004715141_prod",
|
||||
},
|
||||
"lastSSID": {},
|
||||
"provisionedAt": "random_time",
|
||||
}
|
||||
|
@@ -973,3 +973,35 @@ async def test_optimistic(hass: HomeAssistant) -> None:
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == AlarmControlPanelState.ARMED_HOME
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "panel_config"),
|
||||
[
|
||||
(
|
||||
1,
|
||||
{
|
||||
"name": TEST_OBJECT_ID,
|
||||
"state": "{{ states('alarm_control_panel.test') }}",
|
||||
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
|
||||
"optimistic": False,
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style",
|
||||
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_panel")
|
||||
async def test_not_optimistic(hass: HomeAssistant) -> None:
|
||||
"""Test optimistic yaml option set to false."""
|
||||
await hass.services.async_call(
|
||||
ALARM_DOMAIN,
|
||||
"alarm_arm_away",
|
||||
{"entity_id": TEST_ENTITY_ID, "code": "1234"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
@@ -628,11 +628,38 @@ async def test_template_position(
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_cover")
|
||||
async def test_template_not_optimistic(hass: HomeAssistant) -> None:
|
||||
async def test_template_not_optimistic(
|
||||
hass: HomeAssistant,
|
||||
calls: list[ServiceCall],
|
||||
) -> None:
|
||||
"""Test the is_closed attribute."""
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
# Test to make sure optimistic is not set with only a position template.
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_OPEN_COVER,
|
||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
# Test to make sure optimistic is not set with only a position template.
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_CLOSE_COVER,
|
||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1 == 1 }}")])
|
||||
@pytest.mark.parametrize(
|
||||
|
@@ -1885,6 +1885,39 @@ async def test_optimistic_option(hass: HomeAssistant) -> None:
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "fan_config"),
|
||||
[
|
||||
(
|
||||
1,
|
||||
{
|
||||
"name": TEST_OBJECT_ID,
|
||||
"state": "{{ is_state('sensor.test_sensor', 'on') }}",
|
||||
"turn_on": [],
|
||||
"turn_off": [],
|
||||
"optimistic": False,
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style",
|
||||
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_fan")
|
||||
async def test_not_optimistic(hass: HomeAssistant) -> None:
|
||||
"""Test optimistic yaml option set to false."""
|
||||
await hass.services.async_call(
|
||||
fan.DOMAIN,
|
||||
"turn_on",
|
||||
{"entity_id": TEST_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
async def test_setup_config_entry(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
|
@@ -2795,6 +2795,42 @@ async def test_optimistic_option(hass: HomeAssistant) -> None:
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "light_config"),
|
||||
[
|
||||
(
|
||||
1,
|
||||
{
|
||||
"name": TEST_OBJECT_ID,
|
||||
"state": "{{ is_state('light.test_state', 'on') }}",
|
||||
"turn_on": [],
|
||||
"turn_off": [],
|
||||
"optimistic": False,
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("style", "expected"),
|
||||
[
|
||||
(ConfigurationStyle.MODERN, STATE_OFF),
|
||||
(ConfigurationStyle.TRIGGER, STATE_UNKNOWN),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_light")
|
||||
async def test_not_optimistic(hass: HomeAssistant, expected: str) -> None:
|
||||
"""Test optimistic yaml option set to false."""
|
||||
await hass.services.async_call(
|
||||
light.DOMAIN,
|
||||
"turn_on",
|
||||
{"entity_id": TEST_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == expected
|
||||
|
||||
|
||||
async def test_setup_config_entry(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
|
@@ -1190,6 +1190,39 @@ async def test_optimistic(hass: HomeAssistant) -> None:
|
||||
assert state.state == LockState.UNLOCKED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "lock_config"),
|
||||
[
|
||||
(
|
||||
1,
|
||||
{
|
||||
"name": TEST_OBJECT_ID,
|
||||
"state": "{{ is_state('sensor.test_state', 'on') }}",
|
||||
"lock": [],
|
||||
"unlock": [],
|
||||
"optimistic": False,
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style",
|
||||
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_lock")
|
||||
async def test_not_optimistic(hass: HomeAssistant) -> None:
|
||||
"""Test optimistic yaml option set to false."""
|
||||
await hass.services.async_call(
|
||||
lock.DOMAIN,
|
||||
lock.SERVICE_LOCK,
|
||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == LockState.UNLOCKED
|
||||
|
||||
|
||||
async def test_setup_config_entry(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
|
@@ -605,6 +605,37 @@ async def test_optimistic(hass: HomeAssistant) -> None:
|
||||
assert float(state.state) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "number_config"),
|
||||
[
|
||||
(
|
||||
1,
|
||||
{
|
||||
"state": "{{ states('sensor.test_state') }}",
|
||||
"optimistic": False,
|
||||
"set_value": [],
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style",
|
||||
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_number")
|
||||
async def test_not_optimistic(hass: HomeAssistant) -> None:
|
||||
"""Test optimistic yaml option set to false."""
|
||||
await hass.services.async_call(
|
||||
number.DOMAIN,
|
||||
number.SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: _TEST_NUMBER, "value": 4},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(_TEST_NUMBER)
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "number_config"),
|
||||
[
|
||||
|
@@ -601,6 +601,42 @@ async def test_optimistic(hass: HomeAssistant) -> None:
|
||||
assert state.state == "yes"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "select_config"),
|
||||
[
|
||||
(
|
||||
1,
|
||||
{
|
||||
"state": "{{ states('select.test_state') }}",
|
||||
"optimistic": False,
|
||||
"options": "{{ ['test', 'yes', 'no'] }}",
|
||||
"select_option": [],
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style",
|
||||
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_select")
|
||||
async def test_not_optimistic(hass: HomeAssistant) -> None:
|
||||
"""Test optimistic yaml option set to false."""
|
||||
# Ensure Trigger template entities update the options list
|
||||
hass.states.async_set(TEST_STATE_ENTITY_ID, "anything")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
select.DOMAIN,
|
||||
select.SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: _TEST_SELECT, "option": "test"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(_TEST_SELECT)
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "select_config"),
|
||||
[
|
||||
|
@@ -14,6 +14,7 @@ from homeassistant.const import (
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import CoreState, HomeAssistant, ServiceCall, State
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
@@ -1267,3 +1268,39 @@ async def test_optimistic_option(hass: HomeAssistant) -> None:
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "switch_config"),
|
||||
[
|
||||
(
|
||||
1,
|
||||
{
|
||||
"name": TEST_OBJECT_ID,
|
||||
"state": "{{ is_state('switch.test_state', 'on') }}",
|
||||
"turn_on": [],
|
||||
"turn_off": [],
|
||||
"optimistic": False,
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("style", "expected"),
|
||||
[
|
||||
(ConfigurationStyle.MODERN, STATE_OFF),
|
||||
(ConfigurationStyle.TRIGGER, STATE_UNKNOWN),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_switch")
|
||||
async def test_not_optimistic(hass: HomeAssistant, expected: str) -> None:
|
||||
"""Test optimistic yaml option set to false."""
|
||||
await hass.services.async_call(
|
||||
switch.DOMAIN,
|
||||
"turn_on",
|
||||
{"entity_id": TEST_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == expected
|
||||
|
@@ -1299,6 +1299,54 @@ async def test_optimistic_option(
|
||||
assert state.state == VacuumActivity.DOCKED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "vacuum_config"),
|
||||
[
|
||||
(
|
||||
1,
|
||||
{
|
||||
"name": TEST_OBJECT_ID,
|
||||
"state": "{{ states('sensor.test_state') }}",
|
||||
"start": [],
|
||||
**TEMPLATE_VACUUM_ACTIONS,
|
||||
"optimistic": False,
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style",
|
||||
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"service",
|
||||
[
|
||||
vacuum.SERVICE_START,
|
||||
vacuum.SERVICE_PAUSE,
|
||||
vacuum.SERVICE_STOP,
|
||||
vacuum.SERVICE_RETURN_TO_BASE,
|
||||
vacuum.SERVICE_CLEAN_SPOT,
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_vacuum")
|
||||
async def test_not_optimistic(
|
||||
hass: HomeAssistant,
|
||||
service: str,
|
||||
calls: list[ServiceCall],
|
||||
) -> None:
|
||||
"""Test optimistic yaml option set to false."""
|
||||
await hass.services.async_call(
|
||||
vacuum.DOMAIN,
|
||||
service,
|
||||
{"entity_id": TEST_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_setup_config_entry(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
|
@@ -175,3 +175,31 @@ def test_streaming_supported() -> None:
|
||||
|
||||
sync_non_streaming_entity = SyncNonStreamingEntity()
|
||||
assert sync_non_streaming_entity.async_supports_streaming_input() is False
|
||||
|
||||
|
||||
async def test_internal_get_tts_audio_writes_state(
|
||||
hass: HomeAssistant,
|
||||
mock_tts_entity: MockTTSEntity,
|
||||
) -> None:
|
||||
"""Test that only async_internal_get_tts_audio updates and writes the state."""
|
||||
|
||||
entity_id = f"{tts.DOMAIN}.{TEST_DOMAIN}"
|
||||
|
||||
config_entry = await mock_config_entry_setup(hass, mock_tts_entity)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
state1 = hass.states.get(entity_id)
|
||||
assert state1 is not None
|
||||
|
||||
# State should *not* change with external method
|
||||
await mock_tts_entity.async_get_tts_audio("test message", hass.config.language, {})
|
||||
state2 = hass.states.get(entity_id)
|
||||
assert state2 is not None
|
||||
assert state1.state == state2.state
|
||||
|
||||
# State *should* change with internal method
|
||||
await mock_tts_entity.async_internal_get_tts_audio(
|
||||
"test message", hass.config.language, {}
|
||||
)
|
||||
state3 = hass.states.get(entity_id)
|
||||
assert state3 is not None
|
||||
assert state1.state != state3.state
|
||||
|
@@ -2032,3 +2032,34 @@ async def test_tts_cache() -> None:
|
||||
assert await consume_mid_data_task == b"012"
|
||||
with pytest.raises(ValueError):
|
||||
assert await consume_pre_data_loaded_task == b"012"
|
||||
|
||||
|
||||
async def test_async_internal_get_tts_audio_called(
|
||||
hass: HomeAssistant,
|
||||
mock_tts_entity: MockTTSEntity,
|
||||
hass_client: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test that non-streaming entity has its async_internal_get_tts_audio method called."""
|
||||
|
||||
await mock_config_entry_setup(hass, mock_tts_entity)
|
||||
|
||||
# Non-streaming
|
||||
assert mock_tts_entity.async_supports_streaming_input() is False
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tts.entity.TextToSpeechEntity.async_internal_get_tts_audio"
|
||||
) as internal_get_tts_audio:
|
||||
media_source_id = tts.generate_media_source_id(
|
||||
hass,
|
||||
"test message",
|
||||
"tts.test",
|
||||
"en_US",
|
||||
cache=None,
|
||||
)
|
||||
|
||||
url = await get_media_source_url(hass, media_source_id)
|
||||
client = await hass_client()
|
||||
await client.get(url)
|
||||
|
||||
# async_internal_get_tts_audio is called
|
||||
internal_get_tts_audio.assert_called_once_with("test message", "en_US", {})
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@@ -66,11 +67,58 @@ async def test_platform_setup_no_discovery(
|
||||
"mock_device_code",
|
||||
["dj_mki13ie507rlry4r"],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("turn_on_input", "expected_commands"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"white": True,
|
||||
},
|
||||
[
|
||||
{"code": "switch_led", "value": True},
|
||||
{"code": "work_mode", "value": "white"},
|
||||
{"code": "bright_value_v2", "value": 546},
|
||||
],
|
||||
),
|
||||
(
|
||||
{
|
||||
"brightness": 150,
|
||||
},
|
||||
[
|
||||
{"code": "switch_led", "value": True},
|
||||
{"code": "bright_value_v2", "value": 592},
|
||||
],
|
||||
),
|
||||
(
|
||||
{
|
||||
"white": True,
|
||||
"brightness": 150,
|
||||
},
|
||||
[
|
||||
{"code": "switch_led", "value": True},
|
||||
{"code": "work_mode", "value": "white"},
|
||||
{"code": "bright_value_v2", "value": 592},
|
||||
],
|
||||
),
|
||||
(
|
||||
{
|
||||
"white": 150,
|
||||
},
|
||||
[
|
||||
{"code": "switch_led", "value": True},
|
||||
{"code": "work_mode", "value": "white"},
|
||||
{"code": "bright_value_v2", "value": 592},
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_turn_on_white(
|
||||
hass: HomeAssistant,
|
||||
mock_manager: ManagerCompat,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_device: CustomerDevice,
|
||||
turn_on_input: dict[str, Any],
|
||||
expected_commands: list[dict[str, Any]],
|
||||
) -> None:
|
||||
"""Test turn_on service."""
|
||||
entity_id = "light.garage_light"
|
||||
@@ -83,16 +131,13 @@ async def test_turn_on_white(
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
"entity_id": entity_id,
|
||||
"white": 150,
|
||||
**turn_on_input,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_manager.send_commands.assert_called_once_with(
|
||||
mock_device.id,
|
||||
[
|
||||
{"code": "switch_led", "value": True},
|
||||
{"code": "work_mode", "value": "white"},
|
||||
],
|
||||
expected_commands,
|
||||
)
|
||||
|
||||
|
||||
|
@@ -13,7 +13,7 @@ from yarl import URL
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.volvo.const import CONF_VIN, DOMAIN
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_API_KEY, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
@@ -117,6 +117,53 @@ async def test_reauth_flow(
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
async def test_reauth_no_stale_data(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
mock_config_flow_api: VolvoCarsApi,
|
||||
) -> None:
|
||||
"""Test if reauthentication flow does not use stale data."""
|
||||
old_access_token = mock_config_entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.volvo.config_flow._create_volvo_cars_api",
|
||||
return_value=mock_config_flow_api,
|
||||
) as mock_create_volvo_cars_api:
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
hass,
|
||||
{
|
||||
"flow_id": result["flow_id"],
|
||||
"redirect_uri": REDIRECT_URI,
|
||||
},
|
||||
)
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||
assert resp.status == 200
|
||||
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
||||
|
||||
result = await _async_run_flow_to_completion(
|
||||
hass,
|
||||
result,
|
||||
mock_config_flow_api,
|
||||
has_vin_step=False,
|
||||
is_reauth=True,
|
||||
)
|
||||
|
||||
assert mock_create_volvo_cars_api.called
|
||||
call = mock_create_volvo_cars_api.call_args_list[0]
|
||||
access_token_arg = call.args[1]
|
||||
assert old_access_token != access_token_arg
|
||||
|
||||
|
||||
async def test_reconfigure_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
|
@@ -1,19 +1,6 @@
|
||||
# serializer version: 1
|
||||
# name: test_get_tts_audio
|
||||
list([
|
||||
dict({
|
||||
'data': dict({
|
||||
}),
|
||||
'payload': None,
|
||||
'type': 'synthesize-start',
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'text': 'Hello world',
|
||||
}),
|
||||
'payload': None,
|
||||
'type': 'synthesize-chunk',
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'text': 'Hello world',
|
||||
@@ -21,29 +8,10 @@
|
||||
'payload': None,
|
||||
'type': 'synthesize',
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
}),
|
||||
'payload': None,
|
||||
'type': 'synthesize-stop',
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_get_tts_audio_different_formats
|
||||
list([
|
||||
dict({
|
||||
'data': dict({
|
||||
}),
|
||||
'payload': None,
|
||||
'type': 'synthesize-start',
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'text': 'Hello world',
|
||||
}),
|
||||
'payload': None,
|
||||
'type': 'synthesize-chunk',
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'text': 'Hello world',
|
||||
@@ -51,29 +19,10 @@
|
||||
'payload': None,
|
||||
'type': 'synthesize',
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
}),
|
||||
'payload': None,
|
||||
'type': 'synthesize-stop',
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_get_tts_audio_different_formats.1
|
||||
list([
|
||||
dict({
|
||||
'data': dict({
|
||||
}),
|
||||
'payload': None,
|
||||
'type': 'synthesize-start',
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'text': 'Hello world',
|
||||
}),
|
||||
'payload': None,
|
||||
'type': 'synthesize-chunk',
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'text': 'Hello world',
|
||||
@@ -81,12 +30,6 @@
|
||||
'payload': None,
|
||||
'type': 'synthesize',
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
}),
|
||||
'payload': None,
|
||||
'type': 'synthesize-stop',
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_get_tts_audio_streaming
|
||||
@@ -128,23 +71,6 @@
|
||||
# ---
|
||||
# name: test_voice_speaker
|
||||
list([
|
||||
dict({
|
||||
'data': dict({
|
||||
'voice': dict({
|
||||
'name': 'voice1',
|
||||
'speaker': 'speaker1',
|
||||
}),
|
||||
}),
|
||||
'payload': None,
|
||||
'type': 'synthesize-start',
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'text': 'Hello world',
|
||||
}),
|
||||
'payload': None,
|
||||
'type': 'synthesize-chunk',
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
'text': 'Hello world',
|
||||
@@ -156,11 +82,5 @@
|
||||
'payload': None,
|
||||
'type': 'synthesize',
|
||||
}),
|
||||
dict({
|
||||
'data': dict({
|
||||
}),
|
||||
'payload': None,
|
||||
'type': 'synthesize-stop',
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
|
Reference in New Issue
Block a user