2013-09-25 03:39:58 +02:00
|
|
|
"""
|
2015-12-28 06:14:35 +01:00
|
|
|
Core components of Home Assistant.
|
2013-09-25 03:39:58 +02:00
|
|
|
|
2014-01-20 04:10:40 +01:00
|
|
|
Home Assistant is a Home Automation framework for observing the state
|
2014-01-20 08:37:40 +01:00
|
|
|
of entities and react to changes.
|
2013-09-25 03:39:58 +02:00
|
|
|
"""
|
2016-09-13 04:16:14 +02:00
|
|
|
import asyncio
|
2018-07-23 10:24:39 +02:00
|
|
|
import datetime
|
2016-02-19 06:27:50 +01:00
|
|
|
import enum
|
2019-01-15 00:08:44 +01:00
|
|
|
import functools
|
2020-05-08 02:29:47 +02:00
|
|
|
from ipaddress import ip_address
|
2013-09-30 09:20:27 +02:00
|
|
|
import logging
|
2016-02-19 06:27:50 +01:00
|
|
|
import os
|
2017-06-26 00:10:30 +02:00
|
|
|
import pathlib
|
2020-07-23 06:53:01 +02:00
|
|
|
import random
|
2020-02-24 17:35:02 +01:00
|
|
|
import re
|
2016-09-18 03:28:01 +02:00
|
|
|
import threading
|
2017-02-10 18:00:17 +01:00
|
|
|
from time import monotonic
|
2016-02-10 08:27:01 +01:00
|
|
|
from types import MappingProxyType
|
2019-09-04 05:36:04 +02:00
|
|
|
from typing import (
|
2019-12-09 16:42:10 +01:00
|
|
|
TYPE_CHECKING,
|
2019-07-31 21:25:30 +02:00
|
|
|
Any,
|
2019-12-09 16:42:10 +01:00
|
|
|
Awaitable,
|
2019-07-31 21:25:30 +02:00
|
|
|
Callable,
|
|
|
|
Coroutine,
|
2019-12-09 16:42:10 +01:00
|
|
|
Dict,
|
2020-07-24 04:03:42 +02:00
|
|
|
Iterable,
|
2019-12-09 16:42:10 +01:00
|
|
|
List,
|
2019-09-29 19:07:49 +02:00
|
|
|
Mapping,
|
2019-12-09 16:42:10 +01:00
|
|
|
Optional,
|
|
|
|
Set,
|
|
|
|
TypeVar,
|
2020-04-05 00:36:33 +02:00
|
|
|
Union,
|
2020-05-08 02:29:47 +02:00
|
|
|
cast,
|
2019-07-31 21:25:30 +02:00
|
|
|
)
|
2019-12-09 16:42:10 +01:00
|
|
|
import uuid
|
2016-08-08 01:26:35 +02:00
|
|
|
|
2017-04-08 23:53:32 +02:00
|
|
|
from async_timeout import timeout
|
2018-07-29 02:53:37 +02:00
|
|
|
import attr
|
2016-03-31 20:36:59 +02:00
|
|
|
import voluptuous as vol
|
2020-05-08 02:29:47 +02:00
|
|
|
import yarl
|
2016-03-31 20:36:59 +02:00
|
|
|
|
2020-04-16 00:32:10 +02:00
|
|
|
from homeassistant import block_async_io, loader, util
|
2016-02-19 06:27:50 +01:00
|
|
|
from homeassistant.const import (
|
2019-07-31 21:25:30 +02:00
|
|
|
ATTR_DOMAIN,
|
|
|
|
ATTR_FRIENDLY_NAME,
|
|
|
|
ATTR_NOW,
|
2019-12-09 16:42:10 +01:00
|
|
|
ATTR_SECONDS,
|
2019-07-31 21:25:30 +02:00
|
|
|
ATTR_SERVICE,
|
|
|
|
ATTR_SERVICE_DATA,
|
|
|
|
CONF_UNIT_SYSTEM_IMPERIAL,
|
|
|
|
EVENT_CALL_SERVICE,
|
|
|
|
EVENT_CORE_CONFIG_UPDATE,
|
2019-12-09 16:42:10 +01:00
|
|
|
EVENT_HOMEASSISTANT_CLOSE,
|
2020-03-30 19:18:39 +02:00
|
|
|
EVENT_HOMEASSISTANT_FINAL_WRITE,
|
2019-07-31 21:25:30 +02:00
|
|
|
EVENT_HOMEASSISTANT_START,
|
2020-04-24 23:13:39 +02:00
|
|
|
EVENT_HOMEASSISTANT_STARTED,
|
2019-07-31 21:25:30 +02:00
|
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
|
|
EVENT_SERVICE_REGISTERED,
|
2019-12-09 16:42:10 +01:00
|
|
|
EVENT_SERVICE_REMOVED,
|
2019-07-31 21:25:30 +02:00
|
|
|
EVENT_STATE_CHANGED,
|
|
|
|
EVENT_TIME_CHANGED,
|
|
|
|
EVENT_TIMER_OUT_OF_SYNC,
|
2020-04-12 21:44:56 +02:00
|
|
|
LENGTH_METERS,
|
2019-07-31 21:25:30 +02:00
|
|
|
MATCH_ALL,
|
|
|
|
__version__,
|
|
|
|
)
|
2016-02-19 06:27:50 +01:00
|
|
|
from homeassistant.exceptions import (
|
2019-07-31 21:25:30 +02:00
|
|
|
HomeAssistantError,
|
|
|
|
InvalidEntityFormatError,
|
|
|
|
InvalidStateError,
|
|
|
|
ServiceNotFound,
|
2019-12-09 16:42:10 +01:00
|
|
|
Unauthorized,
|
2019-07-31 21:25:30 +02:00
|
|
|
)
|
2020-05-08 02:29:47 +02:00
|
|
|
from homeassistant.util import location, network
|
2019-12-09 16:42:10 +01:00
|
|
|
from homeassistant.util.async_ import fire_coroutine_threadsafe, run_callback_threadsafe
|
|
|
|
import homeassistant.util.dt as dt_util
|
2020-04-28 23:31:35 +02:00
|
|
|
from homeassistant.util.thread import fix_threading_exception_logging
|
2019-12-09 16:42:10 +01:00
|
|
|
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem
|
2013-09-30 09:20:27 +02:00
|
|
|
|
2018-07-18 00:28:44 +02:00
|
|
|
# Typing imports that create a circular dependency
|
|
|
|
if TYPE_CHECKING:
|
2020-06-06 20:34:56 +02:00
|
|
|
from homeassistant.auth import AuthManager
|
2019-10-18 22:06:33 +02:00
|
|
|
from homeassistant.config_entries import ConfigEntries
|
|
|
|
from homeassistant.components.http import HomeAssistantHTTP
|
2018-07-18 00:28:44 +02:00
|
|
|
|
2020-04-16 00:32:10 +02:00
|
|
|
|
|
|
|
block_async_io.enable()
|
2020-04-28 23:31:35 +02:00
|
|
|
fix_threading_exception_logging()
|
2020-04-16 00:32:10 +02:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
T = TypeVar("T")
|
2020-05-08 02:29:47 +02:00
|
|
|
_UNDEF: dict = {}
|
2020-05-01 01:47:14 +02:00
|
|
|
# pylint: disable=invalid-name
|
2019-07-31 21:25:30 +02:00
|
|
|
CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable)
|
2018-07-23 10:24:39 +02:00
|
|
|
CALLBACK_TYPE = Callable[[], None]
|
2018-07-26 08:55:42 +02:00
|
|
|
# pylint: enable=invalid-name
|
2018-05-12 23:44:53 +02:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
CORE_STORAGE_KEY = "core.config"
|
2019-05-20 20:02:36 +02:00
|
|
|
CORE_STORAGE_VERSION = 1
|
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
DOMAIN = "homeassistant"
|
2014-01-05 02:55:05 +01:00
|
|
|
|
2020-07-24 04:03:42 +02:00
|
|
|
# How long to wait to log tasks that are blocking
|
|
|
|
BLOCK_LOG_TIMEOUT = 60
|
|
|
|
|
2014-12-14 07:40:00 +01:00
|
|
|
# How long we wait for the result of a service call
|
|
|
|
SERVICE_CALL_LIMIT = 10 # seconds
|
|
|
|
|
2019-05-20 20:02:36 +02:00
|
|
|
# Source of core configuration
|
2019-07-31 21:25:30 +02:00
|
|
|
SOURCE_DISCOVERED = "discovered"
|
|
|
|
SOURCE_STORAGE = "storage"
|
|
|
|
SOURCE_YAML = "yaml"
|
2019-05-20 20:02:36 +02:00
|
|
|
|
2020-03-12 00:34:50 +01:00
|
|
|
# How long to wait until things that run on startup have to finish.
|
2017-04-08 23:53:32 +02:00
|
|
|
TIMEOUT_EVENT_START = 15
|
2016-11-09 17:41:17 +01:00
|
|
|
|
2014-11-08 22:57:08 +01:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2013-11-11 23:58:57 +01:00
|
|
|
|
2016-08-09 05:21:40 +02:00
|
|
|
def split_entity_id(entity_id: str) -> List[str]:
|
|
|
|
"""Split a state entity_id into domain, object_id."""
|
|
|
|
return entity_id.split(".", 1)
|
|
|
|
|
|
|
|
|
2020-02-24 17:35:02 +01:00
|
|
|
VALID_ENTITY_ID = re.compile(r"^(?!.+__)(?!_)[\da-z_]+(?<!_)\.(?!_)[\da-z_]+(?<!_)$")
|
|
|
|
|
|
|
|
|
2016-08-09 05:21:40 +02:00
|
|
|
def valid_entity_id(entity_id: str) -> bool:
|
2019-01-21 18:45:11 +01:00
|
|
|
"""Test if an entity ID is a valid format.
|
|
|
|
|
|
|
|
Format: <domain>.<entity> where both are slugs.
|
|
|
|
"""
|
2020-02-24 17:35:02 +01:00
|
|
|
return VALID_ENTITY_ID.match(entity_id) is not None
|
2016-08-09 05:21:40 +02:00
|
|
|
|
|
|
|
|
2017-10-25 18:05:30 +02:00
|
|
|
def valid_state(state: str) -> bool:
|
2018-01-27 20:58:27 +01:00
|
|
|
"""Test if a state is valid."""
|
2017-10-25 18:05:30 +02:00
|
|
|
return len(state) < 256
|
|
|
|
|
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def callback(func: CALLABLE_T) -> CALLABLE_T:
|
2016-10-05 05:44:32 +02:00
|
|
|
"""Annotation to mark method as safe to call from within the event loop."""
|
2019-07-31 21:25:30 +02:00
|
|
|
setattr(func, "_hass_callback", True)
|
2016-10-05 05:44:32 +02:00
|
|
|
return func
|
|
|
|
|
|
|
|
|
|
|
|
def is_callback(func: Callable[..., Any]) -> bool:
|
|
|
|
"""Check if function is safe to be called in the event loop."""
|
2019-07-31 21:25:30 +02:00
|
|
|
return getattr(func, "_hass_callback", False) is True
|
2016-10-05 05:44:32 +02:00
|
|
|
|
|
|
|
|
2016-06-30 18:02:12 +02:00
|
|
|
class CoreState(enum.Enum):
|
|
|
|
"""Represent the current state of Home Assistant."""
|
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
not_running = "NOT_RUNNING"
|
|
|
|
starting = "STARTING"
|
|
|
|
running = "RUNNING"
|
|
|
|
stopping = "STOPPING"
|
2020-04-02 19:25:28 +02:00
|
|
|
final_write = "FINAL_WRITE"
|
2020-07-09 16:15:14 +02:00
|
|
|
stopped = "STOPPED"
|
2016-06-30 18:02:12 +02:00
|
|
|
|
2016-07-21 07:38:52 +02:00
|
|
|
def __str__(self) -> str:
|
2016-06-30 18:02:12 +02:00
|
|
|
"""Return the event."""
|
2018-07-13 19:14:45 +02:00
|
|
|
return self.value # type: ignore
|
2016-06-30 18:02:12 +02:00
|
|
|
|
|
|
|
|
2018-07-20 10:45:20 +02:00
|
|
|
class HomeAssistant:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Root object of the Home Assistant home automation."""
|
2013-09-30 09:20:27 +02:00
|
|
|
|
2020-06-06 20:34:56 +02:00
|
|
|
auth: "AuthManager"
|
2019-10-18 22:06:33 +02:00
|
|
|
http: "HomeAssistantHTTP" = None # type: ignore
|
|
|
|
config_entries: "ConfigEntries" = None # type: ignore
|
|
|
|
|
2020-07-07 00:58:53 +02:00
|
|
|
def __init__(self) -> None:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Initialize new Home Assistant object."""
|
2020-07-07 00:58:53 +02:00
|
|
|
self.loop = asyncio.get_running_loop()
|
2019-07-31 01:44:39 +02:00
|
|
|
self._pending_tasks: list = []
|
2017-04-06 08:23:02 +02:00
|
|
|
self._track_task = True
|
2016-10-18 04:38:41 +02:00
|
|
|
self.bus = EventBus(self)
|
2016-11-24 23:02:39 +01:00
|
|
|
self.services = ServiceRegistry(self)
|
2016-09-13 04:16:14 +02:00
|
|
|
self.states = StateMachine(self.bus, self.loop)
|
2019-07-31 01:44:39 +02:00
|
|
|
self.config = Config(self)
|
2017-10-08 17:17:54 +02:00
|
|
|
self.components = loader.Components(self)
|
|
|
|
self.helpers = loader.Helpers(self)
|
2016-10-29 23:51:17 +02:00
|
|
|
# This is a dictionary that any component can store any data on.
|
2019-07-31 01:44:39 +02:00
|
|
|
self.data: dict = {}
|
2016-06-30 18:02:12 +02:00
|
|
|
self.state = CoreState.not_running
|
2019-07-31 01:44:39 +02:00
|
|
|
self.exit_code = 0
|
2018-09-19 15:40:02 +02:00
|
|
|
# If not None, use to signal end-of-loop
|
2019-07-31 01:44:39 +02:00
|
|
|
self._stopped: Optional[asyncio.Event] = None
|
2016-06-30 18:02:12 +02:00
|
|
|
|
|
|
|
@property
|
2016-07-21 07:38:52 +02:00
|
|
|
def is_running(self) -> bool:
|
2016-06-30 18:02:12 +02:00
|
|
|
"""Return if Home Assistant is running."""
|
2016-08-18 03:58:00 +02:00
|
|
|
return self.state in (CoreState.starting, CoreState.running)
|
2015-01-18 06:13:02 +01:00
|
|
|
|
2020-06-02 20:54:11 +02:00
|
|
|
@property
|
|
|
|
def is_stopping(self) -> bool:
|
|
|
|
"""Return if Home Assistant is stopping."""
|
|
|
|
return self.state in (CoreState.stopping, CoreState.final_write)
|
|
|
|
|
2018-05-12 23:44:53 +02:00
|
|
|
def start(self) -> int:
|
2020-01-05 13:09:17 +01:00
|
|
|
"""Start Home Assistant.
|
2018-09-19 15:40:02 +02:00
|
|
|
|
|
|
|
Note: This function is only used for testing.
|
|
|
|
For regular use, use "await hass.run()".
|
|
|
|
"""
|
2016-09-13 04:16:14 +02:00
|
|
|
# Register the async start
|
2017-04-07 06:00:58 +02:00
|
|
|
fire_coroutine_threadsafe(self.async_start(), self.loop)
|
2013-10-08 08:55:19 +02:00
|
|
|
|
2018-09-19 15:40:02 +02:00
|
|
|
# Run forever
|
2016-05-20 08:20:59 +02:00
|
|
|
try:
|
2016-09-13 04:16:14 +02:00
|
|
|
# Block until stopped
|
|
|
|
_LOGGER.info("Starting Home Assistant core loop")
|
|
|
|
self.loop.run_forever()
|
2016-10-08 18:56:36 +02:00
|
|
|
finally:
|
|
|
|
self.loop.close()
|
2018-05-12 23:44:53 +02:00
|
|
|
return self.exit_code
|
2016-09-13 04:16:14 +02:00
|
|
|
|
2018-09-19 15:40:02 +02:00
|
|
|
async def async_run(self, *, attach_signals: bool = True) -> int:
|
|
|
|
"""Home Assistant main entry point.
|
|
|
|
|
|
|
|
Start Home Assistant and block until stopped.
|
|
|
|
|
|
|
|
This method is a coroutine.
|
|
|
|
"""
|
|
|
|
if self.state != CoreState.not_running:
|
2020-01-05 13:09:17 +01:00
|
|
|
raise RuntimeError("Home Assistant is already running")
|
2018-09-19 15:40:02 +02:00
|
|
|
|
|
|
|
# _async_stop will set this instead of stopping the loop
|
|
|
|
self._stopped = asyncio.Event()
|
|
|
|
|
|
|
|
await self.async_start()
|
|
|
|
if attach_signals:
|
2020-04-04 17:07:36 +02:00
|
|
|
# pylint: disable=import-outside-toplevel
|
2019-07-31 21:25:30 +02:00
|
|
|
from homeassistant.helpers.signal import async_register_signal_handling
|
|
|
|
|
2018-09-19 15:40:02 +02:00
|
|
|
async_register_signal_handling(self)
|
|
|
|
|
|
|
|
await self._stopped.wait()
|
|
|
|
return self.exit_code
|
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
async def async_start(self) -> None:
|
2016-09-13 04:16:14 +02:00
|
|
|
"""Finalize startup from inside the event loop.
|
|
|
|
|
|
|
|
This method is a coroutine.
|
|
|
|
"""
|
2016-10-31 16:47:29 +01:00
|
|
|
_LOGGER.info("Starting Home Assistant")
|
2019-07-31 21:25:30 +02:00
|
|
|
setattr(self.loop, "_thread_ident", threading.get_ident())
|
2020-06-16 00:22:53 +02:00
|
|
|
|
|
|
|
self.state = CoreState.starting
|
2020-06-02 20:54:11 +02:00
|
|
|
self.bus.async_fire(EVENT_CORE_CONFIG_UPDATE)
|
2020-06-16 00:22:53 +02:00
|
|
|
self.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
2017-04-08 23:53:32 +02:00
|
|
|
|
|
|
|
try:
|
2017-04-30 07:04:49 +02:00
|
|
|
# Only block for EVENT_HOMEASSISTANT_START listener
|
2017-04-11 18:09:31 +02:00
|
|
|
self.async_stop_track_tasks()
|
2020-03-12 00:34:50 +01:00
|
|
|
async with timeout(TIMEOUT_EVENT_START):
|
2018-02-23 08:22:27 +01:00
|
|
|
await self.async_block_till_done()
|
2017-04-08 23:53:32 +02:00
|
|
|
except asyncio.TimeoutError:
|
|
|
|
_LOGGER.warning(
|
2019-07-31 21:25:30 +02:00
|
|
|
"Something is blocking Home Assistant from wrapping up the "
|
|
|
|
"start up phase. We're going to continue anyway. Please "
|
|
|
|
"report the following info at http://bit.ly/2ogP58T : %s",
|
|
|
|
", ".join(self.config.components),
|
|
|
|
)
|
2017-04-08 23:53:32 +02:00
|
|
|
|
2017-06-27 10:36:00 +02:00
|
|
|
# Allow automations to set up the start triggers before changing state
|
2018-07-29 02:53:37 +02:00
|
|
|
await asyncio.sleep(0)
|
2018-09-19 15:40:02 +02:00
|
|
|
|
|
|
|
if self.state != CoreState.starting:
|
|
|
|
_LOGGER.warning(
|
2019-07-31 21:25:30 +02:00
|
|
|
"Home Assistant startup has been interrupted. "
|
2020-07-05 23:04:19 +02:00
|
|
|
"Its state may be inconsistent"
|
2019-07-31 21:25:30 +02:00
|
|
|
)
|
2018-09-19 15:40:02 +02:00
|
|
|
return
|
|
|
|
|
2016-09-13 04:16:14 +02:00
|
|
|
self.state = CoreState.running
|
2020-06-16 00:22:53 +02:00
|
|
|
self.bus.async_fire(EVENT_CORE_CONFIG_UPDATE)
|
2020-04-24 23:13:39 +02:00
|
|
|
self.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
2020-06-16 00:22:53 +02:00
|
|
|
_async_create_timer(self)
|
2016-09-13 04:16:14 +02:00
|
|
|
|
2019-08-20 21:43:50 +02:00
|
|
|
def add_job(self, target: Callable[..., Any], *args: Any) -> None:
|
2016-11-24 23:49:29 +01:00
|
|
|
"""Add job to the executor pool.
|
2016-11-09 17:41:17 +01:00
|
|
|
|
2016-11-24 23:49:29 +01:00
|
|
|
target: target to call.
|
|
|
|
args: parameters for method to call.
|
2016-11-09 17:41:17 +01:00
|
|
|
"""
|
2016-12-16 06:30:09 +01:00
|
|
|
if target is None:
|
2017-01-20 08:55:29 +01:00
|
|
|
raise ValueError("Don't call add_job with None")
|
2016-11-24 23:49:29 +01:00
|
|
|
self.loop.call_soon_threadsafe(self.async_add_job, target, *args)
|
2016-11-09 17:41:17 +01:00
|
|
|
|
2016-11-24 23:49:29 +01:00
|
|
|
@callback
|
2018-05-12 23:44:53 +02:00
|
|
|
def async_add_job(
|
2019-07-31 21:25:30 +02:00
|
|
|
self, target: Callable[..., Any], *args: Any
|
|
|
|
) -> Optional[asyncio.Future]:
|
2018-07-13 12:24:51 +02:00
|
|
|
"""Add a job from within the event loop.
|
2016-09-18 03:28:01 +02:00
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
This method must be run in the event loop.
|
|
|
|
|
2016-09-18 03:28:01 +02:00
|
|
|
target: target to call.
|
|
|
|
args: parameters for method to call.
|
|
|
|
"""
|
2016-11-05 17:27:55 +01:00
|
|
|
task = None
|
|
|
|
|
2019-01-21 07:27:32 +01:00
|
|
|
# Check for partials to properly determine if coroutine function
|
2019-01-15 00:08:44 +01:00
|
|
|
check_target = target
|
2019-07-07 03:58:33 +02:00
|
|
|
while isinstance(check_target, functools.partial):
|
|
|
|
check_target = check_target.func
|
2019-01-15 00:08:44 +01:00
|
|
|
|
|
|
|
if asyncio.iscoroutine(check_target):
|
2018-07-18 00:28:44 +02:00
|
|
|
task = self.loop.create_task(target) # type: ignore
|
2019-01-15 00:08:44 +01:00
|
|
|
elif asyncio.iscoroutinefunction(check_target):
|
2016-11-05 17:27:55 +01:00
|
|
|
task = self.loop.create_task(target(*args))
|
2020-02-10 04:47:16 +01:00
|
|
|
elif is_callback(check_target):
|
|
|
|
self.loop.call_soon(target, *args)
|
2016-09-18 03:28:01 +02:00
|
|
|
else:
|
2018-07-23 10:24:39 +02:00
|
|
|
task = self.loop.run_in_executor( # type: ignore
|
2019-07-31 21:25:30 +02:00
|
|
|
None, target, *args
|
|
|
|
)
|
2016-11-05 17:27:55 +01:00
|
|
|
|
2017-09-23 17:15:46 +02:00
|
|
|
# If a task is scheduled
|
2017-03-01 05:33:19 +01:00
|
|
|
if self._track_task and task is not None:
|
2016-11-08 10:24:50 +01:00
|
|
|
self._pending_tasks.append(task)
|
|
|
|
|
2017-03-01 05:33:19 +01:00
|
|
|
return task
|
|
|
|
|
2018-07-13 12:24:51 +02:00
|
|
|
@callback
|
|
|
|
def async_create_task(self, target: Coroutine) -> asyncio.tasks.Task:
|
|
|
|
"""Create a task from within the eventloop.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
|
|
|
|
target: target to call.
|
|
|
|
"""
|
2019-07-31 01:44:39 +02:00
|
|
|
task: asyncio.tasks.Task = self.loop.create_task(target)
|
2018-07-13 12:24:51 +02:00
|
|
|
|
|
|
|
if self._track_task:
|
|
|
|
self._pending_tasks.append(task)
|
|
|
|
|
|
|
|
return task
|
|
|
|
|
2018-06-25 18:53:49 +02:00
|
|
|
@callback
|
|
|
|
def async_add_executor_job(
|
2019-07-31 21:25:30 +02:00
|
|
|
self, target: Callable[..., T], *args: Any
|
|
|
|
) -> Awaitable[T]:
|
2018-06-25 18:53:49 +02:00
|
|
|
"""Add an executor job from within the event loop."""
|
2019-07-31 21:25:30 +02:00
|
|
|
task = self.loop.run_in_executor(None, target, *args)
|
2018-06-25 18:53:49 +02:00
|
|
|
|
|
|
|
# If a task is scheduled
|
|
|
|
if self._track_task:
|
|
|
|
self._pending_tasks.append(task)
|
|
|
|
|
|
|
|
return task
|
|
|
|
|
2016-11-24 23:49:29 +01:00
|
|
|
@callback
|
2018-07-23 10:24:39 +02:00
|
|
|
def async_track_tasks(self) -> None:
|
2016-11-24 23:49:29 +01:00
|
|
|
"""Track tasks so you can wait for all tasks to be done."""
|
2017-03-01 05:33:19 +01:00
|
|
|
self._track_task = True
|
2016-11-24 23:49:29 +01:00
|
|
|
|
2017-04-11 18:09:31 +02:00
|
|
|
@callback
|
2018-07-23 10:24:39 +02:00
|
|
|
def async_stop_track_tasks(self) -> None:
|
2017-04-11 18:09:31 +02:00
|
|
|
"""Stop track tasks so you can't wait for all tasks to be done."""
|
2017-03-01 05:33:19 +01:00
|
|
|
self._track_task = False
|
2016-11-30 22:02:45 +01:00
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
@callback
|
2020-04-14 02:41:01 +02:00
|
|
|
def async_run_job(
|
|
|
|
self, target: Callable[..., Union[None, Awaitable]], *args: Any
|
|
|
|
) -> None:
|
2016-10-05 05:44:32 +02:00
|
|
|
"""Run a job from within the event loop.
|
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
This method must be run in the event loop.
|
|
|
|
|
2016-10-05 05:44:32 +02:00
|
|
|
target: target to call.
|
|
|
|
args: parameters for method to call.
|
|
|
|
"""
|
2020-02-10 04:47:16 +01:00
|
|
|
if (
|
|
|
|
not asyncio.iscoroutine(target)
|
|
|
|
and not asyncio.iscoroutinefunction(target)
|
|
|
|
and is_callback(target)
|
|
|
|
):
|
2016-10-05 05:44:32 +02:00
|
|
|
target(*args)
|
|
|
|
else:
|
|
|
|
self.async_add_job(target, *args)
|
|
|
|
|
2016-10-31 16:47:29 +01:00
|
|
|
def block_till_done(self) -> None:
|
2020-03-12 00:34:50 +01:00
|
|
|
"""Block until all pending work is done."""
|
2019-10-01 16:59:06 +02:00
|
|
|
asyncio.run_coroutine_threadsafe(
|
|
|
|
self.async_block_till_done(), self.loop
|
|
|
|
).result()
|
2016-09-13 04:16:14 +02:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
async def async_block_till_done(self) -> None:
|
2020-03-12 00:34:50 +01:00
|
|
|
"""Block until all pending work is done."""
|
2016-11-24 23:49:29 +01:00
|
|
|
# To flush out any call_soon_threadsafe
|
2018-07-29 02:53:37 +02:00
|
|
|
await asyncio.sleep(0)
|
2016-11-24 23:49:29 +01:00
|
|
|
|
|
|
|
while self._pending_tasks:
|
2019-07-31 21:25:30 +02:00
|
|
|
pending = [task for task in self._pending_tasks if not task.done()]
|
2016-11-08 10:24:50 +01:00
|
|
|
self._pending_tasks.clear()
|
2017-04-24 05:41:09 +02:00
|
|
|
if pending:
|
2020-07-24 04:03:42 +02:00
|
|
|
await self._await_and_log_pending(pending)
|
2016-11-24 23:49:29 +01:00
|
|
|
else:
|
2018-07-29 02:53:37 +02:00
|
|
|
await asyncio.sleep(0)
|
2014-11-23 18:51:16 +01:00
|
|
|
|
2020-07-24 04:03:42 +02:00
|
|
|
async def _await_and_log_pending(self, pending: Iterable[Awaitable[Any]]) -> None:
|
|
|
|
"""Await and log tasks that take a long time."""
|
|
|
|
wait_time = 0
|
|
|
|
while pending:
|
|
|
|
_, pending = await asyncio.wait(pending, timeout=BLOCK_LOG_TIMEOUT)
|
|
|
|
if not pending:
|
|
|
|
return
|
|
|
|
wait_time += BLOCK_LOG_TIMEOUT
|
|
|
|
for task in pending:
|
|
|
|
_LOGGER.debug("Waited %s seconds for task: %s", wait_time, task)
|
|
|
|
|
2016-07-21 07:38:52 +02:00
|
|
|
def stop(self) -> None:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Stop Home Assistant and shuts down all threads."""
|
2018-09-19 15:40:02 +02:00
|
|
|
if self.state == CoreState.not_running: # just ignore
|
|
|
|
return
|
2017-04-07 06:00:58 +02:00
|
|
|
fire_coroutine_threadsafe(self.async_stop(), self.loop)
|
2016-09-13 04:16:14 +02:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None:
|
2016-09-13 04:16:14 +02:00
|
|
|
"""Stop Home Assistant and shuts down all threads.
|
|
|
|
|
2018-09-19 15:40:02 +02:00
|
|
|
The "force" flag commands async_stop to proceed regardless of
|
|
|
|
Home Assistan't current state. You should not set this flag
|
|
|
|
unless you're testing.
|
|
|
|
|
2016-09-13 04:16:14 +02:00
|
|
|
This method is a coroutine.
|
|
|
|
"""
|
2018-09-19 15:40:02 +02:00
|
|
|
if not force:
|
|
|
|
# Some tests require async_stop to run,
|
|
|
|
# regardless of the state of the loop.
|
|
|
|
if self.state == CoreState.not_running: # just ignore
|
|
|
|
return
|
2020-05-08 02:29:47 +02:00
|
|
|
if self.state in [CoreState.stopping, CoreState.final_write]:
|
2018-09-19 15:40:02 +02:00
|
|
|
_LOGGER.info("async_stop called twice: ignored")
|
|
|
|
return
|
|
|
|
if self.state == CoreState.starting:
|
|
|
|
# This may not work
|
|
|
|
_LOGGER.warning("async_stop called before startup is complete")
|
|
|
|
|
2017-02-13 06:24:07 +01:00
|
|
|
# stage 1
|
2016-06-30 18:02:12 +02:00
|
|
|
self.state = CoreState.stopping
|
2016-11-24 23:49:29 +01:00
|
|
|
self.async_track_tasks()
|
2016-09-13 04:16:14 +02:00
|
|
|
self.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
2018-02-23 08:22:27 +01:00
|
|
|
await self.async_block_till_done()
|
2017-01-05 23:09:04 +01:00
|
|
|
|
2017-02-13 06:24:07 +01:00
|
|
|
# stage 2
|
2020-04-02 19:25:28 +02:00
|
|
|
self.state = CoreState.final_write
|
2020-03-30 19:18:39 +02:00
|
|
|
self.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE)
|
|
|
|
await self.async_block_till_done()
|
|
|
|
|
|
|
|
# stage 3
|
2017-02-13 06:24:07 +01:00
|
|
|
self.state = CoreState.not_running
|
|
|
|
self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE)
|
2018-02-23 08:22:27 +01:00
|
|
|
await self.async_block_till_done()
|
2020-07-07 00:58:53 +02:00
|
|
|
|
|
|
|
# Python 3.9+ and backported in runner.py
|
|
|
|
await self.loop.shutdown_default_executor() # type: ignore
|
2016-12-17 21:21:52 +01:00
|
|
|
|
2017-02-08 18:17:52 +01:00
|
|
|
self.exit_code = exit_code
|
2020-07-09 16:15:14 +02:00
|
|
|
self.state = CoreState.stopped
|
2018-09-19 15:40:02 +02:00
|
|
|
|
|
|
|
if self._stopped is not None:
|
|
|
|
self._stopped.set()
|
|
|
|
else:
|
|
|
|
self.loop.stop()
|
2014-11-23 18:51:16 +01:00
|
|
|
|
2014-01-27 03:44:36 +01:00
|
|
|
|
2018-07-29 02:53:37 +02:00
|
|
|
@attr.s(slots=True, frozen=True)
|
|
|
|
class Context:
|
|
|
|
"""The context that triggered something."""
|
|
|
|
|
2020-07-14 19:30:30 +02:00
|
|
|
user_id: str = attr.ib(default=None)
|
|
|
|
parent_id: Optional[str] = attr.ib(default=None)
|
2020-07-23 06:53:01 +02:00
|
|
|
# The uuid1 uses a random multicast MAC address instead of the real MAC address
|
|
|
|
# of the machine without the overhead of calling the getrandom() system call.
|
|
|
|
#
|
|
|
|
# This is effectively equivalent to PostgreSQL's uuid_generate_v1mc() function
|
|
|
|
id: str = attr.ib(
|
|
|
|
factory=lambda: uuid.uuid1(node=random.getrandbits(48) | (1 << 40)).hex
|
|
|
|
)
|
2018-07-29 02:53:37 +02:00
|
|
|
|
|
|
|
def as_dict(self) -> dict:
|
|
|
|
"""Return a dictionary representation of the context."""
|
2019-07-31 21:25:30 +02:00
|
|
|
return {"id": self.id, "parent_id": self.parent_id, "user_id": self.user_id}
|
2018-07-29 02:53:37 +02:00
|
|
|
|
|
|
|
|
2014-04-29 09:30:31 +02:00
|
|
|
class EventOrigin(enum.Enum):
|
2016-03-08 00:06:04 +01:00
|
|
|
"""Represent the origin of an event."""
|
2014-04-29 09:30:31 +02:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
local = "LOCAL"
|
|
|
|
remote = "REMOTE"
|
2014-04-29 09:30:31 +02:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def __str__(self) -> str:
|
2016-03-08 00:06:04 +01:00
|
|
|
"""Return the event."""
|
2018-07-23 10:24:39 +02:00
|
|
|
return self.value # type: ignore
|
2014-04-29 09:30:31 +02:00
|
|
|
|
|
|
|
|
2018-07-20 10:45:20 +02:00
|
|
|
class Event:
|
2017-04-30 07:04:49 +02:00
|
|
|
"""Representation of an event within the bus."""
|
2014-01-27 03:44:36 +01:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
__slots__ = ["event_type", "data", "origin", "time_fired", "context"]
|
2014-01-27 03:44:36 +01:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
event_type: str,
|
2020-04-17 20:33:58 +02:00
|
|
|
data: Optional[Dict[str, Any]] = None,
|
2019-07-31 21:25:30 +02:00
|
|
|
origin: EventOrigin = EventOrigin.local,
|
|
|
|
time_fired: Optional[int] = None,
|
|
|
|
context: Optional[Context] = None,
|
|
|
|
) -> None:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Initialize a new event."""
|
2014-01-27 03:44:36 +01:00
|
|
|
self.event_type = event_type
|
|
|
|
self.data = data or {}
|
2014-04-29 09:30:31 +02:00
|
|
|
self.origin = origin
|
2016-04-16 09:55:35 +02:00
|
|
|
self.time_fired = time_fired or dt_util.utcnow()
|
2019-07-31 01:44:39 +02:00
|
|
|
self.context: Context = context or Context()
|
2014-01-27 03:44:36 +01:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def as_dict(self) -> Dict:
|
2016-10-18 04:38:41 +02:00
|
|
|
"""Create a dict representation of this Event.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2015-01-28 09:22:09 +01:00
|
|
|
return {
|
2019-07-31 21:25:30 +02:00
|
|
|
"event_type": self.event_type,
|
|
|
|
"data": dict(self.data),
|
|
|
|
"origin": str(self.origin),
|
|
|
|
"time_fired": self.time_fired,
|
|
|
|
"context": self.context.as_dict(),
|
2015-01-28 09:22:09 +01:00
|
|
|
}
|
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def __repr__(self) -> str:
|
2016-03-08 00:06:04 +01:00
|
|
|
"""Return the representation."""
|
2014-04-29 09:30:31 +02:00
|
|
|
# pylint: disable=maybe-no-member
|
2014-01-27 03:44:36 +01:00
|
|
|
if self.data:
|
2020-04-12 17:18:09 +02:00
|
|
|
return f"<Event {self.event_type}[{str(self.origin)[0]}]: {util.repr_helper(self.data)}>"
|
2017-07-06 05:02:16 +02:00
|
|
|
|
2020-04-12 17:18:09 +02:00
|
|
|
return f"<Event {self.event_type}[{str(self.origin)[0]}]>"
|
2013-11-11 01:46:48 +01:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def __eq__(self, other: Any) -> bool:
|
2016-03-08 00:06:04 +01:00
|
|
|
"""Return the comparison."""
|
2019-07-31 22:08:31 +02:00
|
|
|
return ( # type: ignore
|
2019-07-31 21:25:30 +02:00
|
|
|
self.__class__ == other.__class__
|
2019-07-31 22:08:31 +02:00
|
|
|
and self.event_type == other.event_type
|
2019-07-31 21:25:30 +02:00
|
|
|
and self.data == other.data
|
|
|
|
and self.origin == other.origin
|
|
|
|
and self.time_fired == other.time_fired
|
|
|
|
and self.context == other.context
|
|
|
|
)
|
2015-04-30 08:21:31 +02:00
|
|
|
|
2013-10-09 03:50:30 +02:00
|
|
|
|
2018-07-20 10:45:20 +02:00
|
|
|
class EventBus:
|
2017-04-30 07:04:49 +02:00
|
|
|
"""Allow the firing of and listening for events."""
|
2013-09-30 09:20:27 +02:00
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
def __init__(self, hass: HomeAssistant) -> None:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Initialize a new event bus."""
|
2019-07-31 01:44:39 +02:00
|
|
|
self._listeners: Dict[str, List[Callable]] = {}
|
2016-10-18 04:38:41 +02:00
|
|
|
self._hass = hass
|
2016-09-13 04:16:14 +02:00
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
@callback
|
2018-07-23 10:24:39 +02:00
|
|
|
def async_listeners(self) -> Dict[str, int]:
|
2017-05-02 22:47:20 +02:00
|
|
|
"""Return dictionary with events and the number of listeners.
|
2016-09-13 04:16:14 +02:00
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2019-07-31 21:25:30 +02:00
|
|
|
return {key: len(self._listeners[key]) for key in self._listeners}
|
2013-09-25 03:39:58 +02:00
|
|
|
|
2013-11-01 20:28:18 +01:00
|
|
|
@property
|
2018-07-23 10:24:39 +02:00
|
|
|
def listeners(self) -> Dict[str, int]:
|
2017-05-02 22:47:20 +02:00
|
|
|
"""Return dictionary with events and the number of listeners."""
|
2020-04-17 20:33:58 +02:00
|
|
|
return run_callback_threadsafe(self._hass.loop, self.async_listeners).result()
|
2014-01-30 07:48:35 +01:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
def fire(
|
|
|
|
self,
|
|
|
|
event_type: str,
|
|
|
|
event_data: Optional[Dict] = None,
|
|
|
|
origin: EventOrigin = EventOrigin.local,
|
|
|
|
context: Optional[Context] = None,
|
|
|
|
) -> None:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Fire an event."""
|
2017-01-20 08:55:29 +01:00
|
|
|
self._hass.loop.call_soon_threadsafe(
|
2019-07-31 21:25:30 +02:00
|
|
|
self.async_fire, event_type, event_data, origin, context
|
|
|
|
)
|
2013-09-30 09:20:27 +02:00
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
@callback
|
2019-07-31 21:25:30 +02:00
|
|
|
def async_fire(
|
|
|
|
self,
|
|
|
|
event_type: str,
|
|
|
|
event_data: Optional[Dict] = None,
|
|
|
|
origin: EventOrigin = EventOrigin.local,
|
|
|
|
context: Optional[Context] = None,
|
|
|
|
) -> None:
|
2016-09-13 04:16:14 +02:00
|
|
|
"""Fire an event.
|
2014-01-27 03:44:36 +01:00
|
|
|
|
2016-09-13 04:16:14 +02:00
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2017-02-13 06:24:07 +01:00
|
|
|
listeners = self._listeners.get(event_type, [])
|
|
|
|
|
|
|
|
# EVENT_HOMEASSISTANT_CLOSE should go only to his listeners
|
2017-11-03 14:19:36 +01:00
|
|
|
match_all_listeners = self._listeners.get(MATCH_ALL)
|
2019-07-31 21:25:30 +02:00
|
|
|
if match_all_listeners is not None and event_type != EVENT_HOMEASSISTANT_CLOSE:
|
2017-11-03 14:19:36 +01:00
|
|
|
listeners = match_all_listeners + listeners
|
2014-01-24 01:49:43 +01:00
|
|
|
|
2018-07-29 02:53:37 +02:00
|
|
|
event = Event(event_type, event_data, origin, None, context)
|
2016-09-13 04:16:14 +02:00
|
|
|
|
|
|
|
if event_type != EVENT_TIME_CHANGED:
|
2018-09-18 15:06:52 +02:00
|
|
|
_LOGGER.debug("Bus:Handling %s", event)
|
2014-01-20 04:10:40 +01:00
|
|
|
|
2016-09-13 04:16:14 +02:00
|
|
|
if not listeners:
|
|
|
|
return
|
|
|
|
|
|
|
|
for func in listeners:
|
2016-10-18 04:38:41 +02:00
|
|
|
self._hass.async_add_job(func, event)
|
2013-09-25 03:39:58 +02:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
def listen(self, event_type: str, listener: Callable) -> CALLBACK_TYPE:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Listen for all events or events of a specific type.
|
2013-09-25 03:39:58 +02:00
|
|
|
|
2014-01-20 04:10:40 +01:00
|
|
|
To listen to all events specify the constant ``MATCH_ALL``
|
2013-10-08 08:55:19 +02:00
|
|
|
as event_type.
|
|
|
|
"""
|
2016-10-18 04:38:41 +02:00
|
|
|
async_remove_listener = run_callback_threadsafe(
|
2019-07-31 21:25:30 +02:00
|
|
|
self._hass.loop, self.async_listen, event_type, listener
|
|
|
|
).result()
|
2014-01-20 04:10:40 +01:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def remove_listener() -> None:
|
2016-08-26 08:25:35 +02:00
|
|
|
"""Remove the listener."""
|
2019-07-31 21:25:30 +02:00
|
|
|
run_callback_threadsafe(self._hass.loop, async_remove_listener).result()
|
2016-08-26 08:25:35 +02:00
|
|
|
|
|
|
|
return remove_listener
|
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
@callback
|
2019-07-31 21:25:30 +02:00
|
|
|
def async_listen(self, event_type: str, listener: Callable) -> CALLBACK_TYPE:
|
2016-09-13 04:16:14 +02:00
|
|
|
"""Listen for all events or events of a specific type.
|
|
|
|
|
|
|
|
To listen to all events specify the constant ``MATCH_ALL``
|
|
|
|
as event_type.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
|
|
|
if event_type in self._listeners:
|
|
|
|
self._listeners[event_type].append(listener)
|
|
|
|
else:
|
|
|
|
self._listeners[event_type] = [listener]
|
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def remove_listener() -> None:
|
2016-09-13 04:16:14 +02:00
|
|
|
"""Remove the listener."""
|
2016-10-18 04:38:41 +02:00
|
|
|
self._async_remove_listener(event_type, listener)
|
2016-09-13 04:16:14 +02:00
|
|
|
|
|
|
|
return remove_listener
|
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
def listen_once(self, event_type: str, listener: Callable) -> CALLBACK_TYPE:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Listen once for event of a specific type.
|
2014-11-29 08:19:59 +01:00
|
|
|
|
|
|
|
To listen to all events specify the constant ``MATCH_ALL``
|
|
|
|
as event_type.
|
|
|
|
|
2016-09-07 15:59:59 +02:00
|
|
|
Returns function to unsubscribe the listener.
|
2014-11-29 08:19:59 +01:00
|
|
|
"""
|
2016-10-18 04:38:41 +02:00
|
|
|
async_remove_listener = run_callback_threadsafe(
|
2019-07-31 21:25:30 +02:00
|
|
|
self._hass.loop, self.async_listen_once, event_type, listener
|
2016-10-18 04:38:41 +02:00
|
|
|
).result()
|
2014-11-29 08:19:59 +01:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def remove_listener() -> None:
|
2016-10-18 04:38:41 +02:00
|
|
|
"""Remove the listener."""
|
2019-07-31 21:25:30 +02:00
|
|
|
run_callback_threadsafe(self._hass.loop, async_remove_listener).result()
|
2014-11-29 08:19:59 +01:00
|
|
|
|
2016-09-07 15:59:59 +02:00
|
|
|
return remove_listener
|
2015-07-26 10:45:49 +02:00
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
@callback
|
2019-07-31 21:25:30 +02:00
|
|
|
def async_listen_once(self, event_type: str, listener: Callable) -> CALLBACK_TYPE:
|
2016-09-13 04:16:14 +02:00
|
|
|
"""Listen once for event of a specific type.
|
|
|
|
|
|
|
|
To listen to all events specify the constant ``MATCH_ALL``
|
|
|
|
as event_type.
|
|
|
|
|
|
|
|
Returns registered listener that can be used with remove_listener.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2019-07-31 21:25:30 +02:00
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
@callback
|
2018-07-23 10:24:39 +02:00
|
|
|
def onetime_listener(event: Event) -> None:
|
|
|
|
"""Remove listener from event bus and then fire listener."""
|
2019-07-31 21:25:30 +02:00
|
|
|
if hasattr(onetime_listener, "run"):
|
2016-09-13 04:16:14 +02:00
|
|
|
return
|
|
|
|
# Set variable so that we will never run twice.
|
|
|
|
# Because the event bus loop might have async_fire queued multiple
|
|
|
|
# times, its possible this listener may already be lined up
|
|
|
|
# multiple times as well.
|
|
|
|
# This will make sure the second time it does nothing.
|
2019-07-31 21:25:30 +02:00
|
|
|
setattr(onetime_listener, "run", True)
|
2016-10-18 04:38:41 +02:00
|
|
|
self._async_remove_listener(event_type, onetime_listener)
|
|
|
|
self._hass.async_run_job(listener, event)
|
2016-09-13 04:16:14 +02:00
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
return self.async_listen(event_type, onetime_listener)
|
2016-09-07 15:59:59 +02:00
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
@callback
|
2019-07-31 21:25:30 +02:00
|
|
|
def _async_remove_listener(self, event_type: str, listener: Callable) -> None:
|
2016-09-13 04:16:14 +02:00
|
|
|
"""Remove a listener of a specific event_type.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
self._listeners[event_type].remove(listener)
|
2013-10-24 01:29:33 +02:00
|
|
|
|
2016-09-13 04:16:14 +02:00
|
|
|
# delete event_type list if empty
|
|
|
|
if not self._listeners[event_type]:
|
|
|
|
self._listeners.pop(event_type)
|
|
|
|
except (KeyError, ValueError):
|
|
|
|
# KeyError is key event_type listener did not exist
|
|
|
|
# ValueError if listener did not exist within event_type
|
2017-01-20 08:55:29 +01:00
|
|
|
_LOGGER.warning("Unable to remove unknown listener %s", listener)
|
2013-09-30 09:20:27 +02:00
|
|
|
|
2013-11-11 01:46:48 +01:00
|
|
|
|
2018-07-20 10:45:20 +02:00
|
|
|
class State:
|
2016-03-08 00:06:04 +01:00
|
|
|
"""Object to represent a state within the state machine.
|
2015-01-02 17:48:20 +01:00
|
|
|
|
|
|
|
entity_id: the entity that is represented.
|
|
|
|
state: the state of the entity
|
|
|
|
attributes: extra information on entity and state
|
|
|
|
last_changed: last time the state was changed, not the attributes.
|
2015-01-19 09:00:01 +01:00
|
|
|
last_updated: last time this object was updated.
|
2018-07-29 02:53:37 +02:00
|
|
|
context: Context in which it was created
|
2015-01-02 17:48:20 +01:00
|
|
|
"""
|
2014-01-20 04:10:40 +01:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
__slots__ = [
|
|
|
|
"entity_id",
|
|
|
|
"state",
|
|
|
|
"attributes",
|
|
|
|
"last_changed",
|
|
|
|
"last_updated",
|
|
|
|
"context",
|
|
|
|
]
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
entity_id: str,
|
2019-09-24 23:20:04 +02:00
|
|
|
state: str,
|
2019-09-29 19:07:49 +02:00
|
|
|
attributes: Optional[Mapping] = None,
|
2019-07-31 21:25:30 +02:00
|
|
|
last_changed: Optional[datetime.datetime] = None,
|
|
|
|
last_updated: Optional[datetime.datetime] = None,
|
|
|
|
context: Optional[Context] = None,
|
2020-06-22 06:58:57 +02:00
|
|
|
validate_entity_id: Optional[bool] = True,
|
2019-07-31 21:25:30 +02:00
|
|
|
) -> None:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Initialize a new state."""
|
2017-10-25 18:05:30 +02:00
|
|
|
state = str(state)
|
|
|
|
|
2020-06-22 06:58:57 +02:00
|
|
|
if validate_entity_id and not valid_entity_id(entity_id):
|
2019-07-31 21:25:30 +02:00
|
|
|
raise InvalidEntityFormatError(
|
2020-01-03 14:47:06 +01:00
|
|
|
f"Invalid entity id encountered: {entity_id}. "
|
|
|
|
"Format should be <domain>.<object_id>"
|
2019-07-31 21:25:30 +02:00
|
|
|
)
|
2014-11-05 04:59:22 +01:00
|
|
|
|
2017-10-25 18:05:30 +02:00
|
|
|
if not valid_state(state):
|
2019-07-31 21:25:30 +02:00
|
|
|
raise InvalidStateError(
|
2020-01-03 14:47:06 +01:00
|
|
|
f"Invalid state encountered for entity id: {entity_id}. "
|
|
|
|
"State max length is 255 characters."
|
2019-07-31 21:25:30 +02:00
|
|
|
)
|
2017-10-25 18:05:30 +02:00
|
|
|
|
2015-02-06 09:00:39 +01:00
|
|
|
self.entity_id = entity_id.lower()
|
2019-09-24 23:20:04 +02:00
|
|
|
self.state = state
|
2016-02-10 08:27:01 +01:00
|
|
|
self.attributes = MappingProxyType(attributes or {})
|
2016-04-16 09:55:35 +02:00
|
|
|
self.last_updated = last_updated or dt_util.utcnow()
|
|
|
|
self.last_changed = last_changed or self.last_updated
|
2018-07-29 02:53:37 +02:00
|
|
|
self.context = context or Context()
|
2014-01-20 04:10:40 +01:00
|
|
|
|
2015-03-17 07:32:18 +01:00
|
|
|
@property
|
2018-07-23 10:24:39 +02:00
|
|
|
def domain(self) -> str:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Domain of this state."""
|
2016-01-24 07:49:49 +01:00
|
|
|
return split_entity_id(self.entity_id)[0]
|
2015-03-17 07:32:18 +01:00
|
|
|
|
2015-03-29 23:39:47 +02:00
|
|
|
@property
|
2018-07-23 10:24:39 +02:00
|
|
|
def object_id(self) -> str:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Object id of this state."""
|
2016-01-24 07:49:49 +01:00
|
|
|
return split_entity_id(self.entity_id)[1]
|
2015-03-29 23:39:47 +02:00
|
|
|
|
|
|
|
@property
|
2018-07-23 10:24:39 +02:00
|
|
|
def name(self) -> str:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Name of this state."""
|
2019-07-31 21:25:30 +02:00
|
|
|
return self.attributes.get(ATTR_FRIENDLY_NAME) or self.object_id.replace(
|
|
|
|
"_", " "
|
|
|
|
)
|
2015-03-29 23:39:47 +02:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def as_dict(self) -> Dict:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Return a dict representation of the State.
|
2014-01-20 04:10:40 +01:00
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
Async friendly.
|
|
|
|
|
2015-12-28 06:14:35 +01:00
|
|
|
To be used for JSON serialization.
|
|
|
|
Ensures: state == State.from_dict(state.as_dict())
|
|
|
|
"""
|
2019-07-31 21:25:30 +02:00
|
|
|
return {
|
|
|
|
"entity_id": self.entity_id,
|
|
|
|
"state": self.state,
|
|
|
|
"attributes": dict(self.attributes),
|
|
|
|
"last_changed": self.last_changed,
|
|
|
|
"last_updated": self.last_updated,
|
|
|
|
"context": self.context.as_dict(),
|
|
|
|
}
|
2014-01-20 04:10:40 +01:00
|
|
|
|
2014-04-15 08:48:00 +02:00
|
|
|
@classmethod
|
2018-07-23 10:24:39 +02:00
|
|
|
def from_dict(cls, json_dict: Dict) -> Any:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Initialize a state from a dict.
|
2014-01-20 04:10:40 +01:00
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
Async friendly.
|
|
|
|
|
2015-12-28 06:14:35 +01:00
|
|
|
Ensures: state == State.from_json_dict(state.to_json_dict())
|
|
|
|
"""
|
2019-07-31 21:25:30 +02:00
|
|
|
if not (json_dict and "entity_id" in json_dict and "state" in json_dict):
|
2014-04-15 08:48:00 +02:00
|
|
|
return None
|
2014-01-20 04:10:40 +01:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
last_changed = json_dict.get("last_changed")
|
2014-01-20 04:10:40 +01:00
|
|
|
|
2016-04-16 09:55:35 +02:00
|
|
|
if isinstance(last_changed, str):
|
|
|
|
last_changed = dt_util.parse_datetime(last_changed)
|
2014-04-15 08:48:00 +02:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
last_updated = json_dict.get("last_updated")
|
2015-04-01 08:08:38 +02:00
|
|
|
|
2016-04-16 09:55:35 +02:00
|
|
|
if isinstance(last_updated, str):
|
|
|
|
last_updated = dt_util.parse_datetime(last_updated)
|
2015-04-01 08:08:38 +02:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
context = json_dict.get("context")
|
2018-07-29 02:53:37 +02:00
|
|
|
if context:
|
2019-07-31 21:25:30 +02:00
|
|
|
context = Context(id=context.get("id"), user_id=context.get("user_id"))
|
|
|
|
|
|
|
|
return cls(
|
|
|
|
json_dict["entity_id"],
|
|
|
|
json_dict["state"],
|
|
|
|
json_dict.get("attributes"),
|
|
|
|
last_changed,
|
|
|
|
last_updated,
|
|
|
|
context,
|
|
|
|
)
|
2014-01-20 04:10:40 +01:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def __eq__(self, other: Any) -> bool:
|
2016-03-08 00:06:04 +01:00
|
|
|
"""Return the comparison of the state."""
|
2019-07-31 22:08:31 +02:00
|
|
|
return ( # type: ignore
|
2019-07-31 21:25:30 +02:00
|
|
|
self.__class__ == other.__class__
|
2019-07-31 22:08:31 +02:00
|
|
|
and self.entity_id == other.entity_id
|
2019-07-31 21:25:30 +02:00
|
|
|
and self.state == other.state
|
|
|
|
and self.attributes == other.attributes
|
|
|
|
and self.context == other.context
|
|
|
|
)
|
2014-04-24 09:40:45 +02:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def __repr__(self) -> str:
|
2016-03-08 00:06:04 +01:00
|
|
|
"""Return the representation of the states."""
|
2020-04-12 17:18:09 +02:00
|
|
|
attrs = f"; {util.repr_helper(self.attributes)}" if self.attributes else ""
|
2015-01-02 17:48:20 +01:00
|
|
|
|
2020-04-12 17:18:09 +02:00
|
|
|
return (
|
|
|
|
f"<state {self.entity_id}={self.state}{attrs}"
|
|
|
|
f" @ {dt_util.as_local(self.last_changed).isoformat()}>"
|
2019-07-31 21:25:30 +02:00
|
|
|
)
|
2014-01-20 04:10:40 +01:00
|
|
|
|
|
|
|
|
2018-07-20 10:45:20 +02:00
|
|
|
class StateMachine:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Helper class that tracks the state of different entities."""
|
2013-09-30 09:20:27 +02:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
def __init__(self, bus: EventBus, loop: asyncio.events.AbstractEventLoop) -> None:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Initialize state machine."""
|
2019-09-04 05:36:04 +02:00
|
|
|
self._states: Dict[str, State] = {}
|
2014-04-24 09:40:45 +02:00
|
|
|
self._bus = bus
|
2016-09-13 04:16:14 +02:00
|
|
|
self._loop = loop
|
2013-09-30 09:20:27 +02:00
|
|
|
|
2019-02-05 11:12:09 +01:00
|
|
|
def entity_ids(self, domain_filter: Optional[str] = None) -> List[str]:
|
2016-09-13 04:16:14 +02:00
|
|
|
"""List of entity ids that are being tracked."""
|
|
|
|
future = run_callback_threadsafe(
|
|
|
|
self._loop, self.async_entity_ids, domain_filter
|
|
|
|
)
|
2020-04-17 20:33:58 +02:00
|
|
|
return future.result()
|
2016-09-13 04:16:14 +02:00
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
@callback
|
2019-07-31 21:25:30 +02:00
|
|
|
def async_entity_ids(self, domain_filter: Optional[str] = None) -> List[str]:
|
2016-10-18 04:38:41 +02:00
|
|
|
"""List of entity ids that are being tracked.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2015-07-26 10:45:49 +02:00
|
|
|
if domain_filter is None:
|
2014-11-29 08:19:59 +01:00
|
|
|
return list(self._states.keys())
|
2014-04-15 08:48:00 +02:00
|
|
|
|
2015-07-26 10:45:49 +02:00
|
|
|
domain_filter = domain_filter.lower()
|
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
return [
|
|
|
|
state.entity_id
|
|
|
|
for state in self._states.values()
|
|
|
|
if state.domain == domain_filter
|
|
|
|
]
|
2015-07-26 10:45:49 +02:00
|
|
|
|
2019-02-05 11:12:09 +01:00
|
|
|
def all(self) -> List[State]:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Create a list of all states."""
|
2020-04-17 20:33:58 +02:00
|
|
|
return run_callback_threadsafe(self._loop, self.async_all).result()
|
2016-09-13 04:16:14 +02:00
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
@callback
|
2019-02-05 11:12:09 +01:00
|
|
|
def async_all(self) -> List[State]:
|
2016-09-13 04:16:14 +02:00
|
|
|
"""Create a list of all states.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
|
|
|
return list(self._states.values())
|
2014-04-29 09:30:31 +02:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def get(self, entity_id: str) -> Optional[State]:
|
2016-09-30 21:57:24 +02:00
|
|
|
"""Retrieve state of entity_id or None if not found.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2016-02-10 08:27:01 +01:00
|
|
|
return self._states.get(entity_id.lower())
|
2014-04-15 08:48:00 +02:00
|
|
|
|
2019-05-02 20:18:20 +02:00
|
|
|
def is_state(self, entity_id: str, state: str) -> bool:
|
|
|
|
"""Test if entity exists and is in specified state.
|
2016-09-13 04:16:14 +02:00
|
|
|
|
2016-09-30 21:57:24 +02:00
|
|
|
Async friendly.
|
2016-09-13 04:16:14 +02:00
|
|
|
"""
|
2016-09-30 21:57:24 +02:00
|
|
|
state_obj = self.get(entity_id)
|
2017-04-17 01:36:15 +02:00
|
|
|
return state_obj is not None and state_obj.state == state
|
2013-10-24 01:08:28 +02:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def remove(self, entity_id: str) -> bool:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Remove the state of an entity.
|
2013-11-19 07:45:19 +01:00
|
|
|
|
2015-12-28 06:14:35 +01:00
|
|
|
Returns boolean to indicate if an entity was removed.
|
|
|
|
"""
|
2020-04-17 20:33:58 +02:00
|
|
|
return run_callback_threadsafe(
|
2019-07-31 21:25:30 +02:00
|
|
|
self._loop, self.async_remove, entity_id
|
|
|
|
).result()
|
2016-09-13 04:16:14 +02:00
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
@callback
|
2020-03-24 17:59:17 +01:00
|
|
|
def async_remove(self, entity_id: str, context: Optional[Context] = None) -> bool:
|
2016-09-13 04:16:14 +02:00
|
|
|
"""Remove the state of an entity.
|
|
|
|
|
|
|
|
Returns boolean to indicate if an entity was removed.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2015-02-06 09:17:30 +01:00
|
|
|
entity_id = entity_id.lower()
|
2016-09-13 04:16:14 +02:00
|
|
|
old_state = self._states.pop(entity_id, None)
|
2016-02-14 07:57:40 +01:00
|
|
|
|
2016-09-13 04:16:14 +02:00
|
|
|
if old_state is None:
|
|
|
|
return False
|
2016-02-14 07:57:40 +01:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
self._bus.async_fire(
|
|
|
|
EVENT_STATE_CHANGED,
|
|
|
|
{"entity_id": entity_id, "old_state": old_state, "new_state": None},
|
2020-03-24 17:59:17 +01:00
|
|
|
EventOrigin.local,
|
|
|
|
context=context,
|
2019-07-31 21:25:30 +02:00
|
|
|
)
|
2016-09-13 04:16:14 +02:00
|
|
|
return True
|
2013-11-19 07:45:19 +01:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
def set(
|
|
|
|
self,
|
|
|
|
entity_id: str,
|
2019-09-24 23:20:04 +02:00
|
|
|
new_state: str,
|
2019-07-31 21:25:30 +02:00
|
|
|
attributes: Optional[Dict] = None,
|
|
|
|
force_update: bool = False,
|
|
|
|
context: Optional[Context] = None,
|
|
|
|
) -> None:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Set the state of an entity, add entity if it does not exist.
|
2013-10-24 08:57:08 +02:00
|
|
|
|
2015-01-02 17:48:20 +01:00
|
|
|
Attributes is an optional dict to specify attributes of this state.
|
|
|
|
|
|
|
|
If you just update the attributes and not the state, last changed will
|
|
|
|
not be affected.
|
|
|
|
"""
|
2016-09-13 04:16:14 +02:00
|
|
|
run_callback_threadsafe(
|
|
|
|
self._loop,
|
2019-07-31 21:25:30 +02:00
|
|
|
self.async_set,
|
|
|
|
entity_id,
|
|
|
|
new_state,
|
|
|
|
attributes,
|
|
|
|
force_update,
|
2018-07-29 02:53:37 +02:00
|
|
|
context,
|
2016-09-13 04:16:14 +02:00
|
|
|
).result()
|
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
@callback
|
2019-07-31 21:25:30 +02:00
|
|
|
def async_set(
|
|
|
|
self,
|
|
|
|
entity_id: str,
|
2019-09-24 23:20:04 +02:00
|
|
|
new_state: str,
|
2019-07-31 21:25:30 +02:00
|
|
|
attributes: Optional[Dict] = None,
|
|
|
|
force_update: bool = False,
|
|
|
|
context: Optional[Context] = None,
|
|
|
|
) -> None:
|
2016-09-13 04:16:14 +02:00
|
|
|
"""Set the state of an entity, add entity if it does not exist.
|
|
|
|
|
|
|
|
Attributes is an optional dict to specify attributes of this state.
|
|
|
|
|
|
|
|
If you just update the attributes and not the state, last changed will
|
|
|
|
not be affected.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2015-02-06 09:17:30 +01:00
|
|
|
entity_id = entity_id.lower()
|
2015-01-13 08:31:31 +01:00
|
|
|
new_state = str(new_state)
|
2013-10-24 08:57:08 +02:00
|
|
|
attributes = attributes or {}
|
2016-09-13 04:16:14 +02:00
|
|
|
old_state = self._states.get(entity_id)
|
2018-07-23 10:24:39 +02:00
|
|
|
if old_state is None:
|
|
|
|
same_state = False
|
|
|
|
same_attr = False
|
|
|
|
last_changed = None
|
|
|
|
else:
|
2019-07-31 21:25:30 +02:00
|
|
|
same_state = old_state.state == new_state and not force_update
|
2019-05-02 20:18:20 +02:00
|
|
|
same_attr = old_state.attributes == MappingProxyType(attributes)
|
2018-07-23 10:24:39 +02:00
|
|
|
last_changed = old_state.last_changed if same_state else None
|
2015-01-02 17:48:20 +01:00
|
|
|
|
2016-09-13 04:16:14 +02:00
|
|
|
if same_state and same_attr:
|
|
|
|
return
|
2015-07-26 10:45:49 +02:00
|
|
|
|
2018-07-29 02:53:37 +02:00
|
|
|
if context is None:
|
|
|
|
context = Context()
|
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
state = State(entity_id, new_state, attributes, last_changed, None, context)
|
2016-09-13 04:16:14 +02:00
|
|
|
self._states[entity_id] = state
|
2019-07-31 21:25:30 +02:00
|
|
|
self._bus.async_fire(
|
|
|
|
EVENT_STATE_CHANGED,
|
|
|
|
{"entity_id": entity_id, "old_state": old_state, "new_state": state},
|
|
|
|
EventOrigin.local,
|
|
|
|
context,
|
|
|
|
)
|
2014-04-24 09:40:45 +02:00
|
|
|
|
|
|
|
|
2018-07-20 10:45:20 +02:00
|
|
|
class Service:
|
2017-04-30 07:04:49 +02:00
|
|
|
"""Representation of a callable service."""
|
2015-09-27 08:17:04 +02:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
__slots__ = ["func", "schema", "is_callback", "is_coroutinefunction"]
|
2015-09-27 08:17:04 +02:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
func: Callable,
|
|
|
|
schema: Optional[vol.Schema],
|
|
|
|
context: Optional[Context] = None,
|
|
|
|
) -> None:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Initialize a service."""
|
2015-09-27 08:17:04 +02:00
|
|
|
self.func = func
|
2016-03-31 20:36:59 +02:00
|
|
|
self.schema = schema
|
2019-05-07 18:39:42 +02:00
|
|
|
# Properly detect wrapped functions
|
2019-07-07 03:58:33 +02:00
|
|
|
while isinstance(func, functools.partial):
|
|
|
|
func = func.func
|
2016-10-05 05:44:32 +02:00
|
|
|
self.is_callback = is_callback(func)
|
|
|
|
self.is_coroutinefunction = asyncio.iscoroutinefunction(func)
|
2015-09-27 08:17:04 +02:00
|
|
|
|
|
|
|
|
2018-07-20 10:45:20 +02:00
|
|
|
class ServiceCall:
|
2017-04-30 07:04:49 +02:00
|
|
|
"""Representation of a call to a service."""
|
2014-04-24 09:40:45 +02:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
__slots__ = ["domain", "service", "data", "context"]
|
2014-04-24 09:40:45 +02:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
domain: str,
|
|
|
|
service: str,
|
|
|
|
data: Optional[Dict] = None,
|
|
|
|
context: Optional[Context] = None,
|
|
|
|
) -> None:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Initialize a service call."""
|
2016-08-10 04:41:45 +02:00
|
|
|
self.domain = domain.lower()
|
|
|
|
self.service = service.lower()
|
2016-09-30 21:57:24 +02:00
|
|
|
self.data = MappingProxyType(data or {})
|
2018-07-29 02:53:37 +02:00
|
|
|
self.context = context or Context()
|
2014-04-24 09:40:45 +02:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def __repr__(self) -> str:
|
2018-01-27 20:58:27 +01:00
|
|
|
"""Return the representation of the service."""
|
2014-04-24 09:40:45 +02:00
|
|
|
if self.data:
|
2020-04-12 17:18:09 +02:00
|
|
|
return (
|
|
|
|
f"<ServiceCall {self.domain}.{self.service} "
|
|
|
|
f"(c:{self.context.id}): {util.repr_helper(self.data)}>"
|
2019-07-31 21:25:30 +02:00
|
|
|
)
|
2017-07-06 05:02:16 +02:00
|
|
|
|
2020-01-03 14:47:06 +01:00
|
|
|
return f"<ServiceCall {self.domain}.{self.service} (c:{self.context.id})>"
|
2014-04-24 09:40:45 +02:00
|
|
|
|
|
|
|
|
2018-07-20 10:45:20 +02:00
|
|
|
class ServiceRegistry:
|
2017-04-30 07:04:49 +02:00
|
|
|
"""Offer the services over the eventbus."""
|
2014-04-24 09:40:45 +02:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def __init__(self, hass: HomeAssistant) -> None:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Initialize a service registry."""
|
2019-09-04 05:36:04 +02:00
|
|
|
self._services: Dict[str, Dict[str, Service]] = {}
|
2016-11-24 23:02:39 +01:00
|
|
|
self._hass = hass
|
2014-04-24 09:40:45 +02:00
|
|
|
|
|
|
|
@property
|
2018-07-23 10:24:39 +02:00
|
|
|
def services(self) -> Dict[str, Dict[str, Service]]:
|
2017-05-02 22:47:20 +02:00
|
|
|
"""Return dictionary with per domain a list of available services."""
|
2020-04-17 20:33:58 +02:00
|
|
|
return run_callback_threadsafe(self._hass.loop, self.async_services).result()
|
2016-09-13 04:16:14 +02:00
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
@callback
|
2018-07-23 10:24:39 +02:00
|
|
|
def async_services(self) -> Dict[str, Dict[str, Service]]:
|
2017-05-02 22:47:20 +02:00
|
|
|
"""Return dictionary with per domain a list of available services.
|
2016-10-18 04:38:41 +02:00
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2019-07-31 21:25:30 +02:00
|
|
|
return {domain: self._services[domain].copy() for domain in self._services}
|
2014-04-24 09:40:45 +02:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def has_service(self, domain: str, service: str) -> bool:
|
2016-10-18 04:38:41 +02:00
|
|
|
"""Test if specified service exists.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2016-08-10 04:41:45 +02:00
|
|
|
return service.lower() in self._services.get(domain.lower(), [])
|
2014-04-24 09:40:45 +02:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
def register(
|
|
|
|
self,
|
|
|
|
domain: str,
|
|
|
|
service: str,
|
|
|
|
service_func: Callable,
|
|
|
|
schema: Optional[vol.Schema] = None,
|
|
|
|
) -> None:
|
2015-09-27 08:17:04 +02:00
|
|
|
"""
|
|
|
|
Register a service.
|
|
|
|
|
2016-03-31 20:36:59 +02:00
|
|
|
Schema is called to coerce and validate the service data.
|
2015-09-27 08:17:04 +02:00
|
|
|
"""
|
2016-09-13 04:16:14 +02:00
|
|
|
run_callback_threadsafe(
|
2019-07-31 21:25:30 +02:00
|
|
|
self._hass.loop, self.async_register, domain, service, service_func, schema
|
2016-09-13 04:16:14 +02:00
|
|
|
).result()
|
|
|
|
|
2016-10-18 04:38:41 +02:00
|
|
|
@callback
|
2019-07-31 21:25:30 +02:00
|
|
|
def async_register(
|
|
|
|
self,
|
|
|
|
domain: str,
|
|
|
|
service: str,
|
|
|
|
service_func: Callable,
|
|
|
|
schema: Optional[vol.Schema] = None,
|
|
|
|
) -> None:
|
2016-09-13 04:16:14 +02:00
|
|
|
"""
|
|
|
|
Register a service.
|
|
|
|
|
|
|
|
Schema is called to coerce and validate the service data.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2016-08-10 04:41:45 +02:00
|
|
|
domain = domain.lower()
|
|
|
|
service = service.lower()
|
2018-01-07 23:54:16 +01:00
|
|
|
service_obj = Service(service_func, schema)
|
2014-04-24 09:40:45 +02:00
|
|
|
|
2016-09-13 04:16:14 +02:00
|
|
|
if domain in self._services:
|
|
|
|
self._services[domain][service] = service_obj
|
|
|
|
else:
|
|
|
|
self._services[domain] = {service: service_obj}
|
|
|
|
|
2016-11-24 23:02:39 +01:00
|
|
|
self._hass.bus.async_fire(
|
2019-07-31 21:25:30 +02:00
|
|
|
EVENT_SERVICE_REGISTERED, {ATTR_DOMAIN: domain, ATTR_SERVICE: service}
|
2016-09-13 04:16:14 +02:00
|
|
|
)
|
2015-02-14 07:49:56 +01:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def remove(self, domain: str, service: str) -> None:
|
2017-03-08 07:51:34 +01:00
|
|
|
"""Remove a registered service from service handler."""
|
|
|
|
run_callback_threadsafe(
|
2019-07-31 21:25:30 +02:00
|
|
|
self._hass.loop, self.async_remove, domain, service
|
|
|
|
).result()
|
2017-03-08 07:51:34 +01:00
|
|
|
|
|
|
|
@callback
|
2018-07-23 10:24:39 +02:00
|
|
|
def async_remove(self, domain: str, service: str) -> None:
|
2017-03-08 07:51:34 +01:00
|
|
|
"""Remove a registered service from service handler.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
|
|
|
domain = domain.lower()
|
|
|
|
service = service.lower()
|
|
|
|
|
|
|
|
if service not in self._services.get(domain, {}):
|
2020-07-05 23:04:19 +02:00
|
|
|
_LOGGER.warning("Unable to remove unknown service %s/%s", domain, service)
|
2017-03-08 07:51:34 +01:00
|
|
|
return
|
|
|
|
|
|
|
|
self._services[domain].pop(service)
|
|
|
|
|
2019-12-16 19:45:09 +01:00
|
|
|
if not self._services[domain]:
|
|
|
|
self._services.pop(domain)
|
|
|
|
|
2017-03-08 07:51:34 +01:00
|
|
|
self._hass.bus.async_fire(
|
2019-07-31 21:25:30 +02:00
|
|
|
EVENT_SERVICE_REMOVED, {ATTR_DOMAIN: domain, ATTR_SERVICE: service}
|
2017-03-08 07:51:34 +01:00
|
|
|
)
|
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
def call(
|
|
|
|
self,
|
|
|
|
domain: str,
|
|
|
|
service: str,
|
|
|
|
service_data: Optional[Dict] = None,
|
|
|
|
blocking: bool = False,
|
|
|
|
context: Optional[Context] = None,
|
2020-03-12 00:34:50 +01:00
|
|
|
limit: Optional[float] = SERVICE_CALL_LIMIT,
|
2019-07-31 21:25:30 +02:00
|
|
|
) -> Optional[bool]:
|
2014-12-01 03:42:52 +01:00
|
|
|
"""
|
2015-12-28 06:14:35 +01:00
|
|
|
Call a service.
|
|
|
|
|
2020-03-12 00:34:50 +01:00
|
|
|
See description of async_call for details.
|
2014-12-01 03:42:52 +01:00
|
|
|
"""
|
2019-10-01 16:59:06 +02:00
|
|
|
return asyncio.run_coroutine_threadsafe(
|
2020-03-12 00:34:50 +01:00
|
|
|
self.async_call(domain, service, service_data, blocking, context, limit),
|
2019-07-31 21:25:30 +02:00
|
|
|
self._hass.loop,
|
2016-09-13 04:16:14 +02:00
|
|
|
).result()
|
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
async def async_call(
|
|
|
|
self,
|
|
|
|
domain: str,
|
|
|
|
service: str,
|
|
|
|
service_data: Optional[Dict] = None,
|
|
|
|
blocking: bool = False,
|
|
|
|
context: Optional[Context] = None,
|
2020-03-12 00:34:50 +01:00
|
|
|
limit: Optional[float] = SERVICE_CALL_LIMIT,
|
2019-07-31 21:25:30 +02:00
|
|
|
) -> Optional[bool]:
|
2016-09-13 04:16:14 +02:00
|
|
|
"""
|
|
|
|
Call a service.
|
|
|
|
|
2020-03-12 00:34:50 +01:00
|
|
|
Specify blocking=True to wait until service is executed.
|
|
|
|
Waits a maximum of limit, which may be None for no timeout.
|
2016-09-13 04:16:14 +02:00
|
|
|
|
|
|
|
If blocking = True, will return boolean if service executed
|
2020-03-12 00:34:50 +01:00
|
|
|
successfully within limit.
|
2016-09-13 04:16:14 +02:00
|
|
|
|
2020-03-12 00:34:50 +01:00
|
|
|
This method will fire an event to indicate the service has been called.
|
2016-09-13 04:16:14 +02:00
|
|
|
|
|
|
|
Because the service is sent as an event you are not allowed to use
|
|
|
|
the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data.
|
|
|
|
|
|
|
|
This method is a coroutine.
|
|
|
|
"""
|
2018-11-30 21:28:35 +01:00
|
|
|
domain = domain.lower()
|
|
|
|
service = service.lower()
|
2018-07-29 02:53:37 +02:00
|
|
|
context = context or Context()
|
2018-11-30 21:28:35 +01:00
|
|
|
service_data = service_data or {}
|
|
|
|
|
|
|
|
try:
|
|
|
|
handler = self._services[domain][service]
|
|
|
|
except KeyError:
|
|
|
|
raise ServiceNotFound(domain, service) from None
|
|
|
|
|
|
|
|
if handler.schema:
|
2020-06-28 07:19:54 +02:00
|
|
|
try:
|
|
|
|
processed_data = handler.schema(service_data)
|
|
|
|
except vol.Invalid:
|
|
|
|
_LOGGER.debug(
|
|
|
|
"Invalid data for service call %s.%s: %s",
|
|
|
|
domain,
|
|
|
|
service,
|
|
|
|
service_data,
|
|
|
|
)
|
|
|
|
raise
|
2018-12-10 12:58:51 +01:00
|
|
|
else:
|
|
|
|
processed_data = service_data
|
2018-11-30 21:28:35 +01:00
|
|
|
|
2018-12-10 12:58:51 +01:00
|
|
|
service_call = ServiceCall(domain, service, processed_data, context)
|
2018-11-30 21:28:35 +01:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
self._hass.bus.async_fire(
|
|
|
|
EVENT_CALL_SERVICE,
|
|
|
|
{
|
|
|
|
ATTR_DOMAIN: domain.lower(),
|
|
|
|
ATTR_SERVICE: service.lower(),
|
|
|
|
ATTR_SERVICE_DATA: service_data,
|
|
|
|
},
|
|
|
|
context=context,
|
|
|
|
)
|
2014-12-14 07:40:00 +01:00
|
|
|
|
2020-04-05 00:36:33 +02:00
|
|
|
coro = self._execute_service(handler, service_call)
|
2018-07-29 02:53:37 +02:00
|
|
|
if not blocking:
|
2020-04-05 00:36:33 +02:00
|
|
|
self._run_service_in_background(coro, service_call)
|
2018-07-29 02:53:37 +02:00
|
|
|
return None
|
2014-12-14 07:40:00 +01:00
|
|
|
|
2020-04-05 00:36:33 +02:00
|
|
|
task = self._hass.async_create_task(coro)
|
2016-09-30 21:57:24 +02:00
|
|
|
try:
|
2020-04-05 00:36:33 +02:00
|
|
|
await asyncio.wait({task}, timeout=limit)
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
# Task calling us was cancelled, so cancel service call task, and wait for
|
|
|
|
# it to be cancelled, within reason, before leaving.
|
|
|
|
_LOGGER.debug("Service call was cancelled: %s", service_call)
|
|
|
|
task.cancel()
|
|
|
|
await asyncio.wait({task}, timeout=SERVICE_CALL_LIMIT)
|
|
|
|
raise
|
|
|
|
|
|
|
|
if task.cancelled():
|
|
|
|
# Service call task was cancelled some other way, such as during shutdown.
|
|
|
|
_LOGGER.debug("Service was cancelled: %s", service_call)
|
|
|
|
raise asyncio.CancelledError
|
|
|
|
if task.done():
|
|
|
|
# Propagate any exceptions that might have happened during service call.
|
|
|
|
task.result()
|
|
|
|
# Service call completed successfully!
|
2018-11-30 21:28:35 +01:00
|
|
|
return True
|
2020-04-05 00:36:33 +02:00
|
|
|
# Service call task did not complete before timeout expired.
|
|
|
|
# Let it keep running in background.
|
|
|
|
self._run_service_in_background(task, service_call)
|
|
|
|
_LOGGER.debug("Service did not complete before timeout: %s", service_call)
|
|
|
|
return False
|
|
|
|
|
|
|
|
def _run_service_in_background(
|
|
|
|
self, coro_or_task: Union[Coroutine, asyncio.Task], service_call: ServiceCall
|
|
|
|
) -> None:
|
|
|
|
"""Run service call in background, catching and logging any exceptions."""
|
|
|
|
|
|
|
|
async def catch_exceptions() -> None:
|
|
|
|
try:
|
|
|
|
await coro_or_task
|
|
|
|
except Unauthorized:
|
|
|
|
_LOGGER.warning(
|
|
|
|
"Unauthorized service called %s/%s",
|
|
|
|
service_call.domain,
|
|
|
|
service_call.service,
|
|
|
|
)
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
_LOGGER.debug("Service was cancelled: %s", service_call)
|
|
|
|
except Exception: # pylint: disable=broad-except
|
|
|
|
_LOGGER.exception("Error executing service: %s", service_call)
|
|
|
|
|
|
|
|
self._hass.async_create_task(catch_exceptions())
|
2014-12-14 07:40:00 +01:00
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
async def _execute_service(
|
|
|
|
self, handler: Service, service_call: ServiceCall
|
|
|
|
) -> None:
|
2018-11-30 21:28:35 +01:00
|
|
|
"""Execute a service."""
|
2020-02-10 04:47:16 +01:00
|
|
|
if handler.is_coroutinefunction:
|
2018-11-30 21:28:35 +01:00
|
|
|
await handler.func(service_call)
|
2020-02-10 04:47:16 +01:00
|
|
|
elif handler.is_callback:
|
|
|
|
handler.func(service_call)
|
2018-11-30 21:28:35 +01:00
|
|
|
else:
|
|
|
|
await self._hass.async_add_executor_job(handler.func, service_call)
|
|
|
|
|
2013-11-11 01:46:48 +01:00
|
|
|
|
2018-07-20 10:45:20 +02:00
|
|
|
class Config:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Configuration settings for Home Assistant."""
|
2015-03-22 05:10:46 +01:00
|
|
|
|
2019-05-20 20:02:36 +02:00
|
|
|
def __init__(self, hass: HomeAssistant) -> None:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Initialize a new config object."""
|
2019-05-20 20:02:36 +02:00
|
|
|
self.hass = hass
|
|
|
|
|
2019-09-04 05:36:04 +02:00
|
|
|
self.latitude: float = 0
|
|
|
|
self.longitude: float = 0
|
|
|
|
self.elevation: int = 0
|
|
|
|
self.location_name: str = "Home"
|
|
|
|
self.time_zone: datetime.tzinfo = dt_util.UTC
|
|
|
|
self.units: UnitSystem = METRIC_SYSTEM
|
2020-05-08 02:29:47 +02:00
|
|
|
self.internal_url: Optional[str] = None
|
|
|
|
self.external_url: Optional[str] = None
|
2015-03-19 07:02:58 +01:00
|
|
|
|
2019-09-04 05:36:04 +02:00
|
|
|
self.config_source: str = "default"
|
2019-05-16 16:27:53 +02:00
|
|
|
|
2015-09-04 23:50:57 +02:00
|
|
|
# If True, pip install is skipped for requirements on startup
|
2019-09-04 05:36:04 +02:00
|
|
|
self.skip_pip: bool = False
|
2015-09-04 23:50:57 +02:00
|
|
|
|
2015-03-22 05:10:46 +01:00
|
|
|
# List of loaded components
|
2019-10-28 21:36:26 +01:00
|
|
|
self.components: Set[str] = set()
|
2015-03-22 05:10:46 +01:00
|
|
|
|
2019-03-11 03:55:36 +01:00
|
|
|
# API (HTTP) server configuration, see components.http.ApiConfig
|
2019-09-04 05:36:04 +02:00
|
|
|
self.api: Optional[Any] = None
|
2015-03-22 05:10:46 +01:00
|
|
|
|
2015-03-19 07:02:58 +01:00
|
|
|
# Directory that holds the configuration
|
2019-09-04 05:36:04 +02:00
|
|
|
self.config_dir: Optional[str] = None
|
2015-08-30 03:11:24 +02:00
|
|
|
|
2017-06-26 00:10:30 +02:00
|
|
|
# List of allowed external dirs to access
|
2020-07-13 17:43:11 +02:00
|
|
|
self.allowlist_external_dirs: Set[str] = set()
|
2017-06-26 00:10:30 +02:00
|
|
|
|
2020-06-25 02:37:01 +02:00
|
|
|
# List of allowed external URLs that integrations may use
|
|
|
|
self.allowlist_external_urls: Set[str] = set()
|
|
|
|
|
2020-02-18 20:52:38 +01:00
|
|
|
# If Home Assistant is running in safe mode
|
|
|
|
self.safe_mode: bool = False
|
|
|
|
|
2018-07-18 00:28:44 +02:00
|
|
|
def distance(self, lat: float, lon: float) -> Optional[float]:
|
2016-10-18 04:38:41 +02:00
|
|
|
"""Calculate distance from Home Assistant.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2016-07-31 22:24:49 +02:00
|
|
|
return self.units.length(
|
2020-04-12 21:44:56 +02:00
|
|
|
location.distance(self.latitude, self.longitude, lat, lon), LENGTH_METERS
|
2019-07-31 21:25:30 +02:00
|
|
|
)
|
2015-09-20 18:35:03 +02:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def path(self, *path: str) -> str:
|
2017-04-30 07:04:49 +02:00
|
|
|
"""Generate path to the file within the configuration directory.
|
2016-10-18 04:38:41 +02:00
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2016-08-09 05:21:40 +02:00
|
|
|
if self.config_dir is None:
|
|
|
|
raise HomeAssistantError("config_dir is not set")
|
2015-05-11 08:05:02 +02:00
|
|
|
return os.path.join(self.config_dir, *path)
|
2015-03-19 20:27:56 +01:00
|
|
|
|
2020-06-25 02:37:01 +02:00
|
|
|
def is_allowed_external_url(self, url: str) -> bool:
|
|
|
|
"""Check if an external URL is allowed."""
|
|
|
|
parsed_url = f"{str(yarl.URL(url))}/"
|
|
|
|
|
|
|
|
return any(
|
|
|
|
allowed
|
|
|
|
for allowed in self.allowlist_external_urls
|
|
|
|
if parsed_url.startswith(allowed)
|
|
|
|
)
|
|
|
|
|
2017-06-26 00:10:30 +02:00
|
|
|
def is_allowed_path(self, path: str) -> bool:
|
|
|
|
"""Check if the path is valid for access from outside."""
|
2017-08-15 15:41:37 +02:00
|
|
|
assert path is not None
|
|
|
|
|
2018-03-30 04:57:19 +02:00
|
|
|
thepath = pathlib.Path(path)
|
2017-06-26 00:10:30 +02:00
|
|
|
try:
|
2018-03-30 04:57:19 +02:00
|
|
|
# The file path does not have to exist (it's parent should)
|
|
|
|
if thepath.exists():
|
|
|
|
thepath = thepath.resolve()
|
|
|
|
else:
|
|
|
|
thepath = thepath.parent.resolve()
|
2017-06-26 00:10:30 +02:00
|
|
|
except (FileNotFoundError, RuntimeError, PermissionError):
|
|
|
|
return False
|
|
|
|
|
2020-07-13 17:43:11 +02:00
|
|
|
for allowed_path in self.allowlist_external_dirs:
|
2017-06-26 00:10:30 +02:00
|
|
|
try:
|
2020-07-13 17:43:11 +02:00
|
|
|
thepath.relative_to(allowed_path)
|
2017-06-26 00:10:30 +02:00
|
|
|
return True
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def as_dict(self) -> Dict:
|
2019-05-20 20:02:36 +02:00
|
|
|
"""Create a dictionary representation of the configuration.
|
2016-10-18 04:38:41 +02:00
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2018-07-23 10:24:39 +02:00
|
|
|
time_zone = dt_util.UTC.zone
|
2019-07-31 21:25:30 +02:00
|
|
|
if self.time_zone and getattr(self.time_zone, "zone"):
|
|
|
|
time_zone = getattr(self.time_zone, "zone")
|
2015-05-16 08:28:11 +02:00
|
|
|
|
2015-05-02 03:24:32 +02:00
|
|
|
return {
|
2019-07-31 21:25:30 +02:00
|
|
|
"latitude": self.latitude,
|
|
|
|
"longitude": self.longitude,
|
|
|
|
"elevation": self.elevation,
|
|
|
|
"unit_system": self.units.as_dict(),
|
|
|
|
"location_name": self.location_name,
|
|
|
|
"time_zone": time_zone,
|
|
|
|
"components": self.components,
|
|
|
|
"config_dir": self.config_dir,
|
2020-07-13 17:43:11 +02:00
|
|
|
# legacy, backwards compat
|
|
|
|
"whitelist_external_dirs": self.allowlist_external_dirs,
|
|
|
|
"allowlist_external_dirs": self.allowlist_external_dirs,
|
2020-06-25 02:37:01 +02:00
|
|
|
"allowlist_external_urls": self.allowlist_external_urls,
|
2019-07-31 21:25:30 +02:00
|
|
|
"version": __version__,
|
|
|
|
"config_source": self.config_source,
|
2020-02-18 20:52:38 +01:00
|
|
|
"safe_mode": self.safe_mode,
|
2020-06-02 20:54:11 +02:00
|
|
|
"state": self.hass.state.value,
|
2020-05-08 02:29:47 +02:00
|
|
|
"external_url": self.external_url,
|
|
|
|
"internal_url": self.internal_url,
|
2015-05-02 03:24:32 +02:00
|
|
|
}
|
|
|
|
|
2019-05-20 20:02:36 +02:00
|
|
|
def set_time_zone(self, time_zone_str: str) -> None:
|
|
|
|
"""Help to set the time zone."""
|
|
|
|
time_zone = dt_util.get_time_zone(time_zone_str)
|
|
|
|
|
|
|
|
if time_zone:
|
|
|
|
self.time_zone = time_zone
|
|
|
|
dt_util.set_default_time_zone(time_zone)
|
|
|
|
else:
|
2019-08-23 18:53:33 +02:00
|
|
|
raise ValueError(f"Received invalid time zone {time_zone_str}")
|
2019-05-20 20:02:36 +02:00
|
|
|
|
|
|
|
@callback
|
2019-07-31 21:25:30 +02:00
|
|
|
def _update(
|
|
|
|
self,
|
|
|
|
*,
|
|
|
|
source: str,
|
|
|
|
latitude: Optional[float] = None,
|
|
|
|
longitude: Optional[float] = None,
|
|
|
|
elevation: Optional[int] = None,
|
|
|
|
unit_system: Optional[str] = None,
|
|
|
|
location_name: Optional[str] = None,
|
|
|
|
time_zone: Optional[str] = None,
|
2020-05-08 02:29:47 +02:00
|
|
|
external_url: Optional[Union[str, dict]] = _UNDEF,
|
|
|
|
internal_url: Optional[Union[str, dict]] = _UNDEF,
|
2019-07-31 21:25:30 +02:00
|
|
|
) -> None:
|
2019-06-01 08:03:45 +02:00
|
|
|
"""Update the configuration from a dictionary."""
|
2019-05-20 20:02:36 +02:00
|
|
|
self.config_source = source
|
|
|
|
if latitude is not None:
|
|
|
|
self.latitude = latitude
|
|
|
|
if longitude is not None:
|
|
|
|
self.longitude = longitude
|
|
|
|
if elevation is not None:
|
|
|
|
self.elevation = elevation
|
|
|
|
if unit_system is not None:
|
|
|
|
if unit_system == CONF_UNIT_SYSTEM_IMPERIAL:
|
|
|
|
self.units = IMPERIAL_SYSTEM
|
|
|
|
else:
|
|
|
|
self.units = METRIC_SYSTEM
|
|
|
|
if location_name is not None:
|
|
|
|
self.location_name = location_name
|
|
|
|
if time_zone is not None:
|
|
|
|
self.set_time_zone(time_zone)
|
2020-05-08 02:29:47 +02:00
|
|
|
if external_url is not _UNDEF:
|
|
|
|
self.external_url = cast(Optional[str], external_url)
|
|
|
|
if internal_url is not _UNDEF:
|
|
|
|
self.internal_url = cast(Optional[str], internal_url)
|
2019-05-20 20:02:36 +02:00
|
|
|
|
2019-06-01 08:03:45 +02:00
|
|
|
async def async_update(self, **kwargs: Any) -> None:
|
|
|
|
"""Update the configuration from a dictionary."""
|
2019-05-20 20:02:36 +02:00
|
|
|
self._update(source=SOURCE_STORAGE, **kwargs)
|
|
|
|
await self.async_store()
|
2019-07-31 21:25:30 +02:00
|
|
|
self.hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, kwargs)
|
2019-05-20 20:02:36 +02:00
|
|
|
|
|
|
|
async def async_load(self) -> None:
|
|
|
|
"""Load [homeassistant] core config."""
|
|
|
|
store = self.hass.helpers.storage.Store(
|
2019-07-31 21:25:30 +02:00
|
|
|
CORE_STORAGE_VERSION, CORE_STORAGE_KEY, private=True
|
|
|
|
)
|
2019-05-20 20:02:36 +02:00
|
|
|
data = await store.async_load()
|
2020-05-08 02:29:47 +02:00
|
|
|
|
|
|
|
async def migrate_base_url(_: Event) -> None:
|
|
|
|
"""Migrate base_url to internal_url/external_url."""
|
|
|
|
if self.hass.config.api is None:
|
|
|
|
return
|
|
|
|
|
2020-05-08 17:52:32 +02:00
|
|
|
base_url = yarl.URL(self.hass.config.api.deprecated_base_url)
|
2020-05-08 02:29:47 +02:00
|
|
|
|
|
|
|
# Check if this is an internal URL
|
|
|
|
if str(base_url.host).endswith(".local") or (
|
|
|
|
network.is_ip_address(str(base_url.host))
|
|
|
|
and network.is_private(ip_address(base_url.host))
|
|
|
|
):
|
|
|
|
await self.async_update(
|
|
|
|
internal_url=network.normalize_url(str(base_url))
|
|
|
|
)
|
|
|
|
return
|
|
|
|
|
|
|
|
# External, ensure this is not a loopback address
|
|
|
|
if not (
|
|
|
|
network.is_ip_address(str(base_url.host))
|
|
|
|
and network.is_loopback(ip_address(base_url.host))
|
|
|
|
):
|
|
|
|
await self.async_update(
|
|
|
|
external_url=network.normalize_url(str(base_url))
|
|
|
|
)
|
|
|
|
|
|
|
|
if data:
|
2020-05-16 13:31:15 +02:00
|
|
|
# Try to migrate base_url to internal_url/external_url
|
|
|
|
if "external_url" not in data:
|
|
|
|
self.hass.bus.async_listen_once(
|
|
|
|
EVENT_HOMEASSISTANT_START, migrate_base_url
|
|
|
|
)
|
|
|
|
|
|
|
|
self._update(
|
|
|
|
source=SOURCE_STORAGE,
|
|
|
|
latitude=data.get("latitude"),
|
|
|
|
longitude=data.get("longitude"),
|
|
|
|
elevation=data.get("elevation"),
|
|
|
|
unit_system=data.get("unit_system"),
|
|
|
|
location_name=data.get("location_name"),
|
|
|
|
time_zone=data.get("time_zone"),
|
|
|
|
external_url=data.get("external_url", _UNDEF),
|
|
|
|
internal_url=data.get("internal_url", _UNDEF),
|
|
|
|
)
|
2019-05-20 20:02:36 +02:00
|
|
|
|
|
|
|
async def async_store(self) -> None:
|
|
|
|
"""Store [homeassistant] core config."""
|
|
|
|
time_zone = dt_util.UTC.zone
|
2019-07-31 21:25:30 +02:00
|
|
|
if self.time_zone and getattr(self.time_zone, "zone"):
|
|
|
|
time_zone = getattr(self.time_zone, "zone")
|
2019-05-20 20:02:36 +02:00
|
|
|
|
|
|
|
data = {
|
2019-07-31 21:25:30 +02:00
|
|
|
"latitude": self.latitude,
|
|
|
|
"longitude": self.longitude,
|
|
|
|
"elevation": self.elevation,
|
|
|
|
"unit_system": self.units.name,
|
|
|
|
"location_name": self.location_name,
|
|
|
|
"time_zone": time_zone,
|
2020-05-08 02:29:47 +02:00
|
|
|
"external_url": self.external_url,
|
|
|
|
"internal_url": self.internal_url,
|
2019-05-20 20:02:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
store = self.hass.helpers.storage.Store(
|
2019-07-31 21:25:30 +02:00
|
|
|
CORE_STORAGE_VERSION, CORE_STORAGE_KEY, private=True
|
|
|
|
)
|
2019-05-20 20:02:36 +02:00
|
|
|
await store.async_save(data)
|
|
|
|
|
2015-03-19 07:02:58 +01:00
|
|
|
|
2018-07-23 10:24:39 +02:00
|
|
|
def _async_create_timer(hass: HomeAssistant) -> None:
|
2015-12-28 06:14:35 +01:00
|
|
|
"""Create a timer that will start on HOMEASSISTANT_START."""
|
2017-02-10 18:00:17 +01:00
|
|
|
handle = None
|
2020-07-23 06:52:10 +02:00
|
|
|
timer_context = Context()
|
2015-07-26 10:45:49 +02:00
|
|
|
|
2018-09-17 10:10:50 +02:00
|
|
|
def schedule_tick(now: datetime.datetime) -> None:
|
|
|
|
"""Schedule a timer tick when the next second rolls around."""
|
|
|
|
nonlocal handle
|
|
|
|
|
2019-07-31 21:25:30 +02:00
|
|
|
slp_seconds = 1 - (now.microsecond / 10 ** 6)
|
2018-09-17 10:10:50 +02:00
|
|
|
target = monotonic() + slp_seconds
|
|
|
|
handle = hass.loop.call_later(slp_seconds, fire_time_event, target)
|
|
|
|
|
2016-10-05 05:44:32 +02:00
|
|
|
@callback
|
2018-09-17 10:10:50 +02:00
|
|
|
def fire_time_event(target: float) -> None:
|
2017-02-10 18:00:17 +01:00
|
|
|
"""Fire next time event."""
|
2018-09-17 10:10:50 +02:00
|
|
|
now = dt_util.utcnow()
|
2015-07-26 10:45:49 +02:00
|
|
|
|
2020-07-23 06:52:10 +02:00
|
|
|
hass.bus.async_fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}, context=timer_context)
|
2015-07-26 10:45:49 +02:00
|
|
|
|
2018-10-15 11:34:36 +02:00
|
|
|
# If we are more than a second late, a tick was missed
|
|
|
|
late = monotonic() - target
|
|
|
|
if late > 1:
|
2020-07-23 06:52:10 +02:00
|
|
|
hass.bus.async_fire(
|
|
|
|
EVENT_TIMER_OUT_OF_SYNC, {ATTR_SECONDS: late}, context=timer_context
|
|
|
|
)
|
2015-07-26 10:45:49 +02:00
|
|
|
|
2018-09-17 10:10:50 +02:00
|
|
|
schedule_tick(now)
|
2015-07-26 10:45:49 +02:00
|
|
|
|
2017-02-10 18:00:17 +01:00
|
|
|
@callback
|
2018-07-23 10:24:39 +02:00
|
|
|
def stop_timer(_: Event) -> None:
|
2017-02-10 18:00:17 +01:00
|
|
|
"""Stop the timer."""
|
|
|
|
if handle is not None:
|
|
|
|
handle.cancel()
|
2015-07-26 10:45:49 +02:00
|
|
|
|
2017-04-07 06:00:58 +02:00
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_timer)
|
|
|
|
|
|
|
|
_LOGGER.info("Timer:starting")
|
2018-09-17 10:10:50 +02:00
|
|
|
schedule_tick(dt_util.utcnow())
|