diff --git a/.coveragerc b/.coveragerc index f19e37d00a1..46a945e760b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -17,6 +17,9 @@ omit = homeassistant/components/*/tellstick.py homeassistant/components/*/vera.py + homeassistant/components/ecobee.py + homeassistant/components/*/ecobee.py + homeassistant/components/verisure.py homeassistant/components/*/verisure.py @@ -29,7 +32,7 @@ omit = homeassistant/components/rfxtrx.py homeassistant/components/*/rfxtrx.py - homeassistant/components/ifttt.py + homeassistant/components/binary_sensor/arest.py homeassistant/components/browser.py homeassistant/components/camera/* homeassistant/components/device_tracker/actiontec.py @@ -48,6 +51,7 @@ omit = homeassistant/components/device_tracker/snmp.py homeassistant/components/discovery.py homeassistant/components/downloader.py + homeassistant/components/ifttt.py homeassistant/components/keyboard.py homeassistant/components/light/hue.py homeassistant/components/light/mqtt.py @@ -84,7 +88,6 @@ omit = homeassistant/components/sensor/glances.py homeassistant/components/sensor/mysensors.py homeassistant/components/sensor/openweathermap.py - homeassistant/components/switch/orvibo.py homeassistant/components/sensor/rest.py homeassistant/components/sensor/rpi_gpio.py homeassistant/components/sensor/sabnzbd.py @@ -98,10 +101,13 @@ omit = homeassistant/components/switch/command_switch.py homeassistant/components/switch/edimax.py homeassistant/components/switch/hikvisioncam.py + homeassistant/components/switch/mystrom.py + homeassistant/components/switch/orvibo.py homeassistant/components/switch/rest.py homeassistant/components/switch/rpi_gpio.py homeassistant/components/switch/transmission.py homeassistant/components/switch/wemo.py + homeassistant/components/thermostat/homematic.py homeassistant/components/thermostat/honeywell.py homeassistant/components/thermostat/nest.py homeassistant/components/thermostat/radiotherm.py diff --git a/.travis.yml b/.travis.yml index da3516554ef..f12d318b5d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,10 @@ sudo: false language: python cache: directories: - - $HOME/virtualenv/python3.4.2/ + - $HOME/virtualenv/python$TRAVIS_PYTHON_VERSION/ python: - - "3.4" + - 3.4.2 + - 3.5.0 install: - script/bootstrap_server script: diff --git a/Dockerfile b/Dockerfile index 9344ec65245..a1f9d459295 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,27 @@ -FROM python:3-onbuild +FROM python:3.4 MAINTAINER Paulus Schoutsen VOLUME /config -RUN pip3 install --no-cache-dir -r requirements_all.txt +RUN mkdir -p /usr/src/app +WORKDIR /usr/src/app # For the nmap tracker RUN apt-get update && \ apt-get install -y --no-install-recommends nmap net-tools && \ apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +COPY script/build_python_openzwave script/build_python_openzwave RUN apt-get update && \ apt-get install -y cython3 libudev-dev && \ apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ pip3 install "cython<0.23" && \ script/build_python_openzwave +COPY requirements_all.txt requirements_all.txt +RUN pip3 install --no-cache-dir -r requirements_all.txt + +# Copy source +COPY . . + CMD [ "python", "-m", "homeassistant", "--config", "/config" ] diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 4d68227d4d0..41377aadebf 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -82,7 +82,7 @@ def _setup_component(hass, domain, config): return True component = loader.get_component(domain) - missing_deps = [dep for dep in component.DEPENDENCIES + missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', []) if dep not in hass.config.components] if missing_deps: @@ -106,7 +106,7 @@ def _setup_component(hass, domain, config): # Assumption: if a component does not depend on groups # it communicates with devices - if group.DOMAIN not in component.DEPENDENCIES: + if group.DOMAIN not in getattr(component, 'DEPENDENCIES', []): hass.pool.add_worker() hass.bus.fire( @@ -133,14 +133,13 @@ def prepare_setup_platform(hass, config, domain, platform_name): return platform # Load dependencies - if hasattr(platform, 'DEPENDENCIES'): - for component in platform.DEPENDENCIES: - if not setup_component(hass, component, config): - _LOGGER.error( - 'Unable to prepare setup for platform %s because ' - 'dependency %s could not be initialized', platform_path, - component) - return None + for component in getattr(platform, 'DEPENDENCIES', []): + if not setup_component(hass, component, config): + _LOGGER.error( + 'Unable to prepare setup for platform %s because ' + 'dependency %s could not be initialized', platform_path, + component) + return None if not _handle_requirements(hass, platform, platform_path): return None diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index d3289e08e62..3f5e6362fb6 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -15,7 +15,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent DOMAIN = 'alarm_control_panel' -DEPENDENCIES = [] SCAN_INTERVAL = 30 ENTITY_ID_FORMAT = DOMAIN + '.{}' diff --git a/homeassistant/components/alarm_control_panel/demo.py b/homeassistant/components/alarm_control_panel/demo.py new file mode 100644 index 00000000000..0ace53167de --- /dev/null +++ b/homeassistant/components/alarm_control_panel/demo.py @@ -0,0 +1,13 @@ +""" +homeassistant.components.alarm_control_panel.demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Demo platform that has two fake alarm control panels. +""" +import homeassistant.components.alarm_control_panel.manual as manual + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Demo alarm control panels. """ + add_devices([ + manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10), + ]) diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index ca1816db9e6..63bc989f3df 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -18,8 +18,6 @@ from homeassistant.const import ( _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = [] - DEFAULT_ALARM_NAME = 'HA Alarm' DEFAULT_PENDING_TIME = 60 DEFAULT_TRIGGER_TIME = 120 diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 7ccc1f745e9..1e6e66baee0 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -18,10 +18,10 @@ from homeassistant.bootstrap import ERROR_LOG_FILENAME from homeassistant.const import ( URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM, URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS, - URL_API_CONFIG, URL_API_BOOTSTRAP, URL_API_ERROR_LOG, + URL_API_CONFIG, URL_API_BOOTSTRAP, URL_API_ERROR_LOG, URL_API_LOG_OUT, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL, HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, - HTTP_UNPROCESSABLE_ENTITY, CONTENT_TYPE_TEXT_PLAIN) + HTTP_UNPROCESSABLE_ENTITY) DOMAIN = 'api' @@ -36,10 +36,6 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config): """ Register the API with the HTTP interface. """ - if 'http' not in hass.config.components: - _LOGGER.error('Dependency http is not loaded') - return False - # /api - for validation purposes hass.http.register_path('GET', URL_API, _handle_get_api) @@ -93,6 +89,8 @@ def setup(hass, config): hass.http.register_path('GET', URL_API_ERROR_LOG, _handle_get_api_error_log) + hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out) + return True @@ -108,6 +106,7 @@ def _handle_get_api_stream(handler, path_match, data): wfile = handler.wfile write_lock = threading.Lock() block = threading.Event() + session_id = None restrict = data.get('restrict') if restrict: @@ -121,6 +120,7 @@ def _handle_get_api_stream(handler, path_match, data): try: wfile.write(msg.encode("UTF-8")) wfile.flush() + handler.server.sessions.extend_validation(session_id) except IOError: block.set() @@ -140,6 +140,7 @@ def _handle_get_api_stream(handler, path_match, data): handler.send_response(HTTP_OK) handler.send_header('Content-type', 'text/event-stream') + session_id = handler.set_session_cookie_header() handler.end_headers() hass.bus.listen(MATCH_ALL, forward_events) @@ -347,9 +348,15 @@ def _handle_get_api_components(handler, path_match, data): def _handle_get_api_error_log(handler, path_match, data): """ Returns the logged errors for this session. """ - error_path = handler.server.hass.config.path(ERROR_LOG_FILENAME) - with open(error_path, 'rb') as error_log: - handler.write_file_pointer(CONTENT_TYPE_TEXT_PLAIN, error_log) + handler.write_file(handler.server.hass.config.path(ERROR_LOG_FILENAME), + False) + + +def _handle_post_api_log_out(handler, path_match, data): + """ Log user out. """ + handler.send_response(HTTP_OK) + handler.destroy_session() + handler.end_headers() def _services_json(hass): diff --git a/homeassistant/components/arduino.py b/homeassistant/components/arduino.py index 0c278ceee63..88967ec1f74 100644 --- a/homeassistant/components/arduino.py +++ b/homeassistant/components/arduino.py @@ -19,7 +19,6 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) DOMAIN = "arduino" -DEPENDENCIES = [] REQUIREMENTS = ['PyMata==2.07a'] BOARD = None _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index d3ef80d7192..23d83f554ca 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -123,7 +123,7 @@ def _migrate_old_config(config): _LOGGER.warning( 'You are using an old configuration format. Please upgrade: ' - 'https://home-assistant.io/components/automation.html') + 'https://home-assistant.io/components/automation/') new_conf = { CONF_TRIGGER: dict(config), diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index ab3529235d6..9f099100084 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -14,6 +14,7 @@ from homeassistant.helpers.event import track_state_change CONF_ENTITY_ID = "entity_id" CONF_BELOW = "below" CONF_ABOVE = "above" +CONF_ATTRIBUTE = "attribute" _LOGGER = logging.getLogger(__name__) @@ -28,6 +29,7 @@ def trigger(hass, config, action): below = config.get(CONF_BELOW) above = config.get(CONF_ABOVE) + attribute = config.get(CONF_ATTRIBUTE) if below is None and above is None: _LOGGER.error("Missing configuration key." @@ -40,8 +42,8 @@ def trigger(hass, config, action): """ Listens for state changes and calls action. """ # Fire action if we go from outside range into range - if _in_range(to_s.state, above, below) and \ - (from_s is None or not _in_range(from_s.state, above, below)): + if _in_range(to_s, above, below, attribute) and \ + (from_s is None or not _in_range(from_s, above, below, attribute)): action() track_state_change( @@ -61,6 +63,7 @@ def if_action(hass, config): below = config.get(CONF_BELOW) above = config.get(CONF_ABOVE) + attribute = config.get(CONF_ATTRIBUTE) if below is None and above is None: _LOGGER.error("Missing configuration key." @@ -71,18 +74,19 @@ def if_action(hass, config): def if_numeric_state(): """ Test numeric state condition. """ state = hass.states.get(entity_id) - return state is not None and _in_range(state.state, above, below) + return state is not None and _in_range(state, above, below, attribute) return if_numeric_state -def _in_range(value, range_start, range_end): +def _in_range(state, range_start, range_end, attribute): """ Checks if value is inside the range """ - + value = (state.state if attribute is None + else state.attributes.get(attribute)) try: value = float(value) except ValueError: - _LOGGER.warn("Missing value in numeric check") + _LOGGER.warning("Missing value in numeric check") return False if range_start is not None and range_end is not None: diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 4bf7eccf41e..f0f800bd313 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -46,6 +46,7 @@ def trigger(hass, config, action): from_match = _in_zone(hass, zone_entity_id, from_s) if from_s else None to_match = _in_zone(hass, zone_entity_id, to_s) + # pylint: disable=too-many-boolean-expressions if event == EVENT_ENTER and not from_match and to_match or \ event == EVENT_LEAVE and from_match and not to_match: action() diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 2ef9e83cc30..ccfd57aff8c 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -14,7 +14,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.const import (STATE_ON, STATE_OFF) DOMAIN = 'binary_sensor' -DEPENDENCIES = [] SCAN_INTERVAL = 30 ENTITY_ID_FORMAT = DOMAIN + '.{}' diff --git a/homeassistant/components/binary_sensor/arest.py b/homeassistant/components/binary_sensor/arest.py new file mode 100644 index 00000000000..7eafca9f2ae --- /dev/null +++ b/homeassistant/components/binary_sensor/arest.py @@ -0,0 +1,107 @@ +""" +homeassistant.components.binary_sensor.arest +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The arest sensor will consume an exposed aREST API of a device. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.arest/ +""" +from datetime import timedelta +import logging + +import requests + +from homeassistant.util import Throttle +from homeassistant.components.binary_sensor import BinarySensorDevice + +_LOGGER = logging.getLogger(__name__) + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + +CONF_RESOURCE = 'resource' +CONF_PIN = 'pin' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Get the aREST binary sensor. """ + + resource = config.get(CONF_RESOURCE) + pin = config.get(CONF_PIN) + + if None in (resource, pin): + _LOGGER.error('Not all required config keys present: %s', + ', '.join((CONF_RESOURCE, CONF_PIN))) + return False + + try: + response = requests.get(resource, timeout=10).json() + except requests.exceptions.MissingSchema: + _LOGGER.error('Missing resource or schema in configuration. ' + 'Add http:// to your URL.') + return False + except requests.exceptions.ConnectionError: + _LOGGER.error('No route to device at %s. ' + 'Please check the IP address in the configuration file.', + resource) + return False + + arest = ArestData(resource, pin) + + add_devices([ArestBinarySensor(arest, + resource, + config.get('name', response['name']), + pin)]) + + +# pylint: disable=too-many-instance-attributes, too-many-arguments +class ArestBinarySensor(BinarySensorDevice): + """ Implements an aREST binary sensor for a pin. """ + + def __init__(self, arest, resource, name, pin): + self.arest = arest + self._resource = resource + self._name = name + self._pin = pin + self.update() + + if self._pin is not None: + request = requests.get('{}/mode/{}/i'.format + (self._resource, self._pin), timeout=10) + if request.status_code is not 200: + _LOGGER.error("Can't set mode. Is device offline?") + + @property + def name(self): + """ The name of the binary sensor. """ + return self._name + + @property + def is_on(self): + """ True if the binary sensor is on. """ + return bool(self.arest.data.get('state')) + + def update(self): + """ Gets the latest data from aREST API. """ + self.arest.update() + + +# pylint: disable=too-few-public-methods +class ArestData(object): + """ Class for handling the data retrieval for pins. """ + + def __init__(self, resource, pin): + self._resource = resource + self._pin = pin + self.data = {} + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """ Gets the latest data from aREST device. """ + try: + response = requests.get('{}/digital/{}'.format( + self._resource, self._pin), timeout=10) + self.data = {'state': response.json()['return_value']} + except requests.exceptions.ConnectionError: + _LOGGER.error("No route to device '%s'. Is device offline?", + self._resource) diff --git a/homeassistant/components/binary_sensor/demo.py b/homeassistant/components/binary_sensor/demo.py index a24b893c610..087d7405d9b 100644 --- a/homeassistant/components/binary_sensor/demo.py +++ b/homeassistant/components/binary_sensor/demo.py @@ -9,18 +9,17 @@ from homeassistant.components.binary_sensor import BinarySensorDevice def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Demo binary sensors. """ add_devices([ - DemoBinarySensor('Window Bathroom', True, None), - DemoBinarySensor('Floor Basement', False, None), + DemoBinarySensor('Basement Floor Wet', False), + DemoBinarySensor('Movement Backyard', True), ]) class DemoBinarySensor(BinarySensorDevice): """ A Demo binary sensor. """ - def __init__(self, name, state, icon=None): + def __init__(self, name, state): self._name = name self._state = state - self._icon = icon @property def should_poll(self): @@ -32,11 +31,6 @@ class DemoBinarySensor(BinarySensorDevice): """ Returns the name of the binary sensor. """ return self._name - @property - def icon(self): - """ Returns the icon to use for device if any. """ - return self._icon - @property def is_on(self): """ True if the binary sensor is on. """ diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py new file mode 100644 index 00000000000..cac991d4eb2 --- /dev/null +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -0,0 +1,76 @@ +""" +homeassistant.components.binary_sensor.mqtt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows to configure a MQTT binary sensor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.mqtt/ +""" +import logging +from homeassistant.components.binary_sensor import BinarySensorDevice +import homeassistant.components.mqtt as mqtt + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'MQTT Binary sensor' +DEFAULT_QOS = 0 +DEFAULT_PAYLOAD_ON = 'ON' +DEFAULT_PAYLOAD_OFF = 'OFF' + +DEPENDENCIES = ['mqtt'] + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Add MQTT binary sensor. """ + + if config.get('state_topic') is None: + _LOGGER.error('Missing required variable: state_topic') + return False + + add_devices([MqttBinarySensor( + hass, + config.get('name', DEFAULT_NAME), + config.get('state_topic', None), + config.get('qos', DEFAULT_QOS), + config.get('payload_on', DEFAULT_PAYLOAD_ON), + config.get('payload_off', DEFAULT_PAYLOAD_OFF))]) + + +# pylint: disable=too-many-arguments, too-many-instance-attributes +class MqttBinarySensor(BinarySensorDevice): + """ Represents a binary sensor that is updated by MQTT. """ + def __init__(self, hass, name, state_topic, qos, payload_on, payload_off): + self._hass = hass + self._name = name + self._state = False + self._state_topic = state_topic + self._payload_on = payload_on + self._payload_off = payload_off + self._qos = qos + + def message_received(topic, payload, qos): + """ A new MQTT message has been received. """ + if payload == self._payload_on: + self._state = True + self.update_ha_state() + elif payload == self._payload_off: + self._state = False + self.update_ha_state() + + mqtt.subscribe(hass, self._state_topic, message_received, self._qos) + + @property + def should_poll(self): + """ No polling needed. """ + return False + + @property + def name(self): + """ The name of the binary sensor. """ + return self._name + + @property + def is_on(self): + """ True if the binary sensor is on. """ + return self._state diff --git a/homeassistant/components/browser.py b/homeassistant/components/browser.py index db0f3710158..88548e2a1b3 100644 --- a/homeassistant/components/browser.py +++ b/homeassistant/components/browser.py @@ -8,7 +8,6 @@ https://home-assistant.io/components/browser/ """ DOMAIN = "browser" -DEPENDENCIES = [] SERVICE_BROWSE_URL = "browse_url" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index ae5fe28beac..fc5c739c888 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -7,19 +7,20 @@ Component to interface with various cameras. For more details about this component, please refer to the documentation at https://home-assistant.io/components/camera/ """ -import requests import logging -import time import re +import time + +import requests + from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.const import ( ATTR_ENTITY_PICTURE, HTTP_NOT_FOUND, ATTR_ENTITY_ID, ) -from homeassistant.helpers.entity_component import EntityComponent - DOMAIN = 'camera' DEPENDENCIES = ['http'] @@ -80,19 +81,21 @@ def setup(hass, config): def _proxy_camera_image(handler, path_match, data): """ Proxies the camera image via the HA server. """ entity_id = path_match.group(ATTR_ENTITY_ID) + camera = component.entities.get(entity_id) - camera = None - if entity_id in component.entities.keys(): - camera = component.entities[entity_id] - - if camera: - response = camera.camera_image() - if response is not None: - handler.wfile.write(response) - else: - handler.send_response(HTTP_NOT_FOUND) - else: + if camera is None: handler.send_response(HTTP_NOT_FOUND) + handler.end_headers() + return + + response = camera.camera_image() + + if response is None: + handler.send_response(HTTP_NOT_FOUND) + handler.end_headers() + return + + handler.wfile.write(response) hass.http.register_path( 'GET', @@ -108,12 +111,9 @@ def setup(hass, config): stream even with only a still image URL available. """ entity_id = path_match.group(ATTR_ENTITY_ID) + camera = component.entities.get(entity_id) - camera = None - if entity_id in component.entities.keys(): - camera = component.entities[entity_id] - - if not camera: + if camera is None: handler.send_response(HTTP_NOT_FOUND) handler.end_headers() return @@ -131,7 +131,6 @@ def setup(hass, config): # MJPEG_START_HEADER.format() while True: - img_bytes = camera.camera_image() if img_bytes is None: continue @@ -148,12 +147,12 @@ def setup(hass, config): handler.request.sendall( bytes('--jpgboundary\r\n', 'utf-8')) + time.sleep(0.5) + except (requests.RequestException, IOError): camera.is_streaming = False camera.update_ha_state() - camera.is_streaming = False - hass.http.register_path( 'GET', re.compile( diff --git a/homeassistant/components/camera/demo.py b/homeassistant/components/camera/demo.py index fc3ec263143..0ad992db86d 100644 --- a/homeassistant/components/camera/demo.py +++ b/homeassistant/components/camera/demo.py @@ -4,8 +4,8 @@ homeassistant.components.camera.demo Demo platform that has a fake camera. """ import os -from random import randint from homeassistant.components.camera import Camera +import homeassistant.util.dt as dt_util def setup_platform(hass, config, add_devices, discovery_info=None): @@ -24,12 +24,12 @@ class DemoCamera(Camera): def camera_image(self): """ Return a faked still image response. """ + now = dt_util.utcnow() image_path = os.path.join(os.path.dirname(__file__), - 'demo_{}.png'.format(randint(1, 5))) + 'demo_{}.jpg'.format(now.second % 4)) with open(image_path, 'rb') as file: - output = file.read() - return output + return file.read() @property def name(self): diff --git a/homeassistant/components/camera/demo_0.jpg b/homeassistant/components/camera/demo_0.jpg new file mode 100644 index 00000000000..ff87d5179f8 Binary files /dev/null and b/homeassistant/components/camera/demo_0.jpg differ diff --git a/homeassistant/components/camera/demo_1.jpg b/homeassistant/components/camera/demo_1.jpg new file mode 100644 index 00000000000..06166fffa85 Binary files /dev/null and b/homeassistant/components/camera/demo_1.jpg differ diff --git a/homeassistant/components/camera/demo_1.png b/homeassistant/components/camera/demo_1.png deleted file mode 100644 index fc681fccecd..00000000000 Binary files a/homeassistant/components/camera/demo_1.png and /dev/null differ diff --git a/homeassistant/components/camera/demo_2.jpg b/homeassistant/components/camera/demo_2.jpg new file mode 100644 index 00000000000..71356479ab0 Binary files /dev/null and b/homeassistant/components/camera/demo_2.jpg differ diff --git a/homeassistant/components/camera/demo_2.png b/homeassistant/components/camera/demo_2.png deleted file mode 100644 index 255cd5c45d4..00000000000 Binary files a/homeassistant/components/camera/demo_2.png and /dev/null differ diff --git a/homeassistant/components/camera/demo_3.jpg b/homeassistant/components/camera/demo_3.jpg new file mode 100644 index 00000000000..06166fffa85 Binary files /dev/null and b/homeassistant/components/camera/demo_3.jpg differ diff --git a/homeassistant/components/camera/demo_3.png b/homeassistant/components/camera/demo_3.png deleted file mode 100644 index b54c1ffb57c..00000000000 Binary files a/homeassistant/components/camera/demo_3.png and /dev/null differ diff --git a/homeassistant/components/camera/demo_4.png b/homeassistant/components/camera/demo_4.png deleted file mode 100644 index 4be36ee42d0..00000000000 Binary files a/homeassistant/components/camera/demo_4.png and /dev/null differ diff --git a/homeassistant/components/camera/demo_5.png b/homeassistant/components/camera/demo_5.png deleted file mode 100644 index 874b95ef6a5..00000000000 Binary files a/homeassistant/components/camera/demo_5.png and /dev/null differ diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index d4d707c790f..b210e1a2f1b 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -7,11 +7,12 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.foscam/ """ import logging -from homeassistant.helpers import validate_config -from homeassistant.components.camera import DOMAIN -from homeassistant.components.camera import Camera + import requests +from homeassistant.helpers import validate_config +from homeassistant.components.camera import DOMAIN, Camera + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index 7e9542908d5..c81febccc86 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -7,11 +7,12 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.generic/ """ import logging -from requests.auth import HTTPBasicAuth -from homeassistant.helpers import validate_config -from homeassistant.components.camera import DOMAIN -from homeassistant.components.camera import Camera + import requests +from requests.auth import HTTPBasicAuth + +from homeassistant.helpers import validate_config +from homeassistant.components.camera import DOMAIN, Camera _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index 1e643304add..0d59c8d60c7 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -6,13 +6,14 @@ Support for IP Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.mjpeg/ """ -import logging -from requests.auth import HTTPBasicAuth -from homeassistant.helpers import validate_config -from homeassistant.components.camera import DOMAIN -from homeassistant.components.camera import Camera -import requests from contextlib import closing +import logging + +import requests +from requests.auth import HTTPBasicAuth + +from homeassistant.helpers import validate_config +from homeassistant.components.camera import DOMAIN, Camera _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index 8bec580abf9..515daffc71c 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -15,7 +15,6 @@ from homeassistant.helpers import generate_entity_id from homeassistant.const import EVENT_TIME_CHANGED DOMAIN = "configurator" -DEPENDENCIES = [] ENTITY_ID_FORMAT = DOMAIN + ".{}" SERVICE_CONFIGURE = "configure" diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index d9cba832df7..7cd1193448c 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -14,7 +14,6 @@ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) DOMAIN = "conversation" -DEPENDENCIES = [] SERVICE_PROCESS = "process" diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 547a1d54a03..de87bfc9fb1 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -18,7 +18,7 @@ DEPENDENCIES = ['conversation', 'introduction', 'zone'] COMPONENTS_WITH_DEMO_PLATFORM = [ 'device_tracker', 'light', 'media_player', 'notify', 'switch', 'sensor', - 'thermostat', 'camera', 'binary_sensor'] + 'thermostat', 'camera', 'binary_sensor', 'alarm_control_panel', 'lock'] def setup(hass, config): @@ -55,23 +55,6 @@ def setup(hass, config): group.setup_group(hass, 'bedroom', [lights[0], switches[1], media_players[0]]) - # Setup IP Camera - bootstrap.setup_component( - hass, 'camera', - {'camera': { - 'platform': 'generic', - 'name': 'IP Camera', - 'still_image_url': 'http://home-assistant.io/demo/webcam.jpg', - }}) - - # Setup alarm_control_panel - bootstrap.setup_component( - hass, 'alarm_control_panel', - {'alarm_control_panel': { - 'platform': 'manual', - 'name': 'Test Alarm', - }}) - # Setup scripts bootstrap.setup_component( hass, 'script', diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index 1e45444c74e..bc8e8768be0 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -98,7 +98,7 @@ class NmapDeviceScanner(object): from nmap import PortScanner, PortScannerError scanner = PortScanner() - options = "-F --host-timeout 5" + options = "-F --host-timeout 5s" if self.home_interval: boundary = dt_util.now() - self.home_interval diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 3a43c86f58a..cfd6ffd55eb 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -17,8 +17,7 @@ from homeassistant.const import ( ATTR_SERVICE, ATTR_DISCOVERED) DOMAIN = "discovery" -DEPENDENCIES = [] -REQUIREMENTS = ['netdisco==0.5.1'] +REQUIREMENTS = ['netdisco==0.5.2'] SCAN_INTERVAL = 300 # seconds diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index a69a6ca1517..655bf7d4eb6 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -15,7 +15,6 @@ from homeassistant.helpers import validate_config from homeassistant.util import sanitize_filename DOMAIN = "downloader" -DEPENDENCIES = [] SERVICE_DOWNLOAD_FILE = "download_file" diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py new file mode 100644 index 00000000000..a517e2d5418 --- /dev/null +++ b/homeassistant/components/ecobee.py @@ -0,0 +1,155 @@ +""" +homeassistant.components.ecobee +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Ecobee Component + +This component adds support for Ecobee3 Wireless Thermostats. +You will need to setup developer access to your thermostat, +and create and API key on the ecobee website. + +The first time you run this component you will see a configuration +component card in Home Assistant. This card will contain a PIN code +that you will need to use to authorize access to your thermostat. You +can do this at https://www.ecobee.com/consumerportal/index.html +Click My Apps, Add application, Enter Pin and click Authorize. + +After authorizing the application click the button in the configuration +card. Now your thermostat and sensors should shown in home-assistant. + +You can use the optional hold_temp parameter to set whether or not holds +are set indefintely or until the next scheduled event. + +ecobee: + api_key: asdfasdfasdfasdfasdfaasdfasdfasdfasdf + hold_temp: True + +""" + +from datetime import timedelta +import logging +import os + +from homeassistant.loader import get_component +from homeassistant import bootstrap +from homeassistant.util import Throttle +from homeassistant.const import ( + EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, CONF_API_KEY) + +DOMAIN = "ecobee" +DISCOVER_THERMOSTAT = "ecobee.thermostat" +DISCOVER_SENSORS = "ecobee.sensor" +NETWORK = None +HOLD_TEMP = 'hold_temp' + +REQUIREMENTS = [ + 'https://github.com/nkgilley/python-ecobee-api/archive/' + '5645f843b64ac4f6e59dfb96233a07083c5e10c1.zip#python-ecobee==0.0.3'] + +_LOGGER = logging.getLogger(__name__) + +ECOBEE_CONFIG_FILE = 'ecobee.conf' +_CONFIGURING = {} + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) + + +def request_configuration(network, hass, config): + """ Request configuration steps from the user. """ + configurator = get_component('configurator') + if 'ecobee' in _CONFIGURING: + configurator.notify_errors( + _CONFIGURING['ecobee'], "Failed to register, please try again.") + + return + + # pylint: disable=unused-argument + def ecobee_configuration_callback(callback_data): + """ Actions to do when our configuration callback is called. """ + network.request_tokens() + network.update() + setup_ecobee(hass, network, config) + + _CONFIGURING['ecobee'] = configurator.request_config( + hass, "Ecobee", ecobee_configuration_callback, + description=( + 'Please authorize this app at https://www.ecobee.com/consumer' + 'portal/index.html with pin code: ' + network.pin), + description_image="/static/images/config_ecobee_thermostat.png", + submit_caption="I have authorized the app." + ) + + +def setup_ecobee(hass, network, config): + """ Setup ecobee thermostat """ + # If ecobee has a PIN then it needs to be configured. + if network.pin is not None: + request_configuration(network, hass, config) + return + + if 'ecobee' in _CONFIGURING: + configurator = get_component('configurator') + configurator.request_done(_CONFIGURING.pop('ecobee')) + + # Ensure component is loaded + bootstrap.setup_component(hass, 'thermostat', config) + bootstrap.setup_component(hass, 'sensor', config) + + hold_temp = config[DOMAIN].get(HOLD_TEMP, False) + + # Fire thermostat discovery event + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: DISCOVER_THERMOSTAT, + ATTR_DISCOVERED: {'hold_temp': hold_temp} + }) + + # Fire sensor discovery event + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: DISCOVER_SENSORS, + ATTR_DISCOVERED: {} + }) + + +# pylint: disable=too-few-public-methods +class EcobeeData(object): + """ Gets the latest data and update the states. """ + + def __init__(self, config_file): + from pyecobee import Ecobee + self.ecobee = Ecobee(config_file) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """ Get the latest data from pyecobee. """ + self.ecobee.update() + _LOGGER.info("ecobee data updated successfully.") + + +def setup(hass, config): + """ + Setup Ecobee. + Will automatically load thermostat and sensor components to support + devices discovered on the network. + """ + # pylint: disable=global-statement, import-error + global NETWORK + + if 'ecobee' in _CONFIGURING: + return + + from pyecobee import config_from_file + + # Create ecobee.conf if it doesn't exist + if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)): + if config[DOMAIN].get(CONF_API_KEY) is None: + _LOGGER.error("No ecobee api_key found in config.") + return + jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)} + config_from_file(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig) + + NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE)) + + setup_ecobee(hass, NETWORK.ecobee, config) + + return True diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 5a8fbed34e9..dac2041fa56 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -54,8 +54,7 @@ def setup(hass, config): def _handle_get_root(handler, path_match, data): - """ Renders the debug interface. """ - + """ Renders the frontend. """ handler.send_response(HTTP_OK) handler.send_header('Content-type', 'text/html; charset=utf-8') handler.end_headers() @@ -66,7 +65,7 @@ def _handle_get_root(handler, path_match, data): app_url = "frontend-{}.html".format(version.VERSION) # auto login if no password was set, else check api_password param - auth = ('no_password_set' if handler.server.no_password_set + auth = ('no_password_set' if handler.server.api_password is None else data.get('api_password', '')) with open(INDEX_PATH) as template_file: diff --git a/homeassistant/components/frontend/index.html.template b/homeassistant/components/frontend/index.html.template index 409ea6752db..87c5f6638a7 100644 --- a/homeassistant/components/frontend/index.html.template +++ b/homeassistant/components/frontend/index.html.template @@ -4,16 +4,13 @@ Home Assistant - - - + + + href='/static/favicon-apple-180x180.png'> - + -
- -
Initializing
-
+
diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 0199b05156e..789efa8988c 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 = "dff74f773ea8b0356b0bd8130ed6f0cf" +VERSION = "ece94598f1575599c5aefa7322b1423b" diff --git a/homeassistant/components/frontend/www_static/favicon-384x384.png b/homeassistant/components/frontend/www_static/favicon-384x384.png new file mode 100644 index 00000000000..51f67770790 Binary files /dev/null and b/homeassistant/components/frontend/www_static/favicon-384x384.png differ diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index b2a2eb97b3e..32df087296c 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,6 +1,6 @@ -
\ No newline at end of file + } \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 39e09d85b74..c130933f452 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 39e09d85b74afb332ad2872b5aa556c9c9d113c3 +Subproject commit c130933f45262dd1c7b4fd75d23a90c132fb271f diff --git a/homeassistant/components/frontend/www_static/images/config_ecobee_thermostat.png b/homeassistant/components/frontend/www_static/images/config_ecobee_thermostat.png new file mode 100644 index 00000000000..e62a4165c9b Binary files /dev/null and b/homeassistant/components/frontend/www_static/images/config_ecobee_thermostat.png differ diff --git a/homeassistant/components/frontend/www_static/manifest.json b/homeassistant/components/frontend/www_static/manifest.json index 69143ce5179..3767a4b1c5b 100644 --- a/homeassistant/components/frontend/www_static/manifest.json +++ b/homeassistant/components/frontend/www_static/manifest.json @@ -3,12 +3,17 @@ "short_name": "Assistant", "start_url": "/", "display": "standalone", + "theme_color": "#03A9F4", "icons": [ { - "src": "\/static\/favicon-192x192.png", + "src": "/static/favicon-192x192.png", "sizes": "192x192", - "type": "image\/png", - "density": "4.0" + "type": "image/png", + }, + { + "src": "/static/favicon-384x384.png", + "sizes": "384x384", + "type": "image/png", } ] } diff --git a/homeassistant/components/frontend/www_static/splash.png b/homeassistant/components/frontend/www_static/splash.png deleted file mode 100644 index 582140a2bc3..00000000000 Binary files a/homeassistant/components/frontend/www_static/splash.png and /dev/null differ diff --git a/homeassistant/components/frontend/www_static/webcomponents-lite.min.js b/homeassistant/components/frontend/www_static/webcomponents-lite.min.js index 3a3fd4e8564..4f8af01fd15 100644 --- a/homeassistant/components/frontend/www_static/webcomponents-lite.min.js +++ b/homeassistant/components/frontend/www_static/webcomponents-lite.min.js @@ -7,6 +7,6 @@ * Code distributed by Google as part of the polymer project is also * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */ -// @version 0.7.17 -!function(){window.WebComponents=window.WebComponents||{flags:{}};var e="webcomponents-lite.js",t=document.querySelector('script[src*="'+e+'"]'),n={};if(!n.noOpts){if(location.search.slice(1).split("&").forEach(function(e){var t,r=e.split("=");r[0]&&(t=r[0].match(/wc-(.+)/))&&(n[t[1]]=r[1]||!0)}),t)for(var r,o=0;r=t.attributes[o];o++)"src"!==r.name&&(n[r.name]=r.value||!0);if(n.log&&n.log.split){var i=n.log.split(",");n.log={},i.forEach(function(e){n.log[e]=!0})}else n.log={}}n.register&&(window.CustomElements=window.CustomElements||{flags:{}},window.CustomElements.flags.register=n.register),WebComponents.flags=n}(),function(e){"use strict";function t(e){return void 0!==h[e]}function n(){s.call(this),this._isInvalid=!0}function r(e){return""==e&&n.call(this),e.toLowerCase()}function o(e){var t=e.charCodeAt(0);return t>32&&127>t&&-1==[34,35,60,62,63,96].indexOf(t)?e:encodeURIComponent(e)}function i(e){var t=e.charCodeAt(0);return t>32&&127>t&&-1==[34,35,60,62,96].indexOf(t)?e:encodeURIComponent(e)}function a(e,a,s){function c(e){g.push(e)}var d=a||"scheme start",u=0,l="",_=!1,w=!1,g=[];e:for(;(e[u-1]!=p||0==u)&&!this._isInvalid;){var b=e[u];switch(d){case"scheme start":if(!b||!m.test(b)){if(a){c("Invalid scheme.");break e}l="",d="no scheme";continue}l+=b.toLowerCase(),d="scheme";break;case"scheme":if(b&&v.test(b))l+=b.toLowerCase();else{if(":"!=b){if(a){if(p==b)break e;c("Code point not allowed in scheme: "+b);break e}l="",u=0,d="no scheme";continue}if(this._scheme=l,l="",a)break e;t(this._scheme)&&(this._isRelative=!0),d="file"==this._scheme?"relative":this._isRelative&&s&&s._scheme==this._scheme?"relative or authority":this._isRelative?"authority first slash":"scheme data"}break;case"scheme data":"?"==b?(this._query="?",d="query"):"#"==b?(this._fragment="#",d="fragment"):p!=b&&" "!=b&&"\n"!=b&&"\r"!=b&&(this._schemeData+=o(b));break;case"no scheme":if(s&&t(s._scheme)){d="relative";continue}c("Missing scheme."),n.call(this);break;case"relative or authority":if("/"!=b||"/"!=e[u+1]){c("Expected /, got: "+b),d="relative";continue}d="authority ignore slashes";break;case"relative":if(this._isRelative=!0,"file"!=this._scheme&&(this._scheme=s._scheme),p==b){this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._username=s._username,this._password=s._password;break e}if("/"==b||"\\"==b)"\\"==b&&c("\\ is an invalid code point."),d="relative slash";else if("?"==b)this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query="?",this._username=s._username,this._password=s._password,d="query";else{if("#"!=b){var y=e[u+1],E=e[u+2];("file"!=this._scheme||!m.test(b)||":"!=y&&"|"!=y||p!=E&&"/"!=E&&"\\"!=E&&"?"!=E&&"#"!=E)&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password,this._path=s._path.slice(),this._path.pop()),d="relative path";continue}this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._fragment="#",this._username=s._username,this._password=s._password,d="fragment"}break;case"relative slash":if("/"!=b&&"\\"!=b){"file"!=this._scheme&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password),d="relative path";continue}"\\"==b&&c("\\ is an invalid code point."),d="file"==this._scheme?"file host":"authority ignore slashes";break;case"authority first slash":if("/"!=b){c("Expected '/', got: "+b),d="authority ignore slashes";continue}d="authority second slash";break;case"authority second slash":if(d="authority ignore slashes","/"!=b){c("Expected '/', got: "+b);continue}break;case"authority ignore slashes":if("/"!=b&&"\\"!=b){d="authority";continue}c("Expected authority, got: "+b);break;case"authority":if("@"==b){_&&(c("@ already seen."),l+="%40"),_=!0;for(var L=0;L>>0)+(t++ +"__")};n.prototype={set:function(t,n){var r=t[this.name];return r&&r[0]===t?r[1]=n:e(t,this.name,{value:[t,n],writable:!0}),this},get:function(e){var t;return(t=e[this.name])&&t[0]===e?t[1]:void 0},"delete":function(e){var t=e[this.name];return t&&t[0]===e?(t[0]=t[1]=void 0,!0):!1},has:function(e){var t=e[this.name];return t?t[0]===e:!1}},window.WeakMap=n}(),function(e){function t(e){b.push(e),g||(g=!0,m(r))}function n(e){return window.ShadowDOMPolyfill&&window.ShadowDOMPolyfill.wrapIfNeeded(e)||e}function r(){g=!1;var e=b;b=[],e.sort(function(e,t){return e.uid_-t.uid_});var t=!1;e.forEach(function(e){var n=e.takeRecords();o(e),n.length&&(e.callback_(n,e),t=!0)}),t&&r()}function o(e){e.nodes_.forEach(function(t){var n=v.get(t);n&&n.forEach(function(t){t.observer===e&&t.removeTransientObservers()})})}function i(e,t){for(var n=e;n;n=n.parentNode){var r=v.get(n);if(r)for(var o=0;o0){var o=n[r-1],i=f(o,e);if(i)return void(n[r-1]=i)}else t(this.observer);n[r]=e},addListeners:function(){this.addListeners_(this.target)},addListeners_:function(e){var t=this.options;t.attributes&&e.addEventListener("DOMAttrModified",this,!0),t.characterData&&e.addEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.addEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.addEventListener("DOMNodeRemoved",this,!0)},removeListeners:function(){this.removeListeners_(this.target)},removeListeners_:function(e){var t=this.options;t.attributes&&e.removeEventListener("DOMAttrModified",this,!0),t.characterData&&e.removeEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.removeEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.removeEventListener("DOMNodeRemoved",this,!0)},addTransientObserver:function(e){if(e!==this.target){this.addListeners_(e),this.transientObservedNodes.push(e);var t=v.get(e);t||v.set(e,t=[]),t.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[],e.forEach(function(e){this.removeListeners_(e);for(var t=v.get(e),n=0;n":return">";case" ":return" "}}function t(t){return t.replace(a,e)}var n="template",r=document.implementation.createHTMLDocument("template"),o=!0;HTMLTemplateElement=function(){},HTMLTemplateElement.prototype=Object.create(HTMLElement.prototype),HTMLTemplateElement.decorate=function(e){e.content||(e.content=r.createDocumentFragment());for(var n;n=e.firstChild;)e.content.appendChild(n);if(o)try{Object.defineProperty(e,"innerHTML",{get:function(){for(var e="",n=this.content.firstChild;n;n=n.nextSibling)e+=n.outerHTML||t(n.data);return e},set:function(e){for(r.body.innerHTML=e,HTMLTemplateElement.bootstrap(r);this.content.firstChild;)this.content.removeChild(this.content.firstChild);for(;r.body.firstChild;)this.content.appendChild(r.body.firstChild)},configurable:!0})}catch(i){o=!1}},HTMLTemplateElement.bootstrap=function(e){for(var t,r=e.querySelectorAll(n),o=0,i=r.length;i>o&&(t=r[o]);o++)HTMLTemplateElement.decorate(t)},document.addEventListener("DOMContentLoaded",function(){HTMLTemplateElement.bootstrap(document)});var i=document.createElement;document.createElement=function(){"use strict";var e=i.apply(document,arguments);return"template"==e.localName&&HTMLTemplateElement.decorate(e),e};var a=/[&\u00A0<>]/g}(),function(e){"use strict";if(!window.performance){var t=Date.now();window.performance={now:function(){return Date.now()-t}}}window.requestAnimationFrame||(window.requestAnimationFrame=function(){var e=window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame;return e?function(t){return e(function(){t(performance.now())})}:function(e){return window.setTimeout(e,1e3/60)}}()),window.cancelAnimationFrame||(window.cancelAnimationFrame=function(){return window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||function(e){clearTimeout(e)}}());var n=function(){var e=document.createEvent("Event");return e.initEvent("foo",!0,!0),e.preventDefault(),e.defaultPrevented}();if(!n){var r=Event.prototype.preventDefault;Event.prototype.preventDefault=function(){this.cancelable&&(r.call(this),Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}}))}}var o=/Trident/.test(navigator.userAgent);if((!window.CustomEvent||o&&"function"!=typeof window.CustomEvent)&&(window.CustomEvent=function(e,t){t=t||{};var n=document.createEvent("CustomEvent");return n.initCustomEvent(e,Boolean(t.bubbles),Boolean(t.cancelable),t.detail),n},window.CustomEvent.prototype=window.Event.prototype),!window.Event||o&&"function"!=typeof window.Event){var i=window.Event;window.Event=function(e,t){t=t||{};var n=document.createEvent("Event");return n.initEvent(e,Boolean(t.bubbles),Boolean(t.cancelable)),n},window.Event.prototype=i.prototype}}(window.WebComponents),window.HTMLImports=window.HTMLImports||{flags:{}},function(e){function t(e,t){t=t||p,r(function(){i(e,t)},t)}function n(e){return"complete"===e.readyState||e.readyState===_}function r(e,t){if(n(t))e&&e();else{var o=function(){("complete"===t.readyState||t.readyState===_)&&(t.removeEventListener(w,o),r(e,t))};t.addEventListener(w,o)}}function o(e){e.target.__loaded=!0}function i(e,t){function n(){c==d&&e&&e({allImports:s,loadedImports:u,errorImports:l})}function r(e){o(e),u.push(this),c++,n()}function i(e){l.push(this),c++,n()}var s=t.querySelectorAll("link[rel=import]"),c=0,d=s.length,u=[],l=[];if(d)for(var h,f=0;d>f&&(h=s[f]);f++)a(h)?(c++,n()):(h.addEventListener("load",r),h.addEventListener("error",i));else n()}function a(e){return l?e.__loaded||e["import"]&&"loading"!==e["import"].readyState:e.__importParsed}function s(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)c(t)&&d(t)}function c(e){return"link"===e.localName&&"import"===e.rel}function d(e){var t=e["import"];t?o({target:e}):(e.addEventListener("load",o),e.addEventListener("error",o))}var u="import",l=Boolean(u in document.createElement("link")),h=Boolean(window.ShadowDOMPolyfill),f=function(e){return h?window.ShadowDOMPolyfill.wrapIfNeeded(e):e},p=f(document),m={get:function(){var e=window.HTMLImports.currentScript||document.currentScript||("complete"!==document.readyState?document.scripts[document.scripts.length-1]:null);return f(e)},configurable:!0};Object.defineProperty(document,"_currentScript",m),Object.defineProperty(p,"_currentScript",m);var v=/Trident/.test(navigator.userAgent),_=v?"complete":"interactive",w="readystatechange";l&&(new MutationObserver(function(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)t.addedNodes&&s(t.addedNodes)}).observe(document.head,{childList:!0}),function(){if("loading"===document.readyState)for(var e,t=document.querySelectorAll("link[rel=import]"),n=0,r=t.length;r>n&&(e=t[n]);n++)d(e)}()),t(function(e){window.HTMLImports.ready=!0,window.HTMLImports.readyTime=(new Date).getTime();var t=p.createEvent("CustomEvent");t.initCustomEvent("HTMLImportsLoaded",!0,!0,e),p.dispatchEvent(t)}),e.IMPORT_LINK_TYPE=u,e.useNative=l,e.rootDocument=p,e.whenReady=t,e.isIE=v}(window.HTMLImports),function(e){var t=[],n=function(e){t.push(e)},r=function(){t.forEach(function(t){t(e)})};e.addModule=n,e.initializeModules=r}(window.HTMLImports),window.HTMLImports.addModule(function(e){var t=/(url\()([^)]*)(\))/g,n=/(@import[\s]+(?!url\())([^;]*)(;)/g,r={resolveUrlsInStyle:function(e,t){var n=e.ownerDocument,r=n.createElement("a");return e.textContent=this.resolveUrlsInCssText(e.textContent,t,r),e},resolveUrlsInCssText:function(e,r,o){var i=this.replaceUrls(e,o,r,t);return i=this.replaceUrls(i,o,r,n)},replaceUrls:function(e,t,n,r){return e.replace(r,function(e,r,o,i){var a=o.replace(/["']/g,"");return n&&(a=new URL(a,n).href),t.href=a,a=t.href,r+"'"+a+"'"+i})}};e.path=r}),window.HTMLImports.addModule(function(e){var t={async:!0,ok:function(e){return e.status>=200&&e.status<300||304===e.status||0===e.status},load:function(n,r,o){var i=new XMLHttpRequest;return(e.flags.debug||e.flags.bust)&&(n+="?"+Math.random()),i.open("GET",n,t.async),i.addEventListener("readystatechange",function(e){if(4===i.readyState){var n=null;try{var a=i.getResponseHeader("Location");a&&(n="/"===a.substr(0,1)?location.origin+a:a)}catch(e){console.error(e.message)}r.call(o,!t.ok(i)&&i,i.response||i.responseText,n)}}),i.send(),i},loadDocument:function(e,t,n){this.load(e,t,n).responseType="document"}};e.xhr=t}),window.HTMLImports.addModule(function(e){var t=e.xhr,n=e.flags,r=function(e,t){this.cache={},this.onload=e,this.oncomplete=t,this.inflight=0,this.pending={}};r.prototype={addNodes:function(e){this.inflight+=e.length;for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)this.require(t);this.checkDone()},addNode:function(e){this.inflight++,this.require(e),this.checkDone()},require:function(e){var t=e.src||e.href;e.__nodeUrl=t,this.dedupe(t,e)||this.fetch(t,e)},dedupe:function(e,t){if(this.pending[e])return this.pending[e].push(t),!0;return this.cache[e]?(this.onload(e,t,this.cache[e]),this.tail(),!0):(this.pending[e]=[t],!1)},fetch:function(e,r){if(n.load&&console.log("fetch",e,r),e)if(e.match(/^data:/)){var o=e.split(","),i=o[0],a=o[1];a=i.indexOf(";base64")>-1?atob(a):decodeURIComponent(a),setTimeout(function(){this.receive(e,r,null,a)}.bind(this),0)}else{var s=function(t,n,o){this.receive(e,r,t,n,o)}.bind(this);t.load(e,s)}else setTimeout(function(){this.receive(e,r,{error:"href must be specified"},null)}.bind(this),0)},receive:function(e,t,n,r,o){this.cache[e]=r;for(var i,a=this.pending[e],s=0,c=a.length;c>s&&(i=a[s]);s++)this.onload(e,i,r,n,o),this.tail();this.pending[e]=null},tail:function(){--this.inflight,this.checkDone()},checkDone:function(){this.inflight||this.oncomplete()}},e.Loader=r}),window.HTMLImports.addModule(function(e){var t=function(e){this.addCallback=e,this.mo=new MutationObserver(this.handler.bind(this))};t.prototype={handler:function(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)"childList"===t.type&&t.addedNodes.length&&this.addedNodes(t.addedNodes)},addedNodes:function(e){this.addCallback&&this.addCallback(e);for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)t.children&&t.children.length&&this.addedNodes(t.children)},observe:function(e){this.mo.observe(e,{childList:!0,subtree:!0})}},e.Observer=t}),window.HTMLImports.addModule(function(e){function t(e){return"link"===e.localName&&e.rel===u}function n(e){var t=r(e);return"data:text/javascript;charset=utf-8,"+encodeURIComponent(t)}function r(e){return e.textContent+o(e)}function o(e){var t=e.ownerDocument;t.__importedScripts=t.__importedScripts||0;var n=e.ownerDocument.baseURI,r=t.__importedScripts?"-"+t.__importedScripts:"";return t.__importedScripts++,"\n//# sourceURL="+n+r+".js\n"}function i(e){var t=e.ownerDocument.createElement("style");return t.textContent=e.textContent,a.resolveUrlsInStyle(t),t}var a=e.path,s=e.rootDocument,c=e.flags,d=e.isIE,u=e.IMPORT_LINK_TYPE,l="link[rel="+u+"]",h={documentSelectors:l,importsSelectors:[l,"link[rel=stylesheet]:not([type])","style:not([type])","script:not([type])",'script[type="application/javascript"]','script[type="text/javascript"]'].join(","),map:{link:"parseLink",script:"parseScript",style:"parseStyle"},dynamicElements:[],parseNext:function(){var e=this.nextToParse();e&&this.parse(e)},parse:function(e){if(this.isParsed(e))return void(c.parse&&console.log("[%s] is already parsed",e.localName));var t=this[this.map[e.localName]];t&&(this.markParsing(e),t.call(this,e))},parseDynamic:function(e,t){this.dynamicElements.push(e),t||this.parseNext()},markParsing:function(e){c.parse&&console.log("parsing",e),this.parsingElement=e},markParsingComplete:function(e){e.__importParsed=!0,this.markDynamicParsingComplete(e),e.__importElement&&(e.__importElement.__importParsed=!0,this.markDynamicParsingComplete(e.__importElement)),this.parsingElement=null,c.parse&&console.log("completed",e)},markDynamicParsingComplete:function(e){var t=this.dynamicElements.indexOf(e);t>=0&&this.dynamicElements.splice(t,1)},parseImport:function(e){if(e["import"]=e.__doc,window.HTMLImports.__importsParsingHook&&window.HTMLImports.__importsParsingHook(e),e["import"]&&(e["import"].__importParsed=!0),this.markParsingComplete(e),e.__resource&&!e.__error?e.dispatchEvent(new CustomEvent("load",{bubbles:!1})):e.dispatchEvent(new CustomEvent("error",{bubbles:!1})),e.__pending)for(var t;e.__pending.length;)t=e.__pending.shift(),t&&t({target:e});this.parseNext()},parseLink:function(e){t(e)?this.parseImport(e):(e.href=e.href,this.parseGeneric(e))},parseStyle:function(e){var t=e;e=i(e),t.__appliedElement=e,e.__importElement=t,this.parseGeneric(e)},parseGeneric:function(e){this.trackElement(e),this.addElementToDocument(e)},rootImportForElement:function(e){for(var t=e;t.ownerDocument.__importLink;)t=t.ownerDocument.__importLink;return t},addElementToDocument:function(e){var t=this.rootImportForElement(e.__importElement||e);t.parentNode.insertBefore(e,t)},trackElement:function(e,t){var n=this,r=function(o){e.removeEventListener("load",r),e.removeEventListener("error",r),t&&t(o),n.markParsingComplete(e),n.parseNext()};if(e.addEventListener("load",r),e.addEventListener("error",r),d&&"style"===e.localName){var o=!1;if(-1==e.textContent.indexOf("@import"))o=!0;else if(e.sheet){o=!0;for(var i,a=e.sheet.cssRules,s=a?a.length:0,c=0;s>c&&(i=a[c]);c++)i.type===CSSRule.IMPORT_RULE&&(o=o&&Boolean(i.styleSheet))}o&&setTimeout(function(){e.dispatchEvent(new CustomEvent("load",{bubbles:!1}))})}},parseScript:function(t){var r=document.createElement("script");r.__importElement=t,r.src=t.src?t.src:n(t),e.currentScript=t,this.trackElement(r,function(t){r.parentNode&&r.parentNode.removeChild(r),e.currentScript=null}),this.addElementToDocument(r)},nextToParse:function(){return this._mayParse=[],!this.parsingElement&&(this.nextToParseInDoc(s)||this.nextToParseDynamic())},nextToParseInDoc:function(e,n){if(e&&this._mayParse.indexOf(e)<0){this._mayParse.push(e);for(var r,o=e.querySelectorAll(this.parseSelectorsForNode(e)),i=0,a=o.length;a>i&&(r=o[i]);i++)if(!this.isParsed(r))return this.hasResource(r)?t(r)?this.nextToParseInDoc(r.__doc,r):r:void 0}return n},nextToParseDynamic:function(){return this.dynamicElements[0]},parseSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===s?this.documentSelectors:this.importsSelectors},isParsed:function(e){return e.__importParsed},needsDynamicParsing:function(e){return this.dynamicElements.indexOf(e)>=0},hasResource:function(e){return t(e)&&void 0===e.__doc?!1:!0}};e.parser=h,e.IMPORT_SELECTOR=l}),window.HTMLImports.addModule(function(e){function t(e){return n(e,a)}function n(e,t){return"link"===e.localName&&e.getAttribute("rel")===t}function r(e){return!!Object.getOwnPropertyDescriptor(e,"baseURI")}function o(e,t){var n=document.implementation.createHTMLDocument(a);n._URL=t;var o=n.createElement("base");o.setAttribute("href",t),n.baseURI||r(n)||Object.defineProperty(n,"baseURI",{value:t});var i=n.createElement("meta");return i.setAttribute("charset","utf-8"),n.head.appendChild(i),n.head.appendChild(o),n.body.innerHTML=e,window.HTMLTemplateElement&&HTMLTemplateElement.bootstrap&&HTMLTemplateElement.bootstrap(n),n}var i=e.flags,a=e.IMPORT_LINK_TYPE,s=e.IMPORT_SELECTOR,c=e.rootDocument,d=e.Loader,u=e.Observer,l=e.parser,h={documents:{},documentPreloadSelectors:s,importsPreloadSelectors:[s].join(","),loadNode:function(e){f.addNode(e)},loadSubtree:function(e){var t=this.marshalNodes(e);f.addNodes(t)},marshalNodes:function(e){return e.querySelectorAll(this.loadSelectorsForNode(e))},loadSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===c?this.documentPreloadSelectors:this.importsPreloadSelectors},loaded:function(e,n,r,a,s){if(i.load&&console.log("loaded",e,n),n.__resource=r,n.__error=a,t(n)){var c=this.documents[e];void 0===c&&(c=a?null:o(r,s||e),c&&(c.__importLink=n,this.bootDocument(c)),this.documents[e]=c),n.__doc=c}l.parseNext()},bootDocument:function(e){this.loadSubtree(e),this.observer.observe(e),l.parseNext()},loadedAll:function(){l.parseNext()}},f=new d(h.loaded.bind(h),h.loadedAll.bind(h));if(h.observer=new u,!document.baseURI){var p={get:function(){var e=document.querySelector("base");return e?e.href:window.location.href},configurable:!0};Object.defineProperty(document,"baseURI",p),Object.defineProperty(c,"baseURI",p)}e.importer=h,e.importLoader=f}),window.HTMLImports.addModule(function(e){var t=e.parser,n=e.importer,r={added:function(e){for(var r,o,i,a,s=0,c=e.length;c>s&&(a=e[s]);s++)r||(r=a.ownerDocument,o=t.isParsed(r)),i=this.shouldLoadNode(a),i&&n.loadNode(a),this.shouldParseNode(a)&&o&&t.parseDynamic(a,i)},shouldLoadNode:function(e){return 1===e.nodeType&&o.call(e,n.loadSelectorsForNode(e))},shouldParseNode:function(e){return 1===e.nodeType&&o.call(e,t.parseSelectorsForNode(e))}};n.observer.addCallback=r.added.bind(r);var o=HTMLElement.prototype.matches||HTMLElement.prototype.matchesSelector||HTMLElement.prototype.webkitMatchesSelector||HTMLElement.prototype.mozMatchesSelector||HTMLElement.prototype.msMatchesSelector}),function(e){function t(){window.HTMLImports.importer.bootDocument(r)}var n=e.initializeModules;e.isIE;if(!e.useNative){n();var r=e.rootDocument;"complete"===document.readyState||"interactive"===document.readyState&&!window.attachEvent?t():document.addEventListener("DOMContentLoaded",t)}}(window.HTMLImports),window.CustomElements=window.CustomElements||{flags:{}},function(e){var t=e.flags,n=[],r=function(e){n.push(e)},o=function(){n.forEach(function(t){t(e)})};e.addModule=r,e.initializeModules=o,e.hasNative=Boolean(document.registerElement),e.isIE=/Trident/.test(navigator.userAgent),e.useNative=!t.register&&e.hasNative&&!window.ShadowDOMPolyfill&&(!window.HTMLImports||window.HTMLImports.useNative)}(window.CustomElements),window.CustomElements.addModule(function(e){function t(e,t){n(e,function(e){return t(e)?!0:void r(e,t)}),r(e,t)}function n(e,t,r){var o=e.firstElementChild;if(!o)for(o=e.firstChild;o&&o.nodeType!==Node.ELEMENT_NODE;)o=o.nextSibling;for(;o;)t(o,r)!==!0&&n(o,t,r),o=o.nextElementSibling;return null}function r(e,n){for(var r=e.shadowRoot;r;)t(r,n),r=r.olderShadowRoot}function o(e,t){i(e,t,[])}function i(e,t,n){if(e=window.wrap(e),!(n.indexOf(e)>=0)){n.push(e);for(var r,o=e.querySelectorAll("link[rel="+a+"]"),s=0,c=o.length;c>s&&(r=o[s]);s++)r["import"]&&i(r["import"],t,n);t(e)}}var a=window.HTMLImports?window.HTMLImports.IMPORT_LINK_TYPE:"none";e.forDocumentTree=o,e.forSubtree=t}),window.CustomElements.addModule(function(e){function t(e,t){return n(e,t)||r(e,t)}function n(t,n){return e.upgrade(t,n)?!0:void(n&&a(t))}function r(e,t){b(e,function(e){return n(e,t)?!0:void 0})}function o(e){T.push(e),L||(L=!0,setTimeout(i))}function i(){L=!1;for(var e,t=T,n=0,r=t.length;r>n&&(e=t[n]);n++)e();T=[]}function a(e){E?o(function(){s(e)}):s(e)}function s(e){e.__upgraded__&&!e.__attached&&(e.__attached=!0,e.attachedCallback&&e.attachedCallback())}function c(e){d(e),b(e,function(e){d(e)})}function d(e){E?o(function(){u(e)}):u(e)}function u(e){e.__upgraded__&&e.__attached&&(e.__attached=!1,e.detachedCallback&&e.detachedCallback())}function l(e){for(var t=e,n=window.wrap(document);t;){if(t==n)return!0;t=t.parentNode||t.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&t.host}}function h(e){if(e.shadowRoot&&!e.shadowRoot.__watched){g.dom&&console.log("watching shadow-root for: ",e.localName);for(var t=e.shadowRoot;t;)m(t),t=t.olderShadowRoot}}function f(e,n){if(g.dom){var r=n[0];if(r&&"childList"===r.type&&r.addedNodes&&r.addedNodes){for(var o=r.addedNodes[0];o&&o!==document&&!o.host;)o=o.parentNode;var i=o&&(o.URL||o._URL||o.host&&o.host.localName)||"";i=i.split("/?").shift().split("/").pop()}console.group("mutations (%d) [%s]",n.length,i||"")}var a=l(e);n.forEach(function(e){"childList"===e.type&&(M(e.addedNodes,function(e){e.localName&&t(e,a)}),M(e.removedNodes,function(e){e.localName&&c(e)}))}),g.dom&&console.groupEnd()}function p(e){for(e=window.wrap(e),e||(e=window.wrap(document));e.parentNode;)e=e.parentNode;var t=e.__observer;t&&(f(e,t.takeRecords()),i())}function m(e){if(!e.__observer){var t=new MutationObserver(f.bind(this,e));t.observe(e,{childList:!0,subtree:!0}),e.__observer=t}}function v(e){e=window.wrap(e),g.dom&&console.group("upgradeDocument: ",e.baseURI.split("/").pop());var n=e===window.wrap(document); -t(e,n),m(e),g.dom&&console.groupEnd()}function _(e){y(e,v)}function w(e){HTMLTemplateElement&&HTMLTemplateElement.bootstrap&&HTMLTemplateElement.bootstrap(e),t(e)}var g=e.flags,b=e.forSubtree,y=e.forDocumentTree,E=window.MutationObserver._isPolyfilled&&g["throttle-attached"];e.hasPolyfillMutations=E,e.hasThrottledAttached=E;var L=!1,T=[],M=Array.prototype.forEach.call.bind(Array.prototype.forEach),N=Element.prototype.createShadowRoot;N&&(Element.prototype.createShadowRoot=function(){var e=N.call(this);return window.CustomElements.watchShadow(this),e}),e.watchShadow=h,e.upgradeDocumentTree=_,e.upgradeDocument=v,e.upgradeSubtree=r,e.upgradeAll=w,e.attached=a,e.takeRecords=p}),window.CustomElements.addModule(function(e){function t(t,r){if(!t.__upgraded__&&t.nodeType===Node.ELEMENT_NODE){var o=t.getAttribute("is"),i=e.getRegisteredDefinition(t.localName)||e.getRegisteredDefinition(o);if(i&&(o&&i.tag==t.localName||!o&&!i["extends"]))return n(t,i,r)}}function n(t,n,o){return a.upgrade&&console.group("upgrade:",t.localName),n.is&&t.setAttribute("is",n.is),r(t,n),t.__upgraded__=!0,i(t),o&&e.attached(t),e.upgradeSubtree(t,o),a.upgrade&&console.groupEnd(),t}function r(e,t){Object.__proto__?e.__proto__=t.prototype:(o(e,t.prototype,t["native"]),e.__proto__=t.prototype)}function o(e,t,n){for(var r={},o=t;o!==n&&o!==HTMLElement.prototype;){for(var i,a=Object.getOwnPropertyNames(o),s=0;i=a[s];s++)r[i]||(Object.defineProperty(e,i,Object.getOwnPropertyDescriptor(o,i)),r[i]=1);o=Object.getPrototypeOf(o)}}function i(e){e.createdCallback&&e.createdCallback()}var a=e.flags;e.upgrade=t,e.upgradeWithDefinition=n,e.implementPrototype=r}),window.CustomElements.addModule(function(e){function t(t,r){var c=r||{};if(!t)throw new Error("document.registerElement: first argument `name` must not be empty");if(t.indexOf("-")<0)throw new Error("document.registerElement: first argument ('name') must contain a dash ('-'). Argument provided was '"+String(t)+"'.");if(o(t))throw new Error("Failed to execute 'registerElement' on 'Document': Registration failed for type '"+String(t)+"'. The type name is invalid.");if(d(t))throw new Error("DuplicateDefinitionError: a type with name '"+String(t)+"' is already registered");return c.prototype||(c.prototype=Object.create(HTMLElement.prototype)),c.__name=t.toLowerCase(),c.lifecycle=c.lifecycle||{},c.ancestry=i(c["extends"]),a(c),s(c),n(c.prototype),u(c.__name,c),c.ctor=l(c),c.ctor.prototype=c.prototype,c.prototype.constructor=c.ctor,e.ready&&_(document),c.ctor}function n(e){if(!e.setAttribute._polyfilled){var t=e.setAttribute;e.setAttribute=function(e,n){r.call(this,e,n,t)};var n=e.removeAttribute;e.removeAttribute=function(e){r.call(this,e,null,n)},e.setAttribute._polyfilled=!0}}function r(e,t,n){e=e.toLowerCase();var r=this.getAttribute(e);n.apply(this,arguments);var o=this.getAttribute(e);this.attributeChangedCallback&&o!==r&&this.attributeChangedCallback(e,r,o)}function o(e){for(var t=0;t=0&&b(r,HTMLElement),r)}function p(e,t){var n=e[t];e[t]=function(){var e=n.apply(this,arguments);return w(e),e}}var m,v=e.isIE,_=e.upgradeDocumentTree,w=e.upgradeAll,g=e.upgradeWithDefinition,b=e.implementPrototype,y=e.useNative,E=["annotation-xml","color-profile","font-face","font-face-src","font-face-uri","font-face-format","font-face-name","missing-glyph"],L={},T="http://www.w3.org/1999/xhtml",M=document.createElement.bind(document),N=document.createElementNS.bind(document);m=Object.__proto__||y?function(e,t){return e instanceof t}:function(e,t){if(e instanceof t)return!0;for(var n=e;n;){if(n===t.prototype)return!0;n=n.__proto__}return!1},p(Node.prototype,"cloneNode"),p(document,"importNode"),v&&!function(){var e=document.importNode;document.importNode=function(){var t=e.apply(document,arguments);if(t.nodeType==t.DOCUMENT_FRAGMENT_NODE){var n=document.createDocumentFragment();return n.appendChild(t),n}return t}}(),document.registerElement=t,document.createElement=f,document.createElementNS=h,e.registry=L,e["instanceof"]=m,e.reservedTagList=E,e.getRegisteredDefinition=d,document.register=document.registerElement}),function(e){function t(){i(window.wrap(document)),window.CustomElements.ready=!0;var e=window.requestAnimationFrame||function(e){setTimeout(e,16)};e(function(){setTimeout(function(){window.CustomElements.readyTime=Date.now(),window.HTMLImports&&(window.CustomElements.elapsed=window.CustomElements.readyTime-window.HTMLImports.readyTime),document.dispatchEvent(new CustomEvent("WebComponentsReady",{bubbles:!0}))})})}var n=e.useNative,r=e.initializeModules;e.isIE;if(n){var o=function(){};e.watchShadow=o,e.upgrade=o,e.upgradeAll=o,e.upgradeDocumentTree=o,e.upgradeSubtree=o,e.takeRecords=o,e["instanceof"]=function(e,t){return e instanceof t}}else r();var i=e.upgradeDocumentTree,a=e.upgradeDocument;if(window.wrap||(window.ShadowDOMPolyfill?(window.wrap=window.ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=window.ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}),window.HTMLImports&&(window.HTMLImports.__importsParsingHook=function(e){e["import"]&&a(wrap(e["import"]))}),"complete"===document.readyState||e.flags.eager)t();else if("interactive"!==document.readyState||window.attachEvent||window.HTMLImports&&!window.HTMLImports.ready){var s=window.HTMLImports&&!window.HTMLImports.ready?"HTMLImportsLoaded":"DOMContentLoaded";window.addEventListener(s,t)}else t()}(window.CustomElements),function(e){var t=document.createElement("style");t.textContent="body {transition: opacity ease-in 0.2s; } \nbody[unresolved] {opacity: 0; display: block; overflow: hidden; position: relative; } \n";var n=document.querySelector("head");n.insertBefore(t,n.firstChild)}(window.WebComponents); \ No newline at end of file +// @version 0.7.18 +!function(){window.WebComponents=window.WebComponents||{flags:{}};var e="webcomponents-lite.js",t=document.querySelector('script[src*="'+e+'"]'),n={};if(!n.noOpts){if(location.search.slice(1).split("&").forEach(function(e){var t,r=e.split("=");r[0]&&(t=r[0].match(/wc-(.+)/))&&(n[t[1]]=r[1]||!0)}),t)for(var r,o=0;r=t.attributes[o];o++)"src"!==r.name&&(n[r.name]=r.value||!0);if(n.log&&n.log.split){var i=n.log.split(",");n.log={},i.forEach(function(e){n.log[e]=!0})}else n.log={}}n.register&&(window.CustomElements=window.CustomElements||{flags:{}},window.CustomElements.flags.register=n.register),WebComponents.flags=n}(),function(e){"use strict";function t(e){return void 0!==h[e]}function n(){s.call(this),this._isInvalid=!0}function r(e){return""==e&&n.call(this),e.toLowerCase()}function o(e){var t=e.charCodeAt(0);return t>32&&127>t&&-1==[34,35,60,62,63,96].indexOf(t)?e:encodeURIComponent(e)}function i(e){var t=e.charCodeAt(0);return t>32&&127>t&&-1==[34,35,60,62,96].indexOf(t)?e:encodeURIComponent(e)}function a(e,a,s){function c(e){g.push(e)}var d=a||"scheme start",u=0,l="",w=!1,_=!1,g=[];e:for(;(e[u-1]!=p||0==u)&&!this._isInvalid;){var b=e[u];switch(d){case"scheme start":if(!b||!m.test(b)){if(a){c("Invalid scheme.");break e}l="",d="no scheme";continue}l+=b.toLowerCase(),d="scheme";break;case"scheme":if(b&&v.test(b))l+=b.toLowerCase();else{if(":"!=b){if(a){if(p==b)break e;c("Code point not allowed in scheme: "+b);break e}l="",u=0,d="no scheme";continue}if(this._scheme=l,l="",a)break e;t(this._scheme)&&(this._isRelative=!0),d="file"==this._scheme?"relative":this._isRelative&&s&&s._scheme==this._scheme?"relative or authority":this._isRelative?"authority first slash":"scheme data"}break;case"scheme data":"?"==b?(this._query="?",d="query"):"#"==b?(this._fragment="#",d="fragment"):p!=b&&" "!=b&&"\n"!=b&&"\r"!=b&&(this._schemeData+=o(b));break;case"no scheme":if(s&&t(s._scheme)){d="relative";continue}c("Missing scheme."),n.call(this);break;case"relative or authority":if("/"!=b||"/"!=e[u+1]){c("Expected /, got: "+b),d="relative";continue}d="authority ignore slashes";break;case"relative":if(this._isRelative=!0,"file"!=this._scheme&&(this._scheme=s._scheme),p==b){this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._username=s._username,this._password=s._password;break e}if("/"==b||"\\"==b)"\\"==b&&c("\\ is an invalid code point."),d="relative slash";else if("?"==b)this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query="?",this._username=s._username,this._password=s._password,d="query";else{if("#"!=b){var y=e[u+1],E=e[u+2];("file"!=this._scheme||!m.test(b)||":"!=y&&"|"!=y||p!=E&&"/"!=E&&"\\"!=E&&"?"!=E&&"#"!=E)&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password,this._path=s._path.slice(),this._path.pop()),d="relative path";continue}this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._fragment="#",this._username=s._username,this._password=s._password,d="fragment"}break;case"relative slash":if("/"!=b&&"\\"!=b){"file"!=this._scheme&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password),d="relative path";continue}"\\"==b&&c("\\ is an invalid code point."),d="file"==this._scheme?"file host":"authority ignore slashes";break;case"authority first slash":if("/"!=b){c("Expected '/', got: "+b),d="authority ignore slashes";continue}d="authority second slash";break;case"authority second slash":if(d="authority ignore slashes","/"!=b){c("Expected '/', got: "+b);continue}break;case"authority ignore slashes":if("/"!=b&&"\\"!=b){d="authority";continue}c("Expected authority, got: "+b);break;case"authority":if("@"==b){w&&(c("@ already seen."),l+="%40"),w=!0;for(var L=0;L>>0)+(t++ +"__")};n.prototype={set:function(t,n){var r=t[this.name];return r&&r[0]===t?r[1]=n:e(t,this.name,{value:[t,n],writable:!0}),this},get:function(e){var t;return(t=e[this.name])&&t[0]===e?t[1]:void 0},"delete":function(e){var t=e[this.name];return t&&t[0]===e?(t[0]=t[1]=void 0,!0):!1},has:function(e){var t=e[this.name];return t?t[0]===e:!1}},window.WeakMap=n}(),function(e){function t(e){b.push(e),g||(g=!0,m(r))}function n(e){return window.ShadowDOMPolyfill&&window.ShadowDOMPolyfill.wrapIfNeeded(e)||e}function r(){g=!1;var e=b;b=[],e.sort(function(e,t){return e.uid_-t.uid_});var t=!1;e.forEach(function(e){var n=e.takeRecords();o(e),n.length&&(e.callback_(n,e),t=!0)}),t&&r()}function o(e){e.nodes_.forEach(function(t){var n=v.get(t);n&&n.forEach(function(t){t.observer===e&&t.removeTransientObservers()})})}function i(e,t){for(var n=e;n;n=n.parentNode){var r=v.get(n);if(r)for(var o=0;o0){var o=n[r-1],i=f(o,e);if(i)return void(n[r-1]=i)}else t(this.observer);n[r]=e},addListeners:function(){this.addListeners_(this.target)},addListeners_:function(e){var t=this.options;t.attributes&&e.addEventListener("DOMAttrModified",this,!0),t.characterData&&e.addEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.addEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.addEventListener("DOMNodeRemoved",this,!0)},removeListeners:function(){this.removeListeners_(this.target)},removeListeners_:function(e){var t=this.options;t.attributes&&e.removeEventListener("DOMAttrModified",this,!0),t.characterData&&e.removeEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.removeEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.removeEventListener("DOMNodeRemoved",this,!0)},addTransientObserver:function(e){if(e!==this.target){this.addListeners_(e),this.transientObservedNodes.push(e);var t=v.get(e);t||v.set(e,t=[]),t.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[],e.forEach(function(e){this.removeListeners_(e);for(var t=v.get(e),n=0;n":return">";case" ":return" "}}function t(t){return t.replace(a,e)}var n="template",r=document.implementation.createHTMLDocument("template"),o=!0;HTMLTemplateElement=function(){},HTMLTemplateElement.prototype=Object.create(HTMLElement.prototype),HTMLTemplateElement.decorate=function(e){if(!e.content){e.content=r.createDocumentFragment();for(var n;n=e.firstChild;)e.content.appendChild(n);if(o)try{Object.defineProperty(e,"innerHTML",{get:function(){for(var e="",n=this.content.firstChild;n;n=n.nextSibling)e+=n.outerHTML||t(n.data);return e},set:function(e){for(r.body.innerHTML=e,HTMLTemplateElement.bootstrap(r);this.content.firstChild;)this.content.removeChild(this.content.firstChild);for(;r.body.firstChild;)this.content.appendChild(r.body.firstChild)},configurable:!0})}catch(i){o=!1}HTMLTemplateElement.bootstrap(e.content)}},HTMLTemplateElement.bootstrap=function(e){for(var t,r=e.querySelectorAll(n),o=0,i=r.length;i>o&&(t=r[o]);o++)HTMLTemplateElement.decorate(t)},document.addEventListener("DOMContentLoaded",function(){HTMLTemplateElement.bootstrap(document)});var i=document.createElement;document.createElement=function(){"use strict";var e=i.apply(document,arguments);return"template"==e.localName&&HTMLTemplateElement.decorate(e),e};var a=/[&\u00A0<>]/g}(),function(e){"use strict";if(!window.performance){var t=Date.now();window.performance={now:function(){return Date.now()-t}}}window.requestAnimationFrame||(window.requestAnimationFrame=function(){var e=window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame;return e?function(t){return e(function(){t(performance.now())})}:function(e){return window.setTimeout(e,1e3/60)}}()),window.cancelAnimationFrame||(window.cancelAnimationFrame=function(){return window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||function(e){clearTimeout(e)}}());var n=function(){var e=document.createEvent("Event");return e.initEvent("foo",!0,!0),e.preventDefault(),e.defaultPrevented}();if(!n){var r=Event.prototype.preventDefault;Event.prototype.preventDefault=function(){this.cancelable&&(r.call(this),Object.defineProperty(this,"defaultPrevented",{get:function(){return!0},configurable:!0}))}}var o=/Trident/.test(navigator.userAgent);if((!window.CustomEvent||o&&"function"!=typeof window.CustomEvent)&&(window.CustomEvent=function(e,t){t=t||{};var n=document.createEvent("CustomEvent");return n.initCustomEvent(e,Boolean(t.bubbles),Boolean(t.cancelable),t.detail),n},window.CustomEvent.prototype=window.Event.prototype),!window.Event||o&&"function"!=typeof window.Event){var i=window.Event;window.Event=function(e,t){t=t||{};var n=document.createEvent("Event");return n.initEvent(e,Boolean(t.bubbles),Boolean(t.cancelable)),n},window.Event.prototype=i.prototype}}(window.WebComponents),window.HTMLImports=window.HTMLImports||{flags:{}},function(e){function t(e,t){t=t||p,r(function(){i(e,t)},t)}function n(e){return"complete"===e.readyState||e.readyState===w}function r(e,t){if(n(t))e&&e();else{var o=function(){("complete"===t.readyState||t.readyState===w)&&(t.removeEventListener(_,o),r(e,t))};t.addEventListener(_,o)}}function o(e){e.target.__loaded=!0}function i(e,t){function n(){c==d&&e&&e({allImports:s,loadedImports:u,errorImports:l})}function r(e){o(e),u.push(this),c++,n()}function i(e){l.push(this),c++,n()}var s=t.querySelectorAll("link[rel=import]"),c=0,d=s.length,u=[],l=[];if(d)for(var h,f=0;d>f&&(h=s[f]);f++)a(h)?(c++,n()):(h.addEventListener("load",r),h.addEventListener("error",i));else n()}function a(e){return l?e.__loaded||e["import"]&&"loading"!==e["import"].readyState:e.__importParsed}function s(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)c(t)&&d(t)}function c(e){return"link"===e.localName&&"import"===e.rel}function d(e){var t=e["import"];t?o({target:e}):(e.addEventListener("load",o),e.addEventListener("error",o))}var u="import",l=Boolean(u in document.createElement("link")),h=Boolean(window.ShadowDOMPolyfill),f=function(e){return h?window.ShadowDOMPolyfill.wrapIfNeeded(e):e},p=f(document),m={get:function(){var e=window.HTMLImports.currentScript||document.currentScript||("complete"!==document.readyState?document.scripts[document.scripts.length-1]:null);return f(e)},configurable:!0};Object.defineProperty(document,"_currentScript",m),Object.defineProperty(p,"_currentScript",m);var v=/Trident/.test(navigator.userAgent),w=v?"complete":"interactive",_="readystatechange";l&&(new MutationObserver(function(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)t.addedNodes&&s(t.addedNodes)}).observe(document.head,{childList:!0}),function(){if("loading"===document.readyState)for(var e,t=document.querySelectorAll("link[rel=import]"),n=0,r=t.length;r>n&&(e=t[n]);n++)d(e)}()),t(function(e){window.HTMLImports.ready=!0,window.HTMLImports.readyTime=(new Date).getTime();var t=p.createEvent("CustomEvent");t.initCustomEvent("HTMLImportsLoaded",!0,!0,e),p.dispatchEvent(t)}),e.IMPORT_LINK_TYPE=u,e.useNative=l,e.rootDocument=p,e.whenReady=t,e.isIE=v}(window.HTMLImports),function(e){var t=[],n=function(e){t.push(e)},r=function(){t.forEach(function(t){t(e)})};e.addModule=n,e.initializeModules=r}(window.HTMLImports),window.HTMLImports.addModule(function(e){var t=/(url\()([^)]*)(\))/g,n=/(@import[\s]+(?!url\())([^;]*)(;)/g,r={resolveUrlsInStyle:function(e,t){var n=e.ownerDocument,r=n.createElement("a");return e.textContent=this.resolveUrlsInCssText(e.textContent,t,r),e},resolveUrlsInCssText:function(e,r,o){var i=this.replaceUrls(e,o,r,t);return i=this.replaceUrls(i,o,r,n)},replaceUrls:function(e,t,n,r){return e.replace(r,function(e,r,o,i){var a=o.replace(/["']/g,"");return n&&(a=new URL(a,n).href),t.href=a,a=t.href,r+"'"+a+"'"+i})}};e.path=r}),window.HTMLImports.addModule(function(e){var t={async:!0,ok:function(e){return e.status>=200&&e.status<300||304===e.status||0===e.status},load:function(n,r,o){var i=new XMLHttpRequest;return(e.flags.debug||e.flags.bust)&&(n+="?"+Math.random()),i.open("GET",n,t.async),i.addEventListener("readystatechange",function(e){if(4===i.readyState){var n=null;try{var a=i.getResponseHeader("Location");a&&(n="/"===a.substr(0,1)?location.origin+a:a)}catch(e){console.error(e.message)}r.call(o,!t.ok(i)&&i,i.response||i.responseText,n)}}),i.send(),i},loadDocument:function(e,t,n){this.load(e,t,n).responseType="document"}};e.xhr=t}),window.HTMLImports.addModule(function(e){var t=e.xhr,n=e.flags,r=function(e,t){this.cache={},this.onload=e,this.oncomplete=t,this.inflight=0,this.pending={}};r.prototype={addNodes:function(e){this.inflight+=e.length;for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)this.require(t);this.checkDone()},addNode:function(e){this.inflight++,this.require(e),this.checkDone()},require:function(e){var t=e.src||e.href;e.__nodeUrl=t,this.dedupe(t,e)||this.fetch(t,e)},dedupe:function(e,t){if(this.pending[e])return this.pending[e].push(t),!0;return this.cache[e]?(this.onload(e,t,this.cache[e]),this.tail(),!0):(this.pending[e]=[t],!1)},fetch:function(e,r){if(n.load&&console.log("fetch",e,r),e)if(e.match(/^data:/)){var o=e.split(","),i=o[0],a=o[1];a=i.indexOf(";base64")>-1?atob(a):decodeURIComponent(a),setTimeout(function(){this.receive(e,r,null,a)}.bind(this),0)}else{var s=function(t,n,o){this.receive(e,r,t,n,o)}.bind(this);t.load(e,s)}else setTimeout(function(){this.receive(e,r,{error:"href must be specified"},null)}.bind(this),0)},receive:function(e,t,n,r,o){this.cache[e]=r;for(var i,a=this.pending[e],s=0,c=a.length;c>s&&(i=a[s]);s++)this.onload(e,i,r,n,o),this.tail();this.pending[e]=null},tail:function(){--this.inflight,this.checkDone()},checkDone:function(){this.inflight||this.oncomplete()}},e.Loader=r}),window.HTMLImports.addModule(function(e){var t=function(e){this.addCallback=e,this.mo=new MutationObserver(this.handler.bind(this))};t.prototype={handler:function(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)"childList"===t.type&&t.addedNodes.length&&this.addedNodes(t.addedNodes)},addedNodes:function(e){this.addCallback&&this.addCallback(e);for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)t.children&&t.children.length&&this.addedNodes(t.children)},observe:function(e){this.mo.observe(e,{childList:!0,subtree:!0})}},e.Observer=t}),window.HTMLImports.addModule(function(e){function t(e){return"link"===e.localName&&e.rel===u}function n(e){var t=r(e);return"data:text/javascript;charset=utf-8,"+encodeURIComponent(t)}function r(e){return e.textContent+o(e)}function o(e){var t=e.ownerDocument;t.__importedScripts=t.__importedScripts||0;var n=e.ownerDocument.baseURI,r=t.__importedScripts?"-"+t.__importedScripts:"";return t.__importedScripts++,"\n//# sourceURL="+n+r+".js\n"}function i(e){var t=e.ownerDocument.createElement("style");return t.textContent=e.textContent,a.resolveUrlsInStyle(t),t}var a=e.path,s=e.rootDocument,c=e.flags,d=e.isIE,u=e.IMPORT_LINK_TYPE,l="link[rel="+u+"]",h={documentSelectors:l,importsSelectors:[l,"link[rel=stylesheet]:not([type])","style:not([type])","script:not([type])",'script[type="application/javascript"]','script[type="text/javascript"]'].join(","),map:{link:"parseLink",script:"parseScript",style:"parseStyle"},dynamicElements:[],parseNext:function(){var e=this.nextToParse();e&&this.parse(e)},parse:function(e){if(this.isParsed(e))return void(c.parse&&console.log("[%s] is already parsed",e.localName));var t=this[this.map[e.localName]];t&&(this.markParsing(e),t.call(this,e))},parseDynamic:function(e,t){this.dynamicElements.push(e),t||this.parseNext()},markParsing:function(e){c.parse&&console.log("parsing",e),this.parsingElement=e},markParsingComplete:function(e){e.__importParsed=!0,this.markDynamicParsingComplete(e),e.__importElement&&(e.__importElement.__importParsed=!0,this.markDynamicParsingComplete(e.__importElement)),this.parsingElement=null,c.parse&&console.log("completed",e)},markDynamicParsingComplete:function(e){var t=this.dynamicElements.indexOf(e);t>=0&&this.dynamicElements.splice(t,1)},parseImport:function(e){if(e["import"]=e.__doc,window.HTMLImports.__importsParsingHook&&window.HTMLImports.__importsParsingHook(e),e["import"]&&(e["import"].__importParsed=!0),this.markParsingComplete(e),e.__resource&&!e.__error?e.dispatchEvent(new CustomEvent("load",{bubbles:!1})):e.dispatchEvent(new CustomEvent("error",{bubbles:!1})),e.__pending)for(var t;e.__pending.length;)t=e.__pending.shift(),t&&t({target:e});this.parseNext()},parseLink:function(e){t(e)?this.parseImport(e):(e.href=e.href,this.parseGeneric(e))},parseStyle:function(e){var t=e;e=i(e),t.__appliedElement=e,e.__importElement=t,this.parseGeneric(e)},parseGeneric:function(e){this.trackElement(e),this.addElementToDocument(e)},rootImportForElement:function(e){for(var t=e;t.ownerDocument.__importLink;)t=t.ownerDocument.__importLink;return t},addElementToDocument:function(e){var t=this.rootImportForElement(e.__importElement||e);t.parentNode.insertBefore(e,t)},trackElement:function(e,t){var n=this,r=function(o){e.removeEventListener("load",r),e.removeEventListener("error",r),t&&t(o),n.markParsingComplete(e),n.parseNext()};if(e.addEventListener("load",r),e.addEventListener("error",r),d&&"style"===e.localName){var o=!1;if(-1==e.textContent.indexOf("@import"))o=!0;else if(e.sheet){o=!0;for(var i,a=e.sheet.cssRules,s=a?a.length:0,c=0;s>c&&(i=a[c]);c++)i.type===CSSRule.IMPORT_RULE&&(o=o&&Boolean(i.styleSheet))}o&&setTimeout(function(){e.dispatchEvent(new CustomEvent("load",{bubbles:!1}))})}},parseScript:function(t){var r=document.createElement("script");r.__importElement=t,r.src=t.src?t.src:n(t),e.currentScript=t,this.trackElement(r,function(t){r.parentNode&&r.parentNode.removeChild(r),e.currentScript=null}),this.addElementToDocument(r)},nextToParse:function(){return this._mayParse=[],!this.parsingElement&&(this.nextToParseInDoc(s)||this.nextToParseDynamic())},nextToParseInDoc:function(e,n){if(e&&this._mayParse.indexOf(e)<0){this._mayParse.push(e);for(var r,o=e.querySelectorAll(this.parseSelectorsForNode(e)),i=0,a=o.length;a>i&&(r=o[i]);i++)if(!this.isParsed(r))return this.hasResource(r)?t(r)?this.nextToParseInDoc(r.__doc,r):r:void 0}return n},nextToParseDynamic:function(){return this.dynamicElements[0]},parseSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===s?this.documentSelectors:this.importsSelectors},isParsed:function(e){return e.__importParsed},needsDynamicParsing:function(e){return this.dynamicElements.indexOf(e)>=0},hasResource:function(e){return t(e)&&void 0===e.__doc?!1:!0}};e.parser=h,e.IMPORT_SELECTOR=l}),window.HTMLImports.addModule(function(e){function t(e){return n(e,a)}function n(e,t){return"link"===e.localName&&e.getAttribute("rel")===t}function r(e){return!!Object.getOwnPropertyDescriptor(e,"baseURI")}function o(e,t){var n=document.implementation.createHTMLDocument(a);n._URL=t;var o=n.createElement("base");o.setAttribute("href",t),n.baseURI||r(n)||Object.defineProperty(n,"baseURI",{value:t});var i=n.createElement("meta");return i.setAttribute("charset","utf-8"),n.head.appendChild(i),n.head.appendChild(o),n.body.innerHTML=e,window.HTMLTemplateElement&&HTMLTemplateElement.bootstrap&&HTMLTemplateElement.bootstrap(n),n}var i=e.flags,a=e.IMPORT_LINK_TYPE,s=e.IMPORT_SELECTOR,c=e.rootDocument,d=e.Loader,u=e.Observer,l=e.parser,h={documents:{},documentPreloadSelectors:s,importsPreloadSelectors:[s].join(","),loadNode:function(e){f.addNode(e)},loadSubtree:function(e){var t=this.marshalNodes(e);f.addNodes(t)},marshalNodes:function(e){return e.querySelectorAll(this.loadSelectorsForNode(e))},loadSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===c?this.documentPreloadSelectors:this.importsPreloadSelectors},loaded:function(e,n,r,a,s){if(i.load&&console.log("loaded",e,n),n.__resource=r,n.__error=a,t(n)){var c=this.documents[e];void 0===c&&(c=a?null:o(r,s||e),c&&(c.__importLink=n,this.bootDocument(c)),this.documents[e]=c),n.__doc=c}l.parseNext()},bootDocument:function(e){this.loadSubtree(e),this.observer.observe(e),l.parseNext()},loadedAll:function(){l.parseNext()}},f=new d(h.loaded.bind(h),h.loadedAll.bind(h));if(h.observer=new u,!document.baseURI){var p={get:function(){var e=document.querySelector("base");return e?e.href:window.location.href},configurable:!0};Object.defineProperty(document,"baseURI",p),Object.defineProperty(c,"baseURI",p)}e.importer=h,e.importLoader=f}),window.HTMLImports.addModule(function(e){var t=e.parser,n=e.importer,r={added:function(e){for(var r,o,i,a,s=0,c=e.length;c>s&&(a=e[s]);s++)r||(r=a.ownerDocument,o=t.isParsed(r)),i=this.shouldLoadNode(a),i&&n.loadNode(a),this.shouldParseNode(a)&&o&&t.parseDynamic(a,i)},shouldLoadNode:function(e){return 1===e.nodeType&&o.call(e,n.loadSelectorsForNode(e))},shouldParseNode:function(e){return 1===e.nodeType&&o.call(e,t.parseSelectorsForNode(e))}};n.observer.addCallback=r.added.bind(r);var o=HTMLElement.prototype.matches||HTMLElement.prototype.matchesSelector||HTMLElement.prototype.webkitMatchesSelector||HTMLElement.prototype.mozMatchesSelector||HTMLElement.prototype.msMatchesSelector}),function(e){function t(){window.HTMLImports.importer.bootDocument(r)}var n=e.initializeModules;e.isIE;if(!e.useNative){n();var r=e.rootDocument;"complete"===document.readyState||"interactive"===document.readyState&&!window.attachEvent?t():document.addEventListener("DOMContentLoaded",t)}}(window.HTMLImports),window.CustomElements=window.CustomElements||{flags:{}},function(e){var t=e.flags,n=[],r=function(e){n.push(e)},o=function(){n.forEach(function(t){t(e)})};e.addModule=r,e.initializeModules=o,e.hasNative=Boolean(document.registerElement),e.isIE=/Trident/.test(navigator.userAgent),e.useNative=!t.register&&e.hasNative&&!window.ShadowDOMPolyfill&&(!window.HTMLImports||window.HTMLImports.useNative)}(window.CustomElements),window.CustomElements.addModule(function(e){function t(e,t){n(e,function(e){return t(e)?!0:void r(e,t)}),r(e,t)}function n(e,t,r){var o=e.firstElementChild;if(!o)for(o=e.firstChild;o&&o.nodeType!==Node.ELEMENT_NODE;)o=o.nextSibling;for(;o;)t(o,r)!==!0&&n(o,t,r),o=o.nextElementSibling;return null}function r(e,n){for(var r=e.shadowRoot;r;)t(r,n),r=r.olderShadowRoot}function o(e,t){i(e,t,[])}function i(e,t,n){if(e=window.wrap(e),!(n.indexOf(e)>=0)){n.push(e);for(var r,o=e.querySelectorAll("link[rel="+a+"]"),s=0,c=o.length;c>s&&(r=o[s]);s++)r["import"]&&i(r["import"],t,n);t(e)}}var a=window.HTMLImports?window.HTMLImports.IMPORT_LINK_TYPE:"none";e.forDocumentTree=o,e.forSubtree=t}),window.CustomElements.addModule(function(e){function t(e,t){return n(e,t)||r(e,t)}function n(t,n){return e.upgrade(t,n)?!0:void(n&&a(t))}function r(e,t){g(e,function(e){return n(e,t)?!0:void 0})}function o(e){L.push(e),E||(E=!0,setTimeout(i))}function i(){E=!1;for(var e,t=L,n=0,r=t.length;r>n&&(e=t[n]);n++)e();L=[]}function a(e){y?o(function(){s(e)}):s(e)}function s(e){e.__upgraded__&&!e.__attached&&(e.__attached=!0,e.attachedCallback&&e.attachedCallback())}function c(e){d(e),g(e,function(e){d(e)})}function d(e){y?o(function(){u(e)}):u(e)}function u(e){e.__upgraded__&&e.__attached&&(e.__attached=!1,e.detachedCallback&&e.detachedCallback())}function l(e){for(var t=e,n=window.wrap(document);t;){if(t==n)return!0;t=t.parentNode||t.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&t.host}}function h(e){if(e.shadowRoot&&!e.shadowRoot.__watched){_.dom&&console.log("watching shadow-root for: ",e.localName);for(var t=e.shadowRoot;t;)m(t),t=t.olderShadowRoot}}function f(e,n){if(_.dom){var r=n[0];if(r&&"childList"===r.type&&r.addedNodes&&r.addedNodes){for(var o=r.addedNodes[0];o&&o!==document&&!o.host;)o=o.parentNode;var i=o&&(o.URL||o._URL||o.host&&o.host.localName)||"";i=i.split("/?").shift().split("/").pop()}console.group("mutations (%d) [%s]",n.length,i||"")}var a=l(e);n.forEach(function(e){"childList"===e.type&&(T(e.addedNodes,function(e){e.localName&&t(e,a)}),T(e.removedNodes,function(e){e.localName&&c(e)}))}),_.dom&&console.groupEnd()}function p(e){for(e=window.wrap(e),e||(e=window.wrap(document));e.parentNode;)e=e.parentNode;var t=e.__observer;t&&(f(e,t.takeRecords()),i())}function m(e){if(!e.__observer){var t=new MutationObserver(f.bind(this,e));t.observe(e,{childList:!0,subtree:!0}),e.__observer=t}}function v(e){e=window.wrap(e),_.dom&&console.group("upgradeDocument: ",e.baseURI.split("/").pop()); +var n=e===window.wrap(document);t(e,n),m(e),_.dom&&console.groupEnd()}function w(e){b(e,v)}var _=e.flags,g=e.forSubtree,b=e.forDocumentTree,y=window.MutationObserver._isPolyfilled&&_["throttle-attached"];e.hasPolyfillMutations=y,e.hasThrottledAttached=y;var E=!1,L=[],T=Array.prototype.forEach.call.bind(Array.prototype.forEach),M=Element.prototype.createShadowRoot;M&&(Element.prototype.createShadowRoot=function(){var e=M.call(this);return window.CustomElements.watchShadow(this),e}),e.watchShadow=h,e.upgradeDocumentTree=w,e.upgradeDocument=v,e.upgradeSubtree=r,e.upgradeAll=t,e.attached=a,e.takeRecords=p}),window.CustomElements.addModule(function(e){function t(t,r){if("template"===t.localName&&window.HTMLTemplateElement&&HTMLTemplateElement.decorate&&HTMLTemplateElement.decorate(t),!t.__upgraded__&&t.nodeType===Node.ELEMENT_NODE){var o=t.getAttribute("is"),i=e.getRegisteredDefinition(t.localName)||e.getRegisteredDefinition(o);if(i&&(o&&i.tag==t.localName||!o&&!i["extends"]))return n(t,i,r)}}function n(t,n,o){return a.upgrade&&console.group("upgrade:",t.localName),n.is&&t.setAttribute("is",n.is),r(t,n),t.__upgraded__=!0,i(t),o&&e.attached(t),e.upgradeSubtree(t,o),a.upgrade&&console.groupEnd(),t}function r(e,t){Object.__proto__?e.__proto__=t.prototype:(o(e,t.prototype,t["native"]),e.__proto__=t.prototype)}function o(e,t,n){for(var r={},o=t;o!==n&&o!==HTMLElement.prototype;){for(var i,a=Object.getOwnPropertyNames(o),s=0;i=a[s];s++)r[i]||(Object.defineProperty(e,i,Object.getOwnPropertyDescriptor(o,i)),r[i]=1);o=Object.getPrototypeOf(o)}}function i(e){e.createdCallback&&e.createdCallback()}var a=e.flags;e.upgrade=t,e.upgradeWithDefinition=n,e.implementPrototype=r}),window.CustomElements.addModule(function(e){function t(t,r){var c=r||{};if(!t)throw new Error("document.registerElement: first argument `name` must not be empty");if(t.indexOf("-")<0)throw new Error("document.registerElement: first argument ('name') must contain a dash ('-'). Argument provided was '"+String(t)+"'.");if(o(t))throw new Error("Failed to execute 'registerElement' on 'Document': Registration failed for type '"+String(t)+"'. The type name is invalid.");if(d(t))throw new Error("DuplicateDefinitionError: a type with name '"+String(t)+"' is already registered");return c.prototype||(c.prototype=Object.create(HTMLElement.prototype)),c.__name=t.toLowerCase(),c.lifecycle=c.lifecycle||{},c.ancestry=i(c["extends"]),a(c),s(c),n(c.prototype),u(c.__name,c),c.ctor=l(c),c.ctor.prototype=c.prototype,c.prototype.constructor=c.ctor,e.ready&&w(document),c.ctor}function n(e){if(!e.setAttribute._polyfilled){var t=e.setAttribute;e.setAttribute=function(e,n){r.call(this,e,n,t)};var n=e.removeAttribute;e.removeAttribute=function(e){r.call(this,e,null,n)},e.setAttribute._polyfilled=!0}}function r(e,t,n){e=e.toLowerCase();var r=this.getAttribute(e);n.apply(this,arguments);var o=this.getAttribute(e);this.attributeChangedCallback&&o!==r&&this.attributeChangedCallback(e,r,o)}function o(e){for(var t=0;t=0&&b(r,HTMLElement),r)}function p(e,t){var n=e[t];e[t]=function(){var e=n.apply(this,arguments);return _(e),e}}var m,v=e.isIE,w=e.upgradeDocumentTree,_=e.upgradeAll,g=e.upgradeWithDefinition,b=e.implementPrototype,y=e.useNative,E=["annotation-xml","color-profile","font-face","font-face-src","font-face-uri","font-face-format","font-face-name","missing-glyph"],L={},T="http://www.w3.org/1999/xhtml",M=document.createElement.bind(document),N=document.createElementNS.bind(document);m=Object.__proto__||y?function(e,t){return e instanceof t}:function(e,t){if(e instanceof t)return!0;for(var n=e;n;){if(n===t.prototype)return!0;n=n.__proto__}return!1},p(Node.prototype,"cloneNode"),p(document,"importNode"),v&&!function(){var e=document.importNode;document.importNode=function(){var t=e.apply(document,arguments);if(t.nodeType==t.DOCUMENT_FRAGMENT_NODE){var n=document.createDocumentFragment();return n.appendChild(t),n}return t}}(),document.registerElement=t,document.createElement=f,document.createElementNS=h,e.registry=L,e["instanceof"]=m,e.reservedTagList=E,e.getRegisteredDefinition=d,document.register=document.registerElement}),function(e){function t(){i(window.wrap(document)),window.CustomElements.ready=!0;var e=window.requestAnimationFrame||function(e){setTimeout(e,16)};e(function(){setTimeout(function(){window.CustomElements.readyTime=Date.now(),window.HTMLImports&&(window.CustomElements.elapsed=window.CustomElements.readyTime-window.HTMLImports.readyTime),document.dispatchEvent(new CustomEvent("WebComponentsReady",{bubbles:!0}))})})}var n=e.useNative,r=e.initializeModules;e.isIE;if(n){var o=function(){};e.watchShadow=o,e.upgrade=o,e.upgradeAll=o,e.upgradeDocumentTree=o,e.upgradeSubtree=o,e.takeRecords=o,e["instanceof"]=function(e,t){return e instanceof t}}else r();var i=e.upgradeDocumentTree,a=e.upgradeDocument;if(window.wrap||(window.ShadowDOMPolyfill?(window.wrap=window.ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=window.ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}),window.HTMLImports&&(window.HTMLImports.__importsParsingHook=function(e){e["import"]&&a(wrap(e["import"]))}),"complete"===document.readyState||e.flags.eager)t();else if("interactive"!==document.readyState||window.attachEvent||window.HTMLImports&&!window.HTMLImports.ready){var s=window.HTMLImports&&!window.HTMLImports.ready?"HTMLImportsLoaded":"DOMContentLoaded";window.addEventListener(s,t)}else t()}(window.CustomElements),function(e){var t=document.createElement("style");t.textContent="body {transition: opacity ease-in 0.2s; } \nbody[unresolved] {opacity: 0; display: block; overflow: hidden; position: relative; } \n";var n=document.querySelector("head");n.insertBefore(t,n.firstChild)}(window.WebComponents); \ No newline at end of file diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 9ae83cb734a..52ffe824e42 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -17,7 +17,6 @@ from homeassistant.const import ( STATE_UNKNOWN) DOMAIN = "group" -DEPENDENCIES = [] ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index a7ae0c5af6e..81965179afe 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -12,10 +12,7 @@ import logging import time import gzip import os -import random -import string from datetime import timedelta -from homeassistant.util import Throttle from http.server import SimpleHTTPRequestHandler, HTTPServer from http import cookies from socketserver import ThreadingMixIn @@ -34,7 +31,6 @@ import homeassistant.util.dt as date_util import homeassistant.bootstrap as bootstrap DOMAIN = "http" -DEPENDENCIES = [] CONF_API_PASSWORD = "api_password" CONF_SERVER_HOST = "server_host" @@ -45,40 +41,30 @@ CONF_SESSIONS_ENABLED = "sessions_enabled" DATA_API_PASSWORD = 'api_password' # Throttling time in seconds for expired sessions check -MIN_SEC_SESSION_CLEARING = timedelta(seconds=20) +SESSION_CLEAR_INTERVAL = timedelta(seconds=20) SESSION_TIMEOUT_SECONDS = 1800 SESSION_KEY = 'sessionId' _LOGGER = logging.getLogger(__name__) -def setup(hass, config=None): +def setup(hass, config): """ Sets up the HTTP API and debug interface. """ - if config is None or DOMAIN not in config: - config = {DOMAIN: {}} + conf = config[DOMAIN] - api_password = util.convert(config[DOMAIN].get(CONF_API_PASSWORD), str) - - no_password_set = api_password is None - - if no_password_set: - api_password = util.get_random_string() + api_password = util.convert(conf.get(CONF_API_PASSWORD), str) # If no server host is given, accept all incoming requests - server_host = config[DOMAIN].get(CONF_SERVER_HOST, '0.0.0.0') - - server_port = config[DOMAIN].get(CONF_SERVER_PORT, SERVER_PORT) - - development = str(config[DOMAIN].get(CONF_DEVELOPMENT, "")) == "1" - - sessions_enabled = config[DOMAIN].get(CONF_SESSIONS_ENABLED, True) + server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0') + server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT) + development = str(conf.get(CONF_DEVELOPMENT, "")) == "1" try: server = HomeAssistantHTTPServer( (server_host, server_port), RequestHandler, hass, api_password, - development, no_password_set, sessions_enabled) + development) except OSError: - # Happens if address already in use + # If address already in use _LOGGER.exception("Error setting up HTTP server") return False @@ -103,17 +89,15 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): # pylint: disable=too-many-arguments def __init__(self, server_address, request_handler_class, - hass, api_password, development, no_password_set, - sessions_enabled): + hass, api_password, development): super().__init__(server_address, request_handler_class) self.server_address = server_address self.hass = hass self.api_password = api_password self.development = development - self.no_password_set = no_password_set self.paths = [] - self.sessions = SessionStore(sessions_enabled) + self.sessions = SessionStore() # We will lazy init this one if needed self.event_forwarder = None @@ -162,12 +146,13 @@ class RequestHandler(SimpleHTTPRequestHandler): def __init__(self, req, client_addr, server): """ Contructor, call the base constructor and set up session """ - self._session = None + # Track if this was an authenticated request + self.authenticated = False SimpleHTTPRequestHandler.__init__(self, req, client_addr, server) def log_message(self, fmt, *arguments): """ Redirect built-in log to HA logging """ - if self.server.no_password_set: + if self.server.api_password is None: _LOGGER.info(fmt, *arguments) else: _LOGGER.info( @@ -202,18 +187,17 @@ class RequestHandler(SimpleHTTPRequestHandler): "Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY) return - self._session = self.get_session() - if self.server.no_password_set: - api_password = self.server.api_password - else: + if self.server.api_password is None: + self.authenticated = True + elif HTTP_HEADER_HA_AUTH in self.headers: api_password = self.headers.get(HTTP_HEADER_HA_AUTH) if not api_password and DATA_API_PASSWORD in data: api_password = data[DATA_API_PASSWORD] - if not api_password and self._session is not None: - api_password = self._session.cookie_values.get( - CONF_API_PASSWORD) + self.authenticated = api_password == self.server.api_password + else: + self.authenticated = self.verify_session() if '_METHOD' in data: method = data.pop('_METHOD') @@ -246,18 +230,13 @@ class RequestHandler(SimpleHTTPRequestHandler): # Did we find a handler for the incoming request? if handle_request_method: - # For some calls we need a valid password - if require_auth and api_password != self.server.api_password: + if require_auth and not self.authenticated: self.write_json_message( "API password missing or incorrect.", HTTP_UNAUTHORIZED) + return - else: - if self._session is None and require_auth: - self._session = self.server.sessions.create( - api_password) - - handle_request_method(self, path_match, data) + handle_request_method(self, path_match, data) elif path_matched_but_not_method: self.send_response(HTTP_METHOD_NOT_ALLOWED) @@ -308,18 +287,19 @@ class RequestHandler(SimpleHTTPRequestHandler): json.dumps(data, indent=4, sort_keys=True, cls=rem.JSONEncoder).encode("UTF-8")) - def write_file(self, path): + def write_file(self, path, cache_headers=True): """ Returns a file to the user. """ try: with open(path, 'rb') as inp: - self.write_file_pointer(self.guess_type(path), inp) + self.write_file_pointer(self.guess_type(path), inp, + cache_headers) except IOError: self.send_response(HTTP_NOT_FOUND) self.end_headers() _LOGGER.exception("Unable to serve %s", path) - def write_file_pointer(self, content_type, inp): + def write_file_pointer(self, content_type, inp, cache_headers=True): """ Helper function to write a file pointer to the user. Does not do error handling. @@ -329,7 +309,8 @@ class RequestHandler(SimpleHTTPRequestHandler): self.send_response(HTTP_OK) self.send_header(HTTP_HEADER_CONTENT_TYPE, content_type) - self.set_cache_header() + if cache_headers: + self.set_cache_header() self.set_session_cookie_header() if do_gzip: @@ -356,75 +337,81 @@ class RequestHandler(SimpleHTTPRequestHandler): def set_cache_header(self): """ Add cache headers if not in development """ - if not self.server.development: - # 1 year in seconds - cache_time = 365 * 86400 + if self.server.development: + return - self.send_header( - HTTP_HEADER_CACHE_CONTROL, - "public, max-age={}".format(cache_time)) - self.send_header( - HTTP_HEADER_EXPIRES, - self.date_time_string(time.time()+cache_time)) + # 1 year in seconds + cache_time = 365 * 86400 + + self.send_header( + HTTP_HEADER_CACHE_CONTROL, + "public, max-age={}".format(cache_time)) + self.send_header( + HTTP_HEADER_EXPIRES, + self.date_time_string(time.time()+cache_time)) def set_session_cookie_header(self): - """ Add the header for the session cookie """ - if self.server.sessions.enabled and self._session is not None: - existing_sess_id = self.get_current_session_id() + """ Add the header for the session cookie and return session id. """ + if not self.authenticated: + return - if existing_sess_id != self._session.session_id: - self.send_header( - 'Set-Cookie', - SESSION_KEY+'='+self._session.session_id) + session_id = self.get_cookie_session_id() - def get_session(self): - """ Get the requested session object from cookie value """ - if self.server.sessions.enabled is not True: - return None - - session_id = self.get_current_session_id() if session_id is not None: - session = self.server.sessions.get(session_id) - if session is not None: - session.reset_expiry() - return session + self.server.sessions.extend_validation(session_id) + return - return None + self.send_header( + 'Set-Cookie', + '{}={}'.format(SESSION_KEY, self.server.sessions.create()) + ) - def get_current_session_id(self): + return session_id + + def verify_session(self): + """ Verify that we are in a valid session. """ + return self.get_cookie_session_id() is not None + + def get_cookie_session_id(self): """ Extracts the current session id from the - cookie or returns None if not set + cookie or returns None if not set or invalid """ + if 'Cookie' not in self.headers: + return None + cookie = cookies.SimpleCookie() + try: + cookie.load(self.headers["Cookie"]) + except cookies.CookieError: + return None - if self.headers.get('Cookie', None) is not None: - cookie.load(self.headers.get("Cookie")) + morsel = cookie.get(SESSION_KEY) - if cookie.get(SESSION_KEY, False): - return cookie[SESSION_KEY].value + if morsel is None: + return None + + session_id = cookie[SESSION_KEY].value + + if self.server.sessions.is_valid(session_id): + return session_id return None + def destroy_session(self): + """ Destroys session. """ + session_id = self.get_cookie_session_id() -class ServerSession: - """ A very simple session class """ - def __init__(self, session_id): - """ Set up the expiry time on creation """ - self._expiry = 0 - self.reset_expiry() - self.cookie_values = {} - self.session_id = session_id + if session_id is None: + return - def reset_expiry(self): - """ Resets the expiry based on current time """ - self._expiry = date_util.utcnow() + timedelta( - seconds=SESSION_TIMEOUT_SECONDS) + self.send_header('Set-Cookie', '') + self.server.sessions.destroy(session_id) - @property - def is_expired(self): - """ Return true if the session is expired based on the expiry time """ - return self._expiry < date_util.utcnow() + +def session_valid_time(): + """ Time till when a session will be valid. """ + return date_util.utcnow() + timedelta(seconds=SESSION_TIMEOUT_SECONDS) class SessionStore(object): @@ -432,47 +419,42 @@ class SessionStore(object): def __init__(self, enabled=True): """ Set up the session store """ self._sessions = {} - self.enabled = enabled - self.session_lock = threading.RLock() + self.lock = threading.RLock() - @Throttle(MIN_SEC_SESSION_CLEARING) - def remove_expired(self): + @util.Throttle(SESSION_CLEAR_INTERVAL) + def _remove_expired(self): """ Remove any expired sessions. """ - if self.session_lock.acquire(False): - try: - keys = [] - for key in self._sessions.keys(): - keys.append(key) + now = date_util.utcnow() + for key in [key for key, valid_time in self._sessions.items() + if valid_time < now]: + self._sessions.pop(key) - for key in keys: - if self._sessions[key].is_expired: - del self._sessions[key] - _LOGGER.info("Cleared expired session %s", key) - finally: - self.session_lock.release() + def is_valid(self, key): + """ Return True if a valid session is given. """ + with self.lock: + self._remove_expired() - def add(self, key, session): - """ Add a new session to the list of tracked sessions """ - self.remove_expired() - with self.session_lock: - self._sessions[key] = session + return (key in self._sessions and + self._sessions[key] > date_util.utcnow()) - def get(self, key): - """ get a session by key """ - self.remove_expired() - session = self._sessions.get(key, None) - if session is not None and session.is_expired: - return None - return session + def extend_validation(self, key): + """ Extend a session validation time. """ + with self.lock: + self._sessions[key] = session_valid_time() - def create(self, api_password): - """ Creates a new session and adds it to the sessions """ - if self.enabled is not True: - return None + def destroy(self, key): + """ Destroy a session by key. """ + with self.lock: + self._sessions.pop(key, None) - chars = string.ascii_letters + string.digits - session_id = ''.join([random.choice(chars) for i in range(20)]) - session = ServerSession(session_id) - session.cookie_values[CONF_API_PASSWORD] = api_password - self.add(session_id, session) - return session + def create(self): + """ Creates a new session. """ + with self.lock: + session_id = util.get_random_string(20) + + while session_id in self._sessions: + session_id = util.get_random_string(20) + + self._sessions[session_id] = session_valid_time() + + return session_id diff --git a/homeassistant/components/ifttt.py b/homeassistant/components/ifttt.py index 246265a5268..6f406b24311 100644 --- a/homeassistant/components/ifttt.py +++ b/homeassistant/components/ifttt.py @@ -22,13 +22,11 @@ ATTR_VALUE1 = 'value1' ATTR_VALUE2 = 'value2' ATTR_VALUE3 = 'value3' -DEPENDENCIES = [] - REQUIREMENTS = ['pyfttt==0.3'] def trigger(hass, event, value1=None, value2=None, value3=None): - """ Trigger a Maker IFTTT recipe """ + """ Trigger a Maker IFTTT recipe. """ data = { ATTR_EVENT: event, ATTR_VALUE1: value1, @@ -39,7 +37,7 @@ def trigger(hass, event, value1=None, value2=None, value3=None): def setup(hass, config): - """ Setup the ifttt service component """ + """ Setup the ifttt service component. """ if not validate_config(config, {DOMAIN: ['key']}, _LOGGER): return False diff --git a/homeassistant/components/introduction.py b/homeassistant/components/introduction.py index 08a71b27292..540d928f7f5 100644 --- a/homeassistant/components/introduction.py +++ b/homeassistant/components/introduction.py @@ -9,7 +9,6 @@ https://home-assistant.io/components/introduction/ import logging DOMAIN = 'introduction' -DEPENDENCIES = [] def setup(hass, config=None): diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 427ef4f048e..0f8d24520aa 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -20,7 +20,6 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME) DOMAIN = "isy994" -DEPENDENCIES = [] REQUIREMENTS = ['PyISY==1.0.5'] DISCOVER_LIGHTS = "isy994.lights" DISCOVER_SWITCHES = "isy994.switches" @@ -117,7 +116,6 @@ class ISYDeviceABC(ToggleEntity): def __init__(self, node): # setup properties self.node = node - self.hidden = HIDDEN_STRING in self.raw_name # track changes self._change_handler = self.node.status. \ @@ -182,6 +180,11 @@ class ISYDeviceABC(ToggleEntity): return self.raw_name.replace(HIDDEN_STRING, '').strip() \ .replace('_', ' ') + @property + def hidden(self): + """ Suggestion if the entity should be hidden from UIs. """ + return HIDDEN_STRING in self.raw_name + def update(self): """ Update state of the sensor. """ # ISY objects are automatically updated by the ISY's event stream diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py index ea650d8b421..c772d1c6e74 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard.py @@ -15,7 +15,6 @@ from homeassistant.const import ( DOMAIN = "keyboard" -DEPENDENCIES = [] REQUIREMENTS = ['pyuserinput==0.1.9'] diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index d7f8746de5a..1b80035fb0d 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -21,7 +21,6 @@ import homeassistant.util.color as color_util DOMAIN = "light" -DEPENDENCIES = [] SCAN_INTERVAL = 30 GROUP_NAME_ALL_LIGHTS = 'all lights' diff --git a/homeassistant/components/light/blinksticklight.py b/homeassistant/components/light/blinksticklight.py index 5cc14a9034b..fae9890c93d 100644 --- a/homeassistant/components/light/blinksticklight.py +++ b/homeassistant/components/light/blinksticklight.py @@ -14,7 +14,6 @@ _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ["blinkstick==1.1.7"] -DEPENDENCIES = [] # pylint: disable=unused-argument diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index a9c64a36a3f..7c3af9f968d 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -206,9 +206,7 @@ class HueLight(Light): command = {'on': True} if ATTR_TRANSITION in kwargs: - # Transition time is in 1/10th seconds and cannot exceed - # 900 seconds. - command['transitiontime'] = min(9000, kwargs[ATTR_TRANSITION] * 10) + command['transitiontime'] = kwargs[ATTR_TRANSITION] * 10 if ATTR_BRIGHTNESS in kwargs: command['bri'] = kwargs[ATTR_BRIGHTNESS] diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index ad5f8487a2a..e072c6ce962 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -8,171 +8,277 @@ https://home-assistant.io/components/light.limitlessled/ """ import logging -from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.components.light import (Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, + ATTR_COLOR_TEMP, ATTR_TRANSITION, + ATTR_FLASH, FLASH_LONG, EFFECT_COLORLOOP, EFFECT_WHITE) + _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['ledcontroller==1.1.0'] - -COLOR_TABLE = { - 'white': [0xFF, 0xFF, 0xFF], - 'violet': [0xEE, 0x82, 0xEE], - 'royal_blue': [0x41, 0x69, 0xE1], - 'baby_blue': [0x87, 0xCE, 0xFA], - 'aqua': [0x00, 0xFF, 0xFF], - 'royal_mint': [0x7F, 0xFF, 0xD4], - 'seafoam_green': [0x2E, 0x8B, 0x57], - 'green': [0x00, 0x80, 0x00], - 'lime_green': [0x32, 0xCD, 0x32], - 'yellow': [0xFF, 0xFF, 0x00], - 'yellow_orange': [0xDA, 0xA5, 0x20], - 'orange': [0xFF, 0xA5, 0x00], - 'red': [0xFF, 0x00, 0x00], - 'pink': [0xFF, 0xC0, 0xCB], - 'fusia': [0xFF, 0x00, 0xFF], - 'lilac': [0xDA, 0x70, 0xD6], - 'lavendar': [0xE6, 0xE6, 0xFA], -} +REQUIREMENTS = ['limitlessled==1.0.0'] +RGB_BOUNDARY = 40 +DEFAULT_TRANSITION = 0 +DEFAULT_PORT = 8899 +DEFAULT_VERSION = 5 +DEFAULT_LED_TYPE = 'rgbw' +WHITE = [255, 255, 255] -def _distance_squared(rgb1, rgb2): - """ Return sum of squared distances of each color part. """ - return sum((val1-val2)**2 for val1, val2 in zip(rgb1, rgb2)) - - -def _rgb_to_led_color(rgb_color): - """ Convert an RGB color to the closest color string and color. """ - return sorted((_distance_squared(rgb_color, color), name) - for name, color in COLOR_TABLE.items())[0][1] +def rewrite_legacy(config): + """ Rewrite legacy configuration to new format. """ + bridges = config.get('bridges', [config]) + new_bridges = [] + for bridge_conf in bridges: + groups = [] + if 'groups' in bridge_conf: + groups = bridge_conf['groups'] + else: + _LOGGER.warning("Legacy configuration format detected") + for i in range(1, 5): + name_key = 'group_%d_name' % i + if name_key in bridge_conf: + groups.append({ + 'number': i, + 'type': bridge_conf.get('group_%d_type' % i, + DEFAULT_LED_TYPE), + 'name': bridge_conf.get(name_key) + }) + new_bridges.append({ + 'host': bridge_conf.get('host'), + 'groups': groups + }) + return {'bridges': new_bridges} def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Gets the LimitlessLED lights. """ - import ledcontroller + from limitlessled.bridge import Bridge - # Handle old configuration format: - bridges = config.get('bridges', [config]) - - for bridge_id, bridge in enumerate(bridges): - bridge['id'] = bridge_id - - pool = ledcontroller.LedControllerPool([x['host'] for x in bridges]) + # Two legacy configuration formats are supported to + # maintain backwards compatibility. + config = rewrite_legacy(config) + # Use the expanded configuration format. lights = [] - for bridge in bridges: - for i in range(1, 5): - name_key = 'group_%d_name' % i - if name_key in bridge: - group_type = bridge.get('group_%d_type' % i, 'rgbw') - lights.append(LimitlessLED.factory(pool, bridge['id'], i, - bridge[name_key], - group_type)) - + for bridge_conf in config.get('bridges'): + bridge = Bridge(bridge_conf.get('host'), + port=bridge_conf.get('port', DEFAULT_PORT), + version=bridge_conf.get('version', DEFAULT_VERSION)) + for group_conf in bridge_conf.get('groups'): + group = bridge.add_group(group_conf.get('number'), + group_conf.get('name'), + group_conf.get('type', DEFAULT_LED_TYPE)) + lights.append(LimitlessLEDGroup.factory(group)) add_devices_callback(lights) -class LimitlessLED(Light): - """ Represents a LimitlessLED light """ +def state(new_state): + """ State decorator. + + Specify True (turn on) or False (turn off). + """ + def decorator(function): + """ Decorator function. """ + # pylint: disable=no-member,protected-access + def wrapper(self, **kwargs): + """ Wrap a group state change. """ + from limitlessled.pipeline import Pipeline + pipeline = Pipeline() + transition_time = DEFAULT_TRANSITION + # Stop any repeating pipeline. + if self.repeating: + self.repeating = False + self.group.stop() + # Not on and should be? Turn on. + if not self.is_on and new_state is True: + pipeline.on() + # Set transition time. + if ATTR_TRANSITION in kwargs: + transition_time = kwargs[ATTR_TRANSITION] + # Do group type-specific work. + function(self, transition_time, pipeline, **kwargs) + # Update state. + self._is_on = new_state + self.group.enqueue(pipeline) + self.update_ha_state() + return wrapper + return decorator + + +class LimitlessLEDGroup(Light): + """ LimitessLED group. """ + def __init__(self, group): + """ Initialize a group. """ + self.group = group + self.repeating = False + self._is_on = False + self._brightness = None @staticmethod - def factory(pool, controller_id, group, name, group_type): - ''' Construct a Limitless LED of the appropriate type ''' - if group_type == 'white': - return WhiteLimitlessLED(pool, controller_id, group, name) - elif group_type == 'rgbw': - return RGBWLimitlessLED(pool, controller_id, group, name) - - # pylint: disable=too-many-arguments - def __init__(self, pool, controller_id, group, name, group_type): - self.pool = pool - self.controller_id = controller_id - self.group = group - - self.pool.execute(self.controller_id, "set_group_type", self.group, - group_type) - - # LimitlessLEDs don't report state, we have track it ourselves. - self.pool.execute(self.controller_id, "off", self.group) - - self._name = name or DEVICE_DEFAULT_NAME - self._state = False + def factory(group): + """ Produce LimitlessLEDGroup objects. """ + from limitlessled.group.rgbw import RgbwGroup + from limitlessled.group.white import WhiteGroup + if isinstance(group, WhiteGroup): + return LimitlessLEDWhiteGroup(group) + elif isinstance(group, RgbwGroup): + return LimitlessLEDRGBWGroup(group) @property def should_poll(self): - """ No polling needed. """ + """ No polling needed. + + LimitlessLED state cannot be fetched. + """ return False @property def name(self): - """ Returns the name of the device if any. """ - return self._name + """ Returns the name of the group. """ + return self.group.name @property def is_on(self): """ True if device is on. """ - return self._state - - def turn_off(self, **kwargs): - """ Turn the device off. """ - self._state = False - self.pool.execute(self.controller_id, "off", self.group) - self.update_ha_state() - - -class RGBWLimitlessLED(LimitlessLED): - """ Represents a RGBW LimitlessLED light """ - - def __init__(self, pool, controller_id, group, name): - super().__init__(pool, controller_id, group, name, 'rgbw') - - self._brightness = 100 - self._led_color = 'white' + return self._is_on @property def brightness(self): + """ Brightness property. """ return self._brightness + @state(False) + def turn_off(self, transition_time, pipeline, **kwargs): + """ Turn off a group. """ + if self.is_on: + pipeline.transition(transition_time, brightness=0.0).off() + + +class LimitlessLEDWhiteGroup(LimitlessLEDGroup): + """ LimitlessLED White group. """ + def __init__(self, group): + """ Initialize White group. """ + super().__init__(group) + # Initialize group with known values. + self.group.on = True + self.group.temperature = 1.0 + self.group.brightness = 0.0 + self._brightness = _to_hass_brightness(1.0) + self._temperature = _to_hass_temperature(self.group.temperature) + self.group.on = False + + @property + def color_temp(self): + """ Temperature property. """ + return self._temperature + + @state(True) + def turn_on(self, transition_time, pipeline, **kwargs): + """ Turn on (or adjust property of) a group. """ + # Check arguments. + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs[ATTR_BRIGHTNESS] + if ATTR_COLOR_TEMP in kwargs: + self._temperature = kwargs[ATTR_COLOR_TEMP] + # Set up transition. + pipeline.transition(transition_time, + brightness=_from_hass_brightness( + self._brightness), + temperature=_from_hass_temperature( + self._temperature)) + + +class LimitlessLEDRGBWGroup(LimitlessLEDGroup): + """ LimitlessLED RGBW group. """ + def __init__(self, group): + """ Initialize RGBW group. """ + super().__init__(group) + # Initialize group with known values. + self.group.on = True + self.group.white() + self._color = WHITE + self.group.brightness = 0.0 + self._brightness = _to_hass_brightness(1.0) + self.group.on = False + @property def rgb_color(self): - return COLOR_TABLE[self._led_color] - - def turn_on(self, **kwargs): - """ Turn the device on. """ - self._state = True + """ Color property. """ + return self._color + @state(True) + def turn_on(self, transition_time, pipeline, **kwargs): + """ Turn on (or adjust property of) a group. """ + from limitlessled.presets import COLORLOOP + # Check arguments. if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - if ATTR_RGB_COLOR in kwargs: - self._led_color = _rgb_to_led_color(kwargs[ATTR_RGB_COLOR]) - - effect = kwargs.get(ATTR_EFFECT) - - if effect == EFFECT_COLORLOOP: - self.pool.execute(self.controller_id, "disco", self.group) - elif effect == EFFECT_WHITE: - self.pool.execute(self.controller_id, "white", self.group) - else: - self.pool.execute(self.controller_id, "set_color", - self._led_color, self.group) - - # Brightness can be set independently of color - self.pool.execute(self.controller_id, "set_brightness", - self._brightness / 255.0, self.group) - - self.update_ha_state() + self._color = kwargs[ATTR_RGB_COLOR] + # White is a special case. + if min(self._color) > 256 - RGB_BOUNDARY: + pipeline.white() + self._color = WHITE + # Set up transition. + pipeline.transition(transition_time, + brightness=_from_hass_brightness( + self._brightness), + color=_from_hass_color(self._color)) + # Flash. + if ATTR_FLASH in kwargs: + duration = 0 + if kwargs[ATTR_FLASH] == FLASH_LONG: + duration = 1 + pipeline.flash(duration=duration) + # Add effects. + if ATTR_EFFECT in kwargs: + if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP: + self.repeating = True + pipeline.append(COLORLOOP) + if kwargs[ATTR_EFFECT] == EFFECT_WHITE: + pipeline.white() + self._color = WHITE -class WhiteLimitlessLED(LimitlessLED): - """ Represents a White LimitlessLED light """ +def _from_hass_temperature(temperature): + """ Convert Home Assistant color temperature + units to percentage. + """ + return (temperature - 154) / 346 - def __init__(self, pool, controller_id, group, name): - super().__init__(pool, controller_id, group, name, 'white') - def turn_on(self, **kwargs): - """ Turn the device on. """ - self._state = True - self.pool.execute(self.controller_id, "on", self.group) - self.update_ha_state() +def _to_hass_temperature(temperature): + """ Convert percentage to Home Assistant + color temperature units. + """ + return int(temperature * 346) + 154 + + +def _from_hass_brightness(brightness): + """ Convert Home Assistant brightness units + to percentage. + """ + return brightness / 255 + + +def _to_hass_brightness(brightness): + """ Convert percentage to Home Assistant + brightness units. + """ + return int(brightness * 255) + + +def _from_hass_color(color): + """ Convert Home Assistant RGB list + to Color tuple. + """ + from limitlessled import Color + return Color(*tuple(color)) + + +def _to_hass_color(color): + """ Convert from Color tuple to + Home Assistant RGB list. + """ + return list([int(c) for c in color]) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 13537859a0d..30f968d758f 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -8,7 +8,6 @@ https://home-assistant.io/components/light.mqtt/ """ import logging -import homeassistant.util.color as color_util import homeassistant.components.mqtt as mqtt from homeassistant.components.light import (Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR) @@ -19,7 +18,6 @@ DEFAULT_NAME = "MQTT Light" DEFAULT_QOS = 0 DEFAULT_PAYLOAD_ON = "on" DEFAULT_PAYLOAD_OFF = "off" -DEFAULT_RGB_PATTERN = "%d,%d,%d" DEFAULT_OPTIMISTIC = False DEPENDENCIES = ['mqtt'] @@ -37,45 +35,40 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): add_devices_callback([MqttLight( hass, config.get('name', DEFAULT_NAME), - {"state_topic": config.get('state_topic'), - "command_topic": config.get('command_topic'), - "brightness_state_topic": config.get('brightness_state_topic'), - "brightness_command_topic": - config.get('brightness_command_topic'), - "rgb_state_topic": config.get('rgb_state_topic'), - "rgb_command_topic": config.get('rgb_command_topic')}, - config.get('rgb', None), + { + "state_topic": config.get('state_topic'), + "command_topic": config.get('command_topic'), + "brightness_state_topic": config.get('brightness_state_topic'), + "brightness_command_topic": config.get('brightness_command_topic'), + "rgb_state_topic": config.get('rgb_state_topic'), + "rgb_command_topic": config.get('rgb_command_topic') + }, config.get('qos', DEFAULT_QOS), - {"on": config.get('payload_on', DEFAULT_PAYLOAD_ON), - "off": config.get('payload_off', DEFAULT_PAYLOAD_OFF)}, - config.get('brightness'), + { + "on": config.get('payload_on', DEFAULT_PAYLOAD_ON), + "off": config.get('payload_off', DEFAULT_PAYLOAD_OFF) + }, config.get('optimistic', DEFAULT_OPTIMISTIC))]) -# pylint: disable=too-many-instance-attributes - class MqttLight(Light): """ Provides a MQTT light. """ - # pylint: disable=too-many-arguments - def __init__(self, hass, name, - topic, - rgb, qos, - payload, - brightness, optimistic): + # pylint: disable=too-many-arguments,too-many-instance-attributes + def __init__(self, hass, name, topic, qos, payload, optimistic): self._hass = hass self._name = name self._topic = topic - self._rgb = rgb self._qos = qos self._payload = payload - self._brightness = brightness - self._optimistic = optimistic + self._optimistic = optimistic or topic["state_topic"] is None + self._optimistic_rgb = optimistic or topic["rgb_state_topic"] is None + self._optimistic_brightness = (optimistic or + topic["brightness_state_topic"] is None) self._state = False - self._xy = None - def message_received(topic, payload, qos): + def state_received(topic, payload, qos): """ A new MQTT message has been received. """ if payload == self._payload["on"]: self._state = True @@ -84,27 +77,15 @@ class MqttLight(Light): self.update_ha_state() - if self._topic["state_topic"] is None: - # force optimistic mode - self._optimistic = True - else: - # Subscribe the state_topic + if self._topic["state_topic"] is not None: mqtt.subscribe(self._hass, self._topic["state_topic"], - message_received, self._qos) + state_received, self._qos) def brightness_received(topic, payload, qos): """ A new MQTT message for the brightness has been received. """ self._brightness = int(payload) self.update_ha_state() - def rgb_received(topic, payload, qos): - """ A new MQTT message has been received. """ - self._rgb = [int(val) for val in payload.split(',')] - self._xy = color_util.color_RGB_to_xy(int(self._rgb[0]), - int(self._rgb[1]), - int(self._rgb[2])) - self.update_ha_state() - if self._topic["brightness_state_topic"] is not None: mqtt.subscribe(self._hass, self._topic["brightness_state_topic"], brightness_received, self._qos) @@ -112,12 +93,17 @@ class MqttLight(Light): else: self._brightness = None + def rgb_received(topic, payload, qos): + """ A new MQTT message has been received. """ + self._rgb = [int(val) for val in payload.split(',')] + self.update_ha_state() + if self._topic["rgb_state_topic"] is not None: mqtt.subscribe(self._hass, self._topic["rgb_state_topic"], rgb_received, self._qos) - self._xy = [0, 0] + self._rgb = [255, 255, 255] else: - self._xy = None + self._rgb = None @property def brightness(self): @@ -129,11 +115,6 @@ class MqttLight(Light): """ RGB color value. """ return self._rgb - @property - def color_xy(self): - """ RGB color value. """ - return self._xy - @property def should_poll(self): """ No polling needed for a MQTT light. """ @@ -151,19 +132,26 @@ class MqttLight(Light): def turn_on(self, **kwargs): """ Turn the device on. """ + should_update = False if ATTR_RGB_COLOR in kwargs and \ self._topic["rgb_command_topic"] is not None: - self._rgb = kwargs[ATTR_RGB_COLOR] - rgb = DEFAULT_RGB_PATTERN % tuple(self._rgb) mqtt.publish(self._hass, self._topic["rgb_command_topic"], - rgb, self._qos) + "{},{},{}".format(*kwargs[ATTR_RGB_COLOR]), self._qos) + + if self._optimistic_rgb: + self._rgb = kwargs[ATTR_RGB_COLOR] + should_update = True if ATTR_BRIGHTNESS in kwargs and \ self._topic["brightness_command_topic"] is not None: - self._brightness = kwargs[ATTR_BRIGHTNESS] + mqtt.publish(self._hass, self._topic["brightness_command_topic"], - self._brightness, self._qos) + kwargs[ATTR_BRIGHTNESS], self._qos) + + if self._optimistic_brightness: + self._brightness = kwargs[ATTR_BRIGHTNESS] + should_update = True mqtt.publish(self._hass, self._topic["command_topic"], self._payload["on"], self._qos) @@ -171,6 +159,9 @@ class MqttLight(Light): if self._optimistic: # optimistically assume that switch has changed state self._state = True + should_update = True + + if should_update: self.update_ha_state() def turn_off(self, **kwargs): diff --git a/homeassistant/components/light/rfxtrx.py b/homeassistant/components/light/rfxtrx.py index 8d6a2b86217..6132c10a99c 100644 --- a/homeassistant/components/light/rfxtrx.py +++ b/homeassistant/components/light/rfxtrx.py @@ -12,6 +12,11 @@ import homeassistant.components.rfxtrx as rfxtrx from homeassistant.components.light import Light from homeassistant.util import slugify +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.components.rfxtrx import ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID, \ + ATTR_NAME, EVENT_BUTTON_PRESSED + + DEPENDENCIES = ['rfxtrx'] _LOGGER = logging.getLogger(__name__) @@ -23,12 +28,20 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): lights = [] devices = config.get('devices', None) + if devices: for entity_id, entity_info in devices.items(): if entity_id not in rfxtrx.RFX_DEVICES: - _LOGGER.info("Add %s rfxtrx.light", entity_info['name']) - rfxobject = rfxtrx.get_rfx_object(entity_info['packetid']) - new_light = RfxtrxLight(entity_info['name'], rfxobject, False) + _LOGGER.info("Add %s rfxtrx.light", entity_info[ATTR_NAME]) + + # Check if i must fire event + fire_event = entity_info.get(ATTR_FIREEVENT, False) + datas = {ATTR_STATE: False, ATTR_FIREEVENT: fire_event} + + rfxobject = rfxtrx.get_rfx_object(entity_info[ATTR_PACKETID]) + new_light = RfxtrxLight( + entity_info[ATTR_NAME], rfxobject, datas + ) rfxtrx.RFX_DEVICES[entity_id] = new_light lights.append(new_light) @@ -54,12 +67,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): ) pkt_id = "".join("{0:02x}".format(x) for x in event.data) entity_name = "%s : %s" % (entity_id, pkt_id) - new_light = RfxtrxLight(entity_name, event, False) + datas = {ATTR_STATE: False, ATTR_FIREEVENT: False} + new_light = RfxtrxLight(entity_name, event, datas) rfxtrx.RFX_DEVICES[entity_id] = new_light add_devices_callback([new_light]) # Check if entity exists or previously added automatically - if entity_id in rfxtrx.RFX_DEVICES: + if entity_id in rfxtrx.RFX_DEVICES \ + and isinstance(rfxtrx.RFX_DEVICES[entity_id], RfxtrxLight): _LOGGER.debug( "EntityID: %s light_update. Command: %s", entity_id, @@ -67,10 +82,22 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): ) if event.values['Command'] == 'On'\ or event.values['Command'] == 'Off': - if event.values['Command'] == 'On': - rfxtrx.RFX_DEVICES[entity_id].turn_on() - else: - rfxtrx.RFX_DEVICES[entity_id].turn_off() + + # Update the rfxtrx device state + is_on = event.values['Command'] == 'On' + # pylint: disable=protected-access + rfxtrx.RFX_DEVICES[entity_id]._state = is_on + rfxtrx.RFX_DEVICES[entity_id].update_ha_state() + + # Fire event + if rfxtrx.RFX_DEVICES[entity_id].should_fire_event: + rfxtrx.RFX_DEVICES[entity_id].hass.bus.fire( + EVENT_BUTTON_PRESSED, { + ATTR_ENTITY_ID: + rfxtrx.RFX_DEVICES[entity_id].entity_id, + ATTR_STATE: event.values['Command'].lower() + } + ) # Subscribe to main rfxtrx events if light_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: @@ -79,10 +106,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class RfxtrxLight(Light): """ Provides a RFXtrx light. """ - def __init__(self, name, event, state): + def __init__(self, name, event, datas): self._name = name self._event = event - self._state = state + self._state = datas[ATTR_STATE] + self._should_fire_event = datas[ATTR_FIREEVENT] @property def should_poll(self): @@ -94,6 +122,11 @@ class RfxtrxLight(Light): """ Returns the name of the light if any. """ return self._name + @property + def should_fire_event(self): + """ Returns is the device must fire event""" + return self._should_fire_event + @property def is_on(self): """ True if light is on. """ diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/light/tellstick.py index 24d86c47c51..48a5a3ed814 100644 --- a/homeassistant/components/light/tellstick.py +++ b/homeassistant/components/light/tellstick.py @@ -10,6 +10,7 @@ from homeassistant.components.light import Light, ATTR_BRIGHTNESS from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, ATTR_FRIENDLY_NAME) REQUIREMENTS = ['tellcore-py==1.1.2'] +SIGNAL_REPETITIONS = 1 # pylint: disable=unused-argument @@ -21,13 +22,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): import tellcore.constants as tellcore_constants core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher()) + signal_repetitions = config.get('signal_repetitions', SIGNAL_REPETITIONS) switches_and_lights = core.devices() lights = [] for switch in switches_and_lights: if switch.methods(tellcore_constants.TELLSTICK_DIM): - lights.append(TellstickLight(switch)) + lights.append(TellstickLight(switch, signal_repetitions)) def _device_event_callback(id_, method, data, cid): """ Called from the TelldusCore library to update one device """ @@ -52,11 +54,12 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class TellstickLight(Light): """ Represents a Tellstick light. """ - def __init__(self, tellstick_device): + def __init__(self, tellstick_device, signal_repetitions): import tellcore.constants as tellcore_constants self.tellstick_device = tellstick_device self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name} + self.signal_repetitions = signal_repetitions self._brightness = 0 self.last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | @@ -64,6 +67,7 @@ class TellstickLight(Light): tellcore_constants.TELLSTICK_DIM | tellcore_constants.TELLSTICK_UP | tellcore_constants.TELLSTICK_DOWN) + self.update() @property def name(self): @@ -82,7 +86,8 @@ class TellstickLight(Light): def turn_off(self, **kwargs): """ Turns the switch off. """ - self.tellstick_device.turn_off() + for _ in range(self.signal_repetitions): + self.tellstick_device.turn_off() self._brightness = 0 self.update_ha_state() @@ -95,7 +100,8 @@ class TellstickLight(Light): else: self._brightness = brightness - self.tellstick_device.dim(self._brightness) + for _ in range(self.signal_repetitions): + self.tellstick_device.dim(self._brightness) self.update_ha_state() def update(self): diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index 4fbf87aea2d..eaa703799f7 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -13,8 +13,8 @@ from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' - '9eb39eaba0717922815e673ad1114c685839d890.zip' - '#python-wink==0.1.1'] + '42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip' + '#python-wink==0.2'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 31cd64d2530..02664ed896c 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -6,12 +6,13 @@ Support for Z-Wave lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.zwave/ """ +# Because we do not compile openzwave on CI # pylint: disable=import-error -import homeassistant.components.zwave as zwave +from threading import Timer from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.light import (Light, ATTR_BRIGHTNESS) -from threading import Timer +import homeassistant.components.zwave as zwave def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py new file mode 100644 index 00000000000..0d67679e82c --- /dev/null +++ b/homeassistant/components/lock/__init__.py @@ -0,0 +1,112 @@ +""" +homeassistant.components.lock +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Component to interface with various locks that can be controlled remotely. + +For more details about this component, please refer to the documentation +at https://home-assistant.io/components/lock/ +""" +from datetime import timedelta +import logging +import os + +from homeassistant.config import load_yaml_config_file +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity import Entity + +from homeassistant.const import ( + STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK, + ATTR_ENTITY_ID) +from homeassistant.components import (group, wink) + +DOMAIN = 'lock' +SCAN_INTERVAL = 30 + +GROUP_NAME_ALL_LOCKS = 'all locks' +ENTITY_ID_ALL_LOCKS = group.ENTITY_ID_FORMAT.format('all_locks') + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +ATTR_LOCKED = "locked" + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + +# Maps discovered services to their platforms +DISCOVERY_PLATFORMS = { + wink.DISCOVER_LOCKS: 'wink' +} + +_LOGGER = logging.getLogger(__name__) + + +def is_locked(hass, entity_id=None): + """ Returns if the lock is locked based on the statemachine. """ + entity_id = entity_id or ENTITY_ID_ALL_LOCKS + return hass.states.is_state(entity_id, STATE_LOCKED) + + +def lock(hass, entity_id=None): + """ Locks all or specified locks. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_LOCK, data) + + +def unlock(hass, entity_id=None): + """ Unlocks all or specified locks. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_UNLOCK, data) + + +def setup(hass, config): + """ Track states and offer events for locks. """ + component = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS, + GROUP_NAME_ALL_LOCKS) + component.setup(config) + + def handle_lock_service(service): + """ Handles calls to the lock services. """ + target_locks = component.extract_from_service(service) + + for item in target_locks: + if service.service == SERVICE_LOCK: + item.lock() + else: + item.unlock() + + if item.should_poll: + item.update_ha_state(True) + + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'services.yaml')) + hass.services.register(DOMAIN, SERVICE_UNLOCK, handle_lock_service, + descriptions.get(SERVICE_UNLOCK)) + hass.services.register(DOMAIN, SERVICE_LOCK, handle_lock_service, + descriptions.get(SERVICE_LOCK)) + + return True + + +class LockDevice(Entity): + """ Represents a lock within Home Assistant. """ + # pylint: disable=no-self-use + + @property + def is_locked(self): + """ Is the lock locked or unlocked. """ + return None + + def lock(self): + """ Locks the lock. """ + raise NotImplementedError() + + def unlock(self): + """ Unlocks the lock. """ + raise NotImplementedError() + + @property + def state(self): + locked = self.is_locked + if locked is None: + return STATE_UNKNOWN + return STATE_LOCKED if locked else STATE_UNLOCKED diff --git a/homeassistant/components/lock/demo.py b/homeassistant/components/lock/demo.py new file mode 100644 index 00000000000..472b17f46bf --- /dev/null +++ b/homeassistant/components/lock/demo.py @@ -0,0 +1,49 @@ +""" +homeassistant.components.lock.demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Demo platform that has two fake locks. +""" +from homeassistant.components.lock import LockDevice +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Find and return demo locks. """ + add_devices_callback([ + DemoLock('Front Door', STATE_LOCKED), + DemoLock('Kitchen Door', STATE_UNLOCKED) + ]) + + +class DemoLock(LockDevice): + """ Provides a demo lock. """ + def __init__(self, name, state): + self._name = name + self._state = state + + @property + def should_poll(self): + """ No polling needed for a demo lock. """ + return False + + @property + def name(self): + """ Returns the name of the device if any. """ + return self._name + + @property + def is_locked(self): + """ True if device is locked. """ + return self._state == STATE_LOCKED + + def lock(self, **kwargs): + """ Lock the device. """ + self._state = STATE_LOCKED + self.update_ha_state() + + def unlock(self, **kwargs): + """ Unlock the device. """ + self._state = STATE_UNLOCKED + self.update_ha_state() diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py new file mode 100644 index 00000000000..27f602d65fa --- /dev/null +++ b/homeassistant/components/lock/wink.py @@ -0,0 +1,68 @@ +""" +homeassistant.components.lock.wink +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Wink locks. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.wink/ +""" +import logging + +from homeassistant.components.lock import LockDevice +from homeassistant.const import CONF_ACCESS_TOKEN + +REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' + '42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip' + '#python-wink==0.2'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Wink platform. """ + import pywink + + if discovery_info is None: + token = config.get(CONF_ACCESS_TOKEN) + + if token is None: + logging.getLogger(__name__).error( + "Missing wink access_token. " + "Get one at https://winkbearertoken.appspot.com/") + return + + pywink.set_bearer_token(token) + + add_devices(WinkLockDevice(lock) for lock in pywink.get_locks()) + + +class WinkLockDevice(LockDevice): + """ Represents a Wink lock. """ + + def __init__(self, wink): + self.wink = wink + + @property + def unique_id(self): + """ Returns the id of this wink lock """ + return "{}.{}".format(self.__class__, self.wink.deviceId()) + + @property + def name(self): + """ Returns the name of the lock if any. """ + return self.wink.name() + + def update(self): + """ Update the state of the lock. """ + self.wink.updateState() + + @property + def is_locked(self): + """ True if device is locked. """ + return self.wink.state() + + def lock(self): + """ Lock the device. """ + self.wink.setState(True) + + def unlock(self): + """ Unlock the device. """ + self.wink.setState(False) diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index a6dafa56005..9a5d1c59d1a 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -10,7 +10,6 @@ import logging from collections import OrderedDict DOMAIN = 'logger' -DEPENDENCIES = [] LOGSEVERITY = { 'CRITICAL': 50, diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 8140bbb2af9..8204052b4a9 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -22,7 +22,6 @@ from homeassistant.const import ( SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK) DOMAIN = 'media_player' -DEPENDENCIES = [] SCAN_INTERVAL = 10 ENTITY_ID_FORMAT = DOMAIN + '.{}' diff --git a/homeassistant/components/media_player/firetv.py b/homeassistant/components/media_player/firetv.py index 1b2c921e3d4..e5f9885f86e 100644 --- a/homeassistant/components/media_player/firetv.py +++ b/homeassistant/components/media_player/firetv.py @@ -49,7 +49,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.info( 'Device %s accessible and ready for control', device_id) else: - _LOGGER.warn( + _LOGGER.warning( 'Device %s is not registered with firetv-server', device_id) except requests.exceptions.RequestException: _LOGGER.error('Could not connect to firetv-server at %s', host) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 275e7d96dee..5d08a7e95d4 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -8,6 +8,8 @@ https://home-assistant.io/components/media_player.itunes/ """ import logging +import requests + from homeassistant.components.media_player import ( MediaPlayerDevice, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, @@ -17,8 +19,6 @@ from homeassistant.components.media_player import ( from homeassistant.const import ( STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_ON) -import requests - _LOGGER = logging.getLogger(__name__) SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index eda143b6cce..6fe6be554c6 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -15,11 +15,6 @@ from homeassistant.components.media_player import ( from homeassistant.const import ( STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF) -try: - import jsonrpc_requests -except ImportError: - jsonrpc_requests = None - _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['jsonrpc-requests==0.1'] @@ -31,11 +26,6 @@ SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the kodi platform. """ - global jsonrpc_requests # pylint: disable=invalid-name - if jsonrpc_requests is None: - import jsonrpc_requests as jsonrpc_requests_ - jsonrpc_requests = jsonrpc_requests_ - add_devices([ KodiDevice( config.get('name', 'Kodi'), @@ -60,6 +50,7 @@ class KodiDevice(MediaPlayerDevice): # pylint: disable=too-many-public-methods def __init__(self, name, url, auth=None): + import jsonrpc_requests self._name = name self._url = url self._server = jsonrpc_requests.Server(url, auth=auth) @@ -77,6 +68,7 @@ class KodiDevice(MediaPlayerDevice): def _get_players(self): """ Returns the active player objects or None """ + import jsonrpc_requests try: return self._server.Player.GetActivePlayers() except jsonrpc_requests.jsonrpc.TransportError: diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 87ea9b99efb..71c0c2aeb75 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -39,9 +39,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Sonos platform. """ import soco + if discovery_info: + add_devices([SonosDevice(hass, soco.SoCo(discovery_info))]) + return True + players = soco.discover() + if not players: - _LOGGER.warning('No Sonos speakers found. Disabling: %s', __name__) + _LOGGER.warning('No Sonos speakers found.') return False add_devices(SonosDevice(hass, p) for p in players) diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py index 099801eb7cf..6f53c89835a 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -13,7 +13,6 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_START, DOMAIN = "modbus" -DEPENDENCIES = [] REQUIREMENTS = ['https://github.com/bashwork/pymodbus/archive/' 'd7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0'] diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index cd5b5370175..2c5dbf82923 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -6,9 +6,12 @@ MQTT component, using paho-mqtt. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mqtt/ """ +import json import logging import os import socket +import time + from homeassistant.exceptions import HomeAssistantError import homeassistant.util as util @@ -25,12 +28,12 @@ MQTT_CLIENT = None DEFAULT_PORT = 1883 DEFAULT_KEEPALIVE = 60 DEFAULT_QOS = 0 +DEFAULT_RETAIN = False SERVICE_PUBLISH = 'publish' EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED' -DEPENDENCIES = [] -REQUIREMENTS = ['paho-mqtt==1.1'] +REQUIREMENTS = ['paho-mqtt==1.1', 'jsonpath-rw==1.4.0'] CONF_BROKER = 'broker' CONF_PORT = 'port' @@ -43,9 +46,12 @@ CONF_CERTIFICATE = 'certificate' ATTR_TOPIC = 'topic' ATTR_PAYLOAD = 'payload' ATTR_QOS = 'qos' +ATTR_RETAIN = 'retain' + +MAX_RECONNECT_WAIT = 300 # seconds -def publish(hass, topic, payload, qos=None): +def publish(hass, topic, payload, qos=None, retain=None): """ Send an MQTT message. """ data = { ATTR_TOPIC: topic, @@ -53,6 +59,10 @@ def publish(hass, topic, payload, qos=None): } if qos is not None: data[ATTR_QOS] = qos + + if retain is not None: + data[ATTR_RETAIN] = retain + hass.services.call(DOMAIN, SERVICE_PUBLISH, data) @@ -65,9 +75,7 @@ def subscribe(hass, topic, callback, qos=DEFAULT_QOS): event.data[ATTR_QOS]) hass.bus.listen(EVENT_MQTT_MESSAGE_RECEIVED, mqtt_topic_subscriber) - - if topic not in MQTT_CLIENT.topics: - MQTT_CLIENT.subscribe(topic, qos) + MQTT_CLIENT.subscribe(topic, qos) def setup(hass, config): @@ -116,9 +124,10 @@ def setup(hass, config): msg_topic = call.data.get(ATTR_TOPIC) payload = call.data.get(ATTR_PAYLOAD) qos = call.data.get(ATTR_QOS, DEFAULT_QOS) + retain = call.data.get(ATTR_RETAIN, DEFAULT_RETAIN) if msg_topic is None or payload is None: return - MQTT_CLIENT.publish(msg_topic, payload, qos) + MQTT_CLIENT.publish(msg_topic, payload, qos, retain) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_mqtt) @@ -127,44 +136,69 @@ def setup(hass, config): return True +# pylint: disable=too-few-public-methods +class _JsonFmtParser(object): + """ Implements a JSON parser on xpath. """ + def __init__(self, jsonpath): + import jsonpath_rw + self._expr = jsonpath_rw.parse(jsonpath) + + def __call__(self, payload): + match = self._expr.find(json.loads(payload)) + return match[0].value if len(match) > 0 else payload + + +# pylint: disable=too-few-public-methods +class FmtParser(object): + """ Wrapper for all supported formats. """ + def __init__(self, fmt): + self._parse = lambda x: x + if fmt: + if fmt.startswith('json:'): + self._parse = _JsonFmtParser(fmt[5:]) + + def __call__(self, payload): + return self._parse(payload) + + # This is based on one of the paho-mqtt examples: # http://git.eclipse.org/c/paho/org.eclipse.paho.mqtt.python.git/tree/examples/sub-class.py # pylint: disable=too-many-arguments -class MQTT(object): # pragma: no cover +class MQTT(object): """ Implements messaging service for MQTT. """ def __init__(self, hass, broker, port, client_id, keepalive, username, password, certificate): import paho.mqtt.client as mqtt - self.hass = hass - self._progress = {} - self.topics = {} + self.userdata = { + 'hass': hass, + 'topics': {}, + 'progress': {}, + } if client_id is None: self._mqttc = mqtt.Client() else: self._mqttc = mqtt.Client(client_id) + self._mqttc.user_data_set(self.userdata) + if username is not None: self._mqttc.username_pw_set(username, password) if certificate is not None: self._mqttc.tls_set(certificate) - self._mqttc.on_subscribe = self._mqtt_on_subscribe - self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe - self._mqttc.on_connect = self._mqtt_on_connect - self._mqttc.on_message = self._mqtt_on_message + self._mqttc.on_subscribe = _mqtt_on_subscribe + self._mqttc.on_unsubscribe = _mqtt_on_unsubscribe + self._mqttc.on_connect = _mqtt_on_connect + self._mqttc.on_disconnect = _mqtt_on_disconnect + self._mqttc.on_message = _mqtt_on_message + self._mqttc.connect(broker, port, keepalive) - def publish(self, topic, payload, qos): + def publish(self, topic, payload, qos, retain): """ Publish a MQTT message. """ - self._mqttc.publish(topic, payload, qos) - - def unsubscribe(self, topic): - """ Unsubscribe from topic. """ - result, mid = self._mqttc.unsubscribe(topic) - _raise_on_error(result) - self._progress[mid] = topic + self._mqttc.publish(topic, payload, qos, retain) def start(self): """ Run the MQTT client. """ @@ -176,58 +210,96 @@ class MQTT(object): # pragma: no cover def subscribe(self, topic, qos): """ Subscribe to a topic. """ - if topic in self.topics: + if topic in self.userdata['topics']: return result, mid = self._mqttc.subscribe(topic, qos) _raise_on_error(result) - self._progress[mid] = topic - self.topics[topic] = None + self.userdata['progress'][mid] = topic + self.userdata['topics'][topic] = None - def _mqtt_on_connect(self, mqttc, obj, flags, result_code): - """ On connect, resubscribe to all topics we were subscribed to. """ - if result_code != 0: - _LOGGER.error('Unable to connect to the MQTT broker: %s', { - 1: 'Incorrect protocol version', - 2: 'Invalid client identifier', - 3: 'Server unavailable', - 4: 'Bad username or password', - 5: 'Not authorised' - }.get(result_code)) - self._mqttc.disconnect() - return - - old_topics = self.topics - self._progress = {} - self.topics = {} - for topic, qos in old_topics.items(): - # qos is None if we were in process of subscribing - if qos is not None: - self._mqttc.subscribe(topic, qos) - - def _mqtt_on_subscribe(self, mqttc, obj, mid, granted_qos): - """ Called when subscribe succesfull. """ - topic = self._progress.pop(mid, None) - if topic is None: - return - self.topics[topic] = granted_qos - - def _mqtt_on_unsubscribe(self, mqttc, obj, mid, granted_qos): - """ Called when subscribe succesfull. """ - topic = self._progress.pop(mid, None) - if topic is None: - return - self.topics.pop(topic, None) - - def _mqtt_on_message(self, mqttc, obj, msg): - """ Message callback """ - self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, { - ATTR_TOPIC: msg.topic, - ATTR_QOS: msg.qos, - ATTR_PAYLOAD: msg.payload.decode('utf-8'), - }) + def unsubscribe(self, topic): + """ Unsubscribe from topic. """ + result, mid = self._mqttc.unsubscribe(topic) + _raise_on_error(result) + self.userdata['progress'][mid] = topic -def _raise_on_error(result): # pragma: no cover +def _mqtt_on_message(mqttc, userdata, msg): + """ Message callback """ + userdata['hass'].bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, { + ATTR_TOPIC: msg.topic, + ATTR_QOS: msg.qos, + ATTR_PAYLOAD: msg.payload.decode('utf-8'), + }) + + +def _mqtt_on_connect(mqttc, userdata, flags, result_code): + """ On connect, resubscribe to all topics we were subscribed to. """ + if result_code != 0: + _LOGGER.error('Unable to connect to the MQTT broker: %s', { + 1: 'Incorrect protocol version', + 2: 'Invalid client identifier', + 3: 'Server unavailable', + 4: 'Bad username or password', + 5: 'Not authorised' + }.get(result_code, 'Unknown reason')) + mqttc.disconnect() + return + + old_topics = userdata['topics'] + + userdata['topics'] = {} + userdata['progress'] = {} + + for topic, qos in old_topics.items(): + # qos is None if we were in process of subscribing + if qos is not None: + mqttc.subscribe(topic, qos) + + +def _mqtt_on_subscribe(mqttc, userdata, mid, granted_qos): + """ Called when subscribe successful. """ + topic = userdata['progress'].pop(mid, None) + if topic is None: + return + userdata['topics'][topic] = granted_qos + + +def _mqtt_on_unsubscribe(mqttc, userdata, mid, granted_qos): + """ Called when subscribe successful. """ + topic = userdata['progress'].pop(mid, None) + if topic is None: + return + userdata['topics'].pop(topic, None) + + +def _mqtt_on_disconnect(mqttc, userdata, result_code): + """ Called when being disconnected. """ + # When disconnected because of calling disconnect() + if result_code == 0: + return + + tries = 0 + wait_time = 0 + + while True: + try: + if mqttc.reconnect() == 0: + _LOGGER.info('Successfully reconnected to the MQTT server') + break + except socket.error: + pass + + wait_time = min(2**tries, MAX_RECONNECT_WAIT) + _LOGGER.warning( + 'Disconnected from MQTT (%s). Trying to reconnect in %ss', + result_code, wait_time) + # It is ok to sleep here as we are in the MQTT thread. + time.sleep(wait_time) + tries += 1 + + +def _raise_on_error(result): """ Raise error if error result. """ if result != 0: raise HomeAssistantError('Error talking to MQTT: {}'.format(result)) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 6cd7a2196cf..9182f1dbf3a 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -17,7 +17,6 @@ from homeassistant.helpers import config_per_platform from homeassistant.const import CONF_NAME DOMAIN = "notify" -DEPENDENCIES = [] # Title of notification ATTR_TITLE = "title" diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 16c4e4192eb..941a78ac709 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -47,16 +47,16 @@ class PushBulletNotificationService(BaseNotificationService): self.refresh() def refresh(self): - ''' + """ Refresh devices, contacts, etc pbtargets stores all targets available from this pushbullet instance into a dict. These are PB objects!. It sacrifices a bit of memory - for faster processing at send_message + for faster processing at send_message. As of sept 2015, contacts were replaced by chats. This is not - implemented in the module yet - ''' + implemented in the module yet. + """ self.pushbullet.refresh() self.pbtargets = { 'device': { @@ -72,7 +72,7 @@ class PushBulletNotificationService(BaseNotificationService): If no target specified, a 'normal' push will be sent to all devices linked to the PB account. Email is special, these are assumed to always exist. We use a special - call which doesn't require a push object + call which doesn't require a push object. """ targets = kwargs.get(ATTR_TARGET) title = kwargs.get(ATTR_TITLE) @@ -100,7 +100,7 @@ class PushBulletNotificationService(BaseNotificationService): # This also seems works to send to all devices in own account if ttype == 'email': self.pushbullet.push_note(title, message, email=tname) - _LOGGER.info('Sent notification to self') + _LOGGER.info('Sent notification to email %s', tname) continue # Refresh if name not found. While awaiting periodic refresh @@ -108,18 +108,21 @@ class PushBulletNotificationService(BaseNotificationService): if ttype not in self.pbtargets: _LOGGER.error('Invalid target syntax: %s', target) continue - if tname.lower() not in self.pbtargets[ttype] and not refreshed: + + tname = tname.lower() + + if tname not in self.pbtargets[ttype] and not refreshed: self.refresh() refreshed = True # Attempt push_note on a dict value. Keys are types & target # name. Dict pbtargets has all *actual* targets. try: - self.pbtargets[ttype][tname.lower()].push_note(title, message) + self.pbtargets[ttype][tname].push_note(title, message) + _LOGGER.info('Sent notification to %s/%s', ttype, tname) except KeyError: _LOGGER.error('No such target: %s/%s', ttype, tname) continue except self.pushbullet.errors.PushError: _LOGGER.error('Notify failed to: %s/%s', ttype, tname) continue - _LOGGER.info('Sent notification to %s/%s', ttype, tname) diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py index 016e0a949fd..4b688fb7a79 100644 --- a/homeassistant/components/notify/xmpp.py +++ b/homeassistant/components/notify/xmpp.py @@ -8,14 +8,14 @@ https://home-assistant.io/components/notify.xmpp/ """ import logging -_LOGGER = logging.getLogger(__name__) - from homeassistant.helpers import validate_config from homeassistant.components.notify import ( DOMAIN, ATTR_TITLE, BaseNotificationService) REQUIREMENTS = ['sleekxmpp==1.3.1', 'dnspython3==1.12.0'] +_LOGGER = logging.getLogger(__name__) + def get_service(hass, config): """ Get the Jabber (XMPP) notification service. """ diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index b09e10f7d92..126d8c9f40e 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -23,7 +23,6 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) DOMAIN = "recorder" -DEPENDENCIES = [] DB_FILE = 'home-assistant.db' @@ -59,7 +58,7 @@ def query_events(event_query, arguments=None): def row_to_state(row): - """ Convert a databsae row to a state. """ + """ Convert a database row to a state. """ try: return State( row[1], row[2], json.loads(row[3]), @@ -74,7 +73,7 @@ def row_to_state(row): def row_to_event(row): """ Convert a databse row to an event. """ try: - return Event(row[1], json.loads(row[2]), EventOrigin[row[3].lower()], + return Event(row[1], json.loads(row[2]), EventOrigin(row[3]), date_util.utc_from_timestamp(row[5])) except ValueError: # When json.loads fails @@ -83,8 +82,9 @@ def row_to_event(row): def run_information(point_in_time=None): - """ Returns information about current run or the run that - covers point_in_time. """ + """ + Returns information about current run or the run that covers point_in_time. + """ _verify_instance() if point_in_time is None or point_in_time > _INSTANCE.recording_start: @@ -142,8 +142,10 @@ class RecorderRun(object): @property def where_after_start_run(self): - """ Returns SQL WHERE clause to select rows - created after the start of the run. """ + """ + Returns SQL WHERE clause to select rows created after the start of the + run. + """ return "created >= {} ".format(_adapt_datetime(self.start)) @property @@ -158,9 +160,7 @@ class RecorderRun(object): class Recorder(threading.Thread): - """ - Threaded recorder - """ + """ Threaded recorder class """ def __init__(self, hass): threading.Thread.__init__(self) @@ -208,8 +208,10 @@ class Recorder(threading.Thread): self.queue.task_done() def event_listener(self, event): - """ Listens for new events on the EventBus and puts them - in the process queue. """ + """ + Listens for new events on the EventBus and puts them in the process + queue. + """ self.queue.put(event) def shutdown(self, event): @@ -433,6 +435,6 @@ def _adapt_datetime(datetimestamp): def _verify_instance(): - """ throws error if recorder not initialized. """ + """ Throws error if recorder not initialized. """ if _INSTANCE is None: raise RuntimeError("Recorder not initialized.") diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 3c4675d806d..aea3afe7f05 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -9,13 +9,20 @@ https://home-assistant.io/components/rfxtrx/ import logging from homeassistant.util import slugify -DEPENDENCIES = [] REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/0.2.zip' + '#RFXtrx==0.2'] DOMAIN = "rfxtrx" -CONF_DEVICE = 'device' -CONF_DEBUG = 'debug' + +ATTR_DEVICE = 'device' +ATTR_DEBUG = 'debug' +ATTR_STATE = 'state' +ATTR_NAME = 'name' +ATTR_PACKETID = 'packetid' +ATTR_FIREEVENT = 'fire_event' + +EVENT_BUTTON_PRESSED = 'button_pressed' + RECEIVED_EVT_SUBSCRIBERS = [] RFX_DEVICES = {} _LOGGER = logging.getLogger(__name__) @@ -50,15 +57,15 @@ def setup(hass, config): # Init the rfxtrx module global RFXOBJECT - if CONF_DEVICE not in config[DOMAIN]: + if ATTR_DEVICE not in config[DOMAIN]: _LOGGER.exception( "can found device parameter in %s YAML configuration section", DOMAIN ) return False - device = config[DOMAIN][CONF_DEVICE] - debug = config[DOMAIN].get(CONF_DEBUG, False) + device = config[DOMAIN][ATTR_DEVICE] + debug = config[DOMAIN].get(ATTR_DEBUG, False) RFXOBJECT = rfxtrxmod.Core(device, handle_receive, debug=debug) diff --git a/homeassistant/components/rollershutter/__init__.py b/homeassistant/components/rollershutter/__init__.py new file mode 100644 index 00000000000..517ebf97b25 --- /dev/null +++ b/homeassistant/components/rollershutter/__init__.py @@ -0,0 +1,144 @@ +""" +homeassistant.components.rollershutter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Rollershutter component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/rollershutter/ +""" +import os +import logging + +from homeassistant.config import load_yaml_config_file +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity import Entity +from homeassistant.components import group +from homeassistant.const import ( + SERVICE_MOVE_UP, SERVICE_MOVE_DOWN, SERVICE_STOP, + STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN, ATTR_ENTITY_ID) + + +DOMAIN = 'rollershutter' +SCAN_INTERVAL = 15 + +GROUP_NAME_ALL_ROLLERSHUTTERS = 'all rollershutters' +ENTITY_ID_ALL_ROLLERSHUTTERS = group.ENTITY_ID_FORMAT.format( + 'all_rollershutters') + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +# Maps discovered services to their platforms +DISCOVERY_PLATFORMS = {} + +_LOGGER = logging.getLogger(__name__) + +ATTR_CURRENT_POSITION = 'current_position' + + +def is_open(hass, entity_id=None): + """ Returns if the rollershutter is open based on the statemachine. """ + entity_id = entity_id or ENTITY_ID_ALL_ROLLERSHUTTERS + return hass.states.is_state(entity_id, STATE_OPEN) + + +def move_up(hass, entity_id=None): + """ Move up all or specified rollershutter. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_MOVE_UP, data) + + +def move_down(hass, entity_id=None): + """ Move down all or specified rollershutter. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_MOVE_DOWN, data) + + +def stop(hass, entity_id=None): + """ Stops all or specified rollershutter. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_STOP, data) + + +def setup(hass, config): + """ Track states and offer events for rollershutters. """ + component = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS, + GROUP_NAME_ALL_ROLLERSHUTTERS) + component.setup(config) + + def handle_rollershutter_service(service): + """ Handles calls to the rollershutter services. """ + target_rollershutters = component.extract_from_service(service) + + for rollershutter in target_rollershutters: + if service.service == SERVICE_MOVE_UP: + rollershutter.move_up() + elif service.service == SERVICE_MOVE_DOWN: + rollershutter.move_down() + elif service.service == SERVICE_STOP: + rollershutter.stop() + + if rollershutter.should_poll: + rollershutter.update_ha_state(True) + + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'services.yaml')) + + hass.services.register(DOMAIN, SERVICE_MOVE_UP, + handle_rollershutter_service, + descriptions.get(SERVICE_MOVE_UP)) + hass.services.register(DOMAIN, SERVICE_MOVE_DOWN, + handle_rollershutter_service, + descriptions.get(SERVICE_MOVE_DOWN)) + hass.services.register(DOMAIN, SERVICE_STOP, + handle_rollershutter_service, + descriptions.get(SERVICE_STOP)) + + return True + + +class RollershutterDevice(Entity): + """ Represents a rollershutter within Home Assistant. """ + # pylint: disable=no-self-use + + @property + def current_position(self): + """ + Return current position of rollershutter. + None is unknown, 0 is closed, 100 is fully open. + """ + raise NotImplementedError() + + @property + def state(self): + """ Returns the state of the rollershutter. """ + current = self.current_position + + if current is None: + return STATE_UNKNOWN + + return STATE_CLOSED if current == 0 else STATE_OPEN + + @property + def state_attributes(self): + """ Return the state attributes. """ + current = self.current_position + + if current is None: + return None + + return { + ATTR_CURRENT_POSITION: current + } + + def move_up(self, **kwargs): + """ Move the rollershutter down. """ + raise NotImplementedError() + + def move_down(self, **kwargs): + """ Move the rollershutter up. """ + raise NotImplementedError() + + def stop(self, **kwargs): + """ Stop the rollershutter. """ + raise NotImplementedError() diff --git a/homeassistant/components/rollershutter/mqtt.py b/homeassistant/components/rollershutter/mqtt.py new file mode 100644 index 00000000000..f5eb5652516 --- /dev/null +++ b/homeassistant/components/rollershutter/mqtt.py @@ -0,0 +1,104 @@ +""" +homeassistant.components.rollershutter.mqtt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows to configure a MQTT rollershutter. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/rollershutter.mqtt/ +""" +import logging +import homeassistant.components.mqtt as mqtt +from homeassistant.components.rollershutter import RollershutterDevice +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['mqtt'] + +DEFAULT_NAME = "MQTT Rollershutter" +DEFAULT_QOS = 0 +DEFAULT_PAYLOAD_UP = "UP" +DEFAULT_PAYLOAD_DOWN = "DOWN" +DEFAULT_PAYLOAD_STOP = "STOP" + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Add MQTT Rollershutter """ + + if config.get('command_topic') is None: + _LOGGER.error("Missing required variable: command_topic") + return False + + add_devices_callback([MqttRollershutter( + hass, + config.get('name', DEFAULT_NAME), + config.get('state_topic'), + config.get('command_topic'), + config.get('qos', DEFAULT_QOS), + config.get('payload_up', DEFAULT_PAYLOAD_UP), + config.get('payload_down', DEFAULT_PAYLOAD_DOWN), + config.get('payload_stop', DEFAULT_PAYLOAD_STOP), + config.get('state_format'))]) + + +# pylint: disable=too-many-arguments, too-many-instance-attributes +class MqttRollershutter(RollershutterDevice): + """ Represents a rollershutter that can be controlled using MQTT. """ + def __init__(self, hass, name, state_topic, command_topic, qos, + payload_up, payload_down, payload_stop, state_format): + self._state = None + self._hass = hass + self._name = name + self._state_topic = state_topic + self._command_topic = command_topic + self._qos = qos + self._payload_up = payload_up + self._payload_down = payload_down + self._payload_stop = payload_stop + self._parse = mqtt.FmtParser(state_format) + + if self._state_topic is None: + return + + def message_received(topic, payload, qos): + """ A new MQTT message has been received. """ + value = self._parse(payload) + if value.isnumeric() and 0 <= int(value) <= 100: + self._state = int(value) + self.update_ha_state() + else: + _LOGGER.warning( + "Payload is expected to be an integer between 0 and 100") + + mqtt.subscribe(hass, self._state_topic, message_received, self._qos) + + @property + def should_poll(self): + """ No polling needed """ + return False + + @property + def name(self): + """ The name of the rollershutter. """ + return self._name + + @property + def current_position(self): + """ + Return current position of rollershutter. + None is unknown, 0 is closed, 100 is fully open. + """ + return self._state + + def move_up(self, **kwargs): + """ Move the rollershutter up. """ + mqtt.publish(self.hass, self._command_topic, self._payload_up, + self._qos) + + def move_down(self, **kwargs): + """ Move the rollershutter down. """ + mqtt.publish(self.hass, self._command_topic, self._payload_down, + self._qos) + + def stop(self, **kwargs): + """ Stop the device. """ + mqtt.publish(self.hass, self._command_topic, self._payload_stop, + self._qos) diff --git a/homeassistant/components/rollershutter/services.yaml b/homeassistant/components/rollershutter/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 2b18a5143fd..bf7c51fe3fb 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -9,7 +9,6 @@ https://home-assistant.io/components/script/ """ import logging from datetime import timedelta -import homeassistant.util.dt as date_util from itertools import islice import threading @@ -17,6 +16,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.util import slugify, split_entity_id +import homeassistant.util.dt as date_util from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_TIME_CHANGED, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF) @@ -73,11 +73,12 @@ def setup(hass, config): for object_id, cfg in config[DOMAIN].items(): if object_id != slugify(object_id): - _LOGGER.warn("Found invalid key for script: %s. Use %s instead.", - object_id, slugify(object_id)) + _LOGGER.warning("Found invalid key for script: %s. Use %s instead", + object_id, slugify(object_id)) continue - if not cfg.get(CONF_SEQUENCE): - _LOGGER.warn("Missing key 'sequence' for script %s", object_id) + if not isinstance(cfg.get(CONF_SEQUENCE), list): + _LOGGER.warning("Key 'sequence' for script %s should be a list", + object_id) continue alias = cfg.get(CONF_ALIAS, object_id) script = Script(hass, object_id, alias, cfg[CONF_SEQUENCE]) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 32ee59a6fa9..04770ced241 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -9,10 +9,9 @@ https://home-assistant.io/components/sensor/ import logging from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.components import wink, zwave, isy994, verisure +from homeassistant.components import wink, zwave, isy994, verisure, ecobee DOMAIN = 'sensor' -DEPENDENCIES = [] SCAN_INTERVAL = 30 ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -22,7 +21,8 @@ DISCOVERY_PLATFORMS = { wink.DISCOVER_SENSORS: 'wink', zwave.DISCOVER_SENSORS: 'zwave', isy994.DISCOVER_SENSORS: 'isy994', - verisure.DISCOVER_SENSORS: 'verisure' + verisure.DISCOVER_SENSORS: 'verisure', + ecobee.DISCOVER_SENSORS: 'ecobee' } diff --git a/homeassistant/components/sensor/arest.py b/homeassistant/components/sensor/arest.py index 332725102dd..f1faa7cc932 100644 --- a/homeassistant/components/sensor/arest.py +++ b/homeassistant/components/sensor/arest.py @@ -6,13 +6,14 @@ The arest sensor will consume an exposed aREST API of a device. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.arest/ """ -import logging -import requests from datetime import timedelta +import logging + +import requests -from homeassistant.util import Throttle -from homeassistant.helpers.entity import Entity from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py new file mode 100644 index 00000000000..02a2575d88b --- /dev/null +++ b/homeassistant/components/sensor/ecobee.py @@ -0,0 +1,109 @@ +""" +homeassistant.components.sensor.ecobee +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Ecobee Thermostat Component + +This component adds support for Ecobee3 Wireless Thermostats. +You will need to setup developer access to your thermostat, +and create and API key on the ecobee website. + +The first time you run this component you will see a configuration +component card in Home Assistant. This card will contain a PIN code +that you will need to use to authorize access to your thermostat. You +can do this at https://www.ecobee.com/consumerportal/index.html +Click My Apps, Add application, Enter Pin and click Authorize. + +After authorizing the application click the button in the configuration +card. Now your thermostat and sensors should shown in home-assistant. + +You can use the optional hold_temp parameter to set whether or not holds +are set indefintely or until the next scheduled event. + +ecobee: + api_key: asdfasdfasdfasdfasdfaasdfasdfasdfasdf + hold_temp: True + +""" +import logging + +from homeassistant.helpers.entity import Entity +from homeassistant.components import ecobee +from homeassistant.const import TEMP_FAHRENHEIT + +DEPENDENCIES = ['ecobee'] + +SENSOR_TYPES = { + 'temperature': ['Temperature', TEMP_FAHRENHEIT], + 'humidity': ['Humidity', '%'], + 'occupancy': ['Occupancy', ''] +} + +_LOGGER = logging.getLogger(__name__) + +ECOBEE_CONFIG_FILE = 'ecobee.conf' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the sensors. """ + if discovery_info is None: + return + data = ecobee.NETWORK + dev = list() + for index in range(len(data.ecobee.thermostats)): + for sensor in data.ecobee.get_remote_sensors(index): + for item in sensor['capability']: + if item['type'] not in ('temperature', + 'humidity', 'occupancy'): + continue + + dev.append(EcobeeSensor(sensor['name'], item['type'], index)) + + add_devices(dev) + + +class EcobeeSensor(Entity): + """ An ecobee sensor. """ + + def __init__(self, sensor_name, sensor_type, sensor_index): + self._name = sensor_name + ' ' + SENSOR_TYPES[sensor_type][0] + self.sensor_name = sensor_name + self.type = sensor_type + self.index = sensor_index + self._state = None + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self.update() + + @property + def name(self): + return self._name.rstrip() + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + @property + def unit_of_measurement(self): + return self._unit_of_measurement + + def update(self): + data = ecobee.NETWORK + data.update() + for sensor in data.ecobee.get_remote_sensors(self.index): + for item in sensor['capability']: + if ( + item['type'] == self.type and + self.type == 'temperature' and + self.sensor_name == sensor['name']): + self._state = float(item['value']) / 10 + elif ( + item['type'] == self.type and + self.type == 'humidity' and + self.sensor_name == sensor['name']): + self._state = item['value'] + elif ( + item['type'] == self.type and + self.type == 'occupancy' and + self.sensor_name == sensor['name']): + self._state = item['value'] diff --git a/homeassistant/components/sensor/efergy.py b/homeassistant/components/sensor/efergy.py index 99140b5b2a9..447903a714e 100644 --- a/homeassistant/components/sensor/efergy.py +++ b/homeassistant/components/sensor/efergy.py @@ -97,5 +97,5 @@ class EfergySensor(Entity): self._state = response.json()['sum'] else: self._state = 'Unknown' - except RequestException: + except (RequestException, ValueError): _LOGGER.warning('Could not update status for %s', self.name) diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py index aa3dd3d3d04..c024c19b5f7 100644 --- a/homeassistant/components/sensor/forecast.py +++ b/homeassistant/components/sensor/forecast.py @@ -9,17 +9,11 @@ https://home-assistant.io/components/sensor.forecast/ import logging from datetime import timedelta -REQUIREMENTS = ['python-forecastio==1.3.3'] - -try: - import forecastio -except ImportError: - forecastio = None - from homeassistant.util import Throttle from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS) from homeassistant.helpers.entity import Entity +REQUIREMENTS = ['python-forecastio==1.3.3'] _LOGGER = logging.getLogger(__name__) # Sensor types are defined like so: @@ -53,11 +47,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) def setup_platform(hass, config, add_devices, discovery_info=None): """ Get the Forecast.io sensor. """ - - global forecastio # pylint: disable=invalid-name - if forecastio is None: - import forecastio as forecastio_ - forecastio = forecastio_ + import forecastio if None in (hass.config.latitude, hass.config.longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") @@ -141,6 +131,7 @@ class ForeCastSensor(Entity): # pylint: disable=too-many-branches def update(self): """ Gets the latest data from Forecast.io and updates the states. """ + import forecastio self.forecast_client.update() data = self.forecast_client.data @@ -209,6 +200,7 @@ class ForeCastData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """ Gets the latest data from Forecast.io. """ + import forecastio forecast = forecastio.load_forecast(self._api_key, self.latitude, diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index 092e4c75733..7938ae7e659 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -6,9 +6,10 @@ Gathers system information of hosts which running glances. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.glances/ """ -import logging -import requests from datetime import timedelta +import logging + +import requests from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index 2623d2fdcce..2bbed97e40c 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -31,23 +31,26 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): config.get('name', DEFAULT_NAME), config.get('state_topic'), config.get('qos', DEFAULT_QOS), - config.get('unit_of_measurement'))]) + config.get('unit_of_measurement'), + config.get('state_format'))]) # pylint: disable=too-many-arguments, too-many-instance-attributes class MqttSensor(Entity): """ Represents a sensor that can be updated using MQTT. """ - def __init__(self, hass, name, state_topic, qos, unit_of_measurement): + def __init__(self, hass, name, state_topic, qos, unit_of_measurement, + state_format): self._state = "-" self._hass = hass self._name = name self._state_topic = state_topic self._qos = qos self._unit_of_measurement = unit_of_measurement + self._parse = mqtt.FmtParser(state_format) def message_received(topic, payload, qos): """ A new MQTT message has been received. """ - self._state = payload + self._state = self._parse(payload) self.update_ha_state() mqtt.subscribe(hass, self._state_topic, message_received, self._qos) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 7fe3a583b08..53609dbb237 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -6,10 +6,11 @@ The rest sensor will consume JSON responses sent by an exposed REST API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.rest/ """ -import logging -import requests -from json import loads from datetime import timedelta +from json import loads +import logging + +import requests from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/sensor/rpi_gpio.py b/homeassistant/components/sensor/rpi_gpio.py index 2e2746fe9d4..ef7ea8c33c1 100644 --- a/homeassistant/components/sensor/rpi_gpio.py +++ b/homeassistant/components/sensor/rpi_gpio.py @@ -6,13 +6,10 @@ Allows to configure a binary state sensor using RPi GPIO. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.rpi_gpio/ """ +# pylint: disable=import-error import logging from homeassistant.helpers.entity import Entity -try: - import RPi.GPIO as GPIO -except ImportError: - GPIO = None from homeassistant.const import (DEVICE_DEFAULT_NAME, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) @@ -29,10 +26,7 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Raspberry PI GPIO ports. """ - if GPIO is None: - _LOGGER.error('RPi.GPIO not available. rpi_gpio ports ignored.') - return - # pylint: disable=no-member + import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) sensors = [] @@ -65,6 +59,7 @@ class RPiGPIOSensor(Entity): def __init__(self, port_name, port_num, pull_mode, value_high, value_low, bouncetime): # pylint: disable=no-member + import RPi.GPIO as GPIO self._name = port_name or DEVICE_DEFAULT_NAME self._port = port_num self._pull = GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index e478daac2f9..98d76a302dd 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -6,12 +6,11 @@ Monitors SABnzbd NZB client API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.sabnzbd/ """ -from homeassistant.util import Throttle from datetime import timedelta +import logging from homeassistant.helpers.entity import Entity - -import logging +from homeassistant.util import Throttle REQUIREMENTS = ['https://github.com/jamespcole/home-assistant-nzb-clients/' 'archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip' diff --git a/homeassistant/components/sensor/transmission.py b/homeassistant/components/sensor/transmission.py index 484b045f295..62afdd39bf4 100644 --- a/homeassistant/components/sensor/transmission.py +++ b/homeassistant/components/sensor/transmission.py @@ -6,14 +6,13 @@ Monitors Transmission BitTorrent client API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.transmission/ """ -from homeassistant.util import Throttle from datetime import timedelta -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD - -from homeassistant.helpers.entity import Entity - import logging +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.util import Throttle +from homeassistant.helpers.entity import Entity + REQUIREMENTS = ['transmissionrpc==0.11'] SENSOR_TYPES = { 'current_status': ['Status', ''], diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 1848c680517..7fb72fd91b7 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -95,7 +95,7 @@ class VeraSensor(Entity): @property def state_attributes(self): - attr = super().state_attributes + attr = {} if self.vera_device.has_battery: attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 8bfdb9205fa..26fe6538e05 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -12,8 +12,8 @@ from homeassistant.helpers.entity import Entity from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' - '9eb39eaba0717922815e673ad1114c685839d890.zip' - '#python-wink==0.1.1'] + '42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip' + '#python-wink==0.2'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index 23d2f8948f8..1ed831b286d 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -6,9 +6,11 @@ Interfaces with Z-Wave sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/zwave/ """ +# Because we do not compile openzwave on CI # pylint: disable=import-error -from homeassistant.helpers.event import track_point_in_time import datetime + +from homeassistant.helpers.event import track_point_in_time import homeassistant.util.dt as dt_util import homeassistant.components.zwave as zwave from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/shell_command.py b/homeassistant/components/shell_command.py index 61c9add3f23..5e12c8bfd6e 100644 --- a/homeassistant/components/shell_command.py +++ b/homeassistant/components/shell_command.py @@ -12,7 +12,6 @@ import subprocess from homeassistant.util import slugify DOMAIN = 'shell_command' -DEPENDENCIES = [] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index a5b83e76929..2e1c0c9b377 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -12,13 +12,14 @@ import urllib import homeassistant.util as util import homeassistant.util.dt as dt_util -from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.event import ( + track_point_in_utc_time, track_utc_time_change) from homeassistant.helpers.entity import Entity -DEPENDENCIES = [] REQUIREMENTS = ['astral==0.8.1'] DOMAIN = "sun" ENTITY_ID = "sun.sun" +ENTITY_ID_ELEVATION = "sun.elevation" CONF_ELEVATION = 'elevation' @@ -27,6 +28,7 @@ STATE_BELOW_HORIZON = "below_horizon" STATE_ATTR_NEXT_RISING = "next_rising" STATE_ATTR_NEXT_SETTING = "next_setting" +STATE_ATTR_ELEVATION = "elevation" _LOGGER = logging.getLogger(__name__) @@ -140,11 +142,7 @@ class Sun(Entity): self.hass = hass self.location = location self._state = self.next_rising = self.next_setting = None - - @property - def should_poll(self): - """ We trigger updates ourselves after sunset/sunrise """ - return False + track_utc_time_change(hass, self.timer_update, second=30) @property def name(self): @@ -160,8 +158,11 @@ class Sun(Entity): @property def state_attributes(self): return { - STATE_ATTR_NEXT_RISING: dt_util.datetime_to_str(self.next_rising), - STATE_ATTR_NEXT_SETTING: dt_util.datetime_to_str(self.next_setting) + STATE_ATTR_NEXT_RISING: + dt_util.datetime_to_str(self.next_rising), + STATE_ATTR_NEXT_SETTING: + dt_util.datetime_to_str(self.next_setting), + STATE_ATTR_ELEVATION: round(self.solar_elevation, 2) } @property @@ -169,6 +170,15 @@ class Sun(Entity): """ Returns the datetime when the next change to the state is. """ return min(self.next_rising, self.next_setting) + @property + def solar_elevation(self): + """ Returns the angle the sun is above the horizon""" + from astral import Astral + return Astral().solar_elevation( + dt_util.utcnow(), + self.location.latitude, + self.location.longitude) + def update_as_of(self, utc_point_in_time): """ Calculate sun state at a point in UTC time. """ mod = -1 @@ -199,3 +209,7 @@ class Sun(Entity): track_point_in_utc_time( self.hass, self.point_in_time_listener, self.next_change + timedelta(seconds=1)) + + def timer_update(self, time): + """ Needed to update solar elevation. """ + self.update_ha_state() diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 9a0abb4ce7a..e7b3c629f39 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -20,7 +20,6 @@ from homeassistant.components import ( group, discovery, wink, isy994, verisure, zwave) DOMAIN = 'switch' -DEPENDENCIES = [] SCAN_INTERVAL = 30 GROUP_NAME_ALL_SWITCHES = 'all switches' diff --git a/homeassistant/components/switch/arest.py b/homeassistant/components/switch/arest.py index c7bf1b34e4a..c42295660d4 100644 --- a/homeassistant/components/switch/arest.py +++ b/homeassistant/components/switch/arest.py @@ -34,30 +34,33 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False dev = [] - pins = config.get('pins') + pins = config.get('pins', {}) for pinnum, pin in pins.items(): - dev.append(ArestSwitch(resource, - config.get('name', response.json()['name']), - pin.get('name'), - pinnum)) + dev.append(ArestSwitchPin(resource, + config.get('name', response.json()['name']), + pin.get('name'), + pinnum)) + + functions = config.get('functions', {}) + for funcname, func in functions.items(): + dev.append(ArestSwitchFunction(resource, + config.get('name', + response.json()['name']), + func.get('name'), + funcname)) + add_devices(dev) -class ArestSwitch(SwitchDevice): +class ArestSwitchBase(SwitchDevice): """ Implements an aREST switch. """ - def __init__(self, resource, location, name, pin): + def __init__(self, resource, location, name): self._resource = resource self._name = '{} {}'.format(location.title(), name.title()) \ or DEVICE_DEFAULT_NAME - self._pin = pin self._state = None - request = requests.get('{}/mode/{}/o'.format(self._resource, - self._pin), timeout=10) - if request.status_code is not 200: - _LOGGER.error("Can't set mode. Is device offline?") - @property def name(self): """ The name of the switch. """ @@ -68,6 +71,72 @@ class ArestSwitch(SwitchDevice): """ True if device is on. """ return self._state + +class ArestSwitchFunction(ArestSwitchBase): + """ Implements an aREST switch. Based on functions. """ + + def __init__(self, resource, location, name, func): + super().__init__(resource, location, name) + self._func = func + + request = requests.get('{}/{}'.format(self._resource, self._func), + timeout=10) + + if request.status_code is not 200: + _LOGGER.error("Can't find function. Is device offline?") + return + + try: + request.json()['return_value'] + except KeyError: + _LOGGER.error("No return_value received. " + "Is the function name correct.") + except ValueError: + _LOGGER.error("Response invalid. Is the function name correct.") + + def turn_on(self, **kwargs): + """ Turn the device on. """ + request = requests.get('{}/{}'.format(self._resource, self._func), + timeout=10, params={"params": "1"}) + + if request.status_code == 200: + self._state = True + else: + _LOGGER.error("Can't turn on function %s at %s. " + "Is device offline?", + self._func, self._resource) + + def turn_off(self, **kwargs): + """ Turn the device off. """ + request = requests.get('{}/{}'.format(self._resource, self._func), + timeout=10, params={"params": "0"}) + + if request.status_code == 200: + self._state = False + else: + _LOGGER.error("Can't turn off function %s at %s. " + "Is device offline?", + self._func, self._resource) + + def update(self): + """ Gets the latest data from aREST API and updates the state. """ + request = requests.get('{}/{}'.format(self._resource, + self._func), timeout=10) + self._state = request.json()['return_value'] != 0 + + +class ArestSwitchPin(ArestSwitchBase): + """ Implements an aREST switch. Based on digital I/O """ + + def __init__(self, resource, location, name, pin): + super().__init__(resource, location, name) + self._pin = pin + + request = requests.get('{}/mode/{}/o'.format(self._resource, + self._pin), timeout=10) + if request.status_code is not 200: + _LOGGER.error("Can't set mode. Is device offline?") + def turn_on(self, **kwargs): """ Turn the device on. """ request = requests.get('{}/digital/{}/1'.format(self._resource, @@ -76,7 +145,7 @@ class ArestSwitch(SwitchDevice): self._state = True else: _LOGGER.error("Can't turn on pin %s at %s. Is device offline?", - self._resource, self._pin) + self._pin, self._resource) def turn_off(self, **kwargs): """ Turn the device off. """ @@ -86,7 +155,7 @@ class ArestSwitch(SwitchDevice): self._state = False else: _LOGGER.error("Can't turn off pin %s at %s. Is device offline?", - self._resource, self._pin) + self._pin, self._resource) def update(self): """ Gets the latest data from aREST API and updates the state. """ diff --git a/homeassistant/components/switch/hikvisioncam.py b/homeassistant/components/switch/hikvisioncam.py index 2d91acdf361..c85aae4a7f0 100644 --- a/homeassistant/components/switch/hikvisioncam.py +++ b/homeassistant/components/switch/hikvisioncam.py @@ -6,11 +6,12 @@ Support turning on/off motion detection on Hikvision cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.hikvision/ """ -from homeassistant.helpers.entity import ToggleEntity -from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD import logging +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.const import (STATE_ON, STATE_OFF, + CONF_HOST, CONF_USERNAME, CONF_PASSWORD) + _LOGGING = logging.getLogger(__name__) REQUIREMENTS = ['hikvision==0.4'] # pylint: disable=too-many-arguments diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 12d3f486323..7b973799eed 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -17,6 +17,7 @@ DEFAULT_QOS = 0 DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_OPTIMISTIC = False +DEFAULT_RETAIN = False DEPENDENCIES = ['mqtt'] @@ -35,28 +36,33 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): config.get('state_topic'), config.get('command_topic'), config.get('qos', DEFAULT_QOS), + config.get('retain', DEFAULT_RETAIN), config.get('payload_on', DEFAULT_PAYLOAD_ON), config.get('payload_off', DEFAULT_PAYLOAD_OFF), - config.get('optimistic', DEFAULT_OPTIMISTIC))]) + config.get('optimistic', DEFAULT_OPTIMISTIC), + config.get('state_format'))]) # pylint: disable=too-many-arguments, too-many-instance-attributes class MqttSwitch(SwitchDevice): - """ Represents a switch that can be togggled using MQTT. """ - def __init__(self, hass, name, state_topic, command_topic, qos, - payload_on, payload_off, optimistic): + """ Represents a switch that can be toggled using MQTT. """ + def __init__(self, hass, name, state_topic, command_topic, qos, retain, + payload_on, payload_off, optimistic, state_format): self._state = False self._hass = hass self._name = name self._state_topic = state_topic self._command_topic = command_topic self._qos = qos + self._retain = retain self._payload_on = payload_on self._payload_off = payload_off self._optimistic = optimistic + self._parse = mqtt.FmtParser(state_format) def message_received(topic, payload, qos): """ A new MQTT message has been received. """ + payload = self._parse(payload) if payload == self._payload_on: self._state = True self.update_ha_state() @@ -90,7 +96,7 @@ class MqttSwitch(SwitchDevice): def turn_on(self, **kwargs): """ Turn the device on. """ mqtt.publish(self.hass, self._command_topic, self._payload_on, - self._qos) + self._qos, self._retain) if self._optimistic: # optimistically assume that switch has changed state self._state = True @@ -99,7 +105,7 @@ class MqttSwitch(SwitchDevice): def turn_off(self, **kwargs): """ Turn the device off. """ mqtt.publish(self.hass, self._command_topic, self._payload_off, - self._qos) + self._qos, self._retain) if self._optimistic: # optimistically assume that switch has changed state self._state = False diff --git a/homeassistant/components/switch/mystrom.py b/homeassistant/components/switch/mystrom.py new file mode 100644 index 00000000000..919ff28e4ef --- /dev/null +++ b/homeassistant/components/switch/mystrom.py @@ -0,0 +1,99 @@ +""" +homeassistant.components.switch.mystrom +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for myStrom switches. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/switch.mystrom/ +""" +import logging +import requests + +from homeassistant.components.switch import SwitchDevice + +DEFAULT_NAME = 'myStrom Switch' + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Find and return myStrom switches. """ + host = config.get('host') + + if host is None: + _LOGGER.error('Missing required variable: host') + return False + + resource = 'http://{}'.format(host) + + try: + requests.get(resource, timeout=10) + except requests.exceptions.ConnectionError: + _LOGGER.error("No route to device %s. " + "Please check the IP address in the configuration file", + host) + return False + + add_devices([MyStromSwitch( + config.get('name', DEFAULT_NAME), + resource)]) + + +class MyStromSwitch(SwitchDevice): + """ Represents a myStrom switch. """ + def __init__(self, name, resource): + self._state = False + self._name = name + self._resource = resource + self.consumption = 0 + + @property + def name(self): + """ The name of the switch. """ + return self._name + + @property + def is_on(self): + """ True if switch is on. """ + return self._state + + @property + def current_power_mwh(self): + """ Current power consumption in mwh. """ + return self.consumption + + def turn_on(self, **kwargs): + """ Turn the switch on. """ + try: + request = requests.get('{}/relay'.format(self._resource), + params={'state': '1'}, + timeout=10) + if request.status_code == 200: + self._state = True + except requests.exceptions.ConnectionError: + _LOGGER.error("Can't turn on %s. Is device offline?", + self._resource) + + def turn_off(self, **kwargs): + """ Turn the switch off. """ + try: + request = requests.get('{}/relay'.format(self._resource), + params={'state': '0'}, + timeout=10) + if request.status_code == 200: + self._state = False + except requests.exceptions.ConnectionError: + _LOGGER.error("Can't turn on %s. Is device offline?", + self._resource) + + def update(self): + """ Gets the latest data from REST API and updates the state. """ + try: + request = requests.get('{}/report'.format(self._resource), + timeout=10) + data = request.json() + self._state = bool(data['relay']) + self.consumption = data['power'] + except requests.exceptions.ConnectionError: + _LOGGER.error("No route to device '%s'. Is device offline?", + self._resource) diff --git a/homeassistant/components/switch/orvibo.py b/homeassistant/components/switch/orvibo.py index b9469d15df0..c636a7f3f55 100644 --- a/homeassistant/components/switch/orvibo.py +++ b/homeassistant/components/switch/orvibo.py @@ -11,7 +11,7 @@ import logging from homeassistant.components.switch import SwitchDevice DEFAULT_NAME = "Orvibo S20 Switch" -REQUIREMENTS = ['orvibo==1.0.0'] +REQUIREMENTS = ['orvibo==1.0.1'] _LOGGER = logging.getLogger(__name__) @@ -20,15 +20,23 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return S20 switches. """ from orvibo.s20 import S20, S20Exception - if config.get('host') is None: - _LOGGER.error("Missing required variable: host") - return - try: - s20 = S20(config.get('host')) - add_devices_callback([S20Switch(config.get('name', DEFAULT_NAME), - s20)]) - except S20Exception: - _LOGGER.exception("S20 couldn't be initialized") + switches = [] + switch_conf = config.get('switches', [config]) + + for switch in switch_conf: + if switch.get('host') is None: + _LOGGER.error("Missing required variable: host") + continue + host = switch.get('host') + try: + switches.append(S20Switch(switch.get('name', DEFAULT_NAME), + S20(host))) + _LOGGER.info("Initialized S20 at %s", host) + except S20Exception: + _LOGGER.exception("S20 at %s couldn't be initialized", + host) + + add_devices_callback(switches) class S20Switch(SwitchDevice): diff --git a/homeassistant/components/switch/rfxtrx.py b/homeassistant/components/switch/rfxtrx.py index 86bcf580f41..69e08e7d129 100644 --- a/homeassistant/components/switch/rfxtrx.py +++ b/homeassistant/components/switch/rfxtrx.py @@ -12,6 +12,11 @@ import homeassistant.components.rfxtrx as rfxtrx from homeassistant.components.switch import SwitchDevice from homeassistant.util import slugify +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.components.rfxtrx import ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID, \ + ATTR_NAME, EVENT_BUTTON_PRESSED + + DEPENDENCIES = ['rfxtrx'] _LOGGER = logging.getLogger(__name__) @@ -19,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Setup the RFXtrx platform. """ - from RFXtrx import LightingDevice + import RFXtrx as rfxtrxmod # Add switch from config file switchs = [] @@ -27,9 +32,15 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): if devices: for entity_id, entity_info in devices.items(): if entity_id not in rfxtrx.RFX_DEVICES: - _LOGGER.info("Add %s rfxtrx.switch", entity_info['name']) - rfxobject = rfxtrx.get_rfx_object(entity_info['packetid']) - newswitch = RfxtrxSwitch(entity_info['name'], rfxobject, False) + _LOGGER.info("Add %s rfxtrx.switch", entity_info[ATTR_NAME]) + + # Check if i must fire event + fire_event = entity_info.get(ATTR_FIREEVENT, False) + datas = {ATTR_STATE: False, ATTR_FIREEVENT: fire_event} + + rfxobject = rfxtrx.get_rfx_object(entity_info[ATTR_PACKETID]) + newswitch = RfxtrxSwitch( + entity_info[ATTR_NAME], rfxobject, datas) rfxtrx.RFX_DEVICES[entity_id] = newswitch switchs.append(newswitch) @@ -37,7 +48,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): def switch_update(event): """ Callback for sensor updates from the RFXtrx gateway. """ - if isinstance(event.device, LightingDevice): + if not isinstance(event.device, rfxtrxmod.LightingDevice): return # Add entity if not exist and the automatic_add is True @@ -55,12 +66,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): ) pkt_id = "".join("{0:02x}".format(x) for x in event.data) entity_name = "%s : %s" % (entity_id, pkt_id) - new_switch = RfxtrxSwitch(entity_name, event, False) + datas = {ATTR_STATE: False, ATTR_FIREEVENT: False} + new_switch = RfxtrxSwitch(entity_name, event, datas) rfxtrx.RFX_DEVICES[entity_id] = new_switch add_devices_callback([new_switch]) # Check if entity exists or previously added automatically - if entity_id in rfxtrx.RFX_DEVICES: + if entity_id in rfxtrx.RFX_DEVICES \ + and isinstance(rfxtrx.RFX_DEVICES[entity_id], RfxtrxSwitch): _LOGGER.debug( "EntityID: %s switch_update. Command: %s", entity_id, @@ -68,10 +81,22 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): ) if event.values['Command'] == 'On'\ or event.values['Command'] == 'Off': - if event.values['Command'] == 'On': - rfxtrx.RFX_DEVICES[entity_id].turn_on() - else: - rfxtrx.RFX_DEVICES[entity_id].turn_off() + + # Update the rfxtrx device state + is_on = event.values['Command'] == 'On' + # pylint: disable=protected-access + rfxtrx.RFX_DEVICES[entity_id]._state = is_on + rfxtrx.RFX_DEVICES[entity_id].update_ha_state() + + # Fire event + if rfxtrx.RFX_DEVICES[entity_id].should_fire_event: + rfxtrx.RFX_DEVICES[entity_id].hass.bus.fire( + EVENT_BUTTON_PRESSED, { + ATTR_ENTITY_ID: + rfxtrx.RFX_DEVICES[entity_id].entity_id, + ATTR_STATE: event.values['Command'].lower() + } + ) # Subscribe to main rfxtrx events if switch_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: @@ -80,10 +105,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class RfxtrxSwitch(SwitchDevice): """ Provides a RFXtrx switch. """ - def __init__(self, name, event, state): + def __init__(self, name, event, datas): self._name = name self._event = event - self._state = state + self._state = datas[ATTR_STATE] + self._should_fire_event = datas[ATTR_FIREEVENT] @property def should_poll(self): @@ -95,9 +121,14 @@ class RfxtrxSwitch(SwitchDevice): """ Returns the name of the device if any. """ return self._name + @property + def should_fire_event(self): + """ Returns is the device must fire event""" + return self._should_fire_event + @property def is_on(self): - """ True if device is on. """ + """ True if light is on. """ return self._state def turn_on(self, **kwargs): diff --git a/homeassistant/components/switch/transmission.py b/homeassistant/components/switch/transmission.py index f3f6a9a8765..1f0da4a00e0 100644 --- a/homeassistant/components/switch/transmission.py +++ b/homeassistant/components/switch/transmission.py @@ -6,12 +6,12 @@ Enable or disable Transmission BitTorrent client Turtle Mode. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.transmission/ """ -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD -from homeassistant.const import STATE_ON, STATE_OFF - -from homeassistant.helpers.entity import ToggleEntity import logging +from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD, + STATE_ON, STATE_OFF) +from homeassistant.helpers.entity import ToggleEntity + _LOGGING = logging.getLogger(__name__) REQUIREMENTS = ['transmissionrpc==0.11'] diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 80724f92757..14983919c64 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -126,5 +126,8 @@ class VeraSwitch(ToggleEntity): def update(self): # We need to debounce the status call after turning switch on or off # because the vera has some lag in updating the device status - if (self.last_command_send + 5) < time.time(): - self.is_on_status = self.vera_device.is_switched_on() + try: + if (self.last_command_send + 5) < time.time(): + self.is_on_status = self.vera_device.is_switched_on() + except RequestException: + _LOGGER.warning('Could not update status for %s', self.name) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 1d701bf88cc..bad471ce437 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -11,7 +11,7 @@ import logging from homeassistant.components.switch import SwitchDevice from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY -REQUIREMENTS = ['pywemo==0.3.2'] +REQUIREMENTS = ['pywemo==0.3.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index b022d8cbf72..f0dc18003c6 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -12,8 +12,8 @@ from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' - '9eb39eaba0717922815e673ad1114c685839d890.zip' - '#python-wink==0.1.1'] + '42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip' + '#python-wink==0.2'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/switch/zwave.py b/homeassistant/components/switch/zwave.py index 493e2234bcf..f4777340445 100644 --- a/homeassistant/components/switch/zwave.py +++ b/homeassistant/components/switch/zwave.py @@ -4,8 +4,8 @@ homeassistant.components.switch.zwave Zwave platform that handles simple binary switches. """ +# Because we do not compile openzwave on CI # pylint: disable=import-error - import homeassistant.components.zwave as zwave from homeassistant.components.switch import SwitchDevice diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index 480e3e4805e..edfa22a7840 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -15,12 +15,12 @@ from homeassistant.config import load_yaml_config_file import homeassistant.util as util from homeassistant.helpers.entity import Entity from homeassistant.helpers.temperature import convert +from homeassistant.components import ecobee from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELCIUS) DOMAIN = "thermostat" -DEPENDENCIES = [] ENTITY_ID_FORMAT = DOMAIN + ".{}" SCAN_INTERVAL = 60 @@ -42,6 +42,10 @@ ATTR_OPERATION = "current_operation" _LOGGER = logging.getLogger(__name__) +DISCOVERY_PLATFORMS = { + ecobee.DISCOVER_THERMOSTAT: 'ecobee', +} + def set_away_mode(hass, away_mode, entity_id=None): """ Turn all or specified thermostat away mode on. """ @@ -67,7 +71,8 @@ def set_temperature(hass, temperature, entity_id=None): def setup(hass, config): """ Setup thermostats. """ - component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) + component = EntityComponent(_LOGGER, DOMAIN, hass, + SCAN_INTERVAL, DISCOVERY_PLATFORMS) component.setup(config) def thermostat_service(service): @@ -142,13 +147,13 @@ class ThermostatDevice(Entity): data = { ATTR_CURRENT_TEMPERATURE: self._convert(self.current_temperature, 1), - ATTR_MIN_TEMP: self._convert(self.min_temp, 0), - ATTR_MAX_TEMP: self._convert(self.max_temp, 0), - ATTR_TEMPERATURE: self._convert(self.target_temperature, 0), + ATTR_MIN_TEMP: self._convert(self.min_temp, 1), + ATTR_MAX_TEMP: self._convert(self.max_temp, 1), + ATTR_TEMPERATURE: self._convert(self.target_temperature, 1), ATTR_TEMPERATURE_LOW: - self._convert(self.target_temperature_low, 0), + self._convert(self.target_temperature_low, 1), ATTR_TEMPERATURE_HIGH: - self._convert(self.target_temperature_high, 0), + self._convert(self.target_temperature_high, 1), } operation = self.operation diff --git a/homeassistant/components/thermostat/ecobee.py b/homeassistant/components/thermostat/ecobee.py new file mode 100644 index 00000000000..a10d2940001 --- /dev/null +++ b/homeassistant/components/thermostat/ecobee.py @@ -0,0 +1,213 @@ +""" +homeassistant.components.thermostat.ecobee +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Ecobee Thermostat Component + +This component adds support for Ecobee3 Wireless Thermostats. +You will need to setup developer access to your thermostat, +and create and API key on the ecobee website. + +The first time you run this component you will see a configuration +component card in Home Assistant. This card will contain a PIN code +that you will need to use to authorize access to your thermostat. You +can do this at https://www.ecobee.com/consumerportal/index.html +Click My Apps, Add application, Enter Pin and click Authorize. + +After authorizing the application click the button in the configuration +card. Now your thermostat and sensors should shown in home-assistant. + +You can use the optional hold_temp parameter to set whether or not holds +are set indefintely or until the next scheduled event. + +ecobee: + api_key: asdfasdfasdfasdfasdfaasdfasdfasdfasdf + hold_temp: True + +""" +import logging + +from homeassistant.components import ecobee +from homeassistant.components.thermostat import (ThermostatDevice, STATE_COOL, + STATE_IDLE, STATE_HEAT) +from homeassistant.const import (TEMP_FAHRENHEIT, STATE_ON, STATE_OFF) + +DEPENDENCIES = ['ecobee'] + +_LOGGER = logging.getLogger(__name__) + +ECOBEE_CONFIG_FILE = 'ecobee.conf' +_CONFIGURING = {} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Setup Platform """ + if discovery_info is None: + return + data = ecobee.NETWORK + hold_temp = discovery_info['hold_temp'] + _LOGGER.info("Loading ecobee thermostat component with hold_temp set to " + + str(hold_temp)) + add_devices(Thermostat(data, index, hold_temp) + for index in range(len(data.ecobee.thermostats))) + + +class Thermostat(ThermostatDevice): + """ Thermostat class for Ecobee """ + + def __init__(self, data, thermostat_index, hold_temp): + self.data = data + self.thermostat_index = thermostat_index + self.thermostat = self.data.ecobee.get_thermostat( + self.thermostat_index) + self._name = self.thermostat['name'] + self._away = 'away' in self.thermostat['program']['currentClimateRef'] + self.hold_temp = hold_temp + + def update(self): + self.data.update() + self.thermostat = self.data.ecobee.get_thermostat( + self.thermostat_index) + + @property + def name(self): + """ Returns the name of the Ecobee Thermostat. """ + return self.thermostat['name'] + + @property + def unit_of_measurement(self): + """ Unit of measurement this thermostat expresses itself in. """ + return TEMP_FAHRENHEIT + + @property + def current_temperature(self): + """ Returns the current temperature. """ + return self.thermostat['runtime']['actualTemperature'] / 10 + + @property + def target_temperature(self): + """ Returns the temperature we try to reach. """ + return (self.target_temperature_low + self.target_temperature_high) / 2 + + @property + def target_temperature_low(self): + """ Returns the lower bound temperature we try to reach. """ + return int(self.thermostat['runtime']['desiredHeat'] / 10) + + @property + def target_temperature_high(self): + """ Returns the upper bound temperature we try to reach. """ + return int(self.thermostat['runtime']['desiredCool'] / 10) + + @property + def humidity(self): + """ Returns the current humidity. """ + return self.thermostat['runtime']['actualHumidity'] + + @property + def desired_fan_mode(self): + """ Returns the desired fan mode of operation. """ + return self.thermostat['runtime']['desiredFanMode'] + + @property + def fan(self): + """ Returns the current fan state. """ + if 'fan' in self.thermostat['equipmentStatus']: + return STATE_ON + else: + return STATE_OFF + + @property + def operation(self): + """ Returns current operation ie. heat, cool, idle """ + status = self.thermostat['equipmentStatus'] + if status == '': + return STATE_IDLE + elif 'Cool' in status: + return STATE_COOL + elif 'auxHeat' in status: + return STATE_HEAT + elif 'heatPump' in status: + return STATE_HEAT + else: + return status + + @property + def mode(self): + """ Returns current mode ie. home, away, sleep """ + mode = self.thermostat['program']['currentClimateRef'] + self._away = 'away' in mode + return mode + + @property + def hvac_mode(self): + """ Return current hvac mode ie. auto, auxHeatOnly, cool, heat, off """ + return self.thermostat['settings']['hvacMode'] + + @property + def device_state_attributes(self): + """ Returns device specific state attributes. """ + # Move these to Thermostat Device and make them global + return { + "humidity": self.humidity, + "fan": self.fan, + "mode": self.mode, + "hvac_mode": self.hvac_mode + } + + @property + def is_away_mode_on(self): + """ Returns if away mode is on. """ + return self._away + + def turn_away_mode_on(self): + """ Turns away on. """ + self._away = True + if self.hold_temp: + self.data.ecobee.set_climate_hold(self.thermostat_index, + "away", "indefinite") + else: + self.data.ecobee.set_climate_hold(self.thermostat_index, "away") + + def turn_away_mode_off(self): + """ Turns away off. """ + self._away = False + self.data.ecobee.resume_program(self.thermostat_index) + + def set_temperature(self, temperature): + """ Set new target temperature """ + temperature = int(temperature) + low_temp = temperature - 1 + high_temp = temperature + 1 + if self.hold_temp: + self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp, + high_temp, "indefinite") + else: + self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp, + high_temp) + + def set_hvac_mode(self, mode): + """ Set HVAC mode (auto, auxHeatOnly, cool, heat, off) """ + self.data.ecobee.set_hvac_mode(self.thermostat_index, mode) + + # Home and Sleep mode aren't used in UI yet: + + # def turn_home_mode_on(self): + # """ Turns home mode on. """ + # self._away = False + # self.data.ecobee.set_climate_hold(self.thermostat_index, "home") + + # def turn_home_mode_off(self): + # """ Turns home mode off. """ + # self._away = False + # self.data.ecobee.resume_program(self.thermostat_index) + + # def turn_sleep_mode_on(self): + # """ Turns sleep mode on. """ + # self._away = False + # self.data.ecobee.set_climate_hold(self.thermostat_index, "sleep") + + # def turn_sleep_mode_off(self): + # """ Turns sleep mode off. """ + # self._away = False + # self.data.ecobee.resume_program(self.thermostat_index) diff --git a/homeassistant/components/thermostat/heat_control.py b/homeassistant/components/thermostat/heat_control.py index 7080528fd17..7e560a2276f 100644 --- a/homeassistant/components/thermostat/heat_control.py +++ b/homeassistant/components/thermostat/heat_control.py @@ -24,6 +24,9 @@ CONF_NAME = 'name' DEFAULT_NAME = 'Heat Control' CONF_HEATER = 'heater' CONF_SENSOR = 'target_sensor' +CONF_MIN_TEMP = 'min_temp' +CONF_MAX_TEMP = 'max_temp' +CONF_TARGET_TEMP = 'target_temp' _LOGGER = logging.getLogger(__name__) @@ -34,27 +37,34 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME, DEFAULT_NAME) heater_entity_id = config.get(CONF_HEATER) sensor_entity_id = config.get(CONF_SENSOR) + min_temp = util.convert(config.get(CONF_MIN_TEMP), float, None) + max_temp = util.convert(config.get(CONF_MAX_TEMP), float, None) + target_temp = util.convert(config.get(CONF_TARGET_TEMP), float, None) if None in (heater_entity_id, sensor_entity_id): _LOGGER.error('Missing required key %s or %s', CONF_HEATER, CONF_SENSOR) return False - add_devices([HeatControl(hass, name, heater_entity_id, sensor_entity_id)]) + add_devices([HeatControl(hass, name, heater_entity_id, sensor_entity_id, + min_temp, max_temp, target_temp)]) # pylint: disable=too-many-instance-attributes class HeatControl(ThermostatDevice): """ Represents a HeatControl device. """ - - def __init__(self, hass, name, heater_entity_id, sensor_entity_id): + # pylint: disable=too-many-arguments + def __init__(self, hass, name, heater_entity_id, sensor_entity_id, + min_temp, max_temp, target_temp): self.hass = hass self._name = name self.heater_entity_id = heater_entity_id self._active = False self._cur_temp = None - self._target_temp = None + self._min_temp = min_temp + self._max_temp = max_temp + self._target_temp = target_temp self._unit = None track_state_change(hass, sensor_entity_id, self._sensor_changed) @@ -79,6 +89,7 @@ class HeatControl(ThermostatDevice): @property def current_temperature(self): + """ Returns the sensor temperature. """ return self._cur_temp @property @@ -97,6 +108,26 @@ class HeatControl(ThermostatDevice): self._control_heating() self.update_ha_state() + @property + def min_temp(self): + """ Return minimum temperature. """ + # pylint: disable=no-member + if self._min_temp: + return self._min_temp + else: + # get default temp from super class + return ThermostatDevice.min_temp.fget(self) + + @property + def max_temp(self): + """ Return maximum temperature. """ + # pylint: disable=no-member + if self._min_temp: + return self._max_temp + else: + # get default temp from super class + return ThermostatDevice.max_temp.fget(self) + def _sensor_changed(self, entity_id, old_state, new_state): """ Called when temperature changes. """ if new_state is None: diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py new file mode 100644 index 00000000000..ab81e368589 --- /dev/null +++ b/homeassistant/components/thermostat/homematic.py @@ -0,0 +1,131 @@ +""" +homeassistant.components.thermostat.homematic +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Adds support for Homematic (HM-TC-IT-WM-W-EU, HM-CC-RT-DN) thermostats using +Homegear or Homematic central (CCU1/CCU2). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/thermostat.homematic/ +""" +import logging +import socket +from xmlrpc.client import ServerProxy + +from homeassistant.components.thermostat import ThermostatDevice +from homeassistant.const import TEMP_CELCIUS + +REQUIREMENTS = [] + +CONF_ADDRESS = 'address' +CONF_DEVICES = 'devices' +CONF_ID = 'id' +PROPERTY_SET_TEMPERATURE = 'SET_TEMPERATURE' +PROPERTY_VALVE_STATE = 'VALVE_STATE' +PROPERTY_ACTUAL_TEMPERATURE = 'ACTUAL_TEMPERATURE' +PROPERTY_BATTERY_STATE = 'BATTERY_STATE' +PROPERTY_CONTROL_MODE = 'CONTROL_MODE' + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Homematic thermostat. """ + + devices = [] + try: + homegear = ServerProxy(config[CONF_ADDRESS]) + for name, device_cfg in config[CONF_DEVICES].items(): + # get device description to detect the type + device_type = homegear.getDeviceDescription( + device_cfg[CONF_ID] + ':-1')['TYPE'] + + if device_type in ['HM-CC-RT-DN', 'HM-CC-RT-DN-BoM']: + devices.append(HomematicThermostat(homegear, + device_cfg[CONF_ID], + name, 4)) + elif device_type == 'HM-TC-IT-WM-W-EU': + devices.append(HomematicThermostat(homegear, + device_cfg[CONF_ID], + name, 2)) + else: + raise ValueError( + "Device Type '{}' currently not supported".format( + device_type)) + except socket.error: + _LOGGER.exception("Connection error to homematic web service") + return False + + add_devices(devices) + + return True + + +# pylint: disable=too-many-instance-attributes +class HomematicThermostat(ThermostatDevice): + """ Represents a Homematic thermostat. """ + + def __init__(self, device, _id, name, channel): + self.device = device + self._id = _id + self._channel = channel + self._name = name + self._full_device_name = '{}:{}'.format(self._id, self._channel) + + self._current_temperature = None + self._target_temperature = None + self._valve = None + self._battery = None + self._mode = None + self.update() + + @property + def name(self): + """ Returns the name of the Homematic device. """ + return self._name + + @property + def unit_of_measurement(self): + """ Unit of measurement this thermostat expresses itself in. """ + return TEMP_CELCIUS + + @property + def current_temperature(self): + """ Returns the current temperature. """ + return self._current_temperature + + @property + def target_temperature(self): + """ Returns the temperature we try to reach. """ + return self._target_temperature + + def set_temperature(self, temperature): + """ Set new target temperature. """ + self.device.setValue(self._full_device_name, + PROPERTY_SET_TEMPERATURE, + temperature) + + @property + def device_state_attributes(self): + """ Returns device specific state attributes. """ + return {"valve": self._valve, + "battery": self._battery, + "mode": self._mode} + + def update(self): + """ Update the data from the thermostat. """ + try: + self._current_temperature = self.device.getValue( + self._full_device_name, + PROPERTY_ACTUAL_TEMPERATURE) + self._target_temperature = self.device.getValue( + self._full_device_name, + PROPERTY_SET_TEMPERATURE) + self._valve = self.device.getValue(self._full_device_name, + PROPERTY_VALVE_STATE) + self._battery = self.device.getValue(self._full_device_name, + PROPERTY_BATTERY_STATE) + self._mode = self.device.getValue(self._full_device_name, + PROPERTY_CONTROL_MODE) + except socket.error: + _LOGGER.exception("Did not receive any temperature data from the " + "homematic API.") diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 56880c4eded..4139c5d8aa7 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -1,7 +1,7 @@ """ homeassistant.components.thermostat.honeywell ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Adds support for Honeywell Round Connected thermostats. +Adds support for Honeywell Round Connected and Honeywell Evohome thermostats. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/thermostat.honeywell/ @@ -11,7 +11,7 @@ import logging from homeassistant.components.thermostat import ThermostatDevice from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS) -REQUIREMENTS = ['evohomeclient==0.2.3'] +REQUIREMENTS = ['evohomeclient==0.2.4'] _LOGGER = logging.getLogger(__name__) @@ -31,7 +31,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): evo_api = EvohomeClient(username, password) try: - add_devices([RoundThermostat(evo_api)]) + zones = evo_api.temperatures(force_refresh=True) + for i, zone in enumerate(zones): + add_devices([RoundThermostat(evo_api, zone['id'], i == 0)]) except socket.error: _LOGGER.error( "Connection error logging into the honeywell evohome web service" @@ -42,11 +44,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RoundThermostat(ThermostatDevice): """ Represents a Honeywell Round Connected thermostat. """ - def __init__(self, device): + def __init__(self, device, zone_id, master): self.device = device self._current_temperature = None self._target_temperature = None self._name = "round connected" + self._id = zone_id + self._master = master + self._is_dhw = False self.update() @property @@ -67,6 +72,8 @@ class RoundThermostat(ThermostatDevice): @property def target_temperature(self): """ Returns the temperature we try to reach. """ + if self._is_dhw: + return None return self._target_temperature def set_temperature(self, temperature): @@ -75,8 +82,12 @@ class RoundThermostat(ThermostatDevice): def update(self): try: - # Only take first thermostat data from API for now - data = next(self.device.temperatures(force_refresh=True)) + # Only refresh if this is the "master" device, + # others will pick up the cache + for val in self.device.temperatures(force_refresh=self._master): + if val['id'] == self._id: + data = val + except StopIteration: _LOGGER.error("Did not receive any temperature data from the " "evohomeclient API.") @@ -84,4 +95,9 @@ class RoundThermostat(ThermostatDevice): self._current_temperature = data['temp'] self._target_temperature = data['setpoint'] - self._name = data['name'] + if data['thermostat'] == "DOMESTIC_HOT_WATER": + self._name = "Hot Water" + self._is_dhw = True + else: + self._name = data['name'] + self._is_dhw = False diff --git a/homeassistant/components/thermostat/radiotherm.py b/homeassistant/components/thermostat/radiotherm.py index 748d0421acd..051a2a6413e 100644 --- a/homeassistant/components/thermostat/radiotherm.py +++ b/homeassistant/components/thermostat/radiotherm.py @@ -101,6 +101,7 @@ class RadioThermostat(ThermostatDevice): return round(self._target_temperature, 1) def update(self): + """ Update the data from the thermostat. """ self._current_temperature = self.device.temp['raw'] self._name = self.device.name['raw'] if self.device.tmode['human'] == 'Cool': diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index 803cfa609ca..d4eb97f5ec5 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -16,7 +16,6 @@ from homeassistant.helpers import event _LOGGER = logging.getLogger(__name__) PYPI_URL = 'https://pypi.python.org/pypi/homeassistant/json' -DEPENDENCIES = [] DOMAIN = 'updater' ENTITY_ID = 'updater.updater' @@ -48,10 +47,10 @@ def get_newest_version(): return req.json()['info']['version'] except requests.RequestException: _LOGGER.exception('Could not contact PyPI to check for updates') - return + return None except ValueError: _LOGGER.exception('Received invalid response from PyPI') - return + return None except KeyError: _LOGGER.exception('Response from PyPI did not include version') - return + return None diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 03601f1d958..1ab82236596 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -17,14 +17,14 @@ from homeassistant.const import ( ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME) DOMAIN = "wink" -DEPENDENCIES = [] REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' - '9eb39eaba0717922815e673ad1114c685839d890.zip' - '#python-wink==0.1.1'] + '42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip' + '#python-wink==0.2'] DISCOVER_LIGHTS = "wink.lights" DISCOVER_SWITCHES = "wink.switches" DISCOVER_SENSORS = "wink.sensors" +DISCOVER_LOCKS = "wink.locks" def setup(hass, config): @@ -41,7 +41,8 @@ def setup(hass, config): for component_name, func_exists, discovery_type in ( ('light', pywink.get_bulbs, DISCOVER_LIGHTS), ('switch', pywink.get_switches, DISCOVER_SWITCHES), - ('sensor', pywink.get_sensors, DISCOVER_SENSORS)): + ('sensor', pywink.get_sensors, DISCOVER_SENSORS), + ('lock', pywink.get_locks, DISCOVER_LOCKS)): if func_exists(): component = get_component(component_name) diff --git a/homeassistant/components/zone.py b/homeassistant/components/zone.py index a32a297caeb..da0341129f7 100644 --- a/homeassistant/components/zone.py +++ b/homeassistant/components/zone.py @@ -15,7 +15,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.util.location import distance DOMAIN = "zone" -DEPENDENCIES = [] ENTITY_ID_FORMAT = 'zone.{}' ENTITY_ID_HOME = ENTITY_ID_FORMAT.format('home') STATE = 'zoning' diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index 11515e4031d..b52e430600a 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -17,7 +17,6 @@ from homeassistant.const import ( EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED) DOMAIN = "zwave" -DEPENDENCIES = [] REQUIREMENTS = ['pydispatcher==2.0.5'] CONF_USB_STICK_PATH = "usb_path" @@ -83,7 +82,7 @@ def _obj_to_dict(obj): def nice_print_node(node): - """ Prints a nice formatted node to the output (debug method) """ + """ Prints a nice formatted node to the output (debug method). """ node_dict = _obj_to_dict(node) node_dict['values'] = {value_id: _obj_to_dict(value) for value_id, value in node.values.items()} @@ -95,7 +94,7 @@ def nice_print_node(node): def get_config_value(node, value_index): - """ Returns the current config value for a specific index """ + """ Returns the current config value for a specific index. """ try: for value in node.values.values(): diff --git a/homeassistant/config.py b/homeassistant/config.py index 2c2152df7a0..3d17fce5e17 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -67,7 +67,7 @@ def create_default_config(config_dir, detect_location=True): Returns path to new config file if success, None if failed. """ config_path = os.path.join(config_dir, YAML_CONFIG_FILE) - info = {attr: default for attr, default, *_ in DEFAULT_CONFIG} + info = {attr: default for attr, default, _, _ in DEFAULT_CONFIG} location_info = detect_location and loc_util.detect_location_info() diff --git a/homeassistant/const.py b/homeassistant/const.py index da1e424718f..2a1b7108cdd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -53,6 +53,8 @@ STATE_ALARM_ARMED_HOME = 'armed_home' STATE_ALARM_ARMED_AWAY = 'armed_away' STATE_ALARM_PENDING = 'pending' STATE_ALARM_TRIGGERED = 'triggered' +STATE_LOCKED = 'locked' +STATE_UNLOCKED = 'unlocked' # #### STATE AND EVENT ATTRIBUTES #### # Contains current time for a TIME_CHANGED event @@ -96,6 +98,9 @@ ATTR_BATTERY_LEVEL = "battery_level" # For devices which support an armed state ATTR_ARMED = "device_armed" +# For devices which support a locked state +ATTR_LOCKED = "locked" + # For sensors that support 'tripping', eg. motion and door sensors ATTR_TRIPPED = "device_tripped" @@ -135,6 +140,13 @@ SERVICE_ALARM_ARM_HOME = "alarm_arm_home" SERVICE_ALARM_ARM_AWAY = "alarm_arm_away" SERVICE_ALARM_TRIGGER = "alarm_trigger" +SERVICE_LOCK = "lock" +SERVICE_UNLOCK = "unlock" + +SERVICE_MOVE_UP = 'move_up' +SERVICE_MOVE_DOWN = 'move_down' +SERVICE_STOP = 'stop' + # #### API / REMOTE #### SERVER_PORT = 8123 @@ -152,6 +164,7 @@ URL_API_EVENT_FORWARD = "/api/event_forwarding" URL_API_COMPONENTS = "/api/components" URL_API_BOOTSTRAP = "/api/bootstrap" URL_API_ERROR_LOG = "/api/error_log" +URL_API_LOG_OUT = "/api/log_out" HTTP_OK = 200 HTTP_CREATED = 201 diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index d3c0514dcad..ec22181bf5a 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -4,6 +4,8 @@ homeassistant.helpers.entity_component Provides helpers for components that manage entities. """ +from threading import Lock + from homeassistant.bootstrap import prepare_setup_platform from homeassistant.helpers import ( generate_entity_id, config_per_platform, extract_entity_ids) @@ -37,6 +39,7 @@ class EntityComponent(object): self.is_polling = False self.config = None + self.lock = Lock() def setup(self, config): """ @@ -61,8 +64,11 @@ class EntityComponent(object): Takes in a list of new entities. For each entity will see if it already exists. If not, will add it, set it up and push the first state. """ - for entity in new_entities: - if entity is not None and entity not in self.entities.values(): + with self.lock: + for entity in new_entities: + if entity is None or entity in self.entities.values(): + continue + entity.hass = self.hass if getattr(entity, 'entity_id', None) is None: @@ -74,23 +80,33 @@ class EntityComponent(object): entity.update_ha_state() - if self.group is None and self.group_name is not None: - self.group = group.Group(self.hass, self.group_name, - user_defined=False) + if self.group is None and self.group_name is not None: + self.group = group.Group(self.hass, self.group_name, + user_defined=False) - if self.group is not None: - self.group.update_tracked_entity_ids(self.entities.keys()) + if self.group is not None: + self.group.update_tracked_entity_ids(self.entities.keys()) - self._start_polling() + if self.is_polling or \ + not any(entity.should_poll for entity + in self.entities.values()): + return + + self.is_polling = True + + track_utc_time_change( + self.hass, self._update_entity_states, + second=range(0, 60, self.scan_interval)) def extract_from_service(self, service): """ Takes a service and extracts all known entities. Will return all if no entity IDs given in service. """ - if ATTR_ENTITY_ID not in service.data: - return self.entities.values() - else: + with self.lock: + if ATTR_ENTITY_ID not in service.data: + return list(self.entities.values()) + return [self.entities[entity_id] for entity_id in extract_entity_ids(self.hass, service) if entity_id in self.entities] @@ -99,9 +115,10 @@ class EntityComponent(object): """ Update the states of all the entities. """ self.logger.info("Updating %s entities", self.domain) - for entity in self.entities.values(): - if entity.should_poll: - entity.update_ha_state(True) + with self.lock: + for entity in self.entities.values(): + if entity.should_poll: + entity.update_ha_state(True) def _entity_discovered(self, service, info): """ Called when a entity is discovered. """ @@ -110,18 +127,6 @@ class EntityComponent(object): self._setup_platform(self.discovery_platforms[service], {}, info) - def _start_polling(self): - """ Start polling entities if necessary. """ - if self.is_polling or \ - not any(entity.should_poll for entity in self.entities.values()): - return - - self.is_polling = True - - track_utc_time_change( - self.hass, self._update_entity_states, - second=range(0, 60, self.scan_interval)) - def _setup_platform(self, platform_type, platform_config, discovery_info=None): """ Tries to setup a platform for this component. """ diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 60377fd1f5d..3934a6c52ef 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -124,6 +124,7 @@ def track_utc_time_change(hass, action, year=None, month=None, day=None, mat = _matcher + # pylint: disable=too-many-boolean-expressions if mat(now.year, year) and \ mat(now.month, month) and \ mat(now.day, day) and \ diff --git a/homeassistant/loader.py b/homeassistant/loader.py index b05083b4abd..8b38f5e0966 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -193,7 +193,7 @@ def _load_order_component(comp_name, load_order, loading): loading.add(comp_name) - for dependency in component.DEPENDENCIES: + for dependency in getattr(component, 'DEPENDENCIES', []): # Check not already loaded if dependency in load_order: continue diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index fdfbc133944..fd320090736 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -1,12 +1,13 @@ """Helpers to install PyPi packages.""" import logging import os -import pkg_resources import subprocess import sys import threading from urllib.parse import urlparse +import pkg_resources + _LOGGER = logging.getLogger(__name__) INSTALL_LOCK = threading.Lock() @@ -27,7 +28,7 @@ def install_package(package, upgrade=True, target=None): args += ['--target', os.path.abspath(target)] try: - return 0 == subprocess.call(args) + return subprocess.call(args) == 0 except subprocess.SubprocessError: _LOGGER.exception('Unable to install pacakge %s', package) return False @@ -50,4 +51,5 @@ def check_package_exists(package, lib_dir): return True # Check packages from global + virtual environment + # pylint: disable=not-an-iterable return any(dist in req for dist in pkg_resources.working_set) diff --git a/pylintrc b/pylintrc index e8455cf4245..768cd3d46ff 100644 --- a/pylintrc +++ b/pylintrc @@ -9,6 +9,7 @@ reports=no # abstract-class-not-used - is flaky, should not show up but does # unused-argument - generic callbacks and setup methods create a lot of warnings # global-statement - used for the on-demand requirement installation +# redefined-variable-type - this is Python, we're duck typing! disable= locally-disabled, duplicate-code, @@ -16,7 +17,8 @@ disable= abstract-class-little-used, abstract-class-not-used, unused-argument, - global-statement + global-statement, + redefined-variable-type [EXCEPTIONS] overgeneral-exceptions=Exception,HomeAssistantError diff --git a/requirements_all.txt b/requirements_all.txt index c6b4ae81721..2715ca3288d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -18,7 +18,10 @@ python-nmap==0.4.3 pysnmp==4.2.5 # homeassistant.components.discovery -netdisco==0.5.1 +netdisco==0.5.2 + +# homeassistant.components.ecobee +https://github.com/nkgilley/python-ecobee-api/archive/5645f843b64ac4f6e59dfb96233a07083c5e10c1.zip#python-ecobee==0.0.3 # homeassistant.components.ifttt pyfttt==0.3 @@ -36,7 +39,7 @@ blinkstick==1.1.7 phue==0.8 # homeassistant.components.light.limitlessled -ledcontroller==1.1.0 +limitlessled==1.0.0 # homeassistant.components.light.tellstick # homeassistant.components.sensor.tellstick @@ -50,9 +53,10 @@ https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d # homeassistant.components.wink # homeassistant.components.light.wink +# homeassistant.components.lock.wink # homeassistant.components.sensor.wink # homeassistant.components.switch.wink -https://github.com/balloob/python-wink/archive/9eb39eaba0717922815e673ad1114c685839d890.zip#python-wink==0.1.1 +https://github.com/balloob/python-wink/archive/42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip#python-wink==0.2 # homeassistant.components.media_player.cast pychromecast==0.6.12 @@ -75,6 +79,9 @@ https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b6 # homeassistant.components.mqtt paho-mqtt==1.1 +# homeassistant.components.mqtt +jsonpath-rw==1.4.0 + # homeassistant.components.notify.pushbullet pushbullet.py==0.9.0 @@ -144,10 +151,10 @@ https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f hikvision==0.4 # homeassistant.components.switch.orvibo -orvibo==1.0.0 +orvibo==1.0.1 # homeassistant.components.switch.wemo -pywemo==0.3.2 +pywemo==0.3.3 # homeassistant.components.thermostat.honeywell evohomeclient==0.2.3 @@ -163,4 +170,3 @@ https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60 # homeassistant.components.zwave pydispatcher==2.0.5 - diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index b10c0f38ed8..730886075ec 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -16,6 +16,7 @@ COMMENT_REQUIREMENTS = [ def explore_module(package, explore_children): + """ Explore the modules. """ module = importlib.import_module(package) found = [] @@ -33,10 +34,10 @@ def explore_module(package, explore_children): def core_requirements(): + """ Gather core requirements out of setup.py. """ with open('setup.py') as inp: reqs_raw = re.search( r'REQUIRES = \[(.*?)\]', inp.read(), re.S).group(1) - return re.findall(r"'(.*?)'", reqs_raw) @@ -45,14 +46,13 @@ def comment_requirement(req): return any(ign in req for ign in COMMENT_REQUIREMENTS) -def main(): - if not os.path.isfile('requirements_all.txt'): - print('Run this from HA root dir') - return - +def gather_modules(): + """ Collect the information and construct the output. """ reqs = OrderedDict() errors = [] + output = [] + for package in sorted(explore_module('homeassistant.components', True)): try: module = importlib.import_module(package) @@ -69,22 +69,43 @@ def main(): if errors: print("Found errors") print('\n'.join(errors)) - return - - print('# Home Assistant core') - print('\n'.join(core_requirements())) - print() + return None + output.append('# Home Assistant core') + output.append('\n') + output.append('\n'.join(core_requirements())) + output.append('\n') for pkg, requirements in reqs.items(): for req in sorted(requirements, key=lambda name: (len(name.split('.')), name)): - print('#', req) + output.append('\n# {}'.format(req)) if comment_requirement(pkg): - print('#', pkg) + output.append('\n# {}\n'.format(pkg)) else: - print(pkg) - print() + output.append('\n{}\n'.format(pkg)) + + return ''.join(output) + + +def write_file(data): + """ Writes the modules to the requirements_all.txt. """ + with open('requirements_all.txt', 'w+') as req_file: + req_file.write(data) + + +def main(): + """ Main """ + if not os.path.isfile('requirements_all.txt'): + print('Run this from HA root dir') + return + + data = gather_modules() + + if data is None: + return + + write_file(data) if __name__ == '__main__': main() diff --git a/tests/components/alarm_control_panel/__init__.py b/tests/components/alarm_control_panel/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/alarm_control_panel/test_mqtt.py b/tests/components/alarm_control_panel/test_mqtt.py index 6cba26c15a6..58c55350cd2 100644 --- a/tests/components/alarm_control_panel/test_mqtt.py +++ b/tests/components/alarm_control_panel/test_mqtt.py @@ -101,7 +101,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): alarm_control_panel.alarm_arm_home(self.hass) self.hass.pool.block_till_done() - self.assertEqual(('alarm/command', 'ARM_HOME', 0), + self.assertEqual(('alarm/command', 'ARM_HOME', 0, False), self.mock_publish.mock_calls[-1][1]) def test_arm_home_not_publishes_mqtt_with_invalid_code(self): @@ -130,7 +130,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): alarm_control_panel.alarm_arm_away(self.hass) self.hass.pool.block_till_done() - self.assertEqual(('alarm/command', 'ARM_AWAY', 0), + self.assertEqual(('alarm/command', 'ARM_AWAY', 0, False), self.mock_publish.mock_calls[-1][1]) def test_arm_away_not_publishes_mqtt_with_invalid_code(self): @@ -159,7 +159,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): alarm_control_panel.alarm_disarm(self.hass) self.hass.pool.block_till_done() - self.assertEqual(('alarm/command', 'DISARM', 0), + self.assertEqual(('alarm/command', 'DISARM', 0, False), self.mock_publish.mock_calls[-1][1]) def test_disarm_not_publishes_mqtt_with_invalid_code(self): diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index a04b8d01f4e..8280f396f93 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -253,6 +253,156 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) + def test_if_fires_on_entity_change_below_with_attribute(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'below': 10, + }, + 'action': { + 'service': 'test.automation' + } + } + })) + # 9 is below 10 + self.hass.states.set('test.entity', 9, { 'test_attribute': 11 }) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_not_fires_on_entity_change_not_below_with_attribute(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'below': 10, + }, + 'action': { + 'service': 'test.automation' + } + } + })) + # 11 is not below 10 + self.hass.states.set('test.entity', 11, { 'test_attribute': 9 }) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_fires_on_attribute_change_with_attribute_below(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'attribute': 'test_attribute', + 'below': 10, + }, + 'action': { + 'service': 'test.automation' + } + } + })) + # 9 is below 10 + self.hass.states.set('test.entity', 'entity', { 'test_attribute': 9 }) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_not_fires_on_attribute_change_with_attribute_not_below(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'attribute': 'test_attribute', + 'below': 10, + }, + 'action': { + 'service': 'test.automation' + } + } + })) + # 11 is not below 10 + self.hass.states.set('test.entity', 'entity', { 'test_attribute': 11 }) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_not_fires_on_entity_change_with_attribute_below(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'attribute': 'test_attribute', + 'below': 10, + }, + 'action': { + 'service': 'test.automation' + } + } + })) + # 11 is not below 10, entity state value should not be tested + self.hass.states.set('test.entity', '9', { 'test_attribute': 11 }) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_not_fires_on_entity_change_with_not_attribute_below(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'attribute': 'test_attribute', + 'below': 10, + }, + 'action': { + 'service': 'test.automation' + } + } + })) + # 11 is not below 10, entity state value should not be tested + self.hass.states.set('test.entity', 'entity') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_fires_on_attribute_change_with_attribute_below_multiple_attributes(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'attribute': 'test_attribute', + 'below': 10, + }, + 'action': { + 'service': 'test.automation' + } + } + })) + # 9 is not below 10 + self.hass.states.set('test.entity', 'entity', { 'test_attribute': 9, 'not_test_attribute': 11 }) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_not_fires_on_attribute_change_with_attribute_not_below_multiple_attributes(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'attribute': 'test_attribute', + 'below': 10, + }, + 'action': { + 'service': 'test.automation' + } + } + })) + # 11 is not below 10 + self.hass.states.set('test.entity', 'entity', { 'test_attribute': 11, 'not_test_attribute': 9 }) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + def test_if_action(self): entity_id = 'domain.test_entity' test_state = 10 diff --git a/tests/components/binary_sensor/__init__.py b/tests/components/binary_sensor/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py new file mode 100644 index 00000000000..83fa532d051 --- /dev/null +++ b/tests/components/binary_sensor/test_mqtt.py @@ -0,0 +1,48 @@ +""" +tests.components.binary_sensor.test_mqtt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests MQTT binary sensor. +""" +import unittest + +import homeassistant.core as ha +import homeassistant.components.binary_sensor as binary_sensor +from tests.common import mock_mqtt_component, fire_mqtt_message +from homeassistant.const import (STATE_OFF, STATE_ON) + + +class TestSensorMQTT(unittest.TestCase): + """ Test the MQTT sensor. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_setting_sensor_value_via_mqtt_message(self): + self.assertTrue(binary_sensor.setup(self.hass, { + 'binary_sensor': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'payload_on': 'ON', + 'payload_off': 'OFF', + } + })) + + state = self.hass.states.get('binary_sensor.test') + self.assertEqual(STATE_OFF, state.state) + + fire_mqtt_message(self.hass, 'test-topic', 'ON') + self.hass.pool.block_till_done() + state = self.hass.states.get('binary_sensor.test') + self.assertEqual(STATE_ON, state.state) + + fire_mqtt_message(self.hass, 'test-topic', 'OFF') + self.hass.pool.block_till_done() + state = self.hass.states.get('binary_sensor.test') + self.assertEqual(STATE_OFF, state.state) diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 39c81ee0a04..8172a6c7c63 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -44,7 +44,6 @@ light: payload_off: "off" """ import unittest -import homeassistant.util.color as color_util from homeassistant.const import STATE_ON, STATE_OFF import homeassistant.core as ha @@ -63,6 +62,29 @@ class TestLightMQTT(unittest.TestCase): """ Stop down stuff we started. """ self.hass.stop() + def test_no_color_or_brightness_if_no_topics(self): + self.assertTrue(light.setup(self.hass, { + 'light': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test_light_rgb/status', + 'command_topic': 'test_light_rgb/set', + } + })) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_OFF, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + + fire_mqtt_message(self.hass, 'test_light_rgb/status', 'on') + self.hass.pool.block_till_done() + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) + def test_controlling_state_via_topic(self): self.assertTrue(light.setup(self.hass, { 'light': { @@ -82,12 +104,16 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) + self.assertIsNone(state.attributes.get('rgb_color')) + self.assertIsNone(state.attributes.get('brightness')) fire_mqtt_message(self.hass, 'test_light_rgb/status', 'on') self.hass.pool.block_till_done() state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) + self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual(255, state.attributes.get('brightness')) fire_mqtt_message(self.hass, 'test_light_rgb/status', 'off') self.hass.pool.block_till_done() @@ -123,9 +149,7 @@ class TestLightMQTT(unittest.TestCase): 'platform': 'mqtt', 'name': 'test', 'command_topic': 'test_light_rgb/set', - 'brightness_state_topic': 'test_light_rgb/brightness/status', 'brightness_command_topic': 'test_light_rgb/brightness/set', - 'rgb_state_topic': 'test_light_rgb/rgb/status', 'rgb_command_topic': 'test_light_rgb/rgb/set', 'qos': 2, 'payload_on': 'on', @@ -139,7 +163,7 @@ class TestLightMQTT(unittest.TestCase): light.turn_on(self.hass, 'light.test') self.hass.pool.block_till_done() - self.assertEqual(('test_light_rgb/set', 'on', 2), + self.assertEqual(('test_light_rgb/set', 'on', 2, False), self.mock_publish.mock_calls[-1][1]) state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) @@ -147,7 +171,30 @@ class TestLightMQTT(unittest.TestCase): light.turn_off(self.hass, 'light.test') self.hass.pool.block_till_done() - self.assertEqual(('test_light_rgb/set', 'off', 2), + self.assertEqual(('test_light_rgb/set', 'off', 2, False), self.mock_publish.mock_calls[-1][1]) state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) + + light.turn_on(self.hass, 'light.test', rgb_color=[75, 75, 75], + brightness=50) + self.hass.pool.block_till_done() + + # Calls are threaded so we need to reorder them + bright_call, rgb_call, state_call = \ + sorted((call[1] for call in self.mock_publish.mock_calls[-3:]), + key=lambda call: call[0]) + + self.assertEqual(('test_light_rgb/set', 'on', 2, False), + state_call) + + self.assertEqual(('test_light_rgb/rgb/set', '75,75,75', 2, False), + rgb_call) + + self.assertEqual(('test_light_rgb/brightness/set', 50, 2, False), + bright_call) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual([75, 75, 75], state.attributes['rgb_color']) + self.assertEqual(50, state.attributes['brightness']) diff --git a/tests/components/lock/__init__.py b/tests/components/lock/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/lock/test_demo.py b/tests/components/lock/test_demo.py new file mode 100644 index 00000000000..7320b1aa69a --- /dev/null +++ b/tests/components/lock/test_demo.py @@ -0,0 +1,51 @@ +""" +tests.components.lock.test_demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests demo lock component. +""" +import unittest + +import homeassistant.core as ha +from homeassistant.components import lock + + +FRONT = 'lock.front_door' +KITCHEN = 'lock.kitchen_door' + + +class TestLockDemo(unittest.TestCase): + """ Test the demo lock. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + self.assertTrue(lock.setup(self.hass, { + 'lock': { + 'platform': 'demo' + } + })) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_is_locked(self): + self.assertTrue(lock.is_locked(self.hass, FRONT)) + self.hass.states.is_state(FRONT, 'locked') + + self.assertFalse(lock.is_locked(self.hass, KITCHEN)) + self.hass.states.is_state(KITCHEN, 'unlocked') + + def test_locking(self): + lock.lock(self.hass, KITCHEN) + + self.hass.pool.block_till_done() + + self.assertTrue(lock.is_locked(self.hass, KITCHEN)) + + def test_unlocking(self): + lock.unlock(self.hass, FRONT) + + self.hass.pool.block_till_done() + + self.assertFalse(lock.is_locked(self.hass, FRONT)) diff --git a/tests/components/rollershutter/__init__.py b/tests/components/rollershutter/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/rollershutter/test_mqtt.py b/tests/components/rollershutter/test_mqtt.py new file mode 100644 index 00000000000..261618a5e02 --- /dev/null +++ b/tests/components/rollershutter/test_mqtt.py @@ -0,0 +1,166 @@ +""" +tests.components.rollershutter.test_mqtt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests MQTT rollershutter. +""" +import unittest + +from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN +import homeassistant.core as ha +import homeassistant.components.rollershutter as rollershutter +from tests.common import mock_mqtt_component, fire_mqtt_message + + +class TestRollershutterMQTT(unittest.TestCase): + """ Test the MQTT rollershutter. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + self.mock_publish = mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_controlling_state_via_topic(self): + self.assertTrue(rollershutter.setup(self.hass, { + 'rollershutter': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 0, + 'payload_up': 'UP', + 'payload_down': 'DOWN', + 'payload_stop': 'STOP' + } + })) + + state = self.hass.states.get('rollershutter.test') + self.assertEqual(STATE_UNKNOWN, state.state) + + fire_mqtt_message(self.hass, 'state-topic', '0') + self.hass.pool.block_till_done() + + state = self.hass.states.get('rollershutter.test') + self.assertEqual(STATE_CLOSED, state.state) + + fire_mqtt_message(self.hass, 'state-topic', '50') + self.hass.pool.block_till_done() + + state = self.hass.states.get('rollershutter.test') + self.assertEqual(STATE_OPEN, state.state) + + fire_mqtt_message(self.hass, 'state-topic', '100') + self.hass.pool.block_till_done() + + state = self.hass.states.get('rollershutter.test') + self.assertEqual(STATE_OPEN, state.state) + + def test_send_move_up_command(self): + self.assertTrue(rollershutter.setup(self.hass, { + 'rollershutter': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 2 + } + })) + + state = self.hass.states.get('rollershutter.test') + self.assertEqual(STATE_UNKNOWN, state.state) + + rollershutter.move_up(self.hass, 'rollershutter.test') + self.hass.pool.block_till_done() + + self.assertEqual(('command-topic', 'UP', 2, False), + self.mock_publish.mock_calls[-1][1]) + state = self.hass.states.get('rollershutter.test') + self.assertEqual(STATE_UNKNOWN, state.state) + + def test_send_move_down_command(self): + self.assertTrue(rollershutter.setup(self.hass, { + 'rollershutter': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 2 + } + })) + + state = self.hass.states.get('rollershutter.test') + self.assertEqual(STATE_UNKNOWN, state.state) + + rollershutter.move_down(self.hass, 'rollershutter.test') + self.hass.pool.block_till_done() + + self.assertEqual(('command-topic', 'DOWN', 2, False), + self.mock_publish.mock_calls[-1][1]) + state = self.hass.states.get('rollershutter.test') + self.assertEqual(STATE_UNKNOWN, state.state) + + def test_send_stop_command(self): + self.assertTrue(rollershutter.setup(self.hass, { + 'rollershutter': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 2 + } + })) + + state = self.hass.states.get('rollershutter.test') + self.assertEqual(STATE_UNKNOWN, state.state) + + rollershutter.stop(self.hass, 'rollershutter.test') + self.hass.pool.block_till_done() + + self.assertEqual(('command-topic', 'STOP', 2, False), + self.mock_publish.mock_calls[-1][1]) + state = self.hass.states.get('rollershutter.test') + self.assertEqual(STATE_UNKNOWN, state.state) + + def test_state_attributes_current_position(self): + self.assertTrue(rollershutter.setup(self.hass, { + 'rollershutter': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'payload_up': 'UP', + 'payload_down': 'DOWN', + 'payload_stop': 'STOP' + } + })) + + state_attributes_dict = self.hass.states.get( + 'rollershutter.test').attributes + self.assertFalse('current_position' in state_attributes_dict) + + fire_mqtt_message(self.hass, 'state-topic', '0') + self.hass.pool.block_till_done() + current_position = self.hass.states.get( + 'rollershutter.test').attributes['current_position'] + self.assertEqual(0, current_position) + + fire_mqtt_message(self.hass, 'state-topic', '50') + self.hass.pool.block_till_done() + current_position = self.hass.states.get( + 'rollershutter.test').attributes['current_position'] + self.assertEqual(50, current_position) + + fire_mqtt_message(self.hass, 'state-topic', '101') + self.hass.pool.block_till_done() + current_position = self.hass.states.get( + 'rollershutter.test').attributes['current_position'] + self.assertEqual(50, current_position) + + fire_mqtt_message(self.hass, 'state-topic', 'non-numeric') + self.hass.pool.block_till_done() + current_position = self.hass.states.get( + 'rollershutter.test').attributes['current_position'] + self.assertEqual(50, current_position) diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index b59ea867c58..0c17b95e212 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -39,3 +39,21 @@ class TestSensorMQTT(unittest.TestCase): self.assertEqual('100', state.state) self.assertEqual('fav unit', state.attributes.get('unit_of_measurement')) + + def test_setting_sensor_value_via_mqtt_json_message(self): + self.assertTrue(sensor.setup(self.hass, { + 'sensor': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit', + 'state_format': 'json:val' + } + })) + + fire_mqtt_message(self.hass, 'test-topic', '{ "val": "100" }') + self.hass.pool.block_till_done() + state = self.hass.states.get('sensor.test') + + self.assertEqual('100', state.state) + diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index a09fcf86c58..2cfe29c2910 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -68,7 +68,7 @@ class TestSensorMQTT(unittest.TestCase): switch.turn_on(self.hass, 'switch.test') self.hass.pool.block_till_done() - self.assertEqual(('command-topic', 'beer on', 2), + self.assertEqual(('command-topic', 'beer on', 2, False), self.mock_publish.mock_calls[-1][1]) state = self.hass.states.get('switch.test') self.assertEqual(STATE_ON, state.state) @@ -76,7 +76,35 @@ class TestSensorMQTT(unittest.TestCase): switch.turn_off(self.hass, 'switch.test') self.hass.pool.block_till_done() - self.assertEqual(('command-topic', 'beer off', 2), + self.assertEqual(('command-topic', 'beer off', 2, False), self.mock_publish.mock_calls[-1][1]) state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) + + def test_controlling_state_via_topic_and_json_message(self): + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'payload_on': 'beer on', + 'payload_off': 'beer off', + 'state_format': 'json:val' + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + fire_mqtt_message(self.hass, 'state-topic', '{"val":"beer on"}') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + fire_mqtt_message(self.hass, 'state-topic', '{"val":"beer off"}') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) \ No newline at end of file diff --git a/tests/components/test_api.py b/tests/components/test_api.py index b267e6b3c1c..56694289303 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -8,14 +8,13 @@ Tests Home Assistant HTTP component does what it should do. import unittest import json from unittest.mock import patch +import tempfile import requests +from homeassistant import bootstrap, const import homeassistant.core as ha -import homeassistant.bootstrap as bootstrap -import homeassistant.remote as remote import homeassistant.components.http as http -from homeassistant.const import HTTP_HEADER_HA_AUTH API_PASSWORD = "test1234" @@ -26,7 +25,7 @@ SERVER_PORT = 8120 HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT) -HA_HEADERS = {HTTP_HEADER_HA_AUTH: API_PASSWORD} +HA_HEADERS = {const.HTTP_HEADER_HA_AUTH: API_PASSWORD} hass = None @@ -68,20 +67,20 @@ class TestAPI(unittest.TestCase): # TODO move back to http component and test with use_auth. def test_access_denied_without_password(self): req = requests.get( - _url(remote.URL_API_STATES_ENTITY.format("test"))) + _url(const.URL_API_STATES_ENTITY.format("test"))) self.assertEqual(401, req.status_code) def test_access_denied_with_wrong_password(self): req = requests.get( - _url(remote.URL_API_STATES_ENTITY.format("test")), - headers={HTTP_HEADER_HA_AUTH: 'wrongpassword'}) + _url(const.URL_API_STATES_ENTITY.format("test")), + headers={const.HTTP_HEADER_HA_AUTH: 'wrongpassword'}) self.assertEqual(401, req.status_code) def test_api_list_state_entities(self): """ Test if the debug interface allows us to list state entities. """ - req = requests.get(_url(remote.URL_API_STATES), + req = requests.get(_url(const.URL_API_STATES), headers=HA_HEADERS) remote_data = [ha.State.from_dict(item) for item in req.json()] @@ -91,7 +90,7 @@ class TestAPI(unittest.TestCase): def test_api_get_state(self): """ Test if the debug interface allows us to get a state. """ req = requests.get( - _url(remote.URL_API_STATES_ENTITY.format("test.test")), + _url(const.URL_API_STATES_ENTITY.format("test.test")), headers=HA_HEADERS) data = ha.State.from_dict(req.json()) @@ -105,7 +104,7 @@ class TestAPI(unittest.TestCase): def test_api_get_non_existing_state(self): """ Test if the debug interface allows us to get a state. """ req = requests.get( - _url(remote.URL_API_STATES_ENTITY.format("does_not_exist")), + _url(const.URL_API_STATES_ENTITY.format("does_not_exist")), headers=HA_HEADERS) self.assertEqual(404, req.status_code) @@ -115,7 +114,7 @@ class TestAPI(unittest.TestCase): hass.states.set("test.test", "not_to_be_set") - requests.post(_url(remote.URL_API_STATES_ENTITY.format("test.test")), + requests.post(_url(const.URL_API_STATES_ENTITY.format("test.test")), data=json.dumps({"state": "debug_state_change2"}), headers=HA_HEADERS) @@ -130,7 +129,7 @@ class TestAPI(unittest.TestCase): new_state = "debug_state_change" req = requests.post( - _url(remote.URL_API_STATES_ENTITY.format( + _url(const.URL_API_STATES_ENTITY.format( "test_entity.that_does_not_exist")), data=json.dumps({'state': new_state}), headers=HA_HEADERS) @@ -146,7 +145,7 @@ class TestAPI(unittest.TestCase): """ Test if API sends appropriate error if we omit state. """ req = requests.post( - _url(remote.URL_API_STATES_ENTITY.format( + _url(const.URL_API_STATES_ENTITY.format( "test_entity.that_does_not_exist")), data=json.dumps({}), headers=HA_HEADERS) @@ -165,7 +164,7 @@ class TestAPI(unittest.TestCase): hass.bus.listen_once("test.event_no_data", listener) requests.post( - _url(remote.URL_API_EVENTS_EVENT.format("test.event_no_data")), + _url(const.URL_API_EVENTS_EVENT.format("test.event_no_data")), headers=HA_HEADERS) hass.pool.block_till_done() @@ -186,7 +185,7 @@ class TestAPI(unittest.TestCase): hass.bus.listen_once("test_event_with_data", listener) requests.post( - _url(remote.URL_API_EVENTS_EVENT.format("test_event_with_data")), + _url(const.URL_API_EVENTS_EVENT.format("test_event_with_data")), data=json.dumps({"test": 1}), headers=HA_HEADERS) @@ -206,7 +205,7 @@ class TestAPI(unittest.TestCase): hass.bus.listen_once("test_event_bad_data", listener) req = requests.post( - _url(remote.URL_API_EVENTS_EVENT.format("test_event_bad_data")), + _url(const.URL_API_EVENTS_EVENT.format("test_event_bad_data")), data=json.dumps('not an object'), headers=HA_HEADERS) @@ -217,7 +216,7 @@ class TestAPI(unittest.TestCase): # Try now with valid but unusable JSON req = requests.post( - _url(remote.URL_API_EVENTS_EVENT.format("test_event_bad_data")), + _url(const.URL_API_EVENTS_EVENT.format("test_event_bad_data")), data=json.dumps([1, 2, 3]), headers=HA_HEADERS) @@ -226,9 +225,31 @@ class TestAPI(unittest.TestCase): self.assertEqual(422, req.status_code) self.assertEqual(0, len(test_value)) + def test_api_get_config(self): + req = requests.get(_url(const.URL_API_CONFIG), + headers=HA_HEADERS) + self.assertEqual(hass.config.as_dict(), req.json()) + + def test_api_get_components(self): + req = requests.get(_url(const.URL_API_COMPONENTS), + headers=HA_HEADERS) + self.assertEqual(hass.config.components, req.json()) + + def test_api_get_error_log(self): + test_content = 'Test String' + with tempfile.NamedTemporaryFile() as log: + log.write(test_content.encode('utf-8')) + log.flush() + + with patch.object(hass.config, 'path', return_value=log.name): + req = requests.get(_url(const.URL_API_ERROR_LOG), + headers=HA_HEADERS) + self.assertEqual(test_content, req.text) + self.assertIsNone(req.headers.get('expires')) + def test_api_get_event_listeners(self): """ Test if we can get the list of events being listened for. """ - req = requests.get(_url(remote.URL_API_EVENTS), + req = requests.get(_url(const.URL_API_EVENTS), headers=HA_HEADERS) local = hass.bus.listeners @@ -241,7 +262,7 @@ class TestAPI(unittest.TestCase): def test_api_get_services(self): """ Test if we can get a dict describing current services. """ - req = requests.get(_url(remote.URL_API_SERVICES), + req = requests.get(_url(const.URL_API_SERVICES), headers=HA_HEADERS) local_services = hass.services.services @@ -262,7 +283,7 @@ class TestAPI(unittest.TestCase): hass.services.register("test_domain", "test_service", listener) requests.post( - _url(remote.URL_API_SERVICES_SERVICE.format( + _url(const.URL_API_SERVICES_SERVICE.format( "test_domain", "test_service")), headers=HA_HEADERS) @@ -283,7 +304,7 @@ class TestAPI(unittest.TestCase): hass.services.register("test_domain", "test_service", listener) requests.post( - _url(remote.URL_API_SERVICES_SERVICE.format( + _url(const.URL_API_SERVICES_SERVICE.format( "test_domain", "test_service")), data=json.dumps({"test": 1}), headers=HA_HEADERS) @@ -296,24 +317,24 @@ class TestAPI(unittest.TestCase): """ Test setting up event forwarding. """ req = requests.post( - _url(remote.URL_API_EVENT_FORWARD), + _url(const.URL_API_EVENT_FORWARD), headers=HA_HEADERS) self.assertEqual(400, req.status_code) req = requests.post( - _url(remote.URL_API_EVENT_FORWARD), + _url(const.URL_API_EVENT_FORWARD), data=json.dumps({'host': '127.0.0.1'}), headers=HA_HEADERS) self.assertEqual(400, req.status_code) req = requests.post( - _url(remote.URL_API_EVENT_FORWARD), + _url(const.URL_API_EVENT_FORWARD), data=json.dumps({'api_password': 'bla-di-bla'}), headers=HA_HEADERS) self.assertEqual(400, req.status_code) req = requests.post( - _url(remote.URL_API_EVENT_FORWARD), + _url(const.URL_API_EVENT_FORWARD), data=json.dumps({ 'api_password': 'bla-di-bla', 'host': '127.0.0.1', @@ -323,7 +344,7 @@ class TestAPI(unittest.TestCase): self.assertEqual(422, req.status_code) req = requests.post( - _url(remote.URL_API_EVENT_FORWARD), + _url(const.URL_API_EVENT_FORWARD), data=json.dumps({ 'api_password': 'bla-di-bla', 'host': '127.0.0.1', @@ -334,7 +355,7 @@ class TestAPI(unittest.TestCase): # Setup a real one req = requests.post( - _url(remote.URL_API_EVENT_FORWARD), + _url(const.URL_API_EVENT_FORWARD), data=json.dumps({ 'api_password': API_PASSWORD, 'host': '127.0.0.1', @@ -345,13 +366,13 @@ class TestAPI(unittest.TestCase): # Delete it again.. req = requests.delete( - _url(remote.URL_API_EVENT_FORWARD), + _url(const.URL_API_EVENT_FORWARD), data=json.dumps({}), headers=HA_HEADERS) self.assertEqual(400, req.status_code) req = requests.delete( - _url(remote.URL_API_EVENT_FORWARD), + _url(const.URL_API_EVENT_FORWARD), data=json.dumps({ 'host': '127.0.0.1', 'port': 'abcd' @@ -360,7 +381,7 @@ class TestAPI(unittest.TestCase): self.assertEqual(422, req.status_code) req = requests.delete( - _url(remote.URL_API_EVENT_FORWARD), + _url(const.URL_API_EVENT_FORWARD), data=json.dumps({ 'host': '127.0.0.1', 'port': SERVER_PORT diff --git a/tests/components/test_mqtt.py b/tests/components/test_mqtt.py index 4c3dbb1d20a..47a5ac7b4e1 100644 --- a/tests/components/test_mqtt.py +++ b/tests/components/test_mqtt.py @@ -4,6 +4,7 @@ tests.test_component_mqtt Tests MQTT component. """ +from collections import namedtuple import unittest from unittest import mock import socket @@ -17,8 +18,8 @@ from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message) -class TestDemo(unittest.TestCase): - """ Test the demo module. """ +class TestMQTT(unittest.TestCase): + """ Test the MQTT module. """ def setUp(self): # pylint: disable=invalid-name self.hass = get_test_home_assistant(1) @@ -136,3 +137,72 @@ class TestDemo(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) + + +class TestMQTTCallbacks(unittest.TestCase): + """ Test the MQTT callbacks. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = get_test_home_assistant(1) + mock_mqtt_component(self.hass) + self.calls = [] + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_receiving_mqtt_message_fires_hass_event(self): + calls = [] + + def record(event): + calls.append(event) + + self.hass.bus.listen_once(mqtt.EVENT_MQTT_MESSAGE_RECEIVED, record) + + MQTTMessage = namedtuple('MQTTMessage', ['topic', 'qos', 'payload']) + message = MQTTMessage('test_topic', 1, 'Hello World!'.encode('utf-8')) + + mqtt._mqtt_on_message(None, {'hass': self.hass}, message) + self.hass.pool.block_till_done() + + self.assertEqual(1, len(calls)) + last_event = calls[0] + self.assertEqual('Hello World!', last_event.data['payload']) + self.assertEqual(message.topic, last_event.data['topic']) + self.assertEqual(message.qos, last_event.data['qos']) + + def test_mqtt_failed_connection_results_in_disconnect(self): + for result_code in range(1, 6): + mqttc = mock.MagicMock() + mqtt._mqtt_on_connect(mqttc, {'topics': {}}, 0, result_code) + self.assertTrue(mqttc.disconnect.called) + + def test_mqtt_subscribes_topics_on_connect(self): + prev_topics = { + 'topic/test': 1, + 'home/sensor': 2, + 'still/pending': None + } + mqttc = mock.MagicMock() + mqtt._mqtt_on_connect(mqttc, {'topics': prev_topics}, 0, 0) + self.assertFalse(mqttc.disconnect.called) + + expected = [(topic, qos) for topic, qos in prev_topics.items() + if qos is not None] + self.assertEqual(expected, [call[1] for call + in mqttc.subscribe.mock_calls]) + + def test_mqtt_disconnect_tries_no_reconnect_on_stop(self): + mqttc = mock.MagicMock() + mqtt._mqtt_on_disconnect(mqttc, {}, 0) + self.assertFalse(mqttc.reconnect.called) + + @mock.patch('homeassistant.components.mqtt.time.sleep') + def test_mqtt_disconnect_tries_reconnect(self, mock_sleep): + mqttc = mock.MagicMock() + mqttc.reconnect.side_effect = [1, 1, 1, 0] + mqtt._mqtt_on_disconnect(mqttc, {}, 1) + self.assertTrue(mqttc.reconnect.called) + self.assertEqual(4, len(mqttc.reconnect.mock_calls)) + self.assertEqual([1, 2, 4], + [call[1][0] for call in mock_sleep.mock_calls]) diff --git a/tests/components/test_script.py b/tests/components/test_script.py index 50cfba55ec5..30b7e4e3c8f 100644 --- a/tests/components/test_script.py +++ b/tests/components/test_script.py @@ -27,23 +27,10 @@ class TestScript(unittest.TestCase): """ Stop down stuff we started. """ self.hass.stop() - def test_setup_with_empty_sequence(self): - self.assertTrue(script.setup(self.hass, { - 'script': { - 'test': { - 'sequence': [] - } - } - })) - - self.assertIsNone(self.hass.states.get(ENTITY_ID)) - def test_setup_with_missing_sequence(self): self.assertTrue(script.setup(self.hass, { 'script': { - 'test': { - 'sequence': [] - } + 'test': {} } })) @@ -60,6 +47,19 @@ class TestScript(unittest.TestCase): self.assertEqual(0, len(self.hass.states.entity_ids('script'))) + def test_setup_with_dict_as_sequence(self): + self.assertTrue(script.setup(self.hass, { + 'script': { + 'test': { + 'sequence': { + 'event': 'test_event' + } + } + } + })) + + self.assertEqual(0, len(self.hass.states.entity_ids('script'))) + def test_firing_event(self): event = 'test_event' calls = [] diff --git a/tests/components/test_updater.py b/tests/components/test_updater.py new file mode 100644 index 00000000000..3e5cf55e0b0 --- /dev/null +++ b/tests/components/test_updater.py @@ -0,0 +1,74 @@ +""" +tests.test_updater +~~~~~~~~~~~~~~~~~~ + +Tests updater component. +""" +import unittest +from unittest.mock import patch + +import requests + +import homeassistant.core as ha +from homeassistant.const import __version__ as CURRENT_VERSION +from homeassistant.components import updater +import homeassistant.util.dt as dt_util +from tests.common import fire_time_changed + +NEW_VERSION = '10000.0' + + +class TestUpdater(unittest.TestCase): + """ Test the demo lock. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + @patch('homeassistant.components.updater.get_newest_version') + def test_new_version_shows_entity_on_start(self, mock_get_newest_version): + mock_get_newest_version.return_value = NEW_VERSION + + self.assertTrue(updater.setup(self.hass, { + 'updater': None + })) + + self.assertTrue(self.hass.states.is_state(updater.ENTITY_ID, + NEW_VERSION)) + + @patch('homeassistant.components.updater.get_newest_version') + def test_no_entity_on_same_version(self, mock_get_newest_version): + mock_get_newest_version.return_value = CURRENT_VERSION + + self.assertTrue(updater.setup(self.hass, { + 'updater': None + })) + + self.assertIsNone(self.hass.states.get(updater.ENTITY_ID)) + + mock_get_newest_version.return_value = NEW_VERSION + + fire_time_changed(self.hass, + dt_util.utcnow().replace(hour=0, minute=0, second=0)) + + self.hass.pool.block_till_done() + + self.assertTrue(self.hass.states.is_state(updater.ENTITY_ID, + NEW_VERSION)) + + @patch('homeassistant.components.updater.requests.get') + def test_errors_while_fetching_new_version(self, mock_get): + mock_get.side_effect = requests.RequestException + + self.assertIsNone(updater.get_newest_version()) + + mock_get.side_effect = ValueError + + self.assertIsNone(updater.get_newest_version()) + + mock_get.side_effect = KeyError + + self.assertIsNone(updater.get_newest_version()) diff --git a/tests/components/thermostat/test_heat_control.py b/tests/components/thermostat/test_heat_control.py index f0b487ae86c..4ec305574e2 100644 --- a/tests/components/thermostat/test_heat_control.py +++ b/tests/components/thermostat/test_heat_control.py @@ -21,6 +21,9 @@ from homeassistant.components import switch, thermostat entity = 'thermostat.test' ent_sensor = 'sensor.test' ent_switch = 'switch.test' +min_temp = 3.0 +max_temp = 65.0 +target_temp = 42.0 class TestThermostatHeatControl(unittest.TestCase): @@ -43,6 +46,28 @@ class TestThermostatHeatControl(unittest.TestCase): def test_setup_defaults_to_unknown(self): self.assertEqual('unknown', self.hass.states.get(entity).state) + def test_default_setup_params(self): + state = self.hass.states.get(entity) + self.assertEqual(7, state.attributes.get('min_temp')) + self.assertEqual(35, state.attributes.get('max_temp')) + self.assertEqual(None, state.attributes.get('temperature')) + + def test_custom_setup_params(self): + thermostat.setup(self.hass, {'thermostat': { + 'platform': 'heat_control', + 'name': 'test', + 'heater': ent_switch, + 'target_sensor': ent_sensor, + 'min_temp': min_temp, + 'max_temp': max_temp, + 'target_temp': target_temp + }}) + state = self.hass.states.get(entity) + self.assertEqual(min_temp, state.attributes.get('min_temp')) + self.assertEqual(max_temp, state.attributes.get('max_temp')) + self.assertEqual(target_temp, state.attributes.get('temperature')) + self.assertEqual(str(target_temp), self.hass.states.get(entity).state) + def test_set_target_temp(self): thermostat.set_temperature(self.hass, 30) self.hass.pool.block_till_done() @@ -113,3 +138,4 @@ class TestThermostatHeatControl(unittest.TestCase): self.hass.services.register('switch', SERVICE_TURN_ON, log_call) self.hass.services.register('switch', SERVICE_TURN_OFF, log_call) +