This commit is contained in:
Franck Nijhof
2025-08-15 17:55:32 +02:00
committed by GitHub
61 changed files with 759 additions and 161 deletions

2
Dockerfile generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["habiticalib"],
"quality_scale": "platinum",
"requirements": ["habiticalib==0.4.1"]
"requirements": ["habiticalib==0.4.2"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
try:
self.data = await response.text()
else:
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -665,8 +665,11 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
},
]
elif ATTR_BRIGHTNESS in kwargs and self._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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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