Merge pull request #38332 from home-assistant/rc

Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Phil Bruckner <pnbruckner@gmail.com>
Co-authored-by: Joakim Plate <elupus@ecce.se>
Co-authored-by: Jeroen Van den Keybus <jeroen.vandenkeybus@gmail.com>
Co-authored-by: Mister Wil <1091741+MisterWil@users.noreply.github.com>
Co-authored-by: Greg Dowling <pavoni@users.noreply.github.com>
Co-authored-by: Marcio Granzotto Rodrigues <oscensores@gmail.com>
Co-authored-by: Teemu R <tpr@iki.fi>
Co-authored-by: Kyle Hendricks <kylehendricks@users.noreply.github.com>
This commit is contained in:
Franck Nijhof 2020-07-28 20:24:28 +02:00 committed by GitHub
commit d24b02646e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 369 additions and 182 deletions

View File

@ -1,5 +1,4 @@
"""Support for AdGuard Home.""" """Support for AdGuard Home."""
from distutils.version import LooseVersion
import logging import logging
from typing import Any, Dict from typing import Any, Dict
@ -11,7 +10,6 @@ from homeassistant.components.adguard.const import (
DATA_ADGUARD_CLIENT, DATA_ADGUARD_CLIENT,
DATA_ADGUARD_VERION, DATA_ADGUARD_VERION,
DOMAIN, DOMAIN,
MIN_ADGUARD_HOME_VERSION,
SERVICE_ADD_URL, SERVICE_ADD_URL,
SERVICE_DISABLE_URL, SERVICE_DISABLE_URL,
SERVICE_ENABLE_URL, SERVICE_ENABLE_URL,
@ -67,16 +65,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
hass.data.setdefault(DOMAIN, {})[DATA_ADGUARD_CLIENT] = adguard hass.data.setdefault(DOMAIN, {})[DATA_ADGUARD_CLIENT] = adguard
try: try:
version = await adguard.version() await adguard.version()
except AdGuardHomeConnectionError as exception: except AdGuardHomeConnectionError as exception:
raise ConfigEntryNotReady from exception raise ConfigEntryNotReady from exception
if version and LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
_LOGGER.error(
"This integration requires AdGuard Home v0.99.0 or higher to work correctly"
)
raise ConfigEntryNotReady
for component in "sensor", "switch": for component in "sensor", "switch":
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component) hass.config_entries.async_forward_entry_setup(entry, component)

View File

