From 80f1581d6e0460ff1773855c41e7005a07cf7454 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sat, 4 Apr 2015 04:33:03 -0400 Subject: [PATCH 01/25] Added ISY994 weather data as sensors. --- homeassistant/components/isy994.py | 73 ++++++++++++++ homeassistant/components/sensor/__init__.py | 3 +- homeassistant/components/sensor/isy994.py | 100 ++++++++++++++++++++ 3 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/isy994.py create mode 100644 homeassistant/components/sensor/isy994.py diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py new file mode 100644 index 00000000000..7a556d565e3 --- /dev/null +++ b/homeassistant/components/isy994.py @@ -0,0 +1,73 @@ +""" +Connects to an ISY-994 controller and loads relevant components to control its devices. +""" +# system imports +import logging +from urllib.parse import urlparse + +# addon library imports +import PyISY + +# homeassistant imports +from homeassistant import bootstrap +from homeassistant.loader import get_component +from homeassistant.helpers import validate_config +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.const import ( + CONF_HOST, CONF_USERNAME, CONF_PASSWORD, + EVENT_PLATFORM_DISCOVERED, + ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME) + +# homeassistant constants +DOMAIN = "isy994" +DEPENDENCIES = [] +#DISCOVER_LIGHTS = "isy994.lights" +#DISCOVER_SWITCHES = "isy994.switches" +DISCOVER_SENSORS = "isy994.sensors" +ISY = None + +def setup(hass, config): + """ Sets up the ISY994 component. """ + logger = logging.getLogger(__name__) + logger.setLevel(logging.DEBUG) + + # pull values from configuration file + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, logger): + return False + else: + user = config[DOMAIN][CONF_USERNAME] + password = config[DOMAIN][CONF_PASSWORD] + host = urlparse(config[DOMAIN][CONF_HOST]) + addr = host.geturl() + if host.scheme == 'http': + addr = addr.replace('http://', '') + https = False + elif host.scheme == 'https': + addr = addr.replace('https://', '') + https = True + else: + logger.error('isy994 host value in configuration ' + + 'file is invalid.') + return False + port = host.port + addr = addr.replace(':{}'.format(port), '') + + # connect to ISY controller + global ISY + ISY = PyISY.ISY(addr, port, user, password, use_https=https, log=logger) + if not ISY.connected: + return False + + # Load components for the devices in the ISY controller that we support + for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),)): + component = get_component(comp_name) + bootstrap.setup_component(hass, component.DOMAIN, config) + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, + { + ATTR_SERVICE: discovery, + ATTR_DISCOVERED: {} + }) + + ISY.auto_update = True + return True diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 8248651710a..5cbd07d0e59 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -6,7 +6,7 @@ Component to interface with various sensors that can be monitored. import logging from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.components import wink, zwave +from homeassistant.components import wink, zwave, isy994 DOMAIN = 'sensor' DEPENDENCIES = [] @@ -18,6 +18,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' DISCOVERY_PLATFORMS = { wink.DISCOVER_SENSORS: 'wink', zwave.DISCOVER_SENSORS: 'zwave', + isy994.DISCOVER_SENSORS: 'isy994' } diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py new file mode 100644 index 00000000000..66f5eb84fab --- /dev/null +++ b/homeassistant/components/sensor/isy994.py @@ -0,0 +1,100 @@ +""" Support for ISY994 sensors. """ +# system imports +import logging + +# homeassistant imports +from ..isy994 import ISY +from homeassistant.helpers.entity import Entity +from homeassistant.const import STATE_OPEN, STATE_CLOSED + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the isy994 platform. """ + logger = logging.getLogger(__name__) + devs = [] + # verify connection + if ISY is None or not ISY.connected: + logger.error('A connection has not been made to the ISY controller.') + return False + + # import weather + if ISY.climate is not None: + for prop in ISY.climate._id2name: + if prop is not None: + devs.append(ISYSensorDevice('ISY.weather.' + prop, prop, + getattr(ISY.climate, prop), + getattr(ISY.climate, prop + '_units'))) + + + add_devices(devs) + + +class ISYSensorDevice(Entity): + """ represents a isy sensor within home assistant. """ + + domain = 'sensor' + + def __init__(self, device_id, name, source, units=None): + # setup properties + self._id = device_id + self._name = name + self.entity_id = self.domain + '.' + self.name.replace(' ', '_') + self._source = source + self._units = units + + # track changes + self._changeHandler = self._source.subscribe('changed', self.onUpdate) + + def __del__(self): + self._changeHandler.unsubscribe() + + @property + def should_poll(self): + return False + + @property + def dtype(self): + return 'binary' if self._units is None else 'analog' + + @property + def state(self): + """ Returns the state. """ + if self.dtype is 'binary': + return STATE_OPEN if self.is_open >= 255 else STATE_CLOSED + else: + return self.value + + @property + def state_attributes(self): + return {} + + @property + def unit_of_measurement(self): + return self._units + + @property + def unique_id(self): + """ Returns the id of this isy sensor """ + return self._id + + @property + def name(self): + """ Returns the name of the sensor if any. """ + return self._name + + def update(self): + """ Update state of the sensor. """ + # ISY objects are automatically updated by the ISY's event stream + pass + + @property + def is_open(self): + """ True if door is open. """ + return self.value >= 255 + + @property + def value(self): + return self._source._val + + def onUpdate(self, e): + self.update_ha_state() From 57f27cc97afdc416d85014ded14b34ba85e3de72 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sat, 4 Apr 2015 06:13:27 -0400 Subject: [PATCH 02/25] Addded light controls to isy994 component. --- homeassistant/components/isy994.py | 5 +- homeassistant/components/light/__init__.py | 3 +- homeassistant/components/light/isy994.py | 93 ++++++++++++++++++++++ 3 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/light/isy994.py diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 7a556d565e3..84299cef6a6 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -21,7 +21,7 @@ from homeassistant.const import ( # homeassistant constants DOMAIN = "isy994" DEPENDENCIES = [] -#DISCOVER_LIGHTS = "isy994.lights" +DISCOVER_LIGHTS = "isy994.lights" #DISCOVER_SWITCHES = "isy994.switches" DISCOVER_SENSORS = "isy994.sensors" ISY = None @@ -60,7 +60,8 @@ def setup(hass, config): return False # Load components for the devices in the ISY controller that we support - for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),)): + for comp_name, discovery in ((('sensor', DISCOVER_SENSORS), + ('light', DISCOVER_LIGHTS))): component = get_component(comp_name) bootstrap.setup_component(hass, component.DOMAIN, config) hass.bus.fire(EVENT_PLATFORM_DISCOVERED, diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 16020f1ecb1..ca887b97a19 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -57,7 +57,7 @@ from homeassistant.helpers.entity_component import EntityComponent import homeassistant.util as util from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) -from homeassistant.components import group, discovery, wink +from homeassistant.components import group, discovery, wink, isy994 DOMAIN = "light" @@ -92,6 +92,7 @@ LIGHT_PROFILES_FILE = "light_profiles.csv" # Maps discovered services to their platforms DISCOVERY_PLATFORMS = { wink.DISCOVER_LIGHTS: 'wink', + isy994.DISCOVER_LIGHTS: 'isy994', discovery.services.PHILIPS_HUE: 'hue', } diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py new file mode 100644 index 00000000000..3f498955487 --- /dev/null +++ b/homeassistant/components/light/isy994.py @@ -0,0 +1,93 @@ +""" Support for ISY994 sensors. """ +# system imports +import logging + +# homeassistant imports +from ..isy994 import ISY +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.components.light import ATTR_BRIGHTNESS + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the isy994 platform. """ + print('************ RUNNING') + logger = logging.getLogger(__name__) + devs = [] + # verify connection + if ISY is None or not ISY.connected: + logger.error('A connection has not been made to the ISY controller.') + return False + + # import dimmable nodes + for node in ISY.nodes: + if node.dimmable: + devs.append(ISYLightDevice(node)) + + add_devices(devs) + + +class ISYLightDevice(ToggleEntity): + """ represents as isy light within home assistant. """ + + domain = 'light' + + def __init__(self, node): + # setup properties + self.node = node + #self.entity_id = self.domain + '.' + self.name.replace(' ', '_') + + # track changes + self._changeHandler = self.node.status. \ + subscribe('changed', self.onUpdate) + + def __del__(self): + self._changeHandler.unsubscribe() + + @property + def should_poll(self): + return False + + @property + def dtype(self): + return 'analog' + + @property + def value(self): + """ return the integer setting of the light (brightness) """ + return self.node.status._val + + @property + def is_on(self): + return self.value > 0 + + @property + def state_attributes(self): + return {ATTR_BRIGHTNESS: self.value} + + @property + def unique_id(self): + """ Returns the id of this isy sensor """ + return self.node._id + + @property + def name(self): + """ Returns the name of the sensor if any. """ + return self.node.name + + def update(self): + """ Update state of the sensor. """ + # ISY objects are automatically updated by the ISY's event stream + pass + + def onUpdate(self, e): + self.update_ha_state() + + def turn_on(self, **kwargs): + """ turns the device on """ + brightness = kwargs.get(ATTR_BRIGHTNESS) + self.node.on(brightness) + + def turn_off(self, **kwargs): + """ turns the device off """ + self.node.off() From f6d75f2db2a2dc32b1cc9acafa3747f3acce8e87 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 12 Apr 2015 16:45:23 -0400 Subject: [PATCH 03/25] Cleaned up ISY994 light and sensor code to use the same abstract class. --- homeassistant/components/isy994.py | 132 +++++++++++++++++++--- homeassistant/components/light/isy994.py | 76 ++----------- homeassistant/components/sensor/isy994.py | 89 +++------------ 3 files changed, 141 insertions(+), 156 deletions(-) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 84299cef6a6..fe5d5516cb6 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -1,5 +1,6 @@ """ -Connects to an ISY-994 controller and loads relevant components to control its devices. +Connects to an ISY-994 controller and loads relevant components to control its +devices. Also contains the base classes for ISY Sensors, Lights, and Switches. """ # system imports import logging @@ -14,26 +15,27 @@ from homeassistant.loader import get_component from homeassistant.helpers import validate_config from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( - CONF_HOST, CONF_USERNAME, CONF_PASSWORD, - EVENT_PLATFORM_DISCOVERED, + CONF_HOST, CONF_USERNAME, CONF_PASSWORD, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME) # homeassistant constants DOMAIN = "isy994" DEPENDENCIES = [] DISCOVER_LIGHTS = "isy994.lights" -#DISCOVER_SWITCHES = "isy994.switches" +# DISCOVER_SWITCHES = "isy994.switches" DISCOVER_SENSORS = "isy994.sensors" ISY = None -def setup(hass, config): - """ Sets up the ISY994 component. """ - logger = logging.getLogger(__name__) - logger.setLevel(logging.DEBUG) +# setup logger +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +def setup(hass, config): # pull values from configuration file - if not validate_config(config, - {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, logger): + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, + logger): return False else: user = config[DOMAIN][CONF_USERNAME] @@ -47,8 +49,8 @@ def setup(hass, config): addr = addr.replace('https://', '') https = True else: - logger.error('isy994 host value in configuration ' + - 'file is invalid.') + logger.error('isy994 host value in configuration ' + + 'file is invalid.') return False port = host.port addr = addr.replace(':{}'.format(port), '') @@ -64,11 +66,107 @@ def setup(hass, config): ('light', DISCOVER_LIGHTS))): component = get_component(comp_name) bootstrap.setup_component(hass, component.DOMAIN, config) - hass.bus.fire(EVENT_PLATFORM_DISCOVERED, - { - ATTR_SERVICE: discovery, - ATTR_DISCOVERED: {} - }) + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, + {ATTR_SERVICE: discovery, + ATTR_DISCOVERED: {}}) ISY.auto_update = True return True + + +class ISYDeviceABC(ToggleEntity): + """ Abstract Class for an ISY device within home assistant. """ + + _attrs = {} + _onattrs = [] + _states = [] + _dtype = None + _domain = None + + def __init__(self, node): + # setup properties + self.node = node + + # track changes + self._changeHandler = self.node.status. \ + subscribe('changed', self.onUpdate) + + def __del__(self): + """ cleanup subscriptions because it is the right thing to do. """ + self._changeHandler.unsubscribe() + + @property + def domain(self): + return self._domain + + @property + def dtype(self): + if self._dtype in ['analog', 'binary']: + return self._dtype + return 'binary' if self._units is None else 'analog' + + @property + def should_poll(self): + return False + + @property + def value(self): + """ returns the unclean value from the controller """ + return self.node.status._val + + @property + def state_attributes(self): + attr = {ATTR_FRIENDLY_NAME: self.name} + for name, prop in self._attrs.items(): + attr[name] = getattr(self, prop) + return attr + + @property + def unique_id(self): + """ Returns the id of this isy sensor """ + return self.node._id + + @property + def name(self): + """ Returns the name of the node if any. """ + return self.node.name + + def update(self): + """ Update state of the sensor. """ + # ISY objects are automatically updated by the ISY's event stream + pass + + def onUpdate(self, e): + """ Handles the update recieved event. """ + self.update_ha_state() + + @property + def is_on(self): + return self.value > 0 + + @property + def is_open(self): + return self.is_on + + @property + def state(self): + """ Returns the state of the node. """ + if len(self._states) > 0: + return self._states[0] if self.is_on else self._states[1] + return self.value + + def turn_on(self, **kwargs): + """ turns the device on """ + attrs = [kwargs.get(name) for name in self._onattrs] + self.node.on(*attrs) + + def turn_off(self, **kwargs): + """ turns the device off """ + self.node.off() + + @property + def unit_of_measurement(self): + try: + return self.node.units + except AttributeError: + return None diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index 3f498955487..16d7f6b052c 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -1,17 +1,15 @@ -""" Support for ISY994 sensors. """ +""" Support for ISY994 lights. """ # system imports import logging # homeassistant imports -from ..isy994 import ISY -from homeassistant.helpers.entity import ToggleEntity -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.components.isy994 import ISYDeviceABC, ISY from homeassistant.components.light import ATTR_BRIGHTNESS +from homeassistant.const import STATE_ON, STATE_OFF def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the isy994 platform. """ - print('************ RUNNING') logger = logging.getLogger(__name__) devs = [] # verify connection @@ -27,67 +25,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devs) -class ISYLightDevice(ToggleEntity): +class ISYLightDevice(ISYDeviceABC): """ represents as isy light within home assistant. """ - domain = 'light' - - def __init__(self, node): - # setup properties - self.node = node - #self.entity_id = self.domain + '.' + self.name.replace(' ', '_') - - # track changes - self._changeHandler = self.node.status. \ - subscribe('changed', self.onUpdate) - - def __del__(self): - self._changeHandler.unsubscribe() - - @property - def should_poll(self): - return False - - @property - def dtype(self): - return 'analog' - - @property - def value(self): - """ return the integer setting of the light (brightness) """ - return self.node.status._val - - @property - def is_on(self): - return self.value > 0 - - @property - def state_attributes(self): - return {ATTR_BRIGHTNESS: self.value} - - @property - def unique_id(self): - """ Returns the id of this isy sensor """ - return self.node._id - - @property - def name(self): - """ Returns the name of the sensor if any. """ - return self.node.name - - def update(self): - """ Update state of the sensor. """ - # ISY objects are automatically updated by the ISY's event stream - pass - - def onUpdate(self, e): - self.update_ha_state() - - def turn_on(self, **kwargs): - """ turns the device on """ - brightness = kwargs.get(ATTR_BRIGHTNESS) - self.node.on(brightness) - - def turn_off(self, **kwargs): - """ turns the device off """ - self.node.off() + _domain = 'light' + _dtype = 'analog' + _attrs = {ATTR_BRIGHTNESS: 'value'} + _onattrs = [ATTR_BRIGHTNESS] + _states = [STATE_ON, STATE_OFF] diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index 66f5eb84fab..3d007724219 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -3,7 +3,7 @@ import logging # homeassistant imports -from ..isy994 import ISY +from homeassistant.components.isy994 import ISY, ISYDeviceABC from homeassistant.helpers.entity import Entity from homeassistant.const import STATE_OPEN, STATE_CLOSED @@ -21,80 +21,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if ISY.climate is not None: for prop in ISY.climate._id2name: if prop is not None: - devs.append(ISYSensorDevice('ISY.weather.' + prop, prop, - getattr(ISY.climate, prop), - getattr(ISY.climate, prop + '_units'))) - + node = WeatherPseudoNode('ISY.weather.' + prop, prop, + getattr(ISY.climate, prop), + getattr(ISY.climate, prop + '_units')) + devs.append(ISYSensorDevice(node)) add_devices(devs) -class ISYSensorDevice(Entity): +class WeatherPseudoNode(object): + """ This class allows weather variable to act as regular nodes. """ + + def __init__(self, device_id, name, status, units=None): + self._id = device_id + self.name = name + self.status = status + self.units = units + + +class ISYSensorDevice(ISYDeviceABC): """ represents a isy sensor within home assistant. """ - domain = 'sensor' - - def __init__(self, device_id, name, source, units=None): - # setup properties - self._id = device_id - self._name = name - self.entity_id = self.domain + '.' + self.name.replace(' ', '_') - self._source = source - self._units = units - - # track changes - self._changeHandler = self._source.subscribe('changed', self.onUpdate) - - def __del__(self): - self._changeHandler.unsubscribe() - - @property - def should_poll(self): - return False - - @property - def dtype(self): - return 'binary' if self._units is None else 'analog' - - @property - def state(self): - """ Returns the state. """ - if self.dtype is 'binary': - return STATE_OPEN if self.is_open >= 255 else STATE_CLOSED - else: - return self.value - - @property - def state_attributes(self): - return {} - - @property - def unit_of_measurement(self): - return self._units - - @property - def unique_id(self): - """ Returns the id of this isy sensor """ - return self._id - - @property - def name(self): - """ Returns the name of the sensor if any. """ - return self._name - - def update(self): - """ Update state of the sensor. """ - # ISY objects are automatically updated by the ISY's event stream - pass - - @property - def is_open(self): - """ True if door is open. """ - return self.value >= 255 - - @property - def value(self): - return self._source._val - - def onUpdate(self, e): - self.update_ha_state() + _domain = 'sensor' From 4e3ccfffbb55ade14ef85277587f2ce5959e1049 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 12 Apr 2015 17:18:14 -0400 Subject: [PATCH 04/25] Added not dimming Insteon devices on an ISY controller as switches. --- homeassistant/components/isy994.py | 19 ++++++++---- homeassistant/components/switch/__init__.py | 3 +- homeassistant/components/switch/isy994.py | 33 +++++++++++++++++++++ 3 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/switch/isy994.py diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index fe5d5516cb6..489df94c22e 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -22,7 +22,7 @@ from homeassistant.const import ( DOMAIN = "isy994" DEPENDENCIES = [] DISCOVER_LIGHTS = "isy994.lights" -# DISCOVER_SWITCHES = "isy994.switches" +DISCOVER_SWITCHES = "isy994.switches" DISCOVER_SENSORS = "isy994.sensors" ISY = None @@ -63,7 +63,8 @@ def setup(hass, config): # Load components for the devices in the ISY controller that we support for comp_name, discovery in ((('sensor', DISCOVER_SENSORS), - ('light', DISCOVER_LIGHTS))): + ('light', DISCOVER_LIGHTS), + ('switch', DISCOVER_SWITCHES))): component = get_component(comp_name) bootstrap.setup_component(hass, component.DOMAIN, config) hass.bus.fire(EVENT_PLATFORM_DISCOVERED, @@ -137,7 +138,7 @@ class ISYDeviceABC(ToggleEntity): pass def onUpdate(self, e): - """ Handles the update recieved event. """ + """ Handles the update received event. """ self.update_ha_state() @property @@ -157,12 +158,18 @@ class ISYDeviceABC(ToggleEntity): def turn_on(self, **kwargs): """ turns the device on """ - attrs = [kwargs.get(name) for name in self._onattrs] - self.node.on(*attrs) + if self.domain is not 'sensor': + attrs = [kwargs.get(name) for name in self._onattrs] + self.node.on(*attrs) + else: + logger.error('ISY cannot turn on sensors.') def turn_off(self, **kwargs): """ turns the device off """ - self.node.off() + if self.domain is not 'sensor': + self.node.off() + else: + logger.error('ISY cannot turn off sensors.') @property def unit_of_measurement(self): diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index f246692560d..359839a3946 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -10,7 +10,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) -from homeassistant.components import group, discovery, wink +from homeassistant.components import group, discovery, wink, isy994 DOMAIN = 'switch' DEPENDENCIES = [] @@ -30,6 +30,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) DISCOVERY_PLATFORMS = { discovery.services.BELKIN_WEMO: 'wemo', wink.DISCOVER_SWITCHES: 'wink', + isy994.DISCOVER_SWITCHES: 'isy994', } _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py new file mode 100644 index 00000000000..ae4f7b552e4 --- /dev/null +++ b/homeassistant/components/switch/isy994.py @@ -0,0 +1,33 @@ +""" Support for ISY994 lights. """ +# system imports +import logging + +# homeassistant imports +from homeassistant.components.isy994 import ISY, ISYDeviceABC +from homeassistant.const import STATE_ON, STATE_OFF + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the isy994 platform. """ + logger = logging.getLogger(__name__) + devs = [] + # verify connection + if ISY is None or not ISY.connected: + logger.error('A connection has not been made to the ISY controller.') + return False + + # import not dimmable nodes and groups + for node in ISY.nodes: + if not node.dimmable: + devs.append(ISYSwitchDevice(node)) + # import ISY programs + + add_devices(devs) + + +class ISYSwitchDevice(ISYDeviceABC): + """ represents as isy light within home assistant. """ + + _domain = 'switch' + _dtype = 'binary' + _states = [STATE_ON, STATE_OFF] From 510064d9c84b042700eedfb05304e8f1fb538ddb Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 12 Apr 2015 22:30:14 -0400 Subject: [PATCH 05/25] Added support for the creation of custom switches using the ISY994 device. --- homeassistant/components/isy994.py | 7 +++- homeassistant/components/switch/isy994.py | 49 ++++++++++++++++++++++- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 489df94c22e..6c4a431c100 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -130,7 +130,10 @@ class ISYDeviceABC(ToggleEntity): @property def name(self): """ Returns the name of the node if any. """ - return self.node.name + try: + return self._name + except AttributeError: + return self.node.name def update(self): """ Update state of the sensor. """ @@ -143,7 +146,7 @@ class ISYDeviceABC(ToggleEntity): @property def is_on(self): - return self.value > 0 + return bool(self.value) @property def is_open(self): diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py index ae4f7b552e4..c33e0666fd3 100644 --- a/homeassistant/components/switch/isy994.py +++ b/homeassistant/components/switch/isy994.py @@ -4,7 +4,10 @@ import logging # homeassistant imports from homeassistant.components.isy994 import ISY, ISYDeviceABC -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF # STATE_OPEN, STATE_CLOSED +# The frontend doesn't seem to fully support the open and closed states yet. +# Once it does, the HA.doors programs should report open and closed instead of +# off and on. It appears that on should be open and off should be closed. def setup_platform(hass, config, add_devices, discovery_info=None): @@ -20,7 +23,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for node in ISY.nodes: if not node.dimmable: devs.append(ISYSwitchDevice(node)) - # import ISY programs + + # import ISY doors programs + for folder_name, states in (('HA.doors', [STATE_ON, STATE_OFF]), + ('HA.switches', [STATE_ON, STATE_OFF])): + try: + folder = ISY.programs['My Programs'][folder_name] + except KeyError: + # HA.doors folder does not exist + pass + else: + for dtype, name, node_id in folder.children: + if dtype is 'folder': + custom_switch = folder[node_id] + try: + actions = custom_switch['actions'].leaf + assert actions.dtype == 'program', 'Not a program' + node = custom_switch['status'].leaf + except (KeyError, AssertionError): + pass + else: + devs.append(ISYProgramDevice(name, node, actions, + states)) add_devices(devs) @@ -31,3 +55,24 @@ class ISYSwitchDevice(ISYDeviceABC): _domain = 'switch' _dtype = 'binary' _states = [STATE_ON, STATE_OFF] + + +class ISYProgramDevice(ISYSwitchDevice): + """ represents a door that can be manipulated within home assistant. """ + + _domain = 'switch' + _dtype = 'binary' + + def __init__(self, name, node, actions, states): + super().__init__(node) + self._states = states + self._name = name + self.action_node = actions + + def turn_on(self, **kwargs): + """ turns the device on/closes the device """ + self.action_node.runThen() + + def turn_off(self, **kwargs): + """ turns the device off/opens the device """ + self.action_node.runElse() From 0e9a8a7cc2f06f0f5a7cf637cd6f1a44aa971cce Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Mon, 13 Apr 2015 01:47:32 -0400 Subject: [PATCH 06/25] Added custom program sensors to the isy994 component. --- homeassistant/components/sensor/isy994.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index 3d007724219..a05257f2c3b 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -4,8 +4,8 @@ import logging # homeassistant imports from homeassistant.components.isy994 import ISY, ISYDeviceABC -from homeassistant.helpers.entity import Entity -from homeassistant.const import STATE_OPEN, STATE_CLOSED +from homeassistant.const import (STATE_OPEN, STATE_CLOSED, STATE_HOME, + STATE_NOT_HOME, STATE_ON, STATE_OFF) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -26,6 +26,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None): getattr(ISY.climate, prop + '_units')) devs.append(ISYSensorDevice(node)) + # import sensor programs + for (folder_name, states) in ( + ('HA.locations', [STATE_HOME, STATE_NOT_HOME]), + ('HA.sensors', [STATE_OPEN, STATE_CLOSED]), + ('HA.states', [STATE_ON, STATE_OFF])): + try: + folder = ISY.programs['My Programs'][folder_name] + except KeyError: + # folder does not exist + pass + else: + for dtype, name, node_id in folder.children: + node = folder[node_id].leaf + devs.append(ISYSensorDevice(node, states)) + add_devices(devs) @@ -43,3 +58,7 @@ class ISYSensorDevice(ISYDeviceABC): """ represents a isy sensor within home assistant. """ _domain = 'sensor' + + def __init__(self, node, states=[]): + super().__init__(node) + self._states = states From 83aea10f06101c7dc6cf7f232cd4b413d2ebbc14 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Mon, 13 Apr 2015 12:56:37 -0400 Subject: [PATCH 07/25] Added hidden_string and sensor_string properties to the isy994 configuration to allow nodes to be hidden and to be handled as sensors. Implimented the sensor_string. Any node name that contains the sensor_string in its name will be treated as a sensor instead of a switch or light. The hidden_string will be implimented later. --- homeassistant/components/isy994.py | 42 ++++++++++++++--------- homeassistant/components/light/isy994.py | 4 +-- homeassistant/components/sensor/isy994.py | 7 +++- homeassistant/components/switch/isy994.py | 4 +-- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 6c4a431c100..7fa0524d32f 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -25,6 +25,8 @@ DISCOVER_LIGHTS = "isy994.lights" DISCOVER_SWITCHES = "isy994.switches" DISCOVER_SENSORS = "isy994.sensors" ISY = None +SENSOR_STRING = 'Sensor' +HIDDEN_STRING = '{HIDE ME}' # setup logger logger = logging.getLogger(__name__) @@ -32,28 +34,34 @@ logger.setLevel(logging.DEBUG) def setup(hass, config): - # pull values from configuration file + # check for required values in configuration file if not validate_config(config, {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, logger): return False + + # pull and parse standard configuration + user = config[DOMAIN][CONF_USERNAME] + password = config[DOMAIN][CONF_PASSWORD] + host = urlparse(config[DOMAIN][CONF_HOST]) + addr = host.geturl() + if host.scheme == 'http': + addr = addr.replace('http://', '') + https = False + elif host.scheme == 'https': + addr = addr.replace('https://', '') + https = True else: - user = config[DOMAIN][CONF_USERNAME] - password = config[DOMAIN][CONF_PASSWORD] - host = urlparse(config[DOMAIN][CONF_HOST]) - addr = host.geturl() - if host.scheme == 'http': - addr = addr.replace('http://', '') - https = False - elif host.scheme == 'https': - addr = addr.replace('https://', '') - https = True - else: - logger.error('isy994 host value in configuration ' + - 'file is invalid.') - return False - port = host.port - addr = addr.replace(':{}'.format(port), '') + logger.error('isy994 host value in configuration file is invalid.') + return False + port = host.port + addr = addr.replace(':{}'.format(port), '') + + # pull and parse optional configuration + global SENSOR_STRING + global HIDDEN_STRING + SENSOR_STRING = config[DOMAIN].get('sensor_string', SENSOR_STRING) + HIDDEN_STRING = config[DOMAIN].get('hidden_string', HIDDEN_STRING) # connect to ISY controller global ISY diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index 16d7f6b052c..60a4faafe13 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -3,7 +3,7 @@ import logging # homeassistant imports -from homeassistant.components.isy994 import ISYDeviceABC, ISY +from homeassistant.components.isy994 import ISYDeviceABC, ISY, SENSOR_STRING from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import STATE_ON, STATE_OFF @@ -19,7 +19,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # import dimmable nodes for node in ISY.nodes: - if node.dimmable: + if node.dimmable and SENSOR_STRING not in node.name: devs.append(ISYLightDevice(node)) add_devices(devs) diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index a05257f2c3b..aa98c594910 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -3,7 +3,7 @@ import logging # homeassistant imports -from homeassistant.components.isy994 import ISY, ISYDeviceABC +from homeassistant.components.isy994 import ISY, ISYDeviceABC, SENSOR_STRING from homeassistant.const import (STATE_OPEN, STATE_CLOSED, STATE_HOME, STATE_NOT_HOME, STATE_ON, STATE_OFF) @@ -26,6 +26,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): getattr(ISY.climate, prop + '_units')) devs.append(ISYSensorDevice(node)) + # import sensor nodes + for node in ISY.nodes: + if SENSOR_STRING in node.name: + devs.append(ISYSensorDevice(node, [STATE_ON, STATE_OFF])) + # import sensor programs for (folder_name, states) in ( ('HA.locations', [STATE_HOME, STATE_NOT_HOME]), diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py index c33e0666fd3..1ea87e3fc2e 100644 --- a/homeassistant/components/switch/isy994.py +++ b/homeassistant/components/switch/isy994.py @@ -3,7 +3,7 @@ import logging # homeassistant imports -from homeassistant.components.isy994 import ISY, ISYDeviceABC +from homeassistant.components.isy994 import ISY, ISYDeviceABC, SENSOR_STRING from homeassistant.const import STATE_ON, STATE_OFF # STATE_OPEN, STATE_CLOSED # The frontend doesn't seem to fully support the open and closed states yet. # Once it does, the HA.doors programs should report open and closed instead of @@ -21,7 +21,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # import not dimmable nodes and groups for node in ISY.nodes: - if not node.dimmable: + if not node.dimmable and SENSOR_STRING not in node.name: devs.append(ISYSwitchDevice(node)) # import ISY doors programs From c76644323fdcfcb1da95b0bb0a5df2c70c6f4ac1 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 14 Apr 2015 01:44:39 -0400 Subject: [PATCH 08/25] Updated the broken link to the apple-touch icon in the frontend. --- homeassistant/components/frontend/index.html.template | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontend/index.html.template b/homeassistant/components/frontend/index.html.template index c4da4f0369d..4844eb46760 100644 --- a/homeassistant/components/frontend/index.html.template +++ b/homeassistant/components/frontend/index.html.template @@ -15,8 +15,8 @@ - + From a3d6972268895b0f5475dc201a543a585b6cbe21 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 14 Apr 2015 22:57:32 -0400 Subject: [PATCH 09/25] 1) Added basic back-end framework for supporting hidden entities. 2) Enabled hidden suggestions in the isy994 component entities. --- homeassistant/__init__.py | 12 +++++++- homeassistant/bootstrap.py | 3 ++ homeassistant/components/isy994.py | 17 +++++++---- homeassistant/const.py | 3 ++ homeassistant/helpers/entity.py | 47 +++++++++++++++++++++++++----- 5 files changed, 68 insertions(+), 14 deletions(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 5e54cbd5d0a..80916011d55 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -22,7 +22,7 @@ from homeassistant.const import ( SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL, EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED, - TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME) + TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME, ATTR_HIDDEN) import homeassistant.util as util DOMAIN = "homeassistant" @@ -621,6 +621,16 @@ class StateMachine(object): new_state = str(new_state) attributes = attributes or {} + # Last chance to enforce the visibility property. This is required for + # components that don't use the Entity base class for their entities. + # The sun component is an example of this. The Entity class cannot be + # imported cleanly, so assume the state is shown. This means that for + # visibility to be supported, the state must originate from a class that + # uses the base class Entity or it must manually put the hidden + # attribute in its attributes dictionary. + if ATTR_HIDDEN not in attributes: + attributes[ATTR_HIDDEN] = False + with self._lock: old_state = self._states.get(entity_id) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 4a6aab53483..c0b8fe09af3 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -20,6 +20,7 @@ import homeassistant import homeassistant.loader as loader import homeassistant.components as core_components import homeassistant.components.group as group +from homeassistant.helpers.entity import Entity from homeassistant.const import ( EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, TEMP_CELCIUS, @@ -207,6 +208,8 @@ def process_ha_core_config(hass, config): if key in config: setattr(hass.config, attr, config[key]) + Entity._visibility.update(config.get('visibility', [{}])[0]) + if CONF_TEMPERATURE_UNIT in config: unit = config[CONF_TEMPERATURE_UNIT] diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 7fa0524d32f..d364007d12c 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -60,8 +60,8 @@ def setup(hass, config): # pull and parse optional configuration global SENSOR_STRING global HIDDEN_STRING - SENSOR_STRING = config[DOMAIN].get('sensor_string', SENSOR_STRING) - HIDDEN_STRING = config[DOMAIN].get('hidden_string', HIDDEN_STRING) + SENSOR_STRING = str(config[DOMAIN].get('sensor_string', SENSOR_STRING)) + HIDDEN_STRING = str(config[DOMAIN].get('hidden_string', HIDDEN_STRING)) # connect to ISY controller global ISY @@ -95,6 +95,7 @@ class ISYDeviceABC(ToggleEntity): def __init__(self, node): # setup properties self.node = node + self.hidden = HIDDEN_STRING in self.raw_name # track changes self._changeHandler = self.node.status. \ @@ -135,13 +136,17 @@ class ISYDeviceABC(ToggleEntity): """ Returns the id of this isy sensor """ return self.node._id + @property + def raw_name(self): + try: + return str(self._name) + except AttributeError: + return str(self.node.name) + @property def name(self): """ Returns the name of the node if any. """ - try: - return self._name - except AttributeError: - return self.node.name + return self.raw_name.replace(HIDDEN_STRING, '').strip() def update(self): """ Update state of the sensor. """ diff --git a/homeassistant/const.py b/homeassistant/const.py index 467bb692399..a1aa0df40d1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -86,6 +86,9 @@ ATTR_TRIPPED = "device_tripped" # time the device was tripped ATTR_LAST_TRIP_TIME = "last_tripped_time" +# For all entity's, this hold whether or not it should be hidden +ATTR_HIDDEN = "hidden" + # #### SERVICES #### SERVICE_HOMEASSISTANT_STOP = "stop" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a8ee712b0f7..d7599eaf971 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -8,16 +8,19 @@ Provides ABC for entities in HA. from homeassistant import NoEntitySpecifiedError from homeassistant.const import ( - ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, - DEVICE_DEFAULT_NAME, TEMP_CELCIUS, TEMP_FAHRENHEIT) + ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, ATTR_HIDDEN, STATE_ON, + STATE_OFF, DEVICE_DEFAULT_NAME, TEMP_CELCIUS, TEMP_FAHRENHEIT) class Entity(object): """ ABC for Home Assistant entities. """ # pylint: disable=no-self-use - hass = None - entity_id = None + # SAFE TO OVERWRITE + # The properties and methods here are safe to overwrite when inherting this + # class. These may be used to customize the behavior of the entity. + + _hidden = False # suggestion as to whether the entity should be hidden @property def should_poll(self): @@ -52,6 +55,10 @@ class Entity(object): """ Unit of measurement of this entity, if any. """ return None + def update(self): + """ Retrieve latest state. """ + pass + # DEPRECATION NOTICE: # Device is moving from getters to properties. # For now the new properties will call the old functions @@ -69,9 +76,14 @@ class Entity(object): """ Returns optional state attributes. """ return None - def update(self): - """ Retrieve latest state. """ - pass + # DO NOT OVERWRITE + # These properties and methods are either managed by Home Assistant or they + # are used to perform a very specific function. Overwriting these may + # produce undesirable effects in the entity's operation. + + hass = None + entity_id = None + _visibility = {} def update_ha_state(self, force_refresh=False): """ @@ -97,6 +109,9 @@ class Entity(object): if ATTR_UNIT_OF_MEASUREMENT not in attr and self.unit_of_measurement: attr[ATTR_UNIT_OF_MEASUREMENT] = self.unit_of_measurement + if ATTR_HIDDEN not in attr: + attr[ATTR_HIDDEN] = bool(self.hidden) + # Convert temperature if we detect one if attr.get(ATTR_UNIT_OF_MEASUREMENT) in (TEMP_CELCIUS, TEMP_FAHRENHEIT): @@ -115,6 +130,24 @@ class Entity(object): def __repr__(self): return "".format(self.name, self.state) + @property + def hidden(self): + """ + Returns the official decision of whether the entity should be hidden. + Any value set by the user in the configuration file will overwrite + whatever the component sets for visibility. + """ + if self.entity_id is not None and \ + self.entity_id.lower() in self._visibility: + return self._visibility[self.entity_id.lower()] is 'hide' + else: + return self._hidden + + @hidden.setter + def hidden(self, val): + """ Sets the suggestion for visibility. """ + self._hidden = bool(val) + class ToggleEntity(Entity): """ ABC for entities that can be turned on and off. """ From 0334074a522e20aacf3b4f2b0c746eec45f5619c Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 14 Apr 2015 23:38:14 -0400 Subject: [PATCH 10/25] Quick fix to the comparison to validate if an entity is hidden. --- homeassistant/helpers/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index d7599eaf971..3e559d39532 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -139,7 +139,7 @@ class Entity(object): """ if self.entity_id is not None and \ self.entity_id.lower() in self._visibility: - return self._visibility[self.entity_id.lower()] is 'hide' + return self._visibility[self.entity_id.lower()] == 'hide' else: return self._hidden From caed69d5ea2f591e48b8e94e868cb36d9bb913b4 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 14 Apr 2015 23:55:08 -0400 Subject: [PATCH 11/25] Added state card hiding to the STATE view on the frontend. --- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 272 ++---------------- .../polymer/layouts/partial-states.html | 5 +- scripts/build_frontend | 2 +- 4 files changed, 23 insertions(+), 258 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 095769c9c28..72b1fa41d39 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "1e004712440afc642a44ad927559587e" +VERSION = "9e21c99d2991dd288287d3d3bc3e64e0" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 10ac2336a3c..31c22f9bfc7 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,257 +1,21 @@ - - + -/* Palette generated by Material Palette - materialpalette.com/light-blue/orange */ +if(this.removeAttribute("value"),c)return j(this,"value",b);var e=b,f=m(this,"value",e);return j(this,"value",e.open(k(this,"value",d))),t(this,a,f)},HTMLOptionElement.prototype.bind=function(a,b,c){if("value"!==a)return HTMLElement.prototype.bind.call(this,a,b,c);if(this.removeAttribute("value"),c)return q(this,b);var d=b,e=m(this,"value",d);return q(this,d.open(r(this))),t(this,a,e)},HTMLSelectElement.prototype.bind=function(a,c,d){if("selectedindex"===a&&(a="selectedIndex"),"selectedIndex"!==a&&"value"!==a)return HTMLElement.prototype.bind.call(this,a,c,d);if(this.removeAttribute(a),d)return j(this,a,c);var e=c,f=m(this,a,e);return j(this,a,e.open(k(this,a))),b(this,a,f)}}(this),function(a){"use strict";function b(a){if(!a)throw new Error("Assertion failed")}function c(a){for(var b;b=a.parentNode;)a=b;return a}function d(a,b){if(b){for(var d,e="#"+b;!d&&(a=c(a),a.protoContent_?d=a.protoContent_.querySelector(e):a.getElementById&&(d=a.getElementById(b)),!d&&a.templateCreator_);)a=a.templateCreator_;return d}}function e(a){return"template"==a.tagName&&"http://www.w3.org/2000/svg"==a.namespaceURI}function f(a){return"TEMPLATE"==a.tagName&&"http://www.w3.org/1999/xhtml"==a.namespaceURI}function g(a){return Boolean(L[a.tagName]&&a.hasAttribute("template"))}function h(a){return void 0===a.isTemplate_&&(a.isTemplate_="TEMPLATE"==a.tagName||g(a)),a.isTemplate_}function i(a,b){var c=a.querySelectorAll(N);h(a)&&b(a),G(c,b)}function j(a){function b(a){HTMLTemplateElement.decorate(a)||j(a.content)}i(a,b)}function k(a,b){Object.getOwnPropertyNames(b).forEach(function(c){Object.defineProperty(a,c,Object.getOwnPropertyDescriptor(b,c))})}function l(a){var b=a.ownerDocument;if(!b.defaultView)return b;var c=b.templateContentsOwner_;if(!c){for(c=b.implementation.createHTMLDocument("");c.lastChild;)c.removeChild(c.lastChild);b.templateContentsOwner_=c}return c}function m(a){if(!a.stagingDocument_){var b=a.ownerDocument;if(!b.stagingDocument_){b.stagingDocument_=b.implementation.createHTMLDocument(""),b.stagingDocument_.isStagingDocument=!0;var c=b.stagingDocument_.createElement("base");c.href=document.baseURI,b.stagingDocument_.head.appendChild(c),b.stagingDocument_.stagingDocument_=b.stagingDocument_}a.stagingDocument_=b.stagingDocument_}return a.stagingDocument_}function n(a){var b=a.ownerDocument.createElement("template");a.parentNode.insertBefore(b,a);for(var c=a.attributes,d=c.length;d-->0;){var e=c[d];K[e.name]&&("template"!==e.name&&b.setAttribute(e.name,e.value),a.removeAttribute(e.name))}return b}function o(a){var b=a.ownerDocument.createElement("template");a.parentNode.insertBefore(b,a);for(var c=a.attributes,d=c.length;d-->0;){var e=c[d];b.setAttribute(e.name,e.value),a.removeAttribute(e.name)}return a.parentNode.removeChild(a),b}function p(a,b,c){var d=a.content;if(c)return void d.appendChild(b);for(var e;e=b.firstChild;)d.appendChild(e)}function q(a){P?a.__proto__=HTMLTemplateElement.prototype:k(a,HTMLTemplateElement.prototype)}function r(a){a.setModelFn_||(a.setModelFn_=function(){a.setModelFnScheduled_=!1;var b=z(a,a.delegate_&&a.delegate_.prepareBinding);w(a,b,a.model_)}),a.setModelFnScheduled_||(a.setModelFnScheduled_=!0,Observer.runEOM_(a.setModelFn_))}function s(a,b,c,d){if(a&&a.length){for(var e,f=a.length,g=0,h=0,i=0,j=!0;f>h;){var g=a.indexOf("{{",h),k=a.indexOf("[[",h),l=!1,m="}}";if(k>=0&&(0>g||g>k)&&(g=k,l=!0,m="]]"),i=0>g?-1:a.indexOf(m,g+2),0>i){if(!e)return;e.push(a.slice(h));break}e=e||[],e.push(a.slice(h,g));var n=a.slice(g+2,i).trim();e.push(l),j=j&&l;var o=d&&d(n,b,c);e.push(null==o?Path.get(n):null),e.push(o),h=i+2}return h===f&&e.push(""),e.hasOnePath=5===e.length,e.isSimplePath=e.hasOnePath&&""==e[0]&&""==e[4],e.onlyOneTime=j,e.combinator=function(a){for(var b=e[0],c=1;cc?(this.keys.push(a),this.values.push(b)):this.values[c]=b},get:function(a){var b=this.keys.indexOf(a);if(!(0>b))return this.values[b]},"delete":function(a){var b=this.keys.indexOf(a);return 0>b?!1:(this.keys.splice(b,1),this.values.splice(b,1),!0)},forEach:function(a,b){for(var c=0;cb;)this.reportInstanceMoved(b),b++},closeInstanceBindings:function(a){for(var b=a.bindings_,c=0;c32&&127>b&&-1==[34,35,60,62,63,96].indexOf(b)?a:encodeURIComponent(a)}function f(a){var b=a.charCodeAt(0);return b>32&&127>b&&-1==[34,35,60,62,96].indexOf(b)?a:encodeURIComponent(a)}function g(a,g,h){function i(a){t.push(a)}var j=g||"scheme start",k=0,l="",r=!1,s=!1,t=[];a:for(;(a[k-1]!=o||0==k)&&!this._isInvalid;){var u=a[k];switch(j){case"scheme start":if(!u||!p.test(u)){if(g){i("Invalid scheme.");break a}l="",j="no scheme";continue}l+=u.toLowerCase(),j="scheme";break;case"scheme":if(u&&q.test(u))l+=u.toLowerCase();else{if(":"!=u){if(g){if(o==u)break a;i("Code point not allowed in scheme: "+u);break a}l="",k=0,j="no scheme";continue}if(this._scheme=l,l="",g)break a;b(this._scheme)&&(this._isRelative=!0),j="file"==this._scheme?"relative":this._isRelative&&h&&h._scheme==this._scheme?"relative or authority":this._isRelative?"authority first slash":"scheme data"}break;case"scheme data":"?"==u?(query="?",j="query"):"#"==u?(this._fragment="#",j="fragment"):o!=u&&" "!=u&&"\n"!=u&&"\r"!=u&&(this._schemeData+=e(u));break;case"no scheme":if(h&&b(h._scheme)){j="relative";continue}i("Missing scheme."),c.call(this);break;case"relative or authority":if("/"!=u||"/"!=a[k+1]){i("Expected /, got: "+u),j="relative";continue}j="authority ignore slashes";break;case"relative":if(this._isRelative=!0,"file"!=this._scheme&&(this._scheme=h._scheme),o==u){this._host=h._host,this._port=h._port,this._path=h._path.slice(),this._query=h._query;break a}if("/"==u||"\\"==u)"\\"==u&&i("\\ is an invalid code point."),j="relative slash";else if("?"==u)this._host=h._host,this._port=h._port,this._path=h._path.slice(),this._query="?",j="query";else{if("#"!=u){var v=a[k+1],w=a[k+2];("file"!=this._scheme||!p.test(u)||":"!=v&&"|"!=v||o!=w&&"/"!=w&&"\\"!=w&&"?"!=w&&"#"!=w)&&(this._host=h._host,this._port=h._port,this._path=h._path.slice(),this._path.pop()),j="relative path";continue}this._host=h._host,this._port=h._port,this._path=h._path.slice(),this._query=h._query,this._fragment="#",j="fragment"}break;case"relative slash":if("/"!=u&&"\\"!=u){"file"!=this._scheme&&(this._host=h._host,this._port=h._port),j="relative path";continue}"\\"==u&&i("\\ is an invalid code point."),j="file"==this._scheme?"file host":"authority ignore slashes";break;case"authority first slash":if("/"!=u){i("Expected '/', got: "+u),j="authority ignore slashes";continue}j="authority second slash";break;case"authority second slash":if(j="authority ignore slashes","/"!=u){i("Expected '/', got: "+u);continue}break;case"authority ignore slashes":if("/"!=u&&"\\"!=u){j="authority";continue}i("Expected authority, got: "+u);break;case"authority":if("@"==u){r&&(i("@ already seen."),l+="%40"),r=!0;for(var x=0;xg;g++)f.unshift("..");var i=b.href.slice(-1)===m?m:b.hash;return f.join("/")+b.search+i}var g={resolveDom:function(a,c){c=c||b(a),this.resolveAttributes(a,c),this.resolveStyles(a,c);var d=a.querySelectorAll("template");if(d)for(var e,f=0,g=d.length;g>f&&(e=d[f]);f++)e.content&&this.resolveDom(e.content,c)},resolveTemplate:function(a){this.resolveDom(a.content,b(a))},resolveStyles:function(a,b){var c=a.querySelectorAll("style");if(c)for(var d,e=0,f=c.length;f>e&&(d=c[e]);e++)this.resolveStyle(d,b)},resolveStyle:function(a,c){c=c||b(a),a.textContent=this.resolveCssText(a.textContent,c)},resolveCssText:function(a,b,d){return a=c(a,b,d,h),c(a,b,d,i)},resolveAttributes:function(a,b){a.hasAttributes&&a.hasAttributes()&&this.resolveElementAttributes(a,b);var c=a&&a.querySelectorAll(k);if(c)for(var d,e=0,f=c.length;f>e&&(d=c[e]);e++)this.resolveElementAttributes(d,b)},resolveElementAttributes:function(a,e){e=e||b(a),j.forEach(function(b){var f,g=a.attributes[b],i=g&&g.value;i&&i.search(l)<0&&(f="style"===b?c(i,e,!1,h):d(e,i),g.value=f)})}},h=/(url\()([^)]*)(\))/g,i=/(@import[\s]+(?!url\())([^;]*)(;)/g,j=["href","src","action","style","url"],k="["+j.join("],[")+"]",l="{{.*}}",m="#";a.urlResolver=g}(Polymer),function(a){function b(a){this.cache=Object.create(null),this.map=Object.create(null),this.requests=0,this.regex=a}var c=Polymer.endOfMicrotask;b.prototype={extractUrls:function(a,b){for(var c,d,e=[];c=this.regex.exec(a);)d=new URL(c[1],b),e.push({matched:c[0],url:d.href});return e},process:function(a,b,c){var d=this.extractUrls(a,b),e=c.bind(null,this.map);this.fetch(d,e)},fetch:function(a,b){var c=a.length;if(!c)return b();for(var d,e,f,g=function(){0===--c&&b()},h=0;c>h;h++)d=a[h],f=d.url,e=this.cache[f],e||(e=this.xhr(f),e.match=d,this.cache[f]=e),e.wait(g)},handleXhr:function(a){var b=a.match,c=b.url,d=a.response||a.responseText||"";this.map[c]=d,this.fetch(this.extractUrls(d,c),a.resolve)},xhr:function(a){this.requests++;var b=new XMLHttpRequest;return b.open("GET",a,!0),b.send(),b.onerror=b.onload=this.handleXhr.bind(this,b),b.pending=[],b.resolve=function(){for(var a=b.pending,c=0;ch&&(e=a[h]);h++)this.resolveNode(e,b,d)}};var e=new b;a.styleResolver=e}(Polymer),function(a){function b(a,b){return a&&b&&Object.getOwnPropertyNames(b).forEach(function(c){var d=Object.getOwnPropertyDescriptor(b,c);d&&(Object.defineProperty(a,c,d),"function"==typeof d.value&&(d.value.nom=c))}),a}function c(a){for(var b=a||{},c=1;ce&&(c=d[e]);e++){var g=Object.getOwnPropertyDescriptor(b,c);if("function"==typeof g.value&&g.value===a)return c}b=b.__proto__}}function d(a,b,c){var d=e(c,b,a);return d[b]&&(d[b].nom=b),a._super=d}function e(a,b,c){for(;a;){if(a[b]!==c&&a[b])return a;a=f(a)}return Object}function f(a){return a.__proto__}a["super"]=b}(Polymer),function(a){function b(a){return a}function c(a,b){var c=typeof b;return b instanceof Date&&(c="date"),d[c](a,b)}var d={string:b,undefined:b,date:function(a){return new Date(Date.parse(a)||Date.now())},"boolean":function(a){return""===a?!0:"false"===a?!1:!!a},number:function(a){var b=parseFloat(a);return 0===b&&(b=parseInt(a)),isNaN(b)?a:b},object:function(a,b){if(null===b)return a;try{return JSON.parse(a.replace(/'/g,'"'))}catch(c){return a}},"function":function(a,b){return b}};a.deserializeValue=c}(Polymer),function(a){var b=a.extend,c={};c.declaration={},c.instance={},c.publish=function(a,c){for(var d in a)b(c,a[d])},a.api=c}(Polymer),function(a){var b={async:function(a,b,c){Polymer.flush(),b=b&&b.length?b:[b];var d=function(){(this[a]||a).apply(this,b)}.bind(this),e=c?setTimeout(d,c):requestAnimationFrame(d);return c?e:~e},cancelAsync:function(a){0>a?cancelAnimationFrame(~a):clearTimeout(a)},fire:function(a,b,c,d,e){var f=c||this,b=null===b||void 0===b?{}:b,g=new CustomEvent(a,{bubbles:void 0!==d?d:!0,cancelable:void 0!==e?e:!0,detail:b});return f.dispatchEvent(g),g},asyncFire:function(){this.async("fire",arguments)},classFollows:function(a,b,c){ +b&&b.classList.remove(c),a&&a.classList.add(c)},injectBoundHTML:function(a,b){var c=document.createElement("template");c.innerHTML=a;var d=this.instanceTemplate(c);return b&&(b.textContent="",b.appendChild(d)),d}},c=function(){},d={};b.asyncMethod=b.async,a.api.instance.utils=b,a.nop=c,a.nob=d}(Polymer),function(a){var b=window.WebComponents?WebComponents.flags.log:{},c="on-",d={EVENT_PREFIX:c,addHostListeners:function(){var a=this.eventDelegates;b.events&&Object.keys(a).length>0&&console.log("[%s] addHostListeners:",this.localName,a);for(var c in a){var d=a[c];PolymerGestures.addEventListener(this,c,this.element.getEventHandler(this,this,d))}},dispatchMethod:function(a,c,d){if(a){b.events&&console.group("[%s] dispatch [%s]",a.localName,c);var e="function"==typeof c?c:a[c];e&&e[d?"apply":"call"](a,d),b.events&&console.groupEnd(),Polymer.flush()}}};a.api.instance.events=d,a.addEventListener=function(a,b,c,d){PolymerGestures.addEventListener(wrap(a),b,c,d)},a.removeEventListener=function(a,b,c,d){PolymerGestures.removeEventListener(wrap(a),b,c,d)}}(Polymer),function(a){var b={copyInstanceAttributes:function(){var a=this._instanceAttributes;for(var b in a)this.hasAttribute(b)||this.setAttribute(b,a[b])},takeAttributes:function(){if(this._publishLC)for(var a,b=0,c=this.attributes,d=c.length;(a=c[b])&&d>b;b++)this.attributeToProperty(a.name,a.value)},attributeToProperty:function(b,c){var b=this.propertyForAttribute(b);if(b){if(c&&c.search(a.bindPattern)>=0)return;var d=this[b],c=this.deserializeValue(c,d);c!==d&&(this[b]=c)}},propertyForAttribute:function(a){var b=this._publishLC&&this._publishLC[a];return b},deserializeValue:function(b,c){return a.deserializeValue(b,c)},serializeValue:function(a,b){return"boolean"===b?a?"":void 0:"object"!==b&&"function"!==b&&void 0!==a?a:void 0},reflectPropertyToAttribute:function(a){var b=typeof this[a],c=this.serializeValue(this[a],b);void 0!==c?this.setAttribute(a,c):"boolean"===b&&this.removeAttribute(a)}};a.api.instance.attributes=b}(Polymer),function(a){function b(a,b){return a===b?0!==a||1/a===1/b:f(a)&&f(b)?!0:a!==a&&b!==b}function c(a,b){return void 0===b&&null===a?b:null===b||void 0===b?a:b}var d=window.WebComponents?WebComponents.flags.log:{},e={object:void 0,type:"update",name:void 0,oldValue:void 0},f=Number.isNaN||function(a){return"number"==typeof a&&isNaN(a)},g={createPropertyObserver:function(){var a=this._observeNames;if(a&&a.length){var b=this._propertyObserver=new CompoundObserver(!0);this.registerObserver(b);for(var c,d=0,e=a.length;e>d&&(c=a[d]);d++)b.addPath(this,c),this.observeArrayValue(c,this[c],null)}},openPropertyObserver:function(){this._propertyObserver&&this._propertyObserver.open(this.notifyPropertyChanges,this)},notifyPropertyChanges:function(a,b,c){var d,e,f={};for(var g in b)if(d=c[2*g+1],e=this.observe[d]){var h=b[g],i=a[g];this.observeArrayValue(d,i,h),f[e]||(void 0!==h&&null!==h||void 0!==i&&null!==i)&&(f[e]=!0,this.invokeMethod(e,[h,i,arguments]))}},invokeMethod:function(a,b){var c=this[a]||a;"function"==typeof c&&c.apply(this,b)},deliverChanges:function(){this._propertyObserver&&this._propertyObserver.deliver()},observeArrayValue:function(a,b,c){var e=this.observe[a];if(e&&(Array.isArray(c)&&(d.observe&&console.log("[%s] observeArrayValue: unregister observer [%s]",this.localName,a),this.closeNamedObserver(a+"__array")),Array.isArray(b))){d.observe&&console.log("[%s] observeArrayValue: register observer [%s]",this.localName,a,b);var f=new ArrayObserver(b);f.open(function(a){this.invokeMethod(e,[a])},this),this.registerNamedObserver(a+"__array",f)}},emitPropertyChangeRecord:function(a,c,d){if(!b(c,d)&&(this._propertyChanged(a,c,d),Observer.hasObjectObserve)){var f=this._objectNotifier;f||(f=this._objectNotifier=Object.getNotifier(this)),e.object=this,e.name=a,e.oldValue=d,f.notify(e)}},_propertyChanged:function(a){this.reflect[a]&&this.reflectPropertyToAttribute(a)},bindProperty:function(a,b,d){if(d)return void(this[a]=b);var e=this.element.prototype.computed;if(e&&e[a]){var f=a+"ComputedBoundObservable_";return void(this[f]=b)}return this.bindToAccessor(a,b,c)},bindToAccessor:function(a,c,d){function e(b,c){j[f]=b;var d=j[h];d&&"function"==typeof d.setValue&&d.setValue(b),j.emitPropertyChangeRecord(a,b,c)}var f=a+"_",g=a+"Observable_",h=a+"ComputedBoundObservable_";this[g]=c;var i=this[f],j=this,k=c.open(e);if(d&&!b(i,k)){var l=d(i,k);b(k,l)||(k=l,c.setValue&&c.setValue(k))}e(k,i);var m={close:function(){c.close(),j[g]=void 0,j[h]=void 0}};return this.registerObserver(m),m},createComputedProperties:function(){if(this._computedNames)for(var a=0;ae&&(c=d[e]);e++)b[c.id]=c},onMutation:function(a,b){var c=new MutationObserver(function(a){b.call(this,c,a),c.disconnect()}.bind(this));c.observe(a,{childList:!0,subtree:!0})}};c.prototype=d,d.constructor=c,a.Base=c,a.isBase=b,a.api.instance.base=d}(Polymer),function(a){function b(a){return a.__proto__}function c(a,b){var c="",d=!1;b&&(c=b.localName,d=b.hasAttribute("is"));var e=WebComponents.ShadowCSS.makeScopeSelector(c,d);return WebComponents.ShadowCSS.shimCssText(a,e)}var d=(window.WebComponents?WebComponents.flags.log:{},window.ShadowDOMPolyfill),e="element",f="controller",g={STYLE_SCOPE_ATTRIBUTE:e,installControllerStyles:function(){var a=this.findStyleScope();if(a&&!this.scopeHasNamedStyle(a,this.localName)){for(var c=b(this),d="";c&&c.element;)d+=c.element.cssTextForScope(f),c=b(c);d&&this.installScopeCssText(d,a)}},installScopeStyle:function(a,b,c){var c=c||this.findStyleScope(),b=b||"";if(c&&!this.scopeHasNamedStyle(c,this.localName+b)){var d="";if(a instanceof Array)for(var e,f=0,g=a.length;g>f&&(e=a[f]);f++)d+=e.textContent+"\n\n";else d=a.textContent;this.installScopeCssText(d,c,b)}},installScopeCssText:function(a,b,e){if(b=b||this.findStyleScope(),e=e||"",b){d&&(a=c(a,b.host));var g=this.element.cssTextToScopeStyle(a,f);Polymer.applyStyleToScope(g,b),this.styleCacheForScope(b)[this.localName+e]=!0}},findStyleScope:function(a){for(var b=a||this;b.parentNode;)b=b.parentNode;return b},scopeHasNamedStyle:function(a,b){var c=this.styleCacheForScope(a);return c[b]},styleCacheForScope:function(a){if(d){var b=a.host?a.host.localName:a.localName;return h[b]||(h[b]={})}return a._scopeStyles=a._scopeStyles||{}}},h={};a.api.instance.styles=g}(Polymer),function(a){function b(a,b){if("string"!=typeof a){var c=b||document._currentScript;if(b=a,a=c&&c.parentNode&&c.parentNode.getAttribute?c.parentNode.getAttribute("name"):"",!a)throw"Element name could not be inferred."}if(f(a))throw"Already registered (Polymer) prototype for element "+a;e(a,b),d(a)}function c(a,b){i[a]=b}function d(a){i[a]&&(i[a].registerWhenReady(),delete i[a])}function e(a,b){return j[a]=b||{}}function f(a){return j[a]}function g(a,b){if("string"!=typeof b)return!1;var c=HTMLElement.getPrototypeForTag(b),d=c&&c.constructor;return d?CustomElements["instanceof"]?CustomElements["instanceof"](a,d):a instanceof d:!1}var h=a.extend,i=(a.api,{}),j={};a.getRegisteredPrototype=f,a.waitingForPrototype=c,a.instanceOfType=g,window.Polymer=b,h(Polymer,a),WebComponents.consumeDeclarations&&WebComponents.consumeDeclarations(function(a){if(a)for(var c,d=0,e=a.length;e>d&&(c=a[d]);d++)b.apply(null,c)})}(Polymer),function(a){var b={resolveElementPaths:function(a){Polymer.urlResolver.resolveDom(a)},addResolvePathApi:function(){var a=this.getAttribute("assetpath")||"",b=new URL(a,this.ownerDocument.baseURI);this.prototype.resolvePath=function(a,c){var d=new URL(a,c||b);return d.href}}};a.api.declaration.path=b}(Polymer),function(a){function b(a,b){var c=new URL(a.getAttribute("href"),b).href;return"@import '"+c+"';"}function c(a,b){if(a){b===document&&(b=document.head),i&&(b=document.head);var c=d(a.textContent),e=a.getAttribute(h);e&&c.setAttribute(h,e);var f=b.firstElementChild;if(b===document.head){var g="style["+h+"]",j=document.head.querySelectorAll(g);j.length&&(f=j[j.length-1].nextElementSibling)}b.insertBefore(c,f)}}function d(a,b){b=b||document,b=b.createElement?b:b.ownerDocument;var c=b.createElement("style");return c.textContent=a,c}function e(a){return a&&a.__resource||""}function f(a,b){return q?q.call(a,b):void 0}var g=(window.WebComponents?WebComponents.flags.log:{},a.api.instance.styles),h=g.STYLE_SCOPE_ATTRIBUTE,i=window.ShadowDOMPolyfill,j="style",k="@import",l="link[rel=stylesheet]",m="global",n="polymer-scope",o={loadStyles:function(a){var b=this.fetchTemplate(),c=b&&this.templateContent();if(c){this.convertSheetsToStyles(c);var d=this.findLoadableStyles(c);if(d.length){var e=b.ownerDocument.baseURI;return Polymer.styleResolver.loadStyles(d,e,a)}}a&&a()},convertSheetsToStyles:function(a){for(var c,e,f=a.querySelectorAll(l),g=0,h=f.length;h>g&&(c=f[g]);g++)e=d(b(c,this.ownerDocument.baseURI),this.ownerDocument),this.copySheetAttributes(e,c),c.parentNode.replaceChild(e,c)},copySheetAttributes:function(a,b){for(var c,d=0,e=b.attributes,f=e.length;(c=e[d])&&f>d;d++)"rel"!==c.name&&"href"!==c.name&&a.setAttribute(c.name,c.value)},findLoadableStyles:function(a){var b=[];if(a)for(var c,d=a.querySelectorAll(j),e=0,f=d.length;f>e&&(c=d[e]);e++)c.textContent.match(k)&&b.push(c);return b},installSheets:function(){this.cacheSheets(),this.cacheStyles(),this.installLocalSheets(),this.installGlobalStyles()},cacheSheets:function(){this.sheets=this.findNodes(l),this.sheets.forEach(function(a){a.parentNode&&a.parentNode.removeChild(a)})},cacheStyles:function(){this.styles=this.findNodes(j+"["+n+"]"),this.styles.forEach(function(a){a.parentNode&&a.parentNode.removeChild(a)})},installLocalSheets:function(){var a=this.sheets.filter(function(a){return!a.hasAttribute(n)}),b=this.templateContent();if(b){var c="";if(a.forEach(function(a){c+=e(a)+"\n"}),c){var f=d(c,this.ownerDocument);b.insertBefore(f,b.firstChild)}}},findNodes:function(a,b){var c=this.querySelectorAll(a).array(),d=this.templateContent();if(d){var e=d.querySelectorAll(a).array();c=c.concat(e)}return b?c.filter(b):c},installGlobalStyles:function(){var a=this.styleForScope(m);c(a,document.head)},cssTextForScope:function(a){var b="",c="["+n+"="+a+"]",d=function(a){return f(a,c)},g=this.sheets.filter(d);g.forEach(function(a){b+=e(a)+"\n\n"});var h=this.styles.filter(d);return h.forEach(function(a){b+=a.textContent+"\n\n"}),b},styleForScope:function(a){var b=this.cssTextForScope(a);return this.cssTextToScopeStyle(b,a)},cssTextToScopeStyle:function(a,b){if(a){var c=d(a);return c.setAttribute(h,this.getAttribute("name")+"-"+b),c}}},p=HTMLElement.prototype,q=p.matches||p.matchesSelector||p.webkitMatchesSelector||p.mozMatchesSelector;a.api.declaration.styles=o,a.applyStyleToScope=c}(Polymer),function(a){var b=(window.WebComponents?WebComponents.flags.log:{},a.api.instance.events),c=b.EVENT_PREFIX,d={};["webkitAnimationStart","webkitAnimationEnd","webkitTransitionEnd","DOMFocusOut","DOMFocusIn","DOMMouseScroll"].forEach(function(a){d[a.toLowerCase()]=a});var e={parseHostEvents:function(){var a=this.prototype.eventDelegates;this.addAttributeDelegates(a)},addAttributeDelegates:function(a){for(var b,c=0;b=this.attributes[c];c++)this.hasEventPrefix(b.name)&&(a[this.removeEventPrefix(b.name)]=b.value.replace("{{","").replace("}}","").trim())},hasEventPrefix:function(a){return a&&"o"===a[0]&&"n"===a[1]&&"-"===a[2]},removeEventPrefix:function(a){return a.slice(f)},findController:function(a){for(;a.parentNode;){if(a.eventController)return a.eventController;a=a.parentNode}return a.host},getEventHandler:function(a,b,c){var d=this;return function(e){a&&a.PolymerBase||(a=d.findController(b));var f=[e,e.detail,e.currentTarget];a.dispatchMethod(a,c,f)}},prepareEventBinding:function(a,b){if(this.hasEventPrefix(b)){var c=this.removeEventPrefix(b);c=d[c]||c;var e=this;return function(b,d,f){function g(){return"{{ "+a+" }}"}var h=e.getEventHandler(void 0,d,a);return PolymerGestures.addEventListener(d,c,h),f?void 0:{open:g,discardChanges:g,close:function(){PolymerGestures.removeEventListener(d,c,h)}}}}}},f=c.length;a.api.declaration.events=e}(Polymer),function(a){var b=["attribute"],c={inferObservers:function(a){var b,c=a.observe;for(var d in a)"Changed"===d.slice(-7)&&(b=d.slice(0,-7),this.canObserveProperty(b)&&(c||(c=a.observe={}),c[b]=c[b]||d))},canObserveProperty:function(a){return b.indexOf(a)<0},explodeObservers:function(a){var b=a.observe;if(b){var c={};for(var d in b)for(var e,f=d.split(" "),g=0;e=f[g];g++)c[e]=b[d];a.observe=c}},optimizePropertyMaps:function(a){if(a.observe){var b=a._observeNames=[];for(var c in a.observe)for(var d,e=c.split(" "),f=0;d=e[f];f++)b.push(d)}if(a.publish){var b=a._publishNames=[];for(var c in a.publish)b.push(c)}if(a.computed){var b=a._computedNames=[];for(var c in a.computed)b.push(c)}},publishProperties:function(a,b){var c=a.publish;c&&(this.requireProperties(c,a,b),this.filterInvalidAccessorNames(c),a._publishLC=this.lowerCaseMap(c));var d=a.computed;d&&this.filterInvalidAccessorNames(d)},filterInvalidAccessorNames:function(a){for(var b in a)this.propertyNameBlacklist[b]&&(console.warn('Cannot define property "'+b+'" for element "'+this.name+'" because it has the same name as an HTMLElement property, and not all browsers support overriding that. Consider giving it a different name.'),delete a[b])},requireProperties:function(a,b){b.reflect=b.reflect||{};for(var c in a){var d=a[c];d&&void 0!==d.reflect&&(b.reflect[c]=Boolean(d.reflect),d=d.value),void 0!==d&&(b[c]=d)}},lowerCaseMap:function(a){var b={};for(var c in a)b[c.toLowerCase()]=c;return b},createPropertyAccessor:function(a,b){var c=this.prototype,d=a+"_",e=a+"Observable_";c[d]=c[a],Object.defineProperty(c,a,{get:function(){var a=this[e];return a&&a.deliver(),this[d]},set:function(c){if(b)return this[d];var f=this[e];if(f)return void f.setValue(c);var g=this[d];return this[d]=c,this.emitPropertyChangeRecord(a,c,g),c},configurable:!0})},createPropertyAccessors:function(a){var b=a._computedNames;if(b&&b.length)for(var c,d=0,e=b.length;e>d&&(c=b[d]);d++)this.createPropertyAccessor(c,!0);var b=a._publishNames;if(b&&b.length)for(var c,d=0,e=b.length;e>d&&(c=b[d]);d++)a.computed&&a.computed[c]||this.createPropertyAccessor(c)},propertyNameBlacklist:{children:1,"class":1,id:1,hidden:1,style:1,title:1}};a.api.declaration.properties=c}(Polymer),function(a){var b="attributes",c=/\s|,/,d={inheritAttributesObjects:function(a){this.inheritObject(a,"publishLC"),this.inheritObject(a,"_instanceAttributes")},publishAttributes:function(a){var d=this.getAttribute(b);if(d)for(var e,f=a.publish||(a.publish={}),g=d.split(c),h=0,i=g.length;i>h;h++)e=g[h].trim(),e&&void 0===f[e]&&(f[e]=void 0)},accumulateInstanceAttributes:function(){for(var a,b=this.prototype._instanceAttributes,c=this.attributes,d=0,e=c.length;e>d&&(a=c[d]);d++)this.isInstanceAttribute(a.name)&&(b[a.name]=a.value)},isInstanceAttribute:function(a){return!this.blackList[a]&&"on-"!==a.slice(0,3)},blackList:{name:1,"extends":1,constructor:1,noscript:1,assetpath:1,"cache-csstext":1}};d.blackList[b]=1,a.api.declaration.attributes=d}(Polymer),function(a){var b=a.api.declaration.events,c=new PolymerExpressions,d=c.prepareBinding;c.prepareBinding=function(a,e,f){return b.prepareEventBinding(a,e,f)||d.call(c,a,e,f)};var e={syntax:c,fetchTemplate:function(){return this.querySelector("template")},templateContent:function(){var a=this.fetchTemplate();return a&&a.content},installBindingDelegate:function(a){a&&(a.bindingDelegate=this.syntax)}};a.api.declaration.mdv=e}(Polymer),function(a){function b(a){if(!Object.__proto__){var b=Object.getPrototypeOf(a);a.__proto__=b,d(b)&&(b.__proto__=Object.getPrototypeOf(b))}}var c=a.api,d=a.isBase,e=a.extend,f=window.ShadowDOMPolyfill,g={register:function(a,b){this.buildPrototype(a,b),this.registerPrototype(a,b),this.publishConstructor()},buildPrototype:function(b,c){var d=a.getRegisteredPrototype(b),e=this.generateBasePrototype(c);this.desugarBeforeChaining(d,e),this.prototype=this.chainPrototypes(d,e),this.desugarAfterChaining(b,c)},desugarBeforeChaining:function(a,b){a.element=this,this.publishAttributes(a,b),this.publishProperties(a,b),this.inferObservers(a),this.explodeObservers(a)},chainPrototypes:function(a,c){this.inheritMetaData(a,c);var d=this.chainObject(a,c);return b(d),d},inheritMetaData:function(a,b){this.inheritObject("observe",a,b),this.inheritObject("publish",a,b),this.inheritObject("reflect",a,b),this.inheritObject("_publishLC",a,b),this.inheritObject("_instanceAttributes",a,b),this.inheritObject("eventDelegates",a,b)},desugarAfterChaining:function(a,b){this.optimizePropertyMaps(this.prototype),this.createPropertyAccessors(this.prototype),this.installBindingDelegate(this.fetchTemplate()),this.installSheets(),this.resolveElementPaths(this),this.accumulateInstanceAttributes(),this.parseHostEvents(),this.addResolvePathApi(),f&&WebComponents.ShadowCSS.shimStyling(this.templateContent(),a,b),this.prototype.registerCallback&&this.prototype.registerCallback(this)},publishConstructor:function(){var a=this.getAttribute("constructor");a&&(window[a]=this.ctor)},generateBasePrototype:function(a){var b=this.findBasePrototype(a);if(!b){var b=HTMLElement.getPrototypeForTag(a);b=this.ensureBaseApi(b),h[a]=b}return b},findBasePrototype:function(a){return h[a]},ensureBaseApi:function(a){if(a.PolymerBase)return a;var b=Object.create(a);return c.publish(c.instance,b),this.mixinMethod(b,a,c.instance.mdv,"bind"),b},mixinMethod:function(a,b,c,d){var e=function(a){return b[d].apply(this,a)};a[d]=function(){return this.mixinSuper=e,c[d].apply(this,arguments)}},inheritObject:function(a,b,c){var d=b[a]||{};b[a]=this.chainObject(d,c[a])},registerPrototype:function(a,b){var c={prototype:this.prototype},d=this.findTypeExtension(b);d&&(c["extends"]=d),HTMLElement.register(a,this.prototype),this.ctor=document.registerElement(a,c)},findTypeExtension:function(a){if(a&&a.indexOf("-")<0)return a;var b=this.findBasePrototype(a);return b.element?this.findTypeExtension(b.element["extends"]):void 0}},h={};g.chainObject=Object.__proto__?function(a,b){return a&&b&&a!==b&&(a.__proto__=b),a}:function(a,b){if(a&&b&&a!==b){var c=Object.create(b);a=e(c,a)}return a},c.declaration.prototype=g}(Polymer),function(a){function b(a){return document.contains(a)?j:i}function c(){return i.length?i[0]:j[0]}function d(a){f.waitToReady=!0,Polymer.endOfMicrotask(function(){HTMLImports.whenReady(function(){f.addReadyCallback(a),f.waitToReady=!1,f.check()})})}function e(a){if(void 0===a)return void f.ready();var b=setTimeout(function(){f.ready()},a);Polymer.whenReady(function(){clearTimeout(b)})}var f={wait:function(a){a.__queue||(a.__queue={},g.push(a))},enqueue:function(a,c,d){var e=a.__queue&&!a.__queue.check;return e&&(b(a).push(a),a.__queue.check=c,a.__queue.go=d),0!==this.indexOf(a)},indexOf:function(a){var c=b(a).indexOf(a);return c>=0&&document.contains(a)&&(c+=HTMLImports.useNative||HTMLImports.ready?i.length:1e9),c},go:function(a){var b=this.remove(a);b&&(a.__queue.flushable=!0,this.addToFlushQueue(b),this.check())},remove:function(a){var c=this.indexOf(a);if(0===c)return b(a).shift()},check:function(){var a=this.nextElement();return a&&a.__queue.check.call(a),this.canReady()?(this.ready(),!0):void 0},nextElement:function(){return c()},canReady:function(){return!this.waitToReady&&this.isEmpty()},isEmpty:function(){for(var a,b=0,c=g.length;c>b&&(a=g[b]);b++)if(a.__queue&&!a.__queue.flushable)return;return!0},addToFlushQueue:function(a){h.push(a)},flush:function(){if(!this.flushing){this.flushing=!0;for(var a;h.length;)a=h.shift(),a.__queue.go.call(a),a.__queue=null;this.flushing=!1}},ready:function(){var a=CustomElements.ready;CustomElements.ready=!1,this.flush(),CustomElements.useNative||CustomElements.upgradeDocumentTree(document),CustomElements.ready=a,Polymer.flush(),requestAnimationFrame(this.flushReadyCallbacks)},addReadyCallback:function(a){a&&k.push(a)},flushReadyCallbacks:function(){if(k)for(var a;k.length;)(a=k.shift())()},waitingFor:function(){for(var a,b=[],c=0,d=g.length;d>c&&(a=g[c]);c++)a.__queue&&!a.__queue.flushable&&b.push(a);return b},waitToReady:!0},g=[],h=[],i=[],j=[],k=[];a.elements=g,a.waitingFor=f.waitingFor.bind(f),a.forceReady=e,a.queue=f,a.whenReady=a.whenPolymerReady=d}(Polymer),function(a){function b(a){return Boolean(HTMLElement.getPrototypeForTag(a))}function c(a){return a&&a.indexOf("-")>=0}var d=a.extend,e=a.api,f=a.queue,g=a.whenReady,h=a.getRegisteredPrototype,i=a.waitingForPrototype,j=d(Object.create(HTMLElement.prototype),{createdCallback:function(){this.getAttribute("name")&&this.init()},init:function(){this.name=this.getAttribute("name"),this["extends"]=this.getAttribute("extends"),f.wait(this),this.loadResources(),this.registerWhenReady()},registerWhenReady:function(){this.registered||this.waitingForPrototype(this.name)||this.waitingForQueue()||this.waitingForResources()||f.go(this)},_register:function(){c(this["extends"])&&!b(this["extends"])&&console.warn("%s is attempting to extend %s, an unregistered element or one that was not registered with Polymer.",this.name,this["extends"]),this.register(this.name,this["extends"]),this.registered=!0},waitingForPrototype:function(a){return h(a)?void 0:(i(a,this),this.handleNoScript(a),!0)},handleNoScript:function(a){this.hasAttribute("noscript")&&!this.noscript&&(this.noscript=!0,Polymer(a))},waitingForResources:function(){return this._needsResources},waitingForQueue:function(){return f.enqueue(this,this.registerWhenReady,this._register)},loadResources:function(){this._needsResources=!0,this.loadStyles(function(){this._needsResources=!1,this.registerWhenReady()}.bind(this))}});e.publish(e.declaration,j),g(function(){document.body.removeAttribute("unresolved"),document.dispatchEvent(new CustomEvent("polymer-ready",{bubbles:!0}))}),document.registerElement("polymer-element",{prototype:j})}(Polymer),function(a){function b(a,b){a?(document.head.appendChild(a),d(b)):b&&b()}function c(a,c){if(a&&a.length){for(var d,e,f=document.createDocumentFragment(),g=0,h=a.length;h>g&&(d=a[g]);g++)e=document.createElement("link"),e.rel="import",e.href=d,f.appendChild(e);b(f,c)}else c&&c()}var d=a.whenReady;a["import"]=c,a.importElements=b}(Polymer),function(){var a=document.createElement("polymer-element");a.setAttribute("name","auto-binding"),a.setAttribute("extends","template"),a.init(),Polymer("auto-binding",{createdCallback:function(){this.syntax=this.bindingDelegate=this.makeSyntax(),Polymer.whenPolymerReady(function(){this.model=this,this.setAttribute("bind",""),this.async(function(){this.marshalNodeReferences(this.parentNode),this.fire("template-bound")})}.bind(this))},makeSyntax:function(){var a=Object.create(Polymer.api.declaration.events),b=this;a.findController=function(){return b.model};var c=new PolymerExpressions,d=c.prepareBinding;return c.prepareBinding=function(b,e,f){return a.prepareEventBinding(b,e,f)||d.call(c,b,e,f)},c}})}(); - - - - - +return pickBy("isBefore",args)};moment.max=function(){var args=[].slice.call(arguments,0);return pickBy("isAfter",args)};moment.utc=function(input,format,locale,strict){var c;if(typeof locale==="boolean"){strict=locale;locale=undefined}c={};c._isAMomentObject=true;c._useUTC=true;c._isUTC=true;c._l=locale;c._i=input;c._f=format;c._strict=strict;c._pf=defaultParsingFlags();return makeMoment(c).utc()};moment.unix=function(input){return moment(input*1e3)};moment.duration=function(input,key){var duration=input,match=null,sign,ret,parseIso,diffRes;if(moment.isDuration(input)){duration={ms:input._milliseconds,d:input._days,M:input._months}}else if(typeof input==="number"){duration={};if(key){duration[key]=input}else{duration.milliseconds=input}}else if(!!(match=aspNetTimeSpanJsonRegex.exec(input))){sign=match[1]==="-"?-1:1;duration={y:0,d:toInt(match[DATE])*sign,h:toInt(match[HOUR])*sign,m:toInt(match[MINUTE])*sign,s:toInt(match[SECOND])*sign,ms:toInt(match[MILLISECOND])*sign}}else if(!!(match=isoDurationRegex.exec(input))){sign=match[1]==="-"?-1:1;parseIso=function(inp){var res=inp&&parseFloat(inp.replace(",","."));return(isNaN(res)?0:res)*sign};duration={y:parseIso(match[2]),M:parseIso(match[3]),d:parseIso(match[4]),h:parseIso(match[5]),m:parseIso(match[6]),s:parseIso(match[7]),w:parseIso(match[8])}}else if(duration==null){duration={}}else if(typeof duration==="object"&&("from"in duration||"to"in duration)){diffRes=momentsDifference(moment(duration.from),moment(duration.to));duration={};duration.ms=diffRes.milliseconds;duration.M=diffRes.months}ret=new Duration(duration);if(moment.isDuration(input)&&hasOwnProp(input,"_locale")){ret._locale=input._locale}return ret};moment.version=VERSION;moment.defaultFormat=isoFormat;moment.ISO_8601=function(){};moment.momentProperties=momentProperties;moment.updateOffset=function(){};moment.relativeTimeThreshold=function(threshold,limit){if(relativeTimeThresholds[threshold]===undefined){return false}if(limit===undefined){return relativeTimeThresholds[threshold]}relativeTimeThresholds[threshold]=limit;return true};moment.lang=deprecate("moment.lang is deprecated. Use moment.locale instead.",function(key,value){return moment.locale(key,value)});moment.locale=function(key,values){var data;if(key){if(typeof values!=="undefined"){data=moment.defineLocale(key,values)}else{data=moment.localeData(key)}if(data){moment.duration._locale=moment._locale=data}}return moment._locale._abbr};moment.defineLocale=function(name,values){if(values!==null){values.abbr=name;if(!locales[name]){locales[name]=new Locale}locales[name].set(values);moment.locale(name);return locales[name]}else{delete locales[name];return null}};moment.langData=deprecate("moment.langData is deprecated. Use moment.localeData instead.",function(key){return moment.localeData(key)});moment.localeData=function(key){var locale;if(key&&key._locale&&key._locale._abbr){key=key._locale._abbr}if(!key){return moment._locale}if(!isArray(key)){locale=loadLocale(key);if(locale){return locale}key=[key]}return chooseLocale(key)};moment.isMoment=function(obj){return obj instanceof Moment||obj!=null&&hasOwnProp(obj,"_isAMomentObject")};moment.isDuration=function(obj){return obj instanceof Duration};for(i=lists.length-1;i>=0;--i){makeList(lists[i])}moment.normalizeUnits=function(units){return normalizeUnits(units)};moment.invalid=function(flags){var m=moment.utc(NaN);if(flags!=null){extend(m._pf,flags)}else{m._pf.userInvalidated=true}return m};moment.parseZone=function(){return moment.apply(null,arguments).parseZone()};moment.parseTwoDigitYear=function(input){return toInt(input)+(toInt(input)>68?1900:2e3)};moment.isDate=isDate;extend(moment.fn=Moment.prototype,{clone:function(){return moment(this)},valueOf:function(){return+this._d-(this._offset||0)*6e4},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var m=moment(this).utc();if(00}return false},parsingFlags:function(){return extend({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(keepLocalTime){return this.utcOffset(0,keepLocalTime)},local:function(keepLocalTime){if(this._isUTC){this.utcOffset(0,keepLocalTime);this._isUTC=false;if(keepLocalTime){this.subtract(this._dateUtcOffset(),"m")}}return this},format:function(inputString){var output=formatMoment(this,inputString||moment.defaultFormat);return this.localeData().postformat(output)},add:createAdder(1,"add"),subtract:createAdder(-1,"subtract"),diff:function(input,units,asFloat){var that=makeAs(input,this),zoneDiff=(that.utcOffset()-this.utcOffset())*6e4,anchor,diff,output,daysAdjust;units=normalizeUnits(units);if(units==="year"||units==="month"||units==="quarter"){output=monthDiff(this,that);if(units==="quarter"){output=output/3}else if(units==="year"){output=output/12}}else{diff=this-that;output=units==="second"?diff/1e3:units==="minute"?diff/6e4:units==="hour"?diff/36e5:units==="day"?(diff-zoneDiff)/864e5:units==="week"?(diff-zoneDiff)/6048e5:diff}return asFloat?output:absRound(output)},from:function(time,withoutSuffix){return moment.duration({to:this,from:time}).locale(this.locale()).humanize(!withoutSuffix)},fromNow:function(withoutSuffix){return this.from(moment(),withoutSuffix)},calendar:function(time){var now=time||moment(),sod=makeAs(now,this).startOf("day"),diff=this.diff(sod,"days",true),format=diff<-6?"sameElse":diff<-1?"lastWeek":diff<0?"lastDay":diff<1?"sameDay":diff<2?"nextDay":diff<7?"nextWeek":"sameElse";return this.format(this.localeData().calendar(format,this,moment(now)))},isLeapYear:function(){return isLeapYear(this.year())},isDST:function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},day:function(input){var day=this._isUTC?this._d.getUTCDay():this._d.getDay();if(input!=null){input=parseWeekday(input,this.localeData());return this.add(input-day,"d")}else{return day}},month:makeAccessor("Month",true),startOf:function(units){units=normalizeUnits(units);switch(units){case"year":this.month(0);case"quarter":case"month":this.date(1);case"week":case"isoWeek":case"day":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}if(units==="week"){this.weekday(0)}else if(units==="isoWeek"){this.isoWeekday(1)}if(units==="quarter"){this.month(Math.floor(this.month()/3)*3)}return this},endOf:function(units){units=normalizeUnits(units);if(units===undefined||units==="millisecond"){return this}return this.startOf(units).add(1,units==="isoWeek"?"week":units).subtract(1,"ms")},isAfter:function(input,units){var inputMs;units=normalizeUnits(typeof units!=="undefined"?units:"millisecond");if(units==="millisecond"){input=moment.isMoment(input)?input:moment(input);return+this>+input}else{inputMs=moment.isMoment(input)?+input:+moment(input);return inputMs<+this.clone().startOf(units)}},isBefore:function(input,units){var inputMs;units=normalizeUnits(typeof units!=="undefined"?units:"millisecond");if(units==="millisecond"){input=moment.isMoment(input)?input:moment(input);return+this<+input}else{inputMs=moment.isMoment(input)?+input:+moment(input);return+this.clone().endOf(units)this?this:other}),zone:deprecate("moment().zone is deprecated, use moment().utcOffset instead. "+"https://github.com/moment/moment/issues/1779",function(input,keepLocalTime){if(input!=null){if(typeof input!=="string"){input=-input}this.utcOffset(input,keepLocalTime);return this}else{return-this.utcOffset()}}),utcOffset:function(input,keepLocalTime){var offset=this._offset||0,localAdjust;if(input!=null){if(typeof input==="string"){input=utcOffsetFromString(input)}if(Math.abs(input)<16){input=input*60}if(!this._isUTC&&keepLocalTime){localAdjust=this._dateUtcOffset()}this._offset=input;this._isUTC=true;if(localAdjust!=null){this.add(localAdjust,"m")}if(offset!==input){if(!keepLocalTime||this._changeInProgress){addOrSubtractDurationFromMoment(this,moment.duration(input-offset,"m"),1,false)}else if(!this._changeInProgress){this._changeInProgress=true;moment.updateOffset(this,true);this._changeInProgress=null}}return this}else{return this._isUTC?offset:this._dateUtcOffset()}},isLocal:function(){return!this._isUTC},isUtcOffset:function(){return this._isUTC},isUtc:function(){return this._isUTC&&this._offset===0},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){if(this._tzm){this.utcOffset(this._tzm)}else if(typeof this._i==="string"){this.utcOffset(utcOffsetFromString(this._i))}return this},hasAlignedHourOffset:function(input){if(!input){input=0}else{input=moment(input).utcOffset()}return(this.utcOffset()-input)%60===0},daysInMonth:function(){return daysInMonth(this.year(),this.month())},dayOfYear:function(input){var dayOfYear=round((moment(this).startOf("day")-moment(this).startOf("year"))/864e5)+1;return input==null?dayOfYear:this.add(input-dayOfYear,"d")},quarter:function(input){return input==null?Math.ceil((this.month()+1)/3):this.month((input-1)*3+this.month()%3)},weekYear:function(input){var year=weekOfYear(this,this.localeData()._week.dow,this.localeData()._week.doy).year;return input==null?year:this.add(input-year,"y")},isoWeekYear:function(input){var year=weekOfYear(this,1,4).year;return input==null?year:this.add(input-year,"y")},week:function(input){var week=this.localeData().week(this);return input==null?week:this.add((input-week)*7,"d")},isoWeek:function(input){var week=weekOfYear(this,1,4).week;return input==null?week:this.add((input-week)*7,"d")},weekday:function(input){var weekday=(this.day()+7-this.localeData()._week.dow)%7;return input==null?weekday:this.add(input-weekday,"d")},isoWeekday:function(input){return input==null?this.day()||7:this.day(this.day()%7?input:input-7)},isoWeeksInYear:function(){return weeksInYear(this.year(),1,4)},weeksInYear:function(){var weekInfo=this.localeData()._week;return weeksInYear(this.year(),weekInfo.dow,weekInfo.doy)},get:function(units){units=normalizeUnits(units);return this[units]()},set:function(units,value){var unit;if(typeof units==="object"){for(unit in units){this.set(unit,units[unit])}}else{units=normalizeUnits(units);if(typeof this[units]==="function"){this[units](value)}}return this},locale:function(key){var newLocaleData;if(key===undefined){return this._locale._abbr}else{newLocaleData=moment.localeData(key);if(newLocaleData!=null){this._locale=newLocaleData}return this}},lang:deprecate("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(key){if(key===undefined){return this.localeData()}else{return this.locale(key)}}),localeData:function(){return this._locale},_dateUtcOffset:function(){return-Math.round(this._d.getTimezoneOffset()/15)*15}});function rawMonthSetter(mom,value){var dayOfMonth;if(typeof value==="string"){value=mom.localeData().monthsParse(value);if(typeof value!=="number"){return mom}}dayOfMonth=Math.min(mom.date(),daysInMonth(mom.year(),value));mom._d["set"+(mom._isUTC?"UTC":"")+"Month"](value,dayOfMonth);return mom}function rawGetter(mom,unit){return mom._d["get"+(mom._isUTC?"UTC":"")+unit]()}function rawSetter(mom,unit,value){if(unit==="Month"){return rawMonthSetter(mom,value)}else{return mom._d["set"+(mom._isUTC?"UTC":"")+unit](value)}}function makeAccessor(unit,keepTime){return function(value){if(value!=null){rawSetter(this,unit,value);moment.updateOffset(this,keepTime);return this}else{return rawGetter(this,unit)}}}moment.fn.millisecond=moment.fn.milliseconds=makeAccessor("Milliseconds",false);moment.fn.second=moment.fn.seconds=makeAccessor("Seconds",false);moment.fn.minute=moment.fn.minutes=makeAccessor("Minutes",false);moment.fn.hour=moment.fn.hours=makeAccessor("Hours",true);moment.fn.date=makeAccessor("Date",true);moment.fn.dates=deprecate("dates accessor is deprecated. Use date instead.",makeAccessor("Date",true));moment.fn.year=makeAccessor("FullYear",true);moment.fn.years=deprecate("years accessor is deprecated. Use year instead.",makeAccessor("FullYear",true));moment.fn.days=moment.fn.day;moment.fn.months=moment.fn.month;moment.fn.weeks=moment.fn.week;moment.fn.isoWeeks=moment.fn.isoWeek;moment.fn.quarters=moment.fn.quarter;moment.fn.toJSON=moment.fn.toISOString;moment.fn.isUTC=moment.fn.isUtc;function daysToYears(days){return days*400/146097}function yearsToDays(years){return years*146097/400}extend(moment.duration.fn=Duration.prototype,{_bubble:function(){var milliseconds=this._milliseconds,days=this._days,months=this._months,data=this._data,seconds,minutes,hours,years=0;data.milliseconds=milliseconds%1e3;seconds=absRound(milliseconds/1e3);data.seconds=seconds%60;minutes=absRound(seconds/60);data.minutes=minutes%60;hours=absRound(minutes/60);data.hours=hours%24;days+=absRound(hours/24);years=absRound(daysToYears(days));days-=absRound(yearsToDays(years));months+=absRound(days/30);days%=30;years+=absRound(months/12);months%=12;data.days=days;data.months=months;data.years=years},abs:function(){this._milliseconds=Math.abs(this._milliseconds);this._days=Math.abs(this._days);this._months=Math.abs(this._months);this._data.milliseconds=Math.abs(this._data.milliseconds);this._data.seconds=Math.abs(this._data.seconds);this._data.minutes=Math.abs(this._data.minutes);this._data.hours=Math.abs(this._data.hours);this._data.months=Math.abs(this._data.months);this._data.years=Math.abs(this._data.years);return this},weeks:function(){return absRound(this.days()/7)},valueOf:function(){return this._milliseconds+this._days*864e5+this._months%12*2592e6+toInt(this._months/12)*31536e6},humanize:function(withSuffix){var output=relativeTime(this,!withSuffix,this.localeData());if(withSuffix){output=this.localeData().pastFuture(+this,output)}return this.localeData().postformat(output)},add:function(input,val){var dur=moment.duration(input,val);this._milliseconds+=dur._milliseconds;this._days+=dur._days;this._months+=dur._months;this._bubble();return this},subtract:function(input,val){var dur=moment.duration(input,val);this._milliseconds-=dur._milliseconds;this._days-=dur._days;this._months-=dur._months;this._bubble();return this},get:function(units){units=normalizeUnits(units);return this[units.toLowerCase()+"s"]()},as:function(units){var days,months;units=normalizeUnits(units);if(units==="month"||units==="year"){days=this._days+this._milliseconds/864e5;months=this._months+daysToYears(days)*12;return units==="month"?months:months/12}else{days=this._days+Math.round(yearsToDays(this._months/12));switch(units){case"week":return days/7+this._milliseconds/6048e5;case"day":return days+this._milliseconds/864e5;case"hour":return days*24+this._milliseconds/36e5;case"minute":return days*24*60+this._milliseconds/6e4;case"second":return days*24*60*60+this._milliseconds/1e3;case"millisecond":return Math.floor(days*24*60*60*1e3)+this._milliseconds;default:throw new Error("Unknown unit "+units)}}},lang:moment.fn.lang,locale:moment.fn.locale,toIsoString:deprecate("toIsoString() is deprecated. Please use toISOString() instead "+"(notice the capitals)",function(){return this.toISOString()}),toISOString:function(){var years=Math.abs(this.years()),months=Math.abs(this.months()),days=Math.abs(this.days()),hours=Math.abs(this.hours()),minutes=Math.abs(this.minutes()),seconds=Math.abs(this.seconds()+this.milliseconds()/1e3);if(!this.asSeconds()){return"P0D"}return(this.asSeconds()<0?"-":"")+"P"+(years?years+"Y":"")+(months?months+"M":"")+(days?days+"D":"")+(hours||minutes||seconds?"T":"")+(hours?hours+"H":"")+(minutes?minutes+"M":"")+(seconds?seconds+"S":"")},localeData:function(){return this._locale},toJSON:function(){return this.toISOString()}});moment.duration.fn.toString=moment.duration.fn.toISOString;function makeDurationGetter(name){moment.duration.fn[name]=function(){return this._data[name]}}for(i in unitMillisecondFactors){if(hasOwnProp(unitMillisecondFactors,i)){makeDurationGetter(i.toLowerCase())}}moment.duration.fn.asMilliseconds=function(){return this.as("ms")};moment.duration.fn.asSeconds=function(){return this.as("s")};moment.duration.fn.asMinutes=function(){return this.as("m")};moment.duration.fn.asHours=function(){return this.as("h")};moment.duration.fn.asDays=function(){return this.as("d")};moment.duration.fn.asWeeks=function(){return this.as("weeks")};moment.duration.fn.asMonths=function(){return this.as("M")};moment.duration.fn.asYears=function(){return this.as("y")};moment.locale("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(number){var b=number%10,output=toInt(number%100/10)===1?"th":b===1?"st":b===2?"nd":b===3?"rd":"th";return number+output}});function makeGlobal(shouldDeprecate){if(typeof ender!=="undefined"){return}oldGlobalMoment=globalScope.moment;if(shouldDeprecate){globalScope.moment=deprecate("Accessing Moment through the global scope is "+"deprecated, and will be removed in an upcoming "+"release.",moment)}else{globalScope.moment=moment}}if(hasModule){module.exports=moment}else if(typeof define==="function"&&define.amd){define(function(require,exports,module){if(module.config&&module.config()&&module.config().noGlobal===true){globalScope.moment=oldGlobalMoment}return moment});makeGlobal(true)}else{makeGlobal()}}).call(this); \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/polymer/layouts/partial-states.html b/homeassistant/components/frontend/www_static/polymer/layouts/partial-states.html index 11c29c05a55..079044f3fea 100644 --- a/homeassistant/components/frontend/www_static/polymer/layouts/partial-states.html +++ b/homeassistant/components/frontend/www_static/polymer/layouts/partial-states.html @@ -143,8 +143,9 @@ return !(state.domain in uiConstants.STATE_FILTERS); }); } - - this.states = states.toArray(); + + this.states = states.toArray().filter( + function (el) {return !el.attributes.hidden}); }, handleRefreshClick: function() { diff --git a/scripts/build_frontend b/scripts/build_frontend index 406aed45f46..434f4b24d98 100755 --- a/scripts/build_frontend +++ b/scripts/build_frontend @@ -34,5 +34,5 @@ if [ $(command -v md5) ]; then elif [ $(command -v md5sum) ]; then echo 'VERSION = "'`md5sum www_static/frontend.html | cut -c-32`'"' >> version.py else - echo 'Could not find a MD5 utility' + echo 'Could not find an MD5 utility' fi From b20424261cb96b54f308e0a7f426e9d7fc925337 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Wed, 15 Apr 2015 02:05:34 -0400 Subject: [PATCH 12/25] 1) Performed many pylint and flake8 fixes to clean up isy994 integration and hidden entities addition. 2) Added necessary code to allow groups to also be hidden. 3) Made most of the weather data from the isy994 component be hidden by default. --- homeassistant/__init__.py | 4 +- homeassistant/bootstrap.py | 2 +- homeassistant/components/group.py | 28 +++++++++++++- homeassistant/components/isy994.py | 47 +++++++++++++++-------- homeassistant/components/sensor/isy994.py | 28 +++++++++++--- homeassistant/helpers/entity.py | 6 +-- 6 files changed, 85 insertions(+), 30 deletions(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 80916011d55..1f8f88d3723 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -625,8 +625,8 @@ class StateMachine(object): # components that don't use the Entity base class for their entities. # The sun component is an example of this. The Entity class cannot be # imported cleanly, so assume the state is shown. This means that for - # visibility to be supported, the state must originate from a class that - # uses the base class Entity or it must manually put the hidden + # visibility to be supported, the state must originate from a class + # that uses the base class Entity or it must manually put the hidden # attribute in its attributes dictionary. if ATTR_HIDDEN not in attributes: attributes[ATTR_HIDDEN] = False diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index c0b8fe09af3..e31a4bb66c7 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -208,7 +208,7 @@ def process_ha_core_config(hass, config): if key in config: setattr(hass.config, attr, config[key]) - Entity._visibility.update(config.get('visibility', [{}])[0]) + Entity.visibility.update(config.get('visibility', [{}])[0]) if CONF_TEMPERATURE_UNIT in config: unit = config[CONF_TEMPERATURE_UNIT] diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 4c5e6adb2c6..badd5ac66fe 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -7,10 +7,11 @@ Provides functionality to group devices that can be turned on or off. import homeassistant as ha from homeassistant.helpers import generate_entity_id +from homeassistant.helpers.entity import Entity import homeassistant.util as util from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_ON, STATE_OFF, - STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN) + STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN, ATTR_HIDDEN) DOMAIN = "group" DEPENDENCIES = [] @@ -112,6 +113,10 @@ def setup(hass, config): class Group(object): """ Tracks a group of entity ids. """ + + visibility = Entity.visibility + _hidden = False + def __init__(self, hass, name, entity_ids=None, user_defined=True): self.hass = hass self.name = name @@ -138,7 +143,8 @@ class Group(object): return { ATTR_ENTITY_ID: self.tracking, ATTR_AUTO: not self.user_defined, - ATTR_FRIENDLY_NAME: self.name + ATTR_FRIENDLY_NAME: self.name, + ATTR_HIDDEN: self.hidden } def update_tracked_entity_ids(self, entity_ids): @@ -213,6 +219,24 @@ class Group(object): self.hass.states.set( self.entity_id, group_off, self.state_attr) + @property + def hidden(self): + """ + Returns the official decision of whether the entity should be hidden. + Any value set by the user in the configuration file will overwrite + whatever the component sets for visibility. + """ + if self.entity_id is not None and \ + self.entity_id.lower() in self.visibility: + return self.visibility[self.entity_id.lower()] == 'hide' + else: + return self._hidden + + @hidden.setter + def hidden(self, val): + """ Sets the suggestion for visibility. """ + self._hidden = bool(val) + def setup_group(hass, name, entity_ids, user_defined=True): """ Sets up a group state that is the combined state of diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index d364007d12c..33d11c8ebbe 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -29,15 +29,19 @@ SENSOR_STRING = 'Sensor' HIDDEN_STRING = '{HIDE ME}' # setup logger -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) +_LOGGER = logging.getLogger(__name__) def setup(hass, config): + """ + Setup isy994 component. + This will automatically import associated lights, switches, and sensors. + """ + # pylint: disable=global-statement # check for required values in configuration file if not validate_config(config, {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, - logger): + _LOGGER): return False # pull and parse standard configuration @@ -52,7 +56,7 @@ def setup(hass, config): addr = addr.replace('https://', '') https = True else: - logger.error('isy994 host value in configuration file is invalid.') + _LOGGER.error('isy994 host value in configuration file is invalid.') return False port = host.port addr = addr.replace(':{}'.format(port), '') @@ -65,7 +69,7 @@ def setup(hass, config): # connect to ISY controller global ISY - ISY = PyISY.ISY(addr, port, user, password, use_https=https, log=logger) + ISY = PyISY.ISY(addr, port, user, password, use_https=https, log=_LOGGER) if not ISY.connected: return False @@ -91,6 +95,7 @@ class ISYDeviceABC(ToggleEntity): _states = [] _dtype = None _domain = None + _name = None def __init__(self, node): # setup properties @@ -98,34 +103,39 @@ class ISYDeviceABC(ToggleEntity): self.hidden = HIDDEN_STRING in self.raw_name # track changes - self._changeHandler = self.node.status. \ - subscribe('changed', self.onUpdate) + self._change_handler = self.node.status. \ + subscribe('changed', self.on_update) def __del__(self): """ cleanup subscriptions because it is the right thing to do. """ - self._changeHandler.unsubscribe() + self._change_handler.unsubscribe() @property def domain(self): + """ Returns the domain of the entity. """ return self._domain @property def dtype(self): + """ Returns the data type of the entity (binary or analog). """ if self._dtype in ['analog', 'binary']: return self._dtype - return 'binary' if self._units is None else 'analog' + return 'binary' if self.unit_of_measurement is None else 'analog' @property def should_poll(self): + """ Tells Home Assistant not to poll this entity. """ return False @property def value(self): """ returns the unclean value from the controller """ + # pylint: disable=protected-access return self.node.status._val @property def state_attributes(self): + """ Returns the state attributes for the node. """ attr = {ATTR_FRIENDLY_NAME: self.name} for name, prop in self._attrs.items(): attr[name] = getattr(self, prop) @@ -134,18 +144,18 @@ class ISYDeviceABC(ToggleEntity): @property def unique_id(self): """ Returns the id of this isy sensor """ + # pylint: disable=protected-access return self.node._id @property def raw_name(self): - try: - return str(self._name) - except AttributeError: - return str(self.node.name) + """ Returns the unclean node name. """ + return str(self._name) \ + if self._name is not None else str(self.node.name) @property def name(self): - """ Returns the name of the node if any. """ + """ Returns the cleaned name of the node. """ return self.raw_name.replace(HIDDEN_STRING, '').strip() def update(self): @@ -153,16 +163,18 @@ class ISYDeviceABC(ToggleEntity): # ISY objects are automatically updated by the ISY's event stream pass - def onUpdate(self, e): + def on_update(self, event): """ Handles the update received event. """ self.update_ha_state() @property def is_on(self): + """ Returns boolean response if the node is on. """ return bool(self.value) @property def is_open(self): + """ Returns boolean respons if the node is open. On = Open. """ return self.is_on @property @@ -178,17 +190,18 @@ class ISYDeviceABC(ToggleEntity): attrs = [kwargs.get(name) for name in self._onattrs] self.node.on(*attrs) else: - logger.error('ISY cannot turn on sensors.') + _LOGGER.error('ISY cannot turn on sensors.') def turn_off(self, **kwargs): """ turns the device off """ if self.domain is not 'sensor': self.node.off() else: - logger.error('ISY cannot turn off sensors.') + _LOGGER.error('ISY cannot turn off sensors.') @property def unit_of_measurement(self): + """ Returns the defined units of measurement or None. """ try: return self.node.units except AttributeError: diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index aa98c594910..9b58a574527 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -3,13 +3,29 @@ import logging # homeassistant imports -from homeassistant.components.isy994 import ISY, ISYDeviceABC, SENSOR_STRING +from homeassistant.components.isy994 import (ISY, ISYDeviceABC, SENSOR_STRING, + HIDDEN_STRING) from homeassistant.const import (STATE_OPEN, STATE_CLOSED, STATE_HOME, STATE_NOT_HOME, STATE_ON, STATE_OFF) +DEFAULT_HIDDEN_WEATHER = ['Temperature_High', 'Temperature_Low', 'Feels_Like', + 'Temperature_Average', 'Pressure', 'Dew_Point', + 'Gust_Speed', 'Evapotranspiration', + 'Irrigation_Requirement', 'Water_Deficit_Yesterday', + 'Elevation', 'Average_Temperature_Tomorrow', + 'High_Temperature_Tomorrow', + 'Low_Temperature_Tomorrow', 'Humidity_Tomorrow', + 'Wind_Speed_Tomorrow', 'Gust_Speed_Tomorrow', + 'Rain_Tomorrow', 'Snow_Tomorrow', + 'Forecast_Average_Temperature', + 'Forecast_High_Temperature', + 'Forecast_Low_Temperature', 'Forecast_Humidity', + 'Forecast_Rain', 'Forecast_Snow'] + def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the isy994 platform. """ + # pylint: disable=protected-access logger = logging.getLogger(__name__) devs = [] # verify connection @@ -21,7 +37,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if ISY.climate is not None: for prop in ISY.climate._id2name: if prop is not None: - node = WeatherPseudoNode('ISY.weather.' + prop, prop, + prefix = HIDDEN_STRING if prop in DEFAULT_HIDDEN_WEATHER else '' + node = WeatherPseudoNode('ISY.weather.' + prop, prefix + prop, getattr(ISY.climate, prop), getattr(ISY.climate, prop + '_units')) devs.append(ISYSensorDevice(node)) @@ -42,7 +59,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # folder does not exist pass else: - for dtype, name, node_id in folder.children: + for _, _, node_id in folder.children: node = folder[node_id].leaf devs.append(ISYSensorDevice(node, states)) @@ -51,6 +68,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class WeatherPseudoNode(object): """ This class allows weather variable to act as regular nodes. """ + # pylint: disable=too-few-public-methods def __init__(self, device_id, name, status, units=None): self._id = device_id @@ -64,6 +82,6 @@ class ISYSensorDevice(ISYDeviceABC): _domain = 'sensor' - def __init__(self, node, states=[]): + def __init__(self, node, states=None): super().__init__(node) - self._states = states + self._states = states or [] diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 3e559d39532..7d6ef65e1a7 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -83,7 +83,7 @@ class Entity(object): hass = None entity_id = None - _visibility = {} + visibility = {} def update_ha_state(self, force_refresh=False): """ @@ -138,8 +138,8 @@ class Entity(object): whatever the component sets for visibility. """ if self.entity_id is not None and \ - self.entity_id.lower() in self._visibility: - return self._visibility[self.entity_id.lower()] == 'hide' + self.entity_id.lower() in self.visibility: + return self.visibility[self.entity_id.lower()] == 'hide' else: return self._hidden From 6b2dd69bcb6b516a11b2a4139dbb6ef4aff6ea47 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Fri, 17 Apr 2015 09:27:14 -0400 Subject: [PATCH 13/25] Updated isy994 component to hide any device with the HIDDEN STRING in its ancestry. --- homeassistant/components/group.py | 1 + homeassistant/components/light/isy994.py | 7 +++++-- homeassistant/components/sensor/isy994.py | 7 +++++-- homeassistant/components/switch/isy994.py | 7 +++++-- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index badd5ac66fe..5c1e8268865 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -113,6 +113,7 @@ def setup(hass, config): class Group(object): """ Tracks a group of entity ids. """ + # pylint: disable=too-many-instance-attributes visibility = Entity.visibility _hidden = False diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index 60a4faafe13..ae0225a1e3c 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -3,7 +3,8 @@ import logging # homeassistant imports -from homeassistant.components.isy994 import ISYDeviceABC, ISY, SENSOR_STRING +from homeassistant.components.isy994 import (ISYDeviceABC, ISY, SENSOR_STRING, + HIDDEN_STRING) from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import STATE_ON, STATE_OFF @@ -18,8 +19,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False # import dimmable nodes - for node in ISY.nodes: + for (path, node) in ISY.nodes: if node.dimmable and SENSOR_STRING not in node.name: + if HIDDEN_STRING in path: + node.name += HIDDEN_STRING devs.append(ISYLightDevice(node)) add_devices(devs) diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index 9b58a574527..739a058d24d 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -37,15 +37,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if ISY.climate is not None: for prop in ISY.climate._id2name: if prop is not None: - prefix = HIDDEN_STRING if prop in DEFAULT_HIDDEN_WEATHER else '' + prefix = HIDDEN_STRING \ + if prop in DEFAULT_HIDDEN_WEATHER else '' node = WeatherPseudoNode('ISY.weather.' + prop, prefix + prop, getattr(ISY.climate, prop), getattr(ISY.climate, prop + '_units')) devs.append(ISYSensorDevice(node)) # import sensor nodes - for node in ISY.nodes: + for (path, node) in ISY.nodes: if SENSOR_STRING in node.name: + if HIDDEN_STRING in path: + node.name += HIDDEN_STRING devs.append(ISYSensorDevice(node, [STATE_ON, STATE_OFF])) # import sensor programs diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py index 1ea87e3fc2e..e6432173fc9 100644 --- a/homeassistant/components/switch/isy994.py +++ b/homeassistant/components/switch/isy994.py @@ -3,7 +3,8 @@ import logging # homeassistant imports -from homeassistant.components.isy994 import ISY, ISYDeviceABC, SENSOR_STRING +from homeassistant.components.isy994 import (ISY, ISYDeviceABC, SENSOR_STRING, + HIDDEN_STRING) from homeassistant.const import STATE_ON, STATE_OFF # STATE_OPEN, STATE_CLOSED # The frontend doesn't seem to fully support the open and closed states yet. # Once it does, the HA.doors programs should report open and closed instead of @@ -20,8 +21,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False # import not dimmable nodes and groups - for node in ISY.nodes: + for (path, node) in ISY.nodes: if not node.dimmable and SENSOR_STRING not in node.name: + if HIDDEN_STRING in path: + node.name += HIDDEN_STRING devs.append(ISYSwitchDevice(node)) # import ISY doors programs From da4cf61a09b5f14304f9ebdf012988405cad42a1 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Fri, 17 Apr 2015 09:30:20 -0400 Subject: [PATCH 14/25] Forced the isy994 component to treat underscores as spaces. --- homeassistant/components/isy994.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 33d11c8ebbe..f6ecebb9b2c 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -156,7 +156,8 @@ class ISYDeviceABC(ToggleEntity): @property def name(self): """ Returns the cleaned name of the node. """ - return self.raw_name.replace(HIDDEN_STRING, '').strip() + return self.raw_name.replace(HIDDEN_STRING, '').strip() \ + .replace('_', ' ') def update(self): """ Update state of the sensor. """ From 9a2e6dcba54588d4650167a53763589b4e984d99 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sat, 18 Apr 2015 00:26:40 -0400 Subject: [PATCH 15/25] Added a script for listing entities in running Home Assistant server. Usefule for creating visibility list in configuration file. --- scripts/get_entities.py | 141 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100755 scripts/get_entities.py diff --git a/scripts/get_entities.py b/scripts/get_entities.py new file mode 100755 index 00000000000..e2a5ff50c1a --- /dev/null +++ b/scripts/get_entities.py @@ -0,0 +1,141 @@ +#! /usr/bin/python +""" +get_entities.py + +Usage: get_entities.py [OPTION] ... [ATTRIBUTE] ... + +Query the Home Assistant API for available entities then print them and any +desired attributes to the screen. + +Options: + -h, --help display this text + --password=PASS use the supplied password + --ask-password prompt for password + -a, --address=ADDR use the supplied server address + -p, --port=PORT use the supplied server port +""" +import sys +import getpass +try: + from urllib2 import urlopen + PYTHON = 2 +except ImportError: + from urllib.request import urlopen + PYTHON = 3 +import json + + +def main(password, askpass, attrs, address, port): + """ fetch Home Assistant api json page and post process """ + # ask for password + if askpass: + password = getpass.getpass('Home Assistant API Password: ') + + # fetch API result + url = mk_url(address, port, password) + response = urlopen(url).read() + if PYTHON == 3: + response = response.decode('utf-8') + data = json.loads(response) + + # parse data + output = {'entity_id': []} + output.update([(attr, []) for attr in attrs]) + for item in data: + output['entity_id'].append(item['entity_id']) + for attr in attrs: + output[attr].append(item['attributes'].get(attr, '')) + + # output data + print_table(output, 'entity_id') + + +def print_table(data, first_key): + """ format and print a table of data from a dictionary """ + # get column lengths + lengths = {} + for key, value in data.items(): + lengths[key] = max([len(str(val)) for val in value] + [len(key)]) + + # construct the column order + columns = sorted(list(data.keys())) + ind = columns.index(first_key) + columns.pop(ind) + columns = [first_key] + columns + + # print header + for item in columns: + itemup = item.upper() + sys.stdout.write(itemup + ' ' * (lengths[item] - len(item) + 4)) + sys.stdout.write('\n') + + # print body + for ind in range(len(data[columns[0]])): + for item in columns: + val = str(data[item][ind]) + sys.stdout.write(val + ' ' * (lengths[item] - len(val) + 4)) + sys.stdout.write("\n") + + +def mk_url(address, port, password): + """ construct the url call for the api states page """ + url = '' + if address.startswith('http://'): + url += address + else: + url += 'http://' + address + url += ':' + port + '/api/states?' + if password is not None: + url += 'api_password=' + password + return url + + +def parse(option, all_options): + """ either update the options or set it to be updated next time """ + if len(option) > 1: + all_options[option[0]] = option[1] + return (all_options, None) + else: + return (all_options, option) + + +if __name__ == "__main__": + all_options = {'password': None, 'askpass': False, 'attrs': [], + 'address': 'localhost', 'port': '8123'} + + # parse arguments + next_key = None + for arg in sys.argv[1:]: + if next_key is None: + option = arg.split('=') + + if option[0] in ['-h', '--help']: + print(__doc__) + sys.exit(0) + + elif option[0] == '--password': + all_options['password'] = '='.join(option[1:]) + + elif option[0] == '--ask-password': + all_options['askpass'] = True + + elif option[0] == '-a': + next_key = 'address' + + elif option[0] == '--address': + all_options['address'] = '='.join(option[1:]) + + elif option[0] == '-p': + next_key = 'port' + + elif option[0] == '--port': + all_options['port'] = '='.join(option[1]) + + else: + all_options['attrs'].append('='.join(option)) + + else: + all_options[next_key] = arg + next_key = None + + main(**all_options) From 0032e4b6cf44936f8744fc4612f796ee75872739 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sat, 18 Apr 2015 00:30:09 -0400 Subject: [PATCH 16/25] On second thought, make that script use the specified order, not a sorted order. --- scripts/get_entities.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/scripts/get_entities.py b/scripts/get_entities.py index e2a5ff50c1a..f89f83a8562 100755 --- a/scripts/get_entities.py +++ b/scripts/get_entities.py @@ -47,22 +47,16 @@ def main(password, askpass, attrs, address, port): output[attr].append(item['attributes'].get(attr, '')) # output data - print_table(output, 'entity_id') + print_table(output, ['entity_id'] + attrs) -def print_table(data, first_key): +def print_table(data, columns): """ format and print a table of data from a dictionary """ # get column lengths lengths = {} for key, value in data.items(): lengths[key] = max([len(str(val)) for val in value] + [len(key)]) - # construct the column order - columns = sorted(list(data.keys())) - ind = columns.index(first_key) - columns.pop(ind) - columns = [first_key] + columns - # print header for item in columns: itemup = item.upper() From 4d91c4a51bb22985b4bb5cb67c3b027196cfb7b7 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 21 Apr 2015 23:58:41 -0400 Subject: [PATCH 17/25] Updated contributing documentation to include details about hidding states. --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 973415c1866..83b95535e0c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,6 +27,7 @@ A state can have several attributes that will help the frontend in displaying yo - `friendly_name`: this name will be used as the name of the device - `entity_picture`: this picture will be shown instead of the domain icon - `unit_of_measurement`: this will be appended to the state in the interface + - `hidden`: This is a suggestion to the frontend on if the state should be hidden These attributes are defined in [homeassistant.components](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/__init__.py#L25). From 99ea0dc59dd08240538e158cfe7e4ad7c08d33ae Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 21 Apr 2015 23:59:56 -0400 Subject: [PATCH 18/25] Updated requirements to include PyISY. --- requirements.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9f0352ccef1..c7560c073e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,8 +34,11 @@ python-nest>=2.1 # z-wave pydispatcher>=2.0.5 +# isy994 +PyISY>=1.0.0 + # sensor.systemmonitor psutil>=2.2.1 #pushover notifications -python-pushover>=0.2 \ No newline at end of file +python-pushover>=0.2 From d566a328a31878b629d81c355e4be9d5ff6c2417 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Wed, 22 Apr 2015 00:22:48 -0400 Subject: [PATCH 19/25] pylint fix to isy switches. --- homeassistant/components/switch/isy994.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py index e6432173fc9..192b3c4a3d2 100644 --- a/homeassistant/components/switch/isy994.py +++ b/homeassistant/components/switch/isy994.py @@ -13,6 +13,7 @@ from homeassistant.const import STATE_ON, STATE_OFF # STATE_OPEN, STATE_CLOSED def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the isy994 platform. """ + # pylint: disable=too-many-locals logger = logging.getLogger(__name__) devs = [] # verify connection From 8fcf814eb6e050abb61d4033f2165d8d74196382 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Wed, 22 Apr 2015 21:07:23 -0400 Subject: [PATCH 20/25] Changed visbility property in configuration.yaml to a hash instead of a list of a single hash. --- homeassistant/bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index e31a4bb66c7..a87deaa76d5 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -208,7 +208,7 @@ def process_ha_core_config(hass, config): if key in config: setattr(hass.config, attr, config[key]) - Entity.visibility.update(config.get('visibility', [{}])[0]) + Entity.visibility.update(config.get('visibility', {})) if CONF_TEMPERATURE_UNIT in config: unit = config[CONF_TEMPERATURE_UNIT] From ff3dacedc0cb314e799e14bfd1c8f81256393f98 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Wed, 22 Apr 2015 21:21:50 -0400 Subject: [PATCH 21/25] Moved card visibility logic out of the Entity class and into a VisibilityABC. Then made the Group class inherit the VisibilityABC. No duplication of code now. This is definitely better. --- homeassistant/bootstrap.py | 4 +-- homeassistant/components/group.py | 26 ++------------ homeassistant/helpers/entity.py | 56 +++++++++++++++++++------------ 3 files changed, 38 insertions(+), 48 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index a87deaa76d5..17e8cb3391b 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -20,7 +20,7 @@ import homeassistant import homeassistant.loader as loader import homeassistant.components as core_components import homeassistant.components.group as group -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import VisibilityABC from homeassistant.const import ( EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, TEMP_CELCIUS, @@ -208,7 +208,7 @@ def process_ha_core_config(hass, config): if key in config: setattr(hass.config, attr, config[key]) - Entity.visibility.update(config.get('visibility', {})) + VisibilityABC.visibility.update(config.get('visibility', {})) if CONF_TEMPERATURE_UNIT in config: unit = config[CONF_TEMPERATURE_UNIT] diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 5c1e8268865..d3b4d628842 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -7,7 +7,7 @@ Provides functionality to group devices that can be turned on or off. import homeassistant as ha from homeassistant.helpers import generate_entity_id -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import VisibilityABC import homeassistant.util as util from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_ON, STATE_OFF, @@ -111,12 +111,8 @@ def setup(hass, config): return True -class Group(object): +class Group(VisibilityABC): """ Tracks a group of entity ids. """ - # pylint: disable=too-many-instance-attributes - - visibility = Entity.visibility - _hidden = False def __init__(self, hass, name, entity_ids=None, user_defined=True): self.hass = hass @@ -220,24 +216,6 @@ class Group(object): self.hass.states.set( self.entity_id, group_off, self.state_attr) - @property - def hidden(self): - """ - Returns the official decision of whether the entity should be hidden. - Any value set by the user in the configuration file will overwrite - whatever the component sets for visibility. - """ - if self.entity_id is not None and \ - self.entity_id.lower() in self.visibility: - return self.visibility[self.entity_id.lower()] == 'hide' - else: - return self._hidden - - @hidden.setter - def hidden(self, val): - """ Sets the suggestion for visibility. """ - self._hidden = bool(val) - def setup_group(hass, name, entity_ids, user_defined=True): """ Sets up a group state that is the combined state of diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 7d6ef65e1a7..737a0e5e359 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -12,7 +12,40 @@ from homeassistant.const import ( STATE_OFF, DEVICE_DEFAULT_NAME, TEMP_CELCIUS, TEMP_FAHRENHEIT) -class Entity(object): +class VisibilityABC(object): + """ + Abstract Class for including visibility logic. This class includes the + necessary methods and properties to consider a visibility suggestion form + the component and then determine visibility based on the options in the + configuration file. When using this abstract class, the value for the + hidden property must still be included in the attributes disctionary. The + Entity class takes care of this automatically. + """ + + entity_id = None + visibility = {} + _hidden = False + + @property + def hidden(self): + """ + Returns the official decision of whether the entity should be hidden. + Any value set by the user in the configuration file will overwrite + whatever the component sets for visibility. + """ + if self.entity_id is not None and \ + self.entity_id.lower() in self.visibility: + return self.visibility[self.entity_id.lower()] == 'hide' + else: + return self._hidden + + @hidden.setter + def hidden(self, val): + """ Sets the suggestion for visibility. """ + self._hidden = bool(val) + + +class Entity(VisibilityABC): """ ABC for Home Assistant entities. """ # pylint: disable=no-self-use @@ -20,8 +53,6 @@ class Entity(object): # The properties and methods here are safe to overwrite when inherting this # class. These may be used to customize the behavior of the entity. - _hidden = False # suggestion as to whether the entity should be hidden - @property def should_poll(self): """ @@ -83,7 +114,6 @@ class Entity(object): hass = None entity_id = None - visibility = {} def update_ha_state(self, force_refresh=False): """ @@ -130,24 +160,6 @@ class Entity(object): def __repr__(self): return "".format(self.name, self.state) - @property - def hidden(self): - """ - Returns the official decision of whether the entity should be hidden. - Any value set by the user in the configuration file will overwrite - whatever the component sets for visibility. - """ - if self.entity_id is not None and \ - self.entity_id.lower() in self.visibility: - return self.visibility[self.entity_id.lower()] == 'hide' - else: - return self._hidden - - @hidden.setter - def hidden(self, val): - """ Sets the suggestion for visibility. """ - self._hidden = bool(val) - class ToggleEntity(Entity): """ ABC for entities that can be turned on and off. """ From 2b6edd153ba5d1969fc32032f48473b631564121 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Wed, 22 Apr 2015 21:27:49 -0400 Subject: [PATCH 22/25] Fixed copy pasta error. --- homeassistant/components/switch/isy994.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py index 192b3c4a3d2..fe98bce69f9 100644 --- a/homeassistant/components/switch/isy994.py +++ b/homeassistant/components/switch/isy994.py @@ -1,4 +1,4 @@ -""" Support for ISY994 lights. """ +""" Support for ISY994 switch. """ # system imports import logging From d779662bdd664d3185077c0a788adace15508a1b Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Wed, 22 Apr 2015 22:02:54 -0400 Subject: [PATCH 23/25] Updated get_entities.py script to use argparse module. --- scripts/get_entities.py | 72 ++++++++++------------------------------- 1 file changed, 17 insertions(+), 55 deletions(-) diff --git a/scripts/get_entities.py b/scripts/get_entities.py index f89f83a8562..249a06f0d9b 100755 --- a/scripts/get_entities.py +++ b/scripts/get_entities.py @@ -1,21 +1,12 @@ #! /usr/bin/python """ -get_entities.py - -Usage: get_entities.py [OPTION] ... [ATTRIBUTE] ... - Query the Home Assistant API for available entities then print them and any desired attributes to the screen. - -Options: - -h, --help display this text - --password=PASS use the supplied password - --ask-password prompt for password - -a, --address=ADDR use the supplied server address - -p, --port=PORT use the supplied server port """ + import sys import getpass +import argparse try: from urllib2 import urlopen PYTHON = 2 @@ -84,52 +75,23 @@ def mk_url(address, port, password): return url -def parse(option, all_options): - """ either update the options or set it to be updated next time """ - if len(option) > 1: - all_options[option[0]] = option[1] - return (all_options, None) - else: - return (all_options, option) - - if __name__ == "__main__": all_options = {'password': None, 'askpass': False, 'attrs': [], 'address': 'localhost', 'port': '8123'} - # parse arguments - next_key = None - for arg in sys.argv[1:]: - if next_key is None: - option = arg.split('=') + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('attrs', metavar='ATTRIBUTE', type=str, nargs='*', + help='an attribute to read from the state') + parser.add_argument('--password', dest='password', default=None, + type=str, help='API password for the HA server') + parser.add_argument('--ask-password', dest='askpass', default=False, + action='store_const', const=True, + help='prompt for HA API password') + parser.add_argument('--addr', dest='address', + default='localhost', type=str, + help='address of the HA server') + parser.add_argument('--port', dest='port', default='8123', + type=str, help='port that HA is hosting on') - if option[0] in ['-h', '--help']: - print(__doc__) - sys.exit(0) - - elif option[0] == '--password': - all_options['password'] = '='.join(option[1:]) - - elif option[0] == '--ask-password': - all_options['askpass'] = True - - elif option[0] == '-a': - next_key = 'address' - - elif option[0] == '--address': - all_options['address'] = '='.join(option[1:]) - - elif option[0] == '-p': - next_key = 'port' - - elif option[0] == '--port': - all_options['port'] = '='.join(option[1]) - - else: - all_options['attrs'].append('='.join(option)) - - else: - all_options[next_key] = arg - next_key = None - - main(**all_options) + args = parser.parse_args() + main(args.password, args.askpass, args.attrs, args.address, args.port) From bd3b93f29057252c8d784d3e90ec754a37116cfe Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Wed, 22 Apr 2015 22:19:36 -0400 Subject: [PATCH 24/25] 1) Added visibility documentation to the CONTRIBUTING.md documentation. 2) Pylint fixes to homeassistant/helpers/entity.py --- CONTRIBUTING.md | 15 +++++++++++++++ homeassistant/helpers/entity.py | 1 + 2 files changed, 16 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 83b95535e0c..e6bc947d771 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,6 +31,21 @@ A state can have several attributes that will help the frontend in displaying yo These attributes are defined in [homeassistant.components](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/__init__.py#L25). +## Proper Visibility Handling ## + +Generally, when creating a new entity for Home Assistant you will want it to be a class that inherits the [homeassistant.helpers.entity.Entity](https://github.com/balloob/home-assistant/blob/master/homeassistant/helpers/entity.py] Class. If this is done, visibility will be handled for you. +You can set a suggestion for your entitie's visibility by setting the hidden property by doing something similar to the following. + +```python +self.hidden = True +``` + +This will SUGGEST that the active frontend hide the entity. This requires that the active frontend support hidden cards (the default frontend does) and that the value of hidden be included in your attributes dictionary (see above). The Entity abstract class will take care of this for you. + +Remember: The suggestion set by your component's code will always be overwritten by manual settings in the configuration.yaml file. This is why you may set hidden to be False, but the property may remain True (or vice-versa). + +If you would not like to use the Entity Abstract Class, you may also inherity the Visibility Abstract Class which will include the logic for the hidden property but not automatically add the hidden property to the attributes dictionary. If you use this class, ensure that your class correctly adds the hidden property to the attributes. + ## Working on the frontend The frontend is composed of Polymer web-components and compiled into the file `frontend.html`. During development you do not want to work with the compiled version but with the seperate files. To have Home Assistant serve the seperate files, set `development=1` for the http-component in your config. diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 737a0e5e359..07aaf9a234c 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -21,6 +21,7 @@ class VisibilityABC(object): hidden property must still be included in the attributes disctionary. The Entity class takes care of this automatically. """ + # pylint: disable=too-few-public-methods entity_id = None visibility = {} From dc4ff25d5bace9493d84cd442145c0a792e867d5 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Wed, 22 Apr 2015 23:10:51 -0400 Subject: [PATCH 25/25] 1) Upped the requirement for PyISY to version 1.0.2. 2) Omitted isy994 components from coveralls tests because it requires an external controller. --- .coveragerc | 5 +++++ requirements.txt | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index cc639df0c44..7cfcacaaa2d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -30,6 +30,11 @@ omit = homeassistant/components/device_tracker/ddwrt.py homeassistant/components/sensor/transmission.py + homeassistant/components/isy994.py + homeassistant/components/light/isy994.py + homeassistant/components/switch/isy994.py + homeassistant/components/sensor/isy994.py + [report] # Regexes for lines to exclude from consideration diff --git a/requirements.txt b/requirements.txt index d6ff370f941..e1b0a942f80 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,7 +35,7 @@ python-nest>=2.1 pydispatcher>=2.0.5 # isy994 -PyISY>=1.0.0 +PyISY>=1.0.2 # sensor.systemmonitor psutil>=2.2.1