Skip to content

Commit

Permalink
Merge 331b724 into c8a8d5c
Browse files Browse the repository at this point in the history
  • Loading branch information
phbaer committed Mar 18, 2019
2 parents c8a8d5c + 331b724 commit 428ba34
Show file tree
Hide file tree
Showing 6 changed files with 387 additions and 44 deletions.
66 changes: 41 additions & 25 deletions home-assistant-plugin/custom_components/xknx/light.py
Expand Up @@ -9,17 +9,16 @@
import voluptuous as vol

from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP,
Light)
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_WHITE_VALUE,
PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP,
SUPPORT_WHITE_VALUE, Light)
from homeassistant.const import CONF_NAME
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util

from custom_components.xknx import ATTR_DISCOVER_DEVICES, DATA_XKNX


CONF_ADDRESS = 'address'
CONF_STATE_ADDRESS = 'state_address'
CONF_BRIGHTNESS_ADDRESS = 'brightness_address'
Expand All @@ -29,13 +28,16 @@
CONF_COLOR_TEMP_ADDRESS = 'color_temperature_address'
CONF_COLOR_TEMP_STATE_ADDRESS = 'color_temperature_state_address'
CONF_COLOR_TEMP_MODE = 'color_temperature_mode'
CONF_RGBW_ADDRESS = 'rgbw_address'
CONF_RGBW_STATE_ADDRESS = 'rgbw_state_address'
CONF_MIN_KELVIN = 'min_kelvin'
CONF_MAX_KELVIN = 'max_kelvin'

DEFAULT_NAME = 'XKNX Light'
DEFAULT_COLOR = [255, 255, 255]
DEFAULT_COLOR = (0., 0.)
DEFAULT_BRIGHTNESS = 255
DEFAULT_COLOR_TEMP_MODE = 'absolute'
DEFAULT_WHITE_VALUE = 255
DEFAULT_MIN_KELVIN = 2700 # 370 mireds
DEFAULT_MAX_KELVIN = 6000 # 166 mireds
DEPENDENCIES = ['xknx']
Expand All @@ -60,6 +62,8 @@ class ColorTempModes(Enum):
vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): cv.string,
vol.Optional(CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE):
cv.enum(ColorTempModes),
vol.Optional(CONF_RGBW_ADDRESS): cv.string,
vol.Optional(CONF_RGBW_STATE_ADDRESS): cv.string,
vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN):
Expand Down Expand Up @@ -114,6 +118,8 @@ def async_add_entities_config(hass, config, async_add_entities):
CONF_BRIGHTNESS_STATE_ADDRESS),
group_address_color=config.get(CONF_COLOR_ADDRESS),
group_address_color_state=config.get(CONF_COLOR_STATE_ADDRESS),
group_address_rgbw=config.get(CONF_RGBW_ADDRESS),
group_address_rgbw_state=config.get(CONF_RGBW_STATE_ADDRESS),
group_address_tunable_white=group_address_tunable_white,
group_address_tunable_white_state=group_address_tunable_white_state,
group_address_color_temperature=group_address_color_temp,
Expand Down Expand Up @@ -168,23 +174,27 @@ def should_poll(self):
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
if self.device.supports_color:
if self.device.current_color is None:
return None
return max(self.device.current_color)
if self.device.supports_brightness:
return self.device.current_brightness
if (self.device.supports_color or self.device.supports_rgbw) and self.device.current_color:
return max(self.device.current_color)
return None

@property
def hs_color(self):
"""Return the HS color value."""
if self.device.supports_color:
rgb = self.device.current_color
if rgb is None:
return None
return color_util.color_RGB_to_hs(*rgb)
return None
rgb = None
if self.device.supports_rgbw or self.device.supports_color:
rgb, _ = self.device.current_color
return color_util.color_RGB_to_hs(*rgb) if rgb else None

@property
def white_value(self):
"""Return the white value."""
white = None
if self.device.supports_rgbw:
_, white = self.device.current_color
return white