@ -1,12 +1,11 @@
"""Config flow to configure the AdGuard Home integration.""" """Config flow to configure the AdGuard Home integration."""
from distutils.version import LooseVersion
import logging import logging
from adguardhome import AdGuardHome, AdGuardHomeConnectionError from adguardhome import AdGuardHome, AdGuardHomeConnectionError
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.adguard.const import DOMAIN, MIN_ADGUARD_HOME_VERSION from homeassistant.components.adguard.const import DOMAIN
from homeassistant.config_entries import ConfigFlow from homeassistant.config_entries import ConfigFlow
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
@ -79,20 +78,11 @@ class AdGuardHomeFlowHandler(ConfigFlow):
) )
try: try:
version = await adguard.version() await adguard.version()
except AdGuardHomeConnectionError: except AdGuardHomeConnectionError:
errors["base"] = "connection_error" errors["base"] = "connection_error"
return await self._show_setup_form(errors) return await self._show_setup_form(errors)
if version and LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
return self.async_abort(
reason="adguard_home_outdated",
description_placeholders={
"current_version": version,
"minimal_version": MIN_ADGUARD_HOME_VERSION,
},
)
return self.async_create_entry( return self.async_create_entry(
title=user_input[CONF_HOST], title=user_input[CONF_HOST],
data={ data={
@ -160,20 +150,11 @@ class AdGuardHomeFlowHandler(ConfigFlow):
) )
try: try:
version = await adguard.version() await adguard.version()
except AdGuardHomeConnectionError: except AdGuardHomeConnectionError:
errors["base"] = "connection_error" errors["base"] = "connection_error"
return await self._show_hassio_form(errors) return await self._show_hassio_form(errors)
if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
return self.async_abort(
reason="adguard_home_addon_outdated",
description_placeholders={
"current_version": version,
"minimal_version": MIN_ADGUARD_HOME_VERSION,
},
)
return self.async_create_entry( return self.async_create_entry(
title=self._hassio_discovery["addon"], title=self._hassio_discovery["addon"],
data={ data={

View File

@ -7,8 +7,6 @@ DATA_ADGUARD_VERION = "adguard_version"
CONF_FORCE = "force" CONF_FORCE = "force"
MIN_ADGUARD_HOME_VERSION = "v0.99.0"
SERVICE_ADD_URL = "add_url" SERVICE_ADD_URL = "add_url"
SERVICE_DISABLE_URL = "disable_url" SERVICE_DISABLE_URL = "disable_url"
SERVICE_ENABLE_URL = "enable_url" SERVICE_ENABLE_URL = "enable_url"

View File

@ -19,8 +19,6 @@
}, },
"error": { "connection_error": "Failed to connect." }, "error": { "connection_error": "Failed to connect." },
"abort": { "abort": {
"adguard_home_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}.",
"adguard_home_addon_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}. Please update your Hass.io AdGuard Home add-on.",
"existing_instance_updated": "Updated existing configuration.", "existing_instance_updated": "Updated existing configuration.",
"single_instance_allowed": "Only a single configuration of AdGuard Home is allowed." "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed."
} }

View File

@ -455,6 +455,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
self._async_detach_triggers() self._async_detach_triggers()
self._async_detach_triggers = None self._async_detach_triggers = None
await self.action_script.async_stop()
self.async_write_ha_state() self.async_write_ha_state()
async def _async_attach_triggers( async def _async_attach_triggers(

View File

@ -2,7 +2,7 @@
"domain": "discovery", "domain": "discovery",
"name": "Discovery", "name": "Discovery",
"documentation": "https://www.home-assistant.io/integrations/discovery", "documentation": "https://www.home-assistant.io/integrations/discovery",
"requirements": ["netdisco==2.8.0"], "requirements": ["netdisco==2.8.1"],
"after_dependencies": ["zeroconf"], "after_dependencies": ["zeroconf"],
"codeowners": [], "codeowners": [],
"quality_scale": "internal" "quality_scale": "internal"

View File

@ -86,6 +86,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION])
friendly_name = discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME] friendly_name = discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME]
if self._host_already_configured(parsed_url.hostname):
return self.async_abort(reason="already_configured")
# pylint: disable=no-member # pylint: disable=no-member
self.context["title_placeholders"] = {"name": friendly_name} self.context["title_placeholders"] = {"name": friendly_name}
@ -158,6 +161,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title=validated[CONF_NAME], data=data) return self.async_create_entry(title=validated[CONF_NAME], data=data)
def _host_already_configured(self, host):
"""See if we already have a harmony entry matching the host."""
for entry in self._async_current_entries():
if entry.data[CONF_HOST] == host:
return True
return False
def _options_from_user_input(user_input): def _options_from_user_input(user_input):
options = {} options = {}

View File

@ -112,21 +112,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
try: try:
async with async_timeout.timeout(10): async with async_timeout.timeout(10):
device_info = await async_get_device_info(pv_request) device_info = await async_get_device_info(pv_request)
except HUB_EXCEPTIONS:
_LOGGER.error("Connection error to PowerView hub: %s", hub_address)
raise ConfigEntryNotReady
if not device_info:
_LOGGER.error("Unable to initialize PowerView hub: %s", hub_address)
raise ConfigEntryNotReady
async with async_timeout.timeout(10):
rooms = Rooms(pv_request) rooms = Rooms(pv_request)
room_data = _async_map_data_by_id((await rooms.get_resources())[ROOM_DATA]) room_data = _async_map_data_by_id((await rooms.get_resources())[ROOM_DATA])
async with async_timeout.timeout(10):
scenes = Scenes(pv_request) scenes = Scenes(pv_request)
scene_data = _async_map_data_by_id((await scenes.get_resources())[SCENE_DATA]) scene_data = _async_map_data_by_id(
(await scenes.get_resources())[SCENE_DATA]
)
async with async_timeout.timeout(10):
shades = Shades(pv_request) shades = Shades(pv_request)
shade_data = _async_map_data_by_id((await shades.get_resources())[SHADE_DATA]) shade_data = _async_map_data_by_id(
(await shades.get_resources())[SHADE_DATA]
)
except HUB_EXCEPTIONS:
_LOGGER.error("Connection error to PowerView hub: %s", hub_address)
raise ConfigEntryNotReady
if not device_info:
_LOGGER.error("Unable to initialize PowerView hub: %s", hub_address)
raise ConfigEntryNotReady
async def async_update_data(): async def async_update_data():
"""Fetch data from shade endpoint.""" """Fetch data from shade endpoint."""

View File

@ -2,6 +2,7 @@
import asyncio import asyncio
from aiohttp.client_exceptions import ServerDisconnectedError
from aiopvapi.helpers.aiorequest import PvApiConnectionError from aiopvapi.helpers.aiorequest import PvApiConnectionError
DOMAIN = "hunterdouglas_powerview" DOMAIN = "hunterdouglas_powerview"
@ -64,7 +65,7 @@ PV_SHADE_DATA = "pv_shade_data"
PV_ROOM_DATA = "pv_room_data" PV_ROOM_DATA = "pv_room_data"
COORDINATOR = "coordinator" COORDINATOR = "coordinator"
HUB_EXCEPTIONS = (asyncio.TimeoutError, PvApiConnectionError) HUB_EXCEPTIONS = (ServerDisconnectedError, asyncio.TimeoutError, PvApiConnectionError)
LEGACY_DEVICE_SUB_REVISION = 1 LEGACY_DEVICE_SUB_REVISION = 1
LEGACY_DEVICE_REVISION = 0 LEGACY_DEVICE_REVISION = 0

View File

@ -120,7 +120,7 @@ def determine_zones(receiver):
out = {"zone2": False, "zone3": False} out = {"zone2": False, "zone3": False}
try: try:
_LOGGER.debug("Checking for zone 2 capability") _LOGGER.debug("Checking for zone 2 capability")
receiver.raw("ZPW") receiver.raw("ZPWQSTN")
out["zone2"] = True out["zone2"] = True
except ValueError as error: except ValueError as error:
if str(error) != TIMEOUT_MESSAGE: if str(error) != TIMEOUT_MESSAGE:
@ -128,7 +128,7 @@ def determine_zones(receiver):
_LOGGER.debug("Zone 2 timed out, assuming no functionality") _LOGGER.debug("Zone 2 timed out, assuming no functionality")
try: try:
_LOGGER.debug("Checking for zone 3 capability") _LOGGER.debug("Checking for zone 3 capability")
receiver.raw("PW3") receiver.raw("PW3QSTN")
out["zone3"] = True out["zone3"] = True
except ValueError as error: except ValueError as error:
if str(error) != TIMEOUT_MESSAGE: if str(error) != TIMEOUT_MESSAGE:

View File

@ -6,7 +6,7 @@ from aiohttp.client_exceptions import ServerDisconnectedError
from onvif import ONVIFCamera, ONVIFService from onvif import ONVIFCamera, ONVIFService
from zeep.exceptions import Fault from zeep.exceptions import Fault
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback
from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -114,6 +114,7 @@ class EventManager:
async def async_pull_messages(self, _now: dt = None) -> None: async def async_pull_messages(self, _now: dt = None) -> None:
"""Pull messages from device.""" """Pull messages from device."""
if self.hass.state == CoreState.running:
try: try:
pullpoint = self.device.create_pullpoint_service() pullpoint = self.device.create_pullpoint_service()
req = pullpoint.create_type("PullMessages") req = pullpoint.create_type("PullMessages")

View File

@ -3,6 +3,6 @@
"name": "Tesla Powerwall", "name": "Tesla Powerwall",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/powerwall", "documentation": "https://www.home-assistant.io/integrations/powerwall",
"requirements": ["tesla-powerwall==0.2.11"], "requirements": ["tesla-powerwall==0.2.12"],
"codeowners": ["@bdraco", "@jrester"] "codeowners": ["@bdraco", "@jrester"]
} }

