This commit is contained in:
Paulus Schoutsen 2024-08-25 16:06:09 +02:00 committed by GitHub
commit 516f3295bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 671 additions and 94 deletions

View File

@ -92,7 +92,9 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
except AirGradientError: except AirGradientError:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
else: 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() self._abort_if_unique_id_configured()
await self.set_configuration_source() await self.set_configuration_source()
return self.async_create_entry( return self.async_create_entry(

View File

@ -661,9 +661,12 @@ class RemoteCapabilities(AlexaEntity):
def interfaces(self) -> Generator[AlexaCapability]: def interfaces(self) -> Generator[AlexaCapability]:
"""Yield the supported interfaces.""" """Yield the supported interfaces."""
yield AlexaPowerController(self.entity) yield AlexaPowerController(self.entity)
yield AlexaModeController( supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}" 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 AlexaEndpointHealth(self.hass, self.entity)
yield Alexa(self.entity) yield Alexa(self.entity)

View File

@ -8,8 +8,8 @@ from typing import Any
import aiohttp import aiohttp
import voluptuous as vol import voluptuous as vol
from yalexs.authenticator import ValidationResult from yalexs.authenticator_common import ValidationResult
from yalexs.const import BRANDS, DEFAULT_BRAND from yalexs.const import BRANDS_WITHOUT_OAUTH, DEFAULT_BRAND
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@ -118,7 +118,7 @@ class AugustConfigFlow(ConfigFlow, domain=DOMAIN):
vol.Required( vol.Required(
CONF_BRAND, CONF_BRAND,
default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND), default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND),
): vol.In(BRANDS), ): vol.In(BRANDS_WITHOUT_OAUTH),
vol.Required( vol.Required(
CONF_LOGIN_METHOD, CONF_LOGIN_METHOD,
default=self._user_auth_details.get( default=self._user_auth_details.get(
@ -208,7 +208,7 @@ class AugustConfigFlow(ConfigFlow, domain=DOMAIN):
vol.Required( vol.Required(
CONF_BRAND, CONF_BRAND,
default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND), default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND),
): vol.In(BRANDS), ): vol.In(BRANDS_WITHOUT_OAUTH),
vol.Required(CONF_PASSWORD): str, vol.Required(CONF_PASSWORD): str,
} }
), ),

View File

@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august", "documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"], "loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==6.4.3", "yalexs-ble==2.4.3"] "requirements": ["yalexs==8.4.1", "yalexs-ble==2.4.3"]
} }

View File

@ -7,7 +7,7 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pybravia"], "loggers": ["pybravia"],
"requirements": ["pybravia==0.3.3"], "requirements": ["pybravia==0.3.4"],
"ssdp": [ "ssdp": [
{ {
"st": "urn:schemas-sony-com:service:ScalarWebAPI:1", "st": "urn:schemas-sony-com:service:ScalarWebAPI:1",

View File

@ -49,7 +49,14 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
try: try:
user_response = await self.api.user.get() user_response = await self.api.user.get()
tasks_response = await self.api.tasks.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: except ClientResponseError as error:
raise UpdateFailed(f"Error communicating with API: {error}") from error raise UpdateFailed(f"Error communicating with API: {error}") from error

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday", "documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["holidays==0.53", "babel==2.15.0"] "requirements": ["holidays==0.55", "babel==2.15.0"]
} }

View File

@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller", "documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"], "loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==3.2.2"], "requirements": ["aiohomekit==3.2.3"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/homeworks", "documentation": "https://www.home-assistant.io/integrations/homeworks",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyhomeworks"], "loggers": ["pyhomeworks"],
"requirements": ["pyhomeworks==1.1.1"] "requirements": ["pyhomeworks==1.1.2"]
} }

View File

@ -11,6 +11,6 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiohue"], "loggers": ["aiohue"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["aiohue==4.7.2"], "requirements": ["aiohue==4.7.3"],
"zeroconf": ["_hue._tcp.local."] "zeroconf": ["_hue._tcp.local."]
} }

View File