@property
def color_temp(self):
Expand All @@ -199,9 +209,8 @@ def color_temp(self):
# as KNX devices typically use Kelvin we use it as base for
# calculating ct from percent
return color_util.color_temperature_kelvin_to_mired(
self._min_kelvin + (
(relative_ct / 255) *
(self._max_kelvin - self._min_kelvin)))
self._min_kelvin + ((relative_ct / 255) * (
self._max_kelvin - self._min_kelvin)))
return None

@property
Expand Down Expand Up @@ -237,6 +246,8 @@ def supported_features(self):
flags |= SUPPORT_BRIGHTNESS
if self.device.supports_color:
flags |= SUPPORT_COLOR | SUPPORT_BRIGHTNESS
if self.device.supports_rgbw:
flags |= SUPPORT_COLOR | SUPPORT_WHITE_VALUE
if self.device.supports_color_temperature or \
self.device.supports_tunable_white:
flags |= SUPPORT_COLOR_TEMP
Expand All @@ -246,10 +257,12 @@ async def async_turn_on(self, **kwargs):
"""Turn the light on."""
brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness)
hs_color = kwargs.get(ATTR_HS_COLOR, self.hs_color)
white_value = kwargs.get(ATTR_WHITE_VALUE, self.white_value)
mireds = kwargs.get(ATTR_COLOR_TEMP, self.color_temp)

update_brightness = ATTR_BRIGHTNESS in kwargs
update_color = ATTR_HS_COLOR in kwargs
update_white_value = ATTR_WHITE_VALUE in kwargs
update_color_temp = ATTR_COLOR_TEMP in kwargs

# always only go one path for turning on (avoid conflicting changes
Expand All @@ -260,17 +273,19 @@ async def async_turn_on(self, **kwargs):
# directly if supported; don't do it if color also has to be
# changed, as RGB color implicitly sets the brightness as well
await self.device.set_brightness(brightness)
elif self.device.supports_color and \
(update_brightness or update_color):
# change RGB color (includes brightness)
elif (self.device.supports_rgbw or self.device.supports_color) and \
(update_brightness or update_color or update_white_value):
# change RGB color, white value )if supported), and brightness
# if brightness or hs_color was not yet set use the default value
# to calculate RGB from as a fallback
if brightness is None:
brightness = DEFAULT_BRIGHTNESS
if hs_color is None:
hs_color = DEFAULT_COLOR
await self.device.set_color(
color_util.color_hsv_to_RGB(*hs_color, brightness * 100 / 255))
if white_value is None and self.device.supports_rgbw:
white_value = DEFAULT_WHITE_VALUE
rgb = color_util.color_hsv_to_RGB(*hs_color, brightness * 100 / 255)
await self.device.set_color(rgb, white_value)
elif self.device.supports_color_temperature and \
update_color_temp:
# change color temperature without ON telegram
Expand All @@ -284,13 +299,14 @@ async def async_turn_on(self, **kwargs):
update_color_temp:
# calculate relative_ct from Kelvin to fit typical KNX devices
kelvin = int(color_util.color_temperature_mired_to_kelvin(mireds))
relative_ct = int(255 * (kelvin - self._min_kelvin) /
(self._max_kelvin - self._min_kelvin))
relative_ct = int(255 * (kelvin - self._min_kelvin) / (
self._max_kelvin - self._min_kelvin))
await self.device.set_tunable_white(relative_ct)
else:
# no color/brightness change requested, so just turn it on
await self.device.set_on()

async def async_turn_off(self, **kwargs):
"""Turn the light off."""
del kwargs
await self.device.set_off()
100 changes: 91 additions & 9 deletions test/light_test.py
Expand Up @@ -65,6 +65,30 @@ def test_supports_color_false(self):
group_address_switch='1/6/4')
self.assertFalse(light.supports_color)

