diff --git a/.coveragerc b/.coveragerc index f19e37d00a1..849f52c7ff7 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,8 @@ omit = homeassistant/components/device_tracker/snmp.py homeassistant/components/discovery.py homeassistant/components/downloader.py + homeassistant/components/ifttt.py + homeassistant/components/influx.py homeassistant/components/keyboard.py homeassistant/components/light/hue.py homeassistant/components/light/mqtt.py @@ -84,7 +89,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 +102,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/MANIFEST.in b/MANIFEST.in index 8233015e646..d04d86bae58 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include README.md +include README.rst include LICENSE graft homeassistant prune homeassistant/components/frontend/www_static/home-assistant-polymer diff --git a/README.md b/README.md deleted file mode 100644 index e1528e393bd..00000000000 --- a/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Home Assistant [![Build Status](https://travis-ci.org/balloob/home-assistant.svg?branch=master)](https://travis-ci.org/balloob/home-assistant) [![Coverage Status](https://img.shields.io/coveralls/balloob/home-assistant.svg)](https://coveralls.io/r/balloob/home-assistant?branch=master) [![Join the chat at https://gitter.im/balloob/home-assistant](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/balloob/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - -[demo]: https://home-assistant.io/demo/ - -Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at home and offer a platform for automating control. - -To get started: -```bash -python3 -m pip install homeassistant -hass --open-ui -``` - -Check out [the website](https://home-assistant.io) for [a demo][demo], installation instructions, tutorials and documentation. - -[![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png)][demo] - -Examples of devices it can interface it: - - * Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/) and any SNMP capable Linksys WAP/WRT - * [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, [Edimax](http://www.edimax.com/) switches, [Efergy](https://efergy.com) energy monitoring, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors - * [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), [Plex](https://plex.tv/), [Kodi (XBMC)](http://kodi.tv/), iTunes (by way of [itunes-api](https://github.com/maddox/itunes-api)), and Amazon Fire TV (by way of [python-firetv](https://github.com/happyleavesaoc/python-firetv)) - * Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), [RFXtrx](http://www.rfxcom.com/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/) - * Interaction with [IFTTT](https://ifttt.com/) - * Integrate data from the [Bitcoin](https://bitcoin.org) network, meteorological data from [OpenWeatherMap](http://openweathermap.org/) and [Forecast.io](https://forecast.io/), [Transmission](http://www.transmissionbt.com/), or [SABnzbd](http://sabnzbd.org). - * [See full list of supported devices](https://home-assistant.io/components/) - -Built home automation on top of your devices: - - * Keep a precise history of every change to the state of your house - * Turn on the lights when people get home after sun set - * Turn on lights slowly during sun set to compensate for less light - * Turn off all lights and devices when everybody leaves the house - * Offers a [REST API](https://home-assistant.io/developers/api.html) and can interface with MQTT for easy integration with other projects like [OwnTracks](http://owntracks.org/) - * Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), [Slack](https://slack.com/), [Telegram](https://telegram.org/), and [Jabber (XMPP)](http://xmpp.org) - -The system is built modular so support for other devices or actions can be implemented easily. See also the [section on architecture](https://home-assistant.io/developers/architecture.html) and the [section on creating your own components](https://home-assistant.io/developers/creating_components.html). - -If you run into issues while using Home Assistant or during development of a component, check the [Home Assistant help section](https://home-assistant.io/help/) how to reach us. diff --git a/README.rst b/README.rst new file mode 100644 index 00000000000..c66b3670f32 --- /dev/null +++ b/README.rst @@ -0,0 +1,98 @@ +Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/balloob/home-assistant| +=========================================================================================================== + +Home Assistant is a home automation platform running on Python 3. The +goal of Home Assistant is to be able to track and control all devices at +home and offer a platform for automating control. + +To get started: + +.. code:: bash + + python3 -m pip install homeassistant + hass --open-ui + +Check out `the website `__ for `a +demo `__, installation instructions, +tutorials and documentation. + +|screenshot-states| + +Examples of devices it can interface it: + +- Monitoring connected devices to a wireless router: + `OpenWrt `__, + `Tomato `__, + `Netgear `__, + `DD-WRT `__, + `TPLink `__, + `ASUSWRT `__ and any SNMP + capable Linksys WAP/WRT +- `Philips Hue `__ lights, + `WeMo `__ + switches, `Edimax `__ switches, + `Efergy `__ energy monitoring, and + `Tellstick `__ devices and + sensors +- `Google + Chromecasts `__, + `Music Player Daemon `__, `Logitech + Squeezebox `__, + `Plex `__, `Kodi (XBMC) `__, + iTunes (by way of + `itunes-api `__), and Amazon + Fire TV (by way of + `python-firetv `__) +- Support for + `ISY994 `__ + (Insteon and X10 devices), `Z-Wave `__, `Nest + Thermostats `__, + `RFXtrx `__, + `Arduino `__, `Raspberry + Pi `__, and + `Modbus `__ +- Interaction with `IFTTT `__ +- Integrate data from the `Bitcoin `__ network, + meteorological data from + `OpenWeatherMap `__ and + `Forecast.io `__, + `Transmission `__, or + `SABnzbd `__. +- `See full list of supported + devices `__ + +Built home automation on top of your devices: + +- Keep a precise history of every change to the state of your house +- Turn on the lights when people get home after sun set +- Turn on lights slowly during sun set to compensate for less light +- Turn off all lights and devices when everybody leaves the house +- Offers a `REST API `__ + and can interface with MQTT for easy integration with other projects + like `OwnTracks `__ +- Allow sending notifications using + `Instapush `__, `Notify My Android + (NMA) `__, + `PushBullet `__, + `PushOver `__, `Slack `__, + `Telegram `__, and `Jabber + (XMPP) `__ + +The system is built modular so support for other devices or actions can +be implemented easily. See also the `section on +architecture `__ +and the `section on creating your own +components `__. + +If you run into issues while using Home Assistant or during development +of a component, check the `Home Assistant help +section `__ how to reach us. + +.. |Build Status| image:: https://travis-ci.org/balloob/home-assistant.svg?branch=master + :target: https://travis-ci.org/balloob/home-assistant +.. |Coverage Status| image:: https://img.shields.io/coveralls/balloob/home-assistant.svg + :target: https://coveralls.io/r/balloob/home-assistant?branch=master +.. |Join the chat at https://gitter.im/balloob/home-assistant| image:: https://badges.gitter.im/Join%20Chat.svg + :target: https://gitter.im/balloob/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge +.. |screenshot-states| image:: https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png + :target: https://home-assistant.io/demo/ 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 new file mode 100644 index 00000000000..ccfd57aff8c --- /dev/null +++ b/homeassistant/components/binary_sensor/__init__.py @@ -0,0 +1,49 @@ +""" +homeassistant.components.binary_sensor +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Component to interface with binary sensors (sensors which only know two states) +that can be monitored. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/binary_sensor/ +""" +import logging + +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity import Entity +from homeassistant.const import (STATE_ON, STATE_OFF) + +DOMAIN = 'binary_sensor' +SCAN_INTERVAL = 30 + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + + +def setup(hass, config): + """ Track states and offer events for binary sensors. """ + component = EntityComponent( + logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) + + component.setup(config) + + return True + + +# pylint: disable=no-self-use +class BinarySensorDevice(Entity): + """ Represents a binary sensor. """ + + @property + def is_on(self): + """ True if the binary sensor is on. """ + return None + + @property + def state(self): + """ Returns the state of the binary sensor. """ + return STATE_ON if self.is_on else STATE_OFF + + @property + def friendly_state(self): + """ Returns the friendly state of the binary sensor. """ + return None 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 new file mode 100644 index 00000000000..087d7405d9b --- /dev/null +++ b/homeassistant/components/binary_sensor/demo.py @@ -0,0 +1,37 @@ +""" +homeassistant.components.binary_sensor.demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Demo platform that has two fake binary sensors. +""" +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('Basement Floor Wet', False), + DemoBinarySensor('Movement Backyard', True), + ]) + + +class DemoBinarySensor(BinarySensorDevice): + """ A Demo binary sensor. """ + + def __init__(self, name, state): + self._name = name + self._state = state + + @property + def should_poll(self): + """ No polling needed for a demo binary sensor. """ + return False + + @property + def name(self): + """ Returns 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/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 ff5198b7ab1..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'] @@ -58,7 +59,7 @@ MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n' # pylint: disable=too-many-branches def setup(hass, config): - """ Track states and offer events for sensors. """ + """ Track states and offer events for cameras. """ component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, @@ -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', @@ -101,18 +104,16 @@ def setup(hass, config): # pylint: disable=unused-argument def _proxy_camera_mjpeg_stream(handler, path_match, data): - """ Proxies the camera image as an mjpeg stream via the HA server. + """ + Proxies the camera image as an mjpeg stream via the HA server. This function takes still images from the IP camera and turns them into an MJPEG stream. This means that HA can return a live video 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 @@ -130,7 +131,6 @@ def setup(hass, config): # MJPEG_START_HEADER.format() while True: - img_bytes = camera.camera_image() if img_bytes is None: continue @@ -147,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( @@ -163,7 +163,7 @@ def setup(hass, config): class Camera(Entity): - """ The base class for camera components """ + """ The base class for camera components. """ def __init__(self): self.is_streaming = False @@ -171,23 +171,23 @@ class Camera(Entity): @property # pylint: disable=no-self-use def is_recording(self): - """ Returns true if the device is recording """ + """ Returns true if the device is recording. """ return False @property # pylint: disable=no-self-use def brand(self): - """ Should return a string of the camera brand """ + """ Should return a string of the camera brand. """ return None @property # pylint: disable=no-self-use def model(self): - """ Returns string of camera model """ + """ Returns string of camera model. """ return None def camera_image(self): - """ Return bytes of camera image """ + """ Return bytes of camera image. """ raise NotImplementedError() @property diff --git a/homeassistant/components/camera/demo.py b/homeassistant/components/camera/demo.py new file mode 100644 index 00000000000..0ad992db86d --- /dev/null +++ b/homeassistant/components/camera/demo.py @@ -0,0 +1,37 @@ +""" +homeassistant.components.camera.demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Demo platform that has a fake camera. +""" +import os +from homeassistant.components.camera import Camera +import homeassistant.util.dt as dt_util + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Demo camera. """ + add_devices([ + DemoCamera('Demo camera') + ]) + + +class DemoCamera(Camera): + """ A Demo camera. """ + + def __init__(self, name): + super().__init__() + self._name = name + + def camera_image(self): + """ Return a faked still image response. """ + now = dt_util.utcnow() + + image_path = os.path.join(os.path.dirname(__file__), + 'demo_{}.jpg'.format(now.second % 4)) + with open(image_path, 'rb') as file: + return file.read() + + @property + def name(self): + """ Return the name of this device. """ + return self._name 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_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_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/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 b8be51292bf..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__) @@ -40,7 +41,7 @@ class GenericCamera(Camera): self._still_image_url = device_info['still_image_url'] def camera_image(self): - """ Return a still image reponse from the camera. """ + """ Return a still image response from the camera. """ if self._username and self._password: try: response = requests.get( 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 7c873e834bd..8b4b3fcce6c 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -17,8 +17,19 @@ DOMAIN = "demo" DEPENDENCIES = ['conversation', 'introduction', 'zone'] COMPONENTS_WITH_DEMO_PLATFORM = [ - 'device_tracker', 'light', 'media_player', 'notify', 'switch', 'sensor', - 'thermostat'] + 'alarm_control_panel', + 'binary_sensor', + 'camera', + 'device_tracker', + 'light', + 'lock', + 'media_player', + 'notify', + 'rollershutter', + 'sensor', + 'switch', + 'thermostat', +] def setup(hass, config): @@ -42,9 +53,10 @@ def setup(hass, config): bootstrap.setup_component(hass, 'sun') # Setup demo platforms + demo_config = config.copy() for component in COMPONENTS_WITH_DEMO_PLATFORM: - bootstrap.setup_component( - hass, component, {component: {CONF_PLATFORM: 'demo'}}) + demo_config[component] = {CONF_PLATFORM: 'demo'} + bootstrap.setup_component(hass, component, demo_config) # Setup room groups lights = sorted(hass.states.entity_ids('light')) @@ -55,23 +67,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..f1ce746b48e --- /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/' + '92a2f330cbaf601d0618456fdd97e5a8c42c1c47.zip#python-ecobee==0.0.4'] + +_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/mdi_version.py b/homeassistant/components/frontend/mdi_version.py index c9d06a4b300..a8106ecd77e 100644 --- a/homeassistant/components/frontend/mdi_version.py +++ b/homeassistant/components/frontend/mdi_version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by update_mdi script """ -VERSION = "38EF63D0474411E4B3CF842B2B6CFE1B" +VERSION = "7d76081c37634d36af21f5cc1ca79408" diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 1435fe16c5c..042d3e31b11 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 = "b75e3c9ebd3de2dae0912a89499127a9" +VERSION = "8470cd10f28b20eae9022fa4c8f40c1b" 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 5157f4dd5a6..c67acda405e 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 diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 99af263595d..feb776ec89d 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 99af263595dbbf057d26bb266101fa1e386442c6 +Subproject commit feb776ec89d6872dad2203b352cc6d652c46356d 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/mdi.html b/homeassistant/components/frontend/www_static/mdi.html index 42212a3a301..13b003806b3 100644 --- a/homeassistant/components/frontend/www_static/mdi.html +++ b/homeassistant/components/frontend/www_static/mdi.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file 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..f76e7e16b11 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,51 +31,39 @@ 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" CONF_SERVER_PORT = "server_port" CONF_DEVELOPMENT = "development" -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.get(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 +88,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 +145,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 +186,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 +229,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 +286,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 +308,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 +336,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 +418,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/influx.py b/homeassistant/components/influx.py new file mode 100644 index 00000000000..a65126f8102 --- /dev/null +++ b/homeassistant/components/influx.py @@ -0,0 +1,113 @@ +""" +homeassistant.components.influx +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +InfluxDB component which allows you to send data to an Influx database. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/influx/ + +Configuration: + +influx: + host: localhost + port: 8086 + dbname: home_assistant + dbuser: DB_USER + dbuser_password: DB_USER_PASSWORD +""" +import logging + +import homeassistant.util as util +from homeassistant.helpers import validate_config +from homeassistant.const import (EVENT_STATE_CHANGED, STATE_ON, STATE_OFF, + STATE_UNLOCKED, STATE_LOCKED, STATE_UNKNOWN) +from homeassistant.components.sun import (STATE_ABOVE_HORIZON, + STATE_BELOW_HORIZON) + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "influx" +DEPENDENCIES = [] + +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 8086 +DEFAULT_DATABASE = 'home_assistant' + +REQUIREMENTS = ['influxdb==2.10.0'] + +CONF_HOST = 'host' +CONF_PORT = 'port' +CONF_DB_NAME = 'database' +CONF_USERNAME = 'username' +CONF_PASSWORD = 'password' + + +def setup(hass, config): + """ Setup the Influx component. """ + + from influxdb import InfluxDBClient, exceptions + + if not validate_config(config, {DOMAIN: ['host']}, _LOGGER): + return False + + conf = config[DOMAIN] + + host = conf[CONF_HOST] + port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT) + dbname = util.convert(conf.get(CONF_DB_NAME), str, DEFAULT_DATABASE) + username = util.convert(conf.get(CONF_USERNAME), str) + password = util.convert(conf.get(CONF_PASSWORD), str) + + try: + influx = InfluxDBClient(host=host, port=port, username=username, + password=password, database=dbname) + databases = [i['name'] for i in influx.get_list_database()] + except exceptions.InfluxDBClientError: + _LOGGER.error("Database host is not accessible. " + "Please check your entries in the configuration file.") + return False + + if dbname not in databases: + _LOGGER.error("Database %s doesn't exist", dbname) + return False + + def influx_event_listener(event): + """ Listen for new messages on the bus and sends them to Influx. """ + + state = event.data.get('new_state') + + if state is None: + return + + if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON): + _state = 1 + elif state.state in (STATE_OFF, STATE_UNLOCKED, STATE_UNKNOWN, + STATE_BELOW_HORIZON): + _state = 0 + else: + _state = state.state + + measurement = state.attributes.get('unit_of_measurement', state.domain) + + json_body = [ + { + 'measurement': measurement, + 'tags': { + 'domain': state.domain, + 'entity_id': state.object_id, + }, + 'time': event.time_fired, + 'fields': { + 'value': _state, + } + } + ] + + try: + influx.write_points(json_body) + except exceptions.InfluxDBClientError: + _LOGGER.exception('Error saving event to Influx') + + hass.bus.listen(EVENT_STATE_CHANGED, influx_event_listener) + + return True 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 086a004eba2..fae9890c93d 100644 --- a/homeassistant/components/light/blinksticklight.py +++ b/homeassistant/components/light/blinksticklight.py @@ -8,20 +8,19 @@ https://home-assistant.io/components/light.blinksticklight/ """ import logging -from blinkstick import blinkstick - -from homeassistant.components.light import (Light, ATTR_RGB_COLOR) +from homeassistant.components.light import Light, ATTR_RGB_COLOR _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ["blinkstick==1.1.7"] -DEPENDENCIES = [] # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Add device specified by serial number. """ + from blinkstick import blinkstick + stick = blinkstick.find_by_serial(config['serial']) add_devices_callback([BlinkStickLight(stick, config['name'])]) 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 ff52001353b..6132c10a99c 100644 --- a/homeassistant/components/light/rfxtrx.py +++ b/homeassistant/components/light/rfxtrx.py @@ -8,11 +8,15 @@ https://home-assistant.io/components/light.rfxtrx/ """ import logging import homeassistant.components.rfxtrx as rfxtrx -import RFXtrx as rfxtrxmod 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__) @@ -20,14 +24,24 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Setup the RFXtrx platform. """ + import RFXtrx as rfxtrxmod + 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) @@ -53,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, @@ -66,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: @@ -78,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): @@ -93,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 9a22a4dcdc0..48a5a3ed814 100644 --- a/homeassistant/components/light/tellstick.py +++ b/homeassistant/components/light/tellstick.py @@ -6,35 +6,30 @@ Support for Tellstick lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.tellstick/ """ -import logging -# pylint: disable=no-name-in-module, import-error from homeassistant.components.light import Light, ATTR_BRIGHTNESS from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, ATTR_FRIENDLY_NAME) -import tellcore.constants as tellcore_constants -from tellcore.library import DirectCallbackDispatcher REQUIREMENTS = ['tellcore-py==1.1.2'] +SIGNAL_REPETITIONS = 1 # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return Tellstick lights. """ - try: - import tellcore.telldus as telldus - except ImportError: - logging.getLogger(__name__).exception( - "Failed to import tellcore") - return [] + import tellcore.telldus as telldus + from tellcore.library import DirectCallbackDispatcher + 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 """ @@ -58,17 +53,22 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class TellstickLight(Light): """ Represents a Tellstick light. """ - last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | - tellcore_constants.TELLSTICK_TURNOFF | - tellcore_constants.TELLSTICK_DIM | - tellcore_constants.TELLSTICK_UP | - tellcore_constants.TELLSTICK_DOWN) - 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 | + tellcore_constants.TELLSTICK_TURNOFF | + tellcore_constants.TELLSTICK_DIM | + tellcore_constants.TELLSTICK_UP | + tellcore_constants.TELLSTICK_DOWN) + self.update() + @property def name(self): """ Returns the name of the switch if any. """ @@ -86,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() @@ -99,11 +100,14 @@ 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): """ Update state of the light. """ + import tellcore.constants as tellcore_constants + last_command = self.tellstick_device.last_sent_command( self.last_sent_command_mask) 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 f1cd6f57fc0..02664ed896c 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -6,15 +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 -from openzwave.network import ZWaveNetwork -from pydispatch import dispatcher - -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): @@ -51,6 +49,9 @@ class ZwaveDimmer(Light): """ Provides a Z-Wave dimmer. """ # pylint: disable=too-many-arguments def __init__(self, value): + from openzwave.network import ZWaveNetwork + from pydispatch import dispatcher + self._value = value self._node = value.node 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 9e9b941394e..941a78ac709 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -47,21 +47,23 @@ class PushBulletNotificationService(BaseNotificationService): self.refresh() def refresh(self): - ''' - Refresh devices, contacts, channels, etc + """ + 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. + """ self.pushbullet.refresh() self.pbtargets = { - 'device': - {tgt.nickname: tgt for tgt in self.pushbullet.devices}, - 'contact': - {tgt.email: tgt for tgt in self.pushbullet.contacts}, - 'channel': - {tgt.channel_tag: tgt for tgt in self.pushbullet.channels}, + 'device': { + tgt.nickname.lower(): tgt for tgt in self.pushbullet.devices}, + 'channel': { + tgt.channel_tag.lower(): tgt for + tgt in self.pushbullet.channels}, } def send_message(self, message=None, **kwargs): @@ -69,6 +71,8 @@ class PushBulletNotificationService(BaseNotificationService): Send a message to a specified target. 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. """ targets = kwargs.get(ATTR_TARGET) title = kwargs.get(ATTR_TITLE) @@ -86,24 +90,27 @@ class PushBulletNotificationService(BaseNotificationService): # Main loop, Process all targets specified for target in targets: - - # Allow for untargeted push, combined with other types - if target in ['device', 'device/']: - self.pushbullet.push_note(title, message) - _LOGGER.info('Sent notification to self') - continue - try: ttype, tname = target.split('/', 1) except ValueError: _LOGGER.error('Invalid target syntax: %s', target) continue + # Target is email, send directly, don't use a target object + # 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 email %s', tname) + continue + # Refresh if name not found. While awaiting periodic refresh # solution in component, poor mans refresh ;) if ttype not in self.pbtargets: _LOGGER.error('Invalid target syntax: %s', target) continue + + tname = tname.lower() + if tname not in self.pbtargets[ttype] and not refreshed: self.refresh() refreshed = True @@ -112,10 +119,10 @@ class PushBulletNotificationService(BaseNotificationService): # name. Dict pbtargets has all *actual* targets. try: 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) + _LOGGER.error('No such target: %s/%s', ttype, tname) continue except self.pushbullet.errors.PushError: - _LOGGER.error('Notify failed to: %s.%s', ttype, tname) + _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/demo.py b/homeassistant/components/rollershutter/demo.py new file mode 100644 index 00000000000..a57bf4ddeec --- /dev/null +++ b/homeassistant/components/rollershutter/demo.py @@ -0,0 +1,78 @@ +""" +homeassistant.components.rollershutter.demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Demo platform for rollorshutter component. +""" +from homeassistant.const import EVENT_TIME_CHANGED +from homeassistant.helpers.event import track_utc_time_change +from homeassistant.components.rollershutter import RollershutterDevice + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Demo binary sensors. """ + add_devices([ + DemoRollershutter(hass, 'Kitchen Window', 0), + DemoRollershutter(hass, 'Living Room Window', 100), + ]) + + +class DemoRollershutter(RollershutterDevice): + """ Represents a rollershutter within Home Assistant. """ + # pylint: disable=no-self-use + + def __init__(self, hass, name, position): + self.hass = hass + self._name = name + self._position = position + self._moving_up = True + self._listener = None + + @property + def name(self): + return self._name + + @property + def should_poll(self): + return False + + @property + def current_position(self): + return self._position + + def move_up(self, **kwargs): + """ Move the rollershutter down. """ + if self._position == 0: + return + + self._listen() + self._moving_up = True + + def move_down(self, **kwargs): + """ Move the rollershutter up. """ + if self._position == 100: + return + + self._listen() + self._moving_up = False + + def stop(self, **kwargs): + """ Stop the rollershutter. """ + if self._listener is not None: + self.hass.bus.remove_listener(EVENT_TIME_CHANGED, self._listener) + self._listener = None + + def _listen(self): + if self._listener is None: + self._listener = track_utc_time_change(self.hass, + self._time_changed) + + def _time_changed(self, now): + if self._moving_up: + self._position -= 10 + else: + self._position += 10 + + if self._position % 100 == 0: + self.stop() + + self.update_ha_state() 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..3e13db66699 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]) @@ -200,7 +201,7 @@ class Script(ToggleEntity): self._last_action) domain, service = split_entity_id(conf_service) data = action.get(CONF_SERVICE_DATA, {}) - self.hass.services.call(domain, service, data) + self.hass.services.call(domain, service, data, True) def _fire_event(self, action): """ Fires an event. """ 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 176081336df..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 @@ -116,7 +117,11 @@ class GlancesSensor(Entity): elif self.type == 'disk_use': return round(value['fs'][0]['used'] / 1024**3, 1) elif self.type == 'disk_free': - return round(value['fs'][0]['free'] / 1024**3, 1) + try: + return round(value['fs'][0]['free'] / 1024**3, 1) + except KeyError: + return round((value['fs'][0]['size'] - + value['fs'][0]['used']) / 1024**3, 1) elif self.type == 'memory_use_percent': return value['mem']['percent'] elif self.type == 'memory_use': 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/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index 0118c30ceb6..c67810c86eb 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -11,7 +11,6 @@ from collections import OrderedDict from homeassistant.const import (TEMP_CELCIUS) from homeassistant.helpers.entity import Entity -from RFXtrx import SensorEvent import homeassistant.components.rfxtrx as rfxtrx from homeassistant.util import slugify @@ -28,6 +27,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Setup the RFXtrx platform. """ + from RFXtrx import SensorEvent def sensor_update(event): """ Callback for sensor updates from the RFXtrx gateway. """ 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/tellstick.py b/homeassistant/components/sensor/tellstick.py index b6eb42f6dbb..c6993de462d 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/sensor/tellstick.py @@ -9,9 +9,6 @@ https://home-assistant.io/components/sensor.tellstick/ import logging from collections import namedtuple -import tellcore.telldus as telldus -import tellcore.constants as tellcore_constants - from homeassistant.const import TEMP_CELCIUS from homeassistant.helpers.entity import Entity import homeassistant.util as util @@ -24,6 +21,9 @@ REQUIREMENTS = ['tellcore-py==1.1.2'] # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up Tellstick sensors. """ + import tellcore.telldus as telldus + import tellcore.constants as tellcore_constants + sensor_value_descriptions = { tellcore_constants.TELLSTICK_TEMPERATURE: DatatypeDescription( diff --git a/homeassistant/components/sensor/transmission.py b/homeassistant/components/sensor/transmission.py index c4a40e64470..62afdd39bf4 100644 --- a/homeassistant/components/sensor/transmission.py +++ b/homeassistant/components/sensor/transmission.py @@ -6,18 +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 -# pylint: disable=no-name-in-module, import-error -import transmissionrpc - -from transmissionrpc.error import TransmissionError - 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', ''], @@ -33,6 +28,9 @@ _THROTTLED_REFRESH = None # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Transmission sensors. """ + import transmissionrpc + from transmissionrpc.error import TransmissionError + host = config.get(CONF_HOST) username = config.get(CONF_USERNAME, None) password = config.get(CONF_PASSWORD, None) @@ -97,6 +95,8 @@ class TransmissionSensor(Entity): def refresh_transmission_data(self): """ Calls the throttled Transmission refresh method. """ + from transmissionrpc.error import TransmissionError + if _THROTTLED_REFRESH is not None: try: _THROTTLED_REFRESH() diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 00ad9336705..7fb72fd91b7 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -15,9 +15,9 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, TEMP_CELCIUS, TEMP_FAHRENHEIT) -REQUIREMENTS = ['https://github.com/balloob/home-assistant-vera-api/archive/' - 'a8f823066ead6c7da6fb5e7abaf16fef62e63364.zip' - '#python-vera==0.1'] +REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' + 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' + '#python-vera==0.1.1'] _LOGGER = logging.getLogger(__name__) @@ -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 0a9b3e8290f..1ed831b286d 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -6,11 +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 -from openzwave.network import ZWaveNetwork -from pydispatch import dispatcher 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 @@ -79,6 +79,9 @@ class ZWaveSensor(Entity): """ Represents a Z-Wave sensor. """ def __init__(self, sensor_value): + from openzwave.network import ZWaveNetwork + from pydispatch import dispatcher + self._value = sensor_value self._node = sensor_value.node 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 ec74a83dbc2..c85aae4a7f0 100644 --- a/homeassistant/components/switch/hikvisioncam.py +++ b/homeassistant/components/switch/hikvisioncam.py @@ -6,16 +6,11 @@ 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 -try: - import hikvision.api - from hikvision.error import HikvisionError, MissingParamError -except ImportError: - hikvision.api = None +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'] @@ -25,6 +20,8 @@ REQUIREMENTS = ['hikvision==0.4'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Setup Hikvision camera. """ + import hikvision.api + from hikvision.error import HikvisionError, MissingParamError host = config.get(CONF_HOST, None) port = config.get('port', "80") @@ -32,13 +29,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): username = config.get(CONF_USERNAME, "admin") password = config.get(CONF_PASSWORD, "12345") - if hikvision.api is None: - _LOGGING.error(( - "Failed to import hikvision. Did you maybe not install the " - "'hikvision' dependency?")) - - return False - try: hikvision_cam = hikvision.api.CreateDevice( host, port=port, username=username, 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 04864e13fdd..c636a7f3f55 100644 --- a/homeassistant/components/switch/orvibo.py +++ b/homeassistant/components/switch/orvibo.py @@ -10,33 +10,44 @@ import logging from homeassistant.components.switch import SwitchDevice -from orvibo.s20 import S20, S20Exception - DEFAULT_NAME = "Orvibo S20 Switch" -REQUIREMENTS = ['orvibo==1.0.0'] +REQUIREMENTS = ['orvibo==1.0.1'] _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return S20 switches. """ - 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") + from orvibo.s20 import S20, S20Exception + + 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): """ Represents an S20 switch. """ def __init__(self, name, s20): + from orvibo.s20 import S20Exception + self._name = name self._s20 = s20 self._state = False + self._exc = S20Exception @property def should_poll(self): @@ -57,19 +68,19 @@ class S20Switch(SwitchDevice): """ Update device state. """ try: self._state = self._s20.on - except S20Exception: + except self._exc: _LOGGER.exception("Error while fetching S20 state") def turn_on(self, **kwargs): """ Turn the device on. """ try: self._s20.on = True - except S20Exception: + except self._exc: _LOGGER.exception("Error while turning on S20") def turn_off(self, **kwargs): """ Turn the device off. """ try: self._s20.on = False - except S20Exception: + except self._exc: _LOGGER.exception("Error while turning off S20") diff --git a/homeassistant/components/switch/rfxtrx.py b/homeassistant/components/switch/rfxtrx.py index 83100598245..69e08e7d129 100644 --- a/homeassistant/components/switch/rfxtrx.py +++ b/homeassistant/components/switch/rfxtrx.py @@ -8,11 +8,15 @@ https://home-assistant.io/components/switch.rfxtrx/ """ import logging import homeassistant.components.rfxtrx as rfxtrx -from RFXtrx import LightingDevice 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__) @@ -20,6 +24,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Setup the RFXtrx platform. """ + 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/tellstick.py b/homeassistant/components/switch/tellstick.py index 2966673520f..61edbed0af4 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/switch/tellstick.py @@ -11,8 +11,6 @@ import logging from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, ATTR_FRIENDLY_NAME) from homeassistant.helpers.entity import ToggleEntity -import tellcore.constants as tellcore_constants -from tellcore.library import DirectCallbackDispatcher SIGNAL_REPETITIONS = 1 REQUIREMENTS = ['tellcore-py==1.1.2'] @@ -22,11 +20,9 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return Tellstick switches. """ - try: - import tellcore.telldus as telldus - except ImportError: - _LOGGER.exception("Failed to import tellcore") - return + import tellcore.telldus as telldus + import tellcore.constants as tellcore_constants + from tellcore.library import DirectCallbackDispatcher core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher()) @@ -62,14 +58,17 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class TellstickSwitchDevice(ToggleEntity): """ Represents a Tellstick switch. """ - last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | - tellcore_constants.TELLSTICK_TURNOFF) 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.last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | + tellcore_constants.TELLSTICK_TURNOFF) + @property def should_poll(self): """ Tells Home Assistant not to poll this entity. """ @@ -88,6 +87,8 @@ class TellstickSwitchDevice(ToggleEntity): @property def is_on(self): """ True if switch is on. """ + import tellcore.constants as tellcore_constants + last_command = self.tellstick_device.last_sent_command( self.last_sent_command_mask) diff --git a/homeassistant/components/switch/transmission.py b/homeassistant/components/switch/transmission.py index bb4f6616975..1f0da4a00e0 100644 --- a/homeassistant/components/switch/transmission.py +++ b/homeassistant/components/switch/transmission.py @@ -6,15 +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 -# pylint: disable=no-name-in-module, import-error -import transmissionrpc -from transmissionrpc.error import TransmissionError 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'] @@ -22,6 +19,9 @@ REQUIREMENTS = ['transmissionrpc==0.11'] # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Sets up the transmission sensor. """ + import transmissionrpc + from transmissionrpc.error import TransmissionError + host = config.get(CONF_HOST) username = config.get(CONF_USERNAME, None) password = config.get(CONF_PASSWORD, None) 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 7d86605c646..f4777340445 100644 --- a/homeassistant/components/switch/zwave.py +++ b/homeassistant/components/switch/zwave.py @@ -4,10 +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 -from openzwave.network import ZWaveNetwork -from pydispatch import dispatcher - import homeassistant.components.zwave as zwave from homeassistant.components.switch import SwitchDevice @@ -36,11 +34,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ZwaveSwitch(SwitchDevice): """ Provides a zwave switch. """ def __init__(self, value): + from openzwave.network import ZWaveNetwork + from pydispatch import dispatcher + self._value = value self._node = value.node self._state = value.data - dispatcher.connect( self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) 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 06f471cd692..b52e430600a 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -6,6 +6,9 @@ Connects Home Assistant to a Z-Wave network. For more details about this component, please refer to the documentation at https://home-assistant.io/components/zwave/ """ +import sys +import os.path + from pprint import pprint from homeassistant import bootstrap @@ -14,13 +17,14 @@ 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" DEFAULT_CONF_USB_STICK_PATH = "/zwaveusbstick" CONF_DEBUG = "debug" CONF_POLLING_INTERVAL = "polling_interval" +DEFAULT_ZWAVE_CONFIG_PATH = os.path.join(sys.prefix, 'share', + 'python-openzwave', 'config') DISCOVER_SENSORS = "zwave.sensors" DISCOVER_SWITCHES = "zwave.switch" @@ -78,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()} @@ -90,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(): @@ -120,7 +124,9 @@ def setup(hass, config): # Setup options options = ZWaveOption( config[DOMAIN].get(CONF_USB_STICK_PATH, DEFAULT_CONF_USB_STICK_PATH), - user_path=hass.config.config_dir) + user_path=hass.config.config_dir, + config_path=config[DOMAIN].get('config_path', + DEFAULT_ZWAVE_CONFIG_PATH),) options.set_console_output(use_debug) options.lock() 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 af95153f217..eeb57050d90 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """ Constants used by Home Assistant components. """ -__version__ = "0.8.0" +__version__ = "0.9.0" # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' @@ -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/core.py b/homeassistant/core.py index d7ec3abe458..8ea55c653e3 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -25,7 +25,7 @@ from homeassistant.const import ( from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError) import homeassistant.util as util -import homeassistant.util.dt as date_util +import homeassistant.util.dt as dt_util import homeassistant.util.location as location import homeassistant.helpers.temperature as temp_helper from homeassistant.config import get_default_config_dir @@ -196,8 +196,8 @@ class Event(object): self.event_type = event_type self.data = data or {} self.origin = origin - self.time_fired = date_util.strip_microseconds( - time_fired or date_util.utcnow()) + self.time_fired = dt_util.strip_microseconds( + time_fired or dt_util.utcnow()) def as_dict(self): """ Returns a dict representation of this Event. """ @@ -205,7 +205,7 @@ class Event(object): 'event_type': self.event_type, 'data': dict(self.data), 'origin': str(self.origin), - 'time_fired': date_util.datetime_to_str(self.time_fired), + 'time_fired': dt_util.datetime_to_str(self.time_fired), } def __repr__(self): @@ -351,14 +351,14 @@ class State(object): self.entity_id = entity_id.lower() self.state = state self.attributes = attributes or {} - self.last_updated = date_util.strip_microseconds( - last_updated or date_util.utcnow()) + self.last_updated = dt_util.strip_microseconds( + last_updated or dt_util.utcnow()) # Strip microsecond from last_changed else we cannot guarantee # state == State.from_dict(state.as_dict()) # This behavior occurs because to_dict uses datetime_to_str # which does not preserve microseconds - self.last_changed = date_util.strip_microseconds( + self.last_changed = dt_util.strip_microseconds( last_changed or self.last_updated) @property @@ -381,7 +381,8 @@ class State(object): def copy(self): """ Creates a copy of itself. """ return State(self.entity_id, self.state, - dict(self.attributes), self.last_changed) + dict(self.attributes), self.last_changed, + self.last_updated) def as_dict(self): """ Converts State to a dict to be used within JSON. @@ -390,8 +391,8 @@ class State(object): return {'entity_id': self.entity_id, 'state': self.state, 'attributes': self.attributes, - 'last_changed': date_util.datetime_to_str(self.last_changed), - 'last_updated': date_util.datetime_to_str(self.last_updated)} + 'last_changed': dt_util.datetime_to_str(self.last_changed), + 'last_updated': dt_util.datetime_to_str(self.last_updated)} @classmethod def from_dict(cls, json_dict): @@ -406,12 +407,12 @@ class State(object): last_changed = json_dict.get('last_changed') if last_changed: - last_changed = date_util.str_to_datetime(last_changed) + last_changed = dt_util.str_to_datetime(last_changed) last_updated = json_dict.get('last_updated') if last_updated: - last_updated = date_util.str_to_datetime(last_updated) + last_updated = dt_util.str_to_datetime(last_updated) return cls(json_dict['entity_id'], json_dict['state'], json_dict.get('attributes'), last_changed, last_updated) @@ -428,7 +429,7 @@ class State(object): return "".format( self.entity_id, self.state, attr, - date_util.datetime_to_local_str(self.last_changed)) + dt_util.datetime_to_local_str(self.last_changed)) class StateMachine(object): @@ -732,7 +733,7 @@ class Config(object): def as_dict(self): """ Converts config to a dictionary. """ - time_zone = self.time_zone or date_util.UTC + time_zone = self.time_zone or dt_util.UTC return { 'latitude': self.latitude, @@ -766,7 +767,7 @@ def create_timer(hass, interval=TIMER_INTERVAL): last_fired_on_second = -1 - calc_now = date_util.utcnow + calc_now = dt_util.utcnow while not stop_event.isSet(): now = calc_now() @@ -832,6 +833,6 @@ def create_worker_pool(worker_count=None): for start, job in current_jobs: _LOGGER.warning("WorkerPool:Current job from %s: %s", - date_util.datetime_to_local_str(start), job) + dt_util.datetime_to_local_str(start), job) return util.ThreadPool(job_handler, worker_count, busy_callback) 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 ce6cbfabc96..965b615f95f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,161 +1,175 @@ -# Required for Home Assistant core +# Home Assistant core requests>=2,<3 pyyaml>=3.11,<4 pytz>=2015.4 pip>=7.0.0 vincenty==0.1.3 -# Optional, needed for specific components - -# Sun (sun) -astral==0.8.1 - -# Philips Hue (lights.hue) -phue==0.8 - -# Limitlessled/Easybulb/Milight (lights.limitlessled) -ledcontroller==1.1.0 - -# Chromecast (media_player.cast) -pychromecast==0.6.12 - -# Keyboard (keyboard) -pyuserinput==0.1.9 - -# Tellstick (*.tellstick) -tellcore-py==1.1.2 - -# Nmap (device_tracker.nmap) -python-nmap==0.4.3 - -# PushBullet (notify.pushbullet) -pushbullet.py==0.9.0 - -# Nest Thermostat (thermostat.nest) -python-nest==2.6.0 - -# Z-Wave (*.zwave) -pydispatcher==2.0.5 - -# ISY994 (isy994) -PyISY==1.0.5 - -# PSutil (sensor.systemmonitor) -psutil==3.2.2 - -# Pushover (notify.pushover) -python-pushover==0.2 - -# Transmission Torrent Client (*.transmission) -transmissionrpc==0.11 - -# OpenWeatherMap (sensor.openweathermap) -pyowm==2.2.1 - -# XMPP (notify.xmpp) -sleekxmpp==1.3.1 -dnspython3==1.12.0 - -# Blockchain (sensor.bitcoin) -blockchain==1.1.2 - -# Music Player Daemon (media_player.mpd) -python-mpd2==0.5.4 - -# Hikvision (switch.hikvisioncam) -hikvision==0.4 - -# Console log coloring -colorlog==2.6.0 - -# JSON-RPC interface (media_player.kodi) -jsonrpc-requests==0.1 - -# Forecast.io (sensor.forecast) -python-forecastio==1.3.3 - -# Firmata (*.arduino) +# homeassistant.components.arduino PyMata==2.07a -# Rfxtrx (rfxtrx) -https://github.com/Danielhiversen/pyRFXtrx/archive/ec7a1aaddf8270db6e5da1c13d58c1547effd7cf.zip#RFXtrx==0.15 - -# Mysensors (sensor.mysensors) -https://github.com/theolind/pymysensors/archive/d4b809c2167650691058d1e29bfd2c4b1792b4b0.zip#pymysensors==0.3 - -# Netgear (device_tracker.netgear) +# homeassistant.components.device_tracker.netgear pynetgear==0.3 -# Netdisco (discovery) -netdisco==0.5.1 +# homeassistant.components.device_tracker.nmap_tracker +python-nmap==0.4.3 -# Wemo (switch.wemo) -pywemo==0.3.2 - -# Wink (*.wink) -https://github.com/balloob/python-wink/archive/9eb39eaba0717922815e673ad1114c685839d890.zip#python-wink==0.1.1 - -# Slack notifier (notify.slack) -slacker==0.6.8 - -# Temper sensors (sensor.temper) -https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc704537dd2d09b.zip#temperusb==1.2.3 - -# PyEdimax (switch.edimax) -https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1 - -# RPI-GPIO platform (*.rpi_gpio) -# Uncomment for Raspberry Pi -# RPi.GPIO==0.5.11 - -# Adafruit temperature/humidity sensor (sensor.dht) -# Uncomment on a Raspberry Pi / Beaglebone -# http://github.com/mala-zaba/Adafruit_Python_DHT/archive/4101340de8d2457dd194bca1e8d11cbfc237e919.zip#Adafruit_DHT==1.1.0 - -# PAHO MQTT (mqtt) -paho-mqtt==1.1 - -# PyModbus (modbus) -https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0 - -# Verisure (verisure) -https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6 - -# IFTTT Maker Channel (ifttt) -pyfttt==0.3 - -# SABnzbd (sensor.sabnzbd) -https://github.com/balloob/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1 - -# Vera (*.vera) -https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip#python-vera==0.1.1 - -# Sonos (media_player.sonos) -SoCo==0.11.1 - -# PlexAPI (media_player.plex) -plexapi==1.1.0 - -# SNMP (device_tracker.snmp) +# homeassistant.components.device_tracker.snmp pysnmp==4.2.5 -# Blinkstick (light.blinksticklight) +# homeassistant.components.discovery +netdisco==0.5.2 + +# homeassistant.components.ecobee +https://github.com/nkgilley/python-ecobee-api/archive/92a2f330cbaf601d0618456fdd97e5a8c42c1c47.zip#python-ecobee==0.0.4 + +# homeassistant.components.ifttt +pyfttt==0.3 + +# homeassistant.components.influx +influxdb==2.10.0 + +# homeassistant.components.isy994 +PyISY==1.0.5 + +# homeassistant.components.keyboard +pyuserinput==0.1.9 + +# homeassistant.components.light.blinksticklight blinkstick==1.1.7 -# Telegram (notify.telegram) -python-telegram-bot==2.8.7 +# homeassistant.components.light.hue +phue==0.8 -# CPUinfo (sensor.cpuinfo) -py-cpuinfo==0.1.6 +# homeassistant.components.light.limitlessled +limitlessled==1.0.0 -# Radio Thermostat (thermostat.radiotherm) -radiotherm==1.2 +# homeassistant.components.light.tellstick +# homeassistant.components.sensor.tellstick +# homeassistant.components.switch.tellstick +tellcore-py==1.1.2 -# Honeywell Evo Home Client (thermostat.honeywell) -evohomeclient==0.2.3 +# homeassistant.components.light.vera +# homeassistant.components.sensor.vera +# homeassistant.components.switch.vera +https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip#python-vera==0.1.1 -# Pushetta (notify.pushetta) +# 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/42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip#python-wink==0.2 + +# homeassistant.components.media_player.cast +pychromecast==0.6.12 + +# homeassistant.components.media_player.kodi +jsonrpc-requests==0.1 + +# homeassistant.components.media_player.mpd +python-mpd2==0.5.4 + +# homeassistant.components.media_player.plex +plexapi==1.1.0 + +# homeassistant.components.media_player.sonos +SoCo==0.11.1 + +# homeassistant.components.modbus +https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0 + +# homeassistant.components.mqtt +paho-mqtt==1.1 + +# homeassistant.components.mqtt +jsonpath-rw==1.4.0 + +# homeassistant.components.notify.pushbullet +pushbullet.py==0.9.0 + +# homeassistant.components.notify.pushetta pushetta==1.0.15 -# Orvibo S10 -orvibo==1.0.0 +# homeassistant.components.notify.pushover +python-pushover==0.2 + +# homeassistant.components.notify.slack +slacker==0.6.8 + +# homeassistant.components.notify.telegram +python-telegram-bot==2.8.7 + +# homeassistant.components.notify.xmpp +sleekxmpp==1.3.1 + +# homeassistant.components.notify.xmpp +dnspython3==1.12.0 + +# homeassistant.components.rfxtrx +https://github.com/Danielhiversen/pyRFXtrx/archive/0.2.zip#RFXtrx==0.2 + +# homeassistant.components.sensor.bitcoin +blockchain==1.1.2 + +# homeassistant.components.sensor.cpuspeed +py-cpuinfo==0.1.6 + +# homeassistant.components.sensor.dht +# http://github.com/mala-zaba/Adafruit_Python_DHT/archive/4101340de8d2457dd194bca1e8d11cbfc237e919.zip#Adafruit_DHT==1.1.0 + +# homeassistant.components.sensor.forecast +python-forecastio==1.3.3 + +# homeassistant.components.sensor.mysensors +https://github.com/theolind/pymysensors/archive/d4b809c2167650691058d1e29bfd2c4b1792b4b0.zip#pymysensors==0.3 + +# homeassistant.components.sensor.openweathermap +pyowm==2.2.1 + +# homeassistant.components.sensor.rpi_gpio +# homeassistant.components.switch.rpi_gpio +# RPi.GPIO==0.5.11 + +# homeassistant.components.sensor.sabnzbd +https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1 + +# homeassistant.components.sensor.systemmonitor +psutil==3.2.2 + +# homeassistant.components.sensor.temper +https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc704537dd2d09b.zip#temperusb==1.2.3 + +# homeassistant.components.sensor.transmission +# homeassistant.components.switch.transmission +transmissionrpc==0.11 + +# homeassistant.components.sun +astral==0.8.1 + +# homeassistant.components.switch.edimax +https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1 + +# homeassistant.components.switch.hikvisioncam +hikvision==0.4 + +# homeassistant.components.switch.orvibo +orvibo==1.0.1 + +# homeassistant.components.switch.wemo +pywemo==0.3.3 + +# homeassistant.components.thermostat.honeywell +evohomeclient==0.2.4 + +# homeassistant.components.thermostat.nest +python-nest==2.6.0 + +# homeassistant.components.thermostat.radiotherm +radiotherm==1.2 + +# homeassistant.components.verisure +https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6 + +# homeassistant.components.zwave +pydispatcher==2.0.5 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py new file mode 100755 index 00000000000..730886075ec --- /dev/null +++ b/script/gen_requirements_all.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +Generate an updated requirements_all.txt +""" + +from collections import OrderedDict +import importlib +import os +import pkgutil +import re + +COMMENT_REQUIREMENTS = [ + 'RPi.GPIO', + 'Adafruit_Python_DHT' +] + + +def explore_module(package, explore_children): + """ Explore the modules. """ + module = importlib.import_module(package) + + found = [] + + if not hasattr(module, '__path__'): + return found + + for _, name, ispkg in pkgutil.iter_modules(module.__path__, package + '.'): + found.append(name) + + if explore_children: + found.extend(explore_module(name, False)) + + return found + + +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) + + +def comment_requirement(req): + """ Some requirements don't install on all systems. """ + return any(ign in req for ign in COMMENT_REQUIREMENTS) + + +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) + except ImportError: + errors.append(package) + continue + + if not getattr(module, 'REQUIREMENTS', None): + continue + + for req in module.REQUIREMENTS: + reqs.setdefault(req, []).append(package) + + if errors: + print("Found errors") + print('\n'.join(errors)) + 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)): + output.append('\n# {}'.format(req)) + + if comment_requirement(pkg): + output.append('\n# {}\n'.format(pkg)) + else: + 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/script/update_mdi.py b/script/update_mdi.py index f7899be3964..c61a3808b39 100755 --- a/script/update_mdi.py +++ b/script/update_mdi.py @@ -2,7 +2,7 @@ """ Downloads the latest Polymer v1 iconset version for materialdesignicons.com """ - +import hashlib import os import re import requests @@ -75,17 +75,18 @@ def main(): print("materialdesignicons.com icon updater") local_version = get_local_version() - remote_version, remote_url = get_remote_version() - print('Local version:', local_version) - print('Remote version:', remote_version) + # The remote version is not reliable. + _, remote_url = get_remote_version() - if local_version == remote_version: + source = clean_component(requests.get(remote_url).text) + new_version = hashlib.md5(source.encode('utf-8')).hexdigest() + + if local_version == new_version: print('Already on the latest version.') sys.exit() - write_component(remote_version, - clean_component(requests.get(remote_url).text)) + write_component(new_version, source) print('Updated to latest version') if __name__ == '__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) + diff --git a/tests/test_core.py b/tests/test_core.py index bb59aac03fa..fee46fe2dd4 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -268,7 +268,15 @@ class TestState(unittest.TestCase): def test_copy(self): state = ha.State('domain.hello', 'world', {'some': 'attr'}) - self.assertEqual(state, state.copy()) + # Patch dt_util.utcnow() so we know last_updated got copied too + with patch('homeassistant.core.dt_util.utcnow', + return_value=dt_util.utcnow() + timedelta(seconds=10)): + copy = state.copy() + self.assertEqual(state.entity_id, copy.entity_id) + self.assertEqual(state.state, copy.state) + self.assertEqual(state.attributes, copy.attributes) + self.assertEqual(state.last_changed, copy.last_changed) + self.assertEqual(state.last_updated, copy.last_updated) def test_dict_conversion(self): state = ha.State('domain.hello', 'world', {'some': 'attr'})