Update tradfri v5 (#11187)

* First pass to support simplified colour management in tradfri

* Fix lint

* Fix lint

* Update imports

* Prioritise brightness for transition

* Fix bug

* None check

* Bracket

* Import

* Fix bugs

* Change colour logic

* Denormalise colour

* Lint

* Fix bug

* Fix bugs, expose rgb conversion

* Fix bug

* Fix bug

* Fix bug

* Improve XY

* Improve XY

* async/wait for tradfri.

* Bump requirement

* Formatting.

* Remove comma

* Line length, shadowing

* Switch to new HS colour system, using native data from tradfri gateway.

* Lint.

* Brightness bug.

* Remove guard.

* Temp workaround for bug.

* Temp workaround for bug.

* Temp workaround for bug.

* Safety.

* Switch logic.

* Integrate latest

* Fixes.

* Fixes.

* Mired validation.

* Set bounds.

* Transition time.

* Transition time.

* Transition time.

* Fix brightness values.
This commit is contained in:
Lewis Juggins 2018-03-28 23:50:09 +01:00 committed by Paulus Schoutsen
parent 45ff15bc85
commit b3b7cf3fa7
4 changed files with 100 additions and 136 deletions

View File

@ -4,11 +4,9 @@ Support for the IKEA Tradfri platform.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.tradfri/ https://home-assistant.io/components/light.tradfri/
""" """
import asyncio
import logging import logging
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION,
SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP,
@ -17,20 +15,19 @@ from homeassistant.components.light import \
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA 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 KEY_API
from homeassistant.util import color as color_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_TRANSITION_TIME = 'transition_time'
DEPENDENCIES = ['tradfri'] DEPENDENCIES = ['tradfri']
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA
IKEA = 'IKEA of Sweden' IKEA = 'IKEA of Sweden'
TRADFRI_LIGHT_MANAGER = 'Tradfri Light Manager' TRADFRI_LIGHT_MANAGER = 'Tradfri Light Manager'
SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION) SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION)
ALLOWED_TEMPERATURES = {IKEA}
@asyncio.coroutine async def async_setup_platform(hass, config,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async_add_devices, discovery_info=None):
"""Set up the IKEA Tradfri Light platform.""" """Set up the IKEA Tradfri Light platform."""
if discovery_info is None: if discovery_info is None:
return return
@ -40,8 +37,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
gateway = hass.data[KEY_GATEWAY][gateway_id] gateway = hass.data[KEY_GATEWAY][gateway_id]
devices_command = gateway.get_devices() devices_command = gateway.get_devices()
devices_commands = yield from api(devices_command) devices_commands = await api(devices_command)
devices = yield from api(devices_commands) devices = await api(devices_commands)
lights = [dev for dev in devices if dev.has_light_control] lights = [dev for dev in devices if dev.has_light_control]
if lights: if lights:
async_add_devices(TradfriLight(light, api) for light in lights) async_add_devices(TradfriLight(light, api) for light in lights)
@ -49,8 +46,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id] allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id]
if allow_tradfri_groups: if allow_tradfri_groups:
groups_command = gateway.get_groups() groups_command = gateway.get_groups()
groups_commands = yield from api(groups_command) groups_commands = await api(groups_command)
groups = yield from api(groups_commands) groups = await api(groups_commands)
if groups: if groups:
async_add_devices(TradfriGroup(group, api) for group in groups) async_add_devices(TradfriGroup(group, api) for group in groups)
@ -66,8 +63,7 @@ class TradfriGroup(Light):
self._refresh(light) self._refresh(light)
@asyncio.coroutine async def async_added_to_hass(self):
def async_added_to_hass(self):
"""Start thread when added to hass.""" """Start thread when added to hass."""
self._async_start_observe() self._async_start_observe()
@ -96,13 +92,11 @@ class TradfriGroup(Light):
"""Return the brightness of the group lights.""" """Return the brightness of the group lights."""
return self._group.dimmer return self._group.dimmer
@asyncio.coroutine async def async_turn_off(self, **kwargs):
def async_turn_off(self, **kwargs):
"""Instruct the group lights to turn off.""" """Instruct the group lights to turn off."""
yield from self._api(self._group.set_state(0)) await self._api(self._group.set_state(0))
@asyncio.coroutine async def async_turn_on(self, **kwargs):
def async_turn_on(self, **kwargs):
"""Instruct the group lights to turn on, or dim.""" """Instruct the group lights to turn on, or dim."""
keys = {} keys = {}
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
@ -112,16 +106,16 @@ class TradfriGroup(Light):
if kwargs[ATTR_BRIGHTNESS] == 255: if kwargs[ATTR_BRIGHTNESS] == 255:
kwargs[ATTR_BRIGHTNESS] = 254 kwargs[ATTR_BRIGHTNESS] = 254
yield from self._api( await self._api(
self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys)) self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys))
else: else:
yield from self._api(self._group.set_state(1)) await self._api(self._group.set_state(1))
@callback @callback
def _async_start_observe(self, exc=None): def _async_start_observe(self, exc=None):
"""Start observation of light.""" """Start observation of light."""
# pylint: disable=import-error # pylint: disable=import-error
from pytradfri.error import PyTradFriError from pytradfri.error import PytradfriError
if exc: if exc:
_LOGGER.warning("Observation failed for %s", self._name, _LOGGER.warning("Observation failed for %s", self._name,
exc_info=exc) exc_info=exc)
@ -131,7 +125,7 @@ class TradfriGroup(Light):
err_callback=self._async_start_observe, err_callback=self._async_start_observe,
duration=0) duration=0)
self.hass.async_add_job(self._api(cmd)) self.hass.async_add_job(self._api(cmd))
except PyTradFriError as err: except PytradfriError as err:
_LOGGER.warning("Observation failed, trying again", exc_info=err) _LOGGER.warning("Observation failed, trying again", exc_info=err)
self._async_start_observe() self._async_start_observe()
@ -159,7 +153,6 @@ class TradfriLight(Light):
self._name = None self._name = None
self._hs_color = None self._hs_color = None
self._features = SUPPORTED_FEATURES self._features = SUPPORTED_FEATURES
self._temp_supported = False
self._available = True self._available = True
self._refresh(light) self._refresh(light)
@ -167,33 +160,14 @@ class TradfriLight(Light):
@property @property
def min_mireds(self): def min_mireds(self):
"""Return the coldest color_temp that this light supports.""" """Return the coldest color_temp that this light supports."""
if self._light_control.max_kelvin is not None: return self._light_control.min_mireds
return color_util.color_temperature_kelvin_to_mired(
self._light_control.max_kelvin
)
@property @property
def max_mireds(self): def max_mireds(self):
"""Return the warmest color_temp that this light supports.""" """Return the warmest color_temp that this light supports."""
if self._light_control.min_kelvin is not None: return self._light_control.max_mireds
return color_util.color_temperature_kelvin_to_mired(
self._light_control.min_kelvin
)
@property async def async_added_to_hass(self):
def device_state_attributes(self):
"""Return the devices' state attributes."""
info = self._light.device_info
attrs = {}
if info.battery_level is not None:
attrs[ATTR_BATTERY_LEVEL] = info.battery_level
return attrs
@asyncio.coroutine
def async_added_to_hass(self):
"""Start thread when added to hass.""" """Start thread when added to hass."""
self._async_start_observe() self._async_start_observe()
@ -229,64 +203,73 @@ class TradfriLight(Light):
@property @property
def color_temp(self): def color_temp(self):
"""Return the CT color value in mireds.""" """Return the color temp value in mireds."""
kelvin_color = self._light_data.kelvin_color_inferred return self._light_data.color_temp
if kelvin_color is not None:
return color_util.color_temperature_kelvin_to_mired(
kelvin_color
)
@property @property
def hs_color(self): def hs_color(self):
"""HS color of the light.""" """HS color of the light."""
return self._hs_color if self._light_control.can_set_color:
hsbxy = self._light_data.hsb_xy_color
hue = hsbxy[0] / (65535 / 360)
sat = hsbxy[1] / (65279 / 100)
if hue is not None and sat is not None:
return hue, sat
@asyncio.coroutine async def async_turn_off(self, **kwargs):
def async_turn_off(self, **kwargs):
"""Instruct the light to turn off.""" """Instruct the light to turn off."""
yield from self._api(self._light_control.set_state(False)) await self._api(self._light_control.set_state(False))
@asyncio.coroutine async def async_turn_on(self, **kwargs):
def async_turn_on(self, **kwargs): """Instruct the light to turn on."""
""" params = {}
Instruct the light to turn on. transition_time = None
After adding "self._light_data.hexcolor is not None"
for ATTR_HS_COLOR, this also supports Philips Hue bulbs.
"""
if ATTR_HS_COLOR in kwargs and self._light_data.hex_color is not None:
rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
yield from self._api(
self._light.light_control.set_rgb_color(*rgb))
elif ATTR_COLOR_TEMP in kwargs and \
self._light_data.hex_color is not None and \
self._temp_supported:
kelvin = color_util.color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP])
yield from self._api(
self._light_control.set_kelvin_color(kelvin))
keys = {}
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
keys['transition_time'] = int(kwargs[ATTR_TRANSITION]) * 10 transition_time = int(kwargs[ATTR_TRANSITION]) * 10
if ATTR_BRIGHTNESS in kwargs: brightness = kwargs.get(ATTR_BRIGHTNESS)
if kwargs[ATTR_BRIGHTNESS] == 255:
kwargs[ATTR_BRIGHTNESS] = 254
yield from self._api( if brightness is not None:
self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS], if brightness > 254:
**keys)) brightness = 254
elif brightness < 0:
brightness = 0
if ATTR_HS_COLOR in kwargs and self._light_control.can_set_color:
params[ATTR_BRIGHTNESS] = brightness
hue = int(kwargs[ATTR_HS_COLOR][0] * (65535 / 360))
sat = int(kwargs[ATTR_HS_COLOR][1] * (65279 / 100))
await self._api(
self._light_control.set_hsb(hue, sat, **params))
return
if ATTR_COLOR_TEMP in kwargs and self._light_control.can_set_temp:
temp = kwargs[ATTR_COLOR_TEMP]
if temp > self.max_mireds:
temp = self.max_mireds
elif temp < self.min_mireds:
temp = self.min_mireds
if brightness is None:
params[ATTR_TRANSITION_TIME] = transition_time
await self._api(
self._light_control.set_color_temp(temp,
**params))
if brightness is not None:
params[ATTR_TRANSITION_TIME] = transition_time
await self._api(
self._light_control.set_dimmer(brightness,
**params))
else: else:
yield from self._api( await self._api(
self._light_control.set_state(True)) self._light_control.set_state(True))
@callback @callback
def _async_start_observe(self, exc=None): def _async_start_observe(self, exc=None):
"""Start observation of light.""" """Start observation of light."""
# pylint: disable=import-error # pylint: disable=import-error
from pytradfri.error import PyTradFriError from pytradfri.error import PytradfriError
if exc: if exc:
_LOGGER.warning("Observation failed for %s", self._name, _LOGGER.warning("Observation failed for %s", self._name,
exc_info=exc) exc_info=exc)
@ -296,7 +279,7 @@ class TradfriLight(Light):
err_callback=self._async_start_observe, err_callback=self._async_start_observe,
duration=0) duration=0)
self.hass.async_add_job(self._api(cmd)) self.hass.async_add_job(self._api(cmd))
except PyTradFriError as err: except PytradfriError as err:
_LOGGER.warning("Observation failed, trying again", exc_info=err) _LOGGER.warning("Observation failed, trying again", exc_info=err)
self._async_start_observe() self._async_start_observe()
@ -309,27 +292,15 @@ class TradfriLight(Light):
self._light_control = light.light_control self._light_control = light.light_control
self._light_data = light.light_control.lights[0] self._light_data = light.light_control.lights[0]
self._name = light.name self._name = light.name
self._hs_color = None
self._features = SUPPORTED_FEATURES self._features = SUPPORTED_FEATURES
if self._light.device_info.manufacturer == IKEA: if light.light_control.can_set_color:
if self._light_control.can_set_kelvin: self._features |= SUPPORT_COLOR
if light.light_control.can_set_temp:
self._features |= SUPPORT_COLOR_TEMP self._features |= SUPPORT_COLOR_TEMP
if self._light_control.can_set_color:
self._features |= SUPPORT_COLOR
else:
if self._light_data.hex_color is not None:
self._features |= SUPPORT_COLOR
self._temp_supported = self._light.device_info.manufacturer \
in ALLOWED_TEMPERATURES
@callback @callback
def _observe_update(self, tradfri_device): def _observe_update(self, tradfri_device):
"""Receive new state data for this light.""" """Receive new state data for this light."""
self._refresh(tradfri_device) self._refresh(tradfri_device)
rgb = color_util.rgb_hex_to_rgb_list(
self._light_data.hex_color_inferred
)
self._hs_color = color_util.color_RGB_to_hs(*rgb)
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()

