mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +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()
|
||||
|
||||
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)
|
||||
|
||||
# Set up core.
|
||||
|
@ -19,5 +19,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aranet",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["aranet4==2.3.3"]
|
||||
"requirements": ["aranet4==2.3.4"]
|
||||
}
|
||||
|
@ -13,8 +13,8 @@
|
||||
"crownstone_uart"
|
||||
],
|
||||
"requirements": [
|
||||
"crownstone-cloud==1.4.9",
|
||||
"crownstone-sse==2.0.4",
|
||||
"crownstone-cloud==1.4.11",
|
||||
"crownstone-sse==2.0.5",
|
||||
"crownstone-uart==2.1.0",
|
||||
"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"])
|
||||
|
||||
|
||||
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."""
|
||||
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)
|
||||
@ -94,13 +94,15 @@ async def guess_firmware_type(hass: HomeAssistant, device_path: str) -> Firmware
|
||||
|
||||
for zha_config_entry in hass.config_entries.async_entries(ZHA_DOMAIN):
|
||||
zha_path = get_zha_device_path(zha_config_entry)
|
||||
device_guesses[zha_path].append(
|
||||
FirmwareGuess(
|
||||
is_running=(zha_config_entry.state == ConfigEntryState.LOADED),
|
||||
firmware_type=ApplicationType.EZSP,
|
||||
source="zha",
|
||||
|
||||
if zha_path is not None:
|
||||
device_guesses[zha_path].append(
|
||||
FirmwareGuess(
|
||||
is_running=(zha_config_entry.state == ConfigEntryState.LOADED),
|
||||
firmware_type=ApplicationType.EZSP,
|
||||
source="zha",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if is_hassio(hass):
|
||||
otbr_addon_manager = get_otbr_addon_manager(hass)
|
||||
|
@ -54,7 +54,7 @@ def format_default(value: StateType) -> tuple[StateType, str | None]:
|
||||
if value is not None:
|
||||
# Clean up value and infer unit, e.g. -71dBm, 15 dB
|
||||
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:
|
||||
value = float(match.group("value"))
|
||||
|
@ -52,7 +52,10 @@ DEFAULT_TRANSITION = 0.2
|
||||
# sw version (attributeKey 0/40/10)
|
||||
TRANSITION_BLOCKLIST = (
|
||||
(4488, 514, "1.0", "1.0.0"),
|
||||
(4488, 260, "1.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:
|
||||
"""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
|
||||
while True:
|
||||
try:
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/philips_js",
|
||||
"iot_class": "local_polling",
|
||||
"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)
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = f"{device_id}-{entity_description.key}"
|
||||
self._attr_options = self.device[entity_description.options_key]
|
||||
|
||||
@property
|
||||
def current_option(self) -> str:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
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:
|
||||
"""Change to the selected entity option."""
|
||||
await self.entity_description.command(
|
||||
|
@ -7,5 +7,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyrisco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyrisco==0.6.1"]
|
||||
"requirements": ["pyrisco==0.6.2"]
|
||||
}
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/rympro",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["pyrympro==0.0.7"]
|
||||
"requirements": ["pyrympro==0.0.8"]
|
||||
}
|
||||
|
@ -256,6 +256,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if (
|
||||
current_entry := await self.async_set_unique_id(mac)
|
||||
) 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)
|
||||
if host == INTERNAL_WIFI_AP_IP:
|
||||
# If the device is broadcasting the internal wifi ap ip
|
||||
|
@ -53,14 +53,16 @@ def get_thumbnail_url_full(
|
||||
media_content_type: str,
|
||||
media_content_id: str,
|
||||
media_image_id: str | None = None,
|
||||
item: MusicServiceItem | None = None,
|
||||
) -> str | None:
|
||||
"""Get thumbnail URL."""
|
||||
if is_internal:
|
||||
item = get_media(
|
||||
media.library,
|
||||
media_content_id,
|
||||
media_content_type,
|
||||
)
|
||||
if not item:
|
||||
item = get_media(
|
||||
media.library,
|
||||
media_content_id,
|
||||
media_content_type,
|
||||
)
|
||||
return urllib.parse.unquote(getattr(item, "album_art_uri", ""))
|
||||
|
||||
return urllib.parse.unquote(
|
||||
@ -255,7 +257,7 @@ def item_payload(item: DidlObject, get_thumbnail_url=None) -> BrowseMedia:
|
||||
content_id = get_content_id(item)
|
||||
thumbnail = 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(
|
||||
title=item.title,
|
||||
|
@ -8,6 +8,7 @@ from typing import Any
|
||||
from switchbot import (
|
||||
SwitchbotAccountConnectionError,
|
||||
SwitchBotAdvertisement,
|
||||
SwitchbotApiError,
|
||||
SwitchbotAuthenticationError,
|
||||
SwitchbotLock,
|
||||
SwitchbotModel,
|
||||
@ -33,6 +34,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import (
|
||||
CONF_ENCRYPTION_KEY,
|
||||
@ -175,14 +177,19 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
description_placeholders = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
key_details = await self.hass.async_add_executor_job(
|
||||
SwitchbotLock.retrieve_encryption_key,
|
||||
key_details = await SwitchbotLock.async_retrieve_encryption_key(
|
||||
async_get_clientsession(self.hass),
|
||||
self._discovered_adv.address,
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
except SwitchbotAccountConnectionError as ex:
|
||||
raise AbortFlow("cannot_connect") from ex
|
||||
except (SwitchbotApiError, SwitchbotAccountConnectionError) as 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:
|
||||
_LOGGER.debug("Authentication failed: %s", ex, exc_info=True)
|
||||
errors = {"base": "auth_failed"}
|
||||
|
@ -39,5 +39,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
||||
"iot_class": "local_push",
|
||||
"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%]",
|
||||
"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%]",
|
||||
"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."
|
||||
}
|
||||
},
|
||||
|
@ -27,7 +27,9 @@ from .models import SynologyDSMData
|
||||
|
||||
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
||||
"""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))
|
||||
return SynologyPhotosMediaSource(hass, entries)
|
||||
|
||||
|
@ -37,7 +37,7 @@
|
||||
"not_connected": "Vehicle not connected",
|
||||
"connected": "Vehicle connected",
|
||||
"ready": "Ready to charge",
|
||||
"negociating": "Negociating connection",
|
||||
"negotiating": "Negotiating connection",
|
||||
"error": "Error",
|
||||
"charging_finished": "Charging finished",
|
||||
"waiting_car": "Waiting for car",
|
||||
|
@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["wled==0.17.1"],
|
||||
"requirements": ["wled==0.18.0"],
|
||||
"zeroconf": ["_wled._tcp.local."]
|
||||
}
|
||||
|
@ -514,6 +514,15 @@ class ConfigEntry:
|
||||
|
||||
# Only store setup result as state if it was not forwarded.
|
||||
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)
|
||||
|
||||
if self.supports_unload is None:
|
||||
@ -709,6 +718,17 @@ class ConfigEntry:
|
||||
) -> None:
|
||||
"""Set up while holding the 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)
|
||||
|
||||
@callback
|
||||
|
@ -23,7 +23,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 5
|
||||
PATCH_VERSION: Final = "4"
|
||||
PATCH_VERSION: Final = "5"
|
||||
__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)
|
||||
|
@ -1766,7 +1766,6 @@ class _TrackUTCTimeChange:
|
||||
# time when the timer was scheduled
|
||||
utc_now = time_tracker_utcnow()
|
||||
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:
|
||||
assert self._pattern_time_change_listener_job is not None
|
||||
self._cancel_callback = async_track_point_in_utc_time(
|
||||
@ -1774,6 +1773,7 @@ class _TrackUTCTimeChange:
|
||||
self._pattern_time_change_listener_job,
|
||||
self._calculate_next(utc_now + timedelta(seconds=1)),
|
||||
)
|
||||
hass.async_run_hass_job(self.job, localized_now, background=True)
|
||||
|
||||
@callback
|
||||
def async_cancel(self) -> None:
|
||||
|
@ -96,6 +96,11 @@ BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = {
|
||||
"dreame_vacuum": BlockedIntegration(
|
||||
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"
|
||||
@ -1669,6 +1674,14 @@ def async_get_issue_tracker(
|
||||
# If we know nothing about the entity, suggest opening an issue on HA core
|
||||
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):
|
||||
with suppress(IntegrationNotLoaded):
|
||||
integration = async_get_loaded_integration(hass, integration_domain)
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.5.4"
|
||||
version = "2024.5.5"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
@ -93,7 +93,7 @@ PyQRCode==1.2.1
|
||||
PyRMVtransport==0.3.3
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.45.0
|
||||
PySwitchbot==0.46.1
|
||||
|
||||
# homeassistant.components.switchmate
|
||||
PySwitchmate==0.5.1
|
||||
@ -461,7 +461,7 @@ aprslib==0.7.2
|
||||
aqualogic==2.6
|
||||
|
||||
# homeassistant.components.aranet
|
||||
aranet4==2.3.3
|
||||
aranet4==2.3.4
|
||||
|
||||
# homeassistant.components.arcam_fmj
|
||||
arcam-fmj==1.4.0
|
||||
@ -670,10 +670,10 @@ construct==2.10.68
|
||||
croniter==2.0.2
|
||||
|
||||
# homeassistant.components.crownstone
|
||||
crownstone-cloud==1.4.9
|
||||
crownstone-cloud==1.4.11
|
||||
|
||||
# homeassistant.components.crownstone
|
||||
crownstone-sse==2.0.4
|
||||
crownstone-sse==2.0.5
|
||||
|
||||
# homeassistant.components.crownstone
|
||||
crownstone-uart==2.1.0
|
||||
@ -1029,7 +1029,7 @@ ha-ffmpeg==3.2.0
|
||||
ha-iotawattpy==0.1.2
|
||||
|
||||
# homeassistant.components.philips_js
|
||||
ha-philipsjs==3.1.1
|
||||
ha-philipsjs==3.2.1
|
||||
|
||||
# homeassistant.components.habitica
|
||||
habitipy==0.2.0
|
||||
@ -2093,7 +2093,7 @@ pyrecswitch==1.0.2
|
||||
pyrepetierng==0.1.0
|
||||
|
||||
# homeassistant.components.risco
|
||||
pyrisco==0.6.1
|
||||
pyrisco==0.6.2
|
||||
|
||||
# homeassistant.components.rituals_perfume_genie
|
||||
pyrituals==0.0.6
|
||||
@ -2102,7 +2102,7 @@ pyrituals==0.0.6
|
||||
pyroute2==0.7.5
|
||||
|
||||
# homeassistant.components.rympro
|
||||
pyrympro==0.0.7
|
||||
pyrympro==0.0.8
|
||||
|
||||
# homeassistant.components.sabnzbd
|
||||
pysabnzbd==1.1.1
|
||||
@ -2866,7 +2866,7 @@ wiffi==1.1.2
|
||||
wirelesstagpy==0.8.1
|
||||
|
||||
# homeassistant.components.wled
|
||||
wled==0.17.1
|
||||
wled==0.18.0
|
||||
|
||||
# homeassistant.components.wolflink
|
||||
wolf-comm==0.0.7
|
||||
|
@ -81,7 +81,7 @@ PyQRCode==1.2.1
|
||||
PyRMVtransport==0.3.3
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.45.0
|
||||
PySwitchbot==0.46.1
|
||||
|
||||
# homeassistant.components.syncthru
|
||||
PySyncThru==0.7.10
|
||||
@ -422,7 +422,7 @@ apprise==1.7.4
|
||||
aprslib==0.7.2
|
||||
|
||||
# homeassistant.components.aranet
|
||||
aranet4==2.3.3
|
||||
aranet4==2.3.4
|
||||
|
||||
# homeassistant.components.arcam_fmj
|
||||
arcam-fmj==1.4.0
|
||||
@ -554,10 +554,10 @@ construct==2.10.68
|
||||
croniter==2.0.2
|
||||
|
||||
# homeassistant.components.crownstone
|
||||
crownstone-cloud==1.4.9
|
||||
crownstone-cloud==1.4.11
|
||||
|
||||
# homeassistant.components.crownstone
|
||||
crownstone-sse==2.0.4
|
||||
crownstone-sse==2.0.5
|
||||
|
||||
# homeassistant.components.crownstone
|
||||
crownstone-uart==2.1.0
|
||||
@ -843,7 +843,7 @@ ha-ffmpeg==3.2.0
|
||||
ha-iotawattpy==0.1.2
|
||||
|
||||
# homeassistant.components.philips_js
|
||||
ha-philipsjs==3.1.1
|
||||
ha-philipsjs==3.2.1
|
||||
|
||||
# homeassistant.components.habitica
|
||||
habitipy==0.2.0
|
||||
@ -1635,7 +1635,7 @@ pyqwikswitch==0.93
|
||||
pyrainbird==4.0.2
|
||||
|
||||
# homeassistant.components.risco
|
||||
pyrisco==0.6.1
|
||||
pyrisco==0.6.2
|
||||
|
||||
# homeassistant.components.rituals_perfume_genie
|
||||
pyrituals==0.0.6
|
||||
@ -1644,7 +1644,7 @@ pyrituals==0.0.6
|
||||
pyroute2==0.7.5
|
||||
|
||||
# homeassistant.components.rympro
|
||||
pyrympro==0.0.7
|
||||
pyrympro==0.0.8
|
||||
|
||||
# homeassistant.components.sabnzbd
|
||||
pysabnzbd==1.1.1
|
||||
@ -2222,7 +2222,7 @@ whois==0.9.27
|
||||
wiffi==1.1.2
|
||||
|
||||
# homeassistant.components.wled
|
||||
wled==0.17.1
|
||||
wled==0.18.0
|
||||
|
||||
# homeassistant.components.wolflink
|
||||
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:
|
||||
"""Test guessing the firmware type."""
|
||||
|
||||
|
@ -15,6 +15,8 @@ from homeassistant.const import (
|
||||
("-71 dBm", (-71, SIGNAL_STRENGTH_DECIBELS_MILLIWATT)),
|
||||
("15dB", (15, SIGNAL_STRENGTH_DECIBELS)),
|
||||
(">=-51dBm", (-51, SIGNAL_STRENGTH_DECIBELS_MILLIWATT)),
|
||||
("<-20dB", (-20, SIGNAL_STRENGTH_DECIBELS)),
|
||||
(">=30dB", (30, SIGNAL_STRENGTH_DECIBELS)),
|
||||
],
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@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.DISCOVERY_COOLDOWN", 0.0)
|
||||
@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0)
|
||||
|
@ -316,12 +316,35 @@ def sonos_favorites_fixture() -> SearchResult:
|
||||
class MockMusicServiceItem:
|
||||
"""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."""
|
||||
self.title = title
|
||||
self.item_id = item_id
|
||||
self.item_class = item_class
|
||||
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(
|
||||
@ -398,6 +421,10 @@ def mock_browse_by_idstring(
|
||||
"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 []
|
||||
|
||||
|
||||
@ -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")
|
||||
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."""
|
||||
music_library = MagicMock()
|
||||
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.browse = Mock(return_value=music_library_browse_categories)
|
||||
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 syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.media_player.browse_media import BrowseMedia
|
||||
from homeassistant.components.media_player.const import MediaClass, MediaType
|
||||
from homeassistant.components.sonos.media_browser import (
|
||||
@ -12,6 +14,8 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import SoCoMockFactory
|
||||
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
class MockMusicServiceItem:
|
||||
"""Mocks a Soco MusicServiceItem."""
|
||||
@ -95,3 +99,81 @@ async def test_build_item_response(
|
||||
browse_item.children[1].media_content_id
|
||||
== "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"] == {}
|
||||
|
||||
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"),
|
||||
):
|
||||
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,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key",
|
||||
"homeassistant.components.switchbot.config_flow.SwitchbotLock.async_retrieve_encryption_key",
|
||||
return_value={
|
||||
CONF_KEY_ID: "ff",
|
||||
CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
|
||||
@ -560,8 +560,8 @@ async def test_user_setup_wolock_auth_switchbot_api_down(hass: HomeAssistant) ->
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key",
|
||||
side_effect=SwitchbotAccountConnectionError,
|
||||
"homeassistant.components.switchbot.config_flow.SwitchbotLock.async_retrieve_encryption_key",
|
||||
side_effect=SwitchbotAccountConnectionError("Switchbot API down"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
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()
|
||||
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:
|
||||
|
@ -196,7 +196,7 @@ async def test_flow_ssdp_discovery_changed_udn_match_mac(hass: HomeAssistant) ->
|
||||
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
|
||||
},
|
||||
source=config_entries.SOURCE_SSDP,
|
||||
state=config_entries.ConfigEntryState.LOADED,
|
||||
state=config_entries.ConfigEntryState.NOT_LOADED,
|
||||
)
|
||||
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,
|
||||
},
|
||||
source=config_entries.SOURCE_SSDP,
|
||||
state=config_entries.ConfigEntryState.LOADED,
|
||||
state=config_entries.ConfigEntryState.NOT_LOADED,
|
||||
)
|
||||
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,
|
||||
},
|
||||
source=config_entries.SOURCE_SSDP,
|
||||
state=config_entries.ConfigEntryState.LOADED,
|
||||
state=config_entries.ConfigEntryState.NOT_LOADED,
|
||||
)
|
||||
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,
|
||||
},
|
||||
source=config_entries.SOURCE_SSDP,
|
||||
state=config_entries.ConfigEntryState.LOADED,
|
||||
state=config_entries.ConfigEntryState.NOT_LOADED,
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
"""Test tracking entity registry updates for an entity_id."""
|
||||
|
||||
|
@ -90,6 +90,89 @@ async def manager(hass: HomeAssistant) -> config_entries.ConfigEntries:
|
||||
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:
|
||||
"""Test we call <component>.setup_entry."""
|
||||
entry = MockConfigEntry(domain="comp")
|
||||
@ -386,7 +469,7 @@ async def test_remove_entry(
|
||||
]
|
||||
|
||||
# Setup entry
|
||||
await entry.async_setup(hass)
|
||||
await manager.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check entity state got added
|
||||
@ -1613,7 +1696,9 @@ async def test_entry_reload_succeed(
|
||||
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
||||
) -> None:
|
||||
"""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)
|
||||
|
||||
async_setup = AsyncMock(return_value=True)
|
||||
@ -1637,6 +1722,42 @@ async def test_entry_reload_succeed(
|
||||
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(
|
||||
"state",
|
||||
[
|
||||
@ -4005,7 +4126,9 @@ async def test_entry_reload_concurrency_not_setup_setup(
|
||||
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
||||
) -> None:
|
||||
"""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)
|
||||
|
||||
async_setup = AsyncMock(return_value=True)
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import threading
|
||||
from typing import Any
|
||||
@ -1108,14 +1109,18 @@ CUSTOM_ISSUE_TRACKER = "https://blablabla.com"
|
||||
# Integration domain is not currently deduced from module
|
||||
(None, "homeassistant.components.hue.sensor", CORE_ISSUE_TRACKER),
|
||||
("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", None, CUSTOM_ISSUE_TRACKER),
|
||||
# Custom integration without known issue tracker
|
||||
# Loaded custom integration without known issue tracker
|
||||
(None, "custom_components.bla.sensor", None),
|
||||
("bla_custom_no_tracker", "custom_components.bla_custom.sensor", None),
|
||||
("bla_custom_no_tracker", None, 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
|
||||
("bla_custom_no_tracker", "homeassistant.components.bla_custom.sensor", None),
|
||||
],
|
||||
@ -1133,6 +1138,32 @@ async def test_async_get_issue_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 (
|
||||
loader.async_get_issue_tracker(hass, integration_domain=domain, module=module)
|
||||
== issue_tracker
|
||||
|
Loading…
x
Reference in New Issue
Block a user