#
# TEST SUPPORT COLOR RGBW
#
def test_supports_rgbw_true(self):
"""Test supports_rgbw true."""
xknx = XKNX(loop=self.loop)
light = Light(
xknx,
'Diningroom.Light_1',
group_address_switch='1/6/4',
group_address_rgbw='1/6/5',
group_address_color='1/6/6')
self.assertTrue(light.supports_rgbw)

def test_supports_rgbw_false(self):
"""Test supports_color false."""
xknx = XKNX(loop=self.loop)
light = Light(
xknx,
'Diningroom.Light_1',
group_address_switch='1/6/4',
group_address_color='1/6/6')
self.assertFalse(light.supports_rgbw)

#
# TEST SUPPORT TUNABLE WHITE
#
Expand Down Expand Up @@ -117,10 +141,11 @@ def test_sync(self):
group_address_brightness_state='1/2/5',
group_address_color_state='1/2/6',
group_address_tunable_white_state='1/2/7',
group_address_color_temperature_state='1/2/8')
group_address_color_temperature_state='1/2/8',
group_address_rgbw_state='1/2/9')
self.loop.run_until_complete(asyncio.Task(light.sync(False)))

self.assertEqual(xknx.telegrams.qsize(), 5)
self.assertEqual(xknx.telegrams.qsize(), 6)

telegram1 = xknx.telegrams.get_nowait()
self.assertEqual(telegram1,
Expand All @@ -130,6 +155,10 @@ def test_sync(self):
self.assertEqual(telegram2,
Telegram(GroupAddress('1/2/6'), TelegramType.GROUP_READ))

telegram6 = xknx.telegrams.get_nowait()
self.assertEqual(telegram6,
Telegram(GroupAddress('1/2/9'), TelegramType.GROUP_READ))

telegram3 = xknx.telegrams.get_nowait()
self.assertEqual(telegram3,
Telegram(GroupAddress('1/2/5'), TelegramType.GROUP_READ))
Expand Down Expand Up @@ -159,17 +188,22 @@ def test_sync_state_address(self):
group_address_tunable_white='1/2/9',
group_address_tunable_white_state='1/2/10',
group_address_color_temperature='1/2/11',
group_address_color_temperature_state='1/2/12')
group_address_color_temperature_state='1/2/12',
group_address_rgbw='1/2/13',
group_address_rgbw_state='1/2/14')
self.loop.run_until_complete(asyncio.Task(light.sync(False)))

self.assertEqual(xknx.telegrams.qsize(), 5)
self.assertEqual(xknx.telegrams.qsize(), 6)

telegram1 = xknx.telegrams.get_nowait()
self.assertEqual(telegram1,
Telegram(GroupAddress('1/2/4'), TelegramType.GROUP_READ))
telegram2 = xknx.telegrams.get_nowait()
self.assertEqual(telegram2,
Telegram(GroupAddress('1/2/8'), TelegramType.GROUP_READ))
telegram6 = xknx.telegrams.get_nowait()
self.assertEqual(telegram6,
Telegram(GroupAddress('1/2/14'), TelegramType.GROUP_READ))
telegram3 = xknx.telegrams.get_nowait()
self.assertEqual(telegram3,
Telegram(GroupAddress('1/2/6'), TelegramType.GROUP_READ))
Expand Down Expand Up @@ -255,7 +289,7 @@ def test_set_color(self):
telegram = xknx.telegrams.get_nowait()
self.assertEqual(telegram,
Telegram(GroupAddress('1/2/5'), payload=DPTArray((23, 24, 25))))
self.assertEqual(light.current_color, (23, 24, 25))
self.assertEqual(light.current_color, ((23, 24, 25), None))

def test_set_color_not_possible(self):
"""Test setting the color of a non light without color."""
Expand All @@ -269,6 +303,37 @@ def test_set_color_not_possible(self):
self.assertEqual(xknx.telegrams.qsize(), 0)
mock_warn.assert_called_with('Colors not supported for device %s', 'TestLight')

#
# TEST SET COLOR AS RGBW
#
def test_set_color_rgbw(self):
"""Test setting RGBW value of a Light."""
xknx = XKNX(loop=self.loop)
light = Light(xknx,
name="TestLight",
group_address_switch='1/2/3',
group_address_color='1/2/4',
group_address_rgbw='1/2/5')
self.loop.run_until_complete(asyncio.Task(light.set_color((23, 24, 25), 26)))
self.assertEqual(xknx.telegrams.qsize(), 1)
telegram = xknx.telegrams.get_nowait()
self.assertEqual(telegram,
Telegram(GroupAddress('1/2/5'), payload=DPTArray((0, 15, 23, 24, 25, 26))))
self.assertEqual(light.current_color, ([23, 24, 25], 26))

def test_set_color_rgbw_not_possible(self):
"""Test setting RGBW value of a non light without color."""
# pylint: disable=invalid-name
xknx = XKNX(loop=self.loop)
light = Light(xknx,
name="TestLight",
group_address_switch='1/2/3',
group_address_color='1/2/4')
with patch('logging.Logger.warning') as mock_warn:
self.loop.run_until_complete(asyncio.Task(light.set_color((23, 24, 25), 26)))
self.assertEqual(xknx.telegrams.qsize(), 0)
mock_warn.assert_called_with('RGBW not supported for device %s', 'TestLight')

#
# TEST SET TUNABLE WHITE
#
Expand Down Expand Up @@ -410,10 +475,23 @@ def test_process_color(self):
name="TestLight",
group_address_switch='1/2/3',
group_address_color='1/2/5')
self.assertEqual(light.current_color, None)
self.assertEqual(light.current_color, (None, None))
telegram = Telegram(GroupAddress('1/2/5'), payload=DPTArray((23, 24, 25)))
self.loop.run_until_complete(asyncio.Task(light.process(telegram)))
self.assertEqual(light.current_color, (23, 24, 25))
self.assertEqual(light.current_color, ((23, 24, 25), None))

def test_process_color_rgbw(self):
"""Test process / reading telegrams from telegram queue. Test if RGBW is processed."""
xknx = XKNX(loop=self.loop)
light = Light(xknx,
name="TestLight",
group_address_switch='1/2/3',
group_address_color='1/2/4',
group_address_rgbw='1/2/5')
self.assertEqual(light.current_color, (None, None))
telegram = Telegram(GroupAddress('1/2/5'), payload=DPTArray((0, 15, 23, 24, 25, 26)))
self.loop.run_until_complete(asyncio.Task(light.process(telegram)))
self.assertEqual(light.current_color, ([23, 24, 25], 26))

def test_process_tunable_white(self):
"""Test process / reading telegrams from telegram queue. Test if tunable white is processed."""
Expand Down Expand Up @@ -537,7 +615,9 @@ def test_has_group_address(self):
group_address_tunable_white='1/7/7',
group_address_tunable_white_state='1/7/8',
group_address_color_temperature='1/7/9',
group_address_color_temperature_state='1/7/10')
group_address_color_temperature_state='1/7/10',
group_address_rgbw='1/7/11',
group_address_rgbw_state='1/7/12',)
self.assertTrue(light.has_group_address(GroupAddress('1/7/1')))
self.assertTrue(light.has_group_address(GroupAddress('1/7/2')))
self.assertTrue(light.has_group_address(GroupAddress('1/7/3')))
Expand All @@ -548,4 +628,6 @@ def test_has_group_address(self):
self.assertTrue(light.has_group_address(GroupAddress('1/7/8')))
self.assertTrue(light.has_group_address(GroupAddress('1/7/9')))
self.assertTrue(light.has_group_address(GroupAddress('1/7/10')))
self.assertFalse(light.has_group_address(GroupAddress('1/7/11')))
self.assertTrue(light.has_group_address(GroupAddress('1/7/11')))
self.assertTrue(light.has_group_address(GroupAddress('1/7/12')))
self.assertFalse(light.has_group_address(GroupAddress('1/7/13')))

0 comments on commit 428ba34

Please sign in to comment.