View File

@ -4,7 +4,6 @@ Support for the IKEA Tradfri platform.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.tradfri/ https://home-assistant.io/components/sensor.tradfri/
""" """
import asyncio
import logging import logging
from datetime import timedelta from datetime import timedelta
@ -20,8 +19,8 @@ DEPENDENCIES = ['tradfri']
SCAN_INTERVAL = timedelta(minutes=5) SCAN_INTERVAL = timedelta(minutes=5)
@asyncio.coroutine async def async_setup_platform(hass, config, async_add_devices,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): discovery_info=None):
"""Set up the IKEA Tradfri device platform.""" """Set up the IKEA Tradfri device platform."""
if discovery_info is None: if discovery_info is None:
return return
@ -31,8 +30,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
gateway = hass.data[KEY_GATEWAY][gateway_id] gateway = hass.data[KEY_GATEWAY][gateway_id]
devices_command = gateway.get_devices() devices_command = gateway.get_devices()
devices_commands = yield from api(devices_command) devices_commands = await api(devices_command)
all_devices = yield from api(devices_commands) all_devices = await api(devices_commands)
devices = [dev for dev in all_devices if not dev.has_light_control] devices = [dev for dev in all_devices if not dev.has_light_control]
async_add_devices(TradfriDevice(device, api) for device in devices) async_add_devices(TradfriDevice(device, api) for device in devices)
@ -48,8 +47,7 @@ class TradfriDevice(Entity):
self._refresh(device) self._refresh(device)
@asyncio.coroutine async def async_added_to_hass(self):
def async_added_to_hass(self):
"""Start thread when added to hass.""" """Start thread when added to hass."""
self._async_start_observe() self._async_start_observe()
@ -91,7 +89,7 @@ class TradfriDevice(Entity):
def _async_start_observe(self, exc=None): def _async_start_observe(self, exc=None):
"""Start observation of light.""" """Start observation of light."""
# pylint: disable=import-error # pylint: disable=import-error
from pytradfri.error import PyTradFriError from pytradfri.error import PytradfriError
if exc: if exc:
_LOGGER.warning("Observation failed for %s", self._name, _LOGGER.warning("Observation failed for %s", self._name,
exc_info=exc) exc_info=exc)
@ -101,7 +99,7 @@ class TradfriDevice(Entity):
err_callback=self._async_start_observe, err_callback=self._async_start_observe,
duration=0) duration=0)
self.hass.async_add_job(self._api(cmd)) self.hass.async_add_job(self._api(cmd))
except PyTradFriError as err: except PytradfriError as err:
_LOGGER.warning("Observation failed, trying again", exc_info=err) _LOGGER.warning("Observation failed, trying again", exc_info=err)
self._async_start_observe() self._async_start_observe()

