core/homeassistant/components/scene.py

192 lines
5.9 KiB
Python

"""
homeassistant.components.scene
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Allows users to set and activate scenes within Home Assistant.
A scene is a set of states that describe how you want certain entities to be.
For example, light A should be red with 100 brightness. Light B should be on.
A scene is active if all states of the scene match the real states.
If a scene is manually activated it will store the previous state of the
entities. These will be restored when the state is deactivated manually.
If one of the enties that are being tracked change state on its own, the
old state will not be restored when it is being deactivated.
"""
import logging
from collections import namedtuple
from homeassistant.core import State
from homeassistant.helpers.event import track_state_change
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.state import reproduce_state
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF)
DOMAIN = 'scene'
DEPENDENCIES = ['group']
ATTR_ACTIVE_REQUESTED = "active_requested"
CONF_ENTITIES = "entities"
SceneConfig = namedtuple('SceneConfig', ['name', 'states'])
def setup(hass, config):
""" Sets up scenes. """
logger = logging.getLogger(__name__)
scene_configs = config.get(DOMAIN)
if not isinstance(scene_configs, list):
logger.error('Scene config should be a list of scenes')
return False
component = EntityComponent(logger, DOMAIN, hass)
component.add_entities(Scene(hass, _process_config(scene_config))
for scene_config in scene_configs)
def handle_scene_service(service):
""" Handles calls to the switch services. """
target_scenes = component.extract_from_service(service)
for scene in target_scenes:
if service.service == SERVICE_TURN_ON:
scene.turn_on()
else:
scene.turn_off()
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_scene_service)
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_scene_service)
return True
def _process_config(scene_config):
""" Process passed in config into a format to work with. """
name = scene_config.get('name')
states = {}
c_entities = dict(scene_config.get(CONF_ENTITIES, {}))
for entity_id in c_entities:
if isinstance(c_entities[entity_id], dict):
state = c_entities[entity_id].pop('state', None)
attributes = c_entities[entity_id]
else:
state = c_entities[entity_id]
attributes = {}
# YAML translates 'on' to a boolean
# http://yaml.org/type/bool.html
if isinstance(state, bool):
state = STATE_ON if state else STATE_OFF
else:
state = str(state)
states[entity_id.lower()] = State(entity_id, state, attributes)
return SceneConfig(name, states)
class Scene(ToggleEntity):
""" A scene is a group of entities and the states we want them to be. """
def __init__(self, hass, scene_config):
self.hass = hass
self.scene_config = scene_config
self.is_active = False
self.prev_states = None
self.ignore_updates = False
track_state_change(
self.hass, self.entity_ids, self.entity_state_changed)
self.update()
@property
def should_poll(self):
return False
@property
def name(self):
return self.scene_config.name
@property
def is_on(self):
return self.is_active
@property
def entity_ids(self):
""" Entity IDs part of this scene. """
return self.scene_config.states.keys()
@property
def state_attributes(self):
""" Scene state attributes. """
return {
ATTR_ENTITY_ID: list(self.entity_ids),
ATTR_ACTIVE_REQUESTED: self.prev_states is not None,
}
def turn_on(self):
""" Activates scene. Tries to get entities into requested state. """
self.prev_states = tuple(self.hass.states.get(entity_id)
for entity_id in self.entity_ids)
self._reproduce_state(self.scene_config.states.values())
def turn_off(self):
""" Deactivates scene and restores old states. """
if self.prev_states:
self._reproduce_state(self.prev_states)
self.prev_states = None
def entity_state_changed(self, entity_id, old_state, new_state):
""" Called when an entity part of this scene changes state. """
if self.ignore_updates:
return
# If new state is not what we expect, it can never be active
if self._state_as_requested(new_state):
self.update()
else:
self.is_active = False
self.prev_states = None
self.update_ha_state()
def update(self):
"""
Update if the scene is active.
Will look at each requested state and see if the current entity
has the same state and has at least the same attributes with the
same values. The real state can have more attributes.
"""
self.is_active = all(
self._state_as_requested(self.hass.states.get(entity_id))
for entity_id in self.entity_ids)
def _state_as_requested(self, cur_state):
""" Returns if given state is as requested. """
state = self.scene_config.states.get(cur_state and cur_state.entity_id)
return (cur_state is not None and state.state == cur_state.state and
all(value == cur_state.attributes.get(key)
for key, value in state.attributes.items()))
def _reproduce_state(self, states):
""" Wraps reproduce state with Scence specific logic. """
self.ignore_updates = True
reproduce_state(self.hass, states, True)
self.ignore_updates = False
self.update_ha_state(True)