diff --git a/Dockerfile b/Dockerfile index f0d5accdf3d..908e8481eee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,9 +11,10 @@ MAINTAINER Paulus Schoutsen #ENV INSTALL_FFMPEG no #ENV INSTALL_LIBCEC no #ENV INSTALL_PHANTOMJS no -#ENV INSTALL_COAP_CLIENT no +#ENV INSTALL_COAP no #ENV INSTALL_SSOCR no + VOLUME /config RUN mkdir -p /usr/src/app @@ -25,7 +26,6 @@ RUN virtualization/Docker/setup_docker_prereqs # Install hass component dependencies COPY requirements_all.txt requirements_all.txt - # Uninstall enum34 because some depenndecies install it but breaks Python 3.4+. # See PR #8103 for more info. RUN pip3 install --no-cache-dir -r requirements_all.txt && \ diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index 0f56982dae5..3efab8309fc 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -4,15 +4,18 @@ Support for the IKEA Tradfri platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.tradfri/ """ +import asyncio import logging +from homeassistant.core import callback from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light) -from homeassistant.components.light import ( - PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA) -from homeassistant.components.tradfri import ( - KEY_GATEWAY, KEY_TRADFRI_GROUPS, KEY_API) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, + SUPPORT_RGB_COLOR, Light) +from homeassistant.components.light import \ + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA +from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \ + KEY_API from homeassistant.util import color as color_util _LOGGER = logging.getLogger(__name__) @@ -20,10 +23,13 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['tradfri'] PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA IKEA = 'IKEA of Sweden' +TRADFRI_LIGHT_MANAGER = 'Tradfri Light Manager' +SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION) ALLOWED_TEMPERATURES = {IKEA} -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the IKEA Tradfri Light platform.""" if discovery_info is None: return @@ -31,14 +37,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None): gateway_id = discovery_info['gateway'] api = hass.data[KEY_API][gateway_id] gateway = hass.data[KEY_GATEWAY][gateway_id] - devices = api(gateway.get_devices()) - lights = [dev for dev in devices if api(dev).has_light_control] - add_devices(Tradfri(light, api) for light in lights) + + devices_command = gateway.get_devices() + devices_commands = yield from api(devices_command) + devices = yield from api(*devices_commands) + lights = [dev for dev in devices if dev.has_light_control] + if lights: + async_add_devices(TradfriLight(light, api) for light in lights) allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id] if allow_tradfri_groups: - groups = api(gateway.get_groups()) - add_devices(TradfriGroup(group, api) for group in groups) + groups_command = gateway.get_groups() + groups_commands = yield from api(groups_command) + groups = yield from api(*groups_commands) + if groups: + async_add_devices(TradfriGroup(group, api) for group in groups) class TradfriGroup(Light): @@ -46,14 +59,26 @@ class TradfriGroup(Light): def __init__(self, light, api): """Initialize a Group.""" - self._group = api(light) self._api = api - self._name = self._group.name + self._group = light + self._name = light.name + + self._refresh(light) + + @asyncio.coroutine + def async_added_to_hass(self): + """Start thread when added to hass.""" + self._async_start_observe() + + @property + def should_poll(self): + """No polling needed for tradfri group.""" + return False @property def supported_features(self): """Flag supported features.""" - return SUPPORT_BRIGHTNESS + return SUPPORTED_FEATURES @property def name(self): @@ -70,49 +95,68 @@ class TradfriGroup(Light): """Return the brightness of the group lights.""" return self._group.dimmer - def turn_off(self, **kwargs): + @asyncio.coroutine + def async_turn_off(self, **kwargs): """Instruct the group lights to turn off.""" - self._api(self._group.set_state(0)) + self.hass.async_add_job(self._api(self._group.set_state(0))) - def turn_on(self, **kwargs): + @asyncio.coroutine + def async_turn_on(self, **kwargs): """Instruct the group lights to turn on, or dim.""" + keys = {} + if ATTR_TRANSITION in kwargs: + keys['transition_time'] = int(kwargs[ATTR_TRANSITION]) + if ATTR_BRIGHTNESS in kwargs: - self._api(self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS])) + self.hass.async_add_job(self._api( + self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys))) else: - self._api(self._group.set_state(1)) + self.hass.async_add_job(self._api(self._group.set_state(1))) + + @callback + def _async_start_observe(self, exc=None): + """Start observation of light.""" + from pytradfri.error import PyTradFriError + if exc: + _LOGGER.warning("Observation failed for %s", self._name, + exc_info=exc) - def update(self): - """Fetch new state data for this group.""" - from pytradfri import RequestTimeout try: - self._api(self._group.update()) - except RequestTimeout: - _LOGGER.warning("Tradfri update request timed out") + cmd = self._group.observe(callback=self._observe_update, + err_callback=self._async_start_observe, + duration=0) + self.hass.async_add_job(self._api(cmd)) + except PyTradFriError as err: + _LOGGER.warning("Observation failed, trying again", exc_info=err) + self._async_start_observe() + + def _refresh(self, group): + """Refresh the light data.""" + self._group = group + self._name = group.name + + def _observe_update(self, tradfri_device): + """Receive new state data for this light.""" + self._refresh(tradfri_device) + + self.hass.async_add_job(self.async_update_ha_state()) -class Tradfri(Light): - """The platform class required by Home Asisstant.""" +class TradfriLight(Light): + """The platform class required by Home Assistant.""" def __init__(self, light, api): """Initialize a Light.""" - self._light = api(light) self._api = api - - # Caching of LightControl and light object - self._light_control = self._light.light_control - self._light_data = self._light_control.lights[0] - self._name = self._light.name + self._light = None + self._light_control = None + self._light_data = None + self._name = None self._rgb_color = None - self._features = SUPPORT_BRIGHTNESS + self._features = SUPPORTED_FEATURES + self._temp_supported = False - if self._light_data.hex_color is not None: - if self._light.device_info.manufacturer == IKEA: - self._features |= SUPPORT_COLOR_TEMP - else: - self._features |= SUPPORT_RGB_COLOR - - self._ok_temps = \ - self._light.device_info.manufacturer in ALLOWED_TEMPERATURES + self._refresh(light) @property def min_mireds(self): @@ -126,6 +170,30 @@ class Tradfri(Light): from pytradfri.color import MIN_KELVIN_WS return color_util.color_temperature_kelvin_to_mired(MIN_KELVIN_WS) + @property + def device_state_attributes(self): + """Return the devices' state attributes.""" + info = self._light.device_info + attrs = { + 'manufacturer': info.manufacturer, + 'model_number': info.model_number, + 'serial': info.serial, + 'firmware_version': info.firmware_version, + 'power_source': info.power_source_str, + 'battery_level': info.battery_level + } + return attrs + + @asyncio.coroutine + def async_added_to_hass(self): + """Start thread when added to hass.""" + self._async_start_observe() + + @property + def should_poll(self): + """No polling needed for tradfri light.""" + return False + @property def supported_features(self): """Flag supported features.""" @@ -151,7 +219,7 @@ class Tradfri(Light): """Return the CT color value in mireds.""" if (self._light_data.kelvin_color is None or self.supported_features & SUPPORT_COLOR_TEMP == 0 or - not self._ok_temps): + not self._temp_supported): return None return color_util.color_temperature_kelvin_to_mired( self._light_data.kelvin_color @@ -162,42 +230,90 @@ class Tradfri(Light): """RGB color of the light.""" return self._rgb_color - def turn_off(self, **kwargs): + @asyncio.coroutine + def async_turn_off(self, **kwargs): """Instruct the light to turn off.""" - self._api(self._light_control.set_state(False)) + self.hass.async_add_job(self._api( + self._light_control.set_state(False))) - def turn_on(self, **kwargs): + @asyncio.coroutine + def async_turn_on(self, **kwargs): """ Instruct the light to turn on. After adding "self._light_data.hexcolor is not None" for ATTR_RGB_COLOR, this also supports Philips Hue bulbs. """ - if ATTR_BRIGHTNESS in kwargs: - self._api(self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS])) - else: - self._api(self._light_control.set_state(True)) - if ATTR_RGB_COLOR in kwargs and self._light_data.hex_color is not None: - self._api(self._light.light_control.set_rgb_color( - *kwargs[ATTR_RGB_COLOR])) + self.hass.async_add_job(self._api( + self._light.light_control.set_rgb_color( + *kwargs[ATTR_RGB_COLOR]))) elif ATTR_COLOR_TEMP in kwargs and \ - self._light_data.hex_color is not None and self._ok_temps: + self._light_data.hex_color is not None and \ + self._temp_supported: kelvin = color_util.color_temperature_mired_to_kelvin( kwargs[ATTR_COLOR_TEMP]) - self._api(self._light_control.set_kelvin_color(kelvin)) + self.hass.async_add_job(self._api( + self._light_control.set_kelvin_color(kelvin))) + + keys = {} + if ATTR_TRANSITION in kwargs: + keys['transition_time'] = int(kwargs[ATTR_TRANSITION]) + + if ATTR_BRIGHTNESS in kwargs: + self.hass.async_add_job(self._api( + self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS], + **keys))) + else: + self.hass.async_add_job(self._api( + self._light_control.set_state(True))) + + @callback + def _async_start_observe(self, exc=None): + """Start observation of light.""" + from pytradfri.error import PyTradFriError + if exc: + _LOGGER.warning("Observation failed for %s", self._name, + exc_info=exc) - def update(self): - """Fetch new state data for this light.""" - from pytradfri import RequestTimeout try: - self._api(self._light.update()) - except RequestTimeout as exception: - _LOGGER.warning("Tradfri update request timed out: %s", exception) + cmd = self._light.observe(callback=self._observe_update, + err_callback=self._async_start_observe, + duration=0) + self.hass.async_add_job(self._api(cmd)) + except PyTradFriError as err: + _LOGGER.warning("Observation failed, trying again", exc_info=err) + self._async_start_observe() + + def _refresh(self, light): + """Refresh the light data.""" + self._light = light + + # Caching of LightControl and light object + self._light_control = light.light_control + self._light_data = light.light_control.lights[0] + self._name = light.name + self._rgb_color = None + self._features = SUPPORTED_FEATURES + + if self._light_data.hex_color is not None: + if self._light.device_info.manufacturer == IKEA: + self._features |= SUPPORT_COLOR_TEMP + else: + self._features |= SUPPORT_RGB_COLOR + + self._temp_supported = self._light.device_info.manufacturer \ + in ALLOWED_TEMPERATURES + + def _observe_update(self, tradfri_device): + """Receive new state data for this light.""" + self._refresh(tradfri_device) # Handle Hue lights paired with the gateway # hex_color is 0 when bulb is unreachable if self._light_data.hex_color not in (None, '0'): self._rgb_color = color_util.rgb_hex_to_rgb_list( self._light_data.hex_color) + + self.hass.async_add_job(self.async_update_ha_state()) diff --git a/homeassistant/components/sensor/tradfri.py b/homeassistant/components/sensor/tradfri.py new file mode 100644 index 00000000000..314c18b7636 --- /dev/null +++ b/homeassistant/components/sensor/tradfri.py @@ -0,0 +1,116 @@ +""" +Support for the IKEA Tradfri platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.tradfri/ +""" +import asyncio +import logging + +from datetime import timedelta + +from homeassistant.core import callback +from homeassistant.components.tradfri import KEY_GATEWAY, KEY_API +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['tradfri'] + +SCAN_INTERVAL = timedelta(minutes=5) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the IKEA Tradfri device platform.""" + if discovery_info is None: + return + + gateway_id = discovery_info['gateway'] + api = hass.data[KEY_API][gateway_id] + gateway = hass.data[KEY_GATEWAY][gateway_id] + + devices_command = gateway.get_devices() + devices_commands = yield from api(devices_command) + all_devices = yield from api(*devices_commands) + devices = [dev for dev in all_devices if not dev.has_light_control] + async_add_devices(TradfriDevice(device, api) for device in devices) + + +class TradfriDevice(Entity): + """The platform class required by Home Assistant.""" + + def __init__(self, device, api): + """Initialize the device.""" + self._api = api + self._device = None + self._name = None + + self._refresh(device) + + @asyncio.coroutine + def async_added_to_hass(self): + """Start thread when added to hass.""" + self._async_start_observe() + + @property + def should_poll(self): + """No polling needed for tradfri.""" + return False + + @property + def name(self): + """Return the display name of this device.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit_of_measurement of the device.""" + return '%' + + @property + def device_state_attributes(self): + """Return the devices' state attributes.""" + info = self._device.device_info + attrs = { + 'manufacturer': info.manufacturer, + 'model_number': info.model_number, + 'serial': info.serial, + 'firmware_version': info.firmware_version, + 'power_source': info.power_source_str, + 'battery_level': info.battery_level + } + return attrs + + @property + def state(self): + """Return the current state of the device.""" + return self._device.device_info.battery_level + + @callback + def _async_start_observe(self, exc=None): + """Start observation of light.""" + from pytradfri.error import PyTradFriError + if exc: + _LOGGER.warning("Observation failed for %s", self._name, + exc_info=exc) + + try: + cmd = self._device.observe(callback=self._observe_update, + err_callback=self._async_start_observe, + duration=0) + self.hass.async_add_job(self._api(cmd)) + except PyTradFriError as err: + _LOGGER.warning("Observation failed, trying again", exc_info=err) + self._async_start_observe() + + def _refresh(self, device): + """Refresh the device data.""" + self._device = device + self._name = device.name + + def _observe_update(self, tradfri_device): + """Receive new state data for this device.""" + self._refresh(tradfri_device) + + self.hass.async_add_job(self.async_update_ha_state()) diff --git a/homeassistant/components/tradfri.py b/homeassistant/components/tradfri.py index 34422819743..ef4d7fceed8 100644 --- a/homeassistant/components/tradfri.py +++ b/homeassistant/components/tradfri.py @@ -16,7 +16,7 @@ from homeassistant.helpers import discovery from homeassistant.const import CONF_HOST, CONF_API_KEY from homeassistant.components.discovery import SERVICE_IKEA_TRADFRI -REQUIREMENTS = ['pytradfri==2.2'] +REQUIREMENTS = ['pytradfri==2.2.2'] DOMAIN = 'tradfri' CONFIG_FILE = 'tradfri.conf' @@ -111,16 +111,21 @@ def async_setup(hass, config): def _setup_gateway(hass, hass_config, host, key, allow_tradfri_groups): """Create a gateway.""" from pytradfri import Gateway, RequestError - from pytradfri.api.libcoap_api import api_factory + try: + from pytradfri.api.aiocoap_api import api_factory + except ImportError: + _LOGGER.exception("Looks like something isn't installed!") + return False try: - api = api_factory(host, key) + api = yield from api_factory(host, key, loop=hass.loop) except RequestError: + _LOGGER.exception("Tradfri setup failed.") return False gateway = Gateway() - # pylint: disable=no-member - gateway_id = api(gateway.get_gateway_info()).id + gateway_info_result = yield from api(gateway.get_gateway_info()) + gateway_id = gateway_info_result.id hass.data.setdefault(KEY_API, {}) hass.data.setdefault(KEY_GATEWAY, {}) gateways = hass.data[KEY_GATEWAY] @@ -137,6 +142,8 @@ def _setup_gateway(hass, hass_config, host, key, allow_tradfri_groups): gateways[gateway_id] = gateway hass.async_add_job(discovery.async_load_platform( hass, 'light', DOMAIN, {'gateway': gateway_id}, hass_config)) + hass.async_add_job(discovery.async_load_platform( + hass, 'sensor', DOMAIN, {'gateway': gateway_id}, hass_config)) return True diff --git a/requirements_all.txt b/requirements_all.txt index 61cf8945c04..a04a4240ae4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -829,7 +829,7 @@ pythonegardia==1.0.21 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri==2.2 +pytradfri==2.2.2 # homeassistant.components.device_tracker.unifi pyunifi==2.13 diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index 3aa468ca6a7..70b1a19f46d 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -11,7 +11,7 @@ MAINTAINER Paulus Schoutsen #ENV INSTALL_FFMPEG no #ENV INSTALL_LIBCEC no #ENV INSTALL_PHANTOMJS no -#ENV INSTALL_COAP_CLIENT no +#ENV INSTALL_COAP no #ENV INSTALL_SSOCR no VOLUME /config diff --git a/virtualization/Docker/scripts/aiocoap b/virtualization/Docker/scripts/aiocoap new file mode 100755 index 00000000000..8e36c616cb4 --- /dev/null +++ b/virtualization/Docker/scripts/aiocoap @@ -0,0 +1,23 @@ +#!/bin/sh +# Installs a modified coap client with support for dtls for use with IKEA Tradfri + +# Stop on errors +set -e + +python3 -m pip install cython + +cd /usr/src/app/ +mkdir -p build && cd build + +git clone --depth 1 https://git.fslab.de/jkonra2m/tinydtls +cd tinydtls +autoreconf +./configure --with-ecc --without-debug +cd cython +python3 setup.py install + +cd ../.. +git clone --depth 1 https://github.com/chrysn/aiocoap/ +cd aiocoap +git reset --hard 0df6a1e44582de99ae944b6a7536d08e2a612e8f +python3 -m pip install . diff --git a/virtualization/Docker/scripts/coap_client b/virtualization/Docker/scripts/coap_client deleted file mode 100755 index 82606c5f14d..00000000000 --- a/virtualization/Docker/scripts/coap_client +++ /dev/null @@ -1,17 +0,0 @@ -#!/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 - -cd /usr/src/app/ -mkdir -p build && cd build - -git clone --depth 1 --recursive -b dtls https://github.com/home-assistant/libcoap.git -cd libcoap -./autogen.sh -./configure --disable-documentation --disable-shared --without-debug CFLAGS="-D COAP_DEBUG_FD=stderr" -make -make install diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 91bb9888765..95c8cd3f2e7 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -9,7 +9,7 @@ INSTALL_OPENALPR="${INSTALL_OPENALPR:-yes}" INSTALL_FFMPEG="${INSTALL_FFMPEG:-yes}" INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" INSTALL_PHANTOMJS="${INSTALL_PHANTOMJS:-yes}" -INSTALL_COAP_CLIENT="${INSTALL_COAP_CLIENT:-yes}" +INSTALL_COAP="${INSTALL_COAP:-yes}" INSTALL_SSOCR="${INSTALL_SSOCR:-yes}" # Required debian packages for running hass or components @@ -59,8 +59,8 @@ if [ "$INSTALL_PHANTOMJS" == "yes" ]; then virtualization/Docker/scripts/phantomjs fi -if [ "$INSTALL_COAP_CLIENT" == "yes" ]; then - virtualization/Docker/scripts/coap_client +if [ "$INSTALL_COAP" == "yes" ]; then + virtualization/Docker/scripts/aiocoap fi if [ "$INSTALL_SSOCR" == "yes" ]; then