Compare commits

..

16 Commits

Author SHA1 Message Date
Robert Resch
1f6e078d1d Extract dynamically package version at build time in hassfest image (#168347) 2026-04-16 14:40:13 +02:00
Marc Mueller
71d857b5e1 Update pydantic to 2.13.1 (#168311) 2026-04-16 14:34:30 +02:00
Barry vd. Heuvel
0de75a013b Add weheat standby electricity usage (#168363) 2026-04-16 14:33:36 +02:00
Robert Resch
f87ec0a7b8 Just copy explicit files in the Dockerfile (#168197) 2026-04-16 14:30:54 +02:00
Ariel Ebersberger
6d1bd15256 Fix synology_dsm test for Python 3.14.3 (#168359) 2026-04-16 13:23:09 +02:00
Jürgen
9fe9064884 Fix sonos availability (#161024)
Co-authored-by: Pete Sage <76050312+PeteRager@users.noreply.github.com>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2026-04-16 12:14:19 +01:00
Jamin
f9f57b00bb Fix VOIP blocking call in event loop (#168331) 2026-04-16 12:14:58 +02:00
johanzander
2b65b06003 Fix unit of measurement for SPH power sensors in growatt_server (#168251)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 12:14:13 +02:00
Leo Periou
206c498027 Bump pyaxencoapi to 1.0.7 (#168286) 2026-04-16 12:10:24 +02:00
renovate[bot]
0ac62b241e Update home-assistant-bluetooth to 2.0.0 (#168353)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 12:06:34 +02:00
renovate[bot]
4ba123a1a8 Update PyTurboJPEG to 2.2.0 (#168354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 12:02:56 +02:00
Maciej Bieniek
8b8b39c1b7 Bump imgw-pib to 2.1.0 (#168319) 2026-04-16 11:27:44 +02:00
renovate[bot]
5b70e5f829 Update lru-dict to 1.4.1 (#168336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 11:25:00 +02:00
Erik Montnemery
4f8e7125d4 Add state based condition tests (#168349) 2026-04-16 11:22:14 +02:00
renovate[bot]
baf5e32c59 Update xmltodict to 1.0.4 (#168330)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 10:49:35 +02:00
renovate[bot]
0f0ceaace2 Update PyJWT to 2.12.1 (#168239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Robert Resch <robert@resch.dev>
2026-04-16 10:44:41 +02:00
39 changed files with 487 additions and 213 deletions

5
Dockerfile generated
View File

@@ -28,8 +28,7 @@ COPY rootfs /
COPY --from=ghcr.io/alexxit/go2rtc@sha256:675c318b23c06fd862a61d262240c9a63436b4050d177ffc68a32710d9e05bae /usr/local/bin/go2rtc /bin/go2rtc
## Setup Home Assistant Core dependencies
COPY requirements.txt homeassistant/
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
COPY --parents requirements.txt homeassistant/package_constraints.txt homeassistant/
RUN \
# Verify go2rtc can be executed
go2rtc --version \
@@ -49,7 +48,7 @@ RUN \
-r homeassistant/requirements_all.txt
## Setup Home Assistant Core
COPY . homeassistant/
COPY --parents LICENSE* README* homeassistant/ pyproject.toml homeassistant/
RUN \
uv pip install \
-e ./homeassistant \

View File

@@ -7,23 +7,31 @@ to speed up the process.
from __future__ import annotations
from collections.abc import Container, Iterable, Sequence
from datetime import timedelta
from functools import lru_cache, partial
from typing import Any
from functools import lru_cache
from typing import Any, override
from jwt import DecodeError, PyJWS, PyJWT
from jwt import DecodeError, PyJWK, PyJWS, PyJWT
from jwt.algorithms import AllowedPublicKeys
from jwt.types import Options
from homeassistant.util.json import json_loads
JWT_TOKEN_CACHE_SIZE = 16
MAX_TOKEN_SIZE = 8192
_VERIFY_KEYS = ("signature", "exp", "nbf", "iat", "aud", "iss", "sub", "jti")
_VERIFY_OPTIONS: dict[str, Any] = {f"verify_{key}": True for key in _VERIFY_KEYS} | {
"require": []
}
_NO_VERIFY_OPTIONS = {f"verify_{key}": False for key in _VERIFY_KEYS}
_NO_VERIFY_OPTIONS = Options(
verify_signature=False,
verify_exp=False,
verify_nbf=False,
verify_iat=False,
verify_aud=False,
verify_iss=False,
verify_sub=False,
verify_jti=False,
require=[],
)
class _PyJWSWithLoadCache(PyJWS):
@@ -38,9 +46,6 @@ class _PyJWSWithLoadCache(PyJWS):
return super()._load(jwt)
_jws = _PyJWSWithLoadCache()
@lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)
def _decode_payload(json_payload: str) -> dict[str, Any]:
"""Decode the payload from a JWS dictionary."""
@@ -56,21 +61,12 @@ def _decode_payload(json_payload: str) -> dict[str, Any]:
class _PyJWTWithVerify(PyJWT):
"""PyJWT with a fast decode implementation."""
def decode_payload(
self, jwt: str, key: str, options: dict[str, Any], algorithms: list[str]
) -> dict[str, Any]:
"""Decode a JWT's payload."""
if len(jwt) > MAX_TOKEN_SIZE:
# Avoid caching impossible tokens
raise DecodeError("Token too large")
return _decode_payload(
_jws.decode_complete(
jwt=jwt,
key=key,
algorithms=algorithms,
options=options,
)["payload"]
)
def __init__(self) -> None:
"""Initialize the PyJWT instance."""
# We require exp and iat claims to be present
super().__init__(Options(require=["exp", "iat"]))
# Override the _jws instance with our cached version
self._jws = _PyJWSWithLoadCache()
def verify_and_decode(
self,
@@ -79,37 +75,70 @@ class _PyJWTWithVerify(PyJWT):
algorithms: list[str],
issuer: str | None = None,
leeway: float | timedelta = 0,
options: dict[str, Any] | None = None,
options: Options | None = None,
) -> dict[str, Any]:
"""Verify a JWT's signature and claims."""
merged_options = {**_VERIFY_OPTIONS, **(options or {})}
payload = self.decode_payload(
return self.decode(
jwt=jwt,
key=key,
options=merged_options,
algorithms=algorithms,
)
# These should never be missing since we verify them
# but this is an additional safeguard to make sure
# nothing slips through.
assert "exp" in payload, "exp claim is required"
assert "iat" in payload, "iat claim is required"
self._validate_claims(
payload=payload,
options=merged_options,
issuer=issuer,
leeway=leeway,
options=options,
)
return payload
@override
def decode(
self,
jwt: str | bytes,
key: AllowedPublicKeys | PyJWK | str | bytes = "",
algorithms: Sequence[str] | None = None,
options: Options | None = None,
verify: bool | None = None,
detached_payload: bytes | None = None,
audience: str | Iterable[str] | None = None,
subject: str | None = None,
issuer: str | Container[str] | None = None,
leeway: float | timedelta = 0,
**kwargs: Any,
) -> dict[str, Any]:
"""Decode a JWT, verifying the signature and claims."""
if len(jwt) > MAX_TOKEN_SIZE:
# Avoid caching impossible tokens
raise DecodeError("Token too large")
return super().decode(
jwt=jwt,
key=key,
algorithms=algorithms,
options=options,
verify=verify,
detached_payload=detached_payload,
audience=audience,
subject=subject,
issuer=issuer,
leeway=leeway,
**kwargs,
)
@override
def _decode_payload(self, decoded: dict[str, Any]) -> dict[str, Any]:
return _decode_payload(decoded["payload"])
_jwt = _PyJWTWithVerify()
verify_and_decode = _jwt.verify_and_decode
unverified_hs256_token_decode = lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)(
partial(
_jwt.decode_payload, key="", algorithms=["HS256"], options=_NO_VERIFY_OPTIONS
@lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)
def unverified_hs256_token_decode(jwt: str) -> dict[str, Any]:
"""Decode a JWT without verifying the signature."""
return _jwt.decode(
jwt=jwt,
key="",
algorithms=["HS256"],
options=_NO_VERIFY_OPTIONS,
)
)
__all__ = [
"unverified_hs256_token_decode",

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/camera",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["PyTurboJPEG==1.8.3"]
"requirements": ["PyTurboJPEG==2.2.0"]
}

View File

@@ -9,7 +9,7 @@
"iot_class": "local_polling",
"loggers": ["fritzconnection"],
"quality_scale": "gold",
"requirements": ["fritzconnection[qr]==1.15.1", "xmltodict==1.0.2"],
"requirements": ["fritzconnection[qr]==1.15.1", "xmltodict==1.0.4"],
"ssdp": [
{
"st": "urn:schemas-upnp-org:device:fritzbox:1"

View File

@@ -72,7 +72,7 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
key="mix_export_to_grid",
translation_key="mix_export_to_grid",
api_key="pacToGridTotal",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -80,7 +80,7 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
key="mix_import_from_grid",
translation_key="mix_import_from_grid",
api_key="pacToUserR",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["imgw_pib==2.0.2"]
"requirements": ["imgw_pib==2.1.0"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"quality_scale": "bronze",
"requirements": ["pyaxencoapi==1.0.6"]
"requirements": ["pyaxencoapi==1.0.7"]
}

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/rest",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["jsonpath==0.82.2", "xmltodict==1.0.2"]
"requirements": ["jsonpath==0.82.2", "xmltodict==1.0.4"]
}

View File

@@ -6,7 +6,7 @@ from collections.abc import Iterator
import logging
from typing import TYPE_CHECKING, Any
from soco import SoCo
from soco import SoCo, SoCoException
from soco.alarms import Alarm, Alarms
from soco.events_base import Event as SonosEvent
@@ -30,6 +30,7 @@ class SonosAlarms(SonosHouseholdCoordinator):
super().__init__(*args)
self.alarms: Alarms = Alarms()
self.created_alarm_ids: set[str] = set()
self._household_mismatch_logged = False
def __iter__(self) -> Iterator:
"""Return an iterator for the known alarms."""
@@ -76,21 +77,40 @@ class SonosAlarms(SonosHouseholdCoordinator):
await self.async_update_entities(speaker.soco, event_id)
@soco_error()
def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
"""Update cache of known alarms and return if cache has changed."""
self.alarms.update(soco)
def update_cache(
self,
soco: SoCo,
update_id: int | None = None,
) -> bool:
"""Update cache of known alarms and return whether any were seen."""
try:
self.alarms.update(soco)
except SoCoException as err:
err_msg = str(err)
# Only catch the specific household mismatch error
if "Alarm list UID" in err_msg and "does not match" in err_msg:
if not self._household_mismatch_logged:
_LOGGER.warning(
"Sonos alarms for %s cannot be updated due to a household mismatch. "
"This is a known limitation in setups with multiple households. "
"You can safely ignore this warning, or to silence it, remove the "
"affected household from your Sonos system. Error: %s",
soco.player_name,
err_msg,
)
self._household_mismatch_logged = True
return False
# Let all other exceptions bubble up to be handled by @soco_error()
raise
if update_id and self.alarms.last_id < update_id:
# Skip updates if latest query result is outdated or lagging
return False
if (
self.last_processed_event_id
and self.alarms.last_id <= self.last_processed_event_id
):
# Skip updates already processed
return False
_LOGGER.debug(
"Updating processed event %s from %s (was %s)",
self.alarms.last_id,

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/startca",
"iot_class": "cloud_polling",
"quality_scale": "legacy",
"requirements": ["xmltodict==1.0.2"]
"requirements": ["xmltodict==1.0.4"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["PyTurboJPEG==1.8.3", "av==16.0.1", "numpy==2.3.2"]
"requirements": ["PyTurboJPEG==2.2.0", "av==16.0.1", "numpy==2.3.2"]
}

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/ted5000",
"iot_class": "local_polling",
"quality_scale": "legacy",
"requirements": ["xmltodict==1.0.2"]
"requirements": ["xmltodict==1.0.4"]
}

View File

@@ -150,11 +150,6 @@ class PreRecordMessageProtocol(RtpDatagramProtocol):
if self.transport is None:
return
if self._audio_bytes is None:
# 16Khz, 16-bit mono audio message
file_path = Path(__file__).parent / self.file_name
self._audio_bytes = file_path.read_bytes()
if self._audio_task is None:
self._audio_task = self.hass.async_create_background_task(
self._play_message(),
@@ -162,6 +157,11 @@ class PreRecordMessageProtocol(RtpDatagramProtocol):
)
async def _play_message(self) -> None:
if self._audio_bytes is None:
_LOGGER.debug("Loading audio from file %s", self.file_name)
self._audio_bytes = await self._load_audio()
_LOGGER.debug("Read %s bytes", len(self._audio_bytes))
await self.hass.async_add_executor_job(
partial(
self.send_audio,
@@ -175,3 +175,8 @@ class PreRecordMessageProtocol(RtpDatagramProtocol):
# Allow message to play again
self._audio_task = None
async def _load_audio(self) -> bytes:
# 16Khz, 16-bit mono audio message
file_path = Path(__file__).parent / self.file_name
return await self.hass.async_add_executor_job(file_path.read_bytes)

View File

@@ -245,6 +245,14 @@ ENERGY_SENSORS = [
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda status: status.energy_in_defrost,
),
WeHeatSensorEntityDescription(
translation_key="electricity_used_standby",
key="electricity_used_standby",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda status: status.energy_in_standby,
),
WeHeatSensorEntityDescription(
translation_key="energy_output_heating",
key="energy_output_heating",

View File

@@ -96,6 +96,9 @@
"electricity_used_heating": {
"name": "Electricity used heating"
},
"electricity_used_standby": {
"name": "Electricity used standby"
},
"energy_output": {
"name": "Total energy output"
},

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/zestimate",
"iot_class": "cloud_polling",
"quality_scale": "legacy",
"requirements": ["xmltodict==1.0.2"]
"requirements": ["xmltodict==1.0.4"]
}

View File

@@ -804,6 +804,6 @@ def _decode_jwt(hass: HomeAssistant, encoded: str) -> dict[str, Any] | None:
return None
try:
return jwt.decode(encoded, secret, algorithms=["HS256"]) # type: ignore[no-any-return]
return jwt.decode(encoded, secret, algorithms=["HS256"])
except jwt.InvalidTokenError:
return None

View File

@@ -38,13 +38,13 @@ ha-ffmpeg==3.2.2
habluetooth==6.0.0
hass-nabucasa==2.2.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1
home-assistant-bluetooth==2.0.0
home-assistant-frontend==20260325.7
home-assistant-intents==2026.3.24
httpx==0.28.1
ifaddr==0.2.0
Jinja2==3.1.6
lru-dict==1.3.0
lru-dict==1.4.1
mutagen==1.47.0
openai==2.21.0
orjson==3.11.8
@@ -53,13 +53,13 @@ paho-mqtt==2.1.0
Pillow==12.2.0
propcache==0.4.1
psutil-home-assistant==0.0.1
PyJWT==2.10.1
PyJWT==2.12.1
pymicro-vad==1.0.1
PyNaCl==1.6.2
pyOpenSSL==26.0.0
pyspeex-noise==1.0.2
python-slugify==8.0.4
PyTurboJPEG==1.8.3
PyTurboJPEG==2.2.0
PyYAML==6.0.3
requests==2.33.1
securetar==2026.4.1
@@ -134,7 +134,7 @@ backoff>=2.0
Brotli>=1.2.0
# ensure pydantic version does not float since it might have breaking changes
pydantic==2.13.0
pydantic==2.13.1
# Required for Python 3.14.0 compatibility (#119223).
mashumaro>=3.17.0

View File

@@ -51,11 +51,11 @@ dependencies = [
# When bumping httpx, please check the version pins of
# httpcore, anyio, and h11 in gen_requirements_all
"httpx==0.28.1",
"home-assistant-bluetooth==1.13.1",
"home-assistant-bluetooth==2.0.0",
"ifaddr==0.2.0",
"Jinja2==3.1.6",
"lru-dict==1.3.0",
"PyJWT==2.10.1",
"lru-dict==1.4.1",
"PyJWT==2.12.1",
# PyJWT has loose dependency. We want the latest one.
"cryptography==46.0.7",
"Pillow==12.2.0",

8
requirements.txt generated
View File

@@ -26,25 +26,25 @@ fnv-hash-fast==2.0.2
ha-ffmpeg==3.2.2
hass-nabucasa==2.2.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1
home-assistant-bluetooth==2.0.0
home-assistant-intents==2026.3.24
httpx==0.28.1
ifaddr==0.2.0
infrared-protocols==1.2.0
Jinja2==3.1.6
lru-dict==1.3.0
lru-dict==1.4.1
mutagen==1.47.0
orjson==3.11.8
packaging>=23.1
Pillow==12.2.0
propcache==0.4.1
psutil-home-assistant==0.0.1
PyJWT==2.10.1
PyJWT==2.12.1
pymicro-vad==1.0.1
pyOpenSSL==26.0.0
pyspeex-noise==1.0.2
python-slugify==8.0.4
PyTurboJPEG==1.8.3
PyTurboJPEG==2.2.0
PyYAML==6.0.3
requests==2.33.1
securetar==2026.4.1

8
requirements_all.txt generated
View File

@@ -96,7 +96,7 @@ PyTransportNSW==0.1.1
# homeassistant.components.camera
# homeassistant.components.stream
PyTurboJPEG==1.8.3
PyTurboJPEG==2.2.0
# homeassistant.components.vicare
PyViCare==2.59.0
@@ -1313,7 +1313,7 @@ ihcsdk==2.8.5
imeon_inverter_api==0.4.0
# homeassistant.components.imgw_pib
imgw_pib==2.0.2
imgw_pib==2.1.0
# homeassistant.components.incomfort
incomfort-client==0.7.0
@@ -1980,7 +1980,7 @@ pyatv==0.17.0
pyaussiebb==0.1.5
# homeassistant.components.myneomitis
pyaxencoapi==1.0.6
pyaxencoapi==1.0.7
# homeassistant.components.balboa
pybalboa==1.1.3
@@ -3334,7 +3334,7 @@ xknxproject==3.8.2
# homeassistant.components.startca
# homeassistant.components.ted5000
# homeassistant.components.zestimate
xmltodict==1.0.2
xmltodict==1.0.4
# homeassistant.components.xs1
xs1-api-client==3.0.0

View File

@@ -16,7 +16,7 @@ license-expression==30.4.3
mock-open==1.4.0
mypy==1.20.1
prek==0.2.28
pydantic==2.13.0
pydantic==2.13.1
pylint==4.0.5
pylint-per-file-ignores==3.2.1
pipdeptree==2.26.1

View File

@@ -93,7 +93,7 @@ PyTransportNSW==0.1.1
# homeassistant.components.camera
# homeassistant.components.stream
PyTurboJPEG==1.8.3
PyTurboJPEG==2.2.0
# homeassistant.components.vicare
PyViCare==2.59.0
@@ -1165,7 +1165,7 @@ igloohome-api==0.1.1
imeon_inverter_api==0.4.0
# homeassistant.components.imgw_pib
imgw_pib==2.0.2
imgw_pib==2.1.0
# homeassistant.components.incomfort
incomfort-client==0.7.0
@@ -1717,7 +1717,7 @@ pyatv==0.17.0
pyaussiebb==0.1.5
# homeassistant.components.myneomitis
pyaxencoapi==1.0.6
pyaxencoapi==1.0.7
# homeassistant.components.balboa
pybalboa==1.1.3
@@ -2828,7 +2828,7 @@ xknxproject==3.8.2
# homeassistant.components.startca
# homeassistant.components.ted5000
# homeassistant.components.zestimate
xmltodict==1.0.2
xmltodict==1.0.4
# homeassistant.components.yale_smart_alarm
yalesmartalarmclient==0.4.3

View File

@@ -124,7 +124,7 @@ backoff>=2.0
Brotli>=1.2.0
# ensure pydantic version does not float since it might have breaking changes
pydantic==2.13.0
pydantic==2.13.1
# Required for Python 3.14.0 compatibility (#119223).
mashumaro>=3.17.0

View File

@@ -7,7 +7,6 @@ from homeassistant import core
from homeassistant.util import executor, thread
from .model import Config, Integration
from .requirements import PACKAGE_REGEX, PIP_VERSION_RANGE_SEPARATOR
_GO2RTC_SHA = (
"675c318b23c06fd862a61d262240c9a63436b4050d177ffc68a32710d9e05bae" # 1.9.14
@@ -43,8 +42,7 @@ COPY rootfs /
COPY --from=ghcr.io/alexxit/go2rtc@sha256:{go2rtc} /usr/local/bin/go2rtc /bin/go2rtc
## Setup Home Assistant Core dependencies
COPY requirements.txt homeassistant/
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
COPY --parents requirements.txt homeassistant/package_constraints.txt homeassistant/
RUN \
# Verify go2rtc can be executed
go2rtc --version \
@@ -64,7 +62,7 @@ RUN \
-r homeassistant/requirements_all.txt
## Setup Home Assistant Core
COPY . homeassistant/
COPY --parents LICENSE* README* homeassistant/ pyproject.toml homeassistant/
RUN \
uv pip install \
-e ./homeassistant \
@@ -139,10 +137,12 @@ SHELL ["/bin/sh", "-o", "pipefail", "-c"]
ENTRYPOINT ["/usr/src/homeassistant/script/hassfest/docker/entrypoint.sh"]
WORKDIR "/github/workspace"
COPY . /usr/src/homeassistant
COPY --parents requirements.txt homeassistant/ script /usr/src/homeassistant/
# Uv creates a lock file in /tmp
RUN --mount=type=tmpfs,target=/tmp \
--mount=type=bind,source=requirements_test.txt,target=/tmp/requirements_test.txt,readonly \
--mount=type=bind,source=requirements_test_pre_commit.txt,target=/tmp/requirements_test_pre_commit.txt,readonly \
# Required for PyTurboJPEG
apk add --no-cache libturbojpeg \
# Install uv at the version pinned in the requirements file
@@ -152,9 +152,9 @@ RUN --mount=type=tmpfs,target=/tmp \
--no-cache \
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
-r /usr/src/homeassistant/requirements.txt \
pipdeptree=={pipdeptree} \
tqdm=={tqdm} \
ruff=={ruff}
"pipdeptree==$(awk -F'==' '/^pipdeptree==/{{print $2}}' /tmp/requirements_test.txt)" \
"tqdm==$(awk -F'==' '/^tqdm==/{{print $2}}' /tmp/requirements_test.txt)" \
"ruff==$(awk -F'==' '/^ruff==/{{print $2}}' /tmp/requirements_test_pre_commit.txt)"
LABEL "name"="hassfest"
LABEL "maintainer"="Home Assistant <hello@home-assistant.io>"
@@ -166,36 +166,6 @@ LABEL "com.github.actions.color"="gray-dark"
"""
def _get_package_versions(file: Path, packages: set[str]) -> dict[str, str]:
package_versions: dict[str, str] = {}
with file.open(encoding="UTF-8") as fp:
for _, line in enumerate(fp):
if package_versions.keys() == packages:
return package_versions
if match := PACKAGE_REGEX.match(line):
pkg, sep, version = match.groups()
if pkg not in packages:
continue
if sep != "==" or not version:
raise RuntimeError(
f'Requirement {pkg} need to be pinned "{pkg}==<version>".'
)
for part in version.split(";", 1)[0].split(","):
version_part = PIP_VERSION_RANGE_SEPARATOR.match(part)
if version_part:
package_versions[pkg] = version_part.group(2)
break
if package_versions.keys() == packages:
return package_versions
raise RuntimeError("At least one package was not found in the requirements file.")
@dataclass
class File:
"""File."""
@@ -215,27 +185,16 @@ def _generate_files(config: Config) -> list[File]:
+ 10
) * 1000
package_versions = _get_package_versions(
config.root / "requirements_test.txt", {"pipdeptree", "tqdm"}
)
package_versions |= _get_package_versions(
config.root / "requirements_test_pre_commit.txt", {"ruff"}
)
files = [
File(
DOCKERFILE_TEMPLATE.format(
timeout=timeout,
**package_versions,
go2rtc=_GO2RTC_SHA,
),
config.root / "Dockerfile",
),
File(
_HASSFEST_TEMPLATE.format(
timeout=timeout,
**package_versions,
),
_HASSFEST_TEMPLATE.format(),
config.root / "script/hassfest/docker/Dockerfile",
),
]

View File

@@ -11,10 +11,12 @@ SHELL ["/bin/sh", "-o", "pipefail", "-c"]
ENTRYPOINT ["/usr/src/homeassistant/script/hassfest/docker/entrypoint.sh"]
WORKDIR "/github/workspace"
COPY . /usr/src/homeassistant
COPY --parents requirements.txt homeassistant/ script /usr/src/homeassistant/
# Uv creates a lock file in /tmp
RUN --mount=type=tmpfs,target=/tmp \
--mount=type=bind,source=requirements_test.txt,target=/tmp/requirements_test.txt,readonly \
--mount=type=bind,source=requirements_test_pre_commit.txt,target=/tmp/requirements_test_pre_commit.txt,readonly \
# Required for PyTurboJPEG
apk add --no-cache libturbojpeg \
# Install uv at the version pinned in the requirements file
@@ -24,9 +26,9 @@ RUN --mount=type=tmpfs,target=/tmp \
--no-cache \
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
-r /usr/src/homeassistant/requirements.txt \
pipdeptree==2.26.1 \
tqdm==4.67.1 \
ruff==0.15.1
"pipdeptree==$(awk -F'==' '/^pipdeptree==/{print $2}' /tmp/requirements_test.txt)" \
"tqdm==$(awk -F'==' '/^tqdm==/{print $2}' /tmp/requirements_test.txt)" \
"ruff==$(awk -F'==' '/^ruff==/{print $2}' /tmp/requirements_test_pre_commit.txt)"
LABEL "name"="hassfest"
LABEL "maintainer"="Home Assistant <hello@home-assistant.io>"

View File

@@ -1,11 +1,5 @@
# Ignore everything except the specified files
*
!homeassistant/
!requirements.txt
!script/
script/hassfest/docker/
!script/hassfest/docker/entrypoint.sh
# No need to include the Dockerfile
script/hassfest/docker/Dockerfile*
# Temporary files
**/__pycache__

View File

@@ -6,12 +6,6 @@ import pytest
from homeassistant.auth import jwt_wrapper
async def test_all_default_options_are_in_verify_options() -> None:
"""Test that all default options in _VERIFY_OPTIONS."""
for option in jwt_wrapper._PyJWTWithVerify._get_default_options():
assert option in jwt_wrapper._VERIFY_OPTIONS
async def test_reject_access_token_with_impossible_large_size() -> None:
"""Test rejecting access tokens with impossible sizes."""
with pytest.raises(jwt.DecodeError):

View File

@@ -14143,7 +14143,7 @@
'object_id_base': 'Export to grid',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
@@ -14155,7 +14155,7 @@
'supported_features': 0,
'translation_key': 'mix_export_to_grid',
'unique_id': 'SPH123456-mix_export_to_grid',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
})
# ---
# name: test_sph_sensors_v1_api[sensor.sph123456_export_to_grid-state]
@@ -14164,7 +14164,7 @@
'device_class': 'power',
'friendly_name': 'SPH123456 Export to grid',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.sph123456_export_to_grid',
@@ -14314,7 +14314,7 @@
'object_id_base': 'Import from grid',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
@@ -14326,7 +14326,7 @@
'supported_features': 0,
'translation_key': 'mix_import_from_grid',
'unique_id': 'SPH123456-mix_import_from_grid',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
})
# ---
# name: test_sph_sensors_v1_api[sensor.sph123456_import_from_grid-state]
@@ -14335,7 +14335,7 @@
'device_class': 'power',
'friendly_name': 'SPH123456 Import from grid',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.sph123456_import_from_grid',

View File

@@ -21,6 +21,8 @@ HYDROLOGICAL_DATA = HydrologicalData(
water_temperature=SensorData(name="Water Temperature", value=10.8),
flood_alarm=None,
flood_warning=None,
ice_phenomenon=SensorData(name="Ice Phenomenon", value=20),
ice_phenomenon_measurement_date=datetime(2024, 4, 27, 10, 0, tzinfo=UTC),
water_level_measurement_date=datetime(2024, 4, 27, 10, 0, tzinfo=UTC),
water_temperature_measurement_date=datetime(2024, 4, 27, 10, 10, tzinfo=UTC),
water_flow=SensorData(name="Water Flow", value=123.45),

View File

@@ -41,6 +41,12 @@
'valid_to': '2024-04-28T11:00:00+00:00',
'value': 'rapid_water_level_rise',
}),
'ice_phenomenon': dict({
'name': 'Ice Phenomenon',
'unit': None,
'value': 20,
}),
'ice_phenomenon_measurement_date': '2024-04-27T10:00:00+00:00',
'latitude': None,
'longitude': None,
'river': 'River Name',

View File

@@ -5,6 +5,7 @@ from datetime import timedelta
from unittest.mock import patch
import pytest
from soco.exceptions import SoCoException
from homeassistant.components.sonos import DOMAIN
from homeassistant.components.sonos.const import (
@@ -341,6 +342,28 @@ async def test_alarm_change_device(
assert device.name == soco_br.get_speaker_info()["zone_name"]
async def test_alarm_update_exception_logs_warning(
hass: HomeAssistant,
async_setup_sonos,
entity_registry: er.EntityRegistry,
soco: MockSoCo,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test household mismatch logs warning and alarm update/setup is skipped."""
with patch(
"homeassistant.components.sonos.alarms.Alarms.update",
side_effect=SoCoException(
"Alarm list UID RINCON_0001234567890:31 does not match RINCON_000E987654321:0"
),
):
await async_setup_sonos()
await hass.async_block_till_done()
# Alarm should not be set up due to household mismatch
assert "switch.sonos_alarm_14" not in entity_registry.entities
assert "cannot be updated due to a household mismatch" in caplog.text
async def test_alarm_setup_for_undiscovered_speaker(
hass: HomeAssistant,
async_setup_sonos,

View File

@@ -11,6 +11,7 @@ from homeassistant.components.synology_dsm.const import (
DOMAIN,
SERVICES,
)
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
@@ -22,7 +23,6 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from .consts import HOST, MACS, PASSWORD, PORT, USE_SSL, USERNAME
@@ -57,19 +57,9 @@ async def test_services_registered(hass: HomeAssistant, mock_dsm: MagicMock) ->
async def test_reauth_triggered(hass: HomeAssistant) -> None:
"""Test if reauthentication flow is triggered."""
with (
patch(
"homeassistant.components.synology_dsm.SynoApi.async_setup",
side_effect=SynologyDSMLoginInvalidException(USERNAME),
),
patch(
"homeassistant.components.synology_dsm.config_flow.SynologyDSMFlowHandler.async_step_reauth",
return_value={
"type": FlowResultType.FORM,
"flow_id": "mock_flow",
"step_id": "reauth_confirm",
},
) as mock_async_step_reauth,
with patch(
"homeassistant.components.synology_dsm.SynoApi.async_setup",
side_effect=SynologyDSMLoginInvalidException(USERNAME),
):
entry = MockConfigEntry(
domain=DOMAIN,
@@ -85,7 +75,8 @@ async def test_reauth_triggered(hass: HomeAssistant) -> None:
entry.add_to_hass(hass)
assert not await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
mock_async_step_reauth.assert_called_once()
assert entry.state is ConfigEntryState.SETUP_ERROR
assert any(entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
async def test_config_entry_migrations(

View File

@@ -124,6 +124,7 @@ def mock_weheat_heat_pump_instance() -> MagicMock:
mock_heat_pump_instance.energy_in_dhw = 6789
mock_heat_pump_instance.energy_in_defrost = 555
mock_heat_pump_instance.energy_in_cooling = 9000
mock_heat_pump_instance.energy_in_standby = 684
mock_heat_pump_instance.energy_total = 28689
mock_heat_pump_instance.energy_out_heating = 10000
mock_heat_pump_instance.energy_out_dhw = 6677

View File

@@ -877,6 +877,64 @@
'state': '12345',
})
# ---
# name: test_all_entities[sensor.test_model_electricity_used_standby-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_model_electricity_used_standby',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Electricity used standby',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Electricity used standby',
'platform': 'weheat',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'electricity_used_standby',
'unique_id': '0000-1111-2222-3333_electricity_used_standby',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_all_entities[sensor.test_model_electricity_used_standby-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Test Model Electricity used standby',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_model_electricity_used_standby',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '684',
})
# ---
# name: test_all_entities[sensor.test_model_energy_output_cooling-entry]
EntityRegistryEntrySnapshot({
'aliases': list([

View File

@@ -33,7 +33,7 @@ async def test_all_entities(
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(("has_dhw", "nr_of_entities"), [(False, 22), (True, 27)])
@pytest.mark.parametrize(("has_dhw", "nr_of_entities"), [(False, 23), (True, 28)])
async def test_create_entities(
hass: HomeAssistant,
mock_weheat_discover: AsyncMock,

View File

@@ -28,6 +28,8 @@ from homeassistant.const import (
CONF_ENTITY_ID,
CONF_OPTIONS,
CONF_TARGET,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfTemperature,
@@ -56,6 +58,7 @@ from homeassistant.helpers.condition import (
async_validate_condition_config,
make_entity_numerical_condition,
make_entity_numerical_condition_with_unit,
make_entity_state_condition,
)
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
@@ -3939,3 +3942,188 @@ async def test_numerical_condition_with_unit_behavior(
{ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS},
)
assert test(hass) is False
async def _setup_state_condition(
hass: HomeAssistant,
entity_ids: str | list[str],
states: str | bool | set[str | bool],
condition_options: dict[str, Any] | None = None,
domain_specs: Mapping[str, DomainSpec] | None = None,
) -> condition.ConditionCheckerType:
"""Set up a state condition via a mock platform and return the checker."""
condition_cls = make_entity_state_condition(
domain_specs or _DEFAULT_DOMAIN_SPECS,
states,
)
async def async_get_conditions(
hass: HomeAssistant,
) -> dict[str, type[Condition]]:
return {"_": condition_cls}
mock_integration(hass, MockModule("test"))
mock_platform(
hass, "test.condition", Mock(async_get_conditions=async_get_conditions)
)
if isinstance(entity_ids, str):
entity_ids = [entity_ids]
config: dict[str, Any] = {
CONF_CONDITION: "test",
CONF_TARGET: {CONF_ENTITY_ID: entity_ids},
CONF_OPTIONS: condition_options or {},
}
config = await async_validate_condition_config(hass, config)
test = await condition.async_from_config(hass, config)
assert test is not None
return test
async def test_state_condition_single_entity(hass: HomeAssistant) -> None:
"""Test state condition with a single entity."""
test = await _setup_state_condition(
hass, entity_ids="test.entity_1", states=STATE_ON
)
hass.states.async_set("test.entity_1", STATE_ON)
assert test(hass) is True
hass.states.async_set("test.entity_1", STATE_OFF)
assert test(hass) is False
async def test_state_condition_multiple_target_states(hass: HomeAssistant) -> None:
"""Test state condition matching any of multiple target states."""
test = await _setup_state_condition(
hass, entity_ids="test.entity_1", states={"on", "heat"}
)
hass.states.async_set("test.entity_1", "on")
assert test(hass) is True
hass.states.async_set("test.entity_1", "heat")
assert test(hass) is True
hass.states.async_set("test.entity_1", "off")
assert test(hass) is False
@pytest.mark.parametrize(
"state_value",
[STATE_UNAVAILABLE, STATE_UNKNOWN],
)
async def test_state_condition_unavailable_unknown(
hass: HomeAssistant, state_value: str
) -> None:
"""Test state condition with unavailable/unknown entities.
Uses three entities: entity_1 is on, entity_2 is unavailable/unknown,
entity_3 varies. Unavailable/unknown entities are excluded from
evaluation, so:
- behavior any: passes if at least one *available* entity matches
- behavior all: passes if all *available* entities match
"""
# Single entity: unavailable/unknown → False
test_single = await _setup_state_condition(
hass, entity_ids="test.entity_1", states=STATE_ON
)
hass.states.async_set("test.entity_1", state_value)
assert test_single(hass) is False
# behavior any: entity_1=on, entity_2=unavailable, entity_3=off
# → True (entity_1 matches, entity_2 is skipped)
test_any = await _setup_state_condition(
hass,
entity_ids=["test.entity_1", "test.entity_2", "test.entity_3"],
states=STATE_ON,
condition_options={ATTR_BEHAVIOR: BEHAVIOR_ANY},
)
hass.states.async_set("test.entity_1", STATE_ON)
hass.states.async_set("test.entity_2", state_value)
hass.states.async_set("test.entity_3", STATE_OFF)
assert test_any(hass) is True
# behavior any: entity_1=off, entity_2=unavailable, entity_3=off
# → False (no available entity matches)
hass.states.async_set("test.entity_1", STATE_OFF)
assert test_any(hass) is False
# behavior all: entity_1=on, entity_2=unavailable, entity_3=on
# → True (all *available* entities match, entity_2 is skipped)
test_all = await _setup_state_condition(
hass,
entity_ids=["test.entity_1", "test.entity_2", "test.entity_3"],
states=STATE_ON,
condition_options={ATTR_BEHAVIOR: BEHAVIOR_ALL},
)
hass.states.async_set("test.entity_1", STATE_ON)
hass.states.async_set("test.entity_2", state_value)
hass.states.async_set("test.entity_3", STATE_ON)
assert test_all(hass) is True
# behavior all: entity_1=on, entity_2=unavailable, entity_3=off
# → False (entity_3 is available and doesn't match)
hass.states.async_set("test.entity_3", STATE_OFF)
assert test_all(hass) is False
async def test_state_condition_entity_not_found(hass: HomeAssistant) -> None:
"""Test state condition when entity does not exist."""
test = await _setup_state_condition(
hass, entity_ids="test.nonexistent", states=STATE_ON
)
# Entity doesn't exist — condition should be false
assert test(hass) is False
async def test_state_condition_attribute_value_source(hass: HomeAssistant) -> None:
"""Test state condition reads from attribute when value_source is set."""
test = await _setup_state_condition(
hass,
entity_ids="test.entity_1",
states="heat",
domain_specs={"test": DomainSpec(value_source="hvac_action")},
)
hass.states.async_set("test.entity_1", "on", {"hvac_action": "heat"})
assert test(hass) is True
hass.states.async_set("test.entity_1", "on", {"hvac_action": "idle"})
assert test(hass) is False
# Missing attribute
hass.states.async_set("test.entity_1", "on", {})
assert test(hass) is False
@pytest.mark.parametrize(
("behavior", "one_match_expected"),
[(BEHAVIOR_ANY, True), (BEHAVIOR_ALL, False)],
)
async def test_state_condition_behavior(
hass: HomeAssistant, behavior: str, one_match_expected: bool
) -> None:
"""Test state condition with behavior any/all."""
test = await _setup_state_condition(
hass,
entity_ids=["test.entity_1", "test.entity_2"],
states=STATE_ON,
condition_options={ATTR_BEHAVIOR: behavior},
)
# Both on → True for any and all
hass.states.async_set("test.entity_1", STATE_ON)
hass.states.async_set("test.entity_2", STATE_ON)
assert test(hass) is True
# Only one on → depends on behavior
hass.states.async_set("test.entity_2", STATE_OFF)
assert test(hass) is one_match_expected
# Neither on → False for any and all
hass.states.async_set("test.entity_1", STATE_OFF)
assert test(hass) is False

View File

@@ -124,25 +124,23 @@ async def test_setup_does_discovery(
async def test_set_scan_interval_via_config(hass: HomeAssistant) -> None:
"""Test the setting of the scan interval via configuration."""
async def async_platform_setup(
def platform_setup(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Test the platform setup."""
async_add_entities([MockEntity(should_poll=True)])
add_entities([MockEntity(should_poll=True)])
mock_platform(
hass,
"platform.test_domain",
MockPlatform(async_setup_platform=async_platform_setup),
hass, "platform.test_domain", MockPlatform(setup_platform=platform_setup)
)
component = EntityComponent(_LOGGER, DOMAIN, hass)
with patch.object(hass.loop, "call_later") as mock_track:
await component.async_setup(
component.setup(
{DOMAIN: {"platform": "platform", "scan_interval": timedelta(seconds=30)}}
)
@@ -154,24 +152,22 @@ async def test_set_scan_interval_via_config(hass: HomeAssistant) -> None:
async def test_set_entity_namespace_via_config(hass: HomeAssistant) -> None:
"""Test setting an entity namespace."""
async def async_platform_setup(
def platform_setup(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Test the platform setup."""
async_add_entities([MockEntity(name="beer"), MockEntity(name=None)])
add_entities([MockEntity(name="beer"), MockEntity(name=None)])
platform = MockPlatform(async_setup_platform=async_platform_setup)
platform = MockPlatform(setup_platform=platform_setup)
mock_platform(hass, "platform.test_domain", platform)
component = EntityComponent(_LOGGER, DOMAIN, hass)
await component.async_setup(
{DOMAIN: {"platform": "platform", "entity_namespace": "yummy"}}
)
component.setup({DOMAIN: {"platform": "platform", "entity_namespace": "yummy"}})
await hass.async_block_till_done()

View File

@@ -270,8 +270,8 @@ async def test_adding_entities_with_generator_and_thread_callback(
@pytest.mark.usefixtures("disable_translations_once")
async def test_platform_slow_setup_cancel_warning(hass: HomeAssistant) -> None:
"""Test slow setup warning timer is scheduled and cancelled on success."""
async def test_platform_warn_slow_setup(hass: HomeAssistant) -> None:
"""Warn we log when platform setup takes a long time."""
platform = MockPlatform()
mock_platform(hass, "platform.test_domain", platform)
@@ -283,26 +283,22 @@ async def test_platform_slow_setup_cancel_warning(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert mock_call.called
# Find the platform setup warning by matching the exact format string
platform_warn_call = next(
call
for call in mock_call.call_args_list
if len(call[0]) >= 3
and call[0][1] == _LOGGER.warning
and call[0][2] == "Setup of %s platform %s is taking over %s seconds."
)
scheduled_time = platform_warn_call[0][0]
# mock_calls[3] is the warning message for component setup
# mock_calls[10] is the warning message for platform setup
timeout, logger_method = mock_call.mock_calls[10][1][:2]
assert scheduled_time - hass.loop.time() == pytest.approx(
assert timeout - hass.loop.time() == pytest.approx(
entity_platform.SLOW_SETUP_WARNING, 0.5
)
assert mock_call.return_value.cancel.called
assert logger_method == _LOGGER.warning
assert mock_call().cancel.called
async def test_platform_slow_setup_timeout(
async def test_platform_error_slow_setup(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that platform setup is aborted after SLOW_SETUP_MAX_WAIT."""
"""Don't block startup more than SLOW_SETUP_MAX_WAIT."""
with patch.object(entity_platform, "SLOW_SETUP_MAX_WAIT", 0):
called = []