core/homeassistant/helpers/state.py

265 lines
7.7 KiB
Python
Raw Normal View History

"""Helpers that help with state related things."""
import asyncio
import datetime as dt
import json
2015-03-16 06:36:42 +00:00
import logging
2016-02-19 05:27:50 +00:00
from collections import defaultdict
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
from homeassistant.loader import bind_hass
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,
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
from homeassistant.util.async_ import run_coroutine_threadsafe
from .typing import HomeAssistantType
2015-03-16 06:36:42 +00:00
_LOGGER = logging.getLogger(__name__)
2019-07-31 19:25:30 +00:00
GROUP_DOMAIN = "group"
# 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],
SERVICE_SEND_IR_CODE: [ATTR_IR_CODE],
SERVICE_SELECT_OPTION: [ATTR_OPTION],
Added setting cover tilt position in scene (#15255) ## Description: This feature adds possibly of setting tilt_position in scene for covers. **Related issue (if applicable):** fixes #<home-assistant issue number goes here> **Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here> ## Example entry for `configuration.yaml` (if applicable): ```yaml scene: - name: Close Cover Tilt entities: cover.c_office_north: tilt_position: 0 - name: Open Cover Tilt entities: cover.c_office_north: tilt_position: 100 ``` ## Checklist: - [x] The code change is tested and works locally. - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** If user exposed functionality or configuration variables are added/changed: - [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) If the code communicates with devices, web services, or third-party tools: - [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]). - [ ] New dependencies are only imported inside functions that use them ([example][ex-import]). - [ ] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. - [ ] New files were added to `.coveragerc`. If the code does not interact with devices: - [ ] Tests have been added to verify that the new code works. [ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L14 [ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L54
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],
}
# 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,
SERVICE_OPEN_COVER: STATE_OPEN,
2019-07-31 19:25:30 +00:00
SERVICE_CLOSE_COVER: STATE_CLOSED,
}
2015-03-16 06:36:42 +00:00
class AsyncTrackStates:
"""
Record the time when the with-block is entered.
2016-03-07 22:39:52 +00:00
Add all states that have changed since the start time to the return list
when with-block is exited.
Must be run within the event loop.
2015-03-16 06:36:42 +00:00
"""
def __init__(self, hass: HomeAssistantType) -> None:
"""Initialize a TrackStates block."""
2015-03-16 06:36:42 +00:00
self.hass = hass
self.states = [] # type: List[State]
2015-03-16 06:36:42 +00:00
# pylint: disable=attribute-defined-outside-init
def __enter__(self) -> List[State]:
"""Record time from which to track changes."""
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:
"""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))
2019-07-31 19:25:30 +00:00
def get_changed_since(
states: Iterable[State], utc_point_in_time: dt.datetime
) -> List[State]:
"""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
@bind_hass
2019-07-31 19:25:30 +00:00
def reproduce_state(
hass: HomeAssistantType,
states: Union[State, Iterable[State]],
blocking: bool = False,
) -> None:
"""Reproduce given state."""
return run_coroutine_threadsafe( # type: ignore
2019-07-31 19:25:30 +00:00
async_reproduce_state(hass, states, blocking), hass.loop
).result()
@bind_hass
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:
"""Reproduce a list of states on multiple domains."""
2015-03-16 06:36:42 +00:00
if isinstance(states, State):
states = [states]
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)
else:
await async_reproduce_state_legacy(
2019-07-31 19:25:30 +00:00
hass, domain, data, blocking=blocking, context=context
)
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())
)
@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:
"""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
2015-03-16 06:36:42 +00:00
for state in states:
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
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)
continue
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
):
service = _service
2019-07-31 19:25:30 +00:00
if (
_service in SERVICE_TO_STATE
and SERVICE_TO_STATE[_service] == state.state
):
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
# We group service calls for entities by service call
# 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))
to_call[key].append(state.entity_id)
2015-03-17 06:32:18 +00:00
domain_tasks = [] # type: List[Awaitable[Optional[bool]]]
for (service, service_data), entity_ids in to_call.items():
data = json.loads(service_data)
data[ATTR_ENTITY_ID] = entity_ids
domain_tasks.append(
2019-07-31 19:25:30 +00:00
hass.services.async_call(service_domain, service, data, blocking, context)
)
if domain_tasks:
await asyncio.wait(domain_tasks)
def state_as_number(state: State) -> float:
"""
Try to coerce our state to a number.
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,
):
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,
):
return 0
return float(state.state)