2016-01-03 10:32:09 +00:00
|
|
|
"""Helpers that help with state related things."""
|
2016-12-02 05:38:12 +00:00
|
|
|
import asyncio
|
2018-10-28 19:12:52 +00:00
|
|
|
import datetime as dt
|
2016-01-03 19:27:30 +00:00
|
|
|
import json
|
2015-03-16 06:36:42 +00:00
|
|
|
import logging
|
2016-02-19 05:27:50 +00:00
|
|
|
from collections import defaultdict
|
2018-10-28 19:12:52 +00:00
|
|
|
from types import TracebackType
|
|
|
|
from typing import ( # noqa: F401 pylint: disable=unused-import
|
2019-07-31 19:25:30 +00:00
|
|
|
Awaitable,
|
|
|
|
Dict,
|
|
|
|
Iterable,
|
|
|
|
List,
|
|
|
|
Optional,
|
|
|
|
Tuple,
|
|
|
|
Type,
|
|
|
|
Union,
|
|
|
|
)
|
2015-03-16 06:36:42 +00:00
|
|
|
|
2017-10-08 15:17:54 +00:00
|
|
|
from homeassistant.loader import bind_hass
|
2015-04-29 02:12:05 +00:00
|
|
|
import homeassistant.util.dt as dt_util
|
2019-07-31 19:25:30 +00:00
|
|
|
from homeassistant.components.notify import ATTR_MESSAGE, SERVICE_NOTIFY
|
|
|
|
from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON
|
|
|
|
from homeassistant.components.mysensors.switch import ATTR_IR_CODE, SERVICE_SEND_IR_CODE
|
|
|
|
from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION
|
2015-03-17 06:32:18 +00:00
|
|
|
from homeassistant.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_ENTITY_ID,
|
|
|
|
ATTR_OPTION,
|
|
|
|
SERVICE_ALARM_ARM_AWAY,
|
|
|
|
SERVICE_ALARM_ARM_HOME,
|
|
|
|
SERVICE_ALARM_DISARM,
|
|
|
|
SERVICE_ALARM_TRIGGER,
|
|
|
|
SERVICE_LOCK,
|
|
|
|
SERVICE_TURN_OFF,
|
|
|
|
SERVICE_TURN_ON,
|
|
|
|
SERVICE_UNLOCK,
|
2019-02-06 01:25:27 +00:00
|
|
|
SERVICE_OPEN_COVER,
|
2019-07-31 19:25:30 +00:00
|
|
|
SERVICE_CLOSE_COVER,
|
|
|
|
SERVICE_SET_COVER_POSITION,
|
|
|
|
SERVICE_SET_COVER_TILT_POSITION,
|
|
|
|
STATE_ALARM_ARMED_AWAY,
|
|
|
|
STATE_ALARM_ARMED_HOME,
|
|
|
|
STATE_ALARM_DISARMED,
|
|
|
|
STATE_ALARM_TRIGGERED,
|
|
|
|
STATE_CLOSED,
|
|
|
|
STATE_HOME,
|
|
|
|
STATE_LOCKED,
|
|
|
|
STATE_NOT_HOME,
|
|
|
|
STATE_OFF,
|
|
|
|
STATE_ON,
|
|
|
|
STATE_OPEN,
|
|
|
|
STATE_UNKNOWN,
|
|
|
|
STATE_UNLOCKED,
|
|
|
|
SERVICE_SELECT_OPTION,
|
|
|
|
)
|
|
|
|
from homeassistant.core import Context, State, DOMAIN as HASS_DOMAIN
|
2018-03-11 17:01:12 +00:00
|
|
|
from homeassistant.util.async_ import run_coroutine_threadsafe
|
2018-10-28 19:12:52 +00:00
|
|
|
from .typing import HomeAssistantType
|
2015-10-07 04:39:38 +00:00
|
|
|
|
2015-03-16 06:36:42 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
GROUP_DOMAIN = "group"
|
2016-03-06 03:32:28 +00:00
|
|
|
|
|
|
|
# Update this dict of lists when new services are added to HA.
|
|
|
|
# Each item is a service with a list of required attributes.
|
|
|
|
SERVICE_ATTRIBUTES = {
|
|
|
|
SERVICE_NOTIFY: [ATTR_MESSAGE],
|
2016-12-07 13:33:41 +00:00
|
|
|
SERVICE_SEND_IR_CODE: [ATTR_IR_CODE],
|
2017-07-23 13:59:27 +00:00
|
|
|
SERVICE_SELECT_OPTION: [ATTR_OPTION],
|
2018-07-02 09:44:36 +00:00
|
|
|
SERVICE_SET_COVER_POSITION: [ATTR_POSITION],
|
2019-07-31 19:25:30 +00:00
|
|
|
SERVICE_SET_COVER_TILT_POSITION: [ATTR_TILT_POSITION],
|
2016-03-06 03:32:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
# Update this dict when new services are added to HA.
|
|
|
|
# Each item is a service with a corresponding state.
|
|
|
|
SERVICE_TO_STATE = {
|
|
|
|
SERVICE_TURN_ON: STATE_ON,
|
|
|
|
SERVICE_TURN_OFF: STATE_OFF,
|
|
|
|
SERVICE_ALARM_ARM_AWAY: STATE_ALARM_ARMED_AWAY,
|
|
|
|
SERVICE_ALARM_ARM_HOME: STATE_ALARM_ARMED_HOME,
|
|
|
|
SERVICE_ALARM_DISARM: STATE_ALARM_DISARMED,
|
|
|
|
SERVICE_ALARM_TRIGGER: STATE_ALARM_TRIGGERED,
|
|
|
|
SERVICE_LOCK: STATE_LOCKED,
|
|
|
|
SERVICE_UNLOCK: STATE_UNLOCKED,
|
2016-09-13 01:31:44 +00:00
|
|
|
SERVICE_OPEN_COVER: STATE_OPEN,
|
2019-07-31 19:25:30 +00:00
|
|
|
SERVICE_CLOSE_COVER: STATE_CLOSED,
|
2016-03-06 03:32:28 +00:00
|
|
|
}
|
|
|
|
|
2015-03-16 06:36:42 +00:00
|
|
|
|
2018-07-20 08:45:20 +00:00
|
|
|
class AsyncTrackStates:
|
2016-03-06 03:32:28 +00:00
|
|
|
"""
|
|
|
|
Record the time when the with-block is entered.
|
2016-03-07 22:39:52 +00:00
|
|
|
|
2016-03-06 03:32:28 +00:00
|
|
|
Add all states that have changed since the start time to the return list
|
|
|
|
when with-block is exited.
|
2016-10-24 06:48:01 +00:00
|
|
|
|
|
|
|
Must be run within the event loop.
|
2015-03-16 06:36:42 +00:00
|
|
|
"""
|
2016-01-03 10:32:09 +00:00
|
|
|
|
2018-10-28 19:12:52 +00:00
|
|
|
def __init__(self, hass: HomeAssistantType) -> None:
|
2016-01-03 10:32:09 +00:00
|
|
|
"""Initialize a TrackStates block."""
|
2015-03-16 06:36:42 +00:00
|
|
|
self.hass = hass
|
2018-10-28 19:12:52 +00:00
|
|
|
self.states = [] # type: List[State]
|
2015-03-16 06:36:42 +00:00
|
|
|
|
2016-10-30 21:18:53 +00:00
|
|
|
# pylint: disable=attribute-defined-outside-init
|
2018-10-28 19:12:52 +00:00
|
|
|
def __enter__(self) -> List[State]:
|
2016-01-03 10:32:09 +00:00
|
|
|
"""Record time from which to track changes."""
|
2015-04-29 02:12:05 +00:00
|
|
|
self.now = dt_util.utcnow()
|
2015-03-16 06:36:42 +00:00
|
|
|
return self.states
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def __exit__(
|
|
|
|
self,
|
|
|
|
exc_type: Optional[Type[BaseException]],
|
|
|
|
exc_value: Optional[BaseException],
|
|
|
|
traceback: Optional[TracebackType],
|
|
|
|
) -> None:
|
2016-01-03 10:32:09 +00:00
|
|
|
"""Add changes states to changes list."""
|
2019-07-31 19:25:30 +00:00
|
|
|
self.states.extend(get_changed_since(self.hass.states.async_all(), self.now))
|
2015-07-26 08:45:49 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def get_changed_since(
|
|
|
|
states: Iterable[State], utc_point_in_time: dt.datetime
|
|
|
|
) -> List[State]:
|
2016-03-06 03:32:28 +00:00
|
|
|
"""Return list of states that have been changed since utc_point_in_time."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return [state for state in states if state.last_updated >= utc_point_in_time]
|
2015-03-16 06:36:42 +00:00
|
|
|
|
|
|
|
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-07-31 19:25:30 +00:00
|
|
|
def reproduce_state(
|
|
|
|
hass: HomeAssistantType,
|
|
|
|
states: Union[State, Iterable[State]],
|
|
|
|
blocking: bool = False,
|
|
|
|
) -> None:
|
2016-12-02 05:38:12 +00:00
|
|
|
"""Reproduce given state."""
|
2018-10-28 19:12:52 +00:00
|
|
|
return run_coroutine_threadsafe( # type: ignore
|
2019-07-31 19:25:30 +00:00
|
|
|
async_reproduce_state(hass, states, blocking), hass.loop
|
|
|
|
).result()
|
2016-12-02 05:38:12 +00:00
|
|
|
|
|
|
|
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-02-06 01:25:27 +00:00
|
|
|
async def async_reproduce_state(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass: HomeAssistantType,
|
|
|
|
states: Union[State, Iterable[State]],
|
|
|
|
blocking: bool = False,
|
|
|
|
context: Optional[Context] = None,
|
|
|
|
) -> None:
|
2019-02-06 01:25:27 +00:00
|
|
|
"""Reproduce a list of states on multiple domains."""
|
2015-03-16 06:36:42 +00:00
|
|
|
if isinstance(states, State):
|
|
|
|
states = [states]
|
|
|
|
|
2019-02-06 01:25:27 +00:00
|
|
|
to_call = defaultdict(list) # type: Dict[str, List[State]]
|
|
|
|
|
|
|
|
for state in states:
|
|
|
|
to_call[state.domain].append(state)
|
|
|
|
|
|
|
|
async def worker(domain: str, data: List[State]) -> None:
|
|
|
|
component = getattr(hass.components, domain)
|
2019-07-31 19:25:30 +00:00
|
|
|
if hasattr(component, "async_reproduce_states"):
|
|
|
|
await component.async_reproduce_states(data, context=context)
|
2019-02-06 01:25:27 +00:00
|
|
|
else:
|
|
|
|
await async_reproduce_state_legacy(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass, domain, data, blocking=blocking, context=context
|
|
|
|
)
|
2019-02-06 01:25:27 +00:00
|
|
|
|
|
|
|
if to_call:
|
|
|
|
# run all domains in parallel
|
2019-07-31 19:25:30 +00:00
|
|
|
await asyncio.gather(
|
|
|
|
*(worker(domain, data) for domain, data in to_call.items())
|
|
|
|
)
|
2019-02-06 01:25:27 +00:00
|
|
|
|
|
|
|
|
|
|
|
@bind_hass
|
|
|
|
async def async_reproduce_state_legacy(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass: HomeAssistantType,
|
|
|
|
domain: str,
|
|
|
|
states: Iterable[State],
|
|
|
|
blocking: bool = False,
|
|
|
|
context: Optional[Context] = None,
|
|
|
|
) -> None:
|
2019-02-06 01:25:27 +00:00
|
|
|
"""Reproduce given state."""
|
|
|
|
to_call = defaultdict(list) # type: Dict[Tuple[str, str], List[str]]
|
|
|
|
|
|
|
|
if domain == GROUP_DOMAIN:
|
|
|
|
service_domain = HASS_DOMAIN
|
|
|
|
else:
|
|
|
|
service_domain = domain
|
2016-01-03 10:32:09 +00:00
|
|
|
|
2015-03-16 06:36:42 +00:00
|
|
|
for state in states:
|
|
|
|
|
2016-03-06 03:32:28 +00:00
|
|
|
if hass.states.get(state.entity_id) is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.warning(
|
|
|
|
"reproduce_state: Unable to find entity %s", state.entity_id
|
|
|
|
)
|
2015-03-16 06:36:42 +00:00
|
|
|
continue
|
|
|
|
|
2017-03-20 15:54:51 +00:00
|
|
|
domain_services = hass.services.async_services().get(service_domain)
|
|
|
|
|
|
|
|
if not domain_services:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.warning("reproduce_state: Unable to reproduce state %s (1)", state)
|
2017-03-20 15:54:51 +00:00
|
|
|
continue
|
2016-03-06 03:32:28 +00:00
|
|
|
|
|
|
|
service = None
|
|
|
|
for _service in domain_services.keys():
|
2019-07-31 19:25:30 +00:00
|
|
|
if (
|
|
|
|
_service in SERVICE_ATTRIBUTES
|
|
|
|
and all(
|
|
|
|
attr in state.attributes for attr in SERVICE_ATTRIBUTES[_service]
|
|
|
|
)
|
|
|
|
or _service in SERVICE_TO_STATE
|
|
|
|
and SERVICE_TO_STATE[_service] == state.state
|
|
|
|
):
|
2016-03-06 03:32:28 +00:00
|
|
|
service = _service
|
2019-07-31 19:25:30 +00:00
|
|
|
if (
|
|
|
|
_service in SERVICE_TO_STATE
|
|
|
|
and SERVICE_TO_STATE[_service] == state.state
|
|
|
|
):
|
2016-03-06 03:32:28 +00:00
|
|
|
break
|
|
|
|
|
|
|
|
if not service:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.warning("reproduce_state: Unable to reproduce state %s (2)", state)
|
2015-09-24 05:35:08 +00:00
|
|
|
continue
|
2015-03-17 06:32:18 +00:00
|
|
|
|
2016-01-03 10:32:09 +00:00
|
|
|
# We group service calls for entities by service call
|
2016-01-03 19:27:30 +00:00
|
|
|
# json used to create a hashable version of dict with maybe lists in it
|
2019-07-31 19:25:30 +00:00
|
|
|
key = (service, json.dumps(dict(state.attributes), sort_keys=True))
|
2016-01-03 10:32:09 +00:00
|
|
|
to_call[key].append(state.entity_id)
|
2015-03-17 06:32:18 +00:00
|
|
|
|
2019-02-06 01:25:27 +00:00
|
|
|
domain_tasks = [] # type: List[Awaitable[Optional[bool]]]
|
|
|
|
for (service, service_data), entity_ids in to_call.items():
|
2016-01-03 19:27:30 +00:00
|
|
|
data = json.loads(service_data)
|
2016-01-03 10:32:09 +00:00
|
|
|
data[ATTR_ENTITY_ID] = entity_ids
|
2016-12-07 16:37:35 +00:00
|
|
|
|
2019-02-06 01:25:27 +00:00
|
|
|
domain_tasks.append(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.services.async_call(service_domain, service, data, blocking, context)
|
2016-12-07 16:37:35 +00:00
|
|
|
)
|
|
|
|
|
2019-02-06 01:25:27 +00:00
|
|
|
if domain_tasks:
|
2019-05-23 04:09:59 +00:00
|
|
|
await asyncio.wait(domain_tasks)
|
2016-02-11 17:10:34 +00:00
|
|
|
|
|
|
|
|
2018-10-28 19:12:52 +00:00
|
|
|
def state_as_number(state: State) -> float:
|
2016-03-06 03:32:28 +00:00
|
|
|
"""
|
|
|
|
Try to coerce our state to a number.
|
2016-02-11 17:10:34 +00:00
|
|
|
|
|
|
|
Raises ValueError if this is not possible.
|
|
|
|
"""
|
2019-07-31 19:25:30 +00:00
|
|
|
if state.state in (
|
|
|
|
STATE_ON,
|
|
|
|
STATE_LOCKED,
|
|
|
|
STATE_ABOVE_HORIZON,
|
|
|
|
STATE_OPEN,
|
|
|
|
STATE_HOME,
|
|
|
|
):
|
2016-02-11 17:10:34 +00:00
|
|
|
return 1
|
2019-07-31 19:25:30 +00:00
|
|
|
if state.state in (
|
|
|
|
STATE_OFF,
|
|
|
|
STATE_UNLOCKED,
|
|
|
|
STATE_UNKNOWN,
|
|
|
|
STATE_BELOW_HORIZON,
|
|
|
|
STATE_CLOSED,
|
|
|
|
STATE_NOT_HOME,
|
|
|
|
):
|
2016-02-11 17:10:34 +00:00
|
|
|
return 0
|
2016-02-13 08:08:32 +00:00
|
|
|
|
|
|
|
return float(state.state)
|