@ -80,9 +80,9 @@ async def async_setup_hue_events(bridge: HueBridge):
CONF_DEVICE_ID: device.id, # type: ignore[union-attr] CONF_DEVICE_ID: device.id, # type: ignore[union-attr]
CONF_UNIQUE_ID: hue_resource.id, CONF_UNIQUE_ID: hue_resource.id,
CONF_TYPE: hue_resource.relative_rotary.rotary_report.action.value, CONF_TYPE: hue_resource.relative_rotary.rotary_report.action.value,
CONF_SUBTYPE: hue_resource.relative_rotary.last_event.rotation.direction.value, CONF_SUBTYPE: hue_resource.relative_rotary.rotary_report.rotation.direction.value,
CONF_DURATION: hue_resource.relative_rotary.last_event.rotation.duration, CONF_DURATION: hue_resource.relative_rotary.rotary_report.rotation.duration,
CONF_STEPS: hue_resource.relative_rotary.last_event.rotation.steps, CONF_STEPS: hue_resource.relative_rotary.rotary_report.rotation.steps,
} }
hass.bus.async_fire(ATTR_HUE_EVENT, data) hass.bus.async_fire(ATTR_HUE_EVENT, data)

View File

@ -31,12 +31,14 @@
"round": "[%key:component::integration::config::step::user::data::round%]", "round": "[%key:component::integration::config::step::user::data::round%]",
"source": "[%key:component::integration::config::step::user::data::source%]", "source": "[%key:component::integration::config::step::user::data::source%]",
"unit_prefix": "[%key:component::integration::config::step::user::data::unit_prefix%]", "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": { "data_description": {
"round": "[%key:component::integration::config::step::user::data_description::round%]", "round": "[%key:component::integration::config::step::user::data_description::round%]",
"unit_prefix": "[%key:component::integration::config::step::user::data_description::unit_prefix%]", "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%]"
} }
} }
} }

View File

@ -441,6 +441,9 @@ class ZoneDevice(ClimateEntity):
_attr_name = None _attr_name = None
_attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_target_temperature_step = 0.5 _attr_target_temperature_step = 0.5
_attr_supported_features = (
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
)
def __init__(self, controller: ControllerDevice, zone: Zone) -> None: def __init__(self, controller: ControllerDevice, zone: Zone) -> None:
"""Initialise ZoneDevice.""" """Initialise ZoneDevice."""

View File

@ -11,7 +11,7 @@
"loggers": ["xknx", "xknxproject"], "loggers": ["xknx", "xknxproject"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": [ "requirements": [
"xknx==3.1.0", "xknx==3.1.1",
"xknxproject==3.7.1", "xknxproject==3.7.1",
"knx-frontend==2024.8.9.225351" "knx-frontend==2024.8.9.225351"
], ],

View File

