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:
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(

View File

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

View File

@ -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,
}
),

View File

@ -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"]
}

View File

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

View File

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

View File

@ -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"]
}

View File

@ -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."]
}

View File

@ -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"]
}

View File

@ -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."]
}

View File

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

View File

@ -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%]"
}
}
}

View File

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

View File

@ -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"
],

View File

@ -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"),

View File

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

View File

@ -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"]
}

View File

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

View File

@ -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"
]
}

View File

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

View File

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

View File

@ -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.",

View File

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

View File

@ -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"]
}

View File

@ -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"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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["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,
"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")

View File

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

View File

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

View File

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

View File

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

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(
"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",

View File

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

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

View File

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

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