View File

@ -250,7 +250,7 @@ class SamsungTVWSBridge(SamsungTVBridge):
host=self.host, host=self.host,
port=self.port, port=self.port,
token=self.token, token=self.token,
timeout=1, timeout=10,
name=VALUE_CONF_NAME, name=VALUE_CONF_NAME,
) )
self._remote.open() self._remote.open()

View File

@ -5,7 +5,12 @@ from requests.exceptions import ConnectTimeout, HTTPError
from skybellpy import Skybell from skybellpy import Skybell
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_PASSWORD,
CONF_USERNAME,
__version__,
)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
@ -20,6 +25,8 @@ DOMAIN = "skybell"
DEFAULT_CACHEDB = "./skybell_cache.pickle" DEFAULT_CACHEDB = "./skybell_cache.pickle"
DEFAULT_ENTITY_NAMESPACE = "skybell" DEFAULT_ENTITY_NAMESPACE = "skybell"
AGENT_IDENTIFIER = f"HomeAssistant/{__version__}"
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
@ -42,7 +49,11 @@ def setup(hass, config):
try: try:
cache = hass.config.path(DEFAULT_CACHEDB) cache = hass.config.path(DEFAULT_CACHEDB)
skybell = Skybell( skybell = Skybell(
username=username, password=password, get_devices=True, cache_path=cache username=username,
password=password,
get_devices=True,
cache_path=cache,
agent_identifier=AGENT_IDENTIFIER,
) )
hass.data[DOMAIN] = skybell hass.data[DOMAIN] = skybell

View File

@ -2,6 +2,6 @@
"domain": "skybell", "domain": "skybell",
"name": "SkyBell", "name": "SkyBell",
"documentation": "https://www.home-assistant.io/integrations/skybell", "documentation": "https://www.home-assistant.io/integrations/skybell",
"requirements": ["skybellpy==0.4.0"], "requirements": ["skybellpy==0.6.1"],
"codeowners": [] "codeowners": []
} }

View File

@ -6,7 +6,12 @@ import speedtest
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL from homeassistant.const import (
CONF_MONITORED_CONDITIONS,
CONF_SCAN_INTERVAL,
EVENT_HOMEASSISTANT_STARTED,
)
from homeassistant.core import CoreState
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -70,10 +75,25 @@ async def async_setup_entry(hass, config_entry):
coordinator = SpeedTestDataCoordinator(hass, config_entry) coordinator = SpeedTestDataCoordinator(hass, config_entry)
await coordinator.async_setup() await coordinator.async_setup()
if not config_entry.options[CONF_MANUAL]: async def _enable_scheduled_speedtests(*_):
"""Activate the data update coordinator."""
coordinator.update_interval = timedelta(
minutes=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
)
await coordinator.async_refresh() await coordinator.async_refresh()
if not config_entry.options[CONF_MANUAL]:
if hass.state == CoreState.running:
await _enable_scheduled_speedtests()
if not coordinator.last_update_success: if not coordinator.last_update_success:
raise ConfigEntryNotReady raise ConfigEntryNotReady
else:
# Running a speed test during startup can prevent
# integrations from being able to setup because it
# can saturate the network interface.
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STARTED, _enable_scheduled_speedtests
)
hass.data[DOMAIN] = coordinator hass.data[DOMAIN] = coordinator
@ -107,12 +127,6 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator):
super().__init__( super().__init__(
self.hass, _LOGGER, name=DOMAIN, update_method=self.async_update, self.hass, _LOGGER, name=DOMAIN, update_method=self.async_update,
) )
if not self.config_entry.options.get(CONF_MANUAL):
self.update_interval = timedelta(
minutes=self.config_entry.options.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
)
)
def update_servers(self): def update_servers(self):
"""Update list of test servers.""" """Update list of test servers."""

View File