@ -60,6 +60,8 @@ TRANSITION_BLOCKLIST = (
(4456, 1011, "1.0.0", "2.00.00"), (4456, 1011, "1.0.0", "2.00.00"),
(4488, 260, "1.0", "1.0.0"), (4488, 260, "1.0", "1.0.0"),
(4488, 514, "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, 24875, "1.0", "27.0"),
(4999, 25057, "1.0", "27.0"), (4999, 25057, "1.0", "27.0"),
(5009, 514, "1.0", "1.0.0"), (5009, 514, "1.0", "1.0.0"),

View File

@ -229,12 +229,12 @@ DISCOVERY_SCHEMAS = [
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
translation_key="startup_on_off", translation_key="startup_on_off",
options=["On", "Off", "Toggle", "Previous"], options=["On", "Off", "Toggle", "Previous"],
measurement_to_ha=lambda x: { measurement_to_ha=lambda x: { # pylint: disable=unnecessary-lambda
0: "Off", 0: "Off",
1: "On", 1: "On",
2: "Toggle", 2: "Toggle",
None: "Previous", None: "Previous",
}[x], }.get(x),
ha_to_native_value=lambda x: { ha_to_native_value=lambda x: {
"Off": 0, "Off": 0,
"On": 1, "On": 1,

View File

@ -20,5 +20,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["google_nest_sdm"], "loggers": ["google_nest_sdm"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["google-nest-sdm==4.0.6"] "requirements": ["google-nest-sdm==4.0.7"]
} }

View File

@ -50,13 +50,15 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
async def _async_update_data(self) -> dict[str, Any]: async def _async_update_data(self) -> dict[str, Any]:
"""Fetch data from NextBus.""" """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: def _update_data() -> dict:
"""Fetch data from NextBus.""" """Fetch data from NextBus."""
self.logger.debug("Updating data from API (executor)") self.logger.debug("Updating data from API (executor)")
predictions: dict[RouteStop, dict[str, Any]] = {} 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]] = [] prediction_results: list[dict[str, Any]] = []
try: try:
prediction_results = self.client.predictions_for_stop( prediction_results = self.client.predictions_for_stop(

View File

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

View File

@ -682,6 +682,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
self.entry.async_create_background_task( self.entry.async_create_background_task(
self.hass, self._async_connected(), "rpc device init", eager_start=True self.hass, self._async_connected(), "rpc device init", eager_start=True
) )
# Make sure entities are marked available
self.async_set_updated_data(None) self.async_set_updated_data(None)
elif update_type is RpcUpdateType.DISCONNECTED: elif update_type is RpcUpdateType.DISCONNECTED:
self.entry.async_create_background_task( self.entry.async_create_background_task(
@ -690,6 +691,8 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
"rpc device disconnected", "rpc device disconnected",
eager_start=True, eager_start=True,
) )
# Make sure entities are marked as unavailable
self.async_set_updated_data(None)
elif update_type is RpcUpdateType.STATUS: elif update_type is RpcUpdateType.STATUS:
self.async_set_updated_data(None) self.async_set_updated_data(None)
if self.sleep_period: if self.sleep_period:
@ -711,7 +714,8 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
"""Shutdown the coordinator.""" """Shutdown the coordinator."""
if self.device.connected: if self.device.connected:
try: try:
await async_stop_scanner(self.device) if not self.sleep_period:
await async_stop_scanner(self.device)
await super().shutdown() await super().shutdown()
except InvalidAuthError: except InvalidAuthError:
self.entry.async_start_reauth(self.hass) self.entry.async_start_reauth(self.hass)

View File

@ -358,6 +358,14 @@ class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]):
self._attr_unique_id = f"{coordinator.mac}-{key}" self._attr_unique_id = f"{coordinator.mac}-{key}"
self._attr_name = get_rpc_entity_name(coordinator.device, 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 @property
def status(self) -> dict: def status(self) -> dict:
"""Device status by entity key.""" """Device status by entity key."""

View File

@ -9,7 +9,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aioshelly"], "loggers": ["aioshelly"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["aioshelly==11.2.0"], "requirements": ["aioshelly==11.2.4"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_http._tcp.local.", "type": "_http._tcp.local.",

View File

@ -172,10 +172,17 @@ async def async_browse_media(
# Check for config entry specifier, and extract Spotify URI # Check for config entry specifier, and extract Spotify URI
parsed_url = yarl.URL(media_content_id) parsed_url = yarl.URL(media_content_id)
host = parsed_url.host
if ( if (
parsed_url.host is None host is None
or (entry := hass.config_entries.async_get_entry(parsed_url.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) or not isinstance(entry.runtime_data, HomeAssistantSpotifyData)
): ):
raise BrowseError("Invalid Spotify account specified") raise BrowseError("Invalid Spotify account specified")

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/thethingsnetwork", "documentation": "https://www.home-assistant.io/integrations/thethingsnetwork",
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["ttn_client==1.1.0"] "requirements": ["ttn_client==1.2.0"]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/tplink_omada", "documentation": "https://www.home-assistant.io/integrations/tplink_omada",
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["tplink-omada-client==1.3.12"] "requirements": ["tplink-omada-client==1.4.2"]
} }

View File

@ -40,6 +40,7 @@ SKU_TO_BASE_DEVICE = {
"LAP-C202S-WUSR": "Core200S", # Alt ID Model Core200S "LAP-C202S-WUSR": "Core200S", # Alt ID Model Core200S
"Core300S": "Core300S", "Core300S": "Core300S",
"LAP-C301S-WJP": "Core300S", # Alt ID Model Core300S "LAP-C301S-WJP": "Core300S", # Alt ID Model Core300S
"LAP-C301S-WAAA": "Core300S", # Alt ID Model Core300S
"Core400S": "Core400S", "Core400S": "Core400S",
"LAP-C401S-WJP": "Core400S", # Alt ID Model Core400S "LAP-C401S-WJP": "Core400S", # Alt ID Model Core400S
"LAP-C401S-WUSR": "Core400S", # Alt ID Model Core400S "LAP-C401S-WUSR": "Core400S", # Alt ID Model Core400S

View File

@ -46,7 +46,9 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
except WLEDConnectionError: except WLEDConnectionError:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
else: 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( self._abort_if_unique_id_configured(
updates={CONF_HOST: user_input[CONF_HOST]} updates={CONF_HOST: user_input[CONF_HOST]}
) )
@ -56,8 +58,6 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
CONF_HOST: user_input[CONF_HOST], CONF_HOST: user_input[CONF_HOST],
}, },
) )
else:
user_input = {}
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",

View File

@ -92,7 +92,7 @@ def _get_obj_holidays(
subdiv=province, subdiv=province,
years=year, years=year,
language=language, language=language,
categories=set_categories, # type: ignore[arg-type] categories=set_categories,
) )
if (supported_languages := obj_holidays.supported_languages) and language == "en": if (supported_languages := obj_holidays.supported_languages) and language == "en":
for lang in supported_languages: for lang in supported_languages:
@ -102,7 +102,7 @@ def _get_obj_holidays(
subdiv=province, subdiv=province,
years=year, years=year,
language=lang, language=lang,
categories=set_categories, # type: ignore[arg-type] categories=set_categories,
) )
LOGGER.debug("Changing language from %s to %s", language, lang) LOGGER.debug("Changing language from %s to %s", language, lang)
return obj_holidays return obj_holidays

View File

@ -7,5 +7,5 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["holidays"], "loggers": ["holidays"],
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["holidays==0.53"] "requirements": ["holidays==0.55"]
} }

View File

@ -24,7 +24,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024 MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 8 MINOR_VERSION: Final = 8
PATCH_VERSION: Final = "2" PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)

View File

@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2
aiodiscover==2.1.0 aiodiscover==2.1.0
aiodns==3.2.0 aiodns==3.2.0
aiohttp-fast-zlib==0.1.1 aiohttp-fast-zlib==0.1.1
aiohttp==3.10.3 aiohttp==3.10.5
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
aiozoneinfo==0.2.1 aiozoneinfo==0.2.1
astral==2.2 astral==2.2

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2024.8.2" version = "2024.8.3"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"
@ -24,7 +24,7 @@ classifiers = [
requires-python = ">=3.12.0" requires-python = ">=3.12.0"
dependencies = [ dependencies = [
"aiodns==3.2.0", "aiodns==3.2.0",
"aiohttp==3.10.3", "aiohttp==3.10.5",
"aiohttp_cors==0.7.0", "aiohttp_cors==0.7.0",
"aiohttp-fast-zlib==0.1.1", "aiohttp-fast-zlib==0.1.1",
"aiozoneinfo==0.2.1", "aiozoneinfo==0.2.1",

View File

@ -4,7 +4,7 @@
# Home Assistant Core # Home Assistant Core
aiodns==3.2.0 aiodns==3.2.0
aiohttp==3.10.3 aiohttp==3.10.5
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
aiohttp-fast-zlib==0.1.1 aiohttp-fast-zlib==0.1.1
aiozoneinfo==0.2.1 aiozoneinfo==0.2.1

View File

@ -255,10 +255,10 @@ aioguardian==2022.07.0
aioharmony==0.2.10 aioharmony==0.2.10
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==3.2.2 aiohomekit==3.2.3
# homeassistant.components.hue # homeassistant.components.hue
aiohue==4.7.2 aiohue==4.7.3
# homeassistant.components.imap # homeassistant.components.imap
aioimaplib==1.1.0 aioimaplib==1.1.0
@ -359,7 +359,7 @@ aioruuvigateway==0.1.0
aiosenz==1.0.0 aiosenz==1.0.0
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==11.2.0 aioshelly==11.2.4
# homeassistant.components.skybell # homeassistant.components.skybell
aioskybell==22.7.0 aioskybell==22.7.0
@ -989,7 +989,7 @@ google-cloud-texttospeech==2.16.3
google-generativeai==0.6.0 google-generativeai==0.6.0
# homeassistant.components.nest # homeassistant.components.nest
google-nest-sdm==4.0.6 google-nest-sdm==4.0.7
# homeassistant.components.google_travel_time # homeassistant.components.google_travel_time
googlemaps==2.5.1 googlemaps==2.5.1
@ -1096,7 +1096,7 @@ hole==0.8.0
# homeassistant.components.holiday # homeassistant.components.holiday
# homeassistant.components.workday # homeassistant.components.workday
holidays==0.53 holidays==0.55
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20240809.0 home-assistant-frontend==20240809.0
@ -1759,7 +1759,7 @@ pyblu==0.4.0
pybotvac==0.0.25 pybotvac==0.0.25
# homeassistant.components.braviatv # homeassistant.components.braviatv
pybravia==0.3.3 pybravia==0.3.4
# homeassistant.components.nissan_leaf # homeassistant.components.nissan_leaf
pycarwings2==2.14 pycarwings2==2.14
@ -1912,7 +1912,7 @@ pyhiveapi==0.5.16
pyhomematic==0.1.77 pyhomematic==0.1.77
# homeassistant.components.homeworks # homeassistant.components.homeworks
pyhomeworks==1.1.1 pyhomeworks==1.1.2
# homeassistant.components.ialarm # homeassistant.components.ialarm
pyialarm==2.2.0 pyialarm==2.2.0
@ -2341,7 +2341,7 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3 python-ripple-api==0.0.3
# homeassistant.components.roborock # homeassistant.components.roborock
python-roborock==2.5.0 python-roborock==2.6.0
# homeassistant.components.smarttub # homeassistant.components.smarttub
python-smarttub==0.0.36 python-smarttub==0.0.36
@ -2792,7 +2792,7 @@ total-connect-client==2024.5
tp-connected==0.0.4 tp-connected==0.0.4
# homeassistant.components.tplink_omada # homeassistant.components.tplink_omada
tplink-omada-client==1.3.12 tplink-omada-client==1.4.2
# homeassistant.components.transmission # homeassistant.components.transmission
transmission-rpc==7.0.3 transmission-rpc==7.0.3
@ -2801,7 +2801,7 @@ transmission-rpc==7.0.3
ttls==1.8.3 ttls==1.8.3
# homeassistant.components.thethingsnetwork # homeassistant.components.thethingsnetwork
ttn_client==1.1.0 ttn_client==1.2.0
# homeassistant.components.tuya # homeassistant.components.tuya
tuya-device-sharing-sdk==0.1.9 tuya-device-sharing-sdk==0.1.9
@ -2936,7 +2936,7 @@ xbox-webapi==2.0.11
xiaomi-ble==0.30.2 xiaomi-ble==0.30.2
# homeassistant.components.knx # homeassistant.components.knx
xknx==3.1.0 xknx==3.1.1
# homeassistant.components.knx # homeassistant.components.knx
xknxproject==3.7.1 xknxproject==3.7.1
@ -2959,7 +2959,7 @@ yalesmartalarmclient==0.3.9
yalexs-ble==2.4.3 yalexs-ble==2.4.3
# homeassistant.components.august # homeassistant.components.august
yalexs==6.4.3 yalexs==8.4.1
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.14 yeelight==0.7.14

View File

@ -240,10 +240,10 @@ aioguardian==2022.07.0
aioharmony==0.2.10 aioharmony==0.2.10
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==3.2.2 aiohomekit==3.2.3
# homeassistant.components.hue # homeassistant.components.hue
aiohue==4.7.2 aiohue==4.7.3
# homeassistant.components.imap # homeassistant.components.imap
aioimaplib==1.1.0 aioimaplib==1.1.0
@ -341,7 +341,7 @@ aioruuvigateway==0.1.0
aiosenz==1.0.0 aiosenz==1.0.0
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==11.2.0 aioshelly==11.2.4
# homeassistant.components.skybell # homeassistant.components.skybell
aioskybell==22.7.0 aioskybell==22.7.0
@ -833,7 +833,7 @@ google-cloud-pubsub==2.13.11
google-generativeai==0.6.0 google-generativeai==0.6.0
# homeassistant.components.nest # homeassistant.components.nest
google-nest-sdm==4.0.6 google-nest-sdm==4.0.7
# homeassistant.components.google_travel_time # homeassistant.components.google_travel_time
googlemaps==2.5.1 googlemaps==2.5.1
@ -916,7 +916,7 @@ hole==0.8.0
# homeassistant.components.holiday # homeassistant.components.holiday
# homeassistant.components.workday # homeassistant.components.workday
holidays==0.53 holidays==0.55
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20240809.0 home-assistant-frontend==20240809.0
@ -1421,7 +1421,7 @@ pyblu==0.4.0
pybotvac==0.0.25 pybotvac==0.0.25
# homeassistant.components.braviatv # homeassistant.components.braviatv
pybravia==0.3.3 pybravia==0.3.4
# homeassistant.components.cloudflare # homeassistant.components.cloudflare
pycfdns==3.0.0 pycfdns==3.0.0
@ -1523,7 +1523,7 @@ pyhiveapi==0.5.16
pyhomematic==0.1.77 pyhomematic==0.1.77
# homeassistant.components.homeworks # homeassistant.components.homeworks
pyhomeworks==1.1.1 pyhomeworks==1.1.2
# homeassistant.components.ialarm # homeassistant.components.ialarm
pyialarm==2.2.0 pyialarm==2.2.0
@ -1850,7 +1850,7 @@ python-picnic-api==1.1.0
python-rabbitair==0.0.8 python-rabbitair==0.0.8
# homeassistant.components.roborock # homeassistant.components.roborock
python-roborock==2.5.0 python-roborock==2.6.0
# homeassistant.components.smarttub # homeassistant.components.smarttub
python-smarttub==0.0.36 python-smarttub==0.0.36
@ -2190,7 +2190,7 @@ toonapi==0.3.0
total-connect-client==2024.5 total-connect-client==2024.5
# homeassistant.components.tplink_omada # homeassistant.components.tplink_omada
tplink-omada-client==1.3.12 tplink-omada-client==1.4.2
# homeassistant.components.transmission # homeassistant.components.transmission
transmission-rpc==7.0.3 transmission-rpc==7.0.3
@ -2199,7 +2199,7 @@ transmission-rpc==7.0.3
ttls==1.8.3 ttls==1.8.3
# homeassistant.components.thethingsnetwork # homeassistant.components.thethingsnetwork
ttn_client==1.1.0 ttn_client==1.2.0
# homeassistant.components.tuya # homeassistant.components.tuya
tuya-device-sharing-sdk==0.1.9 tuya-device-sharing-sdk==0.1.9
@ -2316,7 +2316,7 @@ xbox-webapi==2.0.11
xiaomi-ble==0.30.2 xiaomi-ble==0.30.2
# homeassistant.components.knx # homeassistant.components.knx
xknx==3.1.0 xknx==3.1.1
# homeassistant.components.knx # homeassistant.components.knx
xknxproject==3.7.1 xknxproject==3.7.1
@ -2336,7 +2336,7 @@ yalesmartalarmclient==0.3.9
yalexs-ble==2.4.3 yalexs-ble==2.4.3
# homeassistant.components.august # homeassistant.components.august
yalexs==6.4.3 yalexs==8.4.1
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.14 yeelight==0.7.14

View File

@ -124,7 +124,6 @@ EXCEPTIONS = {
"PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201 "PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201
"aiocomelit", # https://github.com/chemelli74/aiocomelit/pull/138 "aiocomelit", # https://github.com/chemelli74/aiocomelit/pull/138
"aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180 "aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180
"aiohappyeyeballs", # Python-2.0.1
"aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94 "aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94
"aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8 "aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8
"aioruuvigateway", # https://github.com/akx/aioruuvigateway/pull/6 "aioruuvigateway", # https://github.com/akx/aioruuvigateway/pull/6

View File

@ -253,3 +253,32 @@ async def test_zeroconf_flow_abort_old_firmware(hass: HomeAssistant) -> None:
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "invalid_version" 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)

View File

@ -70,6 +70,7 @@ async def test_discovery_remote(
{ {
"current_activity": current_activity, "current_activity": current_activity,
"activity_list": activity_list, "activity_list": activity_list,
"supported_features": 4,
}, },
) )
msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) 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( hass.states.async_set(
"remote.unknown", "remote.unknown",
"on", "on",
{"current_activity": "UNKNOWN"}, {
"current_activity": "UNKNOWN",
"supported_features": 4,
},
) )
hass.states.async_set( hass.states.async_set(
"remote.tv", "remote.tv",
"on", "on",
{"current_activity": "TV", "activity_list": ["TV", "MUSIC", "DVD"]}, {
"current_activity": "TV",
"activity_list": ["TV", "MUSIC", "DVD"],
"supported_features": 4,
},
) )
hass.states.async_set( hass.states.async_set(
"remote.music", "remote.music",
"on", "on",
{"current_activity": "MUSIC", "activity_list": ["TV", "MUSIC", "DVD"]}, {
"current_activity": "MUSIC",
"activity_list": ["TV", "MUSIC", "DVD"],
"supported_features": 4,
},
) )
hass.states.async_set( hass.states.async_set(
"remote.dvd", "remote.dvd",
"on", "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") properties = await reported_properties(hass, "remote#unknown")

View File

@ -3,6 +3,7 @@
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from yalexs.manager.ratelimit import _RateLimitChecker
@pytest.fixture(name="mock_discovery", autouse=True) @pytest.fixture(name="mock_discovery", autouse=True)
@ -12,3 +13,10 @@ def mock_discovery_fixture():
"homeassistant.components.august.data.discovery_flow.async_create_flow" "homeassistant.components.august.data.discovery_flow.async_create_flow"
) as mock_discovery: ) as mock_discovery:
yield 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

View File

@ -25,7 +25,7 @@ from yalexs.activity import (
DoorOperationActivity, DoorOperationActivity,
LockOperationActivity, LockOperationActivity,
) )
from yalexs.authenticator import AuthenticationState from yalexs.authenticator_common import AuthenticationState
from yalexs.const import Brand from yalexs.const import Brand
from yalexs.doorbell import Doorbell, DoorbellDetail from yalexs.doorbell import Doorbell, DoorbellDetail
from yalexs.lock import Lock, LockDetail from yalexs.lock import Lock, LockDetail

View File

@ -2,7 +2,7 @@
from unittest.mock import patch 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 yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from homeassistant import config_entries from homeassistant import config_entries

View File

@ -50,5 +50,5 @@ async def _patched_refresh_access_token(
) )
await august_gateway.async_refresh_access_token_if_needed() await august_gateway.async_refresh_access_token_if_needed()
refresh_access_token_mock.assert_called() 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 assert august_gateway.authentication.access_token_expires == new_token_expire_time

View File

@ -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( aioclient_mock.get(
"https://habitica.com/api/v3/tasks/user", "https://habitica.com/api/v3/tasks/user",
json={ 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( aioclient_mock.post(
"https://habitica.com/api/v3/tasks/user", "https://habitica.com/api/v3/tasks/user",

View File

@ -1288,7 +1288,9 @@
}, },
{ {
"button": { "button": {
"last_event": "short_release" "button_report": {
"event": "short_release"
}
}, },
"id": "c658d3d8-a013-4b81-8ac6-78b248537e70", "id": "c658d3d8-a013-4b81-8ac6-78b248537e70",
"id_v1": "/sensors/50", "id_v1": "/sensors/50",
@ -1327,7 +1329,9 @@
}, },
{ {
"button": { "button": {
"last_event": "short_release" "button_report": {
"event": "short_release"
}
}, },
"id": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", "id": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75",
"id_v1": "/sensors/10", "id_v1": "/sensors/10",
@ -1366,7 +1370,9 @@
}, },
{ {
"button": { "button": {
"last_event": "short_release" "button_report": {
"event": "short_release"
}
}, },
"id": "31cffcda-efc2-401f-a152-e10db3eed232", "id": "31cffcda-efc2-401f-a152-e10db3eed232",
"id_v1": "/sensors/5", "id_v1": "/sensors/5",

View File

@ -854,6 +854,27 @@ async def test_rpc_runs_connected_events_when_initialized(
assert call.script_list() in mock_rpc_device.mock_calls 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( async def test_block_sleeping_device_connection_error(
hass: HomeAssistant, hass: HomeAssistant,
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,

View File

@ -43,7 +43,7 @@ from . import (
register_entity, 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 RELAY_BLOCK_ID = 0
SENSOR_BLOCK_ID = 3 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) entry = entity_registry.async_get(entity_id)
assert not entry 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

View File

@ -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

View File

@ -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': <MediaClass.PLAYLIST: 'playlist'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'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': <MediaClass.ARTIST: 'artist'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'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': <MediaClass.ALBUM: 'album'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'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': <MediaClass.TRACK: 'track'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'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': <MediaClass.PODCAST: 'podcast'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'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': <MediaClass.TRACK: 'track'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'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': <MediaClass.ARTIST: 'artist'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'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': <MediaClass.TRACK: 'track'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'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': <MediaClass.GENRE: 'genre'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'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': <MediaClass.PLAYLIST: 'playlist'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'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': <MediaClass.ALBUM: 'album'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/new_releases',
'media_content_type': 'spotify://new_releases',
'thumbnail': None,
'title': 'New Releases',
}),
]),
'children_media_class': <MediaClass.DIRECTORY: 'directory'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'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': <MediaClass.TRACK: 'track'>,
'media_class': <MediaClass.PLAYLIST: 'playlist'>,
'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/spotify:playlist:unique_identifier_00',
'media_content_type': 'spotify://playlist',
'thumbnail': None,
'title': 'Playlist1',
}),
]),
'children_media_class': <MediaClass.PLAYLIST: 'playlist'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'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': <MediaClass.TRACK: 'track'>,
'media_class': <MediaClass.PLAYLIST: 'playlist'>,
'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/spotify:playlist:unique_identifier_00',
'media_content_type': 'spotify://playlist',
'thumbnail': None,
'title': 'Playlist1',
}),
]),
'children_media_class': <MediaClass.PLAYLIST: 'playlist'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'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': <MediaClass.TRACK: 'track'>,
'media_class': <MediaClass.PLAYLIST: 'playlist'>,
'media_content_id': 'spotify://32oesphrnacjcf7vw5bf6odx3/spotify:playlist:unique_identifier_00',
'media_content_type': 'spotify://playlist',
'thumbnail': None,
'title': 'Playlist1',
}),
]),
'children_media_class': <MediaClass.PLAYLIST: 'playlist'>,
'media_class': <MediaClass.DIRECTORY: 'directory'>,
'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': <MediaClass.APP: 'app'>,
'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': <MediaClass.APP: 'app'>,
'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': <MediaClass.APP: 'app'>,
'media_class': <MediaClass.APP: 'app'>,
'media_content_id': 'spotify://',
'media_content_type': 'spotify',
'not_shown': 0,
'thumbnail': 'https://brands.home-assistant.io/_/spotify/logo.png',
'title': 'Spotify',
})
# ---

View File

@ -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