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", "documentation": "https://www.home-assistant.io/integrations/agent_dvr",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["agent"], "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: async def async_set_volume_level(self, volume: float) -> None:
"""Send volume_up command to media player.""" """Send volume_up command to media player."""
volume = int(volume * 100) volume = int(round(volume * 100))
volume = min(100, volume) volume = min(100, volume)
volume = max(0, volume) volume = max(0, volume)

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation", "documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "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", "iot_class": "local_push",
"loggers": ["sense_energy"], "loggers": ["sense_energy"],
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["sense-energy==0.13.2"] "requirements": ["sense-energy==0.13.3"]
} }

View file

@ -4,5 +4,5 @@
"codeowners": [], "codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/ffmpeg", "documentation": "https://www.home-assistant.io/integrations/ffmpeg",
"integration_type": "system", "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", "documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["home-assistant-frontend==20241106.0"] "requirements": ["home-assistant-frontend==20241106.2"]
} }

View file

@ -1,5 +1,8 @@
"""The go2rtc component.""" """The go2rtc component."""
from __future__ import annotations
from dataclasses import dataclass
import logging import logging
import shutil import shutil
@ -38,7 +41,13 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey from homeassistant.util.hass_dict import HassKey
from homeassistant.util.package import is_docker_env 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 from .server import Server
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -85,13 +94,22 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
_DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN) _DATA_GO2RTC: HassKey[Go2RtcData] = HassKey(DOMAIN)
_RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError) _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: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up WebRTC.""" """Set up WebRTC."""
url: str | None = None url: str | None = None
managed = False
if DOMAIN not in config and DEFAULT_CONFIG_DOMAIN not in config: if DOMAIN not in config and DEFAULT_CONFIG_DOMAIN not in config:
await _remove_go2rtc_entries(hass) await _remove_go2rtc_entries(hass)
return True return True
@ -126,8 +144,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop)
url = HA_MANAGED_URL url = HA_MANAGED_URL
managed = True
hass.data[_DATA_GO2RTC] = url hass.data[_DATA_GO2RTC] = Go2RtcData(url, managed)
discovery_flow.async_create_flow( discovery_flow.async_create_flow(
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={} 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: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up go2rtc from a config entry.""" """Set up go2rtc from a config entry."""
url = hass.data[_DATA_GO2RTC] data = hass.data[_DATA_GO2RTC]
# Validate the server URL # Validate the server URL
try: try:
client = Go2RtcRestClient(async_get_clientsession(hass), url) client = Go2RtcRestClient(async_get_clientsession(hass), data.url)
await client.validate_server_version() await client.validate_server_version()
except Go2RtcClientError as err: except Go2RtcClientError as err:
if isinstance(err.__cause__, _RETRYABLE_ERRORS): if isinstance(err.__cause__, _RETRYABLE_ERRORS):
raise ConfigEntryNotReady( raise ConfigEntryNotReady(
f"Could not connect to go2rtc instance on {url}" f"Could not connect to go2rtc instance on {data.url}"
) from err ) 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 return False
except Go2RtcVersionError as err: except Go2RtcVersionError as err:
raise ConfigEntryNotReady( raise ConfigEntryNotReady(
f"The go2rtc server version is not supported, {err}" f"The go2rtc server version is not supported, {err}"
) from err ) from err
except Exception as err: # noqa: BLE001 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 return False
provider = WebRTCProvider(hass, url) provider = WebRTCProvider(hass, data)
async_register_webrtc_provider(hass, provider) async_register_webrtc_provider(hass, provider)
return True return True
@ -181,12 +204,12 @@ async def _get_binary(hass: HomeAssistant) -> str | None:
class WebRTCProvider(CameraWebRTCProvider): class WebRTCProvider(CameraWebRTCProvider):
"""WebRTC provider.""" """WebRTC provider."""
def __init__(self, hass: HomeAssistant, url: str) -> None: def __init__(self, hass: HomeAssistant, data: Go2RtcData) -> None:
"""Initialize the WebRTC provider.""" """Initialize the WebRTC provider."""
self._hass = hass self._hass = hass
self._url = url self._data = data
self._session = async_get_clientsession(hass) 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] = {} self._sessions: dict[str, Go2RtcWsClient] = {}
@property @property
@ -208,7 +231,7 @@ class WebRTCProvider(CameraWebRTCProvider):
) -> None: ) -> None:
"""Handle the WebRTC offer and return the answer via the provided callback.""" """Handle the WebRTC offer and return the answer via the provided callback."""
self._sessions[session_id] = ws_client = Go2RtcWsClient( 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()): if not (stream_source := await camera.stream_source()):
@ -219,8 +242,30 @@ class WebRTCProvider(CameraWebRTCProvider):
streams = await self._rest_client.streams.list() streams = await self._rest_client.streams.list()
if (stream := streams.get(camera.entity_id)) is None or not any( if self._data.managed:
stream_source == producer.url for producer in stream.producers # 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( await self._rest_client.streams.add(
camera.entity_id, 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." DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
HA_MANAGED_API_PORT = 11984 HA_MANAGED_API_PORT = 11984
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/" 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.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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__) _LOGGER = logging.getLogger(__name__)
_TERMINATE_TIMEOUT = 5 _TERMINATE_TIMEOUT = 5
@ -24,15 +24,16 @@ _RESPAWN_COOLDOWN = 1
# Default configuration for HA # Default configuration for HA
# - Api is listening only on localhost # - Api is listening only on localhost
# - Disable rtsp listener # - Enable rtsp for localhost only as ffmpeg needs it
# - Clear default ice servers # - 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: api:
listen: "{api_ip}:{api_port}" listen: "{api_ip}:{api_port}"
rtsp: rtsp:
# ffmpeg needs rtsp for opus audio transcoding listen: "127.0.0.1:{rtsp_port}"
listen: "127.0.0.1:18554"
webrtc: webrtc:
listen: ":18555/tcp" 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: with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file:
file.write( file.write(
_GO2RTC_CONFIG_FORMAT.format( _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() ).encode()
) )
return file.name return file.name

View file

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

View file

@ -112,7 +112,7 @@
"services": { "services": {
"add_all_link": { "add_all_link": {
"name": "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": { "fields": {
"group": { "group": {
"name": "Group", "name": "Group",

View file

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

View file

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

View file

@ -20,5 +20,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["google_nest_sdm"], "loggers": ["google_nest_sdm"],
"quality_scale": "platinum", "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( data_schema=vol.Schema(
{ {
vol.Required(CONF_HOST): TextSelector(), vol.Required(CONF_HOST): TextSelector(),
vol.Required(CONF_PORT, default=80): NumberSelector( vol.Required(CONF_PORT, default=80): vol.All(
NumberSelector(
NumberSelectorConfig( NumberSelectorConfig(
mode=NumberSelectorMode.BOX, min=1, max=65535, mode=NumberSelectorMode.BOX
) ),
),
vol.Coerce(int),
), ),
} }
), ),

View file

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

View file

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/sense", "documentation": "https://www.home-assistant.io/integrations/sense",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["sense_energy"], "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.""" """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 import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.config_entries import ConfigEntry, ConfigEntryState
@ -81,18 +81,7 @@ def setup_services(hass: HomeAssistant) -> None:
return { return {
"packages": [ "packages": [
{ package_to_dict(package)
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,
}
for package in live_packages for package in live_packages
if slugify(package.status) in package_states or package_states == [] 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) 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): async def _validate_service(config_entry_id):
entry: ConfigEntry | None = hass.config_entries.async_get_entry(config_entry_id) entry: ConfigEntry | None = hass.config_entries.async_get_entry(config_entry_id)
if not entry: if not entry:

View file

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

View file

@ -38,7 +38,8 @@
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "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": { "error": {
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", "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: def _async_update_attrs(self) -> None:
"""Update the entity attributes.""" """Update the entity attributes."""
value = self._value self._attr_is_closed = self._value == CLOSED
if value == CLOSED:
self._attr_is_closed = True
elif value == OPEN:
self._attr_is_closed = False
else:
self._attr_is_closed = None
async def async_open_cover(self, **kwargs: Any) -> None: async def async_open_cover(self, **kwargs: Any) -> None:
"""Open rear trunk.""" """Open rear trunk."""

View file

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

View file

@ -78,7 +78,10 @@ class OAuth2FlowHandler(
reauth_entry = self._get_reauth_entry() reauth_entry = self._get_reauth_entry()
self._abort_if_unique_id_mismatch( self._abort_if_unique_id_mismatch(
reason="wrong_account", 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] 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: if unique_id is not UNDEFINED and entry.unique_id != unique_id:
# Deprecated in 2024.11, should fail in 2025.11 # Deprecated in 2024.11, should fail in 2025.11
if ( 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) and self.async_entry_for_domain_unique_id(entry.domain, unique_id)
is not None is not None
): ):
@ -2425,7 +2430,24 @@ class ConfigEntries:
issues.add(issue.issue_id) issues.add(issue.issue_id)
for domain, unique_ids in self._entries._domain_unique_id_index.items(): # noqa: SLF001 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(): 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: if len(entries) < 2:
continue continue
issue_id = f"{ISSUE_UNIQUE_ID_COLLISION}_{domain}_{unique_id}" issue_id = f"{ISSUE_UNIQUE_ID_COLLISION}_{domain}_{unique_id}"

View file

@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024 MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 11 MINOR_VERSION: Final = 11
PATCH_VERSION: Final = "0" PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) 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 aiodns==3.2.0
aiohasupervisor==0.2.1 aiohasupervisor==0.2.1
aiohttp-fast-zlib==0.1.1 aiohttp-fast-zlib==0.1.1
aiohttp==3.10.10 aiohttp==3.10.11
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
aiozoneinfo==0.2.1 aiozoneinfo==0.2.1
astral==2.2 astral==2.2
@ -28,13 +28,13 @@ dbus-fast==2.24.3
fnv-hash-fast==1.0.2 fnv-hash-fast==1.0.2
go2rtc-client==0.1.0 go2rtc-client==0.1.0
ha-av==10.1.1 ha-av==10.1.1
ha-ffmpeg==3.2.1 ha-ffmpeg==3.2.2
habluetooth==3.6.0 habluetooth==3.6.0
hass-nabucasa==0.83.0 hass-nabucasa==0.83.0
hassil==1.7.4 hassil==1.7.4
home-assistant-bluetooth==1.13.0 home-assistant-bluetooth==1.13.0
home-assistant-frontend==20241106.0 home-assistant-frontend==20241106.2
home-assistant-intents==2024.11.4 home-assistant-intents==2024.11.6
httpx==0.27.2 httpx==0.27.2
ifaddr==0.2.0 ifaddr==0.2.0
Jinja2==3.1.4 Jinja2==3.1.4
@ -166,7 +166,7 @@ get-mac==1000000000.0.0
charset-normalizer==3.2.0 charset-normalizer==3.2.0
# dacite: Ensure we have a version that is able to handle type unions for # 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 dacite>=1.7.0
# Musle wheels for pandas 2.2.0 cannot be build for any architecture. # 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] [project]
name = "homeassistant" name = "homeassistant"
version = "2024.11.0" version = "2024.11.1"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"
@ -28,7 +28,7 @@ dependencies = [
# change behavior based on presence of supervisor. Deprecated with #127228 # change behavior based on presence of supervisor. Deprecated with #127228
# Lib can be removed with 2025.11 # Lib can be removed with 2025.11
"aiohasupervisor==0.2.1", "aiohasupervisor==0.2.1",
"aiohttp==3.10.10", "aiohttp==3.10.11",
"aiohttp_cors==0.7.0", "aiohttp_cors==0.7.0",
"aiohttp-fast-zlib==0.1.1", "aiohttp-fast-zlib==0.1.1",
"aiozoneinfo==0.2.1", "aiozoneinfo==0.2.1",

View file

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

View file

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

View file

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

View file

@ -182,7 +182,7 @@ get-mac==1000000000.0.0
charset-normalizer==3.2.0 charset-normalizer==3.2.0
# dacite: Ensure we have a version that is able to handle type unions for # 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 dacite>=1.7.0
# Musle wheels for pandas 2.2.0 cannot be build for any architecture. # 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 \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \
-r /usr/src/homeassistant/requirements.txt \ -r /usr/src/homeassistant/requirements.txt \
stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.7.1 \ 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 "name"="hassfest"
LABEL "maintainer"="Home Assistant <hello@home-assistant.io>" LABEL "maintainer"="Home Assistant <hello@home-assistant.io>"

View file

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

View file

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

View file

@ -345,3 +345,31 @@ async def test_attr_bluesound_group(
).attributes.get("bluesound_group") ).attributes.get("bluesound_group")
assert attr_bluesound_group == ["player-name1111", "player-name2222"] 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 ( with (
pytest.raises(aiohttp.ServerTimeoutError), 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") resp = await client.get("/api/camera_proxy/camera.config_test")

View file

@ -3,7 +3,7 @@
from collections.abc import Callable, Generator from collections.abc import Callable, Generator
import logging import logging
from typing import NamedTuple 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 aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
from go2rtc_client import Stream from go2rtc_client import Stream
@ -296,7 +296,7 @@ async def _test_setup_and_signaling(
], ],
) )
@pytest.mark.parametrize("has_go2rtc_entry", [True, False]) @pytest.mark.parametrize("has_go2rtc_entry", [True, False])
async def test_setup_go_binary( async def test_setup_managed(
hass: HomeAssistant, hass: HomeAssistant,
rest_client: AsyncMock, rest_client: AsyncMock,
ws_client: Mock, ws_client: Mock,
@ -308,15 +308,131 @@ async def test_setup_go_binary(
config: ConfigType, config: ConfigType,
ui_enabled: bool, ui_enabled: bool,
) -> None: ) -> 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 assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry
camera = init_test_integration
def after_setup() -> None: entity_id = camera.entity_id
stream_name_orginal = camera.entity_id + "_orginal"
assert camera.frontend_stream_type == StreamType.HLS
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.assert_called_once_with(hass, "/usr/bin/go2rtc", enable_ui=ui_enabled)
server_start.assert_called_once() server_start.assert_called_once()
await _test_setup_and_signaling( receive_message_callback = Mock(spec_set=WebRTCSendMessage)
hass, rest_client, ws_client, config, after_setup, init_test_integration
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() await hass.async_stop()
@ -332,7 +448,7 @@ async def test_setup_go_binary(
], ],
) )
@pytest.mark.parametrize("has_go2rtc_entry", [True, False]) @pytest.mark.parametrize("has_go2rtc_entry", [True, False])
async def test_setup_go( async def test_setup_self_hosted(
hass: HomeAssistant, hass: HomeAssistant,
rest_client: AsyncMock, rest_client: AsyncMock,
ws_client: Mock, ws_client: Mock,
@ -342,16 +458,83 @@ async def test_setup_go(
mock_is_docker_env: Mock, mock_is_docker_env: Mock,
has_go2rtc_entry: bool, has_go2rtc_entry: bool,
) -> None: ) -> 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 assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry
config = {DOMAIN: {CONF_URL: "http://localhost:1984/"}} config = {DOMAIN: {CONF_URL: "http://localhost:1984/"}}
camera = init_test_integration
def after_setup() -> None: entity_id = camera.entity_id
assert camera.frontend_stream_type == StreamType.HLS
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() server.assert_not_called()
await _test_setup_and_signaling( receive_message_callback = Mock(spec_set=WebRTCSendMessage)
hass, rest_client, ws_client, config, after_setup, init_test_integration
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() 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 # Verify that the config file was written
mock_tempfile.write.assert_called_once_with( mock_tempfile.write.assert_called_once_with(
f""" f"""# This file is managed by Home Assistant
# Do not edit it manually
api: api:
listen: "{api_ip}:11984" listen: "{api_ip}:11984"
rtsp: rtsp:
# ffmpeg needs rtsp for opus audio transcoding
listen: "127.0.0.1:18554" listen: "127.0.0.1:18554"
webrtc: webrtc:

View file

@ -30,6 +30,7 @@ CLIENT_ID = "some-client-id"
CLIENT_SECRET = "some-client-secret" CLIENT_SECRET = "some-client-secret"
CLOUD_PROJECT_ID = "cloud-id-9876" CLOUD_PROJECT_ID = "cloud-id-9876"
SUBSCRIBER_ID = "projects/cloud-id-9876/subscriptions/subscriber-id-9876" SUBSCRIBER_ID = "projects/cloud-id-9876/subscriptions/subscriber-id-9876"
SUBSCRIPTION_NAME = "projects/cloud-id-9876/subscriptions/subscriber-id-9876"
@dataclass @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): class FakeSubscriber(GoogleNestSubscriber):
"""Fake subscriber that supplies a FakeDeviceManager.""" """Fake subscriber that supplies a FakeDeviceManager."""

View file

@ -31,6 +31,7 @@ from .common import (
SUBSCRIBER_ID, SUBSCRIBER_ID,
TEST_CONFIG_ENTRY_LEGACY, TEST_CONFIG_ENTRY_LEGACY,
TEST_CONFIG_LEGACY, TEST_CONFIG_LEGACY,
TEST_CONFIG_NEW_SUBSCRIPTION,
TEST_CONFIGFLOW_APP_CREDS, TEST_CONFIGFLOW_APP_CREDS,
FakeSubscriber, FakeSubscriber,
PlatformSetup, PlatformSetup,
@ -97,6 +98,19 @@ async def test_setup_success(
assert entries[0].state is ConfigEntryState.LOADED 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")]) @pytest.mark.parametrize("subscriber_id", [("invalid-subscriber-format")])
async def test_setup_configuration_failure( async def test_setup_configuration_failure(
hass: HomeAssistant, 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("type") is FlowResultType.CREATE_ENTRY
assert result2.get("title") == "P1 Monitor" assert result2.get("title") == "P1 Monitor"
assert result2.get("data") == {CONF_HOST: "example.com", CONF_PORT: 80} 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_setup_entry.mock_calls) == 1
assert len(mock_p1monitor.mock_calls) == 1 assert len(mock_p1monitor.mock_calls) == 1

View file

@ -102,6 +102,7 @@
'id': '120', 'id': '120',
'mode': 'ro', 'mode': 'ro',
'name': '错误代码', 'name': '错误代码',
'property': '{"range": []}',
'type': 'ENUM', 'type': 'ENUM',
}), }),
dict({ dict({
@ -109,6 +110,7 @@
'id': '121', 'id': '121',
'mode': 'ro', 'mode': 'ro',
'name': '设备状态', 'name': '设备状态',
'property': '{"range": []}',
'type': 'ENUM', 'type': 'ENUM',
}), }),
dict({ dict({
@ -116,6 +118,7 @@
'id': '122', 'id': '122',
'mode': 'ro', 'mode': 'ro',
'name': '设备电量', 'name': '设备电量',
'property': '{"range": []}',
'type': 'ENUM', 'type': 'ENUM',
}), }),
dict({ dict({
@ -123,6 +126,7 @@
'id': '123', 'id': '123',
'mode': 'rw', 'mode': 'rw',
'name': '清扫模式', 'name': '清扫模式',
'property': '{"range": []}',
'type': 'ENUM', 'type': 'ENUM',
}), }),
dict({ dict({
@ -130,6 +134,7 @@
'id': '124', 'id': '124',
'mode': 'rw', 'mode': 'rw',
'name': '拖地模式', 'name': '拖地模式',
'property': '{"range": []}',
'type': 'ENUM', 'type': 'ENUM',
}), }),
dict({ dict({
@ -137,6 +142,7 @@
'id': '125', 'id': '125',
'mode': 'rw', 'mode': 'rw',
'name': '主刷寿命', 'name': '主刷寿命',
'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}',
'type': 'VALUE', 'type': 'VALUE',
}), }),
dict({ dict({
@ -144,6 +150,7 @@
'id': '126', 'id': '126',
'mode': 'rw', 'mode': 'rw',
'name': '边刷寿命', 'name': '边刷寿命',
'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}',
'type': 'VALUE', 'type': 'VALUE',
}), }),
dict({ dict({
@ -151,6 +158,7 @@
'id': '127', 'id': '127',
'mode': 'rw', 'mode': 'rw',
'name': '滤网寿命', 'name': '滤网寿命',
'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}',
'type': 'VALUE', 'type': 'VALUE',
}), }),
dict({ dict({
@ -381,6 +389,7 @@
'id': '120', 'id': '120',
'mode': 'ro', 'mode': 'ro',
'name': '错误代码', 'name': '错误代码',
'property': '{"range": []}',
'type': 'ENUM', 'type': 'ENUM',
}), }),
dict({ dict({
@ -388,6 +397,7 @@
'id': '121', 'id': '121',
'mode': 'ro', 'mode': 'ro',
'name': '设备状态', 'name': '设备状态',
'property': '{"range": []}',
'type': 'ENUM', 'type': 'ENUM',
}), }),
dict({ dict({
@ -395,6 +405,7 @@
'id': '122', 'id': '122',
'mode': 'ro', 'mode': 'ro',
'name': '设备电量', 'name': '设备电量',
'property': '{"range": []}',
'type': 'ENUM', 'type': 'ENUM',
}), }),
dict({ dict({
@ -402,6 +413,7 @@
'id': '123', 'id': '123',
'mode': 'rw', 'mode': 'rw',
'name': '清扫模式', 'name': '清扫模式',
'property': '{"range": []}',
'type': 'ENUM', 'type': 'ENUM',
}), }),
dict({ dict({
@ -409,6 +421,7 @@
'id': '124', 'id': '124',
'mode': 'rw', 'mode': 'rw',
'name': '拖地模式', 'name': '拖地模式',
'property': '{"range": []}',
'type': 'ENUM', 'type': 'ENUM',
}), }),
dict({ dict({
@ -416,6 +429,7 @@
'id': '125', 'id': '125',
'mode': 'rw', 'mode': 'rw',
'name': '主刷寿命', 'name': '主刷寿命',
'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}',
'type': 'VALUE', 'type': 'VALUE',
}), }),
dict({ dict({
@ -423,6 +437,7 @@
'id': '126', 'id': '126',
'mode': 'rw', 'mode': 'rw',
'name': '边刷寿命', 'name': '边刷寿命',
'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}',
'type': 'VALUE', 'type': 'VALUE',
}), }),
dict({ dict({
@ -430,6 +445,7 @@
'id': '127', 'id': '127',
'mode': 'rw', 'mode': 'rw',
'name': '滤网寿命', 'name': '滤网寿命',
'property': '{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}',
'type': 'VALUE', 'type': 'VALUE',
}), }),
dict({ 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): async def _mock_packages(mock_seventeentrack):
package1 = get_package(status=10) package1 = get_package(status=10)
package2 = get_package( package2 = get_package(
@ -167,3 +189,19 @@ async def _mock_packages(mock_seventeentrack):
package2, package2,
package3, 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, TedeeDataUpdateException,
TedeeLocalAuthException, TedeeLocalAuthException,
) )
from pytedee_async.bridge import TedeeBridge
import pytest import pytest
from homeassistant.components.tedee.const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN 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.const import CONF_HOST, CONF_WEBHOOK_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
@ -134,11 +135,10 @@ async def test_reauth_flow(
assert result["reason"] == "reauth_successful" assert result["reason"] == "reauth_successful"
async def test_reconfigure_flow( async def __do_reconfigure_flow(
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_tedee: MagicMock hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None: ) -> ConfigFlowResult:
"""Test that the reconfigure flow works.""" """Initialize a reconfigure flow."""
mock_config_entry.add_to_hass(hass) mock_config_entry.add_to_hass(hass)
reconfigure_result = await mock_config_entry.start_reconfigure_flow(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["type"] is FlowResultType.FORM
assert reconfigure_result["step_id"] == "reconfigure" 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"], reconfigure_result["flow_id"],
{CONF_LOCAL_ACCESS_TOKEN: LOCAL_ACCESS_TOKEN, CONF_HOST: "192.168.1.43"}, {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["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful" assert result["reason"] == "reconfigure_successful"
@ -162,3 +170,18 @@ async def test_reconfigure_flow(
CONF_LOCAL_ACCESS_TOKEN: LOCAL_ACCESS_TOKEN, CONF_LOCAL_ACCESS_TOKEN: LOCAL_ACCESS_TOKEN,
CONF_WEBHOOK_ID: WEBHOOK_ID, 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) 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( async def test_unique_id_collision_issues(
hass: HomeAssistant, hass: HomeAssistant,
manager: config_entries.ConfigEntries, manager: config_entries.ConfigEntries,
@ -7147,6 +7182,12 @@ async def test_unique_id_collision_issues(
for _ in range(6): for _ in range(6):
test3.append(MockConfigEntry(domain="test3", unique_id="not_unique")) test3.append(MockConfigEntry(domain="test3", unique_id="not_unique"))
await manager.async_add(test3[-1]) 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 # Check we get one issue for domain test2 and one issue for domain test3
assert len(issue_registry.issues) == 2 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"), (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) await manager.async_remove(test2_group_2[1].entry_id)
assert not issue_registry.issues assert not issue_registry.issues