View File

@ -1,10 +1,9 @@
""" """
Support for Ikea Tradfri. Support for IKEA Tradfri.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/ikea_tradfri/ https://home-assistant.io/components/ikea_tradfri/
""" """
import asyncio
import logging import logging
from uuid import uuid4 from uuid import uuid4
@ -16,7 +15,7 @@ from homeassistant.const import CONF_HOST
from homeassistant.components.discovery import SERVICE_IKEA_TRADFRI from homeassistant.components.discovery import SERVICE_IKEA_TRADFRI
from homeassistant.util.json import load_json, save_json from homeassistant.util.json import load_json, save_json
REQUIREMENTS = ['pytradfri[async]==4.1.0'] REQUIREMENTS = ['pytradfri[async]==5.4.2']
DOMAIN = 'tradfri' DOMAIN = 'tradfri'
GATEWAY_IDENTITY = 'homeassistant' GATEWAY_IDENTITY = 'homeassistant'
@ -49,8 +48,7 @@ def request_configuration(hass, config, host):
if instance: if instance:
return return
@asyncio.coroutine async def configuration_callback(callback_data):
def configuration_callback(callback_data):
"""Handle the submitted configuration.""" """Handle the submitted configuration."""
try: try:
from pytradfri.api.aiocoap_api import APIFactory from pytradfri.api.aiocoap_api import APIFactory
@ -67,13 +65,13 @@ def request_configuration(hass, config, host):
# pytradfri aiocoap API into an endless loop. # pytradfri aiocoap API into an endless loop.
# Should just raise a requestError or something. # Should just raise a requestError or something.
try: try:
key = yield from api_factory.generate_psk(security_code) key = await api_factory.generate_psk(security_code)
except RequestError: except RequestError:
configurator.async_notify_errors(hass, instance, configurator.async_notify_errors(hass, instance,
"Security Code not accepted.") "Security Code not accepted.")
return return
res = yield from _setup_gateway(hass, config, host, identity, key, res = await _setup_gateway(hass, config, host, identity, key,
DEFAULT_ALLOW_TRADFRI_GROUPS) DEFAULT_ALLOW_TRADFRI_GROUPS)
if not res: if not res:
@ -101,18 +99,16 @@ def request_configuration(hass, config, host):
) )
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Set up the Tradfri component.""" """Set up the Tradfri component."""
conf = config.get(DOMAIN, {}) conf = config.get(DOMAIN, {})
host = conf.get(CONF_HOST) host = conf.get(CONF_HOST)
allow_tradfri_groups = conf.get(CONF_ALLOW_TRADFRI_GROUPS) allow_tradfri_groups = conf.get(CONF_ALLOW_TRADFRI_GROUPS)
known_hosts = yield from hass.async_add_job(load_json, known_hosts = await hass.async_add_job(load_json,
hass.config.path(CONFIG_FILE)) hass.config.path(CONFIG_FILE))
@asyncio.coroutine async def gateway_discovered(service, info,
def gateway_discovered(service, info, allow_groups=DEFAULT_ALLOW_TRADFRI_GROUPS):
allow_tradfri_groups=DEFAULT_ALLOW_TRADFRI_GROUPS):
"""Run when a gateway is discovered.""" """Run when a gateway is discovered."""
host = info['host'] host = info['host']
@ -121,22 +117,21 @@ def async_setup(hass, config):
# identity was hard coded as 'homeassistant' # identity was hard coded as 'homeassistant'
identity = known_hosts[host].get('identity', 'homeassistant') identity = known_hosts[host].get('identity', 'homeassistant')
key = known_hosts[host].get('key') key = known_hosts[host].get('key')
yield from _setup_gateway(hass, config, host, identity, key, await _setup_gateway(hass, config, host, identity, key,
allow_tradfri_groups) allow_groups)
else: else:
hass.async_add_job(request_configuration, hass, config, host) hass.async_add_job(request_configuration, hass, config, host)
discovery.async_listen(hass, SERVICE_IKEA_TRADFRI, gateway_discovered) discovery.async_listen(hass, SERVICE_IKEA_TRADFRI, gateway_discovered)
if host: if host:
yield from gateway_discovered(None, await gateway_discovered(None,
{'host': host}, {'host': host},
allow_tradfri_groups) allow_tradfri_groups)
return True return True
@asyncio.coroutine async def _setup_gateway(hass, hass_config, host, identity, key,
def _setup_gateway(hass, hass_config, host, identity, key,
allow_tradfri_groups): allow_tradfri_groups):
"""Create a gateway.""" """Create a gateway."""
from pytradfri import Gateway, RequestError # pylint: disable=import-error from pytradfri import Gateway, RequestError # pylint: disable=import-error
@ -151,7 +146,7 @@ def _setup_gateway(hass, hass_config, host, identity, key,
loop=hass.loop) loop=hass.loop)
api = factory.request api = factory.request
gateway = Gateway() gateway = Gateway()
gateway_info_result = yield from api(gateway.get_gateway_info()) gateway_info_result = await api(gateway.get_gateway_info())
except RequestError: except RequestError:
_LOGGER.exception("Tradfri setup failed.") _LOGGER.exception("Tradfri setup failed.")
return False return False

View File

@ -1021,7 +1021,7 @@ pytouchline==0.7
pytrackr==0.0.5 pytrackr==0.0.5
# homeassistant.components.tradfri # homeassistant.components.tradfri
pytradfri[async]==4.1.0 pytradfri[async]==5.4.2
# homeassistant.components.device_tracker.unifi # homeassistant.components.device_tracker.unifi
pyunifi==2.13 pyunifi==2.13