diff --git a/homeassistant/components/airgradient/config_flow.py b/homeassistant/components/airgradient/config_flow.py index 93cd0be61c4..70fa8a1755b 100644 --- a/homeassistant/components/airgradient/config_flow.py +++ b/homeassistant/components/airgradient/config_flow.py @@ -92,7 +92,9 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN): except AirGradientError: errors["base"] = "cannot_connect" else: - await self.async_set_unique_id(current_measures.serial_number) + await self.async_set_unique_id( + current_measures.serial_number, raise_on_progress=False + ) self._abort_if_unique_id_configured() await self.set_configuration_source() return self.async_create_entry( diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 8bba4ed2468..ca7b389a0f1 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -661,9 +661,12 @@ class RemoteCapabilities(AlexaEntity): def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" yield AlexaPowerController(self.entity) - yield AlexaModeController( - self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}" - ) + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or [] + if activities and supported & remote.RemoteEntityFeature.ACTIVITY: + yield AlexaModeController( + self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}" + ) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.entity) diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index 18c15ad61a1..2a1a20a9dc4 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -8,8 +8,8 @@ from typing import Any import aiohttp import voluptuous as vol -from yalexs.authenticator import ValidationResult -from yalexs.const import BRANDS, DEFAULT_BRAND +from yalexs.authenticator_common import ValidationResult +from yalexs.const import BRANDS_WITHOUT_OAUTH, DEFAULT_BRAND from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -118,7 +118,7 @@ class AugustConfigFlow(ConfigFlow, domain=DOMAIN): vol.Required( CONF_BRAND, default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND), - ): vol.In(BRANDS), + ): vol.In(BRANDS_WITHOUT_OAUTH), vol.Required( CONF_LOGIN_METHOD, default=self._user_auth_details.get( @@ -208,7 +208,7 @@ class AugustConfigFlow(ConfigFlow, domain=DOMAIN): vol.Required( CONF_BRAND, default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND), - ): vol.In(BRANDS), + ): vol.In(BRANDS_WITHOUT_OAUTH), vol.Required(CONF_PASSWORD): str, } ), diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 293c94c9629..e0739aadff0 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==6.4.3", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.4.1", "yalexs-ble==2.4.3"] } diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 5a0a9def0ae..a445a34cfcd 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -7,7 +7,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["pybravia"], - "requirements": ["pybravia==0.3.3"], + "requirements": ["pybravia==0.3.4"], "ssdp": [ { "st": "urn:schemas-sony-com:service:ScalarWebAPI:1", diff --git a/homeassistant/components/habitica/coordinator.py b/homeassistant/components/habitica/coordinator.py index 9d0ebe651e3..1b17eee6352 100644 --- a/homeassistant/components/habitica/coordinator.py +++ b/homeassistant/components/habitica/coordinator.py @@ -49,7 +49,14 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]): try: user_response = await self.api.user.get() tasks_response = await self.api.tasks.user.get() - tasks_response.extend(await self.api.tasks.user.get(type="completedTodos")) + tasks_response.extend( + [ + {"id": task["_id"], **task} + for task in await self.api.tasks.user.get(type="completedTodos") + if task.get("_id") + ] + ) + except ClientResponseError as error: raise UpdateFailed(f"Error communicating with API: {error}") from error diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index ebe472d7f0e..0a3064450d4 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.53", "babel==2.15.0"] + "requirements": ["holidays==0.55", "babel==2.15.0"] } diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 007153aceaf..b2b215a98b9 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.2.2"], + "requirements": ["aiohomekit==3.2.3"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/homeassistant/components/homeworks/manifest.json b/homeassistant/components/homeworks/manifest.json index a399e0a98e7..011c301d00d 100644 --- a/homeassistant/components/homeworks/manifest.json +++ b/homeassistant/components/homeworks/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/homeworks", "iot_class": "local_push", "loggers": ["pyhomeworks"], - "requirements": ["pyhomeworks==1.1.1"] + "requirements": ["pyhomeworks==1.1.2"] } diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 71aabd4c204..dbd9b511977 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -11,6 +11,6 @@ "iot_class": "local_push", "loggers": ["aiohue"], "quality_scale": "platinum", - "requirements": ["aiohue==4.7.2"], + "requirements": ["aiohue==4.7.3"], "zeroconf": ["_hue._tcp.local."] } diff --git a/homeassistant/components/hue/v2/hue_event.py b/homeassistant/components/hue/v2/hue_event.py index b286a11aade..2eace5139af 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -80,9 +80,9 @@ async def async_setup_hue_events(bridge: HueBridge): CONF_DEVICE_ID: device.id, # type: ignore[union-attr] CONF_UNIQUE_ID: hue_resource.id, CONF_TYPE: hue_resource.relative_rotary.rotary_report.action.value, - CONF_SUBTYPE: hue_resource.relative_rotary.last_event.rotation.direction.value, - CONF_DURATION: hue_resource.relative_rotary.last_event.rotation.duration, - CONF_STEPS: hue_resource.relative_rotary.last_event.rotation.steps, + CONF_SUBTYPE: hue_resource.relative_rotary.rotary_report.rotation.direction.value, + CONF_DURATION: hue_resource.relative_rotary.rotary_report.rotation.duration, + CONF_STEPS: hue_resource.relative_rotary.rotary_report.rotation.steps, } hass.bus.async_fire(ATTR_HUE_EVENT, data) diff --git a/homeassistant/components/integration/strings.json b/homeassistant/components/integration/strings.json index 55d4df1b45e..6186521aa1b 100644 --- a/homeassistant/components/integration/strings.json +++ b/homeassistant/components/integration/strings.json @@ -31,12 +31,14 @@ "round": "[%key:component::integration::config::step::user::data::round%]", "source": "[%key:component::integration::config::step::user::data::source%]", "unit_prefix": "[%key:component::integration::config::step::user::data::unit_prefix%]", - "unit_time": "[%key:component::integration::config::step::user::data::unit_time%]" + "unit_time": "[%key:component::integration::config::step::user::data::unit_time%]", + "max_sub_interval": "[%key:component::integration::config::step::user::data::max_sub_interval%]" }, "data_description": { "round": "[%key:component::integration::config::step::user::data_description::round%]", "unit_prefix": "[%key:component::integration::config::step::user::data_description::unit_prefix%]", - "unit_time": "[%key:component::integration::config::step::user::data_description::unit_time%]" + "unit_time": "[%key:component::integration::config::step::user::data_description::unit_time%]", + "max_sub_interval": "[%key:component::integration::config::step::user::data_description::max_sub_interval%]" } } } diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 3a1279a9bd4..617cdc730cc 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -441,6 +441,9 @@ class ZoneDevice(ClimateEntity): _attr_name = None _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_target_temperature_step = 0.5 + _attr_supported_features = ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) def __init__(self, controller: ControllerDevice, zone: Zone) -> None: """Initialise ZoneDevice.""" diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 9ecf687d6b9..b7efd14fa2a 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -11,7 +11,7 @@ "loggers": ["xknx", "xknxproject"], "quality_scale": "platinum", "requirements": [ - "xknx==3.1.0", + "xknx==3.1.1", "xknxproject==3.7.1", "knx-frontend==2024.8.9.225351" ], diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 6e9019c46fa..58ef8081fa9 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -60,6 +60,8 @@ TRANSITION_BLOCKLIST = ( (4456, 1011, "1.0.0", "2.00.00"), (4488, 260, "1.0", "1.0.0"), (4488, 514, "1.0", "1.0.0"), + (4921, 42, "1.0", "1.01.060"), + (4921, 43, "1.0", "1.01.060"), (4999, 24875, "1.0", "27.0"), (4999, 25057, "1.0", "27.0"), (5009, 514, "1.0", "1.0.0"), diff --git a/homeassistant/components/matter/select.py b/homeassistant/components/matter/select.py index 4a9ef3780d1..b46cad53123 100644 --- a/homeassistant/components/matter/select.py +++ b/homeassistant/components/matter/select.py @@ -229,12 +229,12 @@ DISCOVERY_SCHEMAS = [ entity_category=EntityCategory.CONFIG, translation_key="startup_on_off", options=["On", "Off", "Toggle", "Previous"], - measurement_to_ha=lambda x: { + measurement_to_ha=lambda x: { # pylint: disable=unnecessary-lambda 0: "Off", 1: "On", 2: "Toggle", None: "Previous", - }[x], + }.get(x), ha_to_native_value=lambda x: { "Off": 0, "On": 1, diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index fbe5ddb6534..3472fa64e8f 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -20,5 +20,5 @@ "iot_class": "cloud_push", "loggers": ["google_nest_sdm"], "quality_scale": "platinum", - "requirements": ["google-nest-sdm==4.0.6"] + "requirements": ["google-nest-sdm==4.0.7"] } diff --git a/homeassistant/components/nextbus/coordinator.py b/homeassistant/components/nextbus/coordinator.py index 6c438f6f808..781742e4c08 100644 --- a/homeassistant/components/nextbus/coordinator.py +++ b/homeassistant/components/nextbus/coordinator.py @@ -50,13 +50,15 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, Any]: """Fetch data from NextBus.""" - self.logger.debug("Updating data from API. Routes: %s", str(self._route_stops)) + + _route_stops = set(self._route_stops) + self.logger.debug("Updating data from API. Routes: %s", str(_route_stops)) def _update_data() -> dict: """Fetch data from NextBus.""" self.logger.debug("Updating data from API (executor)") predictions: dict[RouteStop, dict[str, Any]] = {} - for route_stop in self._route_stops: + for route_stop in _route_stops: prediction_results: list[dict[str, Any]] = [] try: prediction_results = self.client.predictions_for_stop( diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 7a80a9083e9..3bb3b9b2046 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["roborock"], "requirements": [ - "python-roborock==2.5.0", + "python-roborock==2.6.0", "vacuum-map-parser-roborock==0.1.2" ] } diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 50140e1890d..6286e515727 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -682,6 +682,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): self.entry.async_create_background_task( self.hass, self._async_connected(), "rpc device init", eager_start=True ) + # Make sure entities are marked available self.async_set_updated_data(None) elif update_type is RpcUpdateType.DISCONNECTED: self.entry.async_create_background_task( @@ -690,6 +691,8 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): "rpc device disconnected", eager_start=True, ) + # Make sure entities are marked as unavailable + self.async_set_updated_data(None) elif update_type is RpcUpdateType.STATUS: self.async_set_updated_data(None) if self.sleep_period: @@ -711,7 +714,8 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): """Shutdown the coordinator.""" if self.device.connected: try: - await async_stop_scanner(self.device) + if not self.sleep_period: + await async_stop_scanner(self.device) await super().shutdown() except InvalidAuthError: self.entry.async_start_reauth(self.hass) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 5bf8a411377..980a39feaba 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -358,6 +358,14 @@ class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): self._attr_unique_id = f"{coordinator.mac}-{key}" self._attr_name = get_rpc_entity_name(coordinator.device, key) + @property + def available(self) -> bool: + """Check if device is available and initialized or sleepy.""" + coordinator = self.coordinator + return super().available and ( + coordinator.device.initialized or bool(coordinator.sleep_period) + ) + @property def status(self) -> dict: """Device status by entity key.""" diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index c742b45632c..da3bbc4bb6e 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==11.2.0"], + "requirements": ["aioshelly==11.2.4"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/spotify/browse_media.py b/homeassistant/components/spotify/browse_media.py index cff7cae5ebd..abcb6df6205 100644 --- a/homeassistant/components/spotify/browse_media.py +++ b/homeassistant/components/spotify/browse_media.py @@ -172,10 +172,17 @@ async def async_browse_media( # Check for config entry specifier, and extract Spotify URI parsed_url = yarl.URL(media_content_id) + host = parsed_url.host if ( - parsed_url.host is None - or (entry := hass.config_entries.async_get_entry(parsed_url.host)) is None + host is None + # config entry ids can be upper or lower case. Yarl always returns host + # names in lower case, so we need to look for the config entry in both + or ( + entry := hass.config_entries.async_get_entry(host) + or hass.config_entries.async_get_entry(host.upper()) + ) + is None or not isinstance(entry.runtime_data, HomeAssistantSpotifyData) ): raise BrowseError("Invalid Spotify account specified") diff --git a/homeassistant/components/thethingsnetwork/manifest.json b/homeassistant/components/thethingsnetwork/manifest.json index c39b2b7c421..8d826750e39 100644 --- a/homeassistant/components/thethingsnetwork/manifest.json +++ b/homeassistant/components/thethingsnetwork/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/thethingsnetwork", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["ttn_client==1.1.0"] + "requirements": ["ttn_client==1.2.0"] } diff --git a/homeassistant/components/tplink_omada/manifest.json b/homeassistant/components/tplink_omada/manifest.json index 9544470d7a9..6bde656dc30 100644 --- a/homeassistant/components/tplink_omada/manifest.json +++ b/homeassistant/components/tplink_omada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/tplink_omada", "integration_type": "hub", "iot_class": "local_polling", - "requirements": ["tplink-omada-client==1.3.12"] + "requirements": ["tplink-omada-client==1.4.2"] } diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py index 483ab89b02e..54fc21d2659 100644 --- a/homeassistant/components/vesync/const.py +++ b/homeassistant/components/vesync/const.py @@ -40,6 +40,7 @@ SKU_TO_BASE_DEVICE = { "LAP-C202S-WUSR": "Core200S", # Alt ID Model Core200S "Core300S": "Core300S", "LAP-C301S-WJP": "Core300S", # Alt ID Model Core300S + "LAP-C301S-WAAA": "Core300S", # Alt ID Model Core300S "Core400S": "Core400S", "LAP-C401S-WJP": "Core400S", # Alt ID Model Core400S "LAP-C401S-WUSR": "Core400S", # Alt ID Model Core400S diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index 7853ad2101e..2798e0d46d1 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -46,7 +46,9 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): except WLEDConnectionError: errors["base"] = "cannot_connect" else: - await self.async_set_unique_id(device.info.mac_address) + await self.async_set_unique_id( + device.info.mac_address, raise_on_progress=False + ) self._abort_if_unique_id_configured( updates={CONF_HOST: user_input[CONF_HOST]} ) @@ -56,8 +58,6 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): CONF_HOST: user_input[CONF_HOST], }, ) - else: - user_input = {} return self.async_show_form( step_id="user", diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 4635b2209a6..33c2e249024 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -92,7 +92,7 @@ def _get_obj_holidays( subdiv=province, years=year, language=language, - categories=set_categories, # type: ignore[arg-type] + categories=set_categories, ) if (supported_languages := obj_holidays.supported_languages) and language == "en": for lang in supported_languages: @@ -102,7 +102,7 @@ def _get_obj_holidays( subdiv=province, years=year, language=lang, - categories=set_categories, # type: ignore[arg-type] + categories=set_categories, ) LOGGER.debug("Changing language from %s to %s", language, lang) return obj_holidays diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 69df8080fa5..fafa870d00a 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.53"] + "requirements": ["holidays==0.55"] } diff --git a/homeassistant/const.py b/homeassistant/const.py index 39df0486e06..2a06c24843a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __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) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e87307e13d2..432e213d267 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-zlib==0.1.1 -aiohttp==3.10.3 +aiohttp==3.10.5 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index 9bc294b2d0f..437aea9f097 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.2" +version = "2024.8.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" @@ -24,7 +24,7 @@ classifiers = [ requires-python = ">=3.12.0" dependencies = [ "aiodns==3.2.0", - "aiohttp==3.10.3", + "aiohttp==3.10.5", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index 7a4b0bd6d09..9af81e775ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.2.0 -aiohttp==3.10.3 +aiohttp==3.10.5 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 diff --git a/requirements_all.txt b/requirements_all.txt index 735997d3208..48fed02cd53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -255,10 +255,10 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.2.2 +aiohomekit==3.2.3 # homeassistant.components.hue -aiohue==4.7.2 +aiohue==4.7.3 # homeassistant.components.imap aioimaplib==1.1.0 @@ -359,7 +359,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==11.2.0 +aioshelly==11.2.4 # homeassistant.components.skybell aioskybell==22.7.0 @@ -989,7 +989,7 @@ google-cloud-texttospeech==2.16.3 google-generativeai==0.6.0 # homeassistant.components.nest -google-nest-sdm==4.0.6 +google-nest-sdm==4.0.7 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -1096,7 +1096,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.53 +holidays==0.55 # homeassistant.components.frontend home-assistant-frontend==20240809.0 @@ -1759,7 +1759,7 @@ pyblu==0.4.0 pybotvac==0.0.25 # homeassistant.components.braviatv -pybravia==0.3.3 +pybravia==0.3.4 # homeassistant.components.nissan_leaf pycarwings2==2.14 @@ -1912,7 +1912,7 @@ pyhiveapi==0.5.16 pyhomematic==0.1.77 # homeassistant.components.homeworks -pyhomeworks==1.1.1 +pyhomeworks==1.1.2 # homeassistant.components.ialarm pyialarm==2.2.0 @@ -2341,7 +2341,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==2.5.0 +python-roborock==2.6.0 # homeassistant.components.smarttub python-smarttub==0.0.36 @@ -2792,7 +2792,7 @@ total-connect-client==2024.5 tp-connected==0.0.4 # homeassistant.components.tplink_omada -tplink-omada-client==1.3.12 +tplink-omada-client==1.4.2 # homeassistant.components.transmission transmission-rpc==7.0.3 @@ -2801,7 +2801,7 @@ transmission-rpc==7.0.3 ttls==1.8.3 # homeassistant.components.thethingsnetwork -ttn_client==1.1.0 +ttn_client==1.2.0 # homeassistant.components.tuya tuya-device-sharing-sdk==0.1.9 @@ -2936,7 +2936,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.30.2 # homeassistant.components.knx -xknx==3.1.0 +xknx==3.1.1 # homeassistant.components.knx xknxproject==3.7.1 @@ -2959,7 +2959,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==6.4.3 +yalexs==8.4.1 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 757f8ccf405..5df5a0836c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -240,10 +240,10 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.2.2 +aiohomekit==3.2.3 # homeassistant.components.hue -aiohue==4.7.2 +aiohue==4.7.3 # homeassistant.components.imap aioimaplib==1.1.0 @@ -341,7 +341,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==11.2.0 +aioshelly==11.2.4 # homeassistant.components.skybell aioskybell==22.7.0 @@ -833,7 +833,7 @@ google-cloud-pubsub==2.13.11 google-generativeai==0.6.0 # homeassistant.components.nest -google-nest-sdm==4.0.6 +google-nest-sdm==4.0.7 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -916,7 +916,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.53 +holidays==0.55 # homeassistant.components.frontend home-assistant-frontend==20240809.0 @@ -1421,7 +1421,7 @@ pyblu==0.4.0 pybotvac==0.0.25 # homeassistant.components.braviatv -pybravia==0.3.3 +pybravia==0.3.4 # homeassistant.components.cloudflare pycfdns==3.0.0 @@ -1523,7 +1523,7 @@ pyhiveapi==0.5.16 pyhomematic==0.1.77 # homeassistant.components.homeworks -pyhomeworks==1.1.1 +pyhomeworks==1.1.2 # homeassistant.components.ialarm pyialarm==2.2.0 @@ -1850,7 +1850,7 @@ python-picnic-api==1.1.0 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==2.5.0 +python-roborock==2.6.0 # homeassistant.components.smarttub python-smarttub==0.0.36 @@ -2190,7 +2190,7 @@ toonapi==0.3.0 total-connect-client==2024.5 # homeassistant.components.tplink_omada -tplink-omada-client==1.3.12 +tplink-omada-client==1.4.2 # homeassistant.components.transmission transmission-rpc==7.0.3 @@ -2199,7 +2199,7 @@ transmission-rpc==7.0.3 ttls==1.8.3 # homeassistant.components.thethingsnetwork -ttn_client==1.1.0 +ttn_client==1.2.0 # homeassistant.components.tuya tuya-device-sharing-sdk==0.1.9 @@ -2316,7 +2316,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.30.2 # homeassistant.components.knx -xknx==3.1.0 +xknx==3.1.1 # homeassistant.components.knx xknxproject==3.7.1 @@ -2336,7 +2336,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==6.4.3 +yalexs==8.4.1 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/script/licenses.py b/script/licenses.py index 659f8cb8dcc..9c584e7f4fc 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -124,7 +124,6 @@ EXCEPTIONS = { "PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201 "aiocomelit", # https://github.com/chemelli74/aiocomelit/pull/138 "aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180 - "aiohappyeyeballs", # Python-2.0.1 "aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94 "aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8 "aioruuvigateway", # https://github.com/akx/aioruuvigateway/pull/6 diff --git a/tests/components/airgradient/test_config_flow.py b/tests/components/airgradient/test_config_flow.py index 222ac5d04af..8730b18676f 100644 --- a/tests/components/airgradient/test_config_flow.py +++ b/tests/components/airgradient/test_config_flow.py @@ -253,3 +253,32 @@ async def test_zeroconf_flow_abort_old_firmware(hass: HomeAssistant) -> None: ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_version" + + +async def test_user_flow_works_discovery( + hass: HomeAssistant, + mock_new_airgradient_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test user flow can continue after discovery happened.""" + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 2 + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "10.0.0.131"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + # Verify the discovery flow was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 162149f095b..b56d8054d7b 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -70,6 +70,7 @@ async def test_discovery_remote( { "current_activity": current_activity, "activity_list": activity_list, + "supported_features": 4, }, ) msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) @@ -790,22 +791,37 @@ async def test_report_remote_activity(hass: HomeAssistant) -> None: hass.states.async_set( "remote.unknown", "on", - {"current_activity": "UNKNOWN"}, + { + "current_activity": "UNKNOWN", + "supported_features": 4, + }, ) hass.states.async_set( "remote.tv", "on", - {"current_activity": "TV", "activity_list": ["TV", "MUSIC", "DVD"]}, + { + "current_activity": "TV", + "activity_list": ["TV", "MUSIC", "DVD"], + "supported_features": 4, + }, ) hass.states.async_set( "remote.music", "on", - {"current_activity": "MUSIC", "activity_list": ["TV", "MUSIC", "DVD"]}, + { + "current_activity": "MUSIC", + "activity_list": ["TV", "MUSIC", "DVD"], + "supported_features": 4, + }, ) hass.states.async_set( "remote.dvd", "on", - {"current_activity": "DVD", "activity_list": ["TV", "MUSIC", "DVD"]}, + { + "current_activity": "DVD", + "activity_list": ["TV", "MUSIC", "DVD"], + "supported_features": 4, + }, ) properties = await reported_properties(hass, "remote#unknown") diff --git a/tests/components/august/conftest.py b/tests/components/august/conftest.py index 052cde7d2a2..78cb2cdad89 100644 --- a/tests/components/august/conftest.py +++ b/tests/components/august/conftest.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from yalexs.manager.ratelimit import _RateLimitChecker @pytest.fixture(name="mock_discovery", autouse=True) @@ -12,3 +13,10 @@ def mock_discovery_fixture(): "homeassistant.components.august.data.discovery_flow.async_create_flow" ) as mock_discovery: yield mock_discovery + + +@pytest.fixture(name="disable_ratelimit_checks", autouse=True) +def disable_ratelimit_checks_fixture(): + """Disable rate limit checks.""" + with patch.object(_RateLimitChecker, "register_wakeup"): + yield diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 30be50e75c9..a0f5b55a607 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -25,7 +25,7 @@ from yalexs.activity import ( DoorOperationActivity, LockOperationActivity, ) -from yalexs.authenticator import AuthenticationState +from yalexs.authenticator_common import AuthenticationState from yalexs.const import Brand from yalexs.doorbell import Doorbell, DoorbellDetail from yalexs.lock import Lock, LockDetail diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index aec08864c65..fdebb8d5c46 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from yalexs.authenticator import ValidationResult +from yalexs.authenticator_common import ValidationResult from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from homeassistant import config_entries diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py index e605fd74f0a..74266397ed5 100644 --- a/tests/components/august/test_gateway.py +++ b/tests/components/august/test_gateway.py @@ -50,5 +50,5 @@ async def _patched_refresh_access_token( ) await august_gateway.async_refresh_access_token_if_needed() refresh_access_token_mock.assert_called() - assert august_gateway.access_token == new_token + assert await august_gateway.async_get_access_token() == new_token assert august_gateway.authentication.access_token_expires == new_token_expire_time diff --git a/tests/components/habitica/test_init.py b/tests/components/habitica/test_init.py index 31c3a1fae39..4c2b1e2aae6 100644 --- a/tests/components/habitica/test_init.py +++ b/tests/components/habitica/test_init.py @@ -73,7 +73,20 @@ def common_requests(aioclient_mock: AiohttpClientMocker) -> AiohttpClientMocker: } }, ) - + aioclient_mock.get( + "https://habitica.com/api/v3/tasks/user?type=completedTodos", + json={ + "data": [ + { + "text": "this is a mock todo #5", + "id": 5, + "_id": 5, + "type": "todo", + "completed": True, + } + ] + }, + ) aioclient_mock.get( "https://habitica.com/api/v3/tasks/user", json={ @@ -88,19 +101,6 @@ def common_requests(aioclient_mock: AiohttpClientMocker) -> AiohttpClientMocker: ] }, ) - aioclient_mock.get( - "https://habitica.com/api/v3/tasks/user?type=completedTodos", - json={ - "data": [ - { - "text": "this is a mock todo #5", - "id": 5, - "type": "todo", - "completed": True, - } - ] - }, - ) aioclient_mock.post( "https://habitica.com/api/v3/tasks/user", diff --git a/tests/components/hue/fixtures/v2_resources.json b/tests/components/hue/fixtures/v2_resources.json index 980086d0988..3d718f24c50 100644 --- a/tests/components/hue/fixtures/v2_resources.json +++ b/tests/components/hue/fixtures/v2_resources.json @@ -1288,7 +1288,9 @@ }, { "button": { - "last_event": "short_release" + "button_report": { + "event": "short_release" + } }, "id": "c658d3d8-a013-4b81-8ac6-78b248537e70", "id_v1": "/sensors/50", @@ -1327,7 +1329,9 @@ }, { "button": { - "last_event": "short_release" + "button_report": { + "event": "short_release" + } }, "id": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", "id_v1": "/sensors/10", @@ -1366,7 +1370,9 @@ }, { "button": { - "last_event": "short_release" + "button_report": { + "event": "short_release" + } }, "id": "31cffcda-efc2-401f-a152-e10db3eed232", "id_v1": "/sensors/5", diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index d3494c094f9..1140c93775b 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -854,6 +854,27 @@ async def test_rpc_runs_connected_events_when_initialized( assert call.script_list() in mock_rpc_device.mock_calls +async def test_rpc_sleeping_device_unload_ignore_ble_scanner( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test RPC sleeping device does not stop ble scanner on unload.""" + monkeypatch.setattr(mock_rpc_device, "connected", True) + entry = await init_integration(hass, 2, sleep_period=1000) + + # Make device online + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + # Unload + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + # BLE script list is called during stop ble scanner + assert call.script_list() not in mock_rpc_device.mock_calls + + async def test_block_sleeping_device_connection_error( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index a39123a6722..2da82a5da87 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -43,7 +43,7 @@ from . import ( register_entity, ) -from tests.common import mock_restore_cache_with_extra_data +from tests.common import async_fire_time_changed, mock_restore_cache_with_extra_data RELAY_BLOCK_ID = 0 SENSOR_BLOCK_ID = 3 @@ -1189,3 +1189,35 @@ async def test_rpc_remove_enum_virtual_sensor_when_orphaned( entry = entity_registry.async_get(entity_id) assert not entry + + +async def test_rpc_device_sensor_goes_unavailable_on_disconnect( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test RPC device with sensor goes unavailable on disconnect.""" + await init_integration(hass, 2) + temp_sensor_state = hass.states.get("sensor.test_name_temperature") + assert temp_sensor_state is not None + assert temp_sensor_state.state != STATE_UNAVAILABLE + monkeypatch.setattr(mock_rpc_device, "connected", False) + monkeypatch.setattr(mock_rpc_device, "initialized", False) + mock_rpc_device.mock_disconnected() + await hass.async_block_till_done() + temp_sensor_state = hass.states.get("sensor.test_name_temperature") + assert temp_sensor_state.state == STATE_UNAVAILABLE + + freezer.tick(60) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert "NotInitialized" not in caplog.text + + monkeypatch.setattr(mock_rpc_device, "connected", True) + monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_initialized() + await hass.async_block_till_done() + temp_sensor_state = hass.states.get("sensor.test_name_temperature") + assert temp_sensor_state.state != STATE_UNAVAILABLE diff --git a/tests/components/spotify/conftest.py b/tests/components/spotify/conftest.py new file mode 100644 index 00000000000..3f248b54529 --- /dev/null +++ b/tests/components/spotify/conftest.py @@ -0,0 +1,128 @@ +"""Common test fixtures.""" + +from collections.abc import Generator +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.spotify import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry_1() -> MockConfigEntry: + """Mock a config entry with an upper case entry id.""" + return MockConfigEntry( + domain=DOMAIN, + title="spotify_1", + data={ + "auth_implementation": "spotify_c95e4090d4d3438b922331e7428f8171", + "token": { + "access_token": "AccessToken", + "token_type": "Bearer", + "expires_in": 3600, + "refresh_token": "RefreshToken", + "scope": "playlist-read-private ...", + "expires_at": 1724198975.8829377, + }, + "id": "32oesphrnacjcf7vw5bf6odx3oiu", + "name": "spotify_account_1", + }, + unique_id="84fce612f5b8", + entry_id="01J5TX5A0FF6G5V0QJX6HBC94T", + ) + + +@pytest.fixture +def mock_config_entry_2() -> MockConfigEntry: + """Mock a config entry with a lower case entry id.""" + return MockConfigEntry( + domain=DOMAIN, + title="spotify_2", + data={ + "auth_implementation": "spotify_c95e4090d4d3438b922331e7428f8171", + "token": { + "access_token": "AccessToken", + "token_type": "Bearer", + "expires_in": 3600, + "refresh_token": "RefreshToken", + "scope": "playlist-read-private ...", + "expires_at": 1724198975.8829377, + }, + "id": "55oesphrnacjcf7vw5bf6odx3oiu", + "name": "spotify_account_2", + }, + unique_id="99fce612f5b8", + entry_id="32oesphrnacjcf7vw5bf6odx3", + ) + + +@pytest.fixture +def spotify_playlists() -> dict[str, Any]: + """Mock the return from getting a list of playlists.""" + return { + "href": "https://api.spotify.com/v1/users/31oesphrnacjcf7vw5bf6odx3oiu/playlists?offset=0&limit=48", + "limit": 48, + "next": None, + "offset": 0, + "previous": None, + "total": 1, + "items": [ + { + "collaborative": False, + "description": "", + "id": "unique_identifier_00", + "name": "Playlist1", + "type": "playlist", + "uri": "spotify:playlist:unique_identifier_00", + } + ], + } + + +@pytest.fixture +def spotify_mock(spotify_playlists: dict[str, Any]) -> Generator[MagicMock]: + """Mock the Spotify API.""" + with patch("homeassistant.components.spotify.Spotify") as spotify_mock: + mock = MagicMock() + mock.current_user_playlists.return_value = spotify_playlists + spotify_mock.return_value = mock + yield spotify_mock + + +@pytest.fixture +async def spotify_setup( + hass: HomeAssistant, + spotify_mock: MagicMock, + mock_config_entry_1: MockConfigEntry, + mock_config_entry_2: MockConfigEntry, +): + """Set up the spotify integration.""" + with patch( + "homeassistant.components.spotify.OAuth2Session.async_ensure_token_valid" + ): + await async_setup_component(hass, "application_credentials", {}) + await hass.async_block_till_done() + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential("CLIENT_ID", "CLIENT_SECRET"), + "spotify_c95e4090d4d3438b922331e7428f8171", + ) + await hass.async_block_till_done() + mock_config_entry_1.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry_1.entry_id) + mock_config_entry_2.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry_2.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done(wait_background_tasks=True) + yield diff --git a/tests/components/spotify/snapshots/test_media_browser.ambr b/tests/components/spotify/snapshots/test_media_browser.ambr new file mode 100644 index 00000000000..4236fcb2e79 --- /dev/null +++ b/tests/components/spotify/snapshots/test_media_browser.ambr @@ -0,0 +1,236 @@ +# serializer version: 1 +# name: test_browse_media_categories + dict({ + 'can_expand': True, + 'can_play': False, + 'children': list([ + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_playlists', + 'media_content_type': 'spotify://current_user_playlists', + 'thumbnail': None, + 'title': 'Playlists', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_followed_artists', + 'media_content_type': 'spotify://current_user_followed_artists', + 'thumbnail': None, + 'title': 'Artists', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_saved_albums', + 'media_content_type': 'spotify://current_user_saved_albums', + 'thumbnail': None, + 'title': 'Albums', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_saved_tracks', + 'media_content_type': 'spotify://current_user_saved_tracks', + 'thumbnail': None, + 'title': 'Tracks', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_saved_shows', + 'media_content_type': 'spotify://current_user_saved_shows', + 'thumbnail': None, + 'title': 'Podcasts', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_recently_played', + 'media_content_type': 'spotify://current_user_recently_played', + 'thumbnail': None, + 'title': 'Recently played', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_top_artists', + 'media_content_type': 'spotify://current_user_top_artists', + 'thumbnail': None, + 'title': 'Top Artists', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_top_tracks', + 'media_content_type': 'spotify://current_user_top_tracks', + 'thumbnail': None, + 'title': 'Top Tracks', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/categories', + 'media_content_type': 'spotify://categories', + 'thumbnail': None, + 'title': 'Categories', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/featured_playlists', + 'media_content_type': 'spotify://featured_playlists', + 'thumbnail': None, + 'title': 'Featured Playlists', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/new_releases', + 'media_content_type': 'spotify://new_releases', + 'thumbnail': None, + 'title': 'New Releases', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/library', + 'media_content_type': 'spotify://library', + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Media Library', + }) +# --- +# name: test_browse_media_playlists + dict({ + 'can_expand': True, + 'can_play': False, + 'children': list([ + dict({ + 'can_expand': True, + 'can_play': True, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/spotify:playlist:unique_identifier_00', + 'media_content_type': 'spotify://playlist', + 'thumbnail': None, + 'title': 'Playlist1', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_playlists', + 'media_content_type': 'spotify://current_user_playlists', + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Playlists', + }) +# --- +# name: test_browse_media_playlists[01J5TX5A0FF6G5V0QJX6HBC94T] + dict({ + 'can_expand': True, + 'can_play': False, + 'children': list([ + dict({ + 'can_expand': True, + 'can_play': True, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/spotify:playlist:unique_identifier_00', + 'media_content_type': 'spotify://playlist', + 'thumbnail': None, + 'title': 'Playlist1', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_playlists', + 'media_content_type': 'spotify://current_user_playlists', + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Playlists', + }) +# --- +# name: test_browse_media_playlists[32oesphrnacjcf7vw5bf6odx3] + dict({ + 'can_expand': True, + 'can_play': False, + 'children': list([ + dict({ + 'can_expand': True, + 'can_play': True, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://32oesphrnacjcf7vw5bf6odx3/spotify:playlist:unique_identifier_00', + 'media_content_type': 'spotify://playlist', + 'thumbnail': None, + 'title': 'Playlist1', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://32oesphrnacjcf7vw5bf6odx3/current_user_playlists', + 'media_content_type': 'spotify://current_user_playlists', + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Playlists', + }) +# --- +# name: test_browse_media_root + dict({ + 'can_expand': True, + 'can_play': False, + 'children': list([ + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'spotify://01J5TX5A0FF6G5V0QJX6HBC94T', + 'media_content_type': 'spotify://library', + 'thumbnail': 'https://brands.home-assistant.io/_/spotify/logo.png', + 'title': 'spotify_1', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'spotify://32oesphrnacjcf7vw5bf6odx3', + 'media_content_type': 'spotify://library', + 'thumbnail': 'https://brands.home-assistant.io/_/spotify/logo.png', + 'title': 'spotify_2', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://', + 'media_content_type': 'spotify', + 'not_shown': 0, + 'thumbnail': 'https://brands.home-assistant.io/_/spotify/logo.png', + 'title': 'Spotify', + }) +# --- diff --git a/tests/components/spotify/test_media_browser.py b/tests/components/spotify/test_media_browser.py new file mode 100644 index 00000000000..2b47aed9ee3 --- /dev/null +++ b/tests/components/spotify/test_media_browser.py @@ -0,0 +1,61 @@ +"""Test the media browser interface.""" + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.spotify import DOMAIN +from homeassistant.components.spotify.browse_media import async_browse_media +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done(wait_background_tasks=True) + + +async def test_browse_media_root( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + spotify_setup, +) -> None: + """Test browsing the root.""" + response = await async_browse_media(hass, None, None) + assert response.as_dict() == snapshot + + +async def test_browse_media_categories( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + spotify_setup, +) -> None: + """Test browsing categories.""" + response = await async_browse_media( + hass, "spotify://library", "spotify://01J5TX5A0FF6G5V0QJX6HBC94T" + ) + assert response.as_dict() == snapshot + + +@pytest.mark.parametrize( + ("config_entry_id"), [("01J5TX5A0FF6G5V0QJX6HBC94T"), ("32oesphrnacjcf7vw5bf6odx3")] +) +async def test_browse_media_playlists( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + config_entry_id: str, + spotify_setup, +) -> None: + """Test browsing playlists for the two config entries.""" + response = await async_browse_media( + hass, + "spotify://current_user_playlists", + f"spotify://{config_entry_id}/current_user_playlists", + ) + assert response.as_dict() == snapshot