diff --git a/.coveragerc b/.coveragerc index 2d40ca8b7b3..ae489fd5926 100644 --- a/.coveragerc +++ b/.coveragerc @@ -97,6 +97,9 @@ omit = homeassistant/components/*/thinkingcleaner.py + homeassistant/components/tradfri.py + homeassistant/components/*/tradfri.py + homeassistant/components/twilio.py homeassistant/components/notify/twilio_sms.py homeassistant/components/notify/twilio_call.py diff --git a/Dockerfile b/Dockerfile index cd219b24084..579229d154a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ MAINTAINER Paulus Schoutsen #ENV INSTALL_OPENZWAVE no #ENV INSTALL_LIBCEC no #ENV INSTALL_PHANTOMJS no +#ENV INSTALL_COAP_CLIENT no VOLUME /config diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index d912f9914b5..3376815b9d5 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -180,7 +180,7 @@ class Configurator(object): # field validation goes here? - callback(call.data.get(ATTR_FIELDS, {})) + self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {})) def _generate_unique_id(self): """Generate a unique configurator ID.""" diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 08190ed1002..7a18148e517 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -28,11 +28,13 @@ SCAN_INTERVAL = timedelta(seconds=300) SERVICE_NETGEAR = 'netgear_router' SERVICE_WEMO = 'belkin_wemo' SERVICE_HASS_IOS_APP = 'hass_ios' +SERVICE_IKEA_TRADFRI = 'ikea_tradfri' SERVICE_HANDLERS = { SERVICE_HASS_IOS_APP: ('ios', None), SERVICE_NETGEAR: ('device_tracker', None), SERVICE_WEMO: ('wemo', None), + SERVICE_IKEA_TRADFRI: ('tradfri', None), 'philips_hue': ('light', 'hue'), 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index 4a2641dc338..277824f4c64 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -1,52 +1,29 @@ """Support for the IKEA Tradfri platform.""" -import logging - - -import voluptuous as vol - -import homeassistant.util.color as color_util +from homeassistant.components.tradfri import KEY_GATEWAY from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, Light, - PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR) -from homeassistant.const import CONF_API_KEY, CONF_HOST -import homeassistant.helpers.config_validation as cv + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, ATTR_RGB_COLOR, + SUPPORT_RGB_COLOR, PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA) +from homeassistant.util import color as color_util +DEPENDENCIES = ['tradfri'] SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) - -# https://github.com/ggravlingen/pytradfri -REQUIREMENTS = ['pytradfri==0.4'] - -_LOGGER = logging.getLogger(__name__) - -# Validation of the user's configuration -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_API_KEY): cv.string, -}) +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the IKEA Tradfri Light platform.""" - import pytradfri + if discovery_info is None: + return - # Assign configuration variables. - host = config.get(CONF_HOST) - securitycode = config.get(CONF_API_KEY) - - api = pytradfri.coap_cli.api_factory(host, securitycode) - - gateway = pytradfri.gateway.Gateway(api) + gateway_id = discovery_info['gateway'] + gateway = hass.data[KEY_GATEWAY][gateway_id] devices = gateway.get_devices() lights = [dev for dev in devices if dev.has_light_control] - - _LOGGER.debug("IKEA Tradfri Hub | init | Initialization Process Complete") - - add_devices(IKEATradfri(light) for light in lights) - _LOGGER.debug("IKEA Tradfri Hub | get_lights | All Lights Loaded") + add_devices(Tradfri(light) for light in lights) -class IKEATradfri(Light): +class Tradfri(Light): """The platform class required by hass.""" def __init__(self, light): @@ -57,8 +34,6 @@ class IKEATradfri(Light): self._light_control = light.light_control self._light_data = light.light_control.lights[0] self._name = light.name - - self._brightness = None self._rgb_color = None @property @@ -98,17 +73,17 @@ class IKEATradfri(Light): for ATTR_RGB_COLOR, this also supports Philips Hue bulbs. """ if ATTR_BRIGHTNESS in kwargs: - self._light.light_control.set_dimmer(kwargs.get(ATTR_BRIGHTNESS)) + self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS]) + else: + self._light_control.set_state(True) + if ATTR_RGB_COLOR in kwargs and self._light_data.hex_color is not None: self._light.light_control.set_hex_color( color_util.color_rgb_to_hex(*kwargs[ATTR_RGB_COLOR])) - else: - self._light.light_control.set_state(True) def update(self): """Fetch new state data for this light.""" self._light.update() - self._brightness = self._light_data.dimmer # Handle Hue lights paired with the gatway if self._light_data.hex_color is not None: diff --git a/homeassistant/components/tradfri.py b/homeassistant/components/tradfri.py new file mode 100644 index 00000000000..264ac9f42c6 --- /dev/null +++ b/homeassistant/components/tradfri.py @@ -0,0 +1,141 @@ +""" +Support for Ikea Tradfri. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ikea_tradfri/ +""" +import asyncio +import json +import logging +import os + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import discovery +from homeassistant.const import CONF_HOST, CONF_API_KEY +from homeassistant.loader import get_component +from homeassistant.components.discovery import SERVICE_IKEA_TRADFRI + +DOMAIN = 'tradfri' +CONFIG_FILE = 'tradfri.conf' +KEY_CONFIG = 'tradfri_configuring' +KEY_GATEWAY = 'tradfri_gateway' +REQUIREMENTS = ['pytradfri==0.4'] + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Inclusive(CONF_HOST, 'gateway'): cv.string, + vol.Inclusive(CONF_API_KEY, 'gateway'): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + +_LOGGER = logging.getLogger(__name__) + + +def request_configuration(hass, config, host): + """Request configuration steps from the user.""" + configurator = get_component('configurator') + hass.data.setdefault(KEY_CONFIG, {}) + instance = hass.data[KEY_CONFIG].get(host) + + # Configuration already in progress + if instance: + return + + @asyncio.coroutine + def configuration_callback(callback_data): + """Called when config is submitted.""" + res = yield from _setup_gateway(hass, config, host, + callback_data.get('key')) + if not res: + hass.async_add_job(configurator.notify_errors, instance, + "Unable to connect.") + return + + def success(): + """Set up was successful.""" + conf = _read_config(hass) + conf[host] = {'key': callback_data.get('key')} + _write_config(hass, conf) + hass.async_add_job(configurator.request_done, instance) + + hass.async_add_job(success) + + instance = configurator.request_config( + hass, "IKEA Trådfri", configuration_callback, + description='Please enter the security code written at the bottom of ' + 'your IKEA Trådfri Gateway.', + submit_caption="Confirm", + fields=[{'id': 'key', 'name': 'Security Code', 'type': 'password'}] + ) + + +@asyncio.coroutine +def async_setup(hass, config): + """Setup Tradfri.""" + conf = config.get(DOMAIN, {}) + host = conf.get(CONF_HOST) + key = conf.get(CONF_API_KEY) + + @asyncio.coroutine + def gateway_discovered(service, info): + """Called when a gateway is discovered.""" + keys = yield from hass.async_add_job(_read_config, hass) + host = info['host'] + + if host in keys: + yield from _setup_gateway(hass, config, host, keys[host]['key']) + else: + hass.async_add_job(request_configuration, hass, config, host) + + discovery.async_listen(hass, SERVICE_IKEA_TRADFRI, gateway_discovered) + + if host is None: + return True + + return (yield from _setup_gateway(hass, config, host, key)) + + +@asyncio.coroutine +def _setup_gateway(hass, hass_config, host, key): + """Create a gateway.""" + from pytradfri import cli_api_factory, Gateway, RequestError + + try: + api = cli_api_factory(host, key) + except RequestError: + return False + + gateway = Gateway(api) + gateway_id = gateway.get_gateway_info().id + hass.data.setdefault(KEY_GATEWAY, {}) + gateways = hass.data[KEY_GATEWAY] + + # Check if already set up + if gateway_id in gateways: + return True + + gateways[gateway_id] = gateway + hass.async_add_job(discovery.async_load_platform( + hass, 'light', DOMAIN, {'gateway': gateway_id}, hass_config)) + return True + + +def _read_config(hass): + """Read tradfri config.""" + path = hass.config.path(CONFIG_FILE) + + if not os.path.isfile(path): + return {} + + with open(path) as f_handle: + # Guard against empty file + return json.loads(f_handle.read() or '{}') + + +def _write_config(hass, config): + """Write tradfri config.""" + data = json.dumps(config) + with open(hass.config.path(CONFIG_FILE), 'w', encoding='utf-8') as outfile: + outfile.write(data) diff --git a/requirements_all.txt b/requirements_all.txt index f82fbaeaaad..d8a07866d2c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ python-wink==1.2.3 # homeassistant.components.device_tracker.trackr pytrackr==0.0.5 -# homeassistant.components.light.tradfri +# homeassistant.components.tradfri pytradfri==0.4 # homeassistant.components.device_tracker.unifi diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 97e5d0b7927..bf2f4e5832f 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -175,8 +175,9 @@ class TestColorUtil(unittest.TestCase): def test_color_rgb_to_hex(self): """Test color_rgb_to_hex.""" - self.assertEqual('000000', - color_util.color_rgb_to_hex(0, 0, 0)) + assert color_util.color_rgb_to_hex(255, 255, 255) == 'ffffff' + assert color_util.color_rgb_to_hex(0, 0, 0) == '000000' + assert color_util.color_rgb_to_hex(51, 153, 255) == '3399ff' class ColorTemperatureMiredToKelvinTests(unittest.TestCase): diff --git a/virtualization/Docker/scripts/coap_client b/virtualization/Docker/scripts/coap_client new file mode 100755 index 00000000000..bbc5ba0a2c2 --- /dev/null +++ b/virtualization/Docker/scripts/coap_client @@ -0,0 +1,14 @@ +#!/bin/sh +# Installs a modified coap client with support for dtls for use with IKEA Tradfri + +# Stop on errors +set -e + +apt-get install -y --no-install-recommends git autoconf automake libtool + +git clone --depth 1 --recursive -b dtls https://github.com/home-assistant/libcoap.git +cd libcoap +./autogen.sh +./configure --disable-documentation --disable-shared +make +make install diff --git a/virtualization/Docker/scripts/openalpr b/virtualization/Docker/scripts/openalpr index ffecc864914..b8fe8d44338 100755 --- a/virtualization/Docker/scripts/openalpr +++ b/virtualization/Docker/scripts/openalpr @@ -12,7 +12,7 @@ PACKAGES=( apt-get install -y --no-install-recommends ${PACKAGES[@]} # Clone the latest code from GitHub -git clone https://github.com/openalpr/openalpr.git /usr/local/src/openalpr +git clone --depth 1 https://github.com/openalpr/openalpr.git /usr/local/src/openalpr # Setup the build directory cd /usr/local/src/openalpr/src diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index f2238e43876..69f76e927e2 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -10,6 +10,7 @@ INSTALL_FFMPEG="${INSTALL_FFMPEG:-yes}" INSTALL_OPENZWAVE="${INSTALL_OPENZWAVE:-yes}" INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" INSTALL_PHANTOMJS="${INSTALL_PHANTOMJS:-yes}" +INSTALL_COAP_CLIENT="${INSTALL_COAP_CLIENT:-yes}" # Required debian packages for running hass or components PACKAGES=( @@ -62,6 +63,10 @@ if [ "$INSTALL_PHANTOMJS" == "yes" ]; then virtualization/Docker/scripts/phantomjs fi +if [ "$INSTALL_COAP_CLIENT" == "yes" ]; then + virtualization/Docker/scripts/coap_client +fi + # Remove packages apt-get remove -y --purge ${PACKAGES_DEV[@]} apt-get -y --purge autoremove