mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 04:37:06 +00:00
2024.5.5 (#118049)
This commit is contained in:
commit
c347311851
@ -414,6 +414,9 @@ async def async_from_config_dict(
|
|||||||
start = monotonic()
|
start = monotonic()
|
||||||
|
|
||||||
hass.config_entries = config_entries.ConfigEntries(hass, config)
|
hass.config_entries = config_entries.ConfigEntries(hass, config)
|
||||||
|
# Prime custom component cache early so we know if registry entries are tied
|
||||||
|
# to a custom integration
|
||||||
|
await loader.async_get_custom_components(hass)
|
||||||
await async_load_base_functionality(hass)
|
await async_load_base_functionality(hass)
|
||||||
|
|
||||||
# Set up core.
|
# Set up core.
|
||||||
|
@ -19,5 +19,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/aranet",
|
"documentation": "https://www.home-assistant.io/integrations/aranet",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["aranet4==2.3.3"]
|
"requirements": ["aranet4==2.3.4"]
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
"crownstone_uart"
|
"crownstone_uart"
|
||||||
],
|
],
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"crownstone-cloud==1.4.9",
|
"crownstone-cloud==1.4.11",
|
||||||
"crownstone-sse==2.0.4",
|
"crownstone-sse==2.0.5",
|
||||||
"crownstone-uart==2.1.0",
|
"crownstone-uart==2.1.0",
|
||||||
"pyserial==3.5"
|
"pyserial==3.5"
|
||||||
]
|
]
|
||||||
|
@ -50,9 +50,9 @@ def get_hardware_variant(config_entry: ConfigEntry) -> HardwareVariant:
|
|||||||
return HardwareVariant.from_usb_product_name(config_entry.data["product"])
|
return HardwareVariant.from_usb_product_name(config_entry.data["product"])
|
||||||
|
|
||||||
|
|
||||||
def get_zha_device_path(config_entry: ConfigEntry) -> str:
|
def get_zha_device_path(config_entry: ConfigEntry) -> str | None:
|
||||||
"""Get the device path from a ZHA config entry."""
|
"""Get the device path from a ZHA config entry."""
|
||||||
return cast(str, config_entry.data["device"]["path"])
|
return cast(str | None, config_entry.data.get("device", {}).get("path", None))
|
||||||
|
|
||||||
|
|
||||||
@singleton(OTBR_ADDON_MANAGER_DATA)
|
@singleton(OTBR_ADDON_MANAGER_DATA)
|
||||||
@ -94,6 +94,8 @@ async def guess_firmware_type(hass: HomeAssistant, device_path: str) -> Firmware
|
|||||||
|
|
||||||
for zha_config_entry in hass.config_entries.async_entries(ZHA_DOMAIN):
|
for zha_config_entry in hass.config_entries.async_entries(ZHA_DOMAIN):
|
||||||
zha_path = get_zha_device_path(zha_config_entry)
|
zha_path = get_zha_device_path(zha_config_entry)
|
||||||
|
|
||||||
|
if zha_path is not None:
|
||||||
device_guesses[zha_path].append(
|
device_guesses[zha_path].append(
|
||||||
FirmwareGuess(
|
FirmwareGuess(
|
||||||
is_running=(zha_config_entry.state == ConfigEntryState.LOADED),
|
is_running=(zha_config_entry.state == ConfigEntryState.LOADED),
|
||||||
|
@ -54,7 +54,7 @@ def format_default(value: StateType) -> tuple[StateType, str | None]:
|
|||||||
if value is not None:
|
if value is not None:
|
||||||
# Clean up value and infer unit, e.g. -71dBm, 15 dB
|
# Clean up value and infer unit, e.g. -71dBm, 15 dB
|
||||||
if match := re.match(
|
if match := re.match(
|
||||||
r"([>=<]*)(?P<value>.+?)\s*(?P<unit>[a-zA-Z]+)\s*$", str(value)
|
r"((&[gl]t;|[><])=?)?(?P<value>.+?)\s*(?P<unit>[a-zA-Z]+)\s*$", str(value)
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
value = float(match.group("value"))
|
value = float(match.group("value"))
|
||||||
|
@ -52,7 +52,10 @@ DEFAULT_TRANSITION = 0.2
|
|||||||
# sw version (attributeKey 0/40/10)
|
# sw version (attributeKey 0/40/10)
|
||||||
TRANSITION_BLOCKLIST = (
|
TRANSITION_BLOCKLIST = (
|
||||||
(4488, 514, "1.0", "1.0.0"),
|
(4488, 514, "1.0", "1.0.0"),
|
||||||
|
(4488, 260, "1.0", "1.0.0"),
|
||||||
(5010, 769, "3.0", "1.0.0"),
|
(5010, 769, "3.0", "1.0.0"),
|
||||||
|
(4999, 25057, "1.0", "27.0"),
|
||||||
|
(4448, 36866, "V1", "V1.0.0.5"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -542,6 +542,14 @@ class MQTT:
|
|||||||
|
|
||||||
def _increase_socket_buffer_size(self, sock: SocketType) -> None:
|
def _increase_socket_buffer_size(self, sock: SocketType) -> None:
|
||||||
"""Increase the socket buffer size."""
|
"""Increase the socket buffer size."""
|
||||||
|
if not hasattr(sock, "setsockopt") and hasattr(sock, "_socket"):
|
||||||
|
# The WebsocketWrapper does not wrap setsockopt
|
||||||
|
# so we need to get the underlying socket
|
||||||
|
# Remove this once
|
||||||
|
# https://github.com/eclipse/paho.mqtt.python/pull/843
|
||||||
|
# is available.
|
||||||
|
sock = sock._socket # pylint: disable=protected-access
|
||||||
|
|
||||||
new_buffer_size = PREFERRED_BUFFER_SIZE
|
new_buffer_size = PREFERRED_BUFFER_SIZE
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/philips_js",
|
"documentation": "https://www.home-assistant.io/integrations/philips_js",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["haphilipsjs"],
|
"loggers": ["haphilipsjs"],
|
||||||
"requirements": ["ha-philipsjs==3.1.1"]
|
"requirements": ["ha-philipsjs==3.2.1"]
|
||||||
}
|
}
|
||||||
|
@ -91,13 +91,17 @@ class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity):
|
|||||||
super().__init__(coordinator, device_id)
|
super().__init__(coordinator, device_id)
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
self._attr_unique_id = f"{device_id}-{entity_description.key}"
|
self._attr_unique_id = f"{device_id}-{entity_description.key}"
|
||||||
self._attr_options = self.device[entity_description.options_key]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_option(self) -> str:
|
def current_option(self) -> str:
|
||||||
"""Return the selected entity option to represent the entity state."""
|
"""Return the selected entity option to represent the entity state."""
|
||||||
return self.device[self.entity_description.key]
|
return self.device[self.entity_description.key]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self) -> list[str]:
|
||||||
|
"""Return the available select-options."""
|
||||||
|
return self.device[self.entity_description.options_key]
|
||||||
|
|
||||||
async def async_select_option(self, option: str) -> None:
|
async def async_select_option(self, option: str) -> None:
|
||||||
"""Change to the selected entity option."""
|
"""Change to the selected entity option."""
|
||||||
await self.entity_description.command(
|
await self.entity_description.command(
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pyrisco"],
|
"loggers": ["pyrisco"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pyrisco==0.6.1"]
|
"requirements": ["pyrisco==0.6.2"]
|
||||||
}
|
}
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/rympro",
|
"documentation": "https://www.home-assistant.io/integrations/rympro",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["pyrympro==0.0.7"]
|
"requirements": ["pyrympro==0.0.8"]
|
||||||
}
|
}
|
||||||
|
@ -256,6 +256,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
if (
|
if (
|
||||||
current_entry := await self.async_set_unique_id(mac)
|
current_entry := await self.async_set_unique_id(mac)
|
||||||
) and current_entry.data.get(CONF_HOST) == host:
|
) and current_entry.data.get(CONF_HOST) == host:
|
||||||
|
LOGGER.debug("async_reconnect_soon: host: %s, mac: %s", host, mac)
|
||||||
await async_reconnect_soon(self.hass, current_entry)
|
await async_reconnect_soon(self.hass, current_entry)
|
||||||
if host == INTERNAL_WIFI_AP_IP:
|
if host == INTERNAL_WIFI_AP_IP:
|
||||||
# If the device is broadcasting the internal wifi ap ip
|
# If the device is broadcasting the internal wifi ap ip
|
||||||
|
@ -53,9 +53,11 @@ def get_thumbnail_url_full(
|
|||||||
media_content_type: str,
|
media_content_type: str,
|
||||||
media_content_id: str,
|
media_content_id: str,
|
||||||
media_image_id: str | None = None,
|
media_image_id: str | None = None,
|
||||||
|
item: MusicServiceItem | None = None,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Get thumbnail URL."""
|
"""Get thumbnail URL."""
|
||||||
if is_internal:
|
if is_internal:
|
||||||
|
if not item:
|
||||||
item = get_media(
|
item = get_media(
|
||||||
media.library,
|
media.library,
|
||||||
media_content_id,
|
media_content_id,
|
||||||
@ -255,7 +257,7 @@ def item_payload(item: DidlObject, get_thumbnail_url=None) -> BrowseMedia:
|
|||||||
content_id = get_content_id(item)
|
content_id = get_content_id(item)
|
||||||
thumbnail = None
|
thumbnail = None
|
||||||
if getattr(item, "album_art_uri", None):
|
if getattr(item, "album_art_uri", None):
|
||||||
thumbnail = get_thumbnail_url(media_class, content_id)
|
thumbnail = get_thumbnail_url(media_class, content_id, item=item)
|
||||||
|
|
||||||
return BrowseMedia(
|
return BrowseMedia(
|
||||||
title=item.title,
|
title=item.title,
|
||||||
|
@ -8,6 +8,7 @@ from typing import Any
|
|||||||
from switchbot import (
|
from switchbot import (
|
||||||
SwitchbotAccountConnectionError,
|
SwitchbotAccountConnectionError,
|
||||||
SwitchBotAdvertisement,
|
SwitchBotAdvertisement,
|
||||||
|
SwitchbotApiError,
|
||||||
SwitchbotAuthenticationError,
|
SwitchbotAuthenticationError,
|
||||||
SwitchbotLock,
|
SwitchbotLock,
|
||||||
SwitchbotModel,
|
SwitchbotModel,
|
||||||
@ -33,6 +34,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.data_entry_flow import AbortFlow
|
from homeassistant.data_entry_flow import AbortFlow
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ENCRYPTION_KEY,
|
CONF_ENCRYPTION_KEY,
|
||||||
@ -175,14 +177,19 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
description_placeholders = {}
|
description_placeholders = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
key_details = await self.hass.async_add_executor_job(
|
key_details = await SwitchbotLock.async_retrieve_encryption_key(
|
||||||
SwitchbotLock.retrieve_encryption_key,
|
async_get_clientsession(self.hass),
|
||||||
self._discovered_adv.address,
|
self._discovered_adv.address,
|
||||||
user_input[CONF_USERNAME],
|
user_input[CONF_USERNAME],
|
||||||
user_input[CONF_PASSWORD],
|
user_input[CONF_PASSWORD],
|
||||||
)
|
)
|
||||||
except SwitchbotAccountConnectionError as ex:
|
except (SwitchbotApiError, SwitchbotAccountConnectionError) as ex:
|
||||||
raise AbortFlow("cannot_connect") from ex
|
_LOGGER.debug(
|
||||||
|
"Failed to connect to SwitchBot API: %s", ex, exc_info=True
|
||||||
|
)
|
||||||
|
raise AbortFlow(
|
||||||
|
"api_error", description_placeholders={"error_detail": str(ex)}
|
||||||
|
) from ex
|
||||||
except SwitchbotAuthenticationError as ex:
|
except SwitchbotAuthenticationError as ex:
|
||||||
_LOGGER.debug("Authentication failed: %s", ex, exc_info=True)
|
_LOGGER.debug("Authentication failed: %s", ex, exc_info=True)
|
||||||
errors = {"base": "auth_failed"}
|
errors = {"base": "auth_failed"}
|
||||||
|
@ -39,5 +39,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["switchbot"],
|
"loggers": ["switchbot"],
|
||||||
"requirements": ["PySwitchbot==0.45.0"]
|
"requirements": ["PySwitchbot==0.46.1"]
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
"no_devices_found": "No supported SwitchBot devices found in range; If the device is in range, ensure the scanner has active scanning enabled, as SwitchBot devices cannot be discovered with passive scans. Active scans can be disabled once the device is configured. If you need clarification on whether the device is in-range, download the diagnostics for the integration that provides your Bluetooth adapter or proxy and check if the MAC address of the SwitchBot device is present.",
|
"no_devices_found": "No supported SwitchBot devices found in range; If the device is in range, ensure the scanner has active scanning enabled, as SwitchBot devices cannot be discovered with passive scans. Active scans can be disabled once the device is configured. If you need clarification on whether the device is in-range, download the diagnostics for the integration that provides your Bluetooth adapter or proxy and check if the MAC address of the SwitchBot device is present.",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"api_error": "Error while communicating with SwitchBot API: {error_detail}",
|
||||||
"switchbot_unsupported_type": "Unsupported Switchbot Type."
|
"switchbot_unsupported_type": "Unsupported Switchbot Type."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -27,7 +27,9 @@ from .models import SynologyDSMData
|
|||||||
|
|
||||||
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
||||||
"""Set up Synology media source."""
|
"""Set up Synology media source."""
|
||||||
entries = hass.config_entries.async_entries(DOMAIN)
|
entries = hass.config_entries.async_entries(
|
||||||
|
DOMAIN, include_disabled=False, include_ignore=False
|
||||||
|
)
|
||||||
hass.http.register_view(SynologyDsmMediaView(hass))
|
hass.http.register_view(SynologyDsmMediaView(hass))
|
||||||
return SynologyPhotosMediaSource(hass, entries)
|
return SynologyPhotosMediaSource(hass, entries)
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
"not_connected": "Vehicle not connected",
|
"not_connected": "Vehicle not connected",
|
||||||
"connected": "Vehicle connected",
|
"connected": "Vehicle connected",
|
||||||
"ready": "Ready to charge",
|
"ready": "Ready to charge",
|
||||||
"negociating": "Negociating connection",
|
"negotiating": "Negotiating connection",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"charging_finished": "Charging finished",
|
"charging_finished": "Charging finished",
|
||||||
"waiting_car": "Waiting for car",
|
"waiting_car": "Waiting for car",
|
||||||
|
@ -7,6 +7,6 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["wled==0.17.1"],
|
"requirements": ["wled==0.18.0"],
|
||||||
"zeroconf": ["_wled._tcp.local."]
|
"zeroconf": ["_wled._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -514,6 +514,15 @@ class ConfigEntry:
|
|||||||
|
|
||||||
# Only store setup result as state if it was not forwarded.
|
# Only store setup result as state if it was not forwarded.
|
||||||
if domain_is_integration := self.domain == integration.domain:
|
if domain_is_integration := self.domain == integration.domain:
|
||||||
|
if self.state in (
|
||||||
|
ConfigEntryState.LOADED,
|
||||||
|
ConfigEntryState.SETUP_IN_PROGRESS,
|
||||||
|
):
|
||||||
|
raise OperationNotAllowed(
|
||||||
|
f"The config entry {self.title} ({self.domain}) with entry_id"
|
||||||
|
f" {self.entry_id} cannot be setup because is already loaded in the"
|
||||||
|
f" {self.state} state"
|
||||||
|
)
|
||||||
self._async_set_state(hass, ConfigEntryState.SETUP_IN_PROGRESS, None)
|
self._async_set_state(hass, ConfigEntryState.SETUP_IN_PROGRESS, None)
|
||||||
|
|
||||||
if self.supports_unload is None:
|
if self.supports_unload is None:
|
||||||
@ -709,6 +718,17 @@ class ConfigEntry:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up while holding the setup lock."""
|
"""Set up while holding the setup lock."""
|
||||||
async with self.setup_lock:
|
async with self.setup_lock:
|
||||||
|
if self.state is ConfigEntryState.LOADED:
|
||||||
|
# If something loaded the config entry while
|
||||||
|
# we were waiting for the lock, we should not
|
||||||
|
# set it up again.
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Not setting up %s (%s %s) again, already loaded",
|
||||||
|
self.title,
|
||||||
|
self.domain,
|
||||||
|
self.entry_id,
|
||||||
|
)
|
||||||
|
return
|
||||||
await self.async_setup(hass, integration=integration)
|
await self.async_setup(hass, integration=integration)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -23,7 +23,7 @@ if TYPE_CHECKING:
|
|||||||
APPLICATION_NAME: Final = "HomeAssistant"
|
APPLICATION_NAME: Final = "HomeAssistant"
|
||||||
MAJOR_VERSION: Final = 2024
|
MAJOR_VERSION: Final = 2024
|
||||||
MINOR_VERSION: Final = 5
|
MINOR_VERSION: Final = 5
|
||||||
PATCH_VERSION: Final = "4"
|
PATCH_VERSION: Final = "5"
|
||||||
__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)
|
||||||
|
@ -1766,7 +1766,6 @@ class _TrackUTCTimeChange:
|
|||||||
# time when the timer was scheduled
|
# time when the timer was scheduled
|
||||||
utc_now = time_tracker_utcnow()
|
utc_now = time_tracker_utcnow()
|
||||||
localized_now = dt_util.as_local(utc_now) if self.local else utc_now
|
localized_now = dt_util.as_local(utc_now) if self.local else utc_now
|
||||||
hass.async_run_hass_job(self.job, localized_now, background=True)
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
assert self._pattern_time_change_listener_job is not None
|
assert self._pattern_time_change_listener_job is not None
|
||||||
self._cancel_callback = async_track_point_in_utc_time(
|
self._cancel_callback = async_track_point_in_utc_time(
|
||||||
@ -1774,6 +1773,7 @@ class _TrackUTCTimeChange:
|
|||||||
self._pattern_time_change_listener_job,
|
self._pattern_time_change_listener_job,
|
||||||
self._calculate_next(utc_now + timedelta(seconds=1)),
|
self._calculate_next(utc_now + timedelta(seconds=1)),
|
||||||
)
|
)
|
||||||
|
hass.async_run_hass_job(self.job, localized_now, background=True)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_cancel(self) -> None:
|
def async_cancel(self) -> None:
|
||||||
|
@ -96,6 +96,11 @@ BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = {
|
|||||||
"dreame_vacuum": BlockedIntegration(
|
"dreame_vacuum": BlockedIntegration(
|
||||||
AwesomeVersion("1.0.4"), "crashes Home Assistant"
|
AwesomeVersion("1.0.4"), "crashes Home Assistant"
|
||||||
),
|
),
|
||||||
|
# Added in 2024.5.5 because of
|
||||||
|
# https://github.com/sh00t2kill/dolphin-robot/issues/185
|
||||||
|
"mydolphin_plus": BlockedIntegration(
|
||||||
|
AwesomeVersion("1.0.13"), "crashes Home Assistant"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
DATA_COMPONENTS = "components"
|
DATA_COMPONENTS = "components"
|
||||||
@ -1669,6 +1674,14 @@ def async_get_issue_tracker(
|
|||||||
# If we know nothing about the entity, suggest opening an issue on HA core
|
# If we know nothing about the entity, suggest opening an issue on HA core
|
||||||
return issue_tracker
|
return issue_tracker
|
||||||
|
|
||||||
|
if (
|
||||||
|
not integration
|
||||||
|
and (hass and integration_domain)
|
||||||
|
and (comps_or_future := hass.data.get(DATA_CUSTOM_COMPONENTS))
|
||||||
|
and not isinstance(comps_or_future, asyncio.Future)
|
||||||
|
):
|
||||||
|
integration = comps_or_future.get(integration_domain)
|
||||||
|
|
||||||
if not integration and (hass and integration_domain):
|
if not integration and (hass and integration_domain):
|
||||||
with suppress(IntegrationNotLoaded):
|
with suppress(IntegrationNotLoaded):
|
||||||
integration = async_get_loaded_integration(hass, integration_domain)
|
integration = async_get_loaded_integration(hass, integration_domain)
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2024.5.4"
|
version = "2024.5.5"
|
||||||
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"
|
||||||
|
@ -93,7 +93,7 @@ PyQRCode==1.2.1
|
|||||||
PyRMVtransport==0.3.3
|
PyRMVtransport==0.3.3
|
||||||
|
|
||||||
# homeassistant.components.switchbot
|
# homeassistant.components.switchbot
|
||||||
PySwitchbot==0.45.0
|
PySwitchbot==0.46.1
|
||||||
|
|
||||||
# homeassistant.components.switchmate
|
# homeassistant.components.switchmate
|
||||||
PySwitchmate==0.5.1
|
PySwitchmate==0.5.1
|
||||||
@ -461,7 +461,7 @@ aprslib==0.7.2
|
|||||||
aqualogic==2.6
|
aqualogic==2.6
|
||||||
|
|
||||||
# homeassistant.components.aranet
|
# homeassistant.components.aranet
|
||||||
aranet4==2.3.3
|
aranet4==2.3.4
|
||||||
|
|
||||||
# homeassistant.components.arcam_fmj
|
# homeassistant.components.arcam_fmj
|
||||||
arcam-fmj==1.4.0
|
arcam-fmj==1.4.0
|
||||||
@ -670,10 +670,10 @@ construct==2.10.68
|
|||||||
croniter==2.0.2
|
croniter==2.0.2
|
||||||
|
|
||||||
# homeassistant.components.crownstone
|
# homeassistant.components.crownstone
|
||||||
crownstone-cloud==1.4.9
|
crownstone-cloud==1.4.11
|
||||||
|
|
||||||
# homeassistant.components.crownstone
|
# homeassistant.components.crownstone
|
||||||
crownstone-sse==2.0.4
|
crownstone-sse==2.0.5
|
||||||
|
|
||||||
# homeassistant.components.crownstone
|
# homeassistant.components.crownstone
|
||||||
crownstone-uart==2.1.0
|
crownstone-uart==2.1.0
|
||||||
@ -1029,7 +1029,7 @@ ha-ffmpeg==3.2.0
|
|||||||
ha-iotawattpy==0.1.2
|
ha-iotawattpy==0.1.2
|
||||||
|
|
||||||
# homeassistant.components.philips_js
|
# homeassistant.components.philips_js
|
||||||
ha-philipsjs==3.1.1
|
ha-philipsjs==3.2.1
|
||||||
|
|
||||||
# homeassistant.components.habitica
|
# homeassistant.components.habitica
|
||||||
habitipy==0.2.0
|
habitipy==0.2.0
|
||||||
@ -2093,7 +2093,7 @@ pyrecswitch==1.0.2
|
|||||||
pyrepetierng==0.1.0
|
pyrepetierng==0.1.0
|
||||||
|
|
||||||
# homeassistant.components.risco
|
# homeassistant.components.risco
|
||||||
pyrisco==0.6.1
|
pyrisco==0.6.2
|
||||||
|
|
||||||
# homeassistant.components.rituals_perfume_genie
|
# homeassistant.components.rituals_perfume_genie
|
||||||
pyrituals==0.0.6
|
pyrituals==0.0.6
|
||||||
@ -2102,7 +2102,7 @@ pyrituals==0.0.6
|
|||||||
pyroute2==0.7.5
|
pyroute2==0.7.5
|
||||||
|
|
||||||
# homeassistant.components.rympro
|
# homeassistant.components.rympro
|
||||||
pyrympro==0.0.7
|
pyrympro==0.0.8
|
||||||
|
|
||||||
# homeassistant.components.sabnzbd
|
# homeassistant.components.sabnzbd
|
||||||
pysabnzbd==1.1.1
|
pysabnzbd==1.1.1
|
||||||
@ -2866,7 +2866,7 @@ wiffi==1.1.2
|
|||||||
wirelesstagpy==0.8.1
|
wirelesstagpy==0.8.1
|
||||||
|
|
||||||
# homeassistant.components.wled
|
# homeassistant.components.wled
|
||||||
wled==0.17.1
|
wled==0.18.0
|
||||||
|
|
||||||
# homeassistant.components.wolflink
|
# homeassistant.components.wolflink
|
||||||
wolf-comm==0.0.7
|
wolf-comm==0.0.7
|
||||||
|
@ -81,7 +81,7 @@ PyQRCode==1.2.1
|
|||||||
PyRMVtransport==0.3.3
|
PyRMVtransport==0.3.3
|
||||||
|
|
||||||
# homeassistant.components.switchbot
|
# homeassistant.components.switchbot
|
||||||
PySwitchbot==0.45.0
|
PySwitchbot==0.46.1
|
||||||
|
|
||||||
# homeassistant.components.syncthru
|
# homeassistant.components.syncthru
|
||||||
PySyncThru==0.7.10
|
PySyncThru==0.7.10
|
||||||
@ -422,7 +422,7 @@ apprise==1.7.4
|
|||||||
aprslib==0.7.2
|
aprslib==0.7.2
|
||||||
|
|
||||||
# homeassistant.components.aranet
|
# homeassistant.components.aranet
|
||||||
aranet4==2.3.3
|
aranet4==2.3.4
|
||||||
|
|
||||||
# homeassistant.components.arcam_fmj
|
# homeassistant.components.arcam_fmj
|
||||||
arcam-fmj==1.4.0
|
arcam-fmj==1.4.0
|
||||||
@ -554,10 +554,10 @@ construct==2.10.68
|
|||||||
croniter==2.0.2
|
croniter==2.0.2
|
||||||
|
|
||||||
# homeassistant.components.crownstone
|
# homeassistant.components.crownstone
|
||||||
crownstone-cloud==1.4.9
|
crownstone-cloud==1.4.11
|
||||||
|
|
||||||
# homeassistant.components.crownstone
|
# homeassistant.components.crownstone
|
||||||
crownstone-sse==2.0.4
|
crownstone-sse==2.0.5
|
||||||
|
|
||||||
# homeassistant.components.crownstone
|
# homeassistant.components.crownstone
|
||||||
crownstone-uart==2.1.0
|
crownstone-uart==2.1.0
|
||||||
@ -843,7 +843,7 @@ ha-ffmpeg==3.2.0
|
|||||||
ha-iotawattpy==0.1.2
|
ha-iotawattpy==0.1.2
|
||||||
|
|
||||||
# homeassistant.components.philips_js
|
# homeassistant.components.philips_js
|
||||||
ha-philipsjs==3.1.1
|
ha-philipsjs==3.2.1
|
||||||
|
|
||||||
# homeassistant.components.habitica
|
# homeassistant.components.habitica
|
||||||
habitipy==0.2.0
|
habitipy==0.2.0
|
||||||
@ -1635,7 +1635,7 @@ pyqwikswitch==0.93
|
|||||||
pyrainbird==4.0.2
|
pyrainbird==4.0.2
|
||||||
|
|
||||||
# homeassistant.components.risco
|
# homeassistant.components.risco
|
||||||
pyrisco==0.6.1
|
pyrisco==0.6.2
|
||||||
|
|
||||||
# homeassistant.components.rituals_perfume_genie
|
# homeassistant.components.rituals_perfume_genie
|
||||||
pyrituals==0.0.6
|
pyrituals==0.0.6
|
||||||
@ -1644,7 +1644,7 @@ pyrituals==0.0.6
|
|||||||
pyroute2==0.7.5
|
pyroute2==0.7.5
|
||||||
|
|
||||||
# homeassistant.components.rympro
|
# homeassistant.components.rympro
|
||||||
pyrympro==0.0.7
|
pyrympro==0.0.8
|
||||||
|
|
||||||
# homeassistant.components.sabnzbd
|
# homeassistant.components.sabnzbd
|
||||||
pysabnzbd==1.1.1
|
pysabnzbd==1.1.1
|
||||||
@ -2222,7 +2222,7 @@ whois==0.9.27
|
|||||||
wiffi==1.1.2
|
wiffi==1.1.2
|
||||||
|
|
||||||
# homeassistant.components.wled
|
# homeassistant.components.wled
|
||||||
wled==0.17.1
|
wled==0.18.0
|
||||||
|
|
||||||
# homeassistant.components.wolflink
|
# homeassistant.components.wolflink
|
||||||
wolf-comm==0.0.7
|
wolf-comm==0.0.7
|
||||||
|
@ -94,6 +94,18 @@ def test_get_zha_device_path() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_zha_device_path_ignored_discovery() -> None:
|
||||||
|
"""Test extracting the ZHA device path from an ignored ZHA discovery."""
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain="zha",
|
||||||
|
unique_id="some_unique_id",
|
||||||
|
data={},
|
||||||
|
version=4,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert get_zha_device_path(config_entry) is None
|
||||||
|
|
||||||
|
|
||||||
async def test_guess_firmware_type_unknown(hass: HomeAssistant) -> None:
|
async def test_guess_firmware_type_unknown(hass: HomeAssistant) -> None:
|
||||||
"""Test guessing the firmware type."""
|
"""Test guessing the firmware type."""
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@ from homeassistant.const import (
|
|||||||
("-71 dBm", (-71, SIGNAL_STRENGTH_DECIBELS_MILLIWATT)),
|
("-71 dBm", (-71, SIGNAL_STRENGTH_DECIBELS_MILLIWATT)),
|
||||||
("15dB", (15, SIGNAL_STRENGTH_DECIBELS)),
|
("15dB", (15, SIGNAL_STRENGTH_DECIBELS)),
|
||||||
(">=-51dBm", (-51, SIGNAL_STRENGTH_DECIBELS_MILLIWATT)),
|
(">=-51dBm", (-51, SIGNAL_STRENGTH_DECIBELS_MILLIWATT)),
|
||||||
|
("<-20dB", (-20, SIGNAL_STRENGTH_DECIBELS)),
|
||||||
|
(">=30dB", (30, SIGNAL_STRENGTH_DECIBELS)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_format_default(value, expected) -> None:
|
def test_format_default(value, expected) -> None:
|
||||||
|
@ -4410,6 +4410,43 @@ async def test_server_sock_buffer_size(
|
|||||||
assert "Unable to increase the socket buffer size" in caplog.text
|
assert "Unable to increase the socket buffer size" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0)
|
||||||
|
@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0)
|
||||||
|
@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0)
|
||||||
|
async def test_server_sock_buffer_size_with_websocket(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mqtt_client_mock: MqttMockPahoClient,
|
||||||
|
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test handling the socket buffer size fails."""
|
||||||
|
mqtt_mock = await mqtt_mock_entry()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mqtt_mock.connected is True
|
||||||
|
|
||||||
|
mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS
|
||||||
|
|
||||||
|
client, server = socket.socketpair(
|
||||||
|
family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0
|
||||||
|
)
|
||||||
|
client.setblocking(False)
|
||||||
|
server.setblocking(False)
|
||||||
|
|
||||||
|
class FakeWebsocket(paho_mqtt.WebsocketWrapper):
|
||||||
|
def _do_handshake(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
wrapped_socket = FakeWebsocket(client, "127.0.01", 1, False, "/", None)
|
||||||
|
|
||||||
|
with patch.object(client, "setsockopt", side_effect=OSError("foo")):
|
||||||
|
mqtt_client_mock.on_socket_open(mqtt_client_mock, None, wrapped_socket)
|
||||||
|
mqtt_client_mock.on_socket_register_write(
|
||||||
|
mqtt_client_mock, None, wrapped_socket
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert "Unable to increase the socket buffer size" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0)
|
@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0)
|
||||||
@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0)
|
@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0)
|
||||||
@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0)
|
@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0)
|
||||||
|
@ -316,12 +316,35 @@ def sonos_favorites_fixture() -> SearchResult:
|
|||||||
class MockMusicServiceItem:
|
class MockMusicServiceItem:
|
||||||
"""Mocks a Soco MusicServiceItem."""
|
"""Mocks a Soco MusicServiceItem."""
|
||||||
|
|
||||||
def __init__(self, title: str, item_id: str, parent_id: str, item_class: str):
|
def __init__(
|
||||||
|
self,
|
||||||
|
title: str,
|
||||||
|
item_id: str,
|
||||||
|
parent_id: str,
|
||||||
|
item_class: str,
|
||||||
|
album_art_uri: None | str = None,
|
||||||
|
):
|
||||||
"""Initialize the mock item."""
|
"""Initialize the mock item."""
|
||||||
self.title = title
|
self.title = title
|
||||||
self.item_id = item_id
|
self.item_id = item_id
|
||||||
self.item_class = item_class
|
self.item_class = item_class
|
||||||
self.parent_id = parent_id
|
self.parent_id = parent_id
|
||||||
|
self.album_art_uri: None | str = album_art_uri
|
||||||
|
|
||||||
|
|
||||||
|
def list_from_json_fixture(file_name: str) -> list[MockMusicServiceItem]:
|
||||||
|
"""Create a list of music service items from a json fixture file."""
|
||||||
|
item_list = load_json_value_fixture(file_name, "sonos")
|
||||||
|
return [
|
||||||
|
MockMusicServiceItem(
|
||||||
|
item.get("title"),
|
||||||
|
item.get("item_id"),
|
||||||
|
item.get("parent_id"),
|
||||||
|
item.get("item_class"),
|
||||||
|
item.get("album_art_uri"),
|
||||||
|
)
|
||||||
|
for item in item_list
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def mock_browse_by_idstring(
|
def mock_browse_by_idstring(
|
||||||
@ -398,6 +421,10 @@ def mock_browse_by_idstring(
|
|||||||
"object.container.album.musicAlbum",
|
"object.container.album.musicAlbum",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
if search_type == "tracks":
|
||||||
|
return list_from_json_fixture("music_library_tracks.json")
|
||||||
|
if search_type == "albums" and idstring == "A:ALBUM":
|
||||||
|
return list_from_json_fixture("music_library_albums.json")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
@ -416,13 +443,23 @@ def mock_get_music_library_information(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="music_library_browse_categories")
|
||||||
|
def music_library_browse_categories() -> list[MockMusicServiceItem]:
|
||||||
|
"""Create fixture for top-level music library categories."""
|
||||||
|
return list_from_json_fixture("music_library_categories.json")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="music_library")
|
@pytest.fixture(name="music_library")
|
||||||
def music_library_fixture(sonos_favorites: SearchResult) -> Mock:
|
def music_library_fixture(
|
||||||
|
sonos_favorites: SearchResult,
|
||||||
|
music_library_browse_categories: list[MockMusicServiceItem],
|
||||||
|
) -> Mock:
|
||||||
"""Create music_library fixture."""
|
"""Create music_library fixture."""
|
||||||
music_library = MagicMock()
|
music_library = MagicMock()
|
||||||
music_library.get_sonos_favorites.return_value = sonos_favorites
|
music_library.get_sonos_favorites.return_value = sonos_favorites
|
||||||
music_library.browse_by_idstring = mock_browse_by_idstring
|
music_library.browse_by_idstring = Mock(side_effect=mock_browse_by_idstring)
|
||||||
music_library.get_music_library_information = mock_get_music_library_information
|
music_library.get_music_library_information = mock_get_music_library_information
|
||||||
|
music_library.browse = Mock(return_value=music_library_browse_categories)
|
||||||
return music_library
|
return music_library
|
||||||
|
|
||||||
|
|
||||||
|
23
tests/components/sonos/fixtures/music_library_albums.json
Normal file
23
tests/components/sonos/fixtures/music_library_albums.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "A Hard Day's Night",
|
||||||
|
"item_id": "A:ALBUM/A%20Hard%20Day's%20Night",
|
||||||
|
"parent_id": "A:ALBUM",
|
||||||
|
"item_class": "object.container.album.musicAlbum",
|
||||||
|
"album_art_uri": "http://192.168.42.2:1400/getaa?u=x-file-cifs%3a%2f%2f192.168.42.100%2fmusic%2fThe%2520Beatles%2fA%2520Hard%2520Day's%2520Night%2f01%2520A%2520Hard%2520Day's%2520Night%25201.m4a&v=53"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Abbey Road",
|
||||||
|
"item_id": "A:ALBUM/Abbey%20Road",
|
||||||
|
"parent_id": "A:ALBUM",
|
||||||
|
"item_class": "object.container.album.musicAlbum",
|
||||||
|
"album_art_uri": "http://192.168.42.2:1400/getaa?u=x-file-cifs%3a%2f%2f192.168.42.100%2fmusic%2fThe%2520Beatles%2fAbbeyA%2520Road%2f01%2520Come%2520Together.m4a&v=53"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Between Good And Evil",
|
||||||
|
"item_id": "A:ALBUM/Between%20Good%20And%20Evil",
|
||||||
|
"parent_id": "A:ALBUM",
|
||||||
|
"item_class": "object.container.album.musicAlbum",
|
||||||
|
"album_art_uri": "http://192.168.42.2:1400/getaa?u=x-file-cifs%3a%2f%2f192.168.42.100%2fmusic%2fSantana%2fA%2520Between%2520Good%2520And%2520Evil%2f02%2520A%2520Persuasion.m4a&v=53"
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,44 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "Contributing Artists",
|
||||||
|
"item_id": "A:ARTIST",
|
||||||
|
"parent_id": "A:",
|
||||||
|
"item_class": "object.container"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Artists",
|
||||||
|
"item_id": "A:ALBUMARTIST",
|
||||||
|
"parent_id": "A:",
|
||||||
|
"item_class": "object.container"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Albums",
|
||||||
|
"item_id": "A:ALBUM",
|
||||||
|
"parent_id": "A:",
|
||||||
|
"item_class": "object.container"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Genres",
|
||||||
|
"item_id": "A:GENRE",
|
||||||
|
"parent_id": "A:",
|
||||||
|
"item_class": "object.container"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Composers",
|
||||||
|
"item_id": "A:COMPOSER",
|
||||||
|
"parent_id": "A:",
|
||||||
|
"item_class": "object.container"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Tracks",
|
||||||
|
"item_id": "A:TRACKS",
|
||||||
|
"parent_id": "A:",
|
||||||
|
"item_class": "object.container.playlistContainer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Playlists",
|
||||||
|
"item_id": "A:PLAYLISTS",
|
||||||
|
"parent_id": "A:",
|
||||||
|
"item_class": "object.container"
|
||||||
|
}
|
||||||
|
]
|
14
tests/components/sonos/fixtures/music_library_tracks.json
Normal file
14
tests/components/sonos/fixtures/music_library_tracks.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "A Hard Day's Night",
|
||||||
|
"item_id": "S://192.168.42.100/music/iTunes/The%20Beatles/A%20Hard%20Day%2fs%20Night/A%20Hard%20Day%2fs%20Night.mp3",
|
||||||
|
"parent_id": "A:ALBUMARTIST/Beatles/A%20Hard%20Day's%20Night",
|
||||||
|
"item_class": "object.container.album.musicTrack"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "I Should Have Known Better",
|
||||||
|
"item_id": "S://192.168.42.100/music/iTunes/The%20Beatles/A%20Hard%20Day%2fs%I%20Should%20Have%20Known%20Better.mp3",
|
||||||
|
"parent_id": "A:ALBUMARTIST/Beatles/A%20Hard%20Day's%20Night",
|
||||||
|
"item_class": "object.container.album.musicTrack"
|
||||||
|
}
|
||||||
|
]
|
133
tests/components/sonos/snapshots/test_media_browser.ambr
Normal file
133
tests/components/sonos/snapshots/test_media_browser.ambr
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_browse_media_library
|
||||||
|
list([
|
||||||
|
dict({
|
||||||
|
'can_expand': True,
|
||||||
|
'can_play': False,
|
||||||
|
'children_media_class': None,
|
||||||
|
'media_class': 'contributing_artist',
|
||||||
|
'media_content_id': 'A:ARTIST',
|
||||||
|
'media_content_type': 'contributing_artist',
|
||||||
|
'thumbnail': None,
|
||||||
|
'title': 'Contributing Artists',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'can_expand': True,
|
||||||
|
'can_play': False,
|
||||||
|
'children_media_class': None,
|
||||||
|
'media_class': 'artist',
|
||||||
|
'media_content_id': 'A:ALBUMARTIST',
|
||||||
|
'media_content_type': 'artist',
|
||||||
|
'thumbnail': None,
|
||||||
|
'title': 'Artists',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'can_expand': True,
|
||||||
|
'can_play': False,
|
||||||
|
'children_media_class': None,
|
||||||
|
'media_class': 'album',
|
||||||
|
'media_content_id': 'A:ALBUM',
|
||||||
|
'media_content_type': 'album',
|
||||||
|
'thumbnail': None,
|
||||||
|
'title': 'Albums',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'can_expand': True,
|
||||||
|
'can_play': False,
|
||||||
|
'children_media_class': None,
|
||||||
|
'media_class': 'genre',
|
||||||
|
'media_content_id': 'A:GENRE',
|
||||||
|
'media_content_type': 'genre',
|
||||||
|
'thumbnail': None,
|
||||||
|
'title': 'Genres',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'can_expand': True,
|
||||||
|
'can_play': False,
|
||||||
|
'children_media_class': None,
|
||||||
|
'media_class': 'composer',
|
||||||
|
'media_content_id': 'A:COMPOSER',
|
||||||
|
'media_content_type': 'composer',
|
||||||
|
'thumbnail': None,
|
||||||
|
'title': 'Composers',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'can_expand': True,
|
||||||
|
'can_play': True,
|
||||||
|
'children_media_class': None,
|
||||||
|
'media_class': 'track',
|
||||||
|
'media_content_id': 'A:TRACKS',
|
||||||
|
'media_content_type': 'track',
|
||||||
|
'thumbnail': None,
|
||||||
|
'title': 'Tracks',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'can_expand': True,
|
||||||
|
'can_play': False,
|
||||||
|
'children_media_class': None,
|
||||||
|
'media_class': 'playlist',
|
||||||
|
'media_content_id': 'A:PLAYLISTS',
|
||||||
|
'media_content_type': 'playlist',
|
||||||
|
'thumbnail': None,
|
||||||
|
'title': 'Playlists',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
# ---
|
||||||
|
# name: test_browse_media_library_albums
|
||||||
|
list([
|
||||||
|
dict({
|
||||||
|
'can_expand': True,
|
||||||
|
'can_play': True,
|
||||||
|
'children_media_class': None,
|
||||||
|
'media_class': 'album',
|
||||||
|
'media_content_id': "A:ALBUM/A%20Hard%20Day's%20Night",
|
||||||
|
'media_content_type': 'album',
|
||||||
|
'thumbnail': "http://192.168.42.2:1400/getaa?u=x-file-cifs://192.168.42.100/music/The%20Beatles/A%20Hard%20Day's%20Night/01%20A%20Hard%20Day's%20Night%201.m4a&v=53",
|
||||||
|
'title': "A Hard Day's Night",
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'can_expand': True,
|
||||||
|
'can_play': True,
|
||||||
|
'children_media_class': None,
|
||||||
|
'media_class': 'album',
|
||||||
|
'media_content_id': 'A:ALBUM/Abbey%20Road',
|
||||||
|
'media_content_type': 'album',
|
||||||
|
'thumbnail': 'http://192.168.42.2:1400/getaa?u=x-file-cifs://192.168.42.100/music/The%20Beatles/AbbeyA%20Road/01%20Come%20Together.m4a&v=53',
|
||||||
|
'title': 'Abbey Road',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'can_expand': True,
|
||||||
|
'can_play': True,
|
||||||
|
'children_media_class': None,
|
||||||
|
'media_class': 'album',
|
||||||
|
'media_content_id': 'A:ALBUM/Between%20Good%20And%20Evil',
|
||||||
|
'media_content_type': 'album',
|
||||||
|
'thumbnail': 'http://192.168.42.2:1400/getaa?u=x-file-cifs://192.168.42.100/music/Santana/A%20Between%20Good%20And%20Evil/02%20A%20Persuasion.m4a&v=53',
|
||||||
|
'title': 'Between Good And Evil',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
# ---
|
||||||
|
# name: test_browse_media_root
|
||||||
|
list([
|
||||||
|
dict({
|
||||||
|
'can_expand': True,
|
||||||
|
'can_play': False,
|
||||||
|
'children_media_class': None,
|
||||||
|
'media_class': 'directory',
|
||||||
|
'media_content_id': '',
|
||||||
|
'media_content_type': 'favorites',
|
||||||
|
'thumbnail': 'https://brands.home-assistant.io/_/sonos/logo.png',
|
||||||
|
'title': 'Favorites',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'can_expand': True,
|
||||||
|
'can_play': False,
|
||||||
|
'children_media_class': None,
|
||||||
|
'media_class': 'directory',
|
||||||
|
'media_content_id': '',
|
||||||
|
'media_content_type': 'library',
|
||||||
|
'thumbnail': 'https://brands.home-assistant.io/_/sonos/logo.png',
|
||||||
|
'title': 'Music Library',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
# ---
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.components.media_player.browse_media import BrowseMedia
|
from homeassistant.components.media_player.browse_media import BrowseMedia
|
||||||
from homeassistant.components.media_player.const import MediaClass, MediaType
|
from homeassistant.components.media_player.const import MediaClass, MediaType
|
||||||
from homeassistant.components.sonos.media_browser import (
|
from homeassistant.components.sonos.media_browser import (
|
||||||
@ -12,6 +14,8 @@ from homeassistant.core import HomeAssistant
|
|||||||
|
|
||||||
from .conftest import SoCoMockFactory
|
from .conftest import SoCoMockFactory
|
||||||
|
|
||||||
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
|
|
||||||
class MockMusicServiceItem:
|
class MockMusicServiceItem:
|
||||||
"""Mocks a Soco MusicServiceItem."""
|
"""Mocks a Soco MusicServiceItem."""
|
||||||
@ -95,3 +99,81 @@ async def test_build_item_response(
|
|||||||
browse_item.children[1].media_content_id
|
browse_item.children[1].media_content_id
|
||||||
== "x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3"
|
== "x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_browse_media_root(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
soco_factory: SoCoMockFactory,
|
||||||
|
async_autosetup_sonos,
|
||||||
|
soco,
|
||||||
|
discover,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test the async_browse_media method."""
|
||||||
|
|
||||||
|
client = await hass_ws_client()
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "media_player/browse_media",
|
||||||
|
"entity_id": "media_player.zone_a",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await client.receive_json()
|
||||||
|
assert response["success"]
|
||||||
|
assert response["result"]["children"] == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
async def test_browse_media_library(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
soco_factory: SoCoMockFactory,
|
||||||
|
async_autosetup_sonos,
|
||||||
|
soco,
|
||||||
|
discover,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test the async_browse_media method."""
|
||||||
|
|
||||||
|
client = await hass_ws_client()
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "media_player/browse_media",
|
||||||
|
"entity_id": "media_player.zone_a",
|
||||||
|
"media_content_id": "",
|
||||||
|
"media_content_type": "library",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await client.receive_json()
|
||||||
|
assert response["success"]
|
||||||
|
assert response["result"]["children"] == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
async def test_browse_media_library_albums(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
soco_factory: SoCoMockFactory,
|
||||||
|
async_autosetup_sonos,
|
||||||
|
soco,
|
||||||
|
discover,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test the async_browse_media method."""
|
||||||
|
soco_mock = soco_factory.mock_list.get("192.168.42.2")
|
||||||
|
|
||||||
|
client = await hass_ws_client()
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "media_player/browse_media",
|
||||||
|
"entity_id": "media_player.zone_a",
|
||||||
|
"media_content_id": "A:ALBUM",
|
||||||
|
"media_content_type": "album",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = await client.receive_json()
|
||||||
|
assert response["success"]
|
||||||
|
assert response["result"]["children"] == snapshot
|
||||||
|
assert soco_mock.music_library.browse_by_idstring.call_count == 1
|
||||||
|
@ -487,7 +487,7 @@ async def test_user_setup_wolock_auth(hass: HomeAssistant) -> None:
|
|||||||
assert result["errors"] == {}
|
assert result["errors"] == {}
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key",
|
"homeassistant.components.switchbot.config_flow.SwitchbotLock.async_retrieve_encryption_key",
|
||||||
side_effect=SwitchbotAuthenticationError("error from api"),
|
side_effect=SwitchbotAuthenticationError("error from api"),
|
||||||
):
|
):
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
@ -510,7 +510,7 @@ async def test_user_setup_wolock_auth(hass: HomeAssistant) -> None:
|
|||||||
return_value=True,
|
return_value=True,
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key",
|
"homeassistant.components.switchbot.config_flow.SwitchbotLock.async_retrieve_encryption_key",
|
||||||
return_value={
|
return_value={
|
||||||
CONF_KEY_ID: "ff",
|
CONF_KEY_ID: "ff",
|
||||||
CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
|
CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
|
||||||
@ -560,8 +560,8 @@ async def test_user_setup_wolock_auth_switchbot_api_down(hass: HomeAssistant) ->
|
|||||||
assert result["errors"] == {}
|
assert result["errors"] == {}
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key",
|
"homeassistant.components.switchbot.config_flow.SwitchbotLock.async_retrieve_encryption_key",
|
||||||
side_effect=SwitchbotAccountConnectionError,
|
side_effect=SwitchbotAccountConnectionError("Switchbot API down"),
|
||||||
):
|
):
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
@ -572,7 +572,8 @@ async def test_user_setup_wolock_auth_switchbot_api_down(hass: HomeAssistant) ->
|
|||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "cannot_connect"
|
assert result["reason"] == "api_error"
|
||||||
|
assert result["description_placeholders"] == {"error_detail": "Switchbot API down"}
|
||||||
|
|
||||||
|
|
||||||
async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None:
|
async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None:
|
||||||
|
@ -196,7 +196,7 @@ async def test_flow_ssdp_discovery_changed_udn_match_mac(hass: HomeAssistant) ->
|
|||||||
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
|
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
|
||||||
},
|
},
|
||||||
source=config_entries.SOURCE_SSDP,
|
source=config_entries.SOURCE_SSDP,
|
||||||
state=config_entries.ConfigEntryState.LOADED,
|
state=config_entries.ConfigEntryState.NOT_LOADED,
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
@ -228,7 +228,7 @@ async def test_flow_ssdp_discovery_changed_udn_match_host(hass: HomeAssistant) -
|
|||||||
CONFIG_ENTRY_HOST: TEST_HOST,
|
CONFIG_ENTRY_HOST: TEST_HOST,
|
||||||
},
|
},
|
||||||
source=config_entries.SOURCE_SSDP,
|
source=config_entries.SOURCE_SSDP,
|
||||||
state=config_entries.ConfigEntryState.LOADED,
|
state=config_entries.ConfigEntryState.NOT_LOADED,
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
@ -266,7 +266,7 @@ async def test_flow_ssdp_discovery_changed_udn_but_st_differs(
|
|||||||
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
|
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
|
||||||
},
|
},
|
||||||
source=config_entries.SOURCE_SSDP,
|
source=config_entries.SOURCE_SSDP,
|
||||||
state=config_entries.ConfigEntryState.LOADED,
|
state=config_entries.ConfigEntryState.NOT_LOADED,
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
@ -320,7 +320,7 @@ async def test_flow_ssdp_discovery_changed_location(hass: HomeAssistant) -> None
|
|||||||
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
|
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
|
||||||
},
|
},
|
||||||
source=config_entries.SOURCE_SSDP,
|
source=config_entries.SOURCE_SSDP,
|
||||||
state=config_entries.ConfigEntryState.LOADED,
|
state=config_entries.ConfigEntryState.NOT_LOADED,
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
@ -4589,6 +4589,40 @@ async def test_async_track_point_in_time_cancel(hass: HomeAssistant) -> None:
|
|||||||
assert "US/Hawaii" in str(times[0].tzinfo)
|
assert "US/Hawaii" in str(times[0].tzinfo)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_track_point_in_time_cancel_in_job(
|
||||||
|
hass: HomeAssistant, freezer: FrozenDateTimeFactory
|
||||||
|
) -> None:
|
||||||
|
"""Test cancel of async track point in time during job execution."""
|
||||||
|
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
times = []
|
||||||
|
|
||||||
|
time_that_will_not_match_right_away = datetime(
|
||||||
|
now.year + 1, 5, 24, 11, 59, 55, tzinfo=dt_util.UTC
|
||||||
|
)
|
||||||
|
freezer.move_to(time_that_will_not_match_right_away)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def action(x: datetime):
|
||||||
|
nonlocal times
|
||||||
|
times.append(x)
|
||||||
|
unsub()
|
||||||
|
|
||||||
|
unsub = async_track_utc_time_change(hass, action, minute=0, second="*")
|
||||||
|
|
||||||
|
async_fire_time_changed(
|
||||||
|
hass, datetime(now.year + 1, 5, 24, 12, 0, 0, 999999, tzinfo=dt_util.UTC)
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(times) == 1
|
||||||
|
|
||||||
|
async_fire_time_changed(
|
||||||
|
hass, datetime(now.year + 1, 5, 24, 13, 0, 0, 999999, tzinfo=dt_util.UTC)
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(times) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_async_track_entity_registry_updated_event(hass: HomeAssistant) -> None:
|
async def test_async_track_entity_registry_updated_event(hass: HomeAssistant) -> None:
|
||||||
"""Test tracking entity registry updates for an entity_id."""
|
"""Test tracking entity registry updates for an entity_id."""
|
||||||
|
|
||||||
|
@ -90,6 +90,89 @@ async def manager(hass: HomeAssistant) -> config_entries.ConfigEntries:
|
|||||||
return manager
|
return manager
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_race_only_setup_once(hass: HomeAssistant) -> None:
|
||||||
|
"""Test ensure that config entries are only setup once."""
|
||||||
|
attempts = 0
|
||||||
|
slow_config_entry_setup_future = hass.loop.create_future()
|
||||||
|
fast_config_entry_setup_future = hass.loop.create_future()
|
||||||
|
slow_setup_future = hass.loop.create_future()
|
||||||
|
|
||||||
|
async def async_setup(hass, config):
|
||||||
|
"""Mock setup."""
|
||||||
|
await slow_setup_future
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry):
|
||||||
|
"""Mock setup entry."""
|
||||||
|
slow = entry.data["slow"]
|
||||||
|
if slow:
|
||||||
|
await slow_config_entry_setup_future
|
||||||
|
return True
|
||||||
|
nonlocal attempts
|
||||||
|
attempts += 1
|
||||||
|
if attempts == 1:
|
||||||
|
raise ConfigEntryNotReady
|
||||||
|
await fast_config_entry_setup_future
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, entry):
|
||||||
|
"""Mock unload entry."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
mock_integration(
|
||||||
|
hass,
|
||||||
|
MockModule(
|
||||||
|
"comp",
|
||||||
|
async_setup=async_setup,
|
||||||
|
async_setup_entry=async_setup_entry,
|
||||||
|
async_unload_entry=async_unload_entry,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mock_platform(hass, "comp.config_flow", None)
|
||||||
|
|
||||||
|
entry = MockConfigEntry(domain="comp", data={"slow": False})
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
entry2 = MockConfigEntry(domain="comp", data={"slow": True})
|
||||||
|
entry2.add_to_hass(hass)
|
||||||
|
await entry2.setup_lock.acquire()
|
||||||
|
|
||||||
|
async def _async_reload_entry(entry: MockConfigEntry):
|
||||||
|
async with entry.setup_lock:
|
||||||
|
await entry.async_unload(hass)
|
||||||
|
await entry.async_setup(hass)
|
||||||
|
|
||||||
|
hass.async_create_task(_async_reload_entry(entry2))
|
||||||
|
|
||||||
|
setup_task = hass.async_create_task(async_setup_component(hass, "comp", {}))
|
||||||
|
entry2.setup_lock.release()
|
||||||
|
|
||||||
|
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
|
||||||
|
assert entry2.state is config_entries.ConfigEntryState.NOT_LOADED
|
||||||
|
|
||||||
|
assert "comp" not in hass.config.components
|
||||||
|
slow_setup_future.set_result(None)
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
assert "comp" in hass.config.components
|
||||||
|
|
||||||
|
assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY
|
||||||
|
assert entry2.state is config_entries.ConfigEntryState.SETUP_IN_PROGRESS
|
||||||
|
|
||||||
|
fast_config_entry_setup_future.set_result(None)
|
||||||
|
# Make sure setup retry is started
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5))
|
||||||
|
slow_config_entry_setup_future.set_result(None)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.state is config_entries.ConfigEntryState.LOADED
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert attempts == 2
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert setup_task.done()
|
||||||
|
assert entry2.state is config_entries.ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
|
||||||
async def test_call_setup_entry(hass: HomeAssistant) -> None:
|
async def test_call_setup_entry(hass: HomeAssistant) -> None:
|
||||||
"""Test we call <component>.setup_entry."""
|
"""Test we call <component>.setup_entry."""
|
||||||
entry = MockConfigEntry(domain="comp")
|
entry = MockConfigEntry(domain="comp")
|
||||||
@ -386,7 +469,7 @@ async def test_remove_entry(
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Setup entry
|
# Setup entry
|
||||||
await entry.async_setup(hass)
|
await manager.async_setup(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# Check entity state got added
|
# Check entity state got added
|
||||||
@ -1613,7 +1696,9 @@ async def test_entry_reload_succeed(
|
|||||||
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that we can reload an entry."""
|
"""Test that we can reload an entry."""
|
||||||
entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED)
|
entry = MockConfigEntry(
|
||||||
|
domain="comp", state=config_entries.ConfigEntryState.NOT_LOADED
|
||||||
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
async_setup = AsyncMock(return_value=True)
|
async_setup = AsyncMock(return_value=True)
|
||||||
@ -1637,6 +1722,42 @@ async def test_entry_reload_succeed(
|
|||||||
assert entry.state is config_entries.ConfigEntryState.LOADED
|
assert entry.state is config_entries.ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"state",
|
||||||
|
[
|
||||||
|
config_entries.ConfigEntryState.LOADED,
|
||||||
|
config_entries.ConfigEntryState.SETUP_IN_PROGRESS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_entry_cannot_be_loaded_twice(
|
||||||
|
hass: HomeAssistant, state: config_entries.ConfigEntryState
|
||||||
|
) -> None:
|
||||||
|
"""Test that a config entry cannot be loaded twice."""
|
||||||
|
entry = MockConfigEntry(domain="comp", state=state)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
async_setup = AsyncMock(return_value=True)
|
||||||
|
async_setup_entry = AsyncMock(return_value=True)
|
||||||
|
async_unload_entry = AsyncMock(return_value=True)
|
||||||
|
|
||||||
|
mock_integration(
|
||||||
|
hass,
|
||||||
|
MockModule(
|
||||||
|
"comp",
|
||||||
|
async_setup=async_setup,
|
||||||
|
async_setup_entry=async_setup_entry,
|
||||||
|
async_unload_entry=async_unload_entry,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mock_platform(hass, "comp.config_flow", None)
|
||||||
|
|
||||||
|
with pytest.raises(config_entries.OperationNotAllowed, match=str(state)):
|
||||||
|
await entry.async_setup(hass)
|
||||||
|
assert len(async_setup.mock_calls) == 0
|
||||||
|
assert len(async_setup_entry.mock_calls) == 0
|
||||||
|
assert entry.state is state
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"state",
|
"state",
|
||||||
[
|
[
|
||||||
@ -4005,7 +4126,9 @@ async def test_entry_reload_concurrency_not_setup_setup(
|
|||||||
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test multiple reload calls do not cause a reload race."""
|
"""Test multiple reload calls do not cause a reload race."""
|
||||||
entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED)
|
entry = MockConfigEntry(
|
||||||
|
domain="comp", state=config_entries.ConfigEntryState.NOT_LOADED
|
||||||
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
async_setup = AsyncMock(return_value=True)
|
async_setup = AsyncMock(return_value=True)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -1108,14 +1109,18 @@ CUSTOM_ISSUE_TRACKER = "https://blablabla.com"
|
|||||||
# Integration domain is not currently deduced from module
|
# Integration domain is not currently deduced from module
|
||||||
(None, "homeassistant.components.hue.sensor", CORE_ISSUE_TRACKER),
|
(None, "homeassistant.components.hue.sensor", CORE_ISSUE_TRACKER),
|
||||||
("hue", "homeassistant.components.mqtt.sensor", CORE_ISSUE_TRACKER_HUE),
|
("hue", "homeassistant.components.mqtt.sensor", CORE_ISSUE_TRACKER_HUE),
|
||||||
# Custom integration with known issue tracker
|
# Loaded custom integration with known issue tracker
|
||||||
("bla_custom", "custom_components.bla_custom.sensor", CUSTOM_ISSUE_TRACKER),
|
("bla_custom", "custom_components.bla_custom.sensor", CUSTOM_ISSUE_TRACKER),
|
||||||
("bla_custom", None, CUSTOM_ISSUE_TRACKER),
|
("bla_custom", None, CUSTOM_ISSUE_TRACKER),
|
||||||
# Custom integration without known issue tracker
|
# Loaded custom integration without known issue tracker
|
||||||
(None, "custom_components.bla.sensor", None),
|
(None, "custom_components.bla.sensor", None),
|
||||||
("bla_custom_no_tracker", "custom_components.bla_custom.sensor", None),
|
("bla_custom_no_tracker", "custom_components.bla_custom.sensor", None),
|
||||||
("bla_custom_no_tracker", None, None),
|
("bla_custom_no_tracker", None, None),
|
||||||
("hue", "custom_components.bla.sensor", None),
|
("hue", "custom_components.bla.sensor", None),
|
||||||
|
# Unloaded custom integration with known issue tracker
|
||||||
|
("bla_custom_not_loaded", None, CUSTOM_ISSUE_TRACKER),
|
||||||
|
# Unloaded custom integration without known issue tracker
|
||||||
|
("bla_custom_not_loaded_no_tracker", None, None),
|
||||||
# Integration domain has priority over module
|
# Integration domain has priority over module
|
||||||
("bla_custom_no_tracker", "homeassistant.components.bla_custom.sensor", None),
|
("bla_custom_no_tracker", "homeassistant.components.bla_custom.sensor", None),
|
||||||
],
|
],
|
||||||
@ -1133,6 +1138,32 @@ async def test_async_get_issue_tracker(
|
|||||||
built_in=False,
|
built_in=False,
|
||||||
)
|
)
|
||||||
mock_integration(hass, MockModule("bla_custom_no_tracker"), built_in=False)
|
mock_integration(hass, MockModule("bla_custom_no_tracker"), built_in=False)
|
||||||
|
|
||||||
|
cust_unloaded_module = MockModule(
|
||||||
|
"bla_custom_not_loaded",
|
||||||
|
partial_manifest={"issue_tracker": CUSTOM_ISSUE_TRACKER},
|
||||||
|
)
|
||||||
|
cust_unloaded = loader.Integration(
|
||||||
|
hass,
|
||||||
|
f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{cust_unloaded_module.DOMAIN}",
|
||||||
|
pathlib.Path(""),
|
||||||
|
cust_unloaded_module.mock_manifest(),
|
||||||
|
set(),
|
||||||
|
)
|
||||||
|
|
||||||
|
cust_unloaded_no_tracker_module = MockModule("bla_custom_not_loaded_no_tracker")
|
||||||
|
cust_unloaded_no_tracker = loader.Integration(
|
||||||
|
hass,
|
||||||
|
f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{cust_unloaded_no_tracker_module.DOMAIN}",
|
||||||
|
pathlib.Path(""),
|
||||||
|
cust_unloaded_no_tracker_module.mock_manifest(),
|
||||||
|
set(),
|
||||||
|
)
|
||||||
|
hass.data["custom_components"] = {
|
||||||
|
"bla_custom_not_loaded": cust_unloaded,
|
||||||
|
"bla_custom_not_loaded_no_tracker": cust_unloaded_no_tracker,
|
||||||
|
}
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
loader.async_get_issue_tracker(hass, integration_domain=domain, module=module)
|
loader.async_get_issue_tracker(hass, integration_domain=domain, module=module)
|
||||||
== issue_tracker
|
== issue_tracker
|
||||||
|
Loading…
x
Reference in New Issue
Block a user