From 980ecdaacb6c5f62d37a99e3a94fbbc3b65167f2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 19 Jan 2015 00:02:25 -0800 Subject: [PATCH] Add initial version of configurator component --- homeassistant/components/configurator.py | 155 ++++++++++++++++++ homeassistant/components/demo.py | 28 ++++ homeassistant/components/http/frontend.py | 2 +- .../components/http/www_static/frontend.html | 14 +- .../www_static/images/config_philips_hue.jpg | Bin 0 -> 8976 bytes .../polymer/components/domain-icon.html | 3 + .../polymer/home-assistant-api.html | 2 +- .../more-infos/more-info-configurator.html | 87 ++++++++++ .../polymer/more-infos/more-info-content.html | 1 + .../polymer/more-infos/more-info-default.html | 2 +- homeassistant/components/light/__init__.py | 51 +++--- homeassistant/components/light/hue.py | 86 ++++++++-- homeassistant/components/light/wink.py | 23 +-- homeassistant/components/switch/__init__.py | 7 +- homeassistant/external/wink/pywink.py | 5 + homeassistant/helpers.py | 16 +- 16 files changed, 418 insertions(+), 64 deletions(-) create mode 100644 homeassistant/components/configurator.py create mode 100644 homeassistant/components/http/www_static/images/config_philips_hue.jpg create mode 100644 homeassistant/components/http/www_static/polymer/more-infos/more-info-configurator.html diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py new file mode 100644 index 00000000000..8b465abb9e9 --- /dev/null +++ b/homeassistant/components/configurator.py @@ -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 diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 4076ca63159..62dafffa9d6 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -5,6 +5,7 @@ homeassistant.components.demo Sets up a demo environment that mimics interaction with devices """ import random +import time import homeassistant as ha import homeassistant.loader as loader @@ -28,6 +29,7 @@ DEPENDENCIES = [] def setup(hass, config): """ Setup a demo environment. """ group = loader.get_component('group') + configurator = loader.get_component('configurator') config.setdefault(ha.DOMAIN, {}) config.setdefault(DOMAIN, {}) @@ -170,4 +172,30 @@ def setup(hass, config): 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 diff --git a/homeassistant/components/http/frontend.py b/homeassistant/components/http/frontend.py index 48100f97ac7..df97ffe708f 100644 --- a/homeassistant/components/http/frontend.py +++ b/homeassistant/components/http/frontend.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "951c0a4e0adb70ec1f0f7e4c76955ed9" +VERSION = "f299ce624d1641191f6f6a9b4b4d05bc" diff --git a/homeassistant/components/http/www_static/frontend.html b/homeassistant/components/http/www_static/frontend.html index 273f01f46bb..7129040444c 100644 --- a/homeassistant/components/http/www_static/frontend.html +++ b/homeassistant/components/http/www_static/frontend.html @@ -50,7 +50,7 @@ if(this.removeAttribute(a),d)return j(this,a,c);var e=c,f=m(this,a,e);return j(t