Compare commits

...
Sign in to create a new pull request.

27 commits

Author SHA1 Message Date
J. Nick Koston
54f0ea113f
Apply suggestions from code review 2024-11-13 10:41:25 -06:00
epenet
716c9590c7
cherry pick epenet fix for flakey advantage_air test f11aba9648 2024-11-12 21:29:37 -06:00
J. Nick Koston
f2292c66a6
fix patch target 2024-11-12 21:03:53 -06:00
J. Nick Koston
f4801c5b06
Bump aiohttp to 3.10.11rc0
changelog: https://github.com/aio-libs/aiohttp/compare/v3.10.10...v3.10.11rc0
2024-11-12 20:39:20 -06:00
Franck Nijhof
c16fb9c93d
Bump version to 2024.11.1 2024-11-08 18:58:21 +01:00
Jan Bouwhuis
da8fc7a2fc
Refrase imap fetch service description string (#130152) 2024-11-08 18:58:07 +01:00
Allen Porter
864b4d86f2
Fix bugs in nest stream expiration handling (#130150) 2024-11-08 18:58:04 +01:00
Louis Christ
1bb0ced7c0
Fix volume_up not working in some cases in bluesound integration (#130146) 2024-11-08 18:58:00 +01:00
Martin Hjelmare
2fe4fc908b
Bump ha-ffmpeg to 3.2.2 (#130142) 2024-11-08 18:57:25 +01:00
Joost Lekkerkerker
aa2c3b046f
Bump spotifyaio to 0.8.7 (#130140) 2024-11-08 18:56:15 +01:00
Robert Resch
22822cb8aa
Add go2rtc workaround for HA managed one until upstream fixes it (#130139) 2024-11-08 18:56:12 +01:00
Shai Ungar
b71383c997
Fix issue when timestamp is None (#130133)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-11-08 18:56:09 +01:00
Bram Kragten
b0b163df48
Update frontend to 20241106.2 (#130128) 2024-11-08 18:56:06 +01:00
Luke Lashley
35539dbf60
Bump python-roborock to 2.7.2 (#130100) 2024-11-08 18:56:02 +01:00
Bram Kragten
09d03e8edf
Update frontend to 20241106.1 (#130086) 2024-11-08 18:55:59 +01:00
Kelvin Dekker
46e37f3bdd
Fix typo in insteon strings (#130085) 2024-11-08 18:55:55 +01:00
Klaas Schoute
0206c149cf
Force int value on port in P1Monitor (#130084) 2024-11-08 18:55:52 +01:00
Josef Zweck
29620ef977
Add missing string to tedee plus test (#130081) 2024-11-08 18:55:49 +01:00
Erik Montnemery
9012b113ad
Don't create repairs asking user to remove duplicate flipr config entries (#130058)
* Don't create repairs asking user to remove duplicate flipr config entries

* Improve comments
2024-11-08 18:55:46 +01:00
Allen Porter
5f5f6cc3d5
Fix KeyError in nest integration when the old key format does not exist (#130057)
* Fix bug in nest setup when the old key format does not exist

* Further simplify the entry.data check

* Update homeassistant/components/nest/api.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-11-08 18:55:42 +01:00
Erik Montnemery
7ff501f3ec
Don't create repairs asking user to remove duplicate ignored config entries (#130056) 2024-11-08 18:55:39 +01:00
sean t
b0f110b9ab
Bump agent-py to 0.0.24 (#130018)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-11-08 18:55:36 +01:00
epenet
2692bc23a5
Add missing placeholder description to twitch (#130013) 2024-11-08 18:55:33 +01:00
Allen Porter
1beac5f0f8
Bump google-nest-sdm to 6.1.4 (#130005)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-11-08 18:55:29 +01:00
Keilin Bickar
ec7ba1b7fd
Update sense energy library to 0.13.3 (#129998) 2024-11-08 18:55:25 +01:00
Brett Adams
5bd1b0dd9c
Fix Trunks in Teslemetry and Tesla Fleet (#129986) 2024-11-08 18:55:21 +01:00
Michael Hansen
a2ad4c9cfd
Bump intents to 2024.11.6 (#129982) 2024-11-08 18:52:43 +01:00
46 changed files with 606 additions and 173 deletions

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/agent_dvr",
"iot_class": "local_polling",
"loggers": ["agent"],
"requirements": ["agent-py==0.0.23"]
"requirements": ["agent-py==0.0.24"]
}

View file

@ -770,7 +770,7 @@ class BluesoundPlayer(MediaPlayerEntity):
async def async_set_volume_level(self, volume: float) -> None:
"""Send volume_up command to media player."""
volume = int(volume * 100)
volume = int(round(volume * 100))
volume = min(100, volume)
volume = max(0, volume)

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.11.4"]
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.11.6"]
}

View file

@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["sense_energy"],
"quality_scale": "internal",
"requirements": ["sense-energy==0.13.2"]
"requirements": ["sense-energy==0.13.3"]
}

View file

@ -4,5 +4,5 @@
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/ffmpeg",
"integration_type": "system",
"requirements": ["ha-ffmpeg==3.2.1"]
"requirements": ["ha-ffmpeg==3.2.2"]
}

View file

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20241106.0"]
"requirements": ["home-assistant-frontend==20241106.2"]
}

View file

@ -1,5 +1,8 @@
"""The go2rtc component."""
from __future__ import annotations
from dataclasses import dataclass
import logging
import shutil
@ -38,7 +41,13 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
from homeassistant.util.package import is_docker_env
from .const import CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN, HA_MANAGED_URL
from .const import (
CONF_DEBUG_UI,
DEBUG_UI_URL_MESSAGE,
DOMAIN,
HA_MANAGED_RTSP_PORT,
HA_MANAGED_URL,
)
from .server import Server
_LOGGER = logging.getLogger(__name__)
@ -85,13 +94,22 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
_DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN)
_DATA_GO2RTC: HassKey[Go2RtcData] = HassKey(DOMAIN)
_RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError)
@dataclass(frozen=True)
class Go2RtcData:
"""Data for go2rtc."""
url: str
managed: bool
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up WebRTC."""
url: str | None = None
managed = False
if DOMAIN not in config and DEFAULT_CONFIG_DOMAIN not in config:
await _remove_go2rtc_entries(hass)
return True
@ -126,8 +144,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop)
url = HA_MANAGED_URL
managed = True
hass.data[_DATA_GO2RTC] = url
hass.data[_DATA_GO2RTC] = Go2RtcData(url, managed)
discovery_flow.async_create_flow(
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
)
@ -142,28 +161,32 @@ async def _remove_go2rtc_entries(hass: HomeAssistant) -> None:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up go2rtc from a config entry."""
url = hass.data[_DATA_GO2RTC]
data = hass.data[_DATA_GO2RTC]
# Validate the server URL
try:
client = Go2RtcRestClient(async_get_clientsession(hass), url)
client = Go2RtcRestClient(async_get_clientsession(hass), data.url)
await client.validate_server_version()
except Go2RtcClientError as err:
if isinstance(err.__cause__, _RETRYABLE_ERRORS):
raise ConfigEntryNotReady(
f"Could not connect to go2rtc instance on {url}"
f"Could not connect to go2rtc instance on {data.url}"
) from err
_LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
_LOGGER.warning(
"Could not connect to go2rtc instance on %s (%s)", data.url, err
)
return False
except Go2RtcVersionError as err:
raise ConfigEntryNotReady(
f"The go2rtc server version is not supported, {err}"
) from err
except Exception as err: # noqa: BLE001
_LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
_LOGGER.warning(
"Could not connect to go2rtc instance on %s (%s)", data.url, err
)
return False
provider = WebRTCProvider(hass, url)
provider = WebRTCProvider(hass, data)
async_register_webrtc_provider(hass, provider)
return True
@ -181,12 +204,12 @@ async def _get_binary(hass: HomeAssistant) -> str | None:
class WebRTCProvider(CameraWebRTCProvider):
"""WebRTC provider."""
def __init__(self, hass: HomeAssistant, url: str) -> None:
def __init__(self, hass: HomeAssistant, data: Go2RtcData) -> None:
"""Initialize the WebRTC provider."""
self._hass = hass
self._url = url
self._data = data
self._session = async_get_clientsession(hass)
self._rest_client = Go2RtcRestClient(self._session, url)
self._rest_client = Go2RtcRestClient(self._session, data.url)
self._sessions: dict[str, Go2RtcWsClient] = {}
@property
@ -208,7 +231,7 @@ class WebRTCProvider(CameraWebRTCProvider):
) -> None:
"""Handle the WebRTC offer and return the answer via the provided callback."""
self._sessions[session_id] = ws_client = Go2RtcWsClient(
self._session, self._url, source=camera.entity_id
self._session, self._data.url, source=camera.entity_id
)
if not (stream_source := await camera.stream_source()):
@ -219,8 +242,30 @@ class WebRTCProvider(CameraWebRTCProvider):
streams = await self._rest_client.streams.list()
if (stream := streams.get(camera.entity_id)) is None or not any(
stream_source == producer.url for producer in stream.producers
if self._data.managed:
# HA manages the go2rtc instance
stream_org_name = camera.entity_id + "_orginal"
stream_redirect_sources = [
f"rtsp://127.0.0.1:{HA_MANAGED_RTSP_PORT}/{stream_org_name}",
f"ffmpeg:{stream_org_name}#audio=opus",
]
if (
(stream_org := streams.get(stream_org_name)) is None
or not any(
stream_source == producer.url for producer in stream_org.producers
)
or (stream_redirect := streams.get(camera.entity_id)) is None
or stream_redirect_sources != [p.url for p in stream_redirect.producers]
):
await self._rest_client.streams.add(stream_org_name, stream_source)
await self._rest_client.streams.add(
camera.entity_id, stream_redirect_sources
)
# go2rtc instance is managed outside HA
elif (stream_org := streams.get(camera.entity_id)) is None or not any(
stream_source == producer.url for producer in stream_org.producers
):
await self._rest_client.streams.add(
camera.entity_id,

View file

@ -6,3 +6,4 @@ CONF_DEBUG_UI = "debug_ui"
DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
HA_MANAGED_API_PORT = 11984
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
HA_MANAGED_RTSP_PORT = 18554

View file

@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL
from .const import HA_MANAGED_API_PORT, HA_MANAGED_RTSP_PORT, HA_MANAGED_URL
_LOGGER = logging.getLogger(__name__)
_TERMINATE_TIMEOUT = 5
@ -24,15 +24,16 @@ _RESPAWN_COOLDOWN = 1
# Default configuration for HA
# - Api is listening only on localhost
# - Disable rtsp listener
# - Enable rtsp for localhost only as ffmpeg needs it
# - Clear default ice servers
_GO2RTC_CONFIG_FORMAT = r"""
_GO2RTC_CONFIG_FORMAT = r"""# This file is managed by Home Assistant
# Do not edit it manually
api:
listen: "{api_ip}:{api_port}"
rtsp:
# ffmpeg needs rtsp for opus audio transcoding
listen: "127.0.0.1:18554"
listen: "127.0.0.1:{rtsp_port}"
webrtc:
listen: ":18555/tcp"
@ -67,7 +68,9 @@ def _create_temp_file(api_ip: str) -> str:
with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file:
file.write(
_GO2RTC_CONFIG_FORMAT.format(
api_ip=api_ip, api_port=HA_MANAGED_API_PORT
api_ip=api_ip,
api_port=HA_MANAGED_API_PORT,
rtsp_port=HA_MANAGED_RTSP_PORT,
).encode()
)
return file.name

View file

@ -104,7 +104,7 @@
"services": {
"fetch": {
"name": "Fetch message",
"description": "Fetch the email message from the server.",
"description": "Fetch an email message from the server.",
"fields": {
"entry": {
"name": "Entry",

View file

@ -112,7 +112,7 @@
"services": {
"add_all_link": {
"name": "Add all link",
"description": "Tells the Insteom Modem (IM) start All-Linking mode. Once the IM is in All-Linking mode, press the link button on the device to complete All-Linking.",
"description": "Tells the Insteon Modem (IM) start All-Linking mode. Once the IM is in All-Linking mode, press the link button on the device to complete All-Linking.",
"fields": {
"group": {
"name": "Group",

View file

@ -114,9 +114,8 @@ async def new_subscriber(
implementation, config_entry_oauth2_flow.LocalOAuth2Implementation
):
raise TypeError(f"Unexpected auth implementation {implementation}")
subscription_name = entry.data.get(
CONF_SUBSCRIPTION_NAME, entry.data[CONF_SUBSCRIBER_ID]
)
if (subscription_name := entry.data.get(CONF_SUBSCRIPTION_NAME)) is None:
subscription_name = entry.data[CONF_SUBSCRIBER_ID]
auth = AsyncConfigEntryAuth(
aiohttp_client.async_get_clientsession(hass),
config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation),

View file

@ -235,7 +235,9 @@ class NestWebRTCEntity(NestCameraBaseEntity):
async def _async_refresh_stream(self) -> None:
"""Refresh stream to extend expiration time."""
now = utcnow()
for webrtc_stream in list(self._webrtc_sessions.values()):
for session_id, webrtc_stream in list(self._webrtc_sessions.items()):
if session_id not in self._webrtc_sessions:
continue
if now < (webrtc_stream.expires_at - STREAM_EXPIRATION_BUFFER):
_LOGGER.debug(
"Stream does not yet expire: %s", webrtc_stream.expires_at
@ -247,7 +249,8 @@ class NestWebRTCEntity(NestCameraBaseEntity):
except ApiException as err:
_LOGGER.debug("Failed to extend stream: %s", err)
else:
self._webrtc_sessions[webrtc_stream.media_session_id] = webrtc_stream
if session_id in self._webrtc_sessions:
self._webrtc_sessions[session_id] = webrtc_stream
async def async_camera_image(
self, width: int | None = None, height: int | None = None

View file

@ -20,5 +20,5 @@
"iot_class": "cloud_push",
"loggers": ["google_nest_sdm"],
"quality_scale": "platinum",
"requirements": ["google-nest-sdm==6.1.3"]
"requirements": ["google-nest-sdm==6.1.4"]
}

View file

@ -57,10 +57,13 @@ class P1MonitorFlowHandler(ConfigFlow, domain=DOMAIN):
data_schema=vol.Schema(
{
vol.Required(CONF_HOST): TextSelector(),
vol.Required(CONF_PORT, default=80): NumberSelector(
NumberSelectorConfig(
mode=NumberSelectorMode.BOX,
)
vol.Required(CONF_PORT, default=80): vol.All(
NumberSelector(
NumberSelectorConfig(
min=1, max=65535, mode=NumberSelectorMode.BOX
),
),
vol.Coerce(int),
),
}
),

View file

@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["roborock"],
"requirements": [
"python-roborock==2.6.1",
"python-roborock==2.7.2",
"vacuum-map-parser-roborock==0.1.2"
]
}

View file

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/sense",
"iot_class": "cloud_polling",
"loggers": ["sense_energy"],
"requirements": ["sense-energy==0.13.2"]
"requirements": ["sense-energy==0.13.3"]
}

View file

@ -1,8 +1,8 @@
"""Services for the seventeentrack integration."""
from typing import Final
from typing import Any, Final
from pyseventeentrack.package import PACKAGE_STATUS_MAP
from pyseventeentrack.package import PACKAGE_STATUS_MAP, Package
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
@ -81,18 +81,7 @@ def setup_services(hass: HomeAssistant) -> None:
return {
"packages": [
{
ATTR_DESTINATION_COUNTRY: package.destination_country,
ATTR_ORIGIN_COUNTRY: package.origin_country,
ATTR_PACKAGE_TYPE: package.package_type,
ATTR_TRACKING_INFO_LANGUAGE: package.tracking_info_language,
ATTR_TRACKING_NUMBER: package.tracking_number,
ATTR_LOCATION: package.location,
ATTR_STATUS: package.status,
ATTR_TIMESTAMP: package.timestamp.isoformat(),
ATTR_INFO_TEXT: package.info_text,
ATTR_FRIENDLY_NAME: package.friendly_name,
}
package_to_dict(package)
for package in live_packages
if slugify(package.status) in package_states or package_states == []
]
@ -110,6 +99,22 @@ def setup_services(hass: HomeAssistant) -> None:
await seventeen_coordinator.client.profile.archive_package(tracking_number)
def package_to_dict(package: Package) -> dict[str, Any]:
result = {
ATTR_DESTINATION_COUNTRY: package.destination_country,
ATTR_ORIGIN_COUNTRY: package.origin_country,
ATTR_PACKAGE_TYPE: package.package_type,
ATTR_TRACKING_INFO_LANGUAGE: package.tracking_info_language,
ATTR_TRACKING_NUMBER: package.tracking_number,
ATTR_LOCATION: package.location,
ATTR_STATUS: package.status,
ATTR_INFO_TEXT: package.info_text,
ATTR_FRIENDLY_NAME: package.friendly_name,
}
if timestamp := package.timestamp:
result[ATTR_TIMESTAMP] = timestamp.isoformat()
return result
async def _validate_service(config_entry_id):
entry: ConfigEntry | None = hass.config_entries.async_get_entry(config_entry_id)
if not entry:

View file

@ -9,6 +9,6 @@
"iot_class": "cloud_polling",
"loggers": ["spotipy"],
"quality_scale": "silver",
"requirements": ["spotifyaio==0.8.5"],
"requirements": ["spotifyaio==0.8.7"],
"zeroconf": ["_spotify-connect._tcp.local."]
}

View file

@ -38,7 +38,8 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unique_id_mismatch": "You selected a different bridge than the one this config entry was configured with, this is not allowed."
},
"error": {
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",

View file

@ -177,13 +177,7 @@ class TeslaFleetRearTrunkEntity(TeslaFleetVehicleEntity, CoverEntity):
def _async_update_attrs(self) -> None:
"""Update the entity attributes."""
value = self._value
if value == CLOSED:
self._attr_is_closed = True
elif value == OPEN:
self._attr_is_closed = False
else:
self._attr_is_closed = None
self._attr_is_closed = self._value == CLOSED
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open rear trunk."""

View file

@ -182,13 +182,7 @@ class TeslemetryRearTrunkEntity(TeslemetryVehicleEntity, CoverEntity):
def _async_update_attrs(self) -> None:
"""Update the entity attributes."""
value = self._value
if value == CLOSED:
self._attr_is_closed = True
elif value == OPEN:
self._attr_is_closed = False
else:
self._attr_is_closed = None
self._attr_is_closed = self._value == CLOSED
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open rear trunk."""

View file

@ -78,7 +78,10 @@ class OAuth2FlowHandler(
reauth_entry = self._get_reauth_entry()
self._abort_if_unique_id_mismatch(
reason="wrong_account",
description_placeholders={"title": reauth_entry.title},
description_placeholders={
"title": reauth_entry.title,
"username": str(reauth_entry.unique_id),
},
)
new_channels = reauth_entry.options[CONF_CHANNELS]

View file

@ -2147,7 +2147,12 @@ class ConfigEntries:
if unique_id is not UNDEFINED and entry.unique_id != unique_id:
# Deprecated in 2024.11, should fail in 2025.11
if (
unique_id is not None
# flipr creates duplicates during migration, and asks users to
# remove the duplicate. We don't need warn about it here too.
# We should remove the special case for "flipr" in HA Core 2025.4,
# when the flipr migration period ends
entry.domain != "flipr"
and unique_id is not None
and self.async_entry_for_domain_unique_id(entry.domain, unique_id)
is not None
):
@ -2425,7 +2430,24 @@ class ConfigEntries:
issues.add(issue.issue_id)
for domain, unique_ids in self._entries._domain_unique_id_index.items(): # noqa: SLF001
# flipr creates duplicates during migration, and asks users to
# remove the duplicate. We don't need warn about it here too.
# We should remove the special case for "flipr" in HA Core 2025.4,
# when the flipr migration period ends
if domain == "flipr":
continue
for unique_id, entries in unique_ids.items():
# We might mutate the list of entries, so we need a copy to not mess up
# the index
entries = list(entries)
# There's no need to raise an issue for ignored entries, we can
# safely remove them once we no longer allow unique id collisions.
# Iterate over a copy of the copy to allow mutating while iterating
for entry in list(entries):
if entry.source == SOURCE_IGNORE:
entries.remove(entry)
if len(entries) < 2:
continue
issue_id = f"{ISSUE_UNIQUE_ID_COLLISION}_{domain}_{unique_id}"

View file

@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 11
PATCH_VERSION: Final = "0"
PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)

View file

@ -5,7 +5,7 @@ aiodiscover==2.1.0
aiodns==3.2.0
aiohasupervisor==0.2.1
aiohttp-fast-zlib==0.1.1
aiohttp==3.10.10
aiohttp==3.10.11
aiohttp_cors==0.7.0
aiozoneinfo==0.2.1
astral==2.2
@ -28,13 +28,13 @@ dbus-fast==2.24.3
fnv-hash-fast==1.0.2
go2rtc-client==0.1.0
ha-av==10.1.1
ha-ffmpeg==3.2.1
ha-ffmpeg==3.2.2
habluetooth==3.6.0
hass-nabucasa==0.83.0
hassil==1.7.4
home-assistant-bluetooth==1.13.0
home-assistant-frontend==20241106.0
home-assistant-intents==2024.11.4
home-assistant-frontend==20241106.2
home-assistant-intents==2024.11.6
httpx==0.27.2
ifaddr==0.2.0
Jinja2==3.1.4
@ -166,7 +166,7 @@ get-mac==1000000000.0.0
charset-normalizer==3.2.0
# dacite: Ensure we have a version that is able to handle type unions for
# Roborock, NAM, Brother, and GIOS.
# NAM, Brother, and GIOS.
dacite>=1.7.0
# Musle wheels for pandas 2.2.0 cannot be build for any architecture.

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2024.11.0"
version = "2024.11.1"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
@ -28,7 +28,7 @@ dependencies = [
# change behavior based on presence of supervisor. Deprecated with #127228
# Lib can be removed with 2025.11
"aiohasupervisor==0.2.1",
"aiohttp==3.10.10",
"aiohttp==3.10.11",
"aiohttp_cors==0.7.0",
"aiohttp-fast-zlib==0.1.1",
"aiozoneinfo==0.2.1",

View file

@ -5,7 +5,7 @@
# Home Assistant Core
aiodns==3.2.0
aiohasupervisor==0.2.1
aiohttp==3.10.10
aiohttp==3.10.11
aiohttp_cors==0.7.0
aiohttp-fast-zlib==0.1.1
aiozoneinfo==0.2.1

View file

@ -152,7 +152,7 @@ advantage-air==0.4.4
afsapi==0.2.7
# homeassistant.components.agent_dvr
agent-py==0.0.23
agent-py==0.0.24
# homeassistant.components.geo_json_events
aio-geojson-generic-client==0.4
@ -1011,7 +1011,7 @@ google-cloud-texttospeech==2.17.2
google-generativeai==0.8.2
# homeassistant.components.nest
google-nest-sdm==6.1.3
google-nest-sdm==6.1.4
# homeassistant.components.google_photos
google-photos-library-api==0.12.1
@ -1069,7 +1069,7 @@ h2==4.1.0
ha-av==10.1.1
# homeassistant.components.ffmpeg
ha-ffmpeg==3.2.1
ha-ffmpeg==3.2.2
# homeassistant.components.iotawatt
ha-iotawattpy==0.1.2
@ -1124,10 +1124,10 @@ hole==0.8.0
holidays==0.60
# homeassistant.components.frontend
home-assistant-frontend==20241106.0
home-assistant-frontend==20241106.2
# homeassistant.components.conversation
home-assistant-intents==2024.11.4
home-assistant-intents==2024.11.6
# homeassistant.components.home_connect
homeconnect==0.8.0
@ -2393,7 +2393,7 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==2.6.1
python-roborock==2.7.2
# homeassistant.components.smarttub
python-smarttub==0.0.36
@ -2623,7 +2623,7 @@ sendgrid==6.8.2
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
sense-energy==0.13.2
sense-energy==0.13.3
# homeassistant.components.sensirion_ble
sensirion-ble==0.1.1
@ -2707,7 +2707,7 @@ speak2mary==1.4.0
speedtest-cli==2.1.3
# homeassistant.components.spotify
spotifyaio==0.8.5
spotifyaio==0.8.7
# homeassistant.components.sql
sqlparse==0.5.0

View file

@ -140,7 +140,7 @@ advantage-air==0.4.4
afsapi==0.2.7
# homeassistant.components.agent_dvr
agent-py==0.0.23
agent-py==0.0.24
# homeassistant.components.geo_json_events
aio-geojson-generic-client==0.4
@ -861,7 +861,7 @@ google-cloud-texttospeech==2.17.2
google-generativeai==0.8.2
# homeassistant.components.nest
google-nest-sdm==6.1.3
google-nest-sdm==6.1.4
# homeassistant.components.google_photos
google-photos-library-api==0.12.1
@ -907,7 +907,7 @@ h2==4.1.0
ha-av==10.1.1
# homeassistant.components.ffmpeg
ha-ffmpeg==3.2.1
ha-ffmpeg==3.2.2
# homeassistant.components.iotawatt
ha-iotawattpy==0.1.2
@ -950,10 +950,10 @@ hole==0.8.0
holidays==0.60
# homeassistant.components.frontend
home-assistant-frontend==20241106.0
home-assistant-frontend==20241106.2
# homeassistant.components.conversation
home-assistant-intents==2024.11.4
home-assistant-intents==2024.11.6
# homeassistant.components.home_connect
homeconnect==0.8.0
@ -1914,7 +1914,7 @@ python-picnic-api==1.1.0
python-rabbitair==0.0.8
# homeassistant.components.roborock
python-roborock==2.6.1
python-roborock==2.7.2
# homeassistant.components.smarttub
python-smarttub==0.0.36
@ -2090,7 +2090,7 @@ securetar==2024.2.1
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
sense-energy==0.13.2
sense-energy==0.13.3
# homeassistant.components.sensirion_ble
sensirion-ble==0.1.1
@ -2159,7 +2159,7 @@ speak2mary==1.4.0
speedtest-cli==2.1.3
# homeassistant.components.spotify
spotifyaio==0.8.5
spotifyaio==0.8.7
# homeassistant.components.sql
sqlparse==0.5.0

View file

@ -182,7 +182,7 @@ get-mac==1000000000.0.0
charset-normalizer==3.2.0
# dacite: Ensure we have a version that is able to handle type unions for
# Roborock, NAM, Brother, and GIOS.
# NAM, Brother, and GIOS.
dacite>=1.7.0
# Musle wheels for pandas 2.2.0 cannot be build for any architecture.

View file

@ -23,7 +23,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.4.28,source=/uv,target=/bin/uv \
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
-r /usr/src/homeassistant/requirements.txt \
stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.7.1 \
PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.1 hassil==1.7.4 home-assistant-intents==2024.11.4 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.2 hassil==1.7.4 home-assistant-intents==2024.11.6 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
LABEL "name"="hassfest"
LABEL "maintainer"="Home Assistant <hello@home-assistant.io>"

View file

@ -1,10 +1,8 @@
"""Test the Advantage Air Binary Sensor Platform."""
from datetime import timedelta
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, patch
from homeassistant.components.advantage_air import ADVANTAGE_AIR_SYNC_INTERVAL
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@ -70,22 +68,14 @@ async def test_binary_sensor_async_setup_entry(
assert not hass.states.get(entity_id)
mock_get.reset_mock()
entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None)
await hass.async_block_till_done()
async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL + 1),
)
await hass.async_block_till_done(wait_background_tasks=True)
assert len(mock_get.mock_calls) == 1
with patch("homeassistant.config_entries.RELOAD_AFTER_UPDATE_DELAY", 1):
entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None)
await hass.async_block_till_done()
async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
)
await hass.async_block_till_done(wait_background_tasks=True)
assert len(mock_get.mock_calls) == 2
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2))
await hass.async_block_till_done(wait_background_tasks=True)
assert len(mock_get.mock_calls) == 1
state = hass.states.get(entity_id)
assert state
@ -101,22 +91,14 @@ async def test_binary_sensor_async_setup_entry(
assert not hass.states.get(entity_id)
mock_get.reset_mock()
entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None)
await hass.async_block_till_done()
async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL + 1),
)
await hass.async_block_till_done(wait_background_tasks=True)
assert len(mock_get.mock_calls) == 1
with patch("homeassistant.config_entries.RELOAD_AFTER_UPDATE_DELAY", 1):
entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None)
await hass.async_block_till_done()
async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
)
await hass.async_block_till_done(wait_background_tasks=True)
assert len(mock_get.mock_calls) == 2
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2))
await hass.async_block_till_done(wait_background_tasks=True)
assert len(mock_get.mock_calls) == 1
state = hass.states.get(entity_id)
assert state

View file

@ -1,15 +1,13 @@
"""Test the Advantage Air Sensor Platform."""
from datetime import timedelta
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, patch
from homeassistant.components.advantage_air import ADVANTAGE_AIR_SYNC_INTERVAL
from homeassistant.components.advantage_air.const import DOMAIN as ADVANTAGE_AIR_DOMAIN
from homeassistant.components.advantage_air.sensor import (
ADVANTAGE_AIR_SERVICE_SET_TIME_TO,
ADVANTAGE_AIR_SET_COUNTDOWN_VALUE,
)
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@ -124,23 +122,15 @@ async def test_sensor_platform_disabled_entity(
assert not hass.states.get(entity_id)
entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None)
await hass.async_block_till_done(wait_background_tasks=True)
mock_get.reset_mock()
async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL + 1),
)
await hass.async_block_till_done(wait_background_tasks=True)
assert len(mock_get.mock_calls) == 1
with patch("homeassistant.config_entries.RELOAD_AFTER_UPDATE_DELAY", 1):
entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None)
await hass.async_block_till_done(wait_background_tasks=True)
async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
)
await hass.async_block_till_done(wait_background_tasks=True)
assert len(mock_get.mock_calls) == 2
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2))
await hass.async_block_till_done(wait_background_tasks=True)
assert len(mock_get.mock_calls) == 1
state = hass.states.get(entity_id)
assert state

View file

@ -345,3 +345,31 @@ async def test_attr_bluesound_group(
).attributes.get("bluesound_group")
assert attr_bluesound_group == ["player-name1111", "player-name2222"]
async def test_volume_up_from_6_to_7(
hass: HomeAssistant,
setup_config_entry: None,
player_mocks: PlayerMocks,
) -> None:
"""Test the media player volume up from 6 to 7.
This fails if if rounding is not done correctly. See https://github.com/home-assistant/core/issues/129956 for more details.
"""
player_mocks.player_data.status_long_polling_mock.set(
dataclasses.replace(
player_mocks.player_data.status_long_polling_mock.get(), volume=6
)
)
# give the long polling loop a chance to update the state; this could be any async call
await hass.async_block_till_done()
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_VOLUME_UP,
{ATTR_ENTITY_ID: "media_player.player_name1111"},
blocking=True,
)
player_mocks.player_data.player.volume.assert_called_once_with(level=7)

View file

@ -275,7 +275,9 @@ async def test_limit_refetch(
with (
pytest.raises(aiohttp.ServerTimeoutError),
patch("asyncio.timeout", side_effect=TimeoutError()),
patch.object(
client.session._connector, "connect", side_effect=asyncio.TimeoutError
),
):
resp = await client.get("/api/camera_proxy/camera.config_test")

View file

@ -3,7 +3,7 @@
from collections.abc import Callable, Generator
import logging
from typing import NamedTuple
from unittest.mock import AsyncMock, Mock, patch
from unittest.mock import AsyncMock, Mock, call, patch
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
from go2rtc_client import Stream
@ -296,7 +296,7 @@ async def _test_setup_and_signaling(
],
)
@pytest.mark.parametrize("has_go2rtc_entry", [True, False])
async def test_setup_go_binary(
async def test_setup_managed(
hass: HomeAssistant,
rest_client: AsyncMock,
ws_client: Mock,
@ -308,15 +308,131 @@ async def test_setup_go_binary(
config: ConfigType,
ui_enabled: bool,
) -> None:
"""Test the go2rtc config entry with binary."""
"""Test the go2rtc setup with managed go2rtc instance."""
assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry
camera = init_test_integration
def after_setup() -> None:
server.assert_called_once_with(hass, "/usr/bin/go2rtc", enable_ui=ui_enabled)
server_start.assert_called_once()
entity_id = camera.entity_id
stream_name_orginal = camera.entity_id + "_orginal"
assert camera.frontend_stream_type == StreamType.HLS
await _test_setup_and_signaling(
hass, rest_client, ws_client, config, after_setup, init_test_integration
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done(wait_background_tasks=True)
config_entries = hass.config_entries.async_entries(DOMAIN)
assert len(config_entries) == 1
assert config_entries[0].state == ConfigEntryState.LOADED
server.assert_called_once_with(hass, "/usr/bin/go2rtc", enable_ui=ui_enabled)
server_start.assert_called_once()
receive_message_callback = Mock(spec_set=WebRTCSendMessage)
async def test() -> None:
await camera.async_handle_async_webrtc_offer(
OFFER_SDP, "session_id", receive_message_callback
)
ws_client.send.assert_called_once_with(
WebRTCOffer(
OFFER_SDP,
camera.async_get_webrtc_client_configuration().configuration.ice_servers,
)
)
ws_client.subscribe.assert_called_once()
# Simulate the answer from the go2rtc server
callback = ws_client.subscribe.call_args[0][0]
callback(WebRTCAnswer(ANSWER_SDP))
receive_message_callback.assert_called_once_with(HAWebRTCAnswer(ANSWER_SDP))
await test()
stream_added_calls = [
call(stream_name_orginal, "rtsp://stream"),
call(
entity_id,
[
f"rtsp://127.0.0.1:18554/{stream_name_orginal}",
f"ffmpeg:{stream_name_orginal}#audio=opus",
],
),
]
assert rest_client.streams.add.call_args_list == stream_added_calls
# Stream original missing
rest_client.streams.add.reset_mock()
rest_client.streams.list.return_value = {
entity_id: Stream(
[
Producer(f"rtsp://127.0.0.1:18554/{stream_name_orginal}"),
Producer(f"ffmpeg:{stream_name_orginal}#audio=opus"),
]
)
}
receive_message_callback.reset_mock()
ws_client.reset_mock()
await test()
assert rest_client.streams.add.call_args_list == stream_added_calls
# Stream original source different
rest_client.streams.add.reset_mock()
rest_client.streams.list.return_value = {
stream_name_orginal: Stream([Producer("rtsp://different")]),
entity_id: Stream(
[
Producer(f"rtsp://127.0.0.1:18554/{stream_name_orginal}"),
Producer(f"ffmpeg:{stream_name_orginal}#audio=opus"),
]
),
}
receive_message_callback.reset_mock()
ws_client.reset_mock()
await test()
assert rest_client.streams.add.call_args_list == stream_added_calls
# Stream source different
rest_client.streams.add.reset_mock()
rest_client.streams.list.return_value = {
stream_name_orginal: Stream([Producer("rtsp://stream")]),
entity_id: Stream([Producer("rtsp://different")]),
}
receive_message_callback.reset_mock()
ws_client.reset_mock()
await test()
assert rest_client.streams.add.call_args_list == stream_added_calls
# If the stream is already added, the stream should not be added again.
rest_client.streams.add.reset_mock()
rest_client.streams.list.return_value = {
stream_name_orginal: Stream([Producer("rtsp://stream")]),
entity_id: Stream(
[
Producer(f"rtsp://127.0.0.1:18554/{stream_name_orginal}"),
Producer(f"ffmpeg:{stream_name_orginal}#audio=opus"),
]
),
}
receive_message_callback.reset_mock()
ws_client.reset_mock()
await test()
rest_client.streams.add.assert_not_called()
assert isinstance(camera._webrtc_provider, WebRTCProvider)
# Set stream source to None and provider should be skipped
rest_client.streams.list.return_value = {}
receive_message_callback.reset_mock()
camera.set_stream_source(None)
await camera.async_handle_async_webrtc_offer(
OFFER_SDP, "session_id", receive_message_callback
)
receive_message_callback.assert_called_once_with(
WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source")
)
await hass.async_stop()
@ -332,7 +448,7 @@ async def test_setup_go_binary(
],
)
@pytest.mark.parametrize("has_go2rtc_entry", [True, False])
async def test_setup_go(
async def test_setup_self_hosted(
hass: HomeAssistant,
rest_client: AsyncMock,
ws_client: Mock,
@ -342,16 +458,83 @@ async def test_setup_go(
mock_is_docker_env: Mock,
has_go2rtc_entry: bool,
) -> None:
"""Test the go2rtc config entry without binary."""
"""Test the go2rtc with selfhosted go2rtc instance."""
assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry
config = {DOMAIN: {CONF_URL: "http://localhost:1984/"}}
camera = init_test_integration
def after_setup() -> None:
server.assert_not_called()
entity_id = camera.entity_id
assert camera.frontend_stream_type == StreamType.HLS
await _test_setup_and_signaling(
hass, rest_client, ws_client, config, after_setup, init_test_integration
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done(wait_background_tasks=True)
config_entries = hass.config_entries.async_entries(DOMAIN)
assert len(config_entries) == 1
assert config_entries[0].state == ConfigEntryState.LOADED
server.assert_not_called()
receive_message_callback = Mock(spec_set=WebRTCSendMessage)
async def test() -> None:
await camera.async_handle_async_webrtc_offer(
OFFER_SDP, "session_id", receive_message_callback
)
ws_client.send.assert_called_once_with(
WebRTCOffer(
OFFER_SDP,
camera.async_get_webrtc_client_configuration().configuration.ice_servers,
)
)
ws_client.subscribe.assert_called_once()
# Simulate the answer from the go2rtc server
callback = ws_client.subscribe.call_args[0][0]
callback(WebRTCAnswer(ANSWER_SDP))
receive_message_callback.assert_called_once_with(HAWebRTCAnswer(ANSWER_SDP))
await test()
rest_client.streams.add.assert_called_once_with(
entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"]
)
# Stream exists but the source is different
rest_client.streams.add.reset_mock()
rest_client.streams.list.return_value = {
entity_id: Stream([Producer("rtsp://different")])
}
receive_message_callback.reset_mock()
ws_client.reset_mock()
await test()
rest_client.streams.add.assert_called_once_with(
entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"]
)
# If the stream is already added, the stream should not be added again.
rest_client.streams.add.reset_mock()
rest_client.streams.list.return_value = {
entity_id: Stream([Producer("rtsp://stream")])
}
receive_message_callback.reset_mock()
ws_client.reset_mock()
await test()
rest_client.streams.add.assert_not_called()
assert isinstance(camera._webrtc_provider, WebRTCProvider)
# Set stream source to None and provider should be skipped
rest_client.streams.list.return_value = {}
receive_message_callback.reset_mock()
camera.set_stream_source(None)
await camera.async_handle_async_webrtc_offer(
OFFER_SDP, "session_id", receive_message_callback
)
receive_message_callback.assert_called_once_with(
WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source")
)
mock_get_binary.assert_not_called()

View file

@ -105,12 +105,13 @@ async def test_server_run_success(
# Verify that the config file was written
mock_tempfile.write.assert_called_once_with(
f"""
f"""# This file is managed by Home Assistant
# Do not edit it manually
api:
listen: "{api_ip}:11984"
rtsp:
# ffmpeg needs rtsp for opus audio transcoding
listen: "127.0.0.1:18554"
webrtc:

View file

@ -30,6 +30,7 @@ CLIENT_ID = "some-client-id"
CLIENT_SECRET = "some-client-secret"
CLOUD_PROJECT_ID = "cloud-id-9876"
SUBSCRIBER_ID = "projects/cloud-id-9876/subscriptions/subscriber-id-9876"
SUBSCRIPTION_NAME = "projects/cloud-id-9876/subscriptions/subscriber-id-9876"
@dataclass
@ -86,6 +87,17 @@ TEST_CONFIG_ENTRY_LEGACY = NestTestConfig(
},
)
TEST_CONFIG_NEW_SUBSCRIPTION = NestTestConfig(
config_entry_data={
"sdm": {},
"project_id": PROJECT_ID,
"cloud_project_id": CLOUD_PROJECT_ID,
"subscription_name": SUBSCRIPTION_NAME,
"auth_implementation": "imported-cred",
},
credential=ClientCredential(CLIENT_ID, CLIENT_SECRET),
)
class FakeSubscriber(GoogleNestSubscriber):
"""Fake subscriber that supplies a FakeDeviceManager."""

View file

@ -31,6 +31,7 @@ from .common import (
SUBSCRIBER_ID,
TEST_CONFIG_ENTRY_LEGACY,
TEST_CONFIG_LEGACY,
TEST_CONFIG_NEW_SUBSCRIPTION,
TEST_CONFIGFLOW_APP_CREDS,
FakeSubscriber,
PlatformSetup,
@ -97,6 +98,19 @@ async def test_setup_success(
assert entries[0].state is ConfigEntryState.LOADED
@pytest.mark.parametrize("nest_test_config", [(TEST_CONFIG_NEW_SUBSCRIPTION)])
async def test_setup_success_new_subscription_format(
hass: HomeAssistant, error_caplog: pytest.LogCaptureFixture, setup_platform
) -> None:
"""Test successful setup."""
await setup_platform()
assert not error_caplog.records
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state is ConfigEntryState.LOADED
@pytest.mark.parametrize("subscriber_id", [("invalid-subscriber-format")])
async def test_setup_configuration_failure(
hass: HomeAssistant,

View file

@ -36,6 +36,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None:
assert result2.get("type") is FlowResultType.CREATE_ENTRY
assert result2.get("title") == "P1 Monitor"
assert result2.get("data") == {CONF_HOST: "example.com", CONF_PORT: 80}
assert isinstance(result2["data"][CONF_PORT], int)
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_p1monitor.mock_calls) == 1

View file

@ -102,6 +102,7 @@
'id': '120',
'mode': 'ro',
'name': '错误代码',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
@ -109,6 +110,7 @@
'id': '121',
'mode': 'ro',
'name': '设备状态',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
@ -116,6 +118,7 @@
'id': '122',
'mode': 'ro',
'name': '设备电量',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
@ -123,6 +126,7 @@
'id': '123',
'mode': 'rw',
'name': '清扫模式',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
@ -130,6 +134,7 @@
'id': '124',
'mode': 'rw',
'name': '拖地模式',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
@ -137,6 +142,7 @@
'id': '125',
'mode': 'rw',
'name': '主刷寿命',
'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}',
'type': 'VALUE',
}),
dict({
@ -144,6 +150,7 @@
'id': '126',
'mode': 'rw',
'name': '边刷寿命',
'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}',
'type': 'VALUE',
}),
dict({
@ -151,6 +158,7 @@
'id': '127',
'mode': 'rw',
'name': '滤网寿命',
'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}',
'type': 'VALUE',
}),
dict({
@ -381,6 +389,7 @@
'id': '120',
'mode': 'ro',
'name': '错误代码',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
@ -388,6 +397,7 @@
'id': '121',
'mode': 'ro',
'name': '设备状态',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
@ -395,6 +405,7 @@
'id': '122',
'mode': 'ro',
'name': '设备电量',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
@ -402,6 +413,7 @@
'id': '123',
'mode': 'rw',
'name': '清扫模式',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
@ -409,6 +421,7 @@
'id': '124',
'mode': 'rw',
'name': '拖地模式',
'property': '{"range": []}',
'type': 'ENUM',
}),
dict({
@ -416,6 +429,7 @@
'id': '125',
'mode': 'rw',
'name': '主刷寿命',
'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}',
'type': 'VALUE',
}),
dict({
@ -423,6 +437,7 @@
'id': '126',
'mode': 'rw',
'name': '边刷寿命',
'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}',
'type': 'VALUE',
}),
dict({
@ -430,6 +445,7 @@
'id': '127',
'mode': 'rw',
'name': '滤网寿命',
'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}',
'type': 'VALUE',
}),
dict({

View file

@ -71,3 +71,32 @@
]),
})
# ---
# name: test_packages_with_none_timestamp
dict({
'packages': list([
dict({
'destination_country': 'Belgium',
'friendly_name': 'friendly name 1',
'info_text': 'info text 1',
'location': 'location 1',
'origin_country': 'Belgium',
'package_type': 'Registered Parcel',
'status': 'In Transit',
'tracking_info_language': 'Unknown',
'tracking_number': '456',
}),
dict({
'destination_country': 'Belgium',
'friendly_name': 'friendly name 2',
'info_text': 'info text 1',
'location': 'location 1',
'origin_country': 'Belgium',
'package_type': 'Registered Parcel',
'status': 'Delivered',
'timestamp': '2020-08-10T10:32:00+00:00',
'tracking_info_language': 'Unknown',
'tracking_number': '789',
}),
]),
})
# ---

View file

@ -150,6 +150,28 @@ async def test_archive_package(
)
async def test_packages_with_none_timestamp(
hass: HomeAssistant,
mock_seventeentrack: AsyncMock,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Ensure service returns all packages when non provided."""
await _mock_invalid_packages(mock_seventeentrack)
await init_integration(hass, mock_config_entry)
service_response = await hass.services.async_call(
DOMAIN,
SERVICE_GET_PACKAGES,
{
CONFIG_ENTRY_ID_KEY: mock_config_entry.entry_id,
},
blocking=True,
return_response=True,
)
assert service_response == snapshot
async def _mock_packages(mock_seventeentrack):
package1 = get_package(status=10)
package2 = get_package(
@ -167,3 +189,19 @@ async def _mock_packages(mock_seventeentrack):
package2,
package3,
]
async def _mock_invalid_packages(mock_seventeentrack):
package1 = get_package(
status=10,
timestamp=None,
)
package2 = get_package(
tracking_number="789",
friendly_name="friendly name 2",
status=40,
)
mock_seventeentrack.return_value.profile.packages.return_value = [
package1,
package2,
]

View file

@ -7,10 +7,11 @@ from pytedee_async import (
TedeeDataUpdateException,
TedeeLocalAuthException,
)
from pytedee_async.bridge import TedeeBridge
import pytest
from homeassistant.components.tedee.const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.config_entries import SOURCE_USER, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_WEBHOOK_ID
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
@ -134,11 +135,10 @@ async def test_reauth_flow(
assert result["reason"] == "reauth_successful"
async def test_reconfigure_flow(
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_tedee: MagicMock
) -> None:
"""Test that the reconfigure flow works."""
async def __do_reconfigure_flow(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> ConfigFlowResult:
"""Initialize a reconfigure flow."""
mock_config_entry.add_to_hass(hass)
reconfigure_result = await mock_config_entry.start_reconfigure_flow(hass)
@ -146,11 +146,19 @@ async def test_reconfigure_flow(
assert reconfigure_result["type"] is FlowResultType.FORM
assert reconfigure_result["step_id"] == "reconfigure"
result = await hass.config_entries.flow.async_configure(
return await hass.config_entries.flow.async_configure(
reconfigure_result["flow_id"],
{CONF_LOCAL_ACCESS_TOKEN: LOCAL_ACCESS_TOKEN, CONF_HOST: "192.168.1.43"},
)
async def test_reconfigure_flow(
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_tedee: MagicMock
) -> None:
"""Test that the reconfigure flow works."""
result = await __do_reconfigure_flow(hass, mock_config_entry)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"
@ -162,3 +170,18 @@ async def test_reconfigure_flow(
CONF_LOCAL_ACCESS_TOKEN: LOCAL_ACCESS_TOKEN,
CONF_WEBHOOK_ID: WEBHOOK_ID,
}
async def test_reconfigure_unique_id_mismatch(
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_tedee: MagicMock
) -> None:
"""Ensure reconfigure flow aborts when the bride changes."""
mock_tedee.get_local_bridge.return_value = TedeeBridge(
0, "1111-1111", "Bridge-R2D2"
)
result = await __do_reconfigure_flow(hass, mock_config_entry)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "unique_id_mismatch"

View file

@ -7118,6 +7118,41 @@ async def test_async_update_entry_unique_id_collision(
assert issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, issue_id)
@pytest.mark.parametrize("domain", ["flipr"])
async def test_async_update_entry_unique_id_collision_allowed_domain(
hass: HomeAssistant,
manager: config_entries.ConfigEntries,
caplog: pytest.LogCaptureFixture,
issue_registry: ir.IssueRegistry,
domain: str,
) -> None:
"""Test we warn when async_update_entry creates a unique_id collision.
This tests we don't warn and don't create issues for domains which have
their own migration path.
"""
assert len(issue_registry.issues) == 0
entry1 = MockConfigEntry(domain=domain, unique_id=None)
entry2 = MockConfigEntry(domain=domain, unique_id="not none")
entry3 = MockConfigEntry(domain=domain, unique_id="very unique")
entry4 = MockConfigEntry(domain=domain, unique_id="also very unique")
entry1.add_to_manager(manager)
entry2.add_to_manager(manager)
entry3.add_to_manager(manager)
entry4.add_to_manager(manager)
manager.async_update_entry(entry2, unique_id=None)
assert len(issue_registry.issues) == 0
assert len(caplog.record_tuples) == 0
manager.async_update_entry(entry4, unique_id="very unique")
assert len(issue_registry.issues) == 0
assert len(caplog.record_tuples) == 0
assert ("already in use") not in caplog.text
async def test_unique_id_collision_issues(
hass: HomeAssistant,
manager: config_entries.ConfigEntries,
@ -7147,6 +7182,12 @@ async def test_unique_id_collision_issues(
for _ in range(6):
test3.append(MockConfigEntry(domain="test3", unique_id="not_unique"))
await manager.async_add(test3[-1])
# Add an ignored config entry
await manager.async_add(
MockConfigEntry(
domain="test2", unique_id="group_1", source=config_entries.SOURCE_IGNORE
)
)
# Check we get one issue for domain test2 and one issue for domain test3
assert len(issue_registry.issues) == 2
@ -7193,7 +7234,7 @@ async def test_unique_id_collision_issues(
(HOMEASSISTANT_DOMAIN, "config_entry_unique_id_collision_test2_group_2"),
}
# Remove the last test2 group2 duplicate, a new issue is created
# Remove the last test2 group2 duplicate, the issue is cleared
await manager.async_remove(test2_group_2[1].entry_id)
assert not issue_registry.issues