diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 84e86bfcaba..71dff2ab6ee 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -1,5 +1,4 @@ """Support for AdGuard Home.""" -from distutils.version import LooseVersion import logging from typing import Any, Dict @@ -11,7 +10,6 @@ from homeassistant.components.adguard.const import ( DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN, - MIN_ADGUARD_HOME_VERSION, SERVICE_ADD_URL, SERVICE_DISABLE_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 try: - version = await adguard.version() + await adguard.version() except AdGuardHomeConnectionError as 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": hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index ede01706c5d..a0ace623862 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -1,12 +1,11 @@ """Config flow to configure the AdGuard Home integration.""" -from distutils.version import LooseVersion import logging from adguardhome import AdGuardHome, AdGuardHomeConnectionError import voluptuous as vol 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.const import ( CONF_HOST, @@ -79,20 +78,11 @@ class AdGuardHomeFlowHandler(ConfigFlow): ) try: - version = await adguard.version() + await adguard.version() except AdGuardHomeConnectionError: errors["base"] = "connection_error" 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( title=user_input[CONF_HOST], data={ @@ -160,20 +150,11 @@ class AdGuardHomeFlowHandler(ConfigFlow): ) try: - version = await adguard.version() + await adguard.version() except AdGuardHomeConnectionError: errors["base"] = "connection_error" 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( title=self._hassio_discovery["addon"], data={ diff --git a/homeassistant/components/adguard/const.py b/homeassistant/components/adguard/const.py index eb12a9c163f..c77d76a70cf 100644 --- a/homeassistant/components/adguard/const.py +++ b/homeassistant/components/adguard/const.py @@ -7,8 +7,6 @@ DATA_ADGUARD_VERION = "adguard_version" CONF_FORCE = "force" -MIN_ADGUARD_HOME_VERSION = "v0.99.0" - SERVICE_ADD_URL = "add_url" SERVICE_DISABLE_URL = "disable_url" SERVICE_ENABLE_URL = "enable_url" diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json index f5f780c70b5..f010f9e2ade 100644 --- a/homeassistant/components/adguard/strings.json +++ b/homeassistant/components/adguard/strings.json @@ -19,8 +19,6 @@ }, "error": { "connection_error": "Failed to connect." }, "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.", "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed." } diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 3cbb98d85bd..2a05b1555c9 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -455,6 +455,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._async_detach_triggers() self._async_detach_triggers = None + await self.action_script.async_stop() + self.async_write_ha_state() async def _async_attach_triggers( diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 962ba9b8e8c..232237484d1 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -2,7 +2,7 @@ "domain": "discovery", "name": "Discovery", "documentation": "https://www.home-assistant.io/integrations/discovery", - "requirements": ["netdisco==2.8.0"], + "requirements": ["netdisco==2.8.1"], "after_dependencies": ["zeroconf"], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 576451ef2d6..9142eed2dba 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -86,6 +86,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) 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 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) + 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): options = {} diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 3df895c94ce..3f78726bf30 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -112,22 +112,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: async with async_timeout.timeout(10): device_info = await async_get_device_info(pv_request) + + async with async_timeout.timeout(10): + rooms = Rooms(pv_request) + room_data = _async_map_data_by_id((await rooms.get_resources())[ROOM_DATA]) + + async with async_timeout.timeout(10): + scenes = Scenes(pv_request) + scene_data = _async_map_data_by_id( + (await scenes.get_resources())[SCENE_DATA] + ) + + async with async_timeout.timeout(10): + shades = Shades(pv_request) + 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 - rooms = Rooms(pv_request) - room_data = _async_map_data_by_id((await rooms.get_resources())[ROOM_DATA]) - - scenes = Scenes(pv_request) - scene_data = _async_map_data_by_id((await scenes.get_resources())[SCENE_DATA]) - - shades = Shades(pv_request) - shade_data = _async_map_data_by_id((await shades.get_resources())[SHADE_DATA]) - async def async_update_data(): """Fetch data from shade endpoint.""" async with async_timeout.timeout(10): diff --git a/homeassistant/components/hunterdouglas_powerview/const.py b/homeassistant/components/hunterdouglas_powerview/const.py index e69fe319c0f..e83a9d8945b 100644 --- a/homeassistant/components/hunterdouglas_powerview/const.py +++ b/homeassistant/components/hunterdouglas_powerview/const.py @@ -2,6 +2,7 @@ import asyncio +from aiohttp.client_exceptions import ServerDisconnectedError from aiopvapi.helpers.aiorequest import PvApiConnectionError DOMAIN = "hunterdouglas_powerview" @@ -64,7 +65,7 @@ PV_SHADE_DATA = "pv_shade_data" PV_ROOM_DATA = "pv_room_data" COORDINATOR = "coordinator" -HUB_EXCEPTIONS = (asyncio.TimeoutError, PvApiConnectionError) +HUB_EXCEPTIONS = (ServerDisconnectedError, asyncio.TimeoutError, PvApiConnectionError) LEGACY_DEVICE_SUB_REVISION = 1 LEGACY_DEVICE_REVISION = 0 diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 30f4ae0800a..da33ff5f018 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -120,7 +120,7 @@ def determine_zones(receiver): out = {"zone2": False, "zone3": False} try: _LOGGER.debug("Checking for zone 2 capability") - receiver.raw("ZPW") + receiver.raw("ZPWQSTN") out["zone2"] = True except ValueError as error: if str(error) != TIMEOUT_MESSAGE: @@ -128,7 +128,7 @@ def determine_zones(receiver): _LOGGER.debug("Zone 2 timed out, assuming no functionality") try: _LOGGER.debug("Checking for zone 3 capability") - receiver.raw("PW3") + receiver.raw("PW3QSTN") out["zone3"] = True except ValueError as error: if str(error) != TIMEOUT_MESSAGE: diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 9084a06e7db..60a92e56ac0 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -6,7 +6,7 @@ from aiohttp.client_exceptions import ServerDisconnectedError from onvif import ONVIFCamera, ONVIFService 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.util import dt as dt_util @@ -114,30 +114,31 @@ class EventManager: async def async_pull_messages(self, _now: dt = None) -> None: """Pull messages from device.""" - try: - pullpoint = self.device.create_pullpoint_service() - req = pullpoint.create_type("PullMessages") - req.MessageLimit = 100 - req.Timeout = dt.timedelta(seconds=60) - response = await pullpoint.PullMessages(req) + if self.hass.state == CoreState.running: + try: + pullpoint = self.device.create_pullpoint_service() + req = pullpoint.create_type("PullMessages") + req.MessageLimit = 100 + req.Timeout = dt.timedelta(seconds=60) + response = await pullpoint.PullMessages(req) - # Renew subscription if less than two hours is left - if ( - dt_util.as_utc(response.TerminationTime) - dt_util.utcnow() - ).total_seconds() < 7200: - await self.async_renew() + # Renew subscription if less than two hours is left + if ( + dt_util.as_utc(response.TerminationTime) - dt_util.utcnow() + ).total_seconds() < 7200: + await self.async_renew() - # Parse response - await self.async_parse_messages(response.NotificationMessage) + # Parse response + await self.async_parse_messages(response.NotificationMessage) - except ServerDisconnectedError: - pass - except Fault: - pass + except ServerDisconnectedError: + pass + except Fault: + pass - # Update entities - for update_callback in self._listeners: - update_callback() + # Update entities + for update_callback in self._listeners: + update_callback() # Reschedule another pull if self._listeners: diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index 1ba9562c4b7..930da38edde 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -3,6 +3,6 @@ "name": "Tesla Powerwall", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/powerwall", - "requirements": ["tesla-powerwall==0.2.11"], + "requirements": ["tesla-powerwall==0.2.12"], "codeowners": ["@bdraco", "@jrester"] } diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 472ce894e1a..83b8ea3d138 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -250,7 +250,7 @@ class SamsungTVWSBridge(SamsungTVBridge): host=self.host, port=self.port, token=self.token, - timeout=1, + timeout=10, name=VALUE_CONF_NAME, ) self._remote.open() diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index a4e4263d360..9606d9bcf12 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -5,7 +5,12 @@ from requests.exceptions import ConnectTimeout, HTTPError from skybellpy import Skybell 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 from homeassistant.helpers.entity import Entity @@ -20,6 +25,8 @@ DOMAIN = "skybell" DEFAULT_CACHEDB = "./skybell_cache.pickle" DEFAULT_ENTITY_NAMESPACE = "skybell" +AGENT_IDENTIFIER = f"HomeAssistant/{__version__}" + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -42,7 +49,11 @@ def setup(hass, config): try: cache = hass.config.path(DEFAULT_CACHEDB) 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 diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index 9e0a0be8905..1b97b800956 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -2,6 +2,6 @@ "domain": "skybell", "name": "SkyBell", "documentation": "https://www.home-assistant.io/integrations/skybell", - "requirements": ["skybellpy==0.4.0"], + "requirements": ["skybellpy==0.6.1"], "codeowners": [] } diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 2ef49877031..6fd2dec5efd 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -6,7 +6,12 @@ import speedtest import voluptuous as vol 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 import homeassistant.helpers.config_validation as cv 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) 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() - if not coordinator.last_update_success: - raise ConfigEntryNotReady + + if not config_entry.options[CONF_MANUAL]: + if hass.state == CoreState.running: + await _enable_scheduled_speedtests() + if not coordinator.last_update_success: + 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 @@ -107,12 +127,6 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator): super().__init__( 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): """Update list of test servers.""" diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index 0889d7da5b2..d071a226a05 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -12,7 +12,6 @@ from .const import ( ATTR_SERVER_ID, ATTR_SERVER_NAME, ATTRIBUTION, - CONF_MANUAL, DEFAULT_NAME, DOMAIN, ICON, @@ -97,10 +96,9 @@ class SpeedtestSensor(RestoreEntity): async def async_added_to_hass(self): """Handle entity which will be added.""" await super().async_added_to_hass() - if self.coordinator.config_entry.options[CONF_MANUAL]: - state = await self.async_get_last_state() - if state: - self._state = state.state + state = await self.async_get_last_state() + if state: + self._state = state.state @callback def update(): diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 3dde2e9002e..85ae4725e93 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (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"], "codeowners": [] } diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index b636477b16d..263f5f0025b 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -194,7 +194,9 @@ class VeraDevice(Entity): 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): """Update the state.""" diff --git a/homeassistant/components/xbox_live/manifest.json b/homeassistant/components/xbox_live/manifest.json index 3ebffc425ad..937f33bd009 100644 --- a/homeassistant/components/xbox_live/manifest.json +++ b/homeassistant/components/xbox_live/manifest.json @@ -2,6 +2,6 @@ "domain": "xbox_live", "name": "Xbox Live", "documentation": "https://www.home-assistant.io/integrations/xbox_live", - "requirements": ["xboxapi==2.0.0"], + "requirements": ["xboxapi==2.0.1"], "codeowners": ["@MartinHjelmare"] } diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 471dc7290df..09719d720c0 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "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"], "zeroconf": ["_miio._udp.local."] } diff --git a/homeassistant/const.py b/homeassistant/const.py index ddbc6ad91ed..5c5877c5e7b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 113 -PATCH_VERSION = "1" +PATCH_VERSION = "2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) diff --git a/homeassistant/core.py b/homeassistant/core.py index 01bfa402348..a40bc18c804 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -392,12 +392,28 @@ class HomeAssistant: """Block until all pending work is done.""" # To flush out any call_soon_threadsafe await asyncio.sleep(0) + start_time: Optional[float] = None while self._pending_tasks: pending = [task for task in self._pending_tasks if not task.done()] self._pending_tasks.clear() if 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: await asyncio.sleep(0) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1eb46a1eded..6929d8a3bd3 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -4,7 +4,19 @@ from datetime import datetime from functools import partial import itertools 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 import voluptuous as vol @@ -134,13 +146,13 @@ class _ScriptRun: self, hass: HomeAssistant, script: "Script", - variables: Optional[Sequence], + variables: Dict[str, Any], context: Optional[Context], log_exceptions: bool, ) -> None: self._hass = hass self._script = script - self._variables = variables or {} + self._variables = variables self._context = context self._log_exceptions = log_exceptions self._step = -1 @@ -595,6 +607,9 @@ async def _async_stop_scripts_at_shutdown(hass, event): ) +_VarsType = Union[Dict[str, Any], MappingProxyType] + + class Script: """Representation of a script.""" @@ -617,6 +632,7 @@ class Script: hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, partial(_async_stop_scripts_at_shutdown, hass) ) + self._top_level = top_level if top_level: all_scripts.append( {"instance": self, "started_before_shutdown": not hass.is_stopping} @@ -732,14 +748,16 @@ class Script: self._referenced_entities = referenced return referenced - def run(self, variables=None, context=None): + def run( + self, variables: Optional[_VarsType] = None, context: Optional[Context] = None + ) -> None: """Run script.""" asyncio.run_coroutine_threadsafe( self.async_run(variables, context), self._hass.loop ).result() async def async_run( - self, variables: Optional[Sequence] = None, context: Optional[Context] = None + self, variables: Optional[_VarsType] = None, context: Optional[Context] = None ) -> None: """Run script.""" if self.is_running: @@ -753,11 +771,19 @@ class Script: self._log("Maximum number of runs exceeded", level=logging.WARNING) 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: cls = _ScriptRun else: 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) try: @@ -795,6 +821,7 @@ class Script: action[CONF_REPEAT][CONF_SEQUENCE], f"{self.name}: {step_name}", script_mode=SCRIPT_MODE_PARALLEL, + max_runs=self.max_runs, logger=self._logger, top_level=False, ) @@ -822,6 +849,7 @@ class Script: choice[CONF_SEQUENCE], f"{self.name}: {step_name}: choice {idx}", script_mode=SCRIPT_MODE_PARALLEL, + max_runs=self.max_runs, logger=self._logger, top_level=False, ) @@ -836,6 +864,7 @@ class Script: action[CONF_DEFAULT], f"{self.name}: {step_name}: default", script_mode=SCRIPT_MODE_PARALLEL, + max_runs=self.max_runs, logger=self._logger, top_level=False, ) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 31fdad0d54a..50e006c3d71 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ hass-nabucasa==0.34.7 home-assistant-frontend==20200716.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.1 -netdisco==2.8.0 +netdisco==2.8.1 paho-mqtt==1.5.0 pip>=8.0.3 python-slugify==4.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3bcfc2e7596..ea30023f8b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -936,7 +936,7 @@ netdata==0.2.0 # homeassistant.components.discovery # homeassistant.components.ssdp -netdisco==2.8.0 +netdisco==2.8.1 # homeassistant.components.neurio_energy neurio==0.3.1 @@ -1701,7 +1701,7 @@ python-juicenet==1.0.1 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.2.1 +python-miio==0.5.3 # homeassistant.components.mpd python-mpd2==1.0.0 @@ -1945,7 +1945,7 @@ simplisafe-python==9.2.1 sisyphus-control==2.2.1 # homeassistant.components.skybell -skybellpy==0.4.0 +skybellpy==0.6.1 # homeassistant.components.slack slackclient==2.5.0 @@ -2080,7 +2080,7 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.powerwall -tesla-powerwall==0.2.11 +tesla-powerwall==0.2.12 # homeassistant.components.tesla teslajsonpy==0.9.3 @@ -2204,7 +2204,7 @@ wled==0.4.3 xbee-helper==0.0.7 # homeassistant.components.xbox_live -xboxapi==2.0.0 +xboxapi==2.0.1 # homeassistant.components.xfinity xfinity-gateway==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc48e7f7e49..0efa7c3664f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -431,7 +431,7 @@ nessclient==0.9.15 # homeassistant.components.discovery # homeassistant.components.ssdp -netdisco==2.8.0 +netdisco==2.8.1 # homeassistant.components.nexia nexia==0.9.3 @@ -767,7 +767,7 @@ python-izone==1.1.2 python-juicenet==1.0.1 # homeassistant.components.xiaomi_miio -python-miio==0.5.2.1 +python-miio==0.5.3 # homeassistant.components.nest python-nest==4.1.0 @@ -909,7 +909,7 @@ sunwatcher==0.2.1 tellduslive==0.10.11 # homeassistant.components.powerwall -tesla-powerwall==0.2.11 +tesla-powerwall==0.2.12 # homeassistant.components.tesla teslajsonpy==0.9.3 diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index d0e874bacdc..69e1285889b 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -4,7 +4,7 @@ import aiohttp from homeassistant import config_entries, data_entry_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 ( CONF_HOST, 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["step_id"] == "hassio_confirm" 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, - } diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 909493ddf53..bdae9a1f326 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,4 +1,6 @@ """The tests for the automation component.""" +import asyncio + import pytest from homeassistant.components import logbook @@ -12,10 +14,11 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, EVENT_HOMEASSISTANT_STARTED, + SERVICE_TURN_OFF, STATE_OFF, 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.setup import async_setup_component 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 +@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): """Ensure states are restored on startup.""" time = dt_util.utcnow() diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 0228983ef9d..d159a0f025f 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -145,6 +145,30 @@ async def test_form_ssdp(hass): 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): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 15ac13c64d5..5d6c36153c2 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -97,7 +97,7 @@ MOCK_CALLS_ENTRY_WS = { "host": "fake", "name": "HomeAssistant", "port": 8001, - "timeout": 1, + "timeout": 10, "token": "abcde", } diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 7a458c49286..33dec87ccd8 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -4,6 +4,7 @@ import asyncio from contextlib import contextmanager from datetime import timedelta import logging +from types import MappingProxyType from unittest import mock import pytest @@ -122,7 +123,7 @@ async def test_firing_event_template(hass): ) 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() assert len(events) == 1 @@ -175,7 +176,7 @@ async def test_calling_service_template(hass): ) 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() assert len(calls) == 1 @@ -235,7 +236,9 @@ async def test_multiple_runs_no_wait(hass): logger.debug("starting 1st script") hass.async_create_task( script_obj.async_run( - {"fire1": "1", "listen1": "2", "fire2": "3", "listen2": "4"} + MappingProxyType( + {"fire1": "1", "listen1": "2", "fire2": "3", "listen2": "4"} + ) ) ) await asyncio.wait_for(heard_event.wait(), 1) @@ -243,7 +246,7 @@ async def test_multiple_runs_no_wait(hass): logger.debug("starting 2nd script") 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() @@ -670,7 +673,9 @@ async def test_wait_template_variables(hass): try: 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) assert script_obj.is_running @@ -882,7 +887,14 @@ async def test_repeat_var_in_condition(hass, condition): 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.""" event = "test_event" events = async_capture_events(hass, event) @@ -892,7 +904,8 @@ async def test_repeat_nested(hass): { "event": event, "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 }}", "index": "{{ repeat.index }}", "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 }}", "index": "{{ repeat.index }}", "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 }}", "index": "{{ repeat.index }}", "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_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", side_effect=AssertionError("Template Error"), ): - await script_obj.async_run() + await script_obj.async_run(variables) assert len(events) == 10 - assert events[0].data == {"repeat": "None"} - assert events[-1].data == {"repeat": "None"} + assert events[0].data == first_last + assert events[-1].data == first_last for index, result in enumerate( ( - ("True", "1", "False"), - ("True", "1", "False"), - ("False", "2", "True"), - ("True", "1", "False"), - ("False", "2", "True"), - ("True", "1", "False"), - ("False", "2", "True"), - ("False", "2", "True"), + ("True", "1", "False", inside_x), + ("True", "1", "False", inside_x), + ("False", "2", "True", inside_x), + ("True", "1", "False", inside_x), + ("False", "2", "True", inside_x), + ("True", "1", "False", inside_x), + ("False", "2", "True", inside_x), + ("False", "2", "True", inside_x), ), 1, ): @@ -967,6 +984,7 @@ async def test_repeat_nested(hass): "first": result[0], "index": result[1], "last": result[2], + "x": result[3], } @@ -998,13 +1016,38 @@ async def test_choose(hass, var, result): ) 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() assert len(events) == 1 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): """Test the last_triggered.""" event = "test_event" diff --git a/tests/test_core.py b/tests/test_core.py index 3c0a9ee4fb8..a63f42af61b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1408,9 +1408,62 @@ async def test_log_blocking_events(hass, caplog): hass.async_create_task(_wait_a_bit_1()) 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()) await hass.async_block_till_done() assert "_wait_a_bit_2" 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