@ -12,7 +12,6 @@ from .const import (
ATTR_SERVER_ID, ATTR_SERVER_ID,
ATTR_SERVER_NAME, ATTR_SERVER_NAME,
ATTRIBUTION, ATTRIBUTION,
CONF_MANUAL,
DEFAULT_NAME, DEFAULT_NAME,
DOMAIN, DOMAIN,
ICON, ICON,
@ -97,7 +96,6 @@ class SpeedtestSensor(RestoreEntity):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Handle entity which will be added.""" """Handle entity which will be added."""
await super().async_added_to_hass() await super().async_added_to_hass()
if self.coordinator.config_entry.options[CONF_MANUAL]:
state = await self.async_get_last_state() state = await self.async_get_last_state()
if state: if state:
self._state = state.state self._state = state.state

View File

@ -2,7 +2,7 @@
"domain": "ssdp", "domain": "ssdp",
"name": "Simple Service Discovery Protocol (SSDP)", "name": "Simple Service Discovery Protocol (SSDP)",
"documentation": "https://www.home-assistant.io/integrations/ssdp", "documentation": "https://www.home-assistant.io/integrations/ssdp",
"requirements": ["defusedxml==0.6.0", "netdisco==2.8.0"], "requirements": ["defusedxml==0.6.0", "netdisco==2.8.1"],
"after_dependencies": ["zeroconf"], "after_dependencies": ["zeroconf"],
"codeowners": [] "codeowners": []
} }

View File

@ -194,7 +194,9 @@ class VeraDevice(Entity):
slugify(vera_device.name), vera_device.device_id slugify(vera_device.name), vera_device.device_id
) )
self.controller.register(vera_device, self._update_callback) async def async_added_to_hass(self):
"""Subscribe to updates."""
self.controller.register(self.vera_device, self._update_callback)
def _update_callback(self, _device): def _update_callback(self, _device):
"""Update the state.""" """Update the state."""

View File

@ -2,6 +2,6 @@
"domain": "xbox_live", "domain": "xbox_live",
"name": "Xbox Live", "name": "Xbox Live",
"documentation": "https://www.home-assistant.io/integrations/xbox_live", "documentation": "https://www.home-assistant.io/integrations/xbox_live",
"requirements": ["xboxapi==2.0.0"], "requirements": ["xboxapi==2.0.1"],
"codeowners": ["@MartinHjelmare"] "codeowners": ["@MartinHjelmare"]
} }

View File

@ -3,7 +3,7 @@
"name": "Xiaomi Miio", "name": "Xiaomi Miio",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
"requirements": ["construct==2.9.45", "python-miio==0.5.2.1"], "requirements": ["construct==2.9.45", "python-miio==0.5.3"],
"codeowners": ["@rytilahti", "@syssi"], "codeowners": ["@rytilahti", "@syssi"],
"zeroconf": ["_miio._udp.local."] "zeroconf": ["_miio._udp.local."]
} }

View File

@ -1,7 +1,7 @@
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 113 MINOR_VERSION = 113
PATCH_VERSION = "1" PATCH_VERSION = "2"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 1) REQUIRED_PYTHON_VER = (3, 7, 1)

View File

@ -392,12 +392,28 @@ class HomeAssistant:
"""Block until all pending work is done.""" """Block until all pending work is done."""
# To flush out any call_soon_threadsafe # To flush out any call_soon_threadsafe
await asyncio.sleep(0) await asyncio.sleep(0)
start_time: Optional[float] = None
while self._pending_tasks: while self._pending_tasks:
pending = [task for task in self._pending_tasks if not task.done()] pending = [task for task in self._pending_tasks if not task.done()]
self._pending_tasks.clear() self._pending_tasks.clear()
if pending: if pending:
await self._await_and_log_pending(pending) await self._await_and_log_pending(pending)
if start_time is None:
# Avoid calling monotonic() until we know
# we may need to start logging blocked tasks.
start_time = 0
elif start_time == 0:
# If we have waited twice then we set the start
# time
start_time = monotonic()
elif monotonic() - start_time > BLOCK_LOG_TIMEOUT:
# We have waited at least three loops and new tasks
# continue to block. At this point we start
# logging all waiting tasks.
for task in pending:
_LOGGER.debug("Waiting for task: %s", task)
else: else:
await asyncio.sleep(0) await asyncio.sleep(0)

View File

@ -4,7 +4,19 @@ from datetime import datetime
from functools import partial from functools import partial
import itertools import itertools
import logging import logging
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple from types import MappingProxyType
from typing import (
Any,
Callable,
Dict,
List,
Optional,
Sequence,
Set,
Tuple,
Union,
cast,
)
from async_timeout import timeout from async_timeout import timeout
import voluptuous as vol import voluptuous as vol
@ -134,13 +146,13 @@ class _ScriptRun:
self, self,
hass: HomeAssistant, hass: HomeAssistant,
script: "Script", script: "Script",
variables: Optional[Sequence], variables: Dict[str, Any],
context: Optional[Context], context: Optional[Context],
log_exceptions: bool, log_exceptions: bool,
) -> None: ) -> None:
self._hass = hass self._hass = hass
self._script = script self._script = script
self._variables = variables or {} self._variables = variables
self._context = context self._context = context
self._log_exceptions = log_exceptions self._log_exceptions = log_exceptions
self._step = -1 self._step = -1
@ -595,6 +607,9 @@ async def _async_stop_scripts_at_shutdown(hass, event):
) )
_VarsType = Union[Dict[str, Any], MappingProxyType]
class Script: class Script:
"""Representation of a script.""" """Representation of a script."""
@ -617,6 +632,7 @@ class Script:
hass.bus.async_listen_once( hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, partial(_async_stop_scripts_at_shutdown, hass) EVENT_HOMEASSISTANT_STOP, partial(_async_stop_scripts_at_shutdown, hass)
) )
self._top_level = top_level
if top_level: if top_level:
all_scripts.append( all_scripts.append(
{"instance": self, "started_before_shutdown": not hass.is_stopping} {"instance": self, "started_before_shutdown": not hass.is_stopping}
@ -732,14 +748,16 @@ class Script:
self._referenced_entities = referenced self._referenced_entities = referenced
return referenced return referenced
def run(self, variables=None, context=None): def run(
self, variables: Optional[_VarsType] = None, context: Optional[Context] = None
) -> None:
"""Run script.""" """Run script."""
asyncio.run_coroutine_threadsafe( asyncio.run_coroutine_threadsafe(
self.async_run(variables, context), self._hass.loop self.async_run(variables, context), self._hass.loop
).result() ).result()
async def async_run( async def async_run(
self, variables: Optional[Sequence] = None, context: Optional[Context] = None self, variables: Optional[_VarsType] = None, context: Optional[Context] = None
) -> None: ) -> None:
"""Run script.""" """Run script."""
if self.is_running: if self.is_running:
@ -753,11 +771,19 @@ class Script:
self._log("Maximum number of runs exceeded", level=logging.WARNING) self._log("Maximum number of runs exceeded", level=logging.WARNING)
return return
# If this is a top level Script then make a copy of the variables in case they
# are read-only, but more importantly, so as not to leak any variables created
# during the run back to the caller.
if self._top_level:
variables = dict(variables) if variables is not None else {}
if self.script_mode != SCRIPT_MODE_QUEUED: if self.script_mode != SCRIPT_MODE_QUEUED:
cls = _ScriptRun cls = _ScriptRun
else: else:
cls = _QueuedScriptRun cls = _QueuedScriptRun
run = cls(self._hass, self, variables, context, self._log_exceptions) run = cls(
self._hass, self, cast(dict, variables), context, self._log_exceptions
)
self._runs.append(run) self._runs.append(run)
try: try:
@ -795,6 +821,7 @@ class Script:
action[CONF_REPEAT][CONF_SEQUENCE], action[CONF_REPEAT][CONF_SEQUENCE],
f"{self.name}: {step_name}", f"{self.name}: {step_name}",
script_mode=SCRIPT_MODE_PARALLEL, script_mode=SCRIPT_MODE_PARALLEL,
max_runs=self.max_runs,
logger=self._logger, logger=self._logger,
top_level=False, top_level=False,
) )
@ -822,6 +849,7 @@ class Script:
choice[CONF_SEQUENCE], choice[CONF_SEQUENCE],
f"{self.name}: {step_name}: choice {idx}", f"{self.name}: {step_name}: choice {idx}",
script_mode=SCRIPT_MODE_PARALLEL, script_mode=SCRIPT_MODE_PARALLEL,
max_runs=self.max_runs,
logger=self._logger, logger=self._logger,
top_level=False, top_level=False,
) )
@ -836,6 +864,7 @@ class Script:
action[CONF_DEFAULT], action[CONF_DEFAULT],
f"{self.name}: {step_name}: default", f"{self.name}: {step_name}: default",
script_mode=SCRIPT_MODE_PARALLEL, script_mode=SCRIPT_MODE_PARALLEL,
max_runs=self.max_runs,
logger=self._logger, logger=self._logger,
top_level=False, top_level=False,
) )

View File

@ -16,7 +16,7 @@ hass-nabucasa==0.34.7
home-assistant-frontend==20200716.0 home-assistant-frontend==20200716.0
importlib-metadata==1.6.0;python_version<'3.8' importlib-metadata==1.6.0;python_version<'3.8'
jinja2>=2.11.1 jinja2>=2.11.1
netdisco==2.8.0 netdisco==2.8.1
paho-mqtt==1.5.0 paho-mqtt==1.5.0
pip>=8.0.3 pip>=8.0.3
python-slugify==4.0.0 python-slugify==4.0.0

View File

@ -936,7 +936,7 @@ netdata==0.2.0
# homeassistant.components.discovery # homeassistant.components.discovery
# homeassistant.components.ssdp # homeassistant.components.ssdp
netdisco==2.8.0 netdisco==2.8.1
# homeassistant.components.neurio_energy # homeassistant.components.neurio_energy
neurio==0.3.1 neurio==0.3.1
@ -1701,7 +1701,7 @@ python-juicenet==1.0.1
# python-lirc==1.2.3 # python-lirc==1.2.3
# homeassistant.components.xiaomi_miio # homeassistant.components.xiaomi_miio
python-miio==0.5.2.1 python-miio==0.5.3
# homeassistant.components.mpd # homeassistant.components.mpd
python-mpd2==1.0.0 python-mpd2==1.0.0
@ -1945,7 +1945,7 @@ simplisafe-python==9.2.1
sisyphus-control==2.2.1 sisyphus-control==2.2.1
# homeassistant.components.skybell # homeassistant.components.skybell
skybellpy==0.4.0 skybellpy==0.6.1
# homeassistant.components.slack # homeassistant.components.slack
slackclient==2.5.0 slackclient==2.5.0
@ -2080,7 +2080,7 @@ temperusb==1.5.3
# tensorflow==1.13.2 # tensorflow==1.13.2
# homeassistant.components.powerwall # homeassistant.components.powerwall
tesla-powerwall==0.2.11 tesla-powerwall==0.2.12
# homeassistant.components.tesla # homeassistant.components.tesla
teslajsonpy==0.9.3 teslajsonpy==0.9.3
@ -2204,7 +2204,7 @@ wled==0.4.3
xbee-helper==0.0.7 xbee-helper==0.0.7
# homeassistant.components.xbox_live # homeassistant.components.xbox_live
xboxapi==2.0.0 xboxapi==2.0.1
# homeassistant.components.xfinity # homeassistant.components.xfinity
xfinity-gateway==0.0.4 xfinity-gateway==0.0.4

View File

@ -431,7 +431,7 @@ nessclient==0.9.15
# homeassistant.components.discovery # homeassistant.components.discovery
# homeassistant.components.ssdp # homeassistant.components.ssdp
netdisco==2.8.0 netdisco==2.8.1
# homeassistant.components.nexia # homeassistant.components.nexia
nexia==0.9.3 nexia==0.9.3
@ -767,7 +767,7 @@ python-izone==1.1.2
python-juicenet==1.0.1 python-juicenet==1.0.1
# homeassistant.components.xiaomi_miio # homeassistant.components.xiaomi_miio
python-miio==0.5.2.1 python-miio==0.5.3
# homeassistant.components.nest # homeassistant.components.nest
python-nest==4.1.0 python-nest==4.1.0
@ -909,7 +909,7 @@ sunwatcher==0.2.1
tellduslive==0.10.11 tellduslive==0.10.11
# homeassistant.components.powerwall # homeassistant.components.powerwall
tesla-powerwall==0.2.11 tesla-powerwall==0.2.12
# homeassistant.components.tesla # homeassistant.components.tesla
teslajsonpy==0.9.3 teslajsonpy==0.9.3

View File

@ -4,7 +4,7 @@ import aiohttp
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
from homeassistant.components.adguard import config_flow from homeassistant.components.adguard import config_flow
from homeassistant.components.adguard.const import DOMAIN, MIN_ADGUARD_HOME_VERSION from homeassistant.components.adguard.const import DOMAIN
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_PASSWORD, CONF_PASSWORD,
@ -229,52 +229,3 @@ async def test_hassio_connection_error(hass, aioclient_mock):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "hassio_confirm" assert result["step_id"] == "hassio_confirm"
assert result["errors"] == {"base": "connection_error"} assert result["errors"] == {"base": "connection_error"}
async def test_outdated_adguard_version(hass, aioclient_mock):
"""Test we show abort when connecting with unsupported AdGuard version."""
aioclient_mock.get(
f"{'https' if FIXTURE_USER_INPUT[CONF_SSL] else 'http'}"
f"://{FIXTURE_USER_INPUT[CONF_HOST]}"
f":{FIXTURE_USER_INPUT[CONF_PORT]}/control/status",
json={"version": "v0.98.0"},
headers={"Content-Type": "application/json"},
)
flow = config_flow.AdGuardHomeFlowHandler()
flow.hass = hass
result = await flow.async_step_user(user_input=None)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "adguard_home_outdated"
assert result["description_placeholders"] == {
"current_version": "v0.98.0",
"minimal_version": MIN_ADGUARD_HOME_VERSION,
}
async def test_outdated_adguard_addon_version(hass, aioclient_mock):
"""Test we show abort when connecting with unsupported AdGuard add-on version."""
aioclient_mock.get(
"http://mock-adguard:3000/control/status",
json={"version": "v0.98.0"},
headers={"Content-Type": "application/json"},
)
result = await hass.config_entries.flow.async_init(
"adguard",
data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000},
context={"source": "hassio"},
)
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "adguard_home_addon_outdated"
assert result["description_placeholders"] == {
"current_version": "v0.98.0",
"minimal_version": MIN_ADGUARD_HOME_VERSION,
}

View File

@ -1,4 +1,6 @@
"""The tests for the automation component.""" """The tests for the automation component."""
import asyncio
import pytest import pytest
from homeassistant.components import logbook from homeassistant.components import logbook
@ -12,10 +14,11 @@ from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_NAME, ATTR_NAME,
EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STARTED,
SERVICE_TURN_OFF,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
) )
from homeassistant.core import Context, CoreState, State from homeassistant.core import Context, CoreState, State, callback
from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.exceptions import HomeAssistantError, Unauthorized
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -553,6 +556,58 @@ async def test_reload_config_handles_load_fails(hass, calls):
assert len(calls) == 2 assert len(calls) == 2
@pytest.mark.parametrize("service", ["turn_off", "reload"])
async def test_automation_stops(hass, calls, service):
"""Test that turning off / reloading an automation stops any running actions."""
entity_id = "automation.hello"
test_entity = "test.entity"
config = {
automation.DOMAIN: {
"alias": "hello",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [
{"event": "running"},
{"wait_template": "{{ is_state('test.entity', 'goodbye') }}"},
{"service": "test.automation"},
],
}
}
assert await async_setup_component(hass, automation.DOMAIN, config,)
running = asyncio.Event()
@callback
def running_cb(event):
running.set()
hass.bus.async_listen_once("running", running_cb)
hass.states.async_set(test_entity, "hello")
hass.bus.async_fire("test_event")
await running.wait()
if service == "turn_off":
await hass.services.async_call(
automation.DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
else:
with patch(
"homeassistant.config.load_yaml_config_file",
autospec=True,
return_value=config,
):
await common.async_reload(hass)
hass.states.async_set(test_entity, "goodbye")
await hass.async_block_till_done()
assert len(calls) == 0
async def test_automation_restore_state(hass): async def test_automation_restore_state(hass):
"""Ensure states are restored on startup.""" """Ensure states are restored on startup."""
time = dt_util.utcnow() time = dt_util.utcnow()

View File

@ -145,6 +145,30 @@ async def test_form_ssdp(hass):
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_form_ssdp_aborts_before_checking_remoteid_if_host_known(hass):
"""Test we abort without connecting if the host is already known."""
await setup.async_setup_component(hass, "persistent_notification", {})
config_entry = MockConfigEntry(
domain=DOMAIN, data={"host": "2.2.2.2", "name": "any"},
)
config_entry.add_to_hass(hass)
harmonyapi = _get_mock_harmonyapi(connect=True)
with patch(
"homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_SSDP},
data={
"friendlyName": "Harmony Hub",
"ssdp_location": "http://2.2.2.2:8088/description",
},
)
assert result["type"] == "abort"
async def test_form_cannot_connect(hass): async def test_form_cannot_connect(hass):
"""Test we handle cannot connect error.""" """Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(

View File

@ -97,7 +97,7 @@ MOCK_CALLS_ENTRY_WS = {
"host": "fake", "host": "fake",
"name": "HomeAssistant", "name": "HomeAssistant",
"port": 8001, "port": 8001,
"timeout": 1, "timeout": 10,
"token": "abcde", "token": "abcde",
} }

View File

@ -4,6 +4,7 @@ import asyncio
from contextlib import contextmanager from contextlib import contextmanager
from datetime import timedelta from datetime import timedelta
import logging import logging
from types import MappingProxyType
from unittest import mock from unittest import mock
import pytest import pytest
@ -122,7 +123,7 @@ async def test_firing_event_template(hass):
) )
script_obj = script.Script(hass, sequence) script_obj = script.Script(hass, sequence)
await script_obj.async_run({"is_world": "yes"}, context=context) await script_obj.async_run(MappingProxyType({"is_world": "yes"}), context=context)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(events) == 1 assert len(events) == 1
@ -175,7 +176,7 @@ async def test_calling_service_template(hass):
) )
script_obj = script.Script(hass, sequence) script_obj = script.Script(hass, sequence)
await script_obj.async_run({"is_world": "yes"}, context=context) await script_obj.async_run(MappingProxyType({"is_world": "yes"}), context=context)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(calls) == 1 assert len(calls) == 1
@ -235,15 +236,17 @@ async def test_multiple_runs_no_wait(hass):
logger.debug("starting 1st script") logger.debug("starting 1st script")
hass.async_create_task( hass.async_create_task(
script_obj.async_run( script_obj.async_run(
MappingProxyType(
{"fire1": "1", "listen1": "2", "fire2": "3", "listen2": "4"} {"fire1": "1", "listen1": "2", "fire2": "3", "listen2": "4"}
) )
) )
)
await asyncio.wait_for(heard_event.wait(), 1) await asyncio.wait_for(heard_event.wait(), 1)
unsub() unsub()
logger.debug("starting 2nd script") logger.debug("starting 2nd script")
await script_obj.async_run( await script_obj.async_run(
{"fire1": "2", "listen1": "3", "fire2": "4", "listen2": "4"} MappingProxyType({"fire1": "2", "listen1": "3", "fire2": "4", "listen2": "4"})
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -670,7 +673,9 @@ async def test_wait_template_variables(hass):
try: try:
hass.states.async_set("switch.test", "on") hass.states.async_set("switch.test", "on")
hass.async_create_task(script_obj.async_run({"data": "switch.test"})) hass.async_create_task(
script_obj.async_run(MappingProxyType({"data": "switch.test"}))
)
await asyncio.wait_for(wait_started_flag.wait(), 1) await asyncio.wait_for(wait_started_flag.wait(), 1)
assert script_obj.is_running assert script_obj.is_running
@ -882,7 +887,14 @@ async def test_repeat_var_in_condition(hass, condition):
assert len(events) == 2 assert len(events) == 2
async def test_repeat_nested(hass): @pytest.mark.parametrize(
"variables,first_last,inside_x",
[
(None, {"repeat": "None", "x": "None"}, "None"),
(MappingProxyType({"x": 1}), {"repeat": "None", "x": "1"}, "1"),
],
)
async def test_repeat_nested(hass, variables, first_last, inside_x):
"""Test nested repeats.""" """Test nested repeats."""
event = "test_event" event = "test_event"
events = async_capture_events(hass, event) events = async_capture_events(hass, event)
@ -892,7 +904,8 @@ async def test_repeat_nested(hass):
{ {
"event": event, "event": event,
"event_data_template": { "event_data_template": {
"repeat": "{{ None if repeat is not defined else repeat }}" "repeat": "{{ None if repeat is not defined else repeat }}",
"x": "{{ None if x is not defined else x }}",
}, },
}, },
{ {
@ -905,6 +918,7 @@ async def test_repeat_nested(hass):
"first": "{{ repeat.first }}", "first": "{{ repeat.first }}",
"index": "{{ repeat.index }}", "index": "{{ repeat.index }}",
"last": "{{ repeat.last }}", "last": "{{ repeat.last }}",
"x": "{{ None if x is not defined else x }}",
}, },
}, },
{ {
@ -916,6 +930,7 @@ async def test_repeat_nested(hass):
"first": "{{ repeat.first }}", "first": "{{ repeat.first }}",
"index": "{{ repeat.index }}", "index": "{{ repeat.index }}",
"last": "{{ repeat.last }}", "last": "{{ repeat.last }}",
"x": "{{ None if x is not defined else x }}",
}, },
}, },
} }
@ -926,6 +941,7 @@ async def test_repeat_nested(hass):
"first": "{{ repeat.first }}", "first": "{{ repeat.first }}",
"index": "{{ repeat.index }}", "index": "{{ repeat.index }}",
"last": "{{ repeat.last }}", "last": "{{ repeat.last }}",
"x": "{{ None if x is not defined else x }}",
}, },
}, },
], ],
@ -934,7 +950,8 @@ async def test_repeat_nested(hass):
{ {
"event": event, "event": event,
"event_data_template": { "event_data_template": {
"repeat": "{{ None if repeat is not defined else repeat }}" "repeat": "{{ None if repeat is not defined else repeat }}",
"x": "{{ None if x is not defined else x }}",
}, },
}, },
] ]
@ -945,21 +962,21 @@ async def test_repeat_nested(hass):
"homeassistant.helpers.condition._LOGGER.error", "homeassistant.helpers.condition._LOGGER.error",
side_effect=AssertionError("Template Error"), side_effect=AssertionError("Template Error"),
): ):
await script_obj.async_run() await script_obj.async_run(variables)
assert len(events) == 10 assert len(events) == 10
assert events[0].data == {"repeat": "None"} assert events[0].data == first_last
assert events[-1].data == {"repeat": "None"} assert events[-1].data == first_last
for index, result in enumerate( for index, result in enumerate(
( (
("True", "1", "False"), ("True", "1", "False", inside_x),
("True", "1", "False"), ("True", "1", "False", inside_x),
("False", "2", "True"), ("False", "2", "True", inside_x),
("True", "1", "False"), ("True", "1", "False", inside_x),
("False", "2", "True"), ("False", "2", "True", inside_x),
("True", "1", "False"), ("True", "1", "False", inside_x),
("False", "2", "True"), ("False", "2", "True", inside_x),
("False", "2", "True"), ("False", "2", "True", inside_x),
), ),
1, 1,
): ):
@ -967,6 +984,7 @@ async def test_repeat_nested(hass):
"first": result[0], "first": result[0],
"index": result[1], "index": result[1],
"last": result[2], "last": result[2],
"x": result[3],
} }
@ -998,13 +1016,38 @@ async def test_choose(hass, var, result):
) )
script_obj = script.Script(hass, sequence) script_obj = script.Script(hass, sequence)
await script_obj.async_run({"var": var}) await script_obj.async_run(MappingProxyType({"var": var}))
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(events) == 1 assert len(events) == 1
assert events[0].data["choice"] == result assert events[0].data["choice"] == result
@pytest.mark.parametrize(
"action",
[
{"repeat": {"count": 1, "sequence": {"event": "abc"}}},
{"choose": {"conditions": [], "sequence": {"event": "abc"}}},
{"choose": [], "default": {"event": "abc"}},
],
)
async def test_multiple_runs_repeat_choose(hass, caplog, action):
"""Test parallel runs with repeat & choose actions & max_runs > default."""
max_runs = script.DEFAULT_MAX + 1
script_obj = script.Script(
hass, cv.SCRIPT_SCHEMA(action), script_mode="parallel", max_runs=max_runs
)
events = async_capture_events(hass, "abc")
for _ in range(max_runs):
hass.async_create_task(script_obj.async_run())
await hass.async_block_till_done()
assert "WARNING" not in caplog.text
assert "ERROR" not in caplog.text
assert len(events) == max_runs
async def test_last_triggered(hass): async def test_last_triggered(hass):
"""Test the last_triggered.""" """Test the last_triggered."""
event = "test_event" event = "test_event"

