Reproduce states by letting each component opt in on handling state recovery itself (#18700)
* Move group to it's own setup * Let each component to handle restore of state * Move constants for climate into const.py For now import all into __init__.py to keep backword compat * Move media plyaer constants to const.py file For now import all constants into __init__.py to keep backword compatibility * Move media player to it's own file * Move climate to it's own file * Remove ecobee service from common components BREAKING CHANGE * Add tests for climate * Add test for media_player * Make sure we clone timestamps of state * Add tests for groups * Remove old tests for media player, it's handled by other tests * Add tests for calls to component functions * Add docstring for climate const * Add docstring for media_player const * Explicitly import constants in climate * Explicitly import constants in media_player * Add period to climate const * Add period to media_player const * Fix some lint errors in climate * Fix some lint errors in media_player * Fix lint warnings on climate tests * Fix lint warnings on group tests * Fix lint warnings on media_player tests * Fix lint warnings on state tests * Adjust indent for state tests
This commit is contained in:
parent
c76a61ad16
commit
3bb5caabe2
13 changed files with 846 additions and 228 deletions
|
@ -22,25 +22,46 @@ from homeassistant.const import (
|
|||
STATE_ON, STATE_OFF, TEMP_CELSIUS, PRECISION_WHOLE,
|
||||
PRECISION_TENTHS)
|
||||
|
||||
from .const import (
|
||||
ATTR_AUX_HEAT,
|
||||
ATTR_AWAY_MODE,
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
ATTR_FAN_LIST,
|
||||
ATTR_FAN_MODE,
|
||||
ATTR_HOLD_MODE,
|
||||
ATTR_HUMIDITY,
|
||||
ATTR_MAX_HUMIDITY,
|
||||
ATTR_MAX_TEMP,
|
||||
ATTR_MIN_HUMIDITY,
|
||||
ATTR_MIN_TEMP,
|
||||
ATTR_OPERATION_LIST,
|
||||
ATTR_OPERATION_MODE,
|
||||
ATTR_SWING_LIST,
|
||||
ATTR_SWING_MODE,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TARGET_TEMP_STEP,
|
||||
DOMAIN,
|
||||
SERVICE_SET_AUX_HEAT,
|
||||
SERVICE_SET_AWAY_MODE,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
SERVICE_SET_HOLD_MODE,
|
||||
SERVICE_SET_HUMIDITY,
|
||||
SERVICE_SET_OPERATION_MODE,
|
||||
SERVICE_SET_SWING_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
)
|
||||
from .reproduce_state import async_reproduce_states # noqa
|
||||
|
||||
DEFAULT_MIN_TEMP = 7
|
||||
DEFAULT_MAX_TEMP = 35
|
||||
DEFAULT_MIN_HUMITIDY = 30
|
||||
DEFAULT_MAX_HUMIDITY = 99
|
||||
|
||||
DOMAIN = 'climate'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
SERVICE_SET_AWAY_MODE = 'set_away_mode'
|
||||
SERVICE_SET_AUX_HEAT = 'set_aux_heat'
|
||||
SERVICE_SET_TEMPERATURE = 'set_temperature'
|
||||
SERVICE_SET_FAN_MODE = 'set_fan_mode'
|
||||
SERVICE_SET_HOLD_MODE = 'set_hold_mode'
|
||||
SERVICE_SET_OPERATION_MODE = 'set_operation_mode'
|
||||
SERVICE_SET_SWING_MODE = 'set_swing_mode'
|
||||
SERVICE_SET_HUMIDITY = 'set_humidity'
|
||||
|
||||
STATE_HEAT = 'heat'
|
||||
STATE_COOL = 'cool'
|
||||
STATE_IDLE = 'idle'
|
||||
|
@ -64,26 +85,6 @@ SUPPORT_AWAY_MODE = 1024
|
|||
SUPPORT_AUX_HEAT = 2048
|
||||
SUPPORT_ON_OFF = 4096
|
||||
|
||||
ATTR_CURRENT_TEMPERATURE = 'current_temperature'
|
||||
ATTR_MAX_TEMP = 'max_temp'
|
||||
ATTR_MIN_TEMP = 'min_temp'
|
||||
ATTR_TARGET_TEMP_HIGH = 'target_temp_high'
|
||||
ATTR_TARGET_TEMP_LOW = 'target_temp_low'
|
||||
ATTR_TARGET_TEMP_STEP = 'target_temp_step'
|
||||
ATTR_AWAY_MODE = 'away_mode'
|
||||
ATTR_AUX_HEAT = 'aux_heat'
|
||||
ATTR_FAN_MODE = 'fan_mode'
|
||||
ATTR_FAN_LIST = 'fan_list'
|
||||
ATTR_CURRENT_HUMIDITY = 'current_humidity'
|
||||
ATTR_HUMIDITY = 'humidity'
|
||||
ATTR_MAX_HUMIDITY = 'max_humidity'
|
||||
ATTR_MIN_HUMIDITY = 'min_humidity'
|
||||
ATTR_HOLD_MODE = 'hold_mode'
|
||||
ATTR_OPERATION_MODE = 'operation_mode'
|
||||
ATTR_OPERATION_LIST = 'operation_list'
|
||||
ATTR_SWING_MODE = 'swing_mode'
|
||||
ATTR_SWING_LIST = 'swing_list'
|
||||
|
||||
CONVERTIBLE_ATTRIBUTE = [
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
|
|
32
homeassistant/components/climate/const.py
Normal file
32
homeassistant/components/climate/const.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
"""Proides the constants needed for component."""
|
||||
|
||||
ATTR_AUX_HEAT = 'aux_heat'
|
||||
ATTR_AWAY_MODE = 'away_mode'
|
||||
ATTR_CURRENT_HUMIDITY = 'current_humidity'
|
||||
ATTR_CURRENT_TEMPERATURE = 'current_temperature'
|
||||
ATTR_FAN_LIST = 'fan_list'
|
||||
ATTR_FAN_MODE = 'fan_mode'
|
||||
ATTR_HOLD_MODE = 'hold_mode'
|
||||
ATTR_HUMIDITY = 'humidity'
|
||||
ATTR_MAX_HUMIDITY = 'max_humidity'
|
||||
ATTR_MAX_TEMP = 'max_temp'
|
||||
ATTR_MIN_HUMIDITY = 'min_humidity'
|
||||
ATTR_MIN_TEMP = 'min_temp'
|
||||
ATTR_OPERATION_LIST = 'operation_list'
|
||||
ATTR_OPERATION_MODE = 'operation_mode'
|
||||
ATTR_SWING_LIST = 'swing_list'
|
||||
ATTR_SWING_MODE = 'swing_mode'
|
||||
ATTR_TARGET_TEMP_HIGH = 'target_temp_high'
|
||||
ATTR_TARGET_TEMP_LOW = 'target_temp_low'
|
||||
ATTR_TARGET_TEMP_STEP = 'target_temp_step'
|
||||
|
||||
DOMAIN = 'climate'
|
||||
|
||||
SERVICE_SET_AUX_HEAT = 'set_aux_heat'
|
||||
SERVICE_SET_AWAY_MODE = 'set_away_mode'
|
||||
SERVICE_SET_FAN_MODE = 'set_fan_mode'
|
||||
SERVICE_SET_HOLD_MODE = 'set_hold_mode'
|
||||
SERVICE_SET_HUMIDITY = 'set_humidity'
|
||||
SERVICE_SET_OPERATION_MODE = 'set_operation_mode'
|
||||
SERVICE_SET_SWING_MODE = 'set_swing_mode'
|
||||
SERVICE_SET_TEMPERATURE = 'set_temperature'
|
91
homeassistant/components/climate/reproduce_state.py
Normal file
91
homeassistant/components/climate/reproduce_state.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
"""Module that groups code required to handle state restore for component."""
|
||||
import asyncio
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON, STATE_OFF, STATE_ON)
|
||||
from homeassistant.core import Context, State
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .const import (
|
||||
ATTR_AUX_HEAT,
|
||||
ATTR_AWAY_MODE,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_HOLD_MODE,
|
||||
ATTR_OPERATION_MODE,
|
||||
ATTR_SWING_MODE,
|
||||
ATTR_HUMIDITY,
|
||||
SERVICE_SET_AWAY_MODE,
|
||||
SERVICE_SET_AUX_HEAT,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
SERVICE_SET_HOLD_MODE,
|
||||
SERVICE_SET_OPERATION_MODE,
|
||||
SERVICE_SET_SWING_MODE,
|
||||
SERVICE_SET_HUMIDITY,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
async def _async_reproduce_states(hass: HomeAssistantType,
|
||||
state: State,
|
||||
context: Optional[Context] = None) -> None:
|
||||
"""Reproduce component states."""
|
||||
async def call_service(service: str, keys: Iterable):
|
||||
"""Call service with set of attributes given."""
|
||||
data = {}
|
||||
data['entity_id'] = state.entity_id
|
||||
for key in keys:
|
||||
if key in state.attributes:
|
||||
data[key] = state.attributes[key]
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN, service, data,
|
||||
blocking=True, context=context)
|
||||
|
||||
if state.state == STATE_ON:
|
||||
await call_service(SERVICE_TURN_ON, [])
|
||||
elif state.state == STATE_OFF:
|
||||
await call_service(SERVICE_TURN_OFF, [])
|
||||
|
||||
if ATTR_AUX_HEAT in state.attributes:
|
||||
await call_service(SERVICE_SET_AUX_HEAT, [ATTR_AUX_HEAT])
|
||||
|
||||
if ATTR_AWAY_MODE in state.attributes:
|
||||
await call_service(SERVICE_SET_AWAY_MODE, [ATTR_AWAY_MODE])
|
||||
|
||||
if (ATTR_TEMPERATURE in state.attributes) or \
|
||||
(ATTR_TARGET_TEMP_HIGH in state.attributes) or \
|
||||
(ATTR_TARGET_TEMP_LOW in state.attributes):
|
||||
await call_service(SERVICE_SET_TEMPERATURE,
|
||||
[ATTR_TEMPERATURE,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW])
|
||||
|
||||
if ATTR_HOLD_MODE in state.attributes:
|
||||
await call_service(SERVICE_SET_HOLD_MODE,
|
||||
[ATTR_HOLD_MODE])
|
||||
|
||||
if ATTR_OPERATION_MODE in state.attributes:
|
||||
await call_service(SERVICE_SET_OPERATION_MODE,
|
||||
[ATTR_OPERATION_MODE])
|
||||
|
||||
if ATTR_SWING_MODE in state.attributes:
|
||||
await call_service(SERVICE_SET_SWING_MODE,
|
||||
[ATTR_SWING_MODE])
|
||||
|
||||
if ATTR_HUMIDITY in state.attributes:
|
||||
await call_service(SERVICE_SET_HUMIDITY,
|
||||
[ATTR_HUMIDITY])
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_reproduce_states(hass: HomeAssistantType,
|
||||
states: Iterable[State],
|
||||
context: Optional[Context] = None) -> None:
|
||||
"""Reproduce component states."""
|
||||
await asyncio.gather(*[
|
||||
_async_reproduce_states(hass, state, context)
|
||||
for state in states])
|
|
@ -23,6 +23,8 @@ from homeassistant.helpers.event import async_track_state_change
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
|
||||
from .reproduce_state import async_reproduce_states # noqa
|
||||
|
||||
DOMAIN = 'group'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
|
28
homeassistant/components/group/reproduce_state.py
Normal file
28
homeassistant/components/group/reproduce_state.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
"""Module that groups code required to handle state restore for component."""
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from homeassistant.core import Context, State
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_reproduce_states(hass: HomeAssistantType,
|
||||
states: Iterable[State],
|
||||
context: Optional[Context] = None) -> None:
|
||||
"""Reproduce component states."""
|
||||
from . import get_entity_ids
|
||||
from homeassistant.helpers.state import async_reproduce_state
|
||||
states_copy = []
|
||||
for state in states:
|
||||
members = get_entity_ids(hass, state.entity_id)
|
||||
for member in members:
|
||||
states_copy.append(
|
||||
State(member,
|
||||
state.state,
|
||||
state.attributes,
|
||||
last_changed=state.last_changed,
|
||||
last_updated=state.last_updated,
|
||||
context=state.context))
|
||||
await async_reproduce_state(hass, states_copy, blocking=True,
|
||||
context=context)
|
|
@ -36,10 +36,44 @@ from homeassistant.helpers.entity import Entity
|
|||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .const import (
|
||||
ATTR_APP_ID,
|
||||
ATTR_APP_NAME,
|
||||
ATTR_INPUT_SOURCE,
|
||||
ATTR_INPUT_SOURCE_LIST,
|
||||
ATTR_MEDIA_ALBUM_ARTIST,
|
||||
ATTR_MEDIA_ALBUM_NAME,
|
||||
ATTR_MEDIA_ARTIST,
|
||||
ATTR_MEDIA_CHANNEL,
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE,
|
||||
ATTR_MEDIA_DURATION,
|
||||
ATTR_MEDIA_ENQUEUE,
|
||||
ATTR_MEDIA_EPISODE,
|
||||
ATTR_MEDIA_PLAYLIST,
|
||||
ATTR_MEDIA_POSITION,
|
||||
ATTR_MEDIA_POSITION_UPDATED_AT,
|
||||
ATTR_MEDIA_SEASON,
|
||||
ATTR_MEDIA_SEEK_POSITION,
|
||||
ATTR_MEDIA_SERIES_TITLE,
|
||||
ATTR_MEDIA_SHUFFLE,
|
||||
ATTR_MEDIA_TITLE,
|
||||
ATTR_MEDIA_TRACK,
|
||||
ATTR_MEDIA_VOLUME_LEVEL,
|
||||
ATTR_MEDIA_VOLUME_MUTED,
|
||||
ATTR_SOUND_MODE,
|
||||
ATTR_SOUND_MODE_LIST,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_PLAYLIST,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
SERVICE_SELECT_SOUND_MODE,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
)
|
||||
from .reproduce_state import async_reproduce_states # noqa
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_RND = SystemRandom()
|
||||
|
||||
DOMAIN = 'media_player'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
@ -55,38 +89,6 @@ ENTITY_IMAGE_CACHE = {
|
|||
CACHE_MAXSIZE: 16
|
||||
}
|
||||
|
||||
SERVICE_PLAY_MEDIA = 'play_media'
|
||||
SERVICE_SELECT_SOURCE = 'select_source'
|
||||
SERVICE_SELECT_SOUND_MODE = 'select_sound_mode'
|
||||
SERVICE_CLEAR_PLAYLIST = 'clear_playlist'
|
||||
|
||||
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
|
||||
ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
|
||||
ATTR_MEDIA_SEEK_POSITION = 'seek_position'
|
||||
ATTR_MEDIA_CONTENT_ID = 'media_content_id'
|
||||
ATTR_MEDIA_CONTENT_TYPE = 'media_content_type'
|
||||
ATTR_MEDIA_DURATION = 'media_duration'
|
||||
ATTR_MEDIA_POSITION = 'media_position'
|
||||
ATTR_MEDIA_POSITION_UPDATED_AT = 'media_position_updated_at'
|
||||
ATTR_MEDIA_TITLE = 'media_title'
|
||||
ATTR_MEDIA_ARTIST = 'media_artist'
|
||||
ATTR_MEDIA_ALBUM_NAME = 'media_album_name'
|
||||
ATTR_MEDIA_ALBUM_ARTIST = 'media_album_artist'
|
||||
ATTR_MEDIA_TRACK = 'media_track'
|
||||
ATTR_MEDIA_SERIES_TITLE = 'media_series_title'
|
||||
ATTR_MEDIA_SEASON = 'media_season'
|
||||
ATTR_MEDIA_EPISODE = 'media_episode'
|
||||
ATTR_MEDIA_CHANNEL = 'media_channel'
|
||||
ATTR_MEDIA_PLAYLIST = 'media_playlist'
|
||||
ATTR_APP_ID = 'app_id'
|
||||
ATTR_APP_NAME = 'app_name'
|
||||
ATTR_INPUT_SOURCE = 'source'
|
||||
ATTR_INPUT_SOURCE_LIST = 'source_list'
|
||||
ATTR_SOUND_MODE = 'sound_mode'
|
||||
ATTR_SOUND_MODE_LIST = 'sound_mode_list'
|
||||
ATTR_MEDIA_ENQUEUE = 'enqueue'
|
||||
ATTR_MEDIA_SHUFFLE = 'shuffle'
|
||||
|
||||
MEDIA_TYPE_MUSIC = 'music'
|
||||
MEDIA_TYPE_TVSHOW = 'tvshow'
|
||||
MEDIA_TYPE_MOVIE = 'movie'
|
||||
|
|
35
homeassistant/components/media_player/const.py
Normal file
35
homeassistant/components/media_player/const.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
"""Proides the constants needed for component."""
|
||||
|
||||
ATTR_APP_ID = 'app_id'
|
||||
ATTR_APP_NAME = 'app_name'
|
||||
ATTR_INPUT_SOURCE = 'source'
|
||||
ATTR_INPUT_SOURCE_LIST = 'source_list'
|
||||
ATTR_MEDIA_ALBUM_ARTIST = 'media_album_artist'
|
||||
ATTR_MEDIA_ALBUM_NAME = 'media_album_name'
|
||||
ATTR_MEDIA_ARTIST = 'media_artist'
|
||||
ATTR_MEDIA_CHANNEL = 'media_channel'
|
||||
ATTR_MEDIA_CONTENT_ID = 'media_content_id'
|
||||
ATTR_MEDIA_CONTENT_TYPE = 'media_content_type'
|
||||
ATTR_MEDIA_DURATION = 'media_duration'
|
||||
ATTR_MEDIA_ENQUEUE = 'enqueue'
|
||||
ATTR_MEDIA_EPISODE = 'media_episode'
|
||||
ATTR_MEDIA_PLAYLIST = 'media_playlist'
|
||||
ATTR_MEDIA_POSITION = 'media_position'
|
||||
ATTR_MEDIA_POSITION_UPDATED_AT = 'media_position_updated_at'
|
||||
ATTR_MEDIA_SEASON = 'media_season'
|
||||
ATTR_MEDIA_SEEK_POSITION = 'seek_position'
|
||||
ATTR_MEDIA_SERIES_TITLE = 'media_series_title'
|
||||
ATTR_MEDIA_SHUFFLE = 'shuffle'
|
||||
ATTR_MEDIA_TITLE = 'media_title'
|
||||
ATTR_MEDIA_TRACK = 'media_track'
|
||||
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
|
||||
ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
|
||||
ATTR_SOUND_MODE = 'sound_mode'
|
||||
ATTR_SOUND_MODE_LIST = 'sound_mode_list'
|
||||
|
||||
DOMAIN = 'media_player'
|
||||
|
||||
SERVICE_CLEAR_PLAYLIST = 'clear_playlist'
|
||||
SERVICE_PLAY_MEDIA = 'play_media'
|
||||
SERVICE_SELECT_SOUND_MODE = 'select_sound_mode'
|
||||
SERVICE_SELECT_SOURCE = 'select_source'
|
87
homeassistant/components/media_player/reproduce_state.py
Normal file
87
homeassistant/components/media_player/reproduce_state.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
"""Module that groups code required to handle state restore for component."""
|
||||
import asyncio
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from homeassistant.const import (
|
||||
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_SEEK,
|
||||
SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_SET, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED,
|
||||
STATE_PLAYING)
|
||||
from homeassistant.core import Context, State
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .const import (
|
||||
ATTR_MEDIA_VOLUME_LEVEL,
|
||||
ATTR_MEDIA_VOLUME_MUTED,
|
||||
ATTR_MEDIA_SEEK_POSITION,
|
||||
ATTR_INPUT_SOURCE,
|
||||
ATTR_SOUND_MODE,
|
||||
ATTR_MEDIA_CONTENT_TYPE,
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_ENQUEUE,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
SERVICE_SELECT_SOUND_MODE,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
async def _async_reproduce_states(hass: HomeAssistantType,
|
||||
state: State,
|
||||
context: Optional[Context] = None) -> None:
|
||||
"""Reproduce component states."""
|
||||
async def call_service(service: str, keys: Iterable):
|
||||
"""Call service with set of attributes given."""
|
||||
data = {}
|
||||
data['entity_id'] = state.entity_id
|
||||
for key in keys:
|
||||
if key in state.attributes:
|
||||
data[key] = state.attributes[key]
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN, service, data,
|
||||
blocking=True, context=context)
|
||||
|
||||
if state.state == STATE_ON:
|
||||
await call_service(SERVICE_TURN_ON, [])
|
||||
elif state.state == STATE_OFF:
|
||||
await call_service(SERVICE_TURN_OFF, [])
|
||||
elif state.state == STATE_PLAYING:
|
||||
await call_service(SERVICE_MEDIA_PLAY, [])
|
||||
elif state.state == STATE_IDLE:
|
||||
await call_service(SERVICE_MEDIA_STOP, [])
|
||||
elif state.state == STATE_PAUSED:
|
||||
await call_service(SERVICE_MEDIA_PAUSE, [])
|
||||
|
||||
if ATTR_MEDIA_VOLUME_LEVEL in state.attributes:
|
||||
await call_service(SERVICE_VOLUME_SET, [ATTR_MEDIA_VOLUME_LEVEL])
|
||||
|
||||
if ATTR_MEDIA_VOLUME_MUTED in state.attributes:
|
||||
await call_service(SERVICE_VOLUME_MUTE, [ATTR_MEDIA_VOLUME_MUTED])
|
||||
|
||||
if ATTR_MEDIA_SEEK_POSITION in state.attributes:
|
||||
await call_service(SERVICE_MEDIA_SEEK, [ATTR_MEDIA_SEEK_POSITION])
|
||||
|
||||
if ATTR_INPUT_SOURCE in state.attributes:
|
||||
await call_service(SERVICE_SELECT_SOURCE, [ATTR_INPUT_SOURCE])
|
||||
|
||||
if ATTR_SOUND_MODE in state.attributes:
|
||||
await call_service(SERVICE_SELECT_SOUND_MODE, [ATTR_SOUND_MODE])
|
||||
|
||||
if (ATTR_MEDIA_CONTENT_TYPE in state.attributes) and \
|
||||
(ATTR_MEDIA_CONTENT_ID in state.attributes):
|
||||
await call_service(SERVICE_PLAY_MEDIA,
|
||||
[ATTR_MEDIA_CONTENT_TYPE,
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_ENQUEUE])
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_reproduce_states(hass: HomeAssistantType,
|
||||
states: Iterable[State],
|
||||
context: Optional[Context] = None) -> None:
|
||||
"""Reproduce component states."""
|
||||
await asyncio.gather(*[
|
||||
_async_reproduce_states(hass, state, context)
|
||||
for state in states])
|
|
@ -10,41 +10,27 @@ from typing import ( # noqa: F401 pylint: disable=unused-import
|
|||
|
||||
from homeassistant.loader import bind_hass
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_SEEK_POSITION,
|
||||
ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, SERVICE_PLAY_MEDIA,
|
||||
SERVICE_SELECT_SOURCE, ATTR_INPUT_SOURCE)
|
||||
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.climate import (
|
||||
ATTR_AUX_HEAT, ATTR_AWAY_MODE, ATTR_FAN_MODE, ATTR_HOLD_MODE,
|
||||
ATTR_HUMIDITY, ATTR_OPERATION_MODE, ATTR_SWING_MODE,
|
||||
SERVICE_SET_AUX_HEAT, SERVICE_SET_AWAY_MODE, SERVICE_SET_HOLD_MODE,
|
||||
SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_OPERATION_MODE,
|
||||
SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, STATE_HEAT, STATE_COOL,
|
||||
STATE_IDLE)
|
||||
from homeassistant.components.ecobee.climate import (
|
||||
ATTR_FAN_MIN_ON_TIME, SERVICE_SET_FAN_MIN_ON_TIME,
|
||||
ATTR_RESUME_ALL, SERVICE_RESUME_PROGRAM)
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION, ATTR_TILT_POSITION)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_OPTION, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY,
|
||||
ATTR_ENTITY_ID, ATTR_OPTION, SERVICE_ALARM_ARM_AWAY,
|
||||
SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER,
|
||||
SERVICE_LOCK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP,
|
||||
SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK,
|
||||
SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_OPEN_COVER,
|
||||
SERVICE_LOCK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK,
|
||||
SERVICE_OPEN_COVER,
|
||||
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_PAUSED, STATE_PLAYING, STATE_UNKNOWN,
|
||||
STATE_ON, STATE_OPEN, STATE_UNKNOWN,
|
||||
STATE_UNLOCKED, SERVICE_SELECT_OPTION)
|
||||
from homeassistant.core import State, DOMAIN as HASS_DOMAIN
|
||||
from homeassistant.core import (
|
||||
Context, State, DOMAIN as HASS_DOMAIN)
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
from .typing import HomeAssistantType
|
||||
|
||||
|
@ -55,22 +41,7 @@ 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_PLAY_MEDIA: [ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID],
|
||||
SERVICE_MEDIA_SEEK: [ATTR_MEDIA_SEEK_POSITION],
|
||||
SERVICE_VOLUME_MUTE: [ATTR_MEDIA_VOLUME_MUTED],
|
||||
SERVICE_VOLUME_SET: [ATTR_MEDIA_VOLUME_LEVEL],
|
||||
SERVICE_NOTIFY: [ATTR_MESSAGE],
|
||||
SERVICE_SET_AWAY_MODE: [ATTR_AWAY_MODE],
|
||||
SERVICE_SET_FAN_MODE: [ATTR_FAN_MODE],
|
||||
SERVICE_SET_FAN_MIN_ON_TIME: [ATTR_FAN_MIN_ON_TIME],
|
||||
SERVICE_RESUME_PROGRAM: [ATTR_RESUME_ALL],
|
||||
SERVICE_SET_TEMPERATURE: [ATTR_TEMPERATURE],
|
||||
SERVICE_SET_HUMIDITY: [ATTR_HUMIDITY],
|
||||
SERVICE_SET_SWING_MODE: [ATTR_SWING_MODE],
|
||||
SERVICE_SET_HOLD_MODE: [ATTR_HOLD_MODE],
|
||||
SERVICE_SET_OPERATION_MODE: [ATTR_OPERATION_MODE],
|
||||
SERVICE_SET_AUX_HEAT: [ATTR_AUX_HEAT],
|
||||
SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE],
|
||||
SERVICE_SEND_IR_CODE: [ATTR_IR_CODE],
|
||||
SERVICE_SELECT_OPTION: [ATTR_OPTION],
|
||||
SERVICE_SET_COVER_POSITION: [ATTR_POSITION],
|
||||
|
@ -82,9 +53,6 @@ SERVICE_ATTRIBUTES = {
|
|||
SERVICE_TO_STATE = {
|
||||
SERVICE_TURN_ON: STATE_ON,
|
||||
SERVICE_TURN_OFF: STATE_OFF,
|
||||
SERVICE_MEDIA_PLAY: STATE_PLAYING,
|
||||
SERVICE_MEDIA_PAUSE: STATE_PAUSED,
|
||||
SERVICE_MEDIA_STOP: STATE_IDLE,
|
||||
SERVICE_ALARM_ARM_AWAY: STATE_ALARM_ARMED_AWAY,
|
||||
SERVICE_ALARM_ARM_HOME: STATE_ALARM_ARMED_HOME,
|
||||
SERVICE_ALARM_DISARM: STATE_ALARM_DISARMED,
|
||||
|
@ -142,14 +110,56 @@ def reproduce_state(hass: HomeAssistantType,
|
|||
|
||||
|
||||
@bind_hass
|
||||
async def async_reproduce_state(hass: HomeAssistantType,
|
||||
async def async_reproduce_state(
|
||||
hass: HomeAssistantType,
|
||||
states: Union[State, Iterable[State]],
|
||||
blocking: bool = False) -> None:
|
||||
"""Reproduce given state."""
|
||||
blocking: bool = False,
|
||||
context: Optional[Context] = None) -> None:
|
||||
"""Reproduce a list of states on multiple domains."""
|
||||
if isinstance(states, State):
|
||||
states = [states]
|
||||
|
||||
to_call = defaultdict(list) # type: Dict[Tuple[str, str, str], List[str]]
|
||||
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)
|
||||
if hasattr(component, 'async_reproduce_states'):
|
||||
await component.async_reproduce_states(
|
||||
data,
|
||||
context=context)
|
||||
else:
|
||||
await async_reproduce_state_legacy(
|
||||
hass,
|
||||
domain,
|
||||
data,
|
||||
blocking=blocking,
|
||||
context=context)
|
||||
|
||||
if to_call:
|
||||
# run all domains in parallel
|
||||
await asyncio.gather(*[
|
||||
worker(domain, data)
|
||||
for domain, data in to_call.items()
|
||||
])
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_reproduce_state_legacy(
|
||||
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
|
||||
|
||||
for state in states:
|
||||
|
||||
|
@ -158,11 +168,6 @@ async def async_reproduce_state(hass: HomeAssistantType,
|
|||
state.entity_id)
|
||||
continue
|
||||
|
||||
if state.domain == GROUP_DOMAIN:
|
||||
service_domain = HASS_DOMAIN
|
||||
else:
|
||||
service_domain = state.domain
|
||||
|
||||
domain_services = hass.services.async_services().get(service_domain)
|
||||
|
||||
if not domain_services:
|
||||
|
@ -189,32 +194,22 @@ async def async_reproduce_state(hass: HomeAssistantType,
|
|||
|
||||
# We group service calls for entities by service call
|
||||
# json used to create a hashable version of dict with maybe lists in it
|
||||
key = (service_domain, service,
|
||||
key = (service,
|
||||
json.dumps(dict(state.attributes), sort_keys=True))
|
||||
to_call[key].append(state.entity_id)
|
||||
|
||||
domain_tasks = {} # type: Dict[str, List[Awaitable[Optional[bool]]]]
|
||||
for (service_domain, service, service_data), entity_ids in to_call.items():
|
||||
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
|
||||
|
||||
if service_domain not in domain_tasks:
|
||||
domain_tasks[service_domain] = []
|
||||
|
||||
domain_tasks[service_domain].append(
|
||||
hass.services.async_call(service_domain, service, data, blocking)
|
||||
domain_tasks.append(
|
||||
hass.services.async_call(service_domain, service, data, blocking,
|
||||
context)
|
||||
)
|
||||
|
||||
async def async_handle_service_calls(
|
||||
coro_list: Iterable[Awaitable]) -> None:
|
||||
"""Handle service calls by domain sequence."""
|
||||
for coro in coro_list:
|
||||
await coro
|
||||
|
||||
execute_tasks = [async_handle_service_calls(coro_list)
|
||||
for coro_list in domain_tasks.values()]
|
||||
if execute_tasks:
|
||||
await asyncio.wait(execute_tasks, loop=hass.loop)
|
||||
if domain_tasks:
|
||||
await asyncio.wait(domain_tasks, loop=hass.loop)
|
||||
|
||||
|
||||
def state_as_number(state: State) -> float:
|
||||
|
@ -223,6 +218,9 @@ def state_as_number(state: State) -> float:
|
|||
|
||||
Raises ValueError if this is not possible.
|
||||
"""
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE)
|
||||
|
||||
if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON,
|
||||
STATE_OPEN, STATE_HOME, STATE_HEAT, STATE_COOL):
|
||||
return 1
|
||||
|
|
162
tests/components/climate/test_reproduce_state.py
Normal file
162
tests/components/climate/test_reproduce_state.py
Normal file
|
@ -0,0 +1,162 @@
|
|||
"""The tests for reproduction of state."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.climate import STATE_HEAT, async_reproduce_states
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_AUX_HEAT, ATTR_AWAY_MODE, ATTR_HOLD_MODE, ATTR_HUMIDITY,
|
||||
ATTR_OPERATION_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW, DOMAIN, SERVICE_SET_AUX_HEAT, SERVICE_SET_AWAY_MODE,
|
||||
SERVICE_SET_HOLD_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_OPERATION_MODE,
|
||||
SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON)
|
||||
from homeassistant.core import Context, State
|
||||
|
||||
from tests.common import async_mock_service
|
||||
|
||||
ENTITY_1 = 'climate.test1'
|
||||
ENTITY_2 = 'climate.test2'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'service,state', [
|
||||
(SERVICE_TURN_ON, STATE_ON),
|
||||
(SERVICE_TURN_OFF, STATE_OFF),
|
||||
])
|
||||
async def test_state(hass, service, state):
|
||||
"""Test that we can turn a state into a service call."""
|
||||
calls_1 = async_mock_service(hass, DOMAIN, service)
|
||||
|
||||
await async_reproduce_states(hass, [
|
||||
State(ENTITY_1, state)
|
||||
])
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls_1) == 1
|
||||
assert calls_1[0].data == {'entity_id': ENTITY_1}
|
||||
|
||||
|
||||
async def test_turn_on_with_mode(hass):
|
||||
"""Test that state with additional attributes call multiple services."""
|
||||
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
|
||||
calls_2 = async_mock_service(hass, DOMAIN, SERVICE_SET_OPERATION_MODE)
|
||||
|
||||
await async_reproduce_states(hass, [
|
||||
State(ENTITY_1, 'on',
|
||||
{ATTR_OPERATION_MODE: STATE_HEAT})
|
||||
])
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls_1) == 1
|
||||
assert calls_1[0].data == {'entity_id': ENTITY_1}
|
||||
|
||||
assert len(calls_2) == 1
|
||||
assert calls_2[0].data == {'entity_id': ENTITY_1,
|
||||
ATTR_OPERATION_MODE: STATE_HEAT}
|
||||
|
||||
|
||||
async def test_multiple_same_state(hass):
|
||||
"""Test that multiple states with same state gets calls."""
|
||||
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
|
||||
|
||||
await async_reproduce_states(hass, [
|
||||
State(ENTITY_1, 'on'),
|
||||
State(ENTITY_2, 'on'),
|
||||
])
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls_1) == 2
|
||||
# order is not guaranteed
|
||||
assert any(call.data == {'entity_id': ENTITY_1} for call in calls_1)
|
||||
assert any(call.data == {'entity_id': ENTITY_2} for call in calls_1)
|
||||
|
||||
|
||||
async def test_multiple_different_state(hass):
|
||||
"""Test that multiple states with different state gets calls."""
|
||||
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
|
||||
calls_2 = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF)
|
||||
|
||||
await async_reproduce_states(hass, [
|
||||
State(ENTITY_1, 'on'),
|
||||
State(ENTITY_2, 'off'),
|
||||
])
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls_1) == 1
|
||||
assert calls_1[0].data == {'entity_id': ENTITY_1}
|
||||
assert len(calls_2) == 1
|
||||
assert calls_2[0].data == {'entity_id': ENTITY_2}
|
||||
|
||||
|
||||
async def test_state_with_context(hass):
|
||||
"""Test that context is forwarded."""
|
||||
calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
|
||||
|
||||
context = Context()
|
||||
|
||||
await async_reproduce_states(hass, [
|
||||
State(ENTITY_1, 'on')
|
||||
], context)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {'entity_id': ENTITY_1}
|
||||
assert calls[0].context == context
|
||||
|
||||
|
||||
async def test_attribute_no_state(hass):
|
||||
"""Test that no state service call is made with none state."""
|
||||
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
|
||||
calls_2 = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF)
|
||||
calls_3 = async_mock_service(hass, DOMAIN, SERVICE_SET_OPERATION_MODE)
|
||||
|
||||
value = "dummy"
|
||||
|
||||
await async_reproduce_states(hass, [
|
||||
State(ENTITY_1, None,
|
||||
{ATTR_OPERATION_MODE: value})
|
||||
])
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls_1) == 0
|
||||
assert len(calls_2) == 0
|
||||
assert len(calls_3) == 1
|
||||
assert calls_3[0].data == {'entity_id': ENTITY_1,
|
||||
ATTR_OPERATION_MODE: value}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'service,attribute', [
|
||||
(SERVICE_SET_OPERATION_MODE, ATTR_OPERATION_MODE),
|
||||
(SERVICE_SET_AUX_HEAT, ATTR_AUX_HEAT),
|
||||
(SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE),
|
||||
(SERVICE_SET_HOLD_MODE, ATTR_HOLD_MODE),
|
||||
(SERVICE_SET_SWING_MODE, ATTR_SWING_MODE),
|
||||
(SERVICE_SET_HUMIDITY, ATTR_HUMIDITY),
|
||||
(SERVICE_SET_TEMPERATURE, ATTR_TEMPERATURE),
|
||||
(SERVICE_SET_TEMPERATURE, ATTR_TARGET_TEMP_HIGH),
|
||||
(SERVICE_SET_TEMPERATURE, ATTR_TARGET_TEMP_LOW),
|
||||
])
|
||||
async def test_attribute(hass, service, attribute):
|
||||
"""Test that service call is made for each attribute."""
|
||||
calls_1 = async_mock_service(hass, DOMAIN, service)
|
||||
|
||||
value = "dummy"
|
||||
|
||||
await async_reproduce_states(hass, [
|
||||
State(ENTITY_1, None,
|
||||
{attribute: value})
|
||||
])
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls_1) == 1
|
||||
assert calls_1[0].data == {'entity_id': ENTITY_1,
|
||||
attribute: value}
|
45
tests/components/group/test_reproduce_state.py
Normal file
45
tests/components/group/test_reproduce_state.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
"""The tests for reproduction of state."""
|
||||
|
||||
from asyncio import Future
|
||||
from unittest.mock import patch
|
||||
from homeassistant.components.group import async_reproduce_states
|
||||
from homeassistant.core import Context, State
|
||||
|
||||
|
||||
async def test_reproduce_group(hass):
|
||||
"""Test reproduce_state with group."""
|
||||
context = Context()
|
||||
|
||||
def clone_state(state, entity_id):
|
||||
"""Return a cloned state with different entity_id."""
|
||||
return State(entity_id,
|
||||
state.state,
|
||||
state.attributes,
|
||||
last_changed=state.last_changed,
|
||||
last_updated=state.last_updated,
|
||||
context=state.context)
|
||||
|
||||
with patch('homeassistant.helpers.state.async_reproduce_state') as fun:
|
||||
fun.return_value = Future()
|
||||
fun.return_value.set_result(None)
|
||||
|
||||
hass.states.async_set('group.test', 'off', {
|
||||
'entity_id': ['light.test1', 'light.test2', 'switch.test1']})
|
||||
hass.states.async_set('light.test1', 'off')
|
||||
hass.states.async_set('light.test2', 'off')
|
||||
hass.states.async_set('switch.test1', 'off')
|
||||
|
||||
state = State('group.test', 'on')
|
||||
|
||||
await async_reproduce_states(
|
||||
hass,
|
||||
[state],
|
||||
context)
|
||||
|
||||
fun.assert_called_once_with(
|
||||
hass,
|
||||
[clone_state(state, 'light.test1'),
|
||||
clone_state(state, 'light.test2'),
|
||||
clone_state(state, 'switch.test1')],
|
||||
blocking=True,
|
||||
context=context)
|
199
tests/components/media_player/test_reproduce_state.py
Normal file
199
tests/components/media_player/test_reproduce_state.py
Normal file
|
@ -0,0 +1,199 @@
|
|||
"""The tests for reproduction of state."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.media_player import async_reproduce_states
|
||||
from homeassistant.components.media_player.const import (
|
||||
ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE,
|
||||
ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_VOLUME_LEVEL,
|
||||
ATTR_MEDIA_VOLUME_MUTED, ATTR_SOUND_MODE, DOMAIN, SERVICE_PLAY_MEDIA,
|
||||
SERVICE_SELECT_SOUND_MODE, SERVICE_SELECT_SOURCE)
|
||||
from homeassistant.const import (
|
||||
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_SEEK,
|
||||
SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_SET, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED,
|
||||
STATE_PLAYING)
|
||||
from homeassistant.core import Context, State
|
||||
|
||||
from tests.common import async_mock_service
|
||||
|
||||
ENTITY_1 = 'media_player.test1'
|
||||
ENTITY_2 = 'media_player.test2'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'service,state', [
|
||||
(SERVICE_TURN_ON, STATE_ON),
|
||||
(SERVICE_TURN_OFF, STATE_OFF),
|
||||
(SERVICE_MEDIA_PLAY, STATE_PLAYING),
|
||||
(SERVICE_MEDIA_STOP, STATE_IDLE),
|
||||
(SERVICE_MEDIA_PAUSE, STATE_PAUSED),
|
||||
])
|
||||
async def test_state(hass, service, state):
|
||||
"""Test that we can turn a state into a service call."""
|
||||
calls_1 = async_mock_service(hass, DOMAIN, service)
|
||||
|
||||
await async_reproduce_states(hass, [
|
||||
State(ENTITY_1, state)
|
||||
])
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls_1) == 1
|
||||
assert calls_1[0].data == {'entity_id': ENTITY_1}
|
||||
|
||||
|
||||
async def test_turn_on_with_mode(hass):
|
||||
"""Test that state with additional attributes call multiple services."""
|
||||
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
|
||||
calls_2 = async_mock_service(hass, DOMAIN, SERVICE_SELECT_SOUND_MODE)
|
||||
|
||||
await async_reproduce_states(hass, [
|
||||
State(ENTITY_1, 'on',
|
||||
{ATTR_SOUND_MODE: 'dummy'})
|
||||
])
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls_1) == 1
|
||||
assert calls_1[0].data == {'entity_id': ENTITY_1}
|
||||
|
||||
assert len(calls_2) == 1
|
||||
assert calls_2[0].data == {'entity_id': ENTITY_1,
|
||||
ATTR_SOUND_MODE: 'dummy'}
|
||||
|
||||
|
||||
async def test_multiple_same_state(hass):
|
||||
"""Test that multiple states with same state gets calls."""
|
||||
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
|
||||
|
||||
await async_reproduce_states(hass, [
|
||||
State(ENTITY_1, 'on'),
|
||||
State(ENTITY_2, 'on'),
|
||||
])
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls_1) == 2
|
||||
# order is not guaranteed
|
||||
assert any(call.data == {'entity_id': 'media_player.test1'}
|
||||
for call in calls_1)
|
||||
assert any(call.data == {'entity_id': 'media_player.test2'}
|
||||
for call in calls_1)
|
||||
|
||||
|
||||
async def test_multiple_different_state(hass):
|
||||
"""Test that multiple states with different state gets calls."""
|
||||
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
|
||||
calls_2 = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF)
|
||||
|
||||
await async_reproduce_states(hass, [
|
||||
State(ENTITY_1, 'on'),
|
||||
State(ENTITY_2, 'off'),
|
||||
])
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls_1) == 1
|
||||
assert calls_1[0].data == {'entity_id': 'media_player.test1'}
|
||||
assert len(calls_2) == 1
|
||||
assert calls_2[0].data == {'entity_id': 'media_player.test2'}
|
||||
|
||||
|
||||
async def test_state_with_context(hass):
|
||||
"""Test that context is forwarded."""
|
||||
calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
|
||||
|
||||
context = Context()
|
||||
|
||||
await async_reproduce_states(hass, [
|
||||
State(ENTITY_1, 'on')
|
||||
], context)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {'entity_id': ENTITY_1}
|
||||
assert calls[0].context == context
|
||||
|
||||
|
||||
async def test_attribute_no_state(hass):
|
||||
"""Test that no state service call is made with none state."""
|
||||
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
|
||||
calls_2 = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF)
|
||||
calls_3 = async_mock_service(hass, DOMAIN, SERVICE_SELECT_SOUND_MODE)
|
||||
|
||||
value = "dummy"
|
||||
|
||||
await async_reproduce_states(hass, [
|
||||
State(ENTITY_1, None,
|
||||
{ATTR_SOUND_MODE: value})
|
||||
])
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls_1) == 0
|
||||
assert len(calls_2) == 0
|
||||
assert len(calls_3) == 1
|
||||
assert calls_3[0].data == {'entity_id': ENTITY_1,
|
||||
ATTR_SOUND_MODE: value}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'service,attribute', [
|
||||
(SERVICE_VOLUME_SET, ATTR_MEDIA_VOLUME_LEVEL),
|
||||
(SERVICE_VOLUME_MUTE, ATTR_MEDIA_VOLUME_MUTED),
|
||||
(SERVICE_MEDIA_SEEK, ATTR_MEDIA_SEEK_POSITION),
|
||||
(SERVICE_SELECT_SOURCE, ATTR_INPUT_SOURCE),
|
||||
(SERVICE_SELECT_SOUND_MODE, ATTR_SOUND_MODE),
|
||||
])
|
||||
async def test_attribute(hass, service, attribute):
|
||||
"""Test that service call is made for each attribute."""
|
||||
calls_1 = async_mock_service(hass, DOMAIN, service)
|
||||
|
||||
value = "dummy"
|
||||
|
||||
await async_reproduce_states(hass, [
|
||||
State(ENTITY_1, None,
|
||||
{attribute: value})
|
||||
])
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls_1) == 1
|
||||
assert calls_1[0].data == {'entity_id': ENTITY_1,
|
||||
attribute: value}
|
||||
|
||||
|
||||
async def test_play_media(hass):
|
||||
"""Test that no state service call is made with none state."""
|
||||
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_PLAY_MEDIA)
|
||||
|
||||
value_1 = "dummy_1"
|
||||
value_2 = "dummy_2"
|
||||
value_3 = "dummy_3"
|
||||
|
||||
await async_reproduce_states(hass, [
|
||||
State(ENTITY_1, None,
|
||||
{ATTR_MEDIA_CONTENT_TYPE: value_1,
|
||||
ATTR_MEDIA_CONTENT_ID: value_2})
|
||||
])
|
||||
|
||||
await async_reproduce_states(hass, [
|
||||
State(ENTITY_1, None,
|
||||
{ATTR_MEDIA_CONTENT_TYPE: value_1,
|
||||
ATTR_MEDIA_CONTENT_ID: value_2,
|
||||
ATTR_MEDIA_ENQUEUE: value_3})
|
||||
])
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls_1) == 2
|
||||
assert calls_1[0].data == {'entity_id': ENTITY_1,
|
||||
ATTR_MEDIA_CONTENT_TYPE: value_1,
|
||||
ATTR_MEDIA_CONTENT_ID: value_2}
|
||||
|
||||
assert calls_1[1].data == {'entity_id': ENTITY_1,
|
||||
ATTR_MEDIA_CONTENT_TYPE: value_1,
|
||||
ATTR_MEDIA_CONTENT_ID: value_2,
|
||||
ATTR_MEDIA_ENQUEUE: value_3}
|
|
@ -15,8 +15,6 @@ from homeassistant.const import (
|
|||
STATE_LOCKED, STATE_UNLOCKED,
|
||||
STATE_ON, STATE_OFF,
|
||||
STATE_HOME, STATE_NOT_HOME)
|
||||
from homeassistant.components.media_player import (
|
||||
SERVICE_PLAY_MEDIA, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE)
|
||||
from homeassistant.components.sun import (STATE_ABOVE_HORIZON,
|
||||
STATE_BELOW_HORIZON)
|
||||
|
||||
|
@ -50,6 +48,40 @@ def test_async_track_states(hass):
|
|||
sorted(states, key=lambda state: state.entity_id)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_call_to_component(hass):
|
||||
"""Test calls to components state reproduction functions."""
|
||||
with patch(('homeassistant.components.media_player.'
|
||||
'async_reproduce_states')) as media_player_fun:
|
||||
media_player_fun.return_value = asyncio.Future()
|
||||
media_player_fun.return_value.set_result(None)
|
||||
|
||||
with patch(('homeassistant.components.climate.'
|
||||
'async_reproduce_states')) as climate_fun:
|
||||
climate_fun.return_value = asyncio.Future()
|
||||
climate_fun.return_value.set_result(None)
|
||||
|
||||
state_media_player = ha.State('media_player.test', 'bad')
|
||||
state_climate = ha.State('climate.test', 'bad')
|
||||
context = "dummy_context"
|
||||
|
||||
yield from state.async_reproduce_state(
|
||||
hass,
|
||||
[state_media_player, state_climate],
|
||||
blocking=True,
|
||||
context=context)
|
||||
|
||||
media_player_fun.assert_called_once_with(
|
||||
hass,
|
||||
[state_media_player],
|
||||
context=context)
|
||||
|
||||
climate_fun.assert_called_once_with(
|
||||
hass,
|
||||
[state_climate],
|
||||
context=context)
|
||||
|
||||
|
||||
class TestStateHelpers(unittest.TestCase):
|
||||
"""Test the Home Assistant event helpers."""
|
||||
|
||||
|
@ -147,63 +179,6 @@ class TestStateHelpers(unittest.TestCase):
|
|||
assert SERVICE_TURN_ON == last_call.service
|
||||
assert complex_data == last_call.data.get('complex')
|
||||
|
||||
def test_reproduce_media_data(self):
|
||||
"""Test reproduce_state with SERVICE_PLAY_MEDIA."""
|
||||
calls = mock_service(self.hass, 'media_player', SERVICE_PLAY_MEDIA)
|
||||
|
||||
self.hass.states.set('media_player.test', 'off')
|
||||
|
||||
media_attributes = {'media_content_type': 'movie',
|
||||
'media_content_id': 'batman'}
|
||||
|
||||
state.reproduce_state(self.hass, ha.State('media_player.test', 'None',
|
||||
media_attributes))
|
||||
|
||||
self.hass.block_till_done()
|
||||
|
||||
assert len(calls) > 0
|
||||
last_call = calls[-1]
|
||||
assert 'media_player' == last_call.domain
|
||||
assert SERVICE_PLAY_MEDIA == last_call.service
|
||||
assert 'movie' == last_call.data.get('media_content_type')
|
||||
assert 'batman' == last_call.data.get('media_content_id')
|
||||
|
||||
def test_reproduce_media_play(self):
|
||||
"""Test reproduce_state with SERVICE_MEDIA_PLAY."""
|
||||
calls = mock_service(self.hass, 'media_player', SERVICE_MEDIA_PLAY)
|
||||
|
||||
self.hass.states.set('media_player.test', 'off')
|
||||
|
||||
state.reproduce_state(
|
||||
self.hass, ha.State('media_player.test', 'playing'))
|
||||
|
||||
self.hass.block_till_done()
|
||||
|
||||
assert len(calls) > 0
|
||||
last_call = calls[-1]
|
||||
assert 'media_player' == last_call.domain
|
||||
assert SERVICE_MEDIA_PLAY == last_call.service
|
||||
assert ['media_player.test'] == \
|
||||
last_call.data.get('entity_id')
|
||||
|
||||
def test_reproduce_media_pause(self):
|
||||
"""Test reproduce_state with SERVICE_MEDIA_PAUSE."""
|
||||
calls = mock_service(self.hass, 'media_player', SERVICE_MEDIA_PAUSE)
|
||||
|
||||
self.hass.states.set('media_player.test', 'playing')
|
||||
|
||||
state.reproduce_state(
|
||||
self.hass, ha.State('media_player.test', 'paused'))
|
||||
|
||||
self.hass.block_till_done()
|
||||
|
||||
assert len(calls) > 0
|
||||
last_call = calls[-1]
|
||||
assert 'media_player' == last_call.domain
|
||||
assert SERVICE_MEDIA_PAUSE == last_call.service
|
||||
assert ['media_player.test'] == \
|
||||
last_call.data.get('entity_id')
|
||||
|
||||
def test_reproduce_bad_state(self):
|
||||
"""Test reproduce_state with bad state."""
|
||||
calls = mock_service(self.hass, 'light', SERVICE_TURN_ON)
|
||||
|
@ -217,45 +192,6 @@ class TestStateHelpers(unittest.TestCase):
|
|||
assert len(calls) == 0
|
||||
assert 'off' == self.hass.states.get('light.test').state
|
||||
|
||||
def test_reproduce_group(self):
|
||||
"""Test reproduce_state with group."""
|
||||
light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON)
|
||||
|
||||
self.hass.states.set('group.test', 'off', {
|
||||
'entity_id': ['light.test1', 'light.test2']})
|
||||
|
||||
state.reproduce_state(self.hass, ha.State('group.test', 'on'))
|
||||
|
||||
self.hass.block_till_done()
|
||||
|
||||
assert 1 == len(light_calls)
|
||||
last_call = light_calls[-1]
|
||||
assert 'light' == last_call.domain
|
||||
assert SERVICE_TURN_ON == last_call.service
|
||||
assert ['light.test1', 'light.test2'] == \
|
||||
last_call.data.get('entity_id')
|
||||
|
||||
def test_reproduce_group_same_data(self):
|
||||
"""Test reproduce_state with group with same domain and data."""
|
||||
light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON)
|
||||
|
||||
self.hass.states.set('light.test1', 'off')
|
||||
self.hass.states.set('light.test2', 'off')
|
||||
|
||||
state.reproduce_state(self.hass, [
|
||||
ha.State('light.test1', 'on', {'brightness': 95}),
|
||||
ha.State('light.test2', 'on', {'brightness': 95})])
|
||||
|
||||
self.hass.block_till_done()
|
||||
|
||||
assert 1 == len(light_calls)
|
||||
last_call = light_calls[-1]
|
||||
assert 'light' == last_call.domain
|
||||
assert SERVICE_TURN_ON == last_call.service
|
||||
assert ['light.test1', 'light.test2'] == \
|
||||
last_call.data.get('entity_id')
|
||||
assert 95 == last_call.data.get('brightness')
|
||||
|
||||
def test_as_number_states(self):
|
||||
"""Test state_as_number with states."""
|
||||
zero_states = (STATE_OFF, STATE_CLOSED, STATE_UNLOCKED,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue