Add initial version of configurator component

This commit is contained in:
Paulus Schoutsen 2015-01-19 00:02:25 -08:00
parent 0c5f1234da
commit 980ecdaacb
16 changed files with 418 additions and 64 deletions

View File

@ -0,0 +1,155 @@
import logging
from homeassistant.helpers import generate_entity_id
from homeassistant.const import EVENT_TIME_CHANGED
DOMAIN = "configurator"
DEPENDENCIES = []
ENTITY_ID_FORMAT = DOMAIN + ".{}"
SERVICE_CONFIGURE = "configure"
STATE_CONFIGURE = "configure"
STATE_CONFIGURED = "configured"
ATTR_CONFIGURE_ID = "configure_id"
ATTR_DESCRIPTION = "description"
ATTR_DESCRIPTION_IMAGE = "description_image"
ATTR_SUBMIT_CAPTION = "submit_caption"
ATTR_FIELDS = "fields"
ATTR_ERRORS = "errors"
_INSTANCES = {}
_LOGGER = logging.getLogger(__name__)
def request_config(
hass, name, callback, description=None, description_image=None,
submit_caption=None, fields=None):
""" Create a new request for config.
Will return an ID to be used for sequent calls. """
return _get_instance(hass).request_config(
name, callback,
description, description_image, submit_caption, fields)
def notify_errors(hass, request_id, error):
_get_instance(hass).notify_errors(request_id, error)
def request_done(hass, request_id):
_get_instance(hass).request_done(request_id)
def setup(hass, config):
return True
def _get_instance(hass):
""" Get an instance per hass object. """
try:
return _INSTANCES[hass]
except KeyError:
print("Creating instance")
_INSTANCES[hass] = Configurator(hass)
if DOMAIN not in hass.components:
hass.components.append(DOMAIN)
return _INSTANCES[hass]
class Configurator(object):
def __init__(self, hass):
self.hass = hass
self._cur_id = 0
self._requests = {}
hass.services.register(
DOMAIN, SERVICE_CONFIGURE, self.handle_service_call)
def request_config(
self, name, callback,
description, description_image, submit_caption, fields):
""" Setup a request for configuration. """
entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass)
if fields is None:
fields = []
request_id = self._generate_unique_id()
self._requests[request_id] = (entity_id, fields, callback)
data = {
ATTR_CONFIGURE_ID: request_id,
ATTR_FIELDS: fields,
}
data.update({
key: value for key, value in [
(ATTR_DESCRIPTION, description),
(ATTR_DESCRIPTION_IMAGE, description_image),
(ATTR_SUBMIT_CAPTION, submit_caption),
] if value is not None
})
self.hass.states.set(entity_id, STATE_CONFIGURE, data)
return request_id
def notify_errors(self, request_id, error):
""" Update the state with errors. """
if not self._validate_request_id(request_id):
return
entity_id = self._requests[request_id][0]
state = self.hass.states.get(entity_id)
new_data = state.attributes
new_data[ATTR_ERRORS] = error
self.hass.states.set(entity_id, STATE_CONFIGURE, new_data)
def request_done(self, request_id):
""" Remove the config request. """
if not self._validate_request_id(request_id):
return
entity_id = self._requests.pop(request_id)[0]
# If we remove the state right away, it will not be passed down
# with the service request (limitation current design).
# Instead we will set it to configured right away and remove it soon.
def deferred_remove(event):
self.hass.states.remove(entity_id)
self.hass.bus.listen_once(EVENT_TIME_CHANGED, deferred_remove)
self.hass.states.set(entity_id, STATE_CONFIGURED)
def handle_service_call(self, call):
request_id = call.data.get(ATTR_CONFIGURE_ID)
if not self._validate_request_id(request_id):
return
entity_id, fields, callback = self._requests[request_id]
# TODO field validation?
callback(call.data.get(ATTR_FIELDS, {}))
def _generate_unique_id(self):
""" Generates a unique configurator id. """
self._cur_id += 1
return "{}-{}".format(id(self), self._cur_id)
def _validate_request_id(self, request_id):
if request_id not in self._requests:
_LOGGER.error("Invalid configure id received: %s", request_id)
return False
return True

View File

@ -5,6 +5,7 @@ homeassistant.components.demo
Sets up a demo environment that mimics interaction with devices Sets up a demo environment that mimics interaction with devices
""" """
import random import random
import time
import homeassistant as ha import homeassistant as ha
import homeassistant.loader as loader import homeassistant.loader as loader
@ -28,6 +29,7 @@ DEPENDENCIES = []
def setup(hass, config): def setup(hass, config):
""" Setup a demo environment. """ """ Setup a demo environment. """
group = loader.get_component('group') group = loader.get_component('group')
configurator = loader.get_component('configurator')
config.setdefault(ha.DOMAIN, {}) config.setdefault(ha.DOMAIN, {})
config.setdefault(DOMAIN, {}) config.setdefault(DOMAIN, {})
@ -170,4 +172,30 @@ def setup(hass, config):
ATTR_AWAY_MODE: STATE_OFF ATTR_AWAY_MODE: STATE_OFF
}) })
configurator_ids = []
def hue_configuration_callback(data):
""" Fake callback, mark config as done. """
time.sleep(2)
# First time it is called, pretend it failed.
if len(configurator_ids) == 1:
configurator.notify_errors(
hass, configurator_ids[0],
"Failed to register, please try again.")
configurator_ids.append(0)
else:
configurator.request_done(hass, configurator_ids[0])
request_id = configurator.request_config(
hass, "Philips Hue", hue_configuration_callback,
description=("Press the button on the bridge to register Philips Hue "
"with Home Assistant."),
description_image="/static/images/config_philips_hue.jpg",
submit_caption="I have pressed the button"
)
configurator_ids.append(request_id)
return True return True

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """ """ DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "951c0a4e0adb70ec1f0f7e4c76955ed9" VERSION = "f299ce624d1641191f6f6a9b4b4d05bc"

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -62,6 +62,9 @@
case "sensor": case "sensor":
return "visibility"; return "visibility";
case "configurator":
return "settings";
default: default:
return "bookmark-outline"; return "bookmark-outline";
} }

View File

@ -40,7 +40,7 @@
</template> </template>
<script> <script>
var domainsWithCard = ['thermostat']; var domainsWithCard = ['thermostat'];
var domainsWithMoreInfo = ['light', 'group', 'sun']; var domainsWithMoreInfo = ['light', 'group', 'sun', 'configurator'];
State = function(json, api) { State = function(json, api) {
this.api = api; this.api = api;

View File

@ -0,0 +1,87 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-button/paper-button.html">
<link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
<polymer-element name="more-info-configurator" attributes="stateObj api">
<template>
<style>
p {
margin: 8px 0;
}
p > img {
max-width: 100%;
}
p.error {
color: #C62828;
}
p.submit {
text-align: center;
height: 41px;
}
p.submit paper-spinner {
margin-right: 16px;
}
p.submit span {
display: inline-block;
vertical-align: top;
margin-top: 6px;
}
</style>
<div layout vertical>
<template if="{{stateObj.state == 'configure'}}">
<p hidden?="{{!stateObj.attributes.description}}">
{{stateObj.attributes.description}}
</p>
<p class='error' hidden?="{{!stateObj.attributes.errors}}">
Errors: {{stateObj.attributes.errors}}
</p>
<p hidden?="{{!stateObj.attributes.description_image}}">
<img src='{{stateObj.attributes.description_image}}' />
</p>
<p class='submit'>
<paper-button raised on-click="{{submitClicked}}"
hidden?="{{action !== 'display'}}">
{{stateObj.attributes.submit_caption || "Set configuration"}}
</paper-button>
<span hidden?="{{action !== 'configuring'}}">
<paper-spinner active="true"></paper-spinner><span>Configuring…</span>
</span>
</p>
</template>
</div>
</template>
<script>
Polymer({
action: "display",
submitClicked: function() {
this.action = "configuring";
var data = {
configure_id: this.stateObj.attributes.configure_id
};
this.api.call_service('configurator', 'configure', data, {
success: function() {
this.action = 'display';
this.api.fetchAll();
}.bind(this),
error: function() {
this.action = 'display';
}.bind(this)
});
}
});
</script>
</polymer-element>

View File

@ -4,6 +4,7 @@
<link rel="import" href="more-info-light.html"> <link rel="import" href="more-info-light.html">
<link rel="import" href="more-info-group.html"> <link rel="import" href="more-info-group.html">
<link rel="import" href="more-info-sun.html"> <link rel="import" href="more-info-sun.html">
<link rel="import" href="more-info-configurator.html">
<polymer-element name="more-info-content" attributes="api stateObj"> <polymer-element name="more-info-content" attributes="api stateObj">
<template> <template>

View File

@ -22,7 +22,7 @@
<div layout vertical> <div layout vertical>
<template repeat="{{key in stateObj.attributes | getKeys}}"> <template repeat="{{key in stateObj.attributes | getKeys}}">
<div layout justified horizontal class='data-entry' id='rising'> <div layout justified horizontal class='data-entry'>
<div> <div>
{{key}} {{key}}
</div> </div>

View File

@ -57,7 +57,7 @@ import homeassistant.util as util
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.helpers import ( from homeassistant.helpers import (
extract_entity_ids, platform_devices_from_config) generate_entity_id, extract_entity_ids, config_per_platform)
from homeassistant.components import group, discovery, wink from homeassistant.components import group, discovery, wink
@ -93,6 +93,7 @@ LIGHT_PROFILES_FILE = "light_profiles.csv"
# Maps discovered services to their platforms # Maps discovered services to their platforms
DISCOVERY_PLATFORMS = { DISCOVERY_PLATFORMS = {
wink.DISCOVER_LIGHTS: 'wink', wink.DISCOVER_LIGHTS: 'wink',
discovery.services.PHILIPS_HUE: 'hue',
} }
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -168,8 +169,32 @@ def setup(hass, config):
return False return False
lights = platform_devices_from_config( # Dict to track entity_id -> lights
config, DOMAIN, hass, ENTITY_ID_FORMAT, _LOGGER) lights = {}
# Track all lights in a group
light_group = group.Group(hass, GROUP_NAME_ALL_LIGHTS, user_defined=False)
def add_lights(new_lights):
""" Add lights to the component to track. """
for light in new_lights:
if light is not None and light not in lights.values():
light.entity_id = generate_entity_id(
ENTITY_ID_FORMAT, light.name, lights.keys())
lights[light.entity_id] = light
light.update_ha_state(hass)
light_group.update_tracked_entity_ids(lights.keys())
for p_type, p_config in config_per_platform(config, DOMAIN, _LOGGER):
platform = get_component(ENTITY_ID_FORMAT.format(p_type))
if platform is None:
_LOGGER.error("Unknown type specified: %s", p_type)
platform.setup_platform(hass, p_config, add_lights)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def update_lights_state(now): def update_lights_state(now):
@ -182,28 +207,12 @@ def setup(hass, config):
update_lights_state(None) update_lights_state(None)
# Track all lights in a group
light_group = group.Group(
hass, GROUP_NAME_ALL_LIGHTS, lights.keys(), False)
def light_discovered(service, info): def light_discovered(service, info):
""" Called when a light is discovered. """ """ Called when a light is discovered. """
platform = get_component( platform = get_component(
"{}.{}".format(DOMAIN, DISCOVERY_PLATFORMS[service])) ENTITY_ID_FORMAT.format(DISCOVERY_PLATFORMS[service]))
discovered = platform.devices_discovered(hass, config, info) platform.setup_platform(hass, {}, add_lights, info)
for light in discovered:
if light is not None and light not in lights.values():
light.entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(light.name)),
lights.keys())
lights[light.entity_id] = light
light.update_ha_state(hass)
light_group.update_tracked_entity_ids(lights.keys())
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(), light_discovered) discovery.listen(hass, DISCOVERY_PLATFORMS.keys(), light_discovered)

View File

@ -2,7 +2,9 @@
import logging import logging
import socket import socket
from datetime import timedelta from datetime import timedelta
from urllib.parse import urlparse
from homeassistant.loader import get_component
import homeassistant.util as util import homeassistant.util as util
from homeassistant.helpers import ToggleDevice from homeassistant.helpers import ToggleDevice
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOST from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOST
@ -16,27 +18,57 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
PHUE_CONFIG_FILE = "phue.conf" PHUE_CONFIG_FILE = "phue.conf"
def get_devices(hass, config): # Map ip to request id for configuring
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Gets the Hue lights. """ """ Gets the Hue lights. """
logger = logging.getLogger(__name__)
try: try:
import phue import phue
except ImportError: except ImportError:
logger.exception("Error while importing dependency phue.") _LOGGER.exception("Error while importing dependency phue.")
return [] return
if discovery_info is not None:
host = urlparse(discovery_info).hostname
else:
host = config.get(CONF_HOST, None) host = config.get(CONF_HOST, None)
# Only act if we are not already configuring this host
if host in _CONFIGURING:
return
setup_bridge(host, hass, add_devices_callback)
def setup_bridge(host, hass, add_devices_callback):
import phue
try: try:
bridge = phue.Bridge( bridge = phue.Bridge(
host, config_file_path=hass.get_config_path(PHUE_CONFIG_FILE)) host, config_file_path=hass.get_config_path(PHUE_CONFIG_FILE))
except socket.error: # Error connecting using Phue except ConnectionRefusedError: # Wrong host was given
logger.exception(( _LOGGER.exception("Error connecting to the Hue bridge at %s", host)
"Error while connecting to the bridge. "
"Did you follow the instructions to set it up?"))
return [] return
except phue.PhueRegistrationException:
_LOGGER.warning("Connected to Hue at %s but not registered.", host)
request_configuration(host, hass, add_devices_callback)
return
# If we came here and configuring this host, mark as done
if host in _CONFIGURING:
request_id = _CONFIGURING.pop(host)
configurator = get_component('configurator')
configurator.request_done(hass, request_id)
lights = {} lights = {}
@ -47,25 +79,53 @@ def get_devices(hass, config):
api = bridge.get_api() api = bridge.get_api()
except socket.error: except socket.error:
# socket.error when we cannot reach Hue # socket.error when we cannot reach Hue
logger.exception("Cannot reach the bridge") _LOGGER.exception("Cannot reach the bridge")
return return
api_states = api.get('lights') api_states = api.get('lights')
if not isinstance(api_states, dict): if not isinstance(api_states, dict):
logger.error("Got unexpected result from Hue API") _LOGGER.error("Got unexpected result from Hue API")
return return
new_lights = []
for light_id, info in api_states.items(): for light_id, info in api_states.items():
if light_id not in lights: if light_id not in lights:
lights[light_id] = HueLight(int(light_id), info, lights[light_id] = HueLight(int(light_id), info,
bridge, update_lights) bridge, update_lights)
new_lights.append(lights[light_id])
else: else:
lights[light_id].info = info lights[light_id].info = info
if new_lights:
add_devices_callback(new_lights)
update_lights() update_lights()
return list(lights.values())
def request_configuration(host, hass, add_devices_callback):
""" Request configuration steps from the user. """
configurator = get_component('configurator')
# If this method called while we are configuring, means we got an error
if host in _CONFIGURING:
configurator.notify_errors(
hass, _CONFIGURING[host], "Failed to register, please try again.")
return
def hue_configuration_callback(data):
""" Actions to do when our configuration callback is called. """
setup_bridge(host, hass, add_devices_callback)
_CONFIGURING[host] = configurator.request_config(
hass, "Philips Hue", hue_configuration_callback,
description=("Press the button on the bridge to register Philips Hue "
"with Home Assistant."),
description_image="/static/images/config_philips_hue.jpg",
submit_caption="I have pressed the button"
)
class HueLight(ToggleDevice): class HueLight(ToggleDevice):

View File

@ -10,30 +10,21 @@ from homeassistant.const import CONF_ACCESS_TOKEN
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get_devices(hass, config): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return Wink lights. """ """ Find and return Wink lights. """
token = config.get(CONF_ACCESS_TOKEN) token = config.get(CONF_ACCESS_TOKEN)
if token is None: if not pywink.is_token_set() and token is None:
logging.getLogger(__name__).error( logging.getLogger(__name__).error(
"Missing wink access_token - " "Missing wink access_token - "
"get one at https://winkbearertoken.appspot.com/") "get one at https://winkbearertoken.appspot.com/")
return [] return
elif token is not None:
pywink.set_bearer_token(token) pywink.set_bearer_token(token)
return get_lights() add_devices_callback(
WinkLight(light) for light in pywink.get_bulbs())
# pylint: disable=unused-argument
def devices_discovered(hass, config, info):
""" Called when a device is discovered. """
return get_lights()
def get_lights():
""" Returns the Wink switches. """
return [WinkLight(light) for light in pywink.get_bulbs()]
class WinkLight(WinkToggleDevice): class WinkLight(WinkToggleDevice):

