Intelligent timeout handler for setup/bootstrap (#38329)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com> Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
caca762088
commit
c291d4aa7d
21 changed files with 901 additions and 89 deletions
|
@ -9,7 +9,6 @@ import sys
|
||||||
from time import monotonic
|
from time import monotonic
|
||||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Set
|
from typing import TYPE_CHECKING, Any, Dict, Optional, Set
|
||||||
|
|
||||||
from async_timeout import timeout
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import yarl
|
import yarl
|
||||||
|
|
||||||
|
@ -44,6 +43,11 @@ DATA_LOGGING = "logging"
|
||||||
|
|
||||||
LOG_SLOW_STARTUP_INTERVAL = 60
|
LOG_SLOW_STARTUP_INTERVAL = 60
|
||||||
|
|
||||||
|
STAGE_1_TIMEOUT = 120
|
||||||
|
STAGE_2_TIMEOUT = 300
|
||||||
|
WRAP_UP_TIMEOUT = 300
|
||||||
|
COOLDOWN_TIME = 60
|
||||||
|
|
||||||
DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"}
|
DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"}
|
||||||
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
|
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
|
||||||
LOGGING_INTEGRATIONS = {
|
LOGGING_INTEGRATIONS = {
|
||||||
|
@ -136,7 +140,7 @@ async def async_setup_hass(
|
||||||
hass.async_track_tasks()
|
hass.async_track_tasks()
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {})
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {})
|
||||||
with contextlib.suppress(asyncio.TimeoutError):
|
with contextlib.suppress(asyncio.TimeoutError):
|
||||||
async with timeout(10):
|
async with hass.timeout.async_timeout(10):
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
safe_mode = True
|
safe_mode = True
|
||||||
|
@ -496,24 +500,42 @@ async def _async_set_up_integrations(
|
||||||
stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains
|
stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains
|
||||||
|
|
||||||
# Kick off loading the registries. They don't need to be awaited.
|
# Kick off loading the registries. They don't need to be awaited.
|
||||||
asyncio.gather(
|
asyncio.create_task(hass.helpers.device_registry.async_get_registry())
|
||||||
hass.helpers.device_registry.async_get_registry(),
|
asyncio.create_task(hass.helpers.entity_registry.async_get_registry())
|
||||||
hass.helpers.entity_registry.async_get_registry(),
|
asyncio.create_task(hass.helpers.area_registry.async_get_registry())
|
||||||
hass.helpers.area_registry.async_get_registry(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Start setup
|
# Start setup
|
||||||
if stage_1_domains:
|
if stage_1_domains:
|
||||||
_LOGGER.info("Setting up stage 1: %s", stage_1_domains)
|
_LOGGER.info("Setting up stage 1: %s", stage_1_domains)
|
||||||
await async_setup_multi_components(hass, stage_1_domains, config, setup_started)
|
try:
|
||||||
|
async with hass.timeout.async_timeout(
|
||||||
|
STAGE_1_TIMEOUT, cool_down=COOLDOWN_TIME
|
||||||
|
):
|
||||||
|
await async_setup_multi_components(
|
||||||
|
hass, stage_1_domains, config, setup_started
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.warning("Setup timed out for stage 1 - moving forward")
|
||||||
|
|
||||||
# Enables after dependencies
|
# Enables after dependencies
|
||||||
async_set_domains_to_be_loaded(hass, stage_1_domains | stage_2_domains)
|
async_set_domains_to_be_loaded(hass, stage_1_domains | stage_2_domains)
|
||||||
|
|
||||||
if stage_2_domains:
|
if stage_2_domains:
|
||||||
_LOGGER.info("Setting up stage 2: %s", stage_2_domains)
|
_LOGGER.info("Setting up stage 2: %s", stage_2_domains)
|
||||||
await async_setup_multi_components(hass, stage_2_domains, config, setup_started)
|
try:
|
||||||
|
async with hass.timeout.async_timeout(
|
||||||
|
STAGE_2_TIMEOUT, cool_down=COOLDOWN_TIME
|
||||||
|
):
|
||||||
|
await async_setup_multi_components(
|
||||||
|
hass, stage_2_domains, config, setup_started
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.warning("Setup timed out for stage 2 - moving forward")
|
||||||
|
|
||||||
# Wrap up startup
|
# Wrap up startup
|
||||||
_LOGGER.debug("Waiting for startup to wrap up")
|
_LOGGER.debug("Waiting for startup to wrap up")
|
||||||
await hass.async_block_till_done()
|
try:
|
||||||
|
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.warning("Setup timed out for bootstrap - moving forward")
|
||||||
|
|
|
@ -35,14 +35,12 @@ from homeassistant.helpers.typing import ConfigType
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from . import migration, purge
|
from . import migration, purge
|
||||||
from .const import DATA_INSTANCE, SQLITE_URL_PREFIX
|
from .const import DATA_INSTANCE, DOMAIN, SQLITE_URL_PREFIX
|
||||||
from .models import Base, Events, RecorderRuns, States
|
from .models import Base, Events, RecorderRuns, States
|
||||||
from .util import session_scope, validate_or_move_away_sqlite_database
|
from .util import session_scope, validate_or_move_away_sqlite_database
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "recorder"
|
|
||||||
|
|
||||||
SERVICE_PURGE = "purge"
|
SERVICE_PURGE = "purge"
|
||||||
|
|
||||||
ATTR_KEEP_DAYS = "keep_days"
|
ATTR_KEEP_DAYS = "keep_days"
|
||||||
|
|
|
@ -2,3 +2,4 @@
|
||||||
|
|
||||||
DATA_INSTANCE = "recorder_instance"
|
DATA_INSTANCE = "recorder_instance"
|
||||||
SQLITE_URL_PREFIX = "sqlite://"
|
SQLITE_URL_PREFIX = "sqlite://"
|
||||||
|
DOMAIN = "recorder"
|
||||||
|
|
|
@ -1,22 +1,19 @@
|
||||||
"""Schema migration helpers."""
|
"""Schema migration helpers."""
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
|
|
||||||
from sqlalchemy import Table, text
|
from sqlalchemy import Table, text
|
||||||
from sqlalchemy.engine import reflection
|
from sqlalchemy.engine import reflection
|
||||||
from sqlalchemy.exc import InternalError, OperationalError, SQLAlchemyError
|
from sqlalchemy.exc import InternalError, OperationalError, SQLAlchemyError
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
from .models import SCHEMA_VERSION, Base, SchemaChanges
|
from .models import SCHEMA_VERSION, Base, SchemaChanges
|
||||||
from .util import session_scope
|
from .util import session_scope
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
PROGRESS_FILE = ".migration_progress"
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_schema(instance):
|
def migrate_schema(instance):
|
||||||
"""Check if the schema needs to be upgraded."""
|
"""Check if the schema needs to be upgraded."""
|
||||||
progress_path = instance.hass.config.path(PROGRESS_FILE)
|
|
||||||
|
|
||||||
with session_scope(session=instance.get_session()) as session:
|
with session_scope(session=instance.get_session()) as session:
|
||||||
res = (
|
res = (
|
||||||
session.query(SchemaChanges)
|
session.query(SchemaChanges)
|
||||||
|
@ -32,20 +29,13 @@ def migrate_schema(instance):
|
||||||
)
|
)
|
||||||
|
|
||||||
if current_version == SCHEMA_VERSION:
|
if current_version == SCHEMA_VERSION:
|
||||||
# Clean up if old migration left file
|
|
||||||
if os.path.isfile(progress_path):
|
|
||||||
_LOGGER.warning("Found existing migration file, cleaning up")
|
|
||||||
os.remove(instance.hass.config.path(PROGRESS_FILE))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(progress_path, "w"):
|
|
||||||
pass
|
|
||||||
|
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Database is about to upgrade. Schema version: %s", current_version
|
"Database is about to upgrade. Schema version: %s", current_version
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
with instance.hass.timeout.freeze(DOMAIN):
|
||||||
for version in range(current_version, SCHEMA_VERSION):
|
for version in range(current_version, SCHEMA_VERSION):
|
||||||
new_version = version + 1
|
new_version = version + 1
|
||||||
_LOGGER.info("Upgrading recorder db schema to version %s", new_version)
|
_LOGGER.info("Upgrading recorder db schema to version %s", new_version)
|
||||||
|
@ -53,8 +43,6 @@ def migrate_schema(instance):
|
||||||
session.add(SchemaChanges(schema_version=new_version))
|
session.add(SchemaChanges(schema_version=new_version))
|
||||||
|
|
||||||
_LOGGER.info("Upgrade to version %s done", new_version)
|
_LOGGER.info("Upgrade to version %s done", new_version)
|
||||||
finally:
|
|
||||||
os.remove(instance.hass.config.path(PROGRESS_FILE))
|
|
||||||
|
|
||||||
|
|
||||||
def _create_index(engine, table_name, index_name):
|
def _create_index(engine, table_name, index_name):
|
||||||
|
|
|
@ -35,7 +35,6 @@ from typing import (
|
||||||
)
|
)
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from async_timeout import timeout
|
|
||||||
import attr
|
import attr
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import yarl
|
import yarl
|
||||||
|
@ -76,6 +75,7 @@ from homeassistant.util import location, network
|
||||||
from homeassistant.util.async_ import fire_coroutine_threadsafe, run_callback_threadsafe
|
from homeassistant.util.async_ import fire_coroutine_threadsafe, run_callback_threadsafe
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.util.thread import fix_threading_exception_logging
|
from homeassistant.util.thread import fix_threading_exception_logging
|
||||||
|
from homeassistant.util.timeout import TimeoutManager
|
||||||
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem
|
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem
|
||||||
|
|
||||||
# Typing imports that create a circular dependency
|
# Typing imports that create a circular dependency
|
||||||
|
@ -184,10 +184,12 @@ class HomeAssistant:
|
||||||
self.helpers = loader.Helpers(self)
|
self.helpers = loader.Helpers(self)
|
||||||
# This is a dictionary that any component can store any data on.
|
# This is a dictionary that any component can store any data on.
|
||||||
self.data: dict = {}
|
self.data: dict = {}
|
||||||
self.state = CoreState.not_running
|
self.state: CoreState = CoreState.not_running
|
||||||
self.exit_code = 0
|
self.exit_code: int = 0
|
||||||
# If not None, use to signal end-of-loop
|
# If not None, use to signal end-of-loop
|
||||||
self._stopped: Optional[asyncio.Event] = None
|
self._stopped: Optional[asyncio.Event] = None
|
||||||
|
# Timeout handler for Core/Helper namespace
|
||||||
|
self.timeout: TimeoutManager = TimeoutManager()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_running(self) -> bool:
|
def is_running(self) -> bool:
|
||||||
|
@ -255,7 +257,7 @@ class HomeAssistant:
|
||||||
try:
|
try:
|
||||||
# Only block for EVENT_HOMEASSISTANT_START listener
|
# Only block for EVENT_HOMEASSISTANT_START listener
|
||||||
self.async_stop_track_tasks()
|
self.async_stop_track_tasks()
|
||||||
async with timeout(TIMEOUT_EVENT_START):
|
async with self.timeout.async_timeout(TIMEOUT_EVENT_START):
|
||||||
await self.async_block_till_done()
|
await self.async_block_till_done()
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
|
@ -460,17 +462,35 @@ class HomeAssistant:
|
||||||
self.state = CoreState.stopping
|
self.state = CoreState.stopping
|
||||||
self.async_track_tasks()
|
self.async_track_tasks()
|
||||||
self.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
self.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||||
await self.async_block_till_done()
|
try:
|
||||||
|
async with self.timeout.async_timeout(120):
|
||||||
|
await self.async_block_till_done()
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Timed out waiting for shutdown stage 1 to complete, the shutdown will continue"
|
||||||
|
)
|
||||||
|
|
||||||
# stage 2
|
# stage 2
|
||||||
self.state = CoreState.final_write
|
self.state = CoreState.final_write
|
||||||
self.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE)
|
self.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE)
|
||||||
await self.async_block_till_done()
|
try:
|
||||||
|
async with self.timeout.async_timeout(60):
|
||||||
|
await self.async_block_till_done()
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Timed out waiting for shutdown stage 2 to complete, the shutdown will continue"
|
||||||
|
)
|
||||||
|
|
||||||
# stage 3
|
# stage 3
|
||||||
self.state = CoreState.not_running
|
self.state = CoreState.not_running
|
||||||
self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE)
|
self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE)
|
||||||
await self.async_block_till_done()
|
try:
|
||||||
|
async with self.timeout.async_timeout(30):
|
||||||
|
await self.async_block_till_done()
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Timed out waiting for shutdown stage 3 to complete, the shutdown will continue"
|
||||||
|
)
|
||||||
|
|
||||||
# Python 3.9+ and backported in runner.py
|
# Python 3.9+ and backported in runner.py
|
||||||
await self.loop.shutdown_default_executor() # type: ignore
|
await self.loop.shutdown_default_executor() # type: ignore
|
||||||
|
|
|
@ -177,7 +177,8 @@ class EntityPlatform:
|
||||||
try:
|
try:
|
||||||
task = async_create_setup_task()
|
task = async_create_setup_task()
|
||||||
|
|
||||||
await asyncio.wait_for(asyncio.shield(task), SLOW_SETUP_MAX_WAIT)
|
async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, self.domain):
|
||||||
|
await asyncio.shield(task)
|
||||||
|
|
||||||
# Block till all entities are done
|
# Block till all entities are done
|
||||||
if self._tasks:
|
if self._tasks:
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict, Iterable, List, Optional, Set, Union, cast
|
from typing import Any, Dict, Iterable, List, Optional, Set, Union, cast
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
@ -14,7 +13,6 @@ DATA_PIP_LOCK = "pip_lock"
|
||||||
DATA_PKG_CACHE = "pkg_cache"
|
DATA_PKG_CACHE = "pkg_cache"
|
||||||
DATA_INTEGRATIONS_WITH_REQS = "integrations_with_reqs"
|
DATA_INTEGRATIONS_WITH_REQS = "integrations_with_reqs"
|
||||||
CONSTRAINT_FILE = "package_constraints.txt"
|
CONSTRAINT_FILE = "package_constraints.txt"
|
||||||
PROGRESS_FILE = ".pip_progress"
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DISCOVERY_INTEGRATIONS: Dict[str, Iterable[str]] = {
|
DISCOVERY_INTEGRATIONS: Dict[str, Iterable[str]] = {
|
||||||
"ssdp": ("ssdp",),
|
"ssdp": ("ssdp",),
|
||||||
|
@ -124,22 +122,16 @@ async def async_process_requirements(
|
||||||
if pkg_util.is_installed(req):
|
if pkg_util.is_installed(req):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ret = await hass.async_add_executor_job(_install, hass, req, kwargs)
|
def _install(req: str, kwargs: Dict) -> bool:
|
||||||
|
"""Install requirement."""
|
||||||
|
return pkg_util.install_package(req, **kwargs)
|
||||||
|
|
||||||
|
ret = await hass.async_add_executor_job(_install, req, kwargs)
|
||||||
|
|
||||||
if not ret:
|
if not ret:
|
||||||
raise RequirementsNotFound(name, [req])
|
raise RequirementsNotFound(name, [req])
|
||||||
|
|
||||||
|
|
||||||
def _install(hass: HomeAssistant, req: str, kwargs: Dict) -> bool:
|
|
||||||
"""Install requirement."""
|
|
||||||
progress_path = Path(hass.config.path(PROGRESS_FILE))
|
|
||||||
progress_path.touch()
|
|
||||||
try:
|
|
||||||
return pkg_util.install_package(req, **kwargs)
|
|
||||||
finally:
|
|
||||||
progress_path.unlink()
|
|
||||||
|
|
||||||
|
|
||||||
def pip_kwargs(config_dir: Optional[str]) -> Dict[str, Any]:
|
def pip_kwargs(config_dir: Optional[str]) -> Dict[str, Any]:
|
||||||
"""Return keyword arguments for PIP install."""
|
"""Return keyword arguments for PIP install."""
|
||||||
is_docker = pkg_util.is_docker_env()
|
is_docker = pkg_util.is_docker_env()
|
||||||
|
|
|
@ -22,11 +22,7 @@ DATA_SETUP = "setup_tasks"
|
||||||
DATA_DEPS_REQS = "deps_reqs_processed"
|
DATA_DEPS_REQS = "deps_reqs_processed"
|
||||||
|
|
||||||
SLOW_SETUP_WARNING = 10
|
SLOW_SETUP_WARNING = 10
|
||||||
|
SLOW_SETUP_MAX_WAIT = 300
|
||||||
# Since its possible for databases to be
|
|
||||||
# upwards of 36GiB (or larger) in the wild
|
|
||||||
# we wait up to 3 hours for startup
|
|
||||||
SLOW_SETUP_MAX_WAIT = 10800
|
|
||||||
|
|
||||||
|
|
||||||
@core.callback
|
@core.callback
|
||||||
|
@ -89,7 +85,8 @@ async def _async_process_dependencies(
|
||||||
return True
|
return True
|
||||||
|
|
||||||
_LOGGER.debug("Dependency %s will wait for %s", integration.domain, list(tasks))
|
_LOGGER.debug("Dependency %s will wait for %s", integration.domain, list(tasks))
|
||||||
results = await asyncio.gather(*tasks.values())
|
async with hass.timeout.async_freeze(integration.domain):
|
||||||
|
results = await asyncio.gather(*tasks.values())
|
||||||
|
|
||||||
failed = [
|
failed = [
|
||||||
domain
|
domain
|
||||||
|
@ -190,7 +187,8 @@ async def _async_setup_component(
|
||||||
hass.data[DATA_SETUP_STARTED].pop(domain)
|
hass.data[DATA_SETUP_STARTED].pop(domain)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
result = await asyncio.wait_for(task, SLOW_SETUP_MAX_WAIT)
|
async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, domain):
|
||||||
|
result = await task
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Setup of %s is taking longer than %s seconds."
|
"Setup of %s is taking longer than %s seconds."
|
||||||
|
@ -319,9 +317,10 @@ async def async_process_deps_reqs(
|
||||||
raise HomeAssistantError("Could not set up all dependencies.")
|
raise HomeAssistantError("Could not set up all dependencies.")
|
||||||
|
|
||||||
if not hass.config.skip_pip and integration.requirements:
|
if not hass.config.skip_pip and integration.requirements:
|
||||||
await requirements.async_get_integration_with_requirements(
|
async with hass.timeout.async_freeze(integration.domain):
|
||||||
hass, integration.domain
|
await requirements.async_get_integration_with_requirements(
|
||||||
)
|
hass, integration.domain
|
||||||
|
)
|
||||||
|
|
||||||
processed.add(integration.domain)
|
processed.add(integration.domain)
|
||||||
|
|
||||||
|
|
508
homeassistant/util/timeout.py
Normal file
508
homeassistant/util/timeout.py
Normal file
|
@ -0,0 +1,508 @@
|
||||||
|
"""Advanced timeout handling.
|
||||||
|
|
||||||
|
Set of helper classes to handle timeouts of tasks with advanced options
|
||||||
|
like zones and freezing of timeouts.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import enum
|
||||||
|
import logging
|
||||||
|
from types import TracebackType
|
||||||
|
from typing import Any, Dict, List, Optional, Type, Union
|
||||||
|
|
||||||
|
from .async_ import run_callback_threadsafe
|
||||||
|
|
||||||
|
ZONE_GLOBAL = "global"
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class _State(str, enum.Enum):
|
||||||
|
"""States of a task."""
|
||||||
|
|
||||||
|
INIT = "INIT"
|
||||||
|
ACTIVE = "ACTIVE"
|
||||||
|
TIMEOUT = "TIMEOUT"
|
||||||
|
EXIT = "EXIT"
|
||||||
|
|
||||||
|
|
||||||
|
class _GlobalFreezeContext:
|
||||||
|
"""Context manager that freezes the global timeout."""
|
||||||
|
|
||||||
|
def __init__(self, manager: TimeoutManager) -> None:
|
||||||
|
"""Initialize internal timeout context manager."""
|
||||||
|
self._loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
|
||||||
|
self._manager: TimeoutManager = manager
|
||||||
|
|
||||||
|
async def __aenter__(self) -> _GlobalFreezeContext:
|
||||||
|
self._enter()
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(
|
||||||
|
self,
|
||||||
|
exc_type: Type[BaseException],
|
||||||
|
exc_val: BaseException,
|
||||||
|
exc_tb: TracebackType,
|
||||||
|
) -> Optional[bool]:
|
||||||
|
self._exit()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __enter__(self) -> _GlobalFreezeContext:
|
||||||
|
self._loop.call_soon_threadsafe(self._enter)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: Type[BaseException],
|
||||||
|
exc_val: BaseException,
|
||||||
|
exc_tb: TracebackType,
|
||||||
|
) -> Optional[bool]:
|
||||||
|
self._loop.call_soon_threadsafe(self._exit)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _enter(self) -> None:
|
||||||
|
"""Run freeze."""
|
||||||
|
if not self._manager.freezes_done:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Global reset
|
||||||
|
for task in self._manager.global_tasks:
|
||||||
|
task.pause()
|
||||||
|
|
||||||
|
# Zones reset
|
||||||
|
for zone in self._manager.zones.values():
|
||||||
|
if not zone.freezes_done:
|
||||||
|
continue
|
||||||
|
zone.pause()
|
||||||
|
|
||||||
|
self._manager.global_freezes.append(self)
|
||||||
|
|
||||||
|
def _exit(self) -> None:
|
||||||
|
"""Finish freeze."""
|
||||||
|
self._manager.global_freezes.remove(self)
|
||||||
|
if not self._manager.freezes_done:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Global reset
|
||||||
|
for task in self._manager.global_tasks:
|
||||||
|
task.reset()
|
||||||
|
|
||||||
|
# Zones reset
|
||||||
|
for zone in self._manager.zones.values():
|
||||||
|
if not zone.freezes_done:
|
||||||
|
continue
|
||||||
|
zone.reset()
|
||||||
|
|
||||||
|
|
||||||
|
class _ZoneFreezeContext:
|
||||||
|
"""Context manager that freezes a zone timeout."""
|
||||||
|
|
||||||
|
def __init__(self, zone: _ZoneTimeoutManager) -> None:
|
||||||
|
"""Initialize internal timeout context manager."""
|
||||||
|
self._loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
|
||||||
|
self._zone: _ZoneTimeoutManager = zone
|
||||||
|
|
||||||
|
async def __aenter__(self) -> _ZoneFreezeContext:
|
||||||
|
self._enter()
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(
|
||||||
|
self,
|
||||||
|
exc_type: Type[BaseException],
|
||||||
|
exc_val: BaseException,
|
||||||
|
exc_tb: TracebackType,
|
||||||
|
) -> Optional[bool]:
|
||||||
|
self._exit()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __enter__(self) -> _ZoneFreezeContext:
|
||||||
|
self._loop.call_soon_threadsafe(self._enter)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: Type[BaseException],
|
||||||
|
exc_val: BaseException,
|
||||||
|
exc_tb: TracebackType,
|
||||||
|
) -> Optional[bool]:
|
||||||
|
self._loop.call_soon_threadsafe(self._exit)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _enter(self) -> None:
|
||||||
|
"""Run freeze."""
|
||||||
|
if self._zone.freezes_done:
|
||||||
|
self._zone.pause()
|
||||||
|
self._zone.enter_freeze(self)
|
||||||
|
|
||||||
|
def _exit(self) -> None:
|
||||||
|
"""Finish freeze."""
|
||||||
|
self._zone.exit_freeze(self)
|
||||||
|
if not self._zone.freezes_done:
|
||||||
|
return
|
||||||
|
self._zone.reset()
|
||||||
|
|
||||||
|
|
||||||
|
class _GlobalTaskContext:
|
||||||
|
"""Context manager that tracks a global task."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
manager: TimeoutManager,
|
||||||
|
task: asyncio.Task[Any],
|
||||||
|
timeout: float,
|
||||||
|
cool_down: float,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize internal timeout context manager."""
|
||||||
|
self._loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
|
||||||
|
self._manager: TimeoutManager = manager
|
||||||
|
self._task: asyncio.Task[Any] = task
|
||||||
|
self._time_left: float = timeout
|
||||||
|
self._expiration_time: Optional[float] = None
|
||||||
|
self._timeout_handler: Optional[asyncio.Handle] = None
|
||||||
|
self._wait_zone: asyncio.Event = asyncio.Event()
|
||||||
|
self._state: _State = _State.INIT
|
||||||
|
self._cool_down: float = cool_down
|
||||||
|
|
||||||
|
async def __aenter__(self) -> _GlobalTaskContext:
|
||||||
|
self._manager.global_tasks.append(self)
|
||||||
|
self._start_timer()
|
||||||
|
self._state = _State.ACTIVE
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(
|
||||||
|
self,
|
||||||
|
exc_type: Type[BaseException],
|
||||||
|
exc_val: BaseException,
|
||||||
|
exc_tb: TracebackType,
|
||||||
|
) -> Optional[bool]:
|
||||||
|
self._stop_timer()
|
||||||
|
self._manager.global_tasks.remove(self)
|
||||||
|
|
||||||
|
# Timeout on exit
|
||||||
|
if exc_type is asyncio.CancelledError and self.state == _State.TIMEOUT:
|
||||||
|
raise asyncio.TimeoutError
|
||||||
|
|
||||||
|
self._state = _State.EXIT
|
||||||
|
self._wait_zone.set()
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> _State:
|
||||||
|
"""Return state of the Global task."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
def zones_done_signal(self) -> None:
|
||||||
|
"""Signal that all zones are done."""
|
||||||
|
self._wait_zone.set()
|
||||||
|
|
||||||
|
def _start_timer(self) -> None:
|
||||||
|
"""Start timeout handler."""
|
||||||
|
if self._timeout_handler:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._expiration_time = self._loop.time() + self._time_left
|
||||||
|
self._timeout_handler = self._loop.call_at(
|
||||||
|
self._expiration_time, self._on_timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
def _stop_timer(self) -> None:
|
||||||
|
"""Stop zone timer."""
|
||||||
|
if self._timeout_handler is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._timeout_handler.cancel()
|
||||||
|
self._timeout_handler = None
|
||||||
|
# Calculate new timeout
|
||||||
|
assert self._expiration_time
|
||||||
|
self._time_left = self._expiration_time - self._loop.time()
|
||||||
|
|
||||||
|
def _on_timeout(self) -> None:
|
||||||
|
"""Process timeout."""
|
||||||
|
self._state = _State.TIMEOUT
|
||||||
|
self._timeout_handler = None
|
||||||
|
|
||||||
|
# Reset timer if zones are running
|
||||||
|
if not self._manager.zones_done:
|
||||||
|
asyncio.create_task(self._on_wait())
|
||||||
|
else:
|
||||||
|
self._cancel_task()
|
||||||
|
|
||||||
|
def _cancel_task(self) -> None:
|
||||||
|
"""Cancel own task."""
|
||||||
|
if self._task.done():
|
||||||
|
return
|
||||||
|
self._task.cancel()
|
||||||
|
|
||||||
|
def pause(self) -> None:
|
||||||
|
"""Pause timers while it freeze."""
|
||||||
|
self._stop_timer()
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Reset timer after freeze."""
|
||||||
|
self._start_timer()
|
||||||
|
|
||||||
|
async def _on_wait(self) -> None:
|
||||||
|
"""Wait until zones are done."""
|
||||||
|
await self._wait_zone.wait()
|
||||||
|
await asyncio.sleep(self._cool_down) # Allow context switch
|
||||||
|
if not self.state == _State.TIMEOUT:
|
||||||
|
return
|
||||||
|
self._cancel_task()
|
||||||
|
|
||||||
|
|
||||||
|
class _ZoneTaskContext:
|
||||||
|
"""Context manager that tracks an active task for a zone."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, zone: _ZoneTimeoutManager, task: asyncio.Task[Any], timeout: float,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize internal timeout context manager."""
|
||||||
|
self._loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
|
||||||
|
self._zone: _ZoneTimeoutManager = zone
|
||||||
|
self._task: asyncio.Task[Any] = task
|
||||||
|
self._state: _State = _State.INIT
|
||||||
|
self._time_left: float = timeout
|
||||||
|
self._expiration_time: Optional[float] = None
|
||||||
|
self._timeout_handler: Optional[asyncio.Handle] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> _State:
|
||||||
|
"""Return state of the Zone task."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
async def __aenter__(self) -> _ZoneTaskContext:
|
||||||
|
self._zone.enter_task(self)
|
||||||
|
self._state = _State.ACTIVE
|
||||||
|
|
||||||
|
# Zone is on freeze
|
||||||
|
if self._zone.freezes_done:
|
||||||
|
self._start_timer()
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(
|
||||||
|
self,
|
||||||
|
exc_type: Type[BaseException],
|
||||||
|
exc_val: BaseException,
|
||||||
|
exc_tb: TracebackType,
|
||||||
|
) -> Optional[bool]:
|
||||||
|
self._zone.exit_task(self)
|
||||||
|
self._stop_timer()
|
||||||
|
|
||||||
|
# Timeout on exit
|
||||||
|
if exc_type is asyncio.CancelledError and self.state == _State.TIMEOUT:
|
||||||
|
raise asyncio.TimeoutError
|
||||||
|
|
||||||
|
self._state = _State.EXIT
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _start_timer(self) -> None:
|
||||||
|
"""Start timeout handler."""
|
||||||
|
if self._timeout_handler:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._expiration_time = self._loop.time() + self._time_left
|
||||||
|
self._timeout_handler = self._loop.call_at(
|
||||||
|
self._expiration_time, self._on_timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
def _stop_timer(self) -> None:
|
||||||
|
"""Stop zone timer."""
|
||||||
|
if self._timeout_handler is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._timeout_handler.cancel()
|
||||||
|
self._timeout_handler = None
|
||||||
|
# Calculate new timeout
|
||||||
|
assert self._expiration_time
|
||||||
|
self._time_left = self._expiration_time - self._loop.time()
|
||||||
|
|
||||||
|
def _on_timeout(self) -> None:
|
||||||
|
"""Process timeout."""
|
||||||
|
self._state = _State.TIMEOUT
|
||||||
|
self._timeout_handler = None
|
||||||
|
|
||||||
|
# Timeout
|
||||||
|
if self._task.done():
|
||||||
|
return
|
||||||
|
self._task.cancel()
|
||||||
|
|
||||||
|
def pause(self) -> None:
|
||||||
|
"""Pause timers while it freeze."""
|
||||||
|
self._stop_timer()
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Reset timer after freeze."""
|
||||||
|
self._start_timer()
|
||||||
|
|
||||||
|
|
||||||
|
class _ZoneTimeoutManager:
|
||||||
|
"""Manage the timeouts for a zone."""
|
||||||
|
|
||||||
|
def __init__(self, manager: TimeoutManager, zone: str) -> None:
|
||||||
|
"""Initialize internal timeout context manager."""
|
||||||
|
self._manager: TimeoutManager = manager
|
||||||
|
self._zone: str = zone
|
||||||
|
self._tasks: List[_ZoneTaskContext] = []
|
||||||
|
self._freezes: List[_ZoneFreezeContext] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return Zone name."""
|
||||||
|
return self._zone
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active(self) -> bool:
|
||||||
|
"""Return True if zone is active."""
|
||||||
|
return len(self._tasks) > 0 or len(self._freezes) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def freezes_done(self) -> bool:
|
||||||
|
"""Return True if all freeze are done."""
|
||||||
|
return len(self._freezes) == 0 and self._manager.freezes_done
|
||||||
|
|
||||||
|
def enter_task(self, task: _ZoneTaskContext) -> None:
|
||||||
|
"""Start into new Task."""
|
||||||
|
self._tasks.append(task)
|
||||||
|
|
||||||
|
def exit_task(self, task: _ZoneTaskContext) -> None:
|
||||||
|
"""Exit a running Task."""
|
||||||
|
self._tasks.remove(task)
|
||||||
|
|
||||||
|
# On latest listener
|
||||||
|
if not self.active:
|
||||||
|
self._manager.drop_zone(self.name)
|
||||||
|
|
||||||
|
def enter_freeze(self, freeze: _ZoneFreezeContext) -> None:
|
||||||
|
"""Start into new freeze."""
|
||||||
|
self._freezes.append(freeze)
|
||||||
|
|
||||||
|
def exit_freeze(self, freeze: _ZoneFreezeContext) -> None:
|
||||||
|
"""Exit a running Freeze."""
|
||||||
|
self._freezes.remove(freeze)
|
||||||
|
|
||||||
|
# On latest listener
|
||||||
|
if not self.active:
|
||||||
|
self._manager.drop_zone(self.name)
|
||||||
|
|
||||||
|
def pause(self) -> None:
|
||||||
|
"""Stop timers while it freeze."""
|
||||||
|
if not self.active:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Forward pause
|
||||||
|
for task in self._tasks:
|
||||||
|
task.pause()
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Reset timer after freeze."""
|
||||||
|
if not self.active:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Forward reset
|
||||||
|
for task in self._tasks:
|
||||||
|
task.reset()
|
||||||
|
|
||||||
|
|
||||||
|
class TimeoutManager:
|
||||||
|
"""Class to manage timeouts over different zones.
|
||||||
|
|
||||||
|
Manages both global and zone based timeouts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize TimeoutManager."""
|
||||||
|
self._loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
|
||||||
|
self._zones: Dict[str, _ZoneTimeoutManager] = {}
|
||||||
|
self._globals: List[_GlobalTaskContext] = []
|
||||||
|
self._freezes: List[_GlobalFreezeContext] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def zones_done(self) -> bool:
|
||||||
|
"""Return True if all zones are finished."""
|
||||||
|
return not bool(self._zones)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def freezes_done(self) -> bool:
|
||||||
|
"""Return True if all freezes are finished."""
|
||||||
|
return not self._freezes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def zones(self) -> Dict[str, _ZoneTimeoutManager]:
|
||||||
|
"""Return all Zones."""
|
||||||
|
return self._zones
|
||||||
|
|
||||||
|
@property
|
||||||
|
def global_tasks(self) -> List[_GlobalTaskContext]:
|
||||||
|
"""Return all global Tasks."""
|
||||||
|
return self._globals
|
||||||
|
|
||||||
|
@property
|
||||||
|
def global_freezes(self) -> List[_GlobalFreezeContext]:
|
||||||
|
"""Return all global Freezes."""
|
||||||
|
return self._freezes
|
||||||
|
|
||||||
|
def drop_zone(self, zone_name: str) -> None:
|
||||||
|
"""Drop a zone out of scope."""
|
||||||
|
self._zones.pop(zone_name, None)
|
||||||
|
if self._zones:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Signal Global task, all zones are done
|
||||||
|
for task in self._globals:
|
||||||
|
task.zones_done_signal()
|
||||||
|
|
||||||
|
def async_timeout(
|
||||||
|
self, timeout: float, zone_name: str = ZONE_GLOBAL, cool_down: float = 0
|
||||||
|
) -> Union[_ZoneTaskContext, _GlobalTaskContext]:
|
||||||
|
"""Timeout based on a zone.
|
||||||
|
|
||||||
|
For using as Async Context Manager.
|
||||||
|
"""
|
||||||
|
current_task: Optional[asyncio.Task[Any]] = asyncio.current_task()
|
||||||
|
assert current_task
|
||||||
|
|
||||||
|
# Global Zone
|
||||||
|
if zone_name == ZONE_GLOBAL:
|
||||||
|
task = _GlobalTaskContext(self, current_task, timeout, cool_down)
|
||||||
|
return task
|
||||||
|
|
||||||
|
# Zone Handling
|
||||||
|
if zone_name in self.zones:
|
||||||
|
zone: _ZoneTimeoutManager = self.zones[zone_name]
|
||||||
|
else:
|
||||||
|
self.zones[zone_name] = zone = _ZoneTimeoutManager(self, zone_name)
|
||||||
|
|
||||||
|
# Create Task
|
||||||
|
return _ZoneTaskContext(zone, current_task, timeout)
|
||||||
|
|
||||||
|
def async_freeze(
|
||||||
|
self, zone_name: str = ZONE_GLOBAL
|
||||||
|
) -> Union[_ZoneFreezeContext, _GlobalFreezeContext]:
|
||||||
|
"""Freeze all timer until job is done.
|
||||||
|
|
||||||
|
For using as Async Context Manager.
|
||||||
|
"""
|
||||||
|
# Global Freeze
|
||||||
|
if zone_name == ZONE_GLOBAL:
|
||||||
|
return _GlobalFreezeContext(self)
|
||||||
|
|
||||||
|
# Zone Freeze
|
||||||
|
if zone_name in self.zones:
|
||||||
|
zone: _ZoneTimeoutManager = self.zones[zone_name]
|
||||||
|
else:
|
||||||
|
self.zones[zone_name] = zone = _ZoneTimeoutManager(self, zone_name)
|
||||||
|
|
||||||
|
return _ZoneFreezeContext(zone)
|
||||||
|
|
||||||
|
def freeze(
|
||||||
|
self, zone_name: str = ZONE_GLOBAL
|
||||||
|
) -> Union[_ZoneFreezeContext, _GlobalFreezeContext]:
|
||||||
|
"""Freeze all timer until job is done.
|
||||||
|
|
||||||
|
For using as Context Manager.
|
||||||
|
"""
|
||||||
|
return run_callback_threadsafe(
|
||||||
|
self._loop, self.async_freeze, zone_name
|
||||||
|
).result()
|
|
@ -133,6 +133,7 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg):
|
||||||
device_id=device_entry.id,
|
device_id=device_entry.id,
|
||||||
)
|
)
|
||||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
expected_capabilities = {
|
expected_capabilities = {
|
||||||
"arm_away": {"extra_fields": []},
|
"arm_away": {"extra_fields": []},
|
||||||
|
@ -170,6 +171,7 @@ async def test_get_action_capabilities_arm_code(hass, device_reg, entity_reg):
|
||||||
device_id=device_entry.id,
|
device_id=device_entry.id,
|
||||||
)
|
)
|
||||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
expected_capabilities = {
|
expected_capabilities = {
|
||||||
"arm_away": {
|
"arm_away": {
|
||||||
|
@ -267,6 +269,8 @@ async def test_action(hass):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
hass.states.get("alarm_control_panel.alarm_no_arm_code").state == STATE_UNKNOWN
|
hass.states.get("alarm_control_panel.alarm_no_arm_code").state == STATE_UNKNOWN
|
||||||
)
|
)
|
||||||
|
|
|
@ -60,6 +60,7 @@ async def test_get_conditions(hass, device_reg, entity_reg):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
expected_conditions = [
|
expected_conditions = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -60,6 +60,7 @@ async def test_get_triggers(hass, device_reg, entity_reg):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
expected_triggers = [
|
expected_triggers = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -46,6 +46,7 @@ async def test_get_actions(hass, device_reg, entity_reg):
|
||||||
DOMAIN, "test", ent.unique_id, device_id=device_entry.id
|
DOMAIN, "test", ent.unique_id, device_id=device_entry.id
|
||||||
)
|
)
|
||||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
expected_actions = [
|
expected_actions = [
|
||||||
{
|
{
|
||||||
|
@ -87,6 +88,7 @@ async def test_get_actions_tilt(hass, device_reg, entity_reg):
|
||||||
DOMAIN, "test", ent.unique_id, device_id=device_entry.id
|
DOMAIN, "test", ent.unique_id, device_id=device_entry.id
|
||||||
)
|
)
|
||||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
expected_actions = [
|
expected_actions = [
|
||||||
{
|
{
|
||||||
|
@ -140,6 +142,7 @@ async def test_get_actions_set_pos(hass, device_reg, entity_reg):
|
||||||
DOMAIN, "test", ent.unique_id, device_id=device_entry.id
|
DOMAIN, "test", ent.unique_id, device_id=device_entry.id
|
||||||
)
|
)
|
||||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
expected_actions = [
|
expected_actions = [
|
||||||
{
|
{
|
||||||
|
@ -169,6 +172,7 @@ async def test_get_actions_set_tilt_pos(hass, device_reg, entity_reg):
|
||||||
DOMAIN, "test", ent.unique_id, device_id=device_entry.id
|
DOMAIN, "test", ent.unique_id, device_id=device_entry.id
|
||||||
)
|
)
|
||||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
expected_actions = [
|
expected_actions = [
|
||||||
{
|
{
|
||||||
|
@ -217,6 +221,7 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
actions = await async_get_device_automations(hass, "action", device_entry.id)
|
actions = await async_get_device_automations(hass, "action", device_entry.id)
|
||||||
assert len(actions) == 3 # open, close, stop
|
assert len(actions) == 3 # open, close, stop
|
||||||
|
@ -244,6 +249,7 @@ async def test_get_action_capabilities_set_pos(hass, device_reg, entity_reg):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
expected_capabilities = {
|
expected_capabilities = {
|
||||||
"extra_fields": [
|
"extra_fields": [
|
||||||
|
@ -286,6 +292,7 @@ async def test_get_action_capabilities_set_tilt_pos(hass, device_reg, entity_reg
|
||||||
)
|
)
|
||||||
|
|
||||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
expected_capabilities = {
|
expected_capabilities = {
|
||||||
"extra_fields": [
|
"extra_fields": [
|
||||||
|
@ -352,6 +359,7 @@ async def test_action(hass):
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
open_calls = async_mock_service(hass, "cover", "open_cover")
|
open_calls = async_mock_service(hass, "cover", "open_cover")
|
||||||
close_calls = async_mock_service(hass, "cover", "close_cover")
|
close_calls = async_mock_service(hass, "cover", "close_cover")
|
||||||
|
@ -408,6 +416,7 @@ async def test_action_tilt(hass):
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
open_calls = async_mock_service(hass, "cover", "open_cover_tilt")
|
open_calls = async_mock_service(hass, "cover", "open_cover_tilt")
|
||||||
close_calls = async_mock_service(hass, "cover", "close_cover_tilt")
|
close_calls = async_mock_service(hass, "cover", "close_cover_tilt")
|
||||||
|
@ -468,6 +477,7 @@ async def test_action_set_position(hass):
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
cover_pos_calls = async_mock_service(hass, "cover", "set_cover_position")
|
cover_pos_calls = async_mock_service(hass, "cover", "set_cover_position")
|
||||||
tilt_pos_calls = async_mock_service(hass, "cover", "set_cover_tilt_position")
|
tilt_pos_calls = async_mock_service(hass, "cover", "set_cover_tilt_position")
|
||||||
|
|
|
@ -366,6 +366,7 @@ async def test_reconnect(hass, monkeypatch, mock_connection_factory):
|
||||||
protocol.wait_closed = wait_closed
|
protocol.wait_closed = wait_closed
|
||||||
|
|
||||||
await async_setup_component(hass, "sensor", {"sensor": config})
|
await async_setup_component(hass, "sensor", {"sensor": config})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert connection_factory.call_count == 1
|
assert connection_factory.call_count == 1
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,7 @@ async def test_valid_config_with_info(hass):
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
async def test_valid_config_no_name(hass):
|
async def test_valid_config_no_name(hass):
|
||||||
|
@ -114,6 +115,7 @@ async def test_valid_config_no_name(hass):
|
||||||
"switch",
|
"switch",
|
||||||
{"switch": {"platform": "flux", "lights": ["light.desk", "light.lamp"]}},
|
{"switch": {"platform": "flux", "lights": ["light.desk", "light.lamp"]}},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
async def test_invalid_config_no_lights(hass):
|
async def test_invalid_config_no_lights(hass):
|
||||||
|
@ -122,6 +124,7 @@ async def test_invalid_config_no_lights(hass):
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass, "switch", {"switch": {"platform": "flux", "name": "flux"}}
|
hass, "switch", {"switch": {"platform": "flux", "name": "flux"}}
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
async def test_flux_when_switch_is_off(hass, legacy_patchable_time):
|
async def test_flux_when_switch_is_off(hass, legacy_patchable_time):
|
||||||
|
@ -168,6 +171,7 @@ async def test_flux_when_switch_is_off(hass, legacy_patchable_time):
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
async_fire_time_changed(hass, test_time)
|
async_fire_time_changed(hass, test_time)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
@ -218,6 +222,7 @@ async def test_flux_before_sunrise(hass, legacy_patchable_time):
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
await common.async_turn_on(hass, "switch.flux")
|
await common.async_turn_on(hass, "switch.flux")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -271,6 +276,7 @@ async def test_flux_before_sunrise_known_location(hass, legacy_patchable_time):
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
await common.async_turn_on(hass, "switch.flux")
|
await common.async_turn_on(hass, "switch.flux")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -325,6 +331,7 @@ async def test_flux_after_sunrise_before_sunset(hass, legacy_patchable_time):
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
await common.async_turn_on(hass, "switch.flux")
|
await common.async_turn_on(hass, "switch.flux")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -380,6 +387,7 @@ async def test_flux_after_sunset_before_stop(hass, legacy_patchable_time):
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
common.turn_on(hass, "switch.flux")
|
common.turn_on(hass, "switch.flux")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -434,6 +442,7 @@ async def test_flux_after_stop_before_sunrise(hass, legacy_patchable_time):
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
common.turn_on(hass, "switch.flux")
|
common.turn_on(hass, "switch.flux")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -490,6 +499,7 @@ async def test_flux_with_custom_start_stop_times(hass, legacy_patchable_time):
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
common.turn_on(hass, "switch.flux")
|
common.turn_on(hass, "switch.flux")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -547,6 +557,7 @@ async def test_flux_before_sunrise_stop_next_day(hass, legacy_patchable_time):
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
common.turn_on(hass, "switch.flux")
|
common.turn_on(hass, "switch.flux")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -608,6 +619,7 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
common.turn_on(hass, "switch.flux")
|
common.turn_on(hass, "switch.flux")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -669,6 +681,7 @@ async def test_flux_after_sunset_before_midnight_stop_next_day(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
common.turn_on(hass, "switch.flux")
|
common.turn_on(hass, "switch.flux")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -729,6 +742,7 @@ async def test_flux_after_sunset_after_midnight_stop_next_day(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
common.turn_on(hass, "switch.flux")
|
common.turn_on(hass, "switch.flux")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -789,6 +803,7 @@ async def test_flux_after_stop_before_sunrise_stop_next_day(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
common.turn_on(hass, "switch.flux")
|
common.turn_on(hass, "switch.flux")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -846,6 +861,7 @@ async def test_flux_with_custom_colortemps(hass, legacy_patchable_time):
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
common.turn_on(hass, "switch.flux")
|
common.turn_on(hass, "switch.flux")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -902,6 +918,7 @@ async def test_flux_with_custom_brightness(hass, legacy_patchable_time):
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
common.turn_on(hass, "switch.flux")
|
common.turn_on(hass, "switch.flux")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -974,6 +991,7 @@ async def test_flux_with_multiple_lights(hass, legacy_patchable_time):
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
common.turn_on(hass, "switch.flux")
|
common.turn_on(hass, "switch.flux")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -1033,6 +1051,7 @@ async def test_flux_with_mired(hass, legacy_patchable_time):
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
common.turn_on(hass, "switch.flux")
|
common.turn_on(hass, "switch.flux")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -1085,6 +1104,7 @@ async def test_flux_with_rgb(hass, legacy_patchable_time):
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
await common.async_turn_on(hass, "switch.flux")
|
await common.async_turn_on(hass, "switch.flux")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
|
@ -34,6 +34,7 @@ async def test_get_actions_support_open(hass, device_reg, entity_reg):
|
||||||
platform = getattr(hass.components, f"test.{DOMAIN}")
|
platform = getattr(hass.components, f"test.{DOMAIN}")
|
||||||
platform.init()
|
platform.init()
|
||||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
config_entry = MockConfigEntry(domain="test", data={})
|
config_entry = MockConfigEntry(domain="test", data={})
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
|
@ -77,6 +78,7 @@ async def test_get_actions_not_support_open(hass, device_reg, entity_reg):
|
||||||
platform = getattr(hass.components, f"test.{DOMAIN}")
|
platform = getattr(hass.components, f"test.{DOMAIN}")
|
||||||
platform.init()
|
platform.init()
|
||||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
config_entry = MockConfigEntry(domain="test", data={})
|
config_entry = MockConfigEntry(domain="test", data={})
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
|
@ -146,6 +148,7 @@ async def test_action(hass):
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
lock_calls = async_mock_service(hass, "lock", "lock")
|
lock_calls = async_mock_service(hass, "lock", "lock")
|
||||||
unlock_calls = async_mock_service(hass, "lock", "unlock")
|
unlock_calls = async_mock_service(hass, "lock", "unlock")
|
||||||
|
|
|
@ -188,8 +188,8 @@ async def test_platform_warn_slow_setup(hass):
|
||||||
assert mock_call.called
|
assert mock_call.called
|
||||||
|
|
||||||
# mock_calls[0] is the warning message for component setup
|
# mock_calls[0] is the warning message for component setup
|
||||||
# mock_calls[6] is the warning message for platform setup
|
# mock_calls[4] is the warning message for platform setup
|
||||||
timeout, logger_method = mock_call.mock_calls[6][1][:2]
|
timeout, logger_method = mock_call.mock_calls[4][1][:2]
|
||||||
|
|
||||||
assert timeout == entity_platform.SLOW_SETUP_WARNING
|
assert timeout == entity_platform.SLOW_SETUP_WARNING
|
||||||
assert logger_method == _LOGGER.warning
|
assert logger_method == _LOGGER.warning
|
||||||
|
|
|
@ -1140,8 +1140,8 @@ async def test_start_taking_too_long(loop, caplog):
|
||||||
caplog.set_level(logging.WARNING)
|
caplog.set_level(logging.WARNING)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with patch(
|
with patch.object(
|
||||||
"homeassistant.core.timeout", side_effect=asyncio.TimeoutError
|
hass, "async_block_till_done", side_effect=asyncio.TimeoutError
|
||||||
), patch("homeassistant.core._async_create_timer") as mock_timer:
|
), patch("homeassistant.core._async_create_timer") as mock_timer:
|
||||||
await hass.async_start()
|
await hass.async_start()
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
"""Test requirements module."""
|
"""Test requirements module."""
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant import loader, setup
|
from homeassistant import loader, setup
|
||||||
from homeassistant.requirements import (
|
from homeassistant.requirements import (
|
||||||
CONSTRAINT_FILE,
|
CONSTRAINT_FILE,
|
||||||
PROGRESS_FILE,
|
|
||||||
RequirementsNotFound,
|
RequirementsNotFound,
|
||||||
_install,
|
|
||||||
async_get_integration_with_requirements,
|
async_get_integration_with_requirements,
|
||||||
async_process_requirements,
|
async_process_requirements,
|
||||||
)
|
)
|
||||||
|
@ -190,24 +187,6 @@ async def test_install_on_docker(hass):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_progress_lock(hass):
|
|
||||||
"""Test an install attempt on an existing package."""
|
|
||||||
progress_path = Path(hass.config.path(PROGRESS_FILE))
|
|
||||||
kwargs = {"hello": "world"}
|
|
||||||
|
|
||||||
def assert_env(req, **passed_kwargs):
|
|
||||||
"""Assert the env."""
|
|
||||||
assert progress_path.exists()
|
|
||||||
assert req == "hello"
|
|
||||||
assert passed_kwargs == kwargs
|
|
||||||
return True
|
|
||||||
|
|
||||||
with patch("homeassistant.util.package.install_package", side_effect=assert_env):
|
|
||||||
_install(hass, "hello", kwargs)
|
|
||||||
|
|
||||||
assert not progress_path.exists()
|
|
||||||
|
|
||||||
|
|
||||||
async def test_discovery_requirements_ssdp(hass):
|
async def test_discovery_requirements_ssdp(hass):
|
||||||
"""Test that we load discovery requirements."""
|
"""Test that we load discovery requirements."""
|
||||||
hass.config.skip_pip = False
|
hass.config.skip_pip = False
|
||||||
|
|
|
@ -488,15 +488,12 @@ async def test_component_warn_slow_setup(hass):
|
||||||
assert result
|
assert result
|
||||||
assert mock_call.called
|
assert mock_call.called
|
||||||
|
|
||||||
assert len(mock_call.mock_calls) == 5
|
assert len(mock_call.mock_calls) == 3
|
||||||
timeout, logger_method = mock_call.mock_calls[0][1][:2]
|
timeout, logger_method = mock_call.mock_calls[0][1][:2]
|
||||||
|
|
||||||
assert timeout == setup.SLOW_SETUP_WARNING
|
assert timeout == setup.SLOW_SETUP_WARNING
|
||||||
assert logger_method == setup._LOGGER.warning
|
assert logger_method == setup._LOGGER.warning
|
||||||
|
|
||||||
timeout, function = mock_call.mock_calls[1][1][:2]
|
|
||||||
assert timeout == setup.SLOW_SETUP_MAX_WAIT
|
|
||||||
|
|
||||||
assert mock_call().cancel.called
|
assert mock_call().cancel.called
|
||||||
|
|
||||||
|
|
||||||
|
@ -508,8 +505,7 @@ async def test_platform_no_warn_slow(hass):
|
||||||
with patch.object(hass.loop, "call_later") as mock_call:
|
with patch.object(hass.loop, "call_later") as mock_call:
|
||||||
result = await setup.async_setup_component(hass, "test_component1", {})
|
result = await setup.async_setup_component(hass, "test_component1", {})
|
||||||
assert result
|
assert result
|
||||||
timeout, function = mock_call.mock_calls[0][1][:2]
|
assert len(mock_call.mock_calls) == 0
|
||||||
assert timeout == setup.SLOW_SETUP_MAX_WAIT
|
|
||||||
|
|
||||||
|
|
||||||
async def test_platform_error_slow_setup(hass, caplog):
|
async def test_platform_error_slow_setup(hass, caplog):
|
||||||
|
|
268
tests/util/test_timeout.py
Normal file
268
tests/util/test_timeout.py
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
"""Test Home Assistant timeout handler."""
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.util.timeout import TimeoutManager
|
||||||
|
|
||||||
|
|
||||||
|
async def test_simple_global_timeout():
|
||||||
|
"""Test a simple global timeout."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
with pytest.raises(asyncio.TimeoutError):
|
||||||
|
async with timeout.async_timeout(0.1):
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_simple_global_timeout_with_executor_job(hass):
|
||||||
|
"""Test a simple global timeout with executor job."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
with pytest.raises(asyncio.TimeoutError):
|
||||||
|
async with timeout.async_timeout(0.1):
|
||||||
|
await hass.async_add_executor_job(lambda: time.sleep(0.2))
|
||||||
|
|
||||||
|
|
||||||
|
async def test_simple_global_timeout_freeze():
|
||||||
|
"""Test a simple global timeout freeze."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
async with timeout.async_timeout(0.2):
|
||||||
|
async with timeout.async_freeze():
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_simple_zone_timeout_freeze_inside_executor_job(hass):
|
||||||
|
"""Test a simple zone timeout freeze inside an executor job."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
def _some_sync_work():
|
||||||
|
with timeout.freeze("recorder"):
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
async with timeout.async_timeout(1.0):
|
||||||
|
async with timeout.async_timeout(0.2, zone_name="recorder"):
|
||||||
|
await hass.async_add_executor_job(_some_sync_work)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_simple_global_timeout_freeze_inside_executor_job(hass):
|
||||||
|
"""Test a simple global timeout freeze inside an executor job."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
def _some_sync_work():
|
||||||
|
with timeout.freeze():
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
async with timeout.async_timeout(0.2):
|
||||||
|
await hass.async_add_executor_job(_some_sync_work)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mix_global_timeout_freeze_and_zone_freeze_inside_executor_job(hass):
|
||||||
|
"""Test a simple global timeout freeze inside an executor job."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
def _some_sync_work():
|
||||||
|
with timeout.freeze("recorder"):
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
async with timeout.async_timeout(0.1):
|
||||||
|
async with timeout.async_timeout(0.2, zone_name="recorder"):
|
||||||
|
await hass.async_add_executor_job(_some_sync_work)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mix_global_timeout_freeze_and_zone_freeze_different_order(hass):
|
||||||
|
"""Test a simple global timeout freeze inside an executor job before timeout was set."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
def _some_sync_work():
|
||||||
|
with timeout.freeze("recorder"):
|
||||||
|
time.sleep(0.4)
|
||||||
|
|
||||||
|
async with timeout.async_timeout(0.1):
|
||||||
|
hass.async_add_executor_job(_some_sync_work)
|
||||||
|
async with timeout.async_timeout(0.2, zone_name="recorder"):
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mix_global_timeout_freeze_and_zone_freeze_other_zone_inside_executor_job(
|
||||||
|
hass,
|
||||||
|
):
|
||||||
|
"""Test a simple global timeout freeze other zone inside an executor job."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
def _some_sync_work():
|
||||||
|
with timeout.freeze("not_recorder"):
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
with pytest.raises(asyncio.TimeoutError):
|
||||||
|
async with timeout.async_timeout(0.1):
|
||||||
|
async with timeout.async_timeout(0.2, zone_name="recorder"):
|
||||||
|
async with timeout.async_timeout(0.2, zone_name="not_recorder"):
|
||||||
|
await hass.async_add_executor_job(_some_sync_work)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mix_global_timeout_freeze_and_zone_freeze_inside_executor_job_second_job_outside_zone_context(
|
||||||
|
hass,
|
||||||
|
):
|
||||||
|
"""Test a simple global timeout freeze inside an executor job with second job outside of zone context."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
def _some_sync_work():
|
||||||
|
with timeout.freeze("recorder"):
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
with pytest.raises(asyncio.TimeoutError):
|
||||||
|
async with timeout.async_timeout(0.1):
|
||||||
|
async with timeout.async_timeout(0.2, zone_name="recorder"):
|
||||||
|
await hass.async_add_executor_job(_some_sync_work)
|
||||||
|
await hass.async_add_executor_job(lambda: time.sleep(0.2))
|
||||||
|
|
||||||
|
|
||||||
|
async def test_simple_global_timeout_freeze_with_executor_job(hass):
|
||||||
|
"""Test a simple global timeout freeze with executor job."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
async with timeout.async_timeout(0.2):
|
||||||
|
async with timeout.async_freeze():
|
||||||
|
await hass.async_add_executor_job(lambda: time.sleep(0.3))
|
||||||
|
|
||||||
|
|
||||||
|
async def test_simple_global_timeout_freeze_reset():
|
||||||
|
"""Test a simple global timeout freeze reset."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
with pytest.raises(asyncio.TimeoutError):
|
||||||
|
async with timeout.async_timeout(0.2):
|
||||||
|
async with timeout.async_freeze():
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_simple_zone_timeout():
|
||||||
|
"""Test a simple zone timeout."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
with pytest.raises(asyncio.TimeoutError):
|
||||||
|
async with timeout.async_timeout(0.1, "test"):
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_multiple_zone_timeout():
|
||||||
|
"""Test a simple zone timeout."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
with pytest.raises(asyncio.TimeoutError):
|
||||||
|
async with timeout.async_timeout(0.1, "test"):
|
||||||
|
async with timeout.async_timeout(0.5, "test"):
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_different_zone_timeout():
|
||||||
|
"""Test a simple zone timeout."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
with pytest.raises(asyncio.TimeoutError):
|
||||||
|
async with timeout.async_timeout(0.1, "test"):
|
||||||
|
async with timeout.async_timeout(0.5, "other"):
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_simple_zone_timeout_freeze():
|
||||||
|
"""Test a simple zone timeout freeze."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
async with timeout.async_timeout(0.2, "test"):
|
||||||
|
async with timeout.async_freeze("test"):
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_simple_zone_timeout_freeze_without_timeout():
|
||||||
|
"""Test a simple zone timeout freeze on a zone that does not have a timeout set."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
async with timeout.async_timeout(0.1, "test"):
|
||||||
|
async with timeout.async_freeze("test"):
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_simple_zone_timeout_freeze_reset():
|
||||||
|
"""Test a simple zone timeout freeze reset."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
with pytest.raises(asyncio.TimeoutError):
|
||||||
|
async with timeout.async_timeout(0.2, "test"):
|
||||||
|
async with timeout.async_freeze("test"):
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
await asyncio.sleep(0.2, "test")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mix_zone_timeout_freeze_and_global_freeze():
|
||||||
|
"""Test a mix zone timeout freeze and global freeze."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
async with timeout.async_timeout(0.2, "test"):
|
||||||
|
async with timeout.async_freeze("test"):
|
||||||
|
async with timeout.async_freeze():
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mix_global_and_zone_timeout_freeze_():
|
||||||
|
"""Test a mix zone timeout freeze and global freeze."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
async with timeout.async_timeout(0.2, "test"):
|
||||||
|
async with timeout.async_freeze():
|
||||||
|
async with timeout.async_freeze("test"):
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mix_zone_timeout_freeze():
|
||||||
|
"""Test a mix zone timeout global freeze."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
async with timeout.async_timeout(0.2, "test"):
|
||||||
|
async with timeout.async_freeze():
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mix_zone_timeout():
|
||||||
|
"""Test a mix zone timeout global."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
async with timeout.async_timeout(0.1):
|
||||||
|
try:
|
||||||
|
async with timeout.async_timeout(0.2, "test"):
|
||||||
|
await asyncio.sleep(0.4)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mix_zone_timeout_trigger_global():
|
||||||
|
"""Test a mix zone timeout global with trigger it."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
with pytest.raises(asyncio.TimeoutError):
|
||||||
|
async with timeout.async_timeout(0.1):
|
||||||
|
try:
|
||||||
|
async with timeout.async_timeout(0.1, "test"):
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mix_zone_timeout_trigger_global_cool_down():
|
||||||
|
"""Test a mix zone timeout global with trigger it with cool_down."""
|
||||||
|
timeout = TimeoutManager()
|
||||||
|
|
||||||
|
async with timeout.async_timeout(0.1, cool_down=0.3):
|
||||||
|
try:
|
||||||
|
async with timeout.async_timeout(0.1, "test"):
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await asyncio.sleep(0.2)
|
Loading…
Add table
Add a link
Reference in a new issue