Merge pull request #66 from balloob/scene-component

Scene component
This commit is contained in:
Paulus Schoutsen 2015-03-16 22:19:56 -07:00
commit 18f27d2e45
13 changed files with 382 additions and 138 deletions

View File

@ -139,3 +139,12 @@ script:
execute_service: light.turn_on
service_data:
entity_id: group.living_room
scene:
- name: Romantic
entities:
light.tv_back_light: on
light.ceiling:
state: on
color: [0.33, 0.66]
brightness: 200

View File

@ -10,7 +10,7 @@ import threading
import json
import homeassistant as ha
from homeassistant.helpers import TrackStates
from homeassistant.helpers.state import TrackStates
import homeassistant.remote as rem
from homeassistant.const import (
URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM,

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "da527496648e2c85d7a5f8c261c18466"
VERSION = "1d8b14c387123a4b42fec6b8f9346675"

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 642a83e437fed356db3e13d5a5b0c28d4b3fb713
Subproject commit e048bf6ece91983b9f03aafeb414ae5c535288a2

View File

@ -81,10 +81,13 @@
<core-icon icon="apps"></core-icon>
States
</paper-item>
<paper-item data-panel="group">
<core-icon icon="homeassistant-24:group"></core-icon>
Groups
</paper-item>
<template repeat="{{activeFilters as filter}}">
<paper-item data-panel="states_{{filter}}">
<core-icon icon="{{filter | filterIcon}}"></core-icon>
{{filter | filterName}}
</paper-item>
</template>
<template if="{{hasHistoryComponent}}">
<paper-item data-panel="history">
@ -93,13 +96,6 @@
</paper-item>
</template>
<template if="{{hasScriptComponent}}">
<paper-item data-panel="script">
<core-icon icon="description"></core-icon>
Scripts
</paper-item>
</template>
<div flex></div>
<paper-item on-click="{{handleLogOutClick}}">
@ -134,7 +130,7 @@
<partial-states hidden?="{{hideStates}}"
main narrow="{{narrow}}"
togglePanel="{{togglePanel}}"
filter="{{selected}}">
filter="{{stateFilter}}">
</partial-states>
<template if="{{selected == 'history'}}">
@ -153,14 +149,18 @@
</template>
<script>
(function() {
var storeListenerMixIn = window.hass.storeListenerMixIn;
var authActions = window.hass.authActions;
var uiUtil = window.hass.uiUtil;
var uiConstants = window.hass.uiConstants;
Polymer(Polymer.mixin({
selected: "states",
stateFilter: null,
narrow: false,
activeFilters: [],
hasHistoryComponent: false,
hasScriptComponent: false,
isStreaming: false,
hasStreamError: false,
@ -177,6 +177,12 @@ Polymer(Polymer.mixin({
this.stopListeningToStores();
},
stateStoreChanged: function(stateStore) {
this.activeFilters = stateStore.domains.filter(function(domain) {
return domain in uiConstants.STATE_FILTERS;
}).toArray();
},
componentStoreChanged: function(componentStore) {
this.hasHistoryComponent = componentStore.isLoaded('history');
this.hasScriptComponent = componentStore.isLoaded('script');
@ -205,14 +211,13 @@ Polymer(Polymer.mixin({
this.togglePanel();
this.selected = newChoice;
}
switch(this.selected) {
case 'states':
case 'group':
case 'script':
hideStates = false;
break;
default:
hideStates = true;
if (this.selected.substr(0, 7) === 'states_') {
this.hideStates = false;
this.stateFilter = this.selected.substr(7);
} else {
this.hideStates = this.selected !== 'states';
this.stateFilter = null;
}
},
@ -227,6 +232,15 @@ Polymer(Polymer.mixin({
handleLogOutClick: function() {
authActions.logOut();
},
filterIcon: function(filter) {
return uiUtil.domainIcon(filter);
},
filterName: function(filter) {
return uiConstants.STATE_FILTERS[filter];
},
}, storeListenerMixIn));
})();
</script>
</polymer-element>

View File

@ -64,16 +64,12 @@
</partial-base>
</template>
<script>
(function(){
var storeListenerMixIn = window.hass.storeListenerMixIn;
var syncActions = window.hass.syncActions;
var voiceActions = window.hass.voiceActions;
var stateStore = window.hass.stateStore;
var stateGroupFilter = function(state) { return state.domain === 'group'; };
var stateScriptFilter = function(state) { return state.domain === 'script'; };
var stateFilter = function(state) {
return !stateGroupFilter(state) && !stateScriptFilter(state);
};
var uiConstants = window.hass.uiConstants;
Polymer(Polymer.mixin({
headerTitle: "States",
@ -123,36 +119,30 @@
this.isTransmitting = voiceStore.isTransmitting;
this.finalTranscript = voiceStore.finalTranscript;
this.interimTranscript = voiceStore.interimTranscript.slice(
this.finalTranscript.length)
this.finalTranscript.length);
},
filterChanged: function() {
this.refreshStates();
switch (this.filter) {
case "group":
this.headerTitle = "Groups";
break;
case "script":
this.headerTitle = "Scripts";
break;
default:
this.headerTitle = "States";
break;
}
this.headerTitle = uiConstants.STATE_FILTERS[this.filter] || 'States';
},
refreshStates: function() {
var states = stateStore.all;
var states;
if (this.filter) {
var filter = this.filter;
states = stateStore.all.filter(function(state) {
console.log(state, state.domain, filter);
return state.domain === filter;
});
if (this.filter == 'group') {
states = states.filter(stateGroupFilter);
} else if (this.filter == 'script') {
states = states.filter(stateScriptFilter);
} else {
states = states.filter(stateFilter);
// all but the STATE_FILTER keys
states = stateStore.all.filter(function(state) {
return !(state.domain in uiConstants.STATE_FILTERS);
});
}
this.states = states.toArray();
@ -170,5 +160,6 @@
}
},
}, storeListenerMixIn));
})();
</script>
</polymer>

View File

@ -84,6 +84,9 @@ window.hass.uiUtil.domainIcon = function(domain, state) {
case "script":
return "description";
case 'scene':
return 'social:pages';
default:
return "bookmark-outline";
}

View File

@ -51,9 +51,17 @@
preferenceStore = window.hass.preferenceStore,
authActions = window.hass.authActions;
window.hass.uiActions = {
window.hass.uiConstants = {
ACTION_SHOW_DIALOG_MORE_INFO: 'ACTION_SHOW_DIALOG_MORE_INFO',
STATE_FILTERS: {
'group': 'Groups',
'script': 'Scripts',
'scene': 'Scenes',
},
};
window.hass.uiActions = {
showMoreInfoDialog: function(entityId) {
dispatcher.dispatch({
actionType: this.ACTION_SHOW_DIALOG_MORE_INFO,
@ -70,6 +78,6 @@
};
// UI specific util methods
window.hass.uiUtil = {}
window.hass.uiUtil = {};
})();
</script>

View File

@ -0,0 +1,183 @@
"""
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 import State
from homeassistant.helpers.device import ToggleDevice
from homeassistant.helpers.device_component import DeviceComponent
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 = DeviceComponent(logger, DOMAIN, hass)
component.add_devices(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()
scene.update_ha_state(True)
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(ToggleDevice):
""" 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.active_requested = False
self.prev_states = None
self.hass.states.track_change(
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)
reproduce_state(self.hass, self.scene_config.states.values())
def turn_off(self):
""" Deactivates scene and restores old states. """
if self.prev_states:
reproduce_state(self.hass, 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 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.update_ha_state(True)
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)
if not self.is_active and self.prev_states:
self.prev_states = None
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()))

View File

@ -1,8 +1,6 @@
"""
Helper methods for components within Home Assistant.
"""
from datetime import datetime
from homeassistant.loader import get_component
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
from homeassistant.util import ensure_unique_string, slugify
@ -43,25 +41,6 @@ def extract_entity_ids(hass, service):
return [ent_id for ent_id in group.expand_entity_ids(hass, service_ent_id)]
# pylint: disable=too-few-public-methods, attribute-defined-outside-init
class TrackStates(object):
"""
Records the time when the with-block is entered. Will add all states
that have changed since the start time to the return list when with-block
is exited.
"""
def __init__(self, hass):
self.hass = hass
self.states = []
def __enter__(self):
self.now = datetime.now()
return self.states
def __exit__(self, exc_type, exc_value, traceback):
self.states.extend(self.hass.states.get_since(self.now))
def validate_config(config, items, logger):
"""
Validates if all items are available in the configuration.

View File

@ -7,6 +7,8 @@ from homeassistant.helpers import (
from homeassistant.components import group, discovery
from homeassistant.const import ATTR_ENTITY_ID
DEFAULT_SCAN_INTERVAL = 15
class DeviceComponent(object):
# pylint: disable=too-many-instance-attributes
@ -14,7 +16,8 @@ class DeviceComponent(object):
"""
Helper class that will help a device component manage its devices.
"""
def __init__(self, logger, domain, hass, scan_interval,
def __init__(self, logger, domain, hass,
scan_interval=DEFAULT_SCAN_INTERVAL,
discovery_platforms=None, group_name=None):
self.logger = logger
self.hass = hass
@ -33,17 +36,8 @@ class DeviceComponent(object):
"""
Sets up a full device component:
- Loads the platforms from the config
- Will update devices on an interval
- Will listen for supported discovered platforms
"""
# only setup group if name is given
if self.group_name is None:
self.group = None
else:
self.group = group.Group(self.hass, self.group_name,
user_defined=False)
# Look in config for Domain, Domain 2, Domain 3 etc and load them
for p_type, p_config in \
config_per_platform(config, self.domain, self.logger):
@ -54,6 +48,31 @@ class DeviceComponent(object):
discovery.listen(self.hass, self.discovery_platforms.keys(),
self._device_discovered)
def add_devices(self, new_devices):
"""
Takes in a list of new devices. For each device will see if it already
exists. If not, will add it, set it up and push the first state.
"""
for device in new_devices:
if device is not None and device not in self.devices.values():
device.hass = self.hass
device.entity_id = generate_entity_id(
self.entity_id_format, device.name, self.devices.keys())
self.devices[device.entity_id] = device
device.update_ha_state()
if self.group is None and self.group_name is not None:
self.group = group.Group(self.hass, self.group_name,
user_defined=False)
if self.group is not None:
self.group.update_tracked_entity_ids(self.devices.keys())
self._start_polling()
def extract_from_service(self, service):
"""
Takes a service and extracts all known devices.
@ -81,27 +100,6 @@ class DeviceComponent(object):
self._setup_platform(self.discovery_platforms[service], {}, info)
def _add_devices(self, new_devices):
"""
Takes in a list of new devices. For each device will see if it already
exists. If not, will add it, set it up and push the first state.
"""
for device in new_devices:
if device is not None and device not in self.devices.values():
device.hass = self.hass
device.entity_id = generate_entity_id(
self.entity_id_format, device.name, self.devices.keys())
self.devices[device.entity_id] = device
device.update_ha_state()
if self.group is not None:
self.group.update_tracked_entity_ids(self.devices.keys())
self._start_polling()
def _start_polling(self):
""" Start polling device states if necessary. """
if self.is_polling or \
@ -125,7 +123,7 @@ class DeviceComponent(object):
try:
platform.setup_platform(
self.hass, config, self._add_devices, discovery_info)
self.hass, config, self.add_devices, discovery_info)
except AttributeError:
# Support old deprecated method for now - 3/1/2015
if hasattr(platform, 'get_devices'):
@ -133,7 +131,7 @@ class DeviceComponent(object):
"Please upgrade %s to return new devices using "
"setup_platform. See %s/demo.py for an example.",
platform_name, self.domain)
self._add_devices(platform.get_devices(self.hass, config))
self.add_devices(platform.get_devices(self.hass, config))
else:
# AttributeError if setup_platform does not exist

View File

@ -0,0 +1,52 @@
"""
homeassistant.helpers.state
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Helpers that help with state related things.
"""
import logging
from datetime import datetime
from homeassistant import State
from homeassistant.const import STATE_ON, STATE_OFF
import homeassistant.components as core_components
_LOGGER = logging.getLogger(__name__)
# pylint: disable=too-few-public-methods, attribute-defined-outside-init
class TrackStates(object):
"""
Records the time when the with-block is entered. Will add all states
that have changed since the start time to the return list when with-block
is exited.
"""
def __init__(self, hass):
self.hass = hass
self.states = []
def __enter__(self):
self.now = datetime.now()
return self.states
def __exit__(self, exc_type, exc_value, traceback):
self.states.extend(self.hass.states.get_since(self.now))
def reproduce_state(hass, states):
""" Takes in a state and will try to have the entity reproduce it. """
if isinstance(states, State):
states = [states]
for state in states:
current_state = hass.states.get(state.entity_id)
if current_state is None:
continue
if state.state == STATE_ON:
core_components.turn_on(hass, state.entity_id, **state.attributes)
elif state.state == STATE_OFF:
core_components.turn_off(hass, state.entity_id, **state.attributes)
else:
_LOGGER.warning("Unable to reproduce state for %s", state)