View File

@ -11,7 +11,7 @@ import homeassistant.util as util
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.helpers import ( from homeassistant.helpers import (
extract_entity_ids, platform_devices_from_config) generate_entity_id, extract_entity_ids, platform_devices_from_config)
from homeassistant.components import group, discovery, wink from homeassistant.components import group, discovery, wink
DOMAIN = 'switch' DOMAIN = 'switch'
@ -90,9 +90,8 @@ def setup(hass, config):
for switch in discovered: for switch in discovered:
if switch is not None and switch not in switches.values(): if switch is not None and switch not in switches.values():
switch.entity_id = util.ensure_unique_string( switch.entity_id = generate_entity_id(
ENTITY_ID_FORMAT.format(util.slugify(switch.name)), ENTITY_ID_FORMAT, switch.name, switches.keys())
switches.keys())
switches[switch.entity_id] = switch switches[switch.entity_id] = switch

View File

@ -389,6 +389,11 @@ def get_switches():
def get_sensors(): def get_sensors():
return get_devices('sensor_pod_id', wink_sensor_pod) return get_devices('sensor_pod_id', wink_sensor_pod)
def is_token_set():
""" Returns if an auth token has been set. """
return bool(headers)
def set_bearer_token(token): def set_bearer_token(token):
global headers global headers

View File

@ -12,6 +12,17 @@ from homeassistant.const import (
from homeassistant.util import ensure_unique_string, slugify from homeassistant.util import ensure_unique_string, slugify
def generate_entity_id(entity_id_format, name, current_ids=None, hass=None):
if current_ids is None:
if hass is None:
raise RuntimeError("Missing required parameter currentids or hass")
current_ids = hass.states.entity_ids()
return ensure_unique_string(
entity_id_format.format(slugify(name)), current_ids)
def extract_entity_ids(hass, service): def extract_entity_ids(hass, service):
""" """
Helper method to extract a list of entity ids from a service call. Helper method to extract a list of entity ids from a service call.
@ -160,9 +171,8 @@ def platform_devices_from_config(config, domain, hass,
no_name_count += 1 no_name_count += 1
name = "{} {}".format(domain, no_name_count) name = "{} {}".format(domain, no_name_count)
entity_id = ensure_unique_string( entity_id = generate_entity_id(
entity_id_format.format(slugify(name)), entity_id_format, name, device_dict.keys())
device_dict.keys())
device.entity_id = entity_id device.entity_id = entity_id
device_dict[entity_id] = device device_dict[entity_id] = device