From 8d0bddac6c5d76ed6fd11a281fbc2379a2eb15b7 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen
Date: Mon, 19 Jan 2015 00:00:01 -0800
Subject: [PATCH 01/13] New: State.last_updated represents creation date
---
homeassistant/__init__.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py
index bb8afbc97fc..a51bb6b47a8 100644
--- a/homeassistant/__init__.py
+++ b/homeassistant/__init__.py
@@ -426,9 +426,11 @@ class State(object):
state: the state of the entity
attributes: extra information on entity and state
last_changed: last time the state was changed, not the attributes.
+ last_updated: last time this object was updated.
"""
- __slots__ = ['entity_id', 'state', 'attributes', 'last_changed']
+ __slots__ = ['entity_id', 'state', 'attributes',
+ 'last_changed', 'last_updated']
def __init__(self, entity_id, state, attributes=None, last_changed=None):
if not ENTITY_ID_PATTERN.match(entity_id):
@@ -439,13 +441,14 @@ class State(object):
self.entity_id = entity_id
self.state = state
self.attributes = attributes or {}
+ self.last_updated = dt.datetime.now()
# Strip microsecond from last_changed else we cannot guarantee
# state == State.from_dict(state.as_dict())
# This behavior occurs because to_dict uses datetime_to_str
# which does not preserve microseconds
self.last_changed = util.strip_microseconds(
- last_changed or dt.datetime.now())
+ last_changed or self.last_updated)
def copy(self):
""" Creates a copy of itself. """
@@ -527,15 +530,12 @@ class StateMachine(object):
def get_since(self, point_in_time):
"""
Returns all states that have been changed since point_in_time.
-
- Note: States keep track of last_changed -without- microseconds.
- Therefore your point_in_time will also be stripped of microseconds.
"""
point_in_time = util.strip_microseconds(point_in_time)
with self._lock:
return [state for state in self._states.values()
- if state.last_changed >= point_in_time]
+ if state.last_updated >= point_in_time]
def is_state(self, entity_id, state):
""" Returns True if entity exists and is specified state. """
From 35094a1667acd0e4ec15cbf6dbc1db5a5cf9e19f Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen
Date: Mon, 19 Jan 2015 00:00:59 -0800
Subject: [PATCH 02/13] Data binding fix: Update instead of replace states
---
.../http/www_static/polymer/home-assistant-api.html | 10 ++--------
1 file changed, 2 insertions(+), 8 deletions(-)
diff --git a/homeassistant/components/http/www_static/polymer/home-assistant-api.html b/homeassistant/components/http/www_static/polymer/home-assistant-api.html
index 5b8be54a1d8..2ac2bbcbd51 100644
--- a/homeassistant/components/http/www_static/polymer/home-assistant-api.html
+++ b/homeassistant/components/http/www_static/polymer/home-assistant-api.html
@@ -216,9 +216,7 @@
},
_pushNewStates: function(new_states) {
- new_states.map(function(state) {
- this._pushNewState(state);
- }.bind(this));
+ new_states.forEach(this._pushNewState.bind(this));
},
// call api methods
@@ -238,11 +236,7 @@
fetchStates: function(onSuccess, onError) {
var successStatesUpdate = function(newStates) {
- this._sortStates(newStates);
-
- this.states = newStates.map(function(json) {
- return new State(json, this);
- }.bind(this));
+ this._pushNewStates(newStates);
this.fire('states-updated');
From 0c5f1234da70e0e06d8049c49d7446911e8ddc16 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen
Date: Mon, 19 Jan 2015 00:01:40 -0800
Subject: [PATCH 03/13] Fix tabs being selectable by clicking on header
---
.../polymer/home-assistant-main.html | 20 ++++++++++---------
1 file changed, 11 insertions(+), 9 deletions(-)
diff --git a/homeassistant/components/http/www_static/polymer/home-assistant-main.html b/homeassistant/components/http/www_static/polymer/home-assistant-main.html
index 851b945e50b..3cbaf0a6f11 100644
--- a/homeassistant/components/http/www_static/polymer/home-assistant-main.html
+++ b/homeassistant/components/http/www_static/polymer/home-assistant-main.html
@@ -94,15 +94,17 @@
-
+
+
+
Date: Mon, 19 Jan 2015 00:02:25 -0800
Subject: [PATCH 04/13] 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
{{label}}
-+
++
{{stateObj.entityDisplay}}
{{stateObj.relativeLastChanged}}
@@ -60,7 +60,7 @@ if(this.removeAttribute(a),d)return j(this,a,c);var e=c,f=m(this,a,e);return j(t
{{stateObj.stateDisplay}}
Currently: {{stateObj.attributes.current_temperature}} {{stateObj.attributes.unit_of_measurement}}
-
Home Assistant
Hi there!
+
Home Assistant
Hi there!
It looks like we have nothing to show you right now. It could be that we have not yet discovered all your devices but it is more likely that you have not configured Home Assistant yet.
Please see the Getting Started section on how to setup your devices.
@@ -71,7 +71,7 @@ if(this.removeAttribute(a),d)return j(this,a,c);var e=c,f=m(this,a,e);return j(t
Cancel Fire Event {{label}}
Cancel Call Service
This dialog will update the representation of the device within Home Assistant.
This will not communicate with the actual device.
-
Cancel Set State
+
Cancel Set State
{{key}}
{{stateObj.attributes[key]}}
@@ -83,7 +83,13 @@ if(this.removeAttribute(a),d)return j(this,a,c);var e=c,f=m(this,a,e);return j(t
Setting {{stateObj.attributes.next_setting | relativeHATime}}
{{stateObj.attributes.next_setting | HATimeStripDate}}
-
Debug Dismiss
+
+ {{stateObj.attributes.description}}
+
+ Errors: {{stateObj.attributes.errors}}
+
+ {{stateObj.attributes.submit_caption || "Set configuration"}}
+ Configuring…
Debug Dismiss
:host {
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
diff --git a/homeassistant/components/http/www_static/images/config_philips_hue.jpg b/homeassistant/components/http/www_static/images/config_philips_hue.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..f10d258bf34f8e7986f6a169a88ce578626f27be
GIT binary patch
literal 8976
zcmc(EcUY54^Jr+HH^l&g5Q+jKB>@Rl484egB1HrZgiaC&AwdyPLhnr=bVNW7AfnQn
zNSC6B^d?p5y|;^=<2mPkzw@1Y|GIac=Y4nIeP?!NXJ%(-caQsz#{nGb?)EkSfVTEU
z01ep-s<~M?J^+vbC{LOd0f6Ii3Q;>8&RJeU0_`e}Kw>Pd#jP+X33r6E1Vmg?
z0-&hu?u@Wst@68(KYdw30*e
zD_;RCy34zxoKe;|1lS$rgm#s8SK|NKx%^4}N3#S!_$La^QHdY^gDcoXPamv`!CHea
zi%W`GNy75#t^&V$P_uTm!rD9I>@jHY
zj~)@07&n{}|B0l(4*}(@r}t;a|EMh#>c_Z#V!PsQTK^@+KVrKYK5({{xM}T*al=|!
zpP&o;B0kaGzcc!Qcp{Cw8pg^EWsSyZt10oHw1^|^k@6aHl5#LLHPw@s6a=C!EeC^X
zs6r*-YHDgQIf$D2FIsK1D-MCSvi?PD{}-*wztYO9VyzK44Au~Xar$LFeLD;e<7$U-
z2CLqZ0Sg=3qmdYbtH=-4{voL~)*f$-)WBj;;GepYxBmzJYAUKQ4K+E4hK!utiR=&+
zxC{iYuAwfceia{&ak6
z^oh4)Pu%)=3BdHzSpmp*C)0=QcnIJN08&y?Qc(h_sDQM|0IW2C
zJ+ku@WUK&kRx%1!vf~Co=*b)>rywIE`)m4B0mR$*F1nx5l5p$yfknlw{--C*z=`
z1Ja++kONL|saS!0kW+B}(`?iR2&jt7lbe=UfvAi#pe~Uv$Ot<8cG6OeA1=hL?_lZM`un!`xVxERkec?6gp?-d1
z-hTrQ0-Uy-fEFc}^b4=#q{nncH1>evMq;+qL78f5!UaJ%JYE9Og-
zpA83B*v)0%da4zq5@)8-%%d2Zdy$o;xBPnf5I~R($fnD$ObTWNW!9qkcTkK;&q2ac
zh6_k8T*3miXsGbZnQ2=^6h9^?*{`|1eQe26oOv}8@0&U}up#+c4>ujpmt5BIZ5@;n
z-caH49VN~2jD3ecEP-pFknQwBM~)!p)qe$efS2MY<87vm_v7d3V%Xx=x=V^BzXUW_
ziel&N)c4cJq@!_7$`B}x&roF&PD;BX>k%GYbFNgywik|{_WFuT4qfaN#xdJ#4f{;0
zJk^mN&$&)BfBUfsxR6EKa9`-cGA5@(`7n%_4`R|2kpE_PN=gnd?>YG04onpgI$)J4
z2Q^LM9Ab=Mmcmqx^hvTK0`I+=0rOV*D9!B=%Xs&P$BSLm3%4Kof?nHU?7zlvlS!sl?M>yGyl7Tk8&^!~gDKbOo7+*~cpDUp^~sF=??4@dq-h9k
z^`)u^4BD`8oZ&l#+6`ffxNBgkW(@G?^feR=E$b72x-z@=G!%cr~H>;2JIHD)@M;QGX`e_HTE=FjdKWh2Owd_%p!QR#)6p|
z{&Eg#9E<7VP*A-|;dR^S&YQE6-HkoGp!R}wE{%^Pp>oLq>CBF6a9N2$o`!#zUtV^6
zr989si+f!iQBM!dsK#$*(NvWgL!}?`PXdA|aUT6VRqh_`ZiH;8_j2em@@7Xsw
zxx`3q>k0_VF%lP7G}n?!3zMjNX)=TPf+6?AWe@<+C)1=86=7`_;ER8A9V+jKXj
ztKQKN@0m%S?=T=7!Sg>t6VrErwcV=gXJg~n`uH+*#5fkNTX8rXkdjl12@5t8hk>Fu
z$_V%go5O*ueF@OR`II&dOQb~0Z=aU5EG9{l=n=+99n@ibNW(x37^@f4aZO2TMj}ZU
z8F240x0-;zayboPKhnf3n*49Tps;!j$bKPW66e`igSLsN4joFpAaS}-pbS0gg+D5M
zE=t@|&|QSPA^%hF@R#=FU=gGY&6DKN)(sE{Eq?ugL?y2`?=8o7dw>N9FLsG{Hl63b
zdwuu-dX8z}u-~IP0=x$A3+#8F+pd?0Pgt*fe1##U>(q+d%w;~_4=;J2RMd2xqSs3Q
z@HYX;$?1A`Kbkj9KFbQ8RjNyf%0*0G;fE$Eq!4Y60k#%0A~%NRUGZ?CQpwmAcOCa&
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
literal 0
HcmV?d00001
diff --git a/homeassistant/components/http/www_static/polymer/components/domain-icon.html b/homeassistant/components/http/www_static/polymer/components/domain-icon.html
index 1980be11d8c..85bbac938f2 100644
--- a/homeassistant/components/http/www_static/polymer/components/domain-icon.html
+++ b/homeassistant/components/http/www_static/polymer/components/domain-icon.html
@@ -62,6 +62,9 @@
case "sensor":
return "visibility";
+ case "configurator":
+ return "settings";
+
default:
return "bookmark-outline";
}
diff --git a/homeassistant/components/http/www_static/polymer/home-assistant-api.html b/homeassistant/components/http/www_static/polymer/home-assistant-api.html
index 2ac2bbcbd51..94be4e0629e 100644
--- a/homeassistant/components/http/www_static/polymer/home-assistant-api.html
+++ b/homeassistant/components/http/www_static/polymer/home-assistant-api.html
@@ -40,7 +40,7 @@
+
diff --git a/homeassistant/components/http/www_static/polymer/more-infos/more-info-content.html b/homeassistant/components/http/www_static/polymer/more-infos/more-info-content.html
index 5926812c844..233d7786afc 100644
--- a/homeassistant/components/http/www_static/polymer/more-infos/more-info-content.html
+++ b/homeassistant/components/http/www_static/polymer/more-infos/more-info-content.html
@@ -4,6 +4,7 @@
+
diff --git a/homeassistant/components/http/www_static/polymer/more-infos/more-info-default.html b/homeassistant/components/http/www_static/polymer/more-infos/more-info-default.html
index db067ab222f..e232beb666e 100644
--- a/homeassistant/components/http/www_static/polymer/more-infos/more-info-default.html
+++ b/homeassistant/components/http/www_static/polymer/more-infos/more-info-default.html
@@ -22,7 +22,7 @@
-
+
{{key}}
diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py
index 75a5f3767da..9b0f9eda162 100644
--- a/homeassistant/components/light/__init__.py
+++ b/homeassistant/components/light/__init__.py
@@ -57,7 +57,7 @@ import homeassistant.util as util
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
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
@@ -93,6 +93,7 @@ LIGHT_PROFILES_FILE = "light_profiles.csv"
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
wink.DISCOVER_LIGHTS: 'wink',
+ discovery.services.PHILIPS_HUE: 'hue',
}
_LOGGER = logging.getLogger(__name__)
@@ -168,8 +169,32 @@ def setup(hass, config):
return False
- lights = platform_devices_from_config(
- config, DOMAIN, hass, ENTITY_ID_FORMAT, _LOGGER)
+ # Dict to track entity_id -> lights
+ 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
def update_lights_state(now):
@@ -182,28 +207,12 @@ def setup(hass, config):
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):
""" Called when a light is discovered. """
platform = get_component(
- "{}.{}".format(DOMAIN, DISCOVERY_PLATFORMS[service]))
+ ENTITY_ID_FORMAT.format(DISCOVERY_PLATFORMS[service]))
- discovered = platform.devices_discovered(hass, config, 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())
+ platform.setup_platform(hass, {}, add_lights, info)
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(), light_discovered)
diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py
index 04291217eff..981a19f1301 100644
--- a/homeassistant/components/light/hue.py
+++ b/homeassistant/components/light/hue.py
@@ -2,7 +2,9 @@
import logging
import socket
from datetime import timedelta
+from urllib.parse import urlparse
+from homeassistant.loader import get_component
import homeassistant.util as util
from homeassistant.helpers import ToggleDevice
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"
-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. """
- logger = logging.getLogger(__name__)
try:
import phue
except ImportError:
- logger.exception("Error while importing dependency phue.")
+ _LOGGER.exception("Error while importing dependency phue.")
- return []
+ return
- host = config.get(CONF_HOST, None)
+ if discovery_info is not None:
+ host = urlparse(discovery_info).hostname
+ else:
+ 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:
bridge = phue.Bridge(
host, config_file_path=hass.get_config_path(PHUE_CONFIG_FILE))
- except socket.error: # Error connecting using Phue
- logger.exception((
- "Error while connecting to the bridge. "
- "Did you follow the instructions to set it up?"))
+ except ConnectionRefusedError: # Wrong host was given
+ _LOGGER.exception("Error connecting to the Hue bridge at %s", host)
- 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 = {}
@@ -47,25 +79,53 @@ def get_devices(hass, config):
api = bridge.get_api()
except socket.error:
# socket.error when we cannot reach Hue
- logger.exception("Cannot reach the bridge")
+ _LOGGER.exception("Cannot reach the bridge")
return
api_states = api.get('lights')
if not isinstance(api_states, dict):
- logger.error("Got unexpected result from Hue API")
+ _LOGGER.error("Got unexpected result from Hue API")
return
+ new_lights = []
+
for light_id, info in api_states.items():
if light_id not in lights:
lights[light_id] = HueLight(int(light_id), info,
bridge, update_lights)
+ new_lights.append(lights[light_id])
else:
lights[light_id].info = info
+ if new_lights:
+ add_devices_callback(new_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):
diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py
index ebfdc44fff1..e8dd3c1fc4d 100644
--- a/homeassistant/components/light/wink.py
+++ b/homeassistant/components/light/wink.py
@@ -10,30 +10,21 @@ from homeassistant.const import CONF_ACCESS_TOKEN
# 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. """
token = config.get(CONF_ACCESS_TOKEN)
- if token is None:
+ if not pywink.is_token_set() and token is None:
logging.getLogger(__name__).error(
"Missing wink access_token - "
"get one at https://winkbearertoken.appspot.com/")
- return []
+ return
- pywink.set_bearer_token(token)
+ elif token is not None:
+ pywink.set_bearer_token(token)
- return get_lights()
-
-
-# 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()]
+ add_devices_callback(
+ WinkLight(light) for light in pywink.get_bulbs())
class WinkLight(WinkToggleDevice):
diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py
index 5d772f9444a..5e1a2dd64a8 100644
--- a/homeassistant/components/switch/__init__.py
+++ b/homeassistant/components/switch/__init__.py
@@ -11,7 +11,7 @@ import homeassistant.util as util
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
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
DOMAIN = 'switch'
@@ -90,9 +90,8 @@ def setup(hass, config):
for switch in discovered:
if switch is not None and switch not in switches.values():
- switch.entity_id = util.ensure_unique_string(
- ENTITY_ID_FORMAT.format(util.slugify(switch.name)),
- switches.keys())
+ switch.entity_id = generate_entity_id(
+ ENTITY_ID_FORMAT, switch.name, switches.keys())
switches[switch.entity_id] = switch
diff --git a/homeassistant/external/wink/pywink.py b/homeassistant/external/wink/pywink.py
index 72a3353a77c..f0148c33dc8 100644
--- a/homeassistant/external/wink/pywink.py
+++ b/homeassistant/external/wink/pywink.py
@@ -389,6 +389,11 @@ def get_switches():
def get_sensors():
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):
global headers
diff --git a/homeassistant/helpers.py b/homeassistant/helpers.py
index e29cf03ed42..d2f5f20ee99 100644
--- a/homeassistant/helpers.py
+++ b/homeassistant/helpers.py
@@ -12,6 +12,17 @@ from homeassistant.const import (
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):
"""
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
name = "{} {}".format(domain, no_name_count)
- entity_id = ensure_unique_string(
- entity_id_format.format(slugify(name)),
- device_dict.keys())
+ entity_id = generate_entity_id(
+ entity_id_format, name, device_dict.keys())
device.entity_id = entity_id
device_dict[entity_id] = device
From cdbcc844cf3f538375fa8067351db70afcf4f9e7 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen
Date: Mon, 19 Jan 2015 21:39:24 -0800
Subject: [PATCH 05/13] Add tests, fix styling
---
homeassistant/components/configurator.py | 75 +++++++----
homeassistant/components/demo.py | 5 +-
.../polymer/home-assistant-api.html | 91 ++++++++++----
.../more-infos/more-info-configurator.html | 2 +-
homeassistant/components/light/hue.py | 11 +-
homeassistant/helpers.py | 1 +
tests/config/custom_components/light/test.py | 7 +-
tests/test_component_configurator.py | 118 ++++++++++++++++++
tests/test_component_light.py | 15 ++-
9 files changed, 265 insertions(+), 60 deletions(-)
create mode 100644 tests/test_component_configurator.py
diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py
index 8b465abb9e9..3ece10d4d40 100644
--- a/homeassistant/components/configurator.py
+++ b/homeassistant/components/configurator.py
@@ -1,7 +1,18 @@
+"""
+homeassistant.components.configurator
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A component to allow pieces of code to request configuration from the user.
+
+Initiate a request by calling the `request_config` method with a callback.
+This will return a request id that has to be used for future calls.
+A callback has to be provided to `request_config` which will be called when
+the user has submitted configuration information.
+"""
import logging
+import threading
from homeassistant.helpers import generate_entity_id
-from homeassistant.const import EVENT_TIME_CHANGED
DOMAIN = "configurator"
DEPENDENCIES = []
@@ -19,30 +30,50 @@ ATTR_SUBMIT_CAPTION = "submit_caption"
ATTR_FIELDS = "fields"
ATTR_ERRORS = "errors"
+_REQUESTS = {}
_INSTANCES = {}
_LOGGER = logging.getLogger(__name__)
+# pylint: disable=too-many-arguments
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(
+ instance = _get_instance(hass)
+
+ request_id = instance.request_config(
name, callback,
description, description_image, submit_caption, fields)
+ _REQUESTS[request_id] = instance
-def notify_errors(hass, request_id, error):
- _get_instance(hass).notify_errors(request_id, error)
+ return request_id
-def request_done(hass, request_id):
- _get_instance(hass).request_done(request_id)
+def notify_errors(request_id, error):
+ """ Add errors to a config request. """
+ try:
+ _REQUESTS[request_id].notify_errors(request_id, error)
+ except KeyError:
+ # If request_id does not exist
+ pass
+def request_done(request_id):
+ """ Mark a config request as done. """
+ try:
+ _REQUESTS.pop(request_id).request_done(request_id)
+ except KeyError:
+ # If request_id does not exist
+ pass
+
+
+# pylint: disable=unused-argument
def setup(hass, config):
+ """ Set up Configurator. """
return True
@@ -51,7 +82,6 @@ def _get_instance(hass):
try:
return _INSTANCES[hass]
except KeyError:
- print("Creating instance")
_INSTANCES[hass] = Configurator(hass)
if DOMAIN not in hass.components:
@@ -61,6 +91,10 @@ def _get_instance(hass):
class Configurator(object):
+ """
+ Class to keep track of current configuration requests.
+ """
+
def __init__(self, hass):
self.hass = hass
self._cur_id = 0
@@ -68,6 +102,7 @@ class Configurator(object):
hass.services.register(
DOMAIN, SERVICE_CONFIGURE, self.handle_service_call)
+ # pylint: disable=too-many-arguments
def request_config(
self, name, callback,
description, description_image, submit_caption, fields):
@@ -120,25 +155,26 @@ class Configurator(object):
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)
+ # If we remove the state right away, it will not be included with
+ # the result fo the service call (limitation current design).
+ # Instead we will set it to configured to give as feedback but delete
+ # it shortly after so that it is deleted when the client updates.
+ threading.Timer(
+ .001, lambda: self.hass.states.remove(entity_id)).start()
+
def handle_service_call(self, call):
+ """ Handle a configure service call. """
request_id = call.data.get(ATTR_CONFIGURE_ID)
if not self._validate_request_id(request_id):
return
+ # pylint: disable=unused-variable
entity_id, fields, callback = self._requests[request_id]
- # TODO field validation?
+ # field validation goes here?
callback(call.data.get(ATTR_FIELDS, {}))
@@ -148,8 +184,5 @@ class Configurator(object):
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
+ """ Validate that the request belongs to this instance. """
+ return request_id in self._requests
diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py
index 62dafffa9d6..cfb37895561 100644
--- a/homeassistant/components/demo.py
+++ b/homeassistant/components/demo.py
@@ -174,6 +174,7 @@ def setup(hass, config):
configurator_ids = []
+ # pylint: disable=unused-argument
def hue_configuration_callback(data):
""" Fake callback, mark config as done. """
time.sleep(2)
@@ -181,12 +182,12 @@ def setup(hass, config):
# First time it is called, pretend it failed.
if len(configurator_ids) == 1:
configurator.notify_errors(
- hass, configurator_ids[0],
+ configurator_ids[0],
"Failed to register, please try again.")
configurator_ids.append(0)
else:
- configurator.request_done(hass, configurator_ids[0])
+ configurator.request_done(configurator_ids[0])
request_id = configurator.request_config(
hass, "Philips Hue", hue_configuration_callback,
diff --git a/homeassistant/components/http/www_static/polymer/home-assistant-api.html b/homeassistant/components/http/www_static/polymer/home-assistant-api.html
index 94be4e0629e..aeffc74cf1a 100644
--- a/homeassistant/components/http/www_static/polymer/home-assistant-api.html
+++ b/homeassistant/components/http/www_static/polymer/home-assistant-api.html
@@ -137,6 +137,14 @@
},
// local methods
+ removeState: function(entityId) {
+ var state = this.getState(entityId);
+
+ if (state !== null) {
+ this.states.splice(this.states.indexOf(state), 1);
+ }
+ },
+
getState: function(entityId) {
var found = this.states.filter(function(state) {
return state.entity_id == entityId;
@@ -158,6 +166,11 @@
return states;
},
+ getEntityIDs: function() {
+ return this.states.map(
+ function(state) { return state.entity_id; });
+ },
+
hasService: function(domain, service) {
var found = this.services.filter(function(serv) {
return serv.domain == domain && serv.services.indexOf(service) !== -1;
@@ -179,8 +192,8 @@
this.stateUpdateTimeout = setTimeout(this.fetchStates.bind(this), 60000);
},
- _sortStates: function(states) {
- states.sort(function(one, two) {
+ _sortStates: function() {
+ this.states.sort(function(one, two) {
if (one.entity_id > two.entity_id) {
return 1;
} else if (one.entity_id < two.entity_id) {
@@ -191,32 +204,62 @@
});
},
+ /**
+ * Pushes a new state to the state machine.
+ * Will resort the states after a push and fire states-updated event.
+ */
_pushNewState: function(new_state) {
- var state;
- var stateFound = false;
-
- for(var i = 0; i < this.states.length; i++) {
- if(this.states[i].entity_id == new_state.entity_id) {
- state = this.states[i];
- state.attributes = new_state.attributes;
- state.last_changed = new_state.last_changed;
- state.state = new_state.state;
-
- stateFound = true;
- break;
- }
- }
-
- if(!stateFound) {
- this.states.push(new State(new_state, this));
- this._sortStates(this.states);
+ if (this.__pushNewState(new_state)) {
+ this._sortStates();
}
this.fire('states-updated');
},
- _pushNewStates: function(new_states) {
- new_states.forEach(this._pushNewState.bind(this));
+ /**
+ * Creates or updates a state. Returns if a new state was added.
+ */
+ __pushNewState: function(new_state) {
+ var curState = this.getState(new_state.entity_id);
+
+ if (curState === null) {
+ this.states.push(new State(new_state, this));
+
+ return true;
+ } else {
+ curState.attributes = new_state.attributes;
+ curState.last_changed = new_state.last_changed;
+ curState.state = new_state.state;
+
+ return false;
+ }
+ },
+
+ _pushNewStates: function(newStates, removeNonPresent) {
+ removeNonPresent = !!removeNonPresent;
+ var currentEntityIds = removeNonPresent ? this.getEntityIDs() : [];
+
+ var hasNew = newStates.reduce(function(hasNew, newState) {
+ var isNewState = this.__pushNewState(newState);
+
+ if (isNewState) {
+ return true;
+ } else if(removeNonPresent) {
+ currentEntityIds.splice(currentEntityIds.indexOf(newState.entity_id), 1);
+ }
+
+ return hasNew;
+ }.bind(this), false);
+
+ currentEntityIds.forEach(function(entityId) {
+ this.removeState(entityId);
+ }.bind(this));
+
+ if (hasNew) {
+ this._sortStates();
+ }
+
+ this.fire('states-updated');
},
// call api methods
@@ -236,9 +279,7 @@
fetchStates: function(onSuccess, onError) {
var successStatesUpdate = function(newStates) {
- this._pushNewStates(newStates);
-
- this.fire('states-updated');
+ this._pushNewStates(newStates, true);
this._laterFetchStates();
diff --git a/homeassistant/components/http/www_static/polymer/more-infos/more-info-configurator.html b/homeassistant/components/http/www_static/polymer/more-infos/more-info-configurator.html
index 517b1cec296..5b9ace3d13e 100644
--- a/homeassistant/components/http/www_static/polymer/more-infos/more-info-configurator.html
+++ b/homeassistant/components/http/www_static/polymer/more-infos/more-info-configurator.html
@@ -41,7 +41,7 @@
- Errors: {{stateObj.attributes.errors}}
+ {{stateObj.attributes.errors}}
diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py
index 981a19f1301..8bbe528c6c3 100644
--- a/homeassistant/components/light/hue.py
+++ b/homeassistant/components/light/hue.py
@@ -26,7 +26,8 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Gets the Hue lights. """
try:
- import phue
+ # pylint: disable=unused-variable
+ import phue # noqa
except ImportError:
_LOGGER.exception("Error while importing dependency phue.")
@@ -45,6 +46,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
def setup_bridge(host, hass, add_devices_callback):
+ """ Setup a phue bridge based on host parameter. """
import phue
try:
@@ -68,7 +70,7 @@ def setup_bridge(host, hass, add_devices_callback):
configurator = get_component('configurator')
- configurator.request_done(hass, request_id)
+ configurator.request_done(request_id)
lights = {}
@@ -108,13 +110,14 @@ 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
+ # We got an error if this method is called while we are configuring
if host in _CONFIGURING:
configurator.notify_errors(
- hass, _CONFIGURING[host], "Failed to register, please try again.")
+ _CONFIGURING[host], "Failed to register, please try again.")
return
+ # pylint: disable=unused-argument
def hue_configuration_callback(data):
""" Actions to do when our configuration callback is called. """
setup_bridge(host, hass, add_devices_callback)
diff --git a/homeassistant/helpers.py b/homeassistant/helpers.py
index d2f5f20ee99..4fbffaf1df7 100644
--- a/homeassistant/helpers.py
+++ b/homeassistant/helpers.py
@@ -13,6 +13,7 @@ from homeassistant.util import ensure_unique_string, slugify
def generate_entity_id(entity_id_format, name, current_ids=None, hass=None):
+ """ Generate a unique entity ID based on given entity IDs or used ids. """
if current_ids is None:
if hass is None:
raise RuntimeError("Missing required parameter currentids or hass")
diff --git a/tests/config/custom_components/light/test.py b/tests/config/custom_components/light/test.py
index 9b4ebcac2b1..c557fc8575a 100644
--- a/tests/config/custom_components/light/test.py
+++ b/tests/config/custom_components/light/test.py
@@ -24,6 +24,11 @@ def init(empty=False):
]
-def get_lights(hass, config):
+def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Returns mock devices. """
+ add_devices_callback(DEVICES)
+
+
+def get_lights():
+ """ Helper method to get current light objects. """
return DEVICES
diff --git a/tests/test_component_configurator.py b/tests/test_component_configurator.py
new file mode 100644
index 00000000000..a6b1ccf1acf
--- /dev/null
+++ b/tests/test_component_configurator.py
@@ -0,0 +1,118 @@
+"""
+tests.test_component_configurator
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Tests Configurator component.
+"""
+# pylint: disable=too-many-public-methods,protected-access
+import unittest
+import time
+
+import homeassistant as ha
+import homeassistant.components.configurator as configurator
+
+
+class TestConfigurator(unittest.TestCase):
+ """ Test the chromecast module. """
+
+ def setUp(self): # pylint: disable=invalid-name
+ self.hass = ha.HomeAssistant()
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """ Stop down stuff we started. """
+ self.hass.stop()
+
+ def test_request_least_info(self):
+ """ Test request config with least amount of data. """
+
+ request_id = configurator.request_config(
+ self.hass, "Test Request", lambda _: None)
+
+ self.assertEqual(
+ 1, len(self.hass.services.services.get(configurator.DOMAIN, [])),
+ "No new service registered")
+
+ states = self.hass.states.all()
+
+ self.assertEqual(1, len(states), "Expected a new state registered")
+
+ state = states[0]
+
+ self.assertEqual(configurator.STATE_CONFIGURE, state.state)
+ self.assertEqual(
+ request_id, state.attributes.get(configurator.ATTR_CONFIGURE_ID))
+
+ def test_request_all_info(self):
+ """ Test request config with all possible info. """
+
+ values = [
+ "config_description", "config image url",
+ "config submit caption", []]
+
+ keys = [
+ configurator.ATTR_DESCRIPTION, configurator.ATTR_DESCRIPTION_IMAGE,
+ configurator.ATTR_SUBMIT_CAPTION, configurator.ATTR_FIELDS]
+
+ exp_attr = dict(zip(keys, values))
+
+ exp_attr[configurator.ATTR_CONFIGURE_ID] = configurator.request_config(
+ self.hass, "Test Request", lambda _: None,
+ *values)
+
+ states = self.hass.states.all()
+
+ self.assertEqual(1, len(states))
+
+ state = states[0]
+
+ self.assertEqual(configurator.STATE_CONFIGURE, state.state)
+ self.assertEqual(exp_attr, state.attributes)
+
+ def test_callback_called_on_configure(self):
+ """ Test if our callback gets called when configure service called. """
+ calls = []
+
+ request_id = configurator.request_config(
+ self.hass, "Test Request", lambda _: calls.append(1))
+
+ self.hass.services.call(
+ configurator.DOMAIN, configurator.SERVICE_CONFIGURE,
+ {configurator.ATTR_CONFIGURE_ID: request_id})
+
+ self.hass.pool.block_till_done()
+
+ self.assertEqual(1, len(calls), "Callback not called")
+
+ def test_state_change_on_notify_errors(self):
+ """ Test state change on notify errors. """
+ request_id = configurator.request_config(
+ self.hass, "Test Request", lambda _: None)
+
+ error = "Oh no bad bad bad"
+
+ configurator.notify_errors(request_id, error)
+
+ state = self.hass.states.all()[0]
+
+ self.assertEqual(error, state.attributes.get(configurator.ATTR_ERRORS))
+
+ def test_notify_errors_fail_silently_on_bad_request_id(self):
+ """ Test if notify errors fails silently with a bad request id. """
+ configurator.notify_errors(2015, "Try this error")
+
+ def test_request_done_works(self):
+ """ Test if calling request done works. """
+ request_id = configurator.request_config(
+ self.hass, "Test Request", lambda _: None)
+
+ configurator.request_done(request_id)
+
+ self.assertEqual(1, len(self.hass.states.all()))
+
+ time.sleep(.02)
+
+ self.assertEqual(0, len(self.hass.states.all()))
+
+ def test_request_done_fail_silently_on_bad_request_id(self):
+ """ Test that request_done fails silently with a bad request id. """
+ configurator.request_done(2016)
diff --git a/tests/test_component_light.py b/tests/test_component_light.py
index c5db2f37299..c9264a37e40 100644
--- a/tests/test_component_light.py
+++ b/tests/test_component_light.py
@@ -8,7 +8,6 @@ Tests switch component.
import unittest
import os
-import homeassistant as ha
import homeassistant.loader as loader
import homeassistant.util as util
from homeassistant.const import (
@@ -104,7 +103,7 @@ class TestLight(unittest.TestCase):
self.assertTrue(
light.setup(self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}}))
- dev1, dev2, dev3 = platform.get_lights(None, None)
+ dev1, dev2, dev3 = platform.get_lights()
# Test init
self.assertTrue(light.is_on(self.hass, dev1.entity_id))
@@ -214,7 +213,7 @@ class TestLight(unittest.TestCase):
light.ATTR_XY_COLOR: [prof_x, prof_y]},
data)
- def test_light_profiles(self):
+ def test_broken_light_profiles(self):
""" Test light profiles. """
platform = loader.get_component('light.test')
platform.init()
@@ -230,8 +229,12 @@ class TestLight(unittest.TestCase):
self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}}
))
- # Clean up broken file
- os.remove(user_light_file)
+ def test_light_profiles(self):
+ """ Test light profiles. """
+ platform = loader.get_component('light.test')
+ platform.init()
+
+ user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE)
with open(user_light_file, 'w') as user_file:
user_file.write('id,x,y,brightness\n')
@@ -241,7 +244,7 @@ class TestLight(unittest.TestCase):
self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}}
))
- dev1, dev2, dev3 = platform.get_lights(None, None)
+ dev1, dev2, dev3 = platform.get_lights()
light.turn_on(self.hass, dev1.entity_id, profile='test')
From 2016da984aec01ae05b11387c87390263c40cbc7 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen
Date: Mon, 19 Jan 2015 22:40:30 -0800
Subject: [PATCH 06/13] Tweaks to the configurator UI
---
homeassistant/components/configurator.py | 17 ++++++-----
homeassistant/components/http/frontend.py | 2 +-
.../components/http/www_static/frontend.html | 13 +++++----
.../cards/state-card-configurator.html | 28 +++++++++++++++++++
.../polymer/cards/state-card-content.html | 1 +
.../polymer/dialogs/more-info-dialog.html | 1 -
.../polymer/home-assistant-api.html | 2 +-
.../more-infos/more-info-configurator.html | 6 +++-
tests/test_component_configurator.py | 6 ++--
9 files changed, 57 insertions(+), 19 deletions(-)
create mode 100644 homeassistant/components/http/www_static/polymer/cards/state-card-configurator.html
diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py
index 3ece10d4d40..2e43e9c54f6 100644
--- a/homeassistant/components/configurator.py
+++ b/homeassistant/components/configurator.py
@@ -10,9 +10,9 @@ A callback has to be provided to `request_config` which will be called when
the user has submitted configuration information.
"""
import logging
-import threading
from homeassistant.helpers import generate_entity_id
+from homeassistant.const import EVENT_TIME_CHANGED
DOMAIN = "configurator"
DEPENDENCIES = []
@@ -155,14 +155,17 @@ class Configurator(object):
entity_id = self._requests.pop(request_id)[0]
+ # If we remove the state right away, it will not be included with
+ # the result fo the service call (current design limitation).
+ # Instead, we will set it to configured to give as feedback but delete
+ # it shortly after so that it is deleted when the client updates.
self.hass.states.set(entity_id, STATE_CONFIGURED)
- # If we remove the state right away, it will not be included with
- # the result fo the service call (limitation current design).
- # Instead we will set it to configured to give as feedback but delete
- # it shortly after so that it is deleted when the client updates.
- threading.Timer(
- .001, lambda: self.hass.states.remove(entity_id)).start()
+ def deferred_remove(event):
+ """ Remove the request state. """
+ self.hass.states.remove(entity_id)
+
+ self.hass.bus.listen_once(EVENT_TIME_CHANGED, deferred_remove)
def handle_service_call(self, call):
""" Handle a configure service call. """
diff --git a/homeassistant/components/http/frontend.py b/homeassistant/components/http/frontend.py
index df97ffe708f..efddb978781 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 = "f299ce624d1641191f6f6a9b4b4d05bc"
+VERSION = "d44fb74a23d83756ed92d5d55f4d81ea"
diff --git a/homeassistant/components/http/www_static/frontend.html b/homeassistant/components/http/www_static/frontend.html
index 7129040444c..bf2aebfa5e5 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
{{label}}
-+
++
{{stateObj.entityDisplay}}
{{stateObj.relativeLastChanged}}
@@ -60,7 +60,8 @@ if(this.removeAttribute(a),d)return j(this,a,c);var e=c,f=m(this,a,e);return j(t
{{stateObj.stateDisplay}}
Currently: {{stateObj.attributes.current_temperature}} {{stateObj.attributes.unit_of_measurement}}
-
Home Assistant
Hi there!
+
{{stateObj.stateDisplay}}
Home Assistant
Hi there!
It looks like we have nothing to show you right now. It could be that we have not yet discovered all your devices but it is more likely that you have not configured Home Assistant yet.
Please see the Getting Started section on how to setup your devices.
@@ -83,13 +84,13 @@ if(this.removeAttribute(a),d)return j(this,a,c);var e=c,f=m(this,a,e);return j(t
Setting {{stateObj.attributes.next_setting | relativeHATime}}
{{stateObj.attributes.next_setting | HATimeStripDate}}
-
{{stateObj.attributes.description}}
- Errors: {{stateObj.attributes.errors}}
-
+ {{stateObj.attributes.errors}}
+
{{stateObj.attributes.submit_caption || "Set configuration"}}
- Configuring…
Debug Dismiss
+ Configuring…
Debug Dismiss
:host {
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
diff --git a/homeassistant/components/http/www_static/polymer/cards/state-card-configurator.html b/homeassistant/components/http/www_static/polymer/cards/state-card-configurator.html
new file mode 100644
index 00000000000..19d0f6340c1
--- /dev/null
+++ b/homeassistant/components/http/www_static/polymer/cards/state-card-configurator.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
{{stateObj.stateDisplay}}
+
+
+
+
+
+
+
+
diff --git a/homeassistant/components/http/www_static/polymer/cards/state-card-content.html b/homeassistant/components/http/www_static/polymer/cards/state-card-content.html
index e0335394df1..c27d5f07e8b 100644
--- a/homeassistant/components/http/www_static/polymer/cards/state-card-content.html
+++ b/homeassistant/components/http/www_static/polymer/cards/state-card-content.html
@@ -3,6 +3,7 @@
+
diff --git a/homeassistant/components/http/www_static/polymer/dialogs/more-info-dialog.html b/homeassistant/components/http/www_static/polymer/dialogs/more-info-dialog.html
index f89cee05e62..bdc77b394ee 100644
--- a/homeassistant/components/http/www_static/polymer/dialogs/more-info-dialog.html
+++ b/homeassistant/components/http/www_static/polymer/dialogs/more-info-dialog.html
@@ -36,7 +36,6 @@ Polymer({
/**
* Whenever the attributes change, the more info component can
* hide or show elements. We will reposition the dialog.
- * DISABLED FOR NOW - BAD UX
*/
reposition: function(oldVal, newVal) {
// Only resize if already open
diff --git a/homeassistant/components/http/www_static/polymer/home-assistant-api.html b/homeassistant/components/http/www_static/polymer/home-assistant-api.html
index aeffc74cf1a..901b0e444ac 100644
--- a/homeassistant/components/http/www_static/polymer/home-assistant-api.html
+++ b/homeassistant/components/http/www_static/polymer/home-assistant-api.html
@@ -39,7 +39,7 @@