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 typing import TYPE_CHECKING, Any, Dict, Optional, Set
|
||||
|
||||
from async_timeout import timeout
|
||||
import voluptuous as vol
|
||||
import yarl
|
||||
|
||||
|
@ -44,6 +43,11 @@ DATA_LOGGING = "logging"
|
|||
|
||||
LOG_SLOW_STARTUP_INTERVAL = 60
|
||||
|
||||
STAGE_1_TIMEOUT = 120
|
||||
STAGE_2_TIMEOUT = 300
|
||||
WRAP_UP_TIMEOUT = 300
|
||||
COOLDOWN_TIME = 60
|
||||
|
||||
DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"}
|
||||
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
|
||||
LOGGING_INTEGRATIONS = {
|
||||
|
@ -136,7 +140,7 @@ async def async_setup_hass(
|
|||
hass.async_track_tasks()
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {})
|
||||
with contextlib.suppress(asyncio.TimeoutError):
|
||||
async with timeout(10):
|
||||
async with hass.timeout.async_timeout(10):
|
||||
await hass.async_block_till_done()
|
||||
|
||||
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
|
||||
|
||||
# Kick off loading the registries. They don't need to be awaited.
|
||||
asyncio.gather(
|
||||
hass.helpers.device_registry.async_get_registry(),
|
||||
hass.helpers.entity_registry.async_get_registry(),
|
||||
hass.helpers.area_registry.async_get_registry(),
|
||||
)
|
||||
asyncio.create_task(hass.helpers.device_registry.async_get_registry())
|
||||
asyncio.create_task(hass.helpers.entity_registry.async_get_registry())
|
||||
asyncio.create_task(hass.helpers.area_registry.async_get_registry())
|
||||
|
||||
# Start setup
|
||||
if 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
|
||||
async_set_domains_to_be_loaded(hass, stage_1_domains | stage_2_domains)
|
||||
|
||||
if 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
|
||||
_LOGGER.debug("Waiting for startup to wrap up")
|
||||
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
|
||||
|
||||
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 .util import session_scope, validate_or_move_away_sqlite_database
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "recorder"
|
||||
|
||||
SERVICE_PURGE = "purge"
|
||||
|
||||
ATTR_KEEP_DAYS = "keep_days"
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
|
||||
DATA_INSTANCE = "recorder_instance"
|
||||
SQLITE_URL_PREFIX = "sqlite://"
|
||||
DOMAIN = "recorder"
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
"""Schema migration helpers."""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from sqlalchemy import Table, text
|
||||
from sqlalchemy.engine import reflection
|
||||
from sqlalchemy.exc import InternalError, OperationalError, SQLAlchemyError
|
||||
|
||||
from .const import DOMAIN
|
||||
from .models import SCHEMA_VERSION, Base, SchemaChanges
|
||||
from .util import session_scope
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PROGRESS_FILE = ".migration_progress"
|
||||
|
||||
|
||||
def migrate_schema(instance):
|
||||
"""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:
|
||||
res = (
|
||||
session.query(SchemaChanges)
|
||||
|
@ -32,20 +29,13 @@ def migrate_schema(instance):
|
|||
)
|
||||
|
||||
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
|
||||
|
||||
with open(progress_path, "w"):
|
||||
pass
|
||||
|
||||
_LOGGER.warning(
|
||||
"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):
|
||||
new_version = version + 1
|
||||
_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))
|
||||
|
||||
_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):
|
||||
|
|
|
@ -35,7 +35,6 @@ from typing import (
|
|||
)
|
||||
import uuid
|
||||
|
||||
from async_timeout import timeout
|
||||
import attr
|
||||
import voluptuous as vol
|
||||
import yarl
|
||||
|
@ -76,6 +75,7 @@ from homeassistant.util import location, network
|
|||
from homeassistant.util.async_ import fire_coroutine_threadsafe, run_callback_threadsafe
|
||||
import homeassistant.util.dt as dt_util
|
||||
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
|
||||
|
||||
# Typing imports that create a circular dependency
|
||||
|
@ -184,10 +184,12 @@ class HomeAssistant:
|
|||
self.helpers = loader.Helpers(self)
|
||||
# This is a dictionary that any component can store any data on.
|
||||
self.data: dict = {}
|
||||
self.state = CoreState.not_running
|
||||
self.exit_code = 0
|
||||
self.state: CoreState = CoreState.not_running
|
||||
self.exit_code: int = 0
|
||||
# If not None, use to signal end-of-loop
|
||||
self._stopped: Optional[asyncio.Event] = None
|
||||
# Timeout handler for Core/Helper namespace
|
||||
self.timeout: TimeoutManager = TimeoutManager()
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
|
@ -255,7 +257,7 @@ class HomeAssistant:
|
|||
try:
|
||||
# Only block for EVENT_HOMEASSISTANT_START listener
|
||||
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()
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.warning(
|
||||
|
@ -460,17 +462,35 @@ class HomeAssistant:
|
|||
self.state = CoreState.stopping
|
||||
self.async_track_tasks()
|
||||
self.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||
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
|
||||
self.state = CoreState.final_write
|
||||
self.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE)
|
||||
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
|
||||
self.state = CoreState.not_running
|
||||
self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE)
|
||||
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
|
||||
await self.loop.shutdown_default_executor() # type: ignore
|
||||
|
|
|
@ -177,7 +177,8 @@ class EntityPlatform:
|
|||
try:
|
||||
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
|
||||
if self._tasks:
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List, Optional, Set, Union, cast
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
@ -14,7 +13,6 @@ DATA_PIP_LOCK = "pip_lock"
|
|||
DATA_PKG_CACHE = "pkg_cache"
|
||||
DATA_INTEGRATIONS_WITH_REQS = "integrations_with_reqs"
|
||||
CONSTRAINT_FILE = "package_constraints.txt"
|
||||
PROGRESS_FILE = ".pip_progress"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DISCOVERY_INTEGRATIONS: Dict[str, Iterable[str]] = {
|
||||
"ssdp": ("ssdp",),
|
||||
|
@ -124,22 +122,16 @@ async def async_process_requirements(
|
|||
if pkg_util.is_installed(req):
|
||||
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:
|
||||
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]:
|
||||
"""Return keyword arguments for PIP install."""
|
||||
is_docker = pkg_util.is_docker_env()
|
||||
|
|
|
@ -22,11 +22,7 @@ DATA_SETUP = "setup_tasks"
|
|||
DATA_DEPS_REQS = "deps_reqs_processed"
|
||||
|
||||
SLOW_SETUP_WARNING = 10
|
||||
|
||||
# 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
|
||||
SLOW_SETUP_MAX_WAIT = 300
|
||||
|
||||
|
||||
@core.callback
|
||||
|
@ -89,6 +85,7 @@ async def _async_process_dependencies(
|
|||
return True
|
||||
|
||||
_LOGGER.debug("Dependency %s will wait for %s", integration.domain, list(tasks))
|
||||
async with hass.timeout.async_freeze(integration.domain):
|
||||
results = await asyncio.gather(*tasks.values())
|
||||
|
||||
failed = [
|
||||
|
@ -190,7 +187,8 @@ async def _async_setup_component(
|
|||
hass.data[DATA_SETUP_STARTED].pop(domain)
|
||||
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:
|
||||
_LOGGER.error(
|
||||
"Setup of %s is taking longer than %s seconds."
|
||||
|
@ -319,6 +317,7 @@ async def async_process_deps_reqs(
|
|||
raise HomeAssistantError("Could not set up all dependencies.")
|
||||
|
||||
if not hass.config.skip_pip and integration.requirements:
|
||||
async with hass.timeout.async_freeze(integration.domain):
|
||||
await requirements.async_get_integration_with_requirements(
|
||||
hass, 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,
|
||||
)
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
expected_capabilities = {
|
||||
"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,
|
||||
)
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
expected_capabilities = {
|
||||
"arm_away": {
|
||||
|
@ -267,6 +269,8 @@ async def test_action(hass):
|
|||
},
|
||||
)
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
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"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
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"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
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
|
||||
)
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
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
|
||||
)
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
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
|
||||
)
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
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
|
||||
)
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
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"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
actions = await async_get_device_automations(hass, "action", device_entry.id)
|
||||
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"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
expected_capabilities = {
|
||||
"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"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
expected_capabilities = {
|
||||
"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")
|
||||
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")
|
||||
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")
|
||||
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
|
||||
|
||||
await async_setup_component(hass, "sensor", {"sensor": config})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
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):
|
||||
|
@ -114,6 +115,7 @@ async def test_valid_config_no_name(hass):
|
|||
"switch",
|
||||
{"switch": {"platform": "flux", "lights": ["light.desk", "light.lamp"]}},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
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(
|
||||
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):
|
||||
|
@ -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)
|
||||
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)
|
||||
await common.async_turn_on(hass, "switch.flux")
|
||||
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)
|
||||
await common.async_turn_on(hass, "switch.flux")
|
||||
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)
|
||||
await common.async_turn_on(hass, "switch.flux")
|
||||
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)
|
||||
common.turn_on(hass, "switch.flux")
|
||||
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)
|
||||
common.turn_on(hass, "switch.flux")
|
||||
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)
|
||||
common.turn_on(hass, "switch.flux")
|
||||
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)
|
||||
common.turn_on(hass, "switch.flux")
|
||||
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)
|
||||
common.turn_on(hass, "switch.flux")
|
||||
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)
|
||||
common.turn_on(hass, "switch.flux")
|
||||
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)
|
||||
common.turn_on(hass, "switch.flux")
|
||||
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)
|
||||
common.turn_on(hass, "switch.flux")
|
||||
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)
|
||||
common.turn_on(hass, "switch.flux")
|
||||
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)
|
||||
common.turn_on(hass, "switch.flux")
|
||||
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)
|
||||
common.turn_on(hass, "switch.flux")
|
||||
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)
|
||||
common.turn_on(hass, "switch.flux")
|
||||
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)
|
||||
await common.async_turn_on(hass, "switch.flux")
|
||||
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.init()
|
||||
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.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.init()
|
||||
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.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")
|
||||
unlock_calls = async_mock_service(hass, "lock", "unlock")
|
||||
|
|
|
@ -188,8 +188,8 @@ async def test_platform_warn_slow_setup(hass):
|
|||
assert mock_call.called
|
||||
|
||||
# mock_calls[0] is the warning message for component setup
|
||||
# mock_calls[6] is the warning message for platform setup
|
||||
timeout, logger_method = mock_call.mock_calls[6][1][:2]
|
||||
# mock_calls[4] is the warning message for platform setup
|
||||
timeout, logger_method = mock_call.mock_calls[4][1][:2]
|
||||
|
||||
assert timeout == entity_platform.SLOW_SETUP_WARNING
|
||||
assert logger_method == _LOGGER.warning
|
||||
|
|
|
@ -1140,8 +1140,8 @@ async def test_start_taking_too_long(loop, caplog):
|
|||
caplog.set_level(logging.WARNING)
|
||||
|
||||
try:
|
||||
with patch(
|
||||
"homeassistant.core.timeout", side_effect=asyncio.TimeoutError
|
||||
with patch.object(
|
||||
hass, "async_block_till_done", side_effect=asyncio.TimeoutError
|
||||
), patch("homeassistant.core._async_create_timer") as mock_timer:
|
||||
await hass.async_start()
|
||||
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
"""Test requirements module."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import loader, setup
|
||||
from homeassistant.requirements import (
|
||||
CONSTRAINT_FILE,
|
||||
PROGRESS_FILE,
|
||||
RequirementsNotFound,
|
||||
_install,
|
||||
async_get_integration_with_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):
|
||||
"""Test that we load discovery requirements."""
|
||||
hass.config.skip_pip = False
|
||||
|
|
|
@ -488,15 +488,12 @@ async def test_component_warn_slow_setup(hass):
|
|||
assert result
|
||||
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]
|
||||
|
||||
assert timeout == setup.SLOW_SETUP_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
|
||||
|
||||
|
||||
|
@ -508,8 +505,7 @@ async def test_platform_no_warn_slow(hass):
|
|||
with patch.object(hass.loop, "call_later") as mock_call:
|
||||
result = await setup.async_setup_component(hass, "test_component1", {})
|
||||
assert result
|
||||
timeout, function = mock_call.mock_calls[0][1][:2]
|
||||
assert timeout == setup.SLOW_SETUP_MAX_WAIT
|
||||
assert len(mock_call.mock_calls) == 0
|
||||
|
||||
|
||||
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