-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Async support with resource observation. #7815
Changes from 25 commits
fb921f3
140cd0b
416559b
976527c
ebc408a
304857f
264b43c
b83ce62
836bb63
93bdbd0
6a13613
b1e1fc2
089c65e
b6ed0ed
acf27bc
dd6d5b6
27ff361
e432e7f
748a7cf
0158959
597464d
949ace2
10887e2
2e7e6c1
2d6b3df
d4255c2
44f70f8
0a3f3d5
462c641
7858e0c
c726069
a45c7dc
8899efb
1052a78
dec727d
0c841c3
5906591
814f420
a2ac6a9
c9ce479
14af049
b6bc24d
f4e1df5
61797d3
b2ba3e7
82d9aef
bf68cfd
7c9ff4b
fae1ff5
06e594e
952dbc0
a243655
cfcf227
90e513c
6e30db9
5db61e4
0cbb660
03647e0
8b3ca01
aef1005
76b1913
719c1f1
fb168b4
4197a91
0b33ebc
98f4cc1
cc327a0
152f279
72b6c59
06d3e72
537f3df
b6749eb
c839e66
7189363
1813db0
047d1b1
c7f4373
e86ea21
32ae7c1
3eba9aa
4141908
bc589c8
f77fd09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,55 +4,80 @@ | |
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) | ||
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 | ||
from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \ | ||
KEY_API | ||
from homeassistant.util import color as color_util | ||
|
||
_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: {2200: 'efd275', 2700: 'f1e0b5', 4000: 'f5faf6'} | ||
} | ||
|
||
|
||
def setup_platform(hass, config, add_devices, discovery_info=None): | ||
@asyncio.coroutine | ||
def async_setup_platform(hass, config, add_devices, discovery_info=None): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rename that into |
||
"""Set up the IKEA Tradfri Light platform.""" | ||
if discovery_info is None: | ||
return | ||
|
||
gateway_id = discovery_info['gateway'] | ||
gateway = hass.data[KEY_GATEWAY][gateway_id] | ||
devices = gateway.get_devices() | ||
api = hass.data[KEY_API] | ||
|
||
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] | ||
add_devices(Tradfri(light) for light in lights) | ||
add_devices(TradfriLight(light, api) for light in lights) | ||
|
||
allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id] | ||
if allow_tradfri_groups: | ||
groups = gateway.get_groups() | ||
add_devices(TradfriGroup(group) for group in groups) | ||
groups_command = gateway.get_groups() | ||
groups_commands = yield from api(groups_command) | ||
groups = yield from api(*groups_commands) | ||
add_devices(TradfriGroup(group, api) for group in groups) | ||
|
||
|
||
class TradfriGroup(Light): | ||
"""The platform class required by hass.""" | ||
|
||
def __init__(self, light): | ||
def __init__(self, light, api): | ||
"""Initialize a Group.""" | ||
self._api = api | ||
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.""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a group There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is? |
||
return False | ||
|
||
@property | ||
def supported_features(self): | ||
"""Flag supported features.""" | ||
return SUPPORT_BRIGHTNESS | ||
return SUPPORTED_FEATURES | ||
|
||
@property | ||
def name(self): | ||
|
@@ -69,44 +94,71 @@ def brightness(self): | |
"""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.""" | ||
return 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.""" | ||
if ATTR_BRIGHTNESS in kwargs: | ||
self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS]) | ||
transition = kwargs[ATTR_TRANSITION] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. undefined name 'ATTR_TRANSITION' |
||
self.hass.async_add_job(self._api( | ||
self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], | ||
transition_time=transition))) | ||
else: | ||
self._group.set_state(1) | ||
self.hass.async_add_job(self._api(self._group.set_state(1))) | ||
|
||
def update(self): | ||
"""Fetch new state data for this group.""" | ||
self._group.update() | ||
@callback | ||
def _async_start_observe(self, exc=None): | ||
"""Start observation of light.""" | ||
if exc: | ||
_LOGGER.info("Observation failed for %s", self._name, | ||
exc_info=exc) | ||
|
||
cmd = self._group.observe(callback=self._observe_update, | ||
err_callback=self._async_start_observe, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will the error callback also be called when the time expires? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't believe so |
||
duration=0) | ||
self.hass.async_add_job(self._api(cmd)) | ||
|
||
class Tradfri(Light): | ||
"""The platform class required by Home Asisstant.""" | ||
def _refresh(self, group): | ||
"""Refresh the light data.""" | ||
self._group = group | ||
self._name = group.name | ||
|
||
def __init__(self, light): | ||
"""Initialize a Light.""" | ||
self._light = light | ||
def _observe_update(self, tradfri_device): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also when a method is an async friendly callback, add an |
||
"""Receive new state data for this light.""" | ||
self._refresh(tradfri_device) | ||
|
||
# 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.hass.async_add_job(self.async_update_ha_state()) | ||
|
||
|
||
class TradfriLight(Light): | ||
"""The platform class required by Home Assistant.""" | ||
|
||
def __init__(self, light, api): | ||
"""Initialize a Light.""" | ||
self._api = api | ||
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._ok_temps = None | ||
|
||
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._refresh(light) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that should be call inside There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not actually doing a refresh here, just setting instance variables. |
||
|
||
self._ok_temps = ALLOWED_TEMPERATURES.get( | ||
self._light.device_info.manufacturer) | ||
@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): | ||
|
@@ -151,40 +203,83 @@ def rgb_color(self): | |
"""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.""" | ||
return 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._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS]) | ||
transition = kwargs[ATTR_TRANSITION] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. undefined name 'ATTR_TRANSITION' |
||
self.hass.async_add_job(self._api( | ||
self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS], | ||
transition_time=transition))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. continuation line under-indented for visual indent |
||
else: | ||
self._light_control.set_state(True) | ||
self.hass.async_add_job(self._api( | ||
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])) | ||
self.hass.async_add_job(self._api( | ||
self._light.light_control.set_hex_color( | ||
color_util.color_rgb_to_hex(*kwargs[ATTR_RGB_COLOR])))) | ||
|
||
elif ATTR_COLOR_TEMP in kwargs and \ | ||
self._light_data.hex_color is not None and self._ok_temps: | ||
kelvin = color_util.color_temperature_mired_to_kelvin( | ||
kwargs[ATTR_COLOR_TEMP]) | ||
# find closest allowed kelvin temp from user input | ||
kelvin = min(self._ok_temps.keys(), key=lambda x: abs(x - kelvin)) | ||
self._light_control.set_hex_color(self._ok_temps[kelvin]) | ||
self.hass.async_add_job(self._api( | ||
self._light_control.set_hex_color(self._ok_temps[kelvin]))) | ||
|
||
@callback | ||
def _async_start_observe(self, exc=None): | ||
"""Start observation of light.""" | ||
if exc: | ||
_LOGGER.info("Observation failed for %s", self._name, | ||
exc_info=exc) | ||
|
||
cmd = self._light.observe(callback=self._observe_update, | ||
err_callback=self._async_start_observe, | ||
duration=0) | ||
self.hass.async_add_job(self._api(cmd)) | ||
|
||
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 | ||
|
||
def update(self): | ||
"""Fetch new state data for this light.""" | ||
self._light.update() | ||
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 = ALLOWED_TEMPERATURES.get( | ||
self._light.device_info.manufacturer) | ||
|
||
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()) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,13 +17,14 @@ | |
from homeassistant.loader import get_component | ||
from homeassistant.components.discovery import SERVICE_IKEA_TRADFRI | ||
|
||
REQUIREMENTS = ['pytradfri==1.1'] | ||
REQUIREMENTS = ['pytradfri==2.1.1'] | ||
|
||
DOMAIN = 'tradfri' | ||
CONFIG_FILE = 'tradfri.conf' | ||
KEY_CONFIG = 'tradfri_configuring' | ||
KEY_GATEWAY = 'tradfri_gateway' | ||
KEY_TRADFRI_GROUPS = 'tradfri_allow_tradfri_groups' | ||
KEY_API = 'tradfri_api' | ||
CONF_ALLOW_TRADFRI_GROUPS = 'allow_tradfri_groups' | ||
DEFAULT_ALLOW_TRADFRI_GROUPS = True | ||
|
||
|
@@ -110,15 +111,24 @@ def gateway_discovered(service, info): | |
@asyncio.coroutine | ||
def _setup_gateway(hass, hass_config, host, key, allow_tradfri_groups): | ||
"""Create a gateway.""" | ||
from pytradfri import cli_api_factory, Gateway, RequestError, retry_timeout | ||
from pytradfri import Gateway, RequestError | ||
try: | ||
from pytradfri.api.aiocoap_api import api_factory | ||
except ImportError: | ||
_LOGGER.exception("Looks like something isn't installed!") | ||
return False | ||
|
||
try: | ||
api = retry_timeout(cli_api_factory(host, key)) | ||
api = yield from api_factory(host, key, loop=hass.loop) | ||
hass.data[KEY_API] = api | ||
except RequestError: | ||
_LOGGER.exception("Tradfri setup failed.") | ||
return False | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we print an error ? |
||
|
||
gateway = Gateway(api) | ||
gateway_id = gateway.get_gateway_info().id | ||
gateway = Gateway() | ||
gateway_info_cmd = gateway.get_gateway_info() | ||
gateway_info_result = yield from api(gateway_info_cmd) | ||
gateway_id = gateway_info_result.id | ||
hass.data.setdefault(KEY_GATEWAY, {}) | ||
gateways = hass.data[KEY_GATEWAY] | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,7 +11,7 @@ MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> | |
#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 | ||
|
@@ -26,7 +26,7 @@ RUN virtualization/Docker/setup_docker_prereqs | |
# Install hass component dependencies | ||
COPY requirements_all.txt requirements_all.txt | ||
RUN pip3 install --no-cache-dir -r requirements_all.txt && \ | ||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet | ||
pip3 install --no-cache-dir mysqlclient psycopg2 cchardet | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please reinstate uvloop There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As uvloop isn't compatible with aiocoap, how can I get around it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is uvloop not compatible? Why would aiocoap be tied to an event loop implementation ? We can't just remove uvloop, that's a big performance degradation. Also, many users will run under uvloop. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When running with uvloop installed;
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like it but I'm down to remove There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, I'm not happy about it either. Hopefully we can re-enable it soon. |
||
|
||
# BEGIN: Development additions | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
#!/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 | ||
|
||
mkdir -p /usr/src/build | ||
cd /usr/src/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 -b tinydtls https://github.com/chrysn/aiocoap/ | ||
cd aiocoap | ||
python3 -m pip install . |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't remove uvloop