View File

@ -1408,9 +1408,62 @@ async def test_log_blocking_events(hass, caplog):
hass.async_create_task(_wait_a_bit_1()) hass.async_create_task(_wait_a_bit_1())
await hass.async_block_till_done() await hass.async_block_till_done()
with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.00001): with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.0001):
hass.async_create_task(_wait_a_bit_2()) hass.async_create_task(_wait_a_bit_2())
await hass.async_block_till_done() await hass.async_block_till_done()
assert "_wait_a_bit_2" in caplog.text assert "_wait_a_bit_2" in caplog.text
assert "_wait_a_bit_1" not in caplog.text assert "_wait_a_bit_1" not in caplog.text
async def test_chained_logging_hits_log_timeout(hass, caplog):
"""Ensure we log which task is blocking startup when there is a task chain and debug logging is on."""
caplog.set_level(logging.DEBUG)
created = 0
async def _task_chain_1():
nonlocal created
created += 1
if created > 10:
return
hass.async_create_task(_task_chain_2())
async def _task_chain_2():
nonlocal created
created += 1
if created > 10:
return
hass.async_create_task(_task_chain_1())
with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.0001):
hass.async_create_task(_task_chain_1())
await hass.async_block_till_done()
assert "_task_chain_" in caplog.text
async def test_chained_logging_misses_log_timeout(hass, caplog):
"""Ensure we do not log which task is blocking startup if we do not hit the timeout."""
caplog.set_level(logging.DEBUG)
created = 0
async def _task_chain_1():
nonlocal created
created += 1
if created > 10:
return
hass.async_create_task(_task_chain_2())
async def _task_chain_2():
nonlocal created
created += 1
if created > 10:
return
hass.async_create_task(_task_chain_1())
hass.async_create_task(_task_chain_1())
await hass.async_block_till_done()
assert "_task_chain_" not in caplog.text