Compare commits
27 commits
dev
...
bump_aioht
Author | SHA1 | Date | |
---|---|---|---|
|
54f0ea113f | ||
|
716c9590c7 | ||
|
f2292c66a6 | ||
|
f4801c5b06 | ||
|
c16fb9c93d | ||
|
da8fc7a2fc | ||
|
864b4d86f2 | ||
|
1bb0ced7c0 | ||
|
2fe4fc908b | ||
|
aa2c3b046f | ||
|
22822cb8aa | ||
|
b71383c997 | ||
|
b0b163df48 | ||
|
35539dbf60 | ||
|
09d03e8edf | ||
|
46e37f3bdd | ||
|
0206c149cf | ||
|
29620ef977 | ||
|
9012b113ad | ||
|
5f5f6cc3d5 | ||
|
7ff501f3ec | ||
|
b0f110b9ab | ||
|
2692bc23a5 | ||
|
1beac5f0f8 | ||
|
ec7ba1b7fd | ||
|
5bd1b0dd9c | ||
|
a2ad4c9cfd |
46 changed files with 606 additions and 173 deletions
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
}
|
||||
),
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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."]
|
||||
}
|
||||
|
|
|
@ -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%]",
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue