From 8b50ce0ef9ce23e17df38c09c053459649263666 Mon Sep 17 00:00:00 2001 From: farmio Date: Sun, 7 Oct 2018 00:43:14 +0200 Subject: [PATCH 01/12] tunable white function for lights - added tunable white function for setting relative color temperature for lights - tests for tunable white function - whitelist 'make' for tox - removed double whitespace in device.py --- test/light_test.py | 115 +++++++++++++++++++++++++++++++++++++++--- tox.ini | 1 + xknx/devices/light.py | 66 ++++++++++++++++++++---- 3 files changed, 165 insertions(+), 17 deletions(-) diff --git a/test/light_test.py b/test/light_test.py index be362b269..a6ea03ad6 100644 --- a/test/light_test.py +++ b/test/light_test.py @@ -65,6 +65,26 @@ def test_supports_color_false(self): group_address_switch='1/6/4') self.assertFalse(light.supports_color) + # + # TEST SUPPORT TUNABLE WHITE + # + def test_supports_tw_yes(self): + """Test supports_tw attribute with a light with tunable white function.""" + xknx = XKNX(loop=self.loop) + light = Light(xknx, + 'Diningroom.Light_1', + group_address_switch='1/6/4', + group_address_tunable_white='1/6/6') + self.assertTrue(light.supports_tunable_white) + + def test_supports_tw_no(self): + """Test supports_tw attribute with a Light without tunable white function.""" + xknx = XKNX(loop=self.loop) + light = Light(xknx, + 'Diningroom.Light_1', + group_address_switch='1/6/4') + self.assertFalse(light.supports_tunable_white) + # # SYNC # @@ -75,10 +95,11 @@ def test_sync(self): name="TestLight", group_address_switch_state='1/2/3', group_address_brightness_state='1/2/5', - group_address_color_state='1/2/6') + group_address_color_state='1/2/6', + group_address_tunable_white_state='1/2/7') self.loop.run_until_complete(asyncio.Task(light.sync(False))) - self.assertEqual(xknx.telegrams.qsize(), 3) + self.assertEqual(xknx.telegrams.qsize(), 4) telegram1 = xknx.telegrams.get_nowait() self.assertEqual(telegram1, @@ -92,6 +113,10 @@ def test_sync(self): self.assertEqual(telegram3, Telegram(GroupAddress('1/2/5'), TelegramType.GROUP_READ)) + telegram4 = xknx.telegrams.get_nowait() + self.assertEqual(telegram4, + Telegram(GroupAddress('1/2/7'), TelegramType.GROUP_READ)) + # # SYNC WITH STATE ADDRESS # @@ -105,10 +130,12 @@ def test_sync_state_address(self): group_address_brightness='1/2/5', group_address_brightness_state='1/2/6', group_address_color='1/2/7', - group_address_color_state='1/2/8') + group_address_color_state='1/2/8', + group_address_tunable_white='1/2/9', + group_address_tunable_white_state='1/2/10') self.loop.run_until_complete(asyncio.Task(light.sync(False))) - self.assertEqual(xknx.telegrams.qsize(), 3) + self.assertEqual(xknx.telegrams.qsize(), 4) telegram1 = xknx.telegrams.get_nowait() self.assertEqual(telegram1, @@ -119,6 +146,9 @@ def test_sync_state_address(self): telegram3 = xknx.telegrams.get_nowait() self.assertEqual(telegram3, Telegram(GroupAddress('1/2/6'), TelegramType.GROUP_READ)) + telegram4 = xknx.telegrams.get_nowait() + self.assertEqual(telegram4, + Telegram(GroupAddress('1/2/10'), TelegramType.GROUP_READ)) # # TEST SET ON @@ -209,6 +239,34 @@ 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 TUNABLE WHITE + # + def test_set_tw(self): + """Test setting the tunable white value of a Light.""" + xknx = XKNX(loop=self.loop) + light = Light(xknx, + name="TestLight", + group_address_switch='1/2/3', + group_address_tunable_white='1/2/5') + self.loop.run_until_complete(asyncio.Task(light.set_tunable_white(23))) + self.assertEqual(xknx.telegrams.qsize(), 1) + telegram = xknx.telegrams.get_nowait() + self.assertEqual(telegram, + Telegram(GroupAddress('1/2/5'), payload=DPTArray(23))) + + def test_set_tw_not_dimmable(self): + """Test setting the tunable white value of a non tw Light.""" + # pylint: disable=invalid-name + xknx = XKNX(loop=self.loop) + light = Light(xknx, + name="TestLight", + group_address_switch='1/2/3') + with patch('logging.Logger.warning') as mock_warn: + self.loop.run_until_complete(asyncio.Task(light.set_tunable_white(23))) + self.assertEqual(xknx.telegrams.qsize(), 0) + mock_warn.assert_called_with('Tunable white not supported for device %s', 'TestLight') + # # TEST PROCESS # @@ -299,6 +357,42 @@ def test_process_color(self): self.loop.run_until_complete(asyncio.Task(light.process(telegram))) self.assertEqual(light.current_color, (23, 24, 25)) + def test_process_tunable_white(self): + """Test process / reading telegrams from telegram queue. Test if tunable white is processed.""" + xknx = XKNX(loop=self.loop) + light = Light(xknx, + name="TestLight", + group_address_switch='1/2/3', + group_address_tunable_white='1/2/5') + self.assertEqual(light.current_tunable_white, None) + + telegram = Telegram(GroupAddress('1/2/5'), payload=DPTArray(23)) + self.loop.run_until_complete(asyncio.Task(light.process(telegram))) + self.assertEqual(light.current_tunable_white, 23) + + def test_process_tunable_white_wrong_payload(self): + """Test process wrong telegrams. (wrong payload type).""" + xknx = XKNX(loop=self.loop) + light = Light(xknx, + name="TestLight", + group_address_switch='1/2/3', + group_address_tunable_white='1/2/5') + telegram = Telegram(GroupAddress('1/2/5'), payload=DPTBinary(1)) + with self.assertRaises(CouldNotParseTelegram): + self.loop.run_until_complete(asyncio.Task(light.process(telegram))) + + def test_process_tunable_white_payload_invalid_length(self): + """Test process wrong telegrams. (wrong payload length).""" + # pylint: disable=invalid-name + xknx = XKNX(loop=self.loop) + light = Light(xknx, + name="TestLight", + group_address_switch='1/2/3', + group_address_tunable_white='1/2/5') + telegram = Telegram(GroupAddress('1/2/5'), payload=DPTArray((23, 24))) + with self.assertRaises(CouldNotParseTelegram): + self.loop.run_until_complete(asyncio.Task(light.process(telegram))) + # # TEST DO # @@ -308,11 +402,14 @@ def test_do(self): light = Light(xknx, name="TestLight", group_address_switch='1/2/3', - group_address_brightness='1/2/5') + group_address_brightness='1/2/5', + group_address_tunable_white='1/2/9') self.loop.run_until_complete(asyncio.Task(light.do("on"))) self.assertTrue(light.state) self.loop.run_until_complete(asyncio.Task(light.do("brightness:80"))) self.assertEqual(light.current_brightness, 80) + self.loop.run_until_complete(asyncio.Task(light.do("tunable_white:80"))) + self.assertEqual(light.current_tunable_white, 80) self.loop.run_until_complete(asyncio.Task(light.do("off"))) self.assertFalse(light.state) @@ -339,11 +436,15 @@ def test_has_group_address(self): group_address_brightness='1/7/3', group_address_brightness_state='1/7/4', group_address_color='1/7/5', - group_address_color_state='1/7/6') + group_address_color_state='1/7/6', + group_address_tunable_white='1/7/7', + group_address_tunable_white_state='1/7/8') 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'))) self.assertTrue(light.has_group_address(GroupAddress('1/7/4'))) self.assertTrue(light.has_group_address(GroupAddress('1/7/5'))) self.assertTrue(light.has_group_address(GroupAddress('1/7/6'))) - self.assertFalse(light.has_group_address(GroupAddress('1/7/7'))) + self.assertTrue(light.has_group_address(GroupAddress('1/7/7'))) + self.assertTrue(light.has_group_address(GroupAddress('1/7/8'))) + self.assertFalse(light.has_group_address(GroupAddress('1/7/9'))) diff --git a/tox.ini b/tox.ini index 2a6efb74a..8c75c97d5 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ setenv = PYTHONPATH = {toxinidir} commands = py.test --cov --cov-report= {posargs} +whitelist_externals = make deps = -rrequirements/testing.txt [testenv:flake8] diff --git a/xknx/devices/light.py b/xknx/devices/light.py index 290b4b54a..19b3cab7c 100644 --- a/xknx/devices/light.py +++ b/xknx/devices/light.py @@ -5,6 +5,8 @@ * switching light 'on' and 'off'. * setting the brightness. +* setting the color. +* setting the relative color temperature (tunable white). * reading the current state from KNX bus. """ from .device import Device @@ -25,6 +27,8 @@ def __init__(self, group_address_brightness_state=None, group_address_color=None, group_address_color_state=None, + group_address_tunable_white=None, + group_address_tunable_white_state=None, device_updated_cb=None): """Initialize Light class.""" # pylint: disable=too-many-arguments @@ -53,16 +57,30 @@ def __init__(self, device_name=self.name, after_update_cb=self.after_update) + self.tunable_white = RemoteValueScaling( + xknx, + group_address_tunable_white, + group_address_tunable_white_state, + device_name=self.name, + after_update_cb=self.after_update, + range_from=0, + range_to=255) + @property def supports_brightness(self): - """Return if cover supports direct positioning.""" + """Return if light supports brightness.""" return self.brightness.initialized @property def supports_color(self): - """Return if cover supports direct positioning.""" + """Return if light supports color.""" return self.color.initialized + @property + def supports_tunable_white(self): + """Return if light supports tunable white / relative color temperature.""" + return self.tunable_white.initialized + @classmethod def from_config(cls, xknx, name, config): """Initialize object from configuration structure.""" @@ -78,6 +96,10 @@ def from_config(cls, xknx, name, config): config.get('group_address_color') group_address_color_state = \ config.get('group_address_color_state') + group_address_tunable_white = \ + config.get('group_address_tunable_white') + group_address_tunable_white_state = \ + config.get('group_address_tunable_white_state') return cls(xknx, name, @@ -86,13 +108,16 @@ def from_config(cls, xknx, name, config): group_address_brightness=group_address_brightness, group_address_brightness_state=group_address_brightness_state, group_address_color=group_address_color, - group_address_color_state=group_address_color_state) + group_address_color_state=group_address_color_state, + group_address_tunable_white=group_address_tunable_white, + group_address_tunable_white_state=group_address_tunable_white_state) def has_group_address(self, group_address): """Test if device has given group address.""" return (self.switch.has_group_address(group_address) or self.brightness.has_group_address(group_address) or - self.color.has_group_address(group_address)) + self.color.has_group_address(group_address) or + self.tunable_white.has_group_address(group_address)) def __str__(self): """Return object as readable string.""" @@ -104,13 +129,18 @@ def __str__(self): ' color="{0}"'.format( self.color.group_addr_str()) + str_tunable_white = '' if not self.supports_tunable_white else \ + ' tunable white="{0}"'.format( + self.tunable_white.group_addr_str()) + return '' \ + 'switch="{1}"{2}{3}{4} />' \ .format( self.name, self.switch.group_addr_str(), str_brightness, - str_color) + str_color, + str_tunable_white) @property def state(self): @@ -127,7 +157,7 @@ async def set_off(self): @property def current_brightness(self): - """Return current color of light.""" + """Return current brightness of light.""" return self.brightness.value async def set_brightness(self, brightness): @@ -137,6 +167,11 @@ async def set_brightness(self, brightness): return await self.brightness.set(brightness) + @property + def current_color(self): + """Return current color of light.""" + return self.color.value + async def set_color(self, color): """Set color of light.""" if not self.supports_color: @@ -145,9 +180,16 @@ async def set_color(self, color): await self.color.set(color) @property - def current_color(self): - """Return current color of light.""" - return self.color.value + def current_tunable_white(self): + """Return current relative color temperature of light.""" + return self.tunable_white.value + + async def set_tunable_white(self, tunable_white): + """Set relative color temperature of light.""" + if not self.supports_tunable_white: + self.xknx.logger.warning("Tunable white not supported for device %s", self.get_name()) + return + await self.tunable_white.set(tunable_white) async def do(self, action): """Execute 'do' commands.""" @@ -157,6 +199,8 @@ async def do(self, action): await self.set_off() elif action.startswith("brightness:"): await self.set_brightness(int(action[11:])) + elif action.startswith("tunable_white:"): + await self.set_tunable_white(int(action[14:])) else: self.xknx.logger.warning("Could not understand action %s for device %s", action, self.get_name()) @@ -166,6 +210,7 @@ def state_addresses(self): state_addresses.extend(self.switch.state_addresses()) state_addresses.extend(self.color.state_addresses()) state_addresses.extend(self.brightness.state_addresses()) + state_addresses.extend(self.tunable_white.state_addresses()) return state_addresses async def process_group_write(self, telegram): @@ -173,6 +218,7 @@ async def process_group_write(self, telegram): await self.switch.process(telegram) await self.color.process(telegram) await self.brightness.process(telegram) + await self.tunable_white.process(telegram) def __eq__(self, other): """Equal operator.""" From eb90d7a5103665a05d6b9f46a1de0d87b3d8b3f2 Mon Sep 17 00:00:00 2001 From: farmio Date: Sun, 7 Oct 2018 02:13:44 +0200 Subject: [PATCH 02/12] add RemoteValueDpt2ByteUnsigned --- test/remote_value_dpt_2_byte_unsigned_test.py | 99 +++++++++++++++++++ xknx/devices/__init__.py | 1 + .../remote_value_dpt_2_byte_unsigned.py | 36 +++++++ xknx/knx/dpt_2byte.py | 2 + 4 files changed, 138 insertions(+) create mode 100644 test/remote_value_dpt_2_byte_unsigned_test.py create mode 100644 xknx/devices/remote_value_dpt_2_byte_unsigned.py diff --git a/test/remote_value_dpt_2_byte_unsigned_test.py b/test/remote_value_dpt_2_byte_unsigned_test.py new file mode 100644 index 000000000..9989e2e38 --- /dev/null +++ b/test/remote_value_dpt_2_byte_unsigned_test.py @@ -0,0 +1,99 @@ +"""Unit test for RemoteValueDpt2ByteUnsigned objects.""" +import asyncio +import unittest + +from xknx import XKNX +from xknx.knx import DPTArray, DPTBinary, Telegram, GroupAddress +from xknx.exceptions import ConversionError, CouldNotParseTelegram +from xknx.devices import RemoteValueDpt2ByteUnsigned + + +class TestRemoteValueDptValue1Ucount(unittest.TestCase): + """Test class for RemoteValueDpt2ByteUnsigned objects.""" + + def setUp(self): + """Set up test class.""" + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + def tearDown(self): + """Tear down test class.""" + self.loop.close() + + def test_to_knx(self): + """Test to_knx function with normal operation.""" + xknx = XKNX(loop=self.loop) + remote_value = RemoteValueDpt2ByteUnsigned(xknx) + self.assertEqual(remote_value.to_knx(2571), DPTArray((0x0A, 0x0B))) + + def test_from_knx(self): + """Test from_knx function with normal operation.""" + xknx = XKNX(loop=self.loop) + remote_value = RemoteValueDpt2ByteUnsigned(xknx) + self.assertEqual(remote_value.from_knx(DPTArray((0x0A, 0x0B))), 2571) + + def test_to_knx_error(self): + """Test to_knx function with wrong parametern.""" + xknx = XKNX(loop=self.loop) + remote_value = RemoteValueDpt2ByteUnsigned(xknx) + with self.assertRaises(ConversionError): + remote_value.to_knx(65536) + with self.assertRaises(ConversionError): + remote_value.to_knx("256") + + def test_set(self): + """Test setting value.""" + xknx = XKNX(loop=self.loop) + remote_value = RemoteValueDpt2ByteUnsigned( + xknx, + group_address=GroupAddress("1/2/3")) + self.loop.run_until_complete(asyncio.Task(remote_value.set(2571))) + self.assertEqual(xknx.telegrams.qsize(), 1) + telegram = xknx.telegrams.get_nowait() + self.assertEqual( + telegram, + Telegram( + GroupAddress('1/2/3'), + payload=DPTArray((0x0A, 0x0B)))) + self.loop.run_until_complete(asyncio.Task(remote_value.set(5500))) + self.assertEqual(xknx.telegrams.qsize(), 1) + telegram = xknx.telegrams.get_nowait() + self.assertEqual( + telegram, + Telegram( + GroupAddress('1/2/3'), + payload=DPTArray((0x15, 0x7C, )))) + + def test_process(self): + """Test process telegram.""" + xknx = XKNX(loop=self.loop) + remote_value = RemoteValueDpt2ByteUnsigned( + xknx, + group_address=GroupAddress("1/2/3")) + telegram = Telegram( + group_address=GroupAddress("1/2/3"), + payload=DPTArray((0x0A, 0x0B))) + self.loop.run_until_complete(asyncio.Task(remote_value.process(telegram))) + self.assertEqual(remote_value.value, 2571) + + def test_to_process_error(self): + """Test process errornous telegram.""" + xknx = XKNX(loop=self.loop) + remote_value = RemoteValueDpt2ByteUnsigned( + xknx, + group_address=GroupAddress("1/2/3")) + with self.assertRaises(CouldNotParseTelegram): + telegram = Telegram( + group_address=GroupAddress("1/2/3"), + payload=DPTBinary(1)) + self.loop.run_until_complete(asyncio.Task(remote_value.process(telegram))) + with self.assertRaises(CouldNotParseTelegram): + telegram = Telegram( + group_address=GroupAddress("1/2/3"), + payload=DPTArray((0x64, ))) + self.loop.run_until_complete(asyncio.Task(remote_value.process(telegram))) + with self.assertRaises(CouldNotParseTelegram): + telegram = Telegram( + group_address=GroupAddress("1/2/3"), + payload=DPTArray((0x64, 0x53, 0x42, ))) + self.loop.run_until_complete(asyncio.Task(remote_value.process(telegram))) diff --git a/xknx/devices/__init__.py b/xknx/devices/__init__.py index 71388b134..6e7bdfa87 100644 --- a/xknx/devices/__init__.py +++ b/xknx/devices/__init__.py @@ -28,3 +28,4 @@ from .remote_value_temp import RemoteValueTemp from .remote_value_scaling import RemoteValueScaling from. remote_value_dpt_value_1_ucount import RemoteValueDptValue1Ucount +from. remote_value_dpt_2_byte_unsigned import RemoteValueDpt2ByteUnsigned diff --git a/xknx/devices/remote_value_dpt_2_byte_unsigned.py b/xknx/devices/remote_value_dpt_2_byte_unsigned.py new file mode 100644 index 000000000..ef24ee4fd --- /dev/null +++ b/xknx/devices/remote_value_dpt_2_byte_unsigned.py @@ -0,0 +1,36 @@ +""" +Module for managing a DTP 7001 remote value. + +DPT 7.001. +""" +from xknx.knx import DPTArray, DPT2ByteUnsigned + +from .remote_value import RemoteValue + + +class RemoteValueDpt2ByteUnsigned(RemoteValue): + """Abstraction for remote value of KNX DPT 7.001.""" + + def __init__(self, + xknx, + group_address=None, + device_name=None, + after_update_cb=None): + """Initialize remote value of KNX DPT 7.001.""" + # pylint: disable=too-many-arguments + super(RemoteValueDpt2ByteUnsigned, self).__init__( + xknx, group_address, None, + device_name=device_name, after_update_cb=after_update_cb) + + def payload_valid(self, payload): + """Test if telegram payload may be parsed.""" + return (isinstance(payload, DPTArray) + and len(payload.value) == 2) + + def to_knx(self, value): + """Convert value to payload.""" + return DPTArray(DPT2ByteUnsigned.to_knx(value)) + + def from_knx(self, payload): + """Convert current payload to value.""" + return DPT2ByteUnsigned.from_knx(payload.value) diff --git a/xknx/knx/dpt_2byte.py b/xknx/knx/dpt_2byte.py index be00e0362..ed1bfee5a 100644 --- a/xknx/knx/dpt_2byte.py +++ b/xknx/knx/dpt_2byte.py @@ -29,6 +29,8 @@ def from_knx(cls, raw): @classmethod def to_knx(cls, value): """Serialize to KNX/IP raw data.""" + if not isinstance(value, (int)): + raise ConversionError("Cant serialize DPT2ByteUnsigned", value=value) if not cls._test_boundaries(value): raise ConversionError("Cant serialize DPT2ByteUnsigned", value=value) return value >> 8, value & 0xff From 9333226bbb8580ae5d0bd795d2de6efc64eb6c48 Mon Sep 17 00:00:00 2001 From: farmio Date: Sun, 7 Oct 2018 11:17:08 +0200 Subject: [PATCH 03/12] color temperature function for light - added color temperature function for setting absolute color temperature values for lights - added tests for color remperature function - reordered DPTMAP in remote_value_sensor.py by DPT number - added DPTColorTemperature class for DPT 7.600 --- test/light_test.py | 117 ++++++++++++++++-- xknx/devices/light.py | 52 +++++++- .../remote_value_dpt_2_byte_unsigned.py | 3 +- xknx/devices/remote_value_sensor.py | 17 +-- xknx/knx/__init__.py | 2 +- xknx/knx/dpt_2byte.py | 6 + 6 files changed, 175 insertions(+), 22 deletions(-) diff --git a/test/light_test.py b/test/light_test.py index a6ea03ad6..4fd76a601 100644 --- a/test/light_test.py +++ b/test/light_test.py @@ -85,6 +85,26 @@ def test_supports_tw_no(self): group_address_switch='1/6/4') self.assertFalse(light.supports_tunable_white) + # + # TEST SUPPORT COLOR TEMPERATURE + # + def test_supports_color_temp_true(self): + """Test supports_color_temp attribute with a light with color temperature function.""" + xknx = XKNX(loop=self.loop) + light = Light(xknx, + 'Diningroom.Light_1', + group_address_switch='1/6/4', + group_address_color_temperature='1/6/6') + self.assertTrue(light.supports_color_temperature) + + def test_supports_color_temp_false(self): + """Test supports_color_temp attribute with a Light without color temperature function.""" + xknx = XKNX(loop=self.loop) + light = Light(xknx, + 'Diningroom.Light_1', + group_address_switch='1/6/4') + self.assertFalse(light.supports_color_temperature) + # # SYNC # @@ -96,10 +116,11 @@ def test_sync(self): group_address_switch_state='1/2/3', group_address_brightness_state='1/2/5', group_address_color_state='1/2/6', - group_address_tunable_white_state='1/2/7') + group_address_tunable_white_state='1/2/7', + group_address_color_temperature_state='1/2/8') self.loop.run_until_complete(asyncio.Task(light.sync(False))) - self.assertEqual(xknx.telegrams.qsize(), 4) + self.assertEqual(xknx.telegrams.qsize(), 5) telegram1 = xknx.telegrams.get_nowait() self.assertEqual(telegram1, @@ -117,6 +138,10 @@ def test_sync(self): self.assertEqual(telegram4, Telegram(GroupAddress('1/2/7'), TelegramType.GROUP_READ)) + telegram5 = xknx.telegrams.get_nowait() + self.assertEqual(telegram5, + Telegram(GroupAddress('1/2/8'), TelegramType.GROUP_READ)) + # # SYNC WITH STATE ADDRESS # @@ -132,10 +157,12 @@ def test_sync_state_address(self): group_address_color='1/2/7', group_address_color_state='1/2/8', group_address_tunable_white='1/2/9', - group_address_tunable_white_state='1/2/10') + group_address_tunable_white_state='1/2/10', + group_address_color_temperature='1/2/11', + group_address_color_temperature_state='1/2/12') self.loop.run_until_complete(asyncio.Task(light.sync(False))) - self.assertEqual(xknx.telegrams.qsize(), 4) + self.assertEqual(xknx.telegrams.qsize(), 5) telegram1 = xknx.telegrams.get_nowait() self.assertEqual(telegram1, @@ -149,6 +176,9 @@ def test_sync_state_address(self): telegram4 = xknx.telegrams.get_nowait() self.assertEqual(telegram4, Telegram(GroupAddress('1/2/10'), TelegramType.GROUP_READ)) + telegram5 = xknx.telegrams.get_nowait() + self.assertEqual(telegram5, + Telegram(GroupAddress('1/2/12'), TelegramType.GROUP_READ)) # # TEST SET ON @@ -255,7 +285,7 @@ def test_set_tw(self): self.assertEqual(telegram, Telegram(GroupAddress('1/2/5'), payload=DPTArray(23))) - def test_set_tw_not_dimmable(self): + def test_set_tw_unsupported(self): """Test setting the tunable white value of a non tw Light.""" # pylint: disable=invalid-name xknx = XKNX(loop=self.loop) @@ -267,6 +297,34 @@ def test_set_tw_not_dimmable(self): self.assertEqual(xknx.telegrams.qsize(), 0) mock_warn.assert_called_with('Tunable white not supported for device %s', 'TestLight') + # + # TEST SET COLOR TEMPERATURE + # + def test_set_color_temp(self): + """Test setting the color temperature value of a Light.""" + xknx = XKNX(loop=self.loop) + light = Light(xknx, + name="TestLight", + group_address_switch='1/2/3', + group_address_color_temperature='1/2/5') + self.loop.run_until_complete(asyncio.Task(light.set_color_temperature(4000))) + self.assertEqual(xknx.telegrams.qsize(), 1) + telegram = xknx.telegrams.get_nowait() + self.assertEqual(telegram, + Telegram(GroupAddress('1/2/5'), payload=DPTArray((0x0F, 0xA0, )))) + + def test_set_color_temp_unsupported(self): + """Test setting the color temperature value of an unsupported Light.""" + # pylint: disable=invalid-name + xknx = XKNX(loop=self.loop) + light = Light(xknx, + name="TestLight", + group_address_switch='1/2/3') + with patch('logging.Logger.warning') as mock_warn: + self.loop.run_until_complete(asyncio.Task(light.set_color_temperature(4000))) + self.assertEqual(xknx.telegrams.qsize(), 0) + mock_warn.assert_called_with('Absolute Color Temperature not supported for device %s', 'TestLight') + # # TEST PROCESS # @@ -393,6 +451,42 @@ def test_process_tunable_white_payload_invalid_length(self): with self.assertRaises(CouldNotParseTelegram): self.loop.run_until_complete(asyncio.Task(light.process(telegram))) + def test_process_color_temperature(self): + """Test process / reading telegrams from telegram queue. Test if color temperature is processed.""" + xknx = XKNX(loop=self.loop) + light = Light(xknx, + name="TestLight", + group_address_switch='1/2/3', + group_address_color_temperature='1/2/5') + self.assertEqual(light.current_color_temperature, None) + + telegram = Telegram(GroupAddress('1/2/5'), payload=DPTArray((0x0F, 0xA0, ))) + self.loop.run_until_complete(asyncio.Task(light.process(telegram))) + self.assertEqual(light.current_color_temperature, 4000) + + def test_process_color_temperature_wrong_payload(self): + """Test process wrong telegrams. (wrong payload type).""" + xknx = XKNX(loop=self.loop) + light = Light(xknx, + name="TestLight", + group_address_switch='1/2/3', + group_address_color_temperature='1/2/5') + telegram = Telegram(GroupAddress('1/2/5'), payload=DPTBinary(1)) + with self.assertRaises(CouldNotParseTelegram): + self.loop.run_until_complete(asyncio.Task(light.process(telegram))) + + def test_process_color_temperature_payload_invalid_length(self): + """Test process wrong telegrams. (wrong payload length).""" + # pylint: disable=invalid-name + xknx = XKNX(loop=self.loop) + light = Light(xknx, + name="TestLight", + group_address_switch='1/2/3', + group_address_color_temperature='1/2/5') + telegram = Telegram(GroupAddress('1/2/5'), payload=DPTArray((23))) + with self.assertRaises(CouldNotParseTelegram): + self.loop.run_until_complete(asyncio.Task(light.process(telegram))) + # # TEST DO # @@ -403,13 +497,16 @@ def test_do(self): name="TestLight", group_address_switch='1/2/3', group_address_brightness='1/2/5', - group_address_tunable_white='1/2/9') + group_address_tunable_white='1/2/9', + group_address_color_temperature='1/2/11') self.loop.run_until_complete(asyncio.Task(light.do("on"))) self.assertTrue(light.state) self.loop.run_until_complete(asyncio.Task(light.do("brightness:80"))) self.assertEqual(light.current_brightness, 80) self.loop.run_until_complete(asyncio.Task(light.do("tunable_white:80"))) self.assertEqual(light.current_tunable_white, 80) + self.loop.run_until_complete(asyncio.Task(light.do("color_temperature:3750"))) + self.assertEqual(light.current_color_temperature, 3750) self.loop.run_until_complete(asyncio.Task(light.do("off"))) self.assertFalse(light.state) @@ -438,7 +535,9 @@ def test_has_group_address(self): group_address_color='1/7/5', group_address_color_state='1/7/6', group_address_tunable_white='1/7/7', - group_address_tunable_white_state='1/7/8') + group_address_tunable_white_state='1/7/8', + group_address_color_temperature='1/7/9', + group_address_color_temperature_state='1/7/10') 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'))) @@ -447,4 +546,6 @@ def test_has_group_address(self): self.assertTrue(light.has_group_address(GroupAddress('1/7/6'))) self.assertTrue(light.has_group_address(GroupAddress('1/7/7'))) self.assertTrue(light.has_group_address(GroupAddress('1/7/8'))) - self.assertFalse(light.has_group_address(GroupAddress('1/7/9'))) + 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'))) diff --git a/xknx/devices/light.py b/xknx/devices/light.py index 19b3cab7c..8acbd92e5 100644 --- a/xknx/devices/light.py +++ b/xknx/devices/light.py @@ -7,10 +7,12 @@ * setting the brightness. * setting the color. * setting the relative color temperature (tunable white). +* setting the absolute color temperature. * reading the current state from KNX bus. """ from .device import Device from .remote_value_color_rgb import RemoteValueColorRGB +from .remote_value_dpt_2_byte_unsigned import RemoteValueDpt2ByteUnsigned from .remote_value_scaling import RemoteValueScaling from .remote_value_switch import RemoteValueSwitch @@ -29,6 +31,8 @@ def __init__(self, group_address_color_state=None, group_address_tunable_white=None, group_address_tunable_white_state=None, + group_address_color_temperature=None, + group_address_color_temperature_state=None, device_updated_cb=None): """Initialize Light class.""" # pylint: disable=too-many-arguments @@ -66,6 +70,13 @@ def __init__(self, range_from=0, range_to=255) + self.color_temperature = RemoteValueDpt2ByteUnsigned( + xknx, + group_address_color_temperature, + group_address_color_temperature_state, + device_name=self.name, + after_update_cb=self.after_update) + @property def supports_brightness(self): """Return if light supports brightness.""" @@ -81,6 +92,11 @@ def supports_tunable_white(self): """Return if light supports tunable white / relative color temperature.""" return self.tunable_white.initialized + @property + def supports_color_temperature(self): + """Return if light supports absolute color temperature.""" + return self.color_temperature.initialized + @classmethod def from_config(cls, xknx, name, config): """Initialize object from configuration structure.""" @@ -100,6 +116,10 @@ def from_config(cls, xknx, name, config): config.get('group_address_tunable_white') group_address_tunable_white_state = \ config.get('group_address_tunable_white_state') + group_address_color_temperature = \ + config.get('group_address_color_temperature') + group_address_color_temperature_state = \ + config.get('group_address_color_temperature_state') return cls(xknx, name, @@ -110,14 +130,17 @@ def from_config(cls, xknx, name, config): group_address_color=group_address_color, group_address_color_state=group_address_color_state, group_address_tunable_white=group_address_tunable_white, - group_address_tunable_white_state=group_address_tunable_white_state) + group_address_tunable_white_state=group_address_tunable_white_state, + group_address_color_temperature=group_address_color_temperature, + group_address_color_temperature_state=group_address_color_temperature_state,) def has_group_address(self, group_address): """Test if device has given group address.""" return (self.switch.has_group_address(group_address) or self.brightness.has_group_address(group_address) or self.color.has_group_address(group_address) or - self.tunable_white.has_group_address(group_address)) + self.tunable_white.has_group_address(group_address) or + self.color_temperature.has_group_address(group_address)) def __str__(self): """Return object as readable string.""" @@ -133,14 +156,19 @@ def __str__(self): ' tunable white="{0}"'.format( self.tunable_white.group_addr_str()) + str_color_temperature = '' if not self.supports_color_temperature else \ + ' color temperature="{0}"'.format( + self.color_temperature.group_addr_str()) + return '' \ + 'switch="{1}"{2}{3}{4}{5} />' \ .format( self.name, self.switch.group_addr_str(), str_brightness, str_color, - str_tunable_white) + str_tunable_white, + str_color_temperature) @property def state(self): @@ -191,6 +219,18 @@ async def set_tunable_white(self, tunable_white): return await self.tunable_white.set(tunable_white) + @property + def current_color_temperature(self): + """Return current absolute color temperature of light.""" + return self.color_temperature.value + + async def set_color_temperature(self, color_temperature): + """Set absolute color temperature of light.""" + if not self.supports_color_temperature: + self.xknx.logger.warning("Absolute Color Temperature not supported for device %s", self.get_name()) + return + await self.color_temperature.set(color_temperature) + async def do(self, action): """Execute 'do' commands.""" if action == "on": @@ -201,6 +241,8 @@ async def do(self, action): await self.set_brightness(int(action[11:])) elif action.startswith("tunable_white:"): await self.set_tunable_white(int(action[14:])) + elif action.startswith("color_temperature:"): + await self.set_color_temperature(int(action[18:])) else: self.xknx.logger.warning("Could not understand action %s for device %s", action, self.get_name()) @@ -211,6 +253,7 @@ def state_addresses(self): state_addresses.extend(self.color.state_addresses()) state_addresses.extend(self.brightness.state_addresses()) state_addresses.extend(self.tunable_white.state_addresses()) + state_addresses.extend(self.color_temperature.state_addresses()) return state_addresses async def process_group_write(self, telegram): @@ -219,6 +262,7 @@ async def process_group_write(self, telegram): await self.color.process(telegram) await self.brightness.process(telegram) await self.tunable_white.process(telegram) + await self.color_temperature.process(telegram) def __eq__(self, other): """Equal operator.""" diff --git a/xknx/devices/remote_value_dpt_2_byte_unsigned.py b/xknx/devices/remote_value_dpt_2_byte_unsigned.py index ef24ee4fd..fdd2d33ae 100644 --- a/xknx/devices/remote_value_dpt_2_byte_unsigned.py +++ b/xknx/devices/remote_value_dpt_2_byte_unsigned.py @@ -14,12 +14,13 @@ class RemoteValueDpt2ByteUnsigned(RemoteValue): def __init__(self, xknx, group_address=None, + group_address_state=None, device_name=None, after_update_cb=None): """Initialize remote value of KNX DPT 7.001.""" # pylint: disable=too-many-arguments super(RemoteValueDpt2ByteUnsigned, self).__init__( - xknx, group_address, None, + xknx, group_address, group_address_state, device_name=device_name, after_update_cb=after_update_cb) def payload_valid(self, payload): diff --git a/xknx/devices/remote_value_sensor.py b/xknx/devices/remote_value_sensor.py index 7d40da3fe..6f9c4c3f8 100644 --- a/xknx/devices/remote_value_sensor.py +++ b/xknx/devices/remote_value_sensor.py @@ -7,11 +7,11 @@ from xknx.exceptions import ConversionError from xknx.knx import ( DPT2ByteFloat, DPT2ByteUnsigned, DPT4ByteFloat, DPT4ByteSigned, - DPT4ByteUnsigned, DPTArray, DPTBrightness, DPTElectricCurrent, - DPTElectricPotential, DPTEnergy, DPTEnthalpy, DPTFrequency, - DPTHeatFlowRate, DPTHumidity, DPTLux, DPTPartsPerMillion, DPTPhaseAngleDeg, - DPTPhaseAngleRad, DPTPower, DPTPowerFactor, DPTSpeed, DPTTemperature, - DPTUElCurrentmA, DPTVoltage, DPTWsp) + DPT4ByteUnsigned, DPTArray, DPTBrightness, DPTColorTemperature, + DPTElectricCurrent, DPTElectricPotential, DPTEnergy, DPTEnthalpy, + DPTFrequency, DPTHeatFlowRate, DPTHumidity, DPTLux, DPTPartsPerMillion, + DPTPhaseAngleDeg, DPTPhaseAngleRad, DPTPower, DPTPowerFactor, DPTSpeed, + DPTTemperature, DPTUElCurrentmA, DPTVoltage, DPTWsp) from .remote_value import RemoteValue @@ -20,12 +20,13 @@ class RemoteValueSensor(RemoteValue): """Abstraction for many different sensor DPT types.""" DPTMAP = { + 'current': DPTUElCurrentmA, + 'brightness': DPTBrightness, + 'color_temperature': DPTColorTemperature, 'temperature': DPTTemperature, - 'humidity': DPTHumidity, 'illuminance': DPTLux, - 'brightness': DPTBrightness, 'speed_ms': DPTWsp, - 'current': DPTUElCurrentmA, + 'humidity': DPTHumidity, 'voltage': DPTVoltage, 'power': DPTPower, 'electric_current': DPTElectricCurrent, diff --git a/xknx/knx/__init__.py b/xknx/knx/__init__.py index 17ce56afd..78614da43 100644 --- a/xknx/knx/__init__.py +++ b/xknx/knx/__init__.py @@ -18,7 +18,7 @@ from .dpt_hvac_mode import HVACOperationMode, DPTHVACMode, \ DPTControllerStatus from .dpt_hvac_contr_mode import DPTHVACContrMode -from .dpt_2byte import DPT2ByteUnsigned, DPTUElCurrentmA, DPT2Ucount, DPTBrightness +from .dpt_2byte import DPT2ByteUnsigned, DPTUElCurrentmA, DPT2Ucount, DPTBrightness, DPTColorTemperature from .dpt_4byte import DPT4ByteUnsigned, DPT4ByteSigned from .dpt_scene_number import DPTSceneNumber from .dpt_time import DPTTime diff --git a/xknx/knx/dpt_2byte.py b/xknx/knx/dpt_2byte.py index ed1bfee5a..ca1cc14aa 100644 --- a/xknx/knx/dpt_2byte.py +++ b/xknx/knx/dpt_2byte.py @@ -57,3 +57,9 @@ class DPTBrightness(DPT2ByteUnsigned): """DPT 7.013 DPT_Brightness (lux).""" unit = "lx" + + +class DPTColorTemperature(DPT2ByteUnsigned): + """DPT 7.600 DPT_Color_Temperature (K).""" + + unit = "K" From e3a5c53f2868bfec7e12d9b6bca0dc59fc5952d9 Mon Sep 17 00:00:00 2001 From: farmio Date: Wed, 24 Oct 2018 12:54:20 +0200 Subject: [PATCH 04/12] tuneable white for home assistant plugin - added color temperature for home assistant plugin - added min and max kelvin / mireds --- .../custom_components/light/xknx.py | 69 +++++++++++++++++-- xknx/devices/light.py | 13 +++- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/home-assistant-plugin/custom_components/light/xknx.py b/home-assistant-plugin/custom_components/light/xknx.py index 25785e04c..b4d32afa2 100644 --- a/home-assistant-plugin/custom_components/light/xknx.py +++ b/home-assistant-plugin/custom_components/light/xknx.py @@ -9,8 +9,9 @@ from custom_components.xknx import ATTR_DISCOVER_DEVICES, DATA_XKNX from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, Light) + Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP, + ATTR_WHITE_VALUE, SUPPORT_WHITE_VALUE) from homeassistant.const import CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -22,10 +23,19 @@ CONF_BRIGHTNESS_STATE_ADDRESS = 'brightness_state_address' CONF_COLOR_ADDRESS = 'color_address' CONF_COLOR_STATE_ADDRESS = 'color_state_address' +CONF_KELVIN_ADDRESS = 'color_temperature_address' +CONF_KELVIN_STATE_ADDRESS = 'color_temperature_state_address' +CONF_WHITE_VALUE_ADDRESS = 'white_value_address' +CONF_WHITE_VALUE_STATE_ADDRESS = 'white_value_state_address' +CONF_MIN_KELVIN = 'min_kelvin' +CONF_MAX_KELVIN = 'max_kelvin' DEFAULT_NAME = 'XKNX Light' DEFAULT_COLOR = [255, 255, 255] DEFAULT_BRIGHTNESS = 255 +DEFAULT_COLOR_TEMPERATURE = 333 # 3000 K +DEFAULT_MIN_MIREDS = 166 # 6000 K +DEFAULT_MAX_MIREDS = 370 # 2700 K DEPENDENCIES = ['xknx'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -36,6 +46,12 @@ vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string, vol.Optional(CONF_COLOR_ADDRESS): cv.string, vol.Optional(CONF_COLOR_STATE_ADDRESS): cv.string, + vol.Optional(CONF_KELVIN_ADDRESS): cv.string, + vol.Optional(CONF_KELVIN_STATE_ADDRESS): cv.string, + vol.Optional(CONF_WHITE_VALUE_ADDRESS): cv.string, + vol.Optional(CONF_WHITE_VALUE_STATE_ADDRESS): cv.string, + vol.Optional(CONF_MIN_KELVIN): vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Optional(CONF_MAX_KELVIN): vol.All(vol.Coerce(int), vol.Range(min=1)), }) @@ -71,7 +87,13 @@ def async_add_entities_config(hass, config, async_add_entities): group_address_brightness_state=config.get( CONF_BRIGHTNESS_STATE_ADDRESS), group_address_color=config.get(CONF_COLOR_ADDRESS), - group_address_color_state=config.get(CONF_COLOR_STATE_ADDRESS)) + group_address_color_state=config.get(CONF_COLOR_STATE_ADDRESS), + group_address_tunable_white=config.get(CONF_WHITE_VALUE_ADDRESS), + group_address_tunable_white_state=config.get(CONF_WHITE_VALUE_STATE_ADDRESS), + group_address_color_temperature=config.get(CONF_KELVIN_ADDRESS), + group_address_color_temperature_state=config.get(CONF_KELVIN_STATE_ADDRESS), + min_kelvin=config.get(CONF_MIN_KELVIN), + max_kelvin=config.get(CONF_MAX_KELVIN)) hass.data[DATA_XKNX].xknx.devices.add(light) async_add_entities([KNXLight(light)]) @@ -133,12 +155,36 @@ def hs_color(self): @property def color_temp(self): - """Return the CT color temperature.""" + """Return the color temperature in mireds.""" + if self.device.supports_color_temperature: + kelvin = self.device.current_color_temperature + return color_util.color_temperature_kelvin_to_mired(kelvin) \ + if kelvin is not None else DEFAULT_COLOR_TEMPERATURE return None @property def white_value(self): """Return the white value of this light between 0..255.""" + if self.device.supports_tunable_white: + return self.device.current_tunable_white + return None + + @property + def min_mireds(self): + """Return the coldest color temperature this light supports in mireds.""" + if self.device.supports_color_temperature: + kelvin = self.device.max_kelvin + return color_util.color_temperature_kelvin_to_mired(kelvin) \ + if kelvin is not None else DEFAULT_MIN_MIREDS + return None + + @property + def max_mireds(self): + """Return the warmest color temperature this light supports in mireds.""" + if self.device.supports_color_temperature: + kelvin = self.device.min_kelvin + return color_util.color_temperature_kelvin_to_mired(kelvin) \ + if kelvin is not None else DEFAULT_MAX_MIREDS return None @property @@ -164,6 +210,10 @@ def supported_features(self): flags |= SUPPORT_BRIGHTNESS if self.device.supports_color: flags |= SUPPORT_COLOR | SUPPORT_BRIGHTNESS + if self.device.supports_color_temperature: + flags |= SUPPORT_COLOR_TEMP + if self.device.supports_tunable_white: + flags |= SUPPORT_WHITE_VALUE return flags async def async_turn_on(self, **kwargs): @@ -179,6 +229,7 @@ async def async_turn_on(self, **kwargs): update_brightness = ATTR_BRIGHTNESS in kwargs update_color = ATTR_HS_COLOR in kwargs + update_color_temp = ATTR_COLOR_TEMP in kwargs # always only go one path for turning on (avoid conflicting changes # and weird effects) @@ -193,6 +244,16 @@ async def async_turn_on(self, **kwargs): # change RGB color (includes brightness) await self.device.set_color( color_util.color_hsv_to_RGB(*hs_color, brightness * 100 / 255)) + elif self.device.supports_color_temperature and \ + update_color_temp: + # change color temperature without ON command + mireds = kwargs[ATTR_COLOR_TEMP] + if mireds > self.max_mireds: + mireds = self.max_mireds + elif mireds < self.min_mireds: + mireds = self.min_mireds + kelvin = int(color_util.color_temperature_mired_to_kelvin(mireds)) + await self.device.set_color_temperature(kelvin) else: # no color/brightness change requested, so just turn it on await self.device.set_on() diff --git a/xknx/devices/light.py b/xknx/devices/light.py index 8acbd92e5..8b7815038 100644 --- a/xknx/devices/light.py +++ b/xknx/devices/light.py @@ -33,6 +33,8 @@ def __init__(self, group_address_tunable_white_state=None, group_address_color_temperature=None, group_address_color_temperature_state=None, + min_kelvin=None, + max_kelvin=None, device_updated_cb=None): """Initialize Light class.""" # pylint: disable=too-many-arguments @@ -77,6 +79,9 @@ def __init__(self, device_name=self.name, after_update_cb=self.after_update) + self.min_kelvin = min_kelvin + self.max_kelvin = max_kelvin + @property def supports_brightness(self): """Return if light supports brightness.""" @@ -120,6 +125,10 @@ def from_config(cls, xknx, name, config): config.get('group_address_color_temperature') group_address_color_temperature_state = \ config.get('group_address_color_temperature_state') + min_kelvin = \ + config.get('min_kelvin') + max_kelvin = \ + config.get('max_kelvin') return cls(xknx, name, @@ -132,7 +141,9 @@ def from_config(cls, xknx, name, config): 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_temperature, - group_address_color_temperature_state=group_address_color_temperature_state,) + group_address_color_temperature_state=group_address_color_temperature_state, + min_kelvin=min_kelvin, + max_kelvin=max_kelvin) def has_group_address(self, group_address): """Test if device has given group address.""" From facf362c9a9efe7fe10a330977cd2e4dc6ce1c68 Mon Sep 17 00:00:00 2001 From: farmio Date: Sun, 11 Nov 2018 17:05:03 +0100 Subject: [PATCH 05/12] minor fixes to soothe the ci beast - removed unused import - bumped required version of xknx in ha-plugin - disabled pylint too-many-locals warning in light.py --- home-assistant-plugin/custom_components/light/xknx.py | 3 +-- xknx/devices/light.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/home-assistant-plugin/custom_components/light/xknx.py b/home-assistant-plugin/custom_components/light/xknx.py index b4d32afa2..627d3fec8 100644 --- a/home-assistant-plugin/custom_components/light/xknx.py +++ b/home-assistant-plugin/custom_components/light/xknx.py @@ -10,8 +10,7 @@ from custom_components.xknx import ATTR_DISCOVER_DEVICES, DATA_XKNX from homeassistant.components.light import ( Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, - SUPPORT_COLOR, SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP, - ATTR_WHITE_VALUE, SUPPORT_WHITE_VALUE) + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP, SUPPORT_WHITE_VALUE) from homeassistant.const import CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv diff --git a/xknx/devices/light.py b/xknx/devices/light.py index 8b7815038..669a61908 100644 --- a/xknx/devices/light.py +++ b/xknx/devices/light.py @@ -20,6 +20,8 @@ class Light(Device): """Class for managing a light.""" + # pylint: disable=too-many-locals + def __init__(self, xknx, name, From a8c12228039c2f4a3aae06dcafad641222961d54 Mon Sep 17 00:00:00 2001 From: farmio Date: Sun, 7 Oct 2018 11:17:08 +0200 Subject: [PATCH 06/12] color temperature function for light - added color temperature function for setting absolute color temperature values for lights - added tests for color remperature function - reordered DPTMAP in remote_value_sensor.py by DPT number - added DPTColorTemperature class for DPT 7.600 --- xknx/devices/light.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xknx/devices/light.py b/xknx/devices/light.py index 669a61908..50971d4c2 100644 --- a/xknx/devices/light.py +++ b/xknx/devices/light.py @@ -15,6 +15,7 @@ from .remote_value_dpt_2_byte_unsigned import RemoteValueDpt2ByteUnsigned from .remote_value_scaling import RemoteValueScaling from .remote_value_switch import RemoteValueSwitch +from .remote_value_dpt_2_byte_unsigned import RemoteValueDpt2ByteUnsigned class Light(Device): From 7589134fcc68b7961f86539238a776f388c086d0 Mon Sep 17 00:00:00 2001 From: farmio Date: Wed, 24 Oct 2018 12:54:20 +0200 Subject: [PATCH 07/12] tuneable white for home assistant plugin - added color temperature for home assistant plugin - added min and max kelvin / mireds --- home-assistant-plugin/custom_components/light/xknx.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/home-assistant-plugin/custom_components/light/xknx.py b/home-assistant-plugin/custom_components/light/xknx.py index 627d3fec8..95b7bdd93 100644 --- a/home-assistant-plugin/custom_components/light/xknx.py +++ b/home-assistant-plugin/custom_components/light/xknx.py @@ -9,8 +9,9 @@ from custom_components.xknx import ATTR_DISCOVER_DEVICES, DATA_XKNX from homeassistant.components.light import ( - Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, - SUPPORT_COLOR, SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP, SUPPORT_WHITE_VALUE) + 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 @@ -245,7 +246,7 @@ async def async_turn_on(self, **kwargs): color_util.color_hsv_to_RGB(*hs_color, brightness * 100 / 255)) elif self.device.supports_color_temperature and \ update_color_temp: - # change color temperature without ON command + # change color temperature without ON telegram mireds = kwargs[ATTR_COLOR_TEMP] if mireds > self.max_mireds: mireds = self.max_mireds From 1be80a2ed9c1ba687ff5d5a4aa0dd9fe621b9d7d Mon Sep 17 00:00:00 2001 From: farmio Date: Sun, 11 Nov 2018 17:05:03 +0100 Subject: [PATCH 08/12] minor fixes to soothe the ci beast - removed unused import - bumped required version of xknx in ha-plugin - disabled pylint too-many-locals warning in light.py --- home-assistant-plugin/custom_components/light/xknx.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/home-assistant-plugin/custom_components/light/xknx.py b/home-assistant-plugin/custom_components/light/xknx.py index 95b7bdd93..b9b971bd2 100644 --- a/home-assistant-plugin/custom_components/light/xknx.py +++ b/home-assistant-plugin/custom_components/light/xknx.py @@ -9,9 +9,9 @@ from custom_components.xknx import ATTR_DISCOVER_DEVICES, DATA_XKNX from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_WHITE_VALUE, - PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - SUPPORT_WHITE_VALUE, Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, 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 From edd7f6672a6d5b32be350f5d64552ab0be2e0af2 Mon Sep 17 00:00:00 2001 From: farmio Date: Thu, 27 Dec 2018 01:37:35 +0100 Subject: [PATCH 09/12] rebased to 0.9.3 --- test/remote_value_dpt_2_byte_unsigned_test.py | 4 ++-- xknx/devices/light.py | 1 - xknx/devices/remote_value_dpt_2_byte_unsigned.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/test/remote_value_dpt_2_byte_unsigned_test.py b/test/remote_value_dpt_2_byte_unsigned_test.py index 9989e2e38..a1672d757 100644 --- a/test/remote_value_dpt_2_byte_unsigned_test.py +++ b/test/remote_value_dpt_2_byte_unsigned_test.py @@ -3,9 +3,9 @@ import unittest from xknx import XKNX -from xknx.knx import DPTArray, DPTBinary, Telegram, GroupAddress -from xknx.exceptions import ConversionError, CouldNotParseTelegram from xknx.devices import RemoteValueDpt2ByteUnsigned +from xknx.exceptions import ConversionError, CouldNotParseTelegram +from xknx.knx import DPTArray, DPTBinary, GroupAddress, Telegram class TestRemoteValueDptValue1Ucount(unittest.TestCase): diff --git a/xknx/devices/light.py b/xknx/devices/light.py index 50971d4c2..669a61908 100644 --- a/xknx/devices/light.py +++ b/xknx/devices/light.py @@ -15,7 +15,6 @@ from .remote_value_dpt_2_byte_unsigned import RemoteValueDpt2ByteUnsigned from .remote_value_scaling import RemoteValueScaling from .remote_value_switch import RemoteValueSwitch -from .remote_value_dpt_2_byte_unsigned import RemoteValueDpt2ByteUnsigned class Light(Device): diff --git a/xknx/devices/remote_value_dpt_2_byte_unsigned.py b/xknx/devices/remote_value_dpt_2_byte_unsigned.py index fdd2d33ae..da289b303 100644 --- a/xknx/devices/remote_value_dpt_2_byte_unsigned.py +++ b/xknx/devices/remote_value_dpt_2_byte_unsigned.py @@ -3,7 +3,7 @@ DPT 7.001. """ -from xknx.knx import DPTArray, DPT2ByteUnsigned +from xknx.knx import DPT2ByteUnsigned, DPTArray from .remote_value import RemoteValue From 2df482079c94071ff61b58566f0586736227c9b5 Mon Sep 17 00:00:00 2001 From: farmio Date: Thu, 27 Dec 2018 02:09:04 +0100 Subject: [PATCH 10/12] set relative color temperature (tuneable white) from HA plugin --- home-assistant-plugin/custom_components/light/xknx.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/home-assistant-plugin/custom_components/light/xknx.py b/home-assistant-plugin/custom_components/light/xknx.py index b9b971bd2..b0869537b 100644 --- a/home-assistant-plugin/custom_components/light/xknx.py +++ b/home-assistant-plugin/custom_components/light/xknx.py @@ -9,7 +9,7 @@ from custom_components.xknx import ATTR_DISCOVER_DEVICES, DATA_XKNX from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, PLATFORM_SCHEMA, + 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 @@ -220,6 +220,8 @@ async def async_turn_on(self, **kwargs): """Turn the light on.""" brightness = int(kwargs.get(ATTR_BRIGHTNESS, self.brightness)) hs_color = kwargs.get(ATTR_HS_COLOR, self.hs_color) + mireds = kwargs.get(ATTR_COLOR_TEMP, self.color_temp) + tunable_white = int(kwargs.get(ATTR_WHITE_VALUE, self.white_value)) # fall back to default values, if required if brightness is None: @@ -230,6 +232,7 @@ async def async_turn_on(self, **kwargs): update_brightness = ATTR_BRIGHTNESS in kwargs update_color = ATTR_HS_COLOR in kwargs update_color_temp = ATTR_COLOR_TEMP in kwargs + update_tunable_white = ATTR_WHITE_VALUE in kwargs # always only go one path for turning on (avoid conflicting changes # and weird effects) @@ -247,13 +250,15 @@ async def async_turn_on(self, **kwargs): elif self.device.supports_color_temperature and \ update_color_temp: # change color temperature without ON telegram - mireds = kwargs[ATTR_COLOR_TEMP] if mireds > self.max_mireds: mireds = self.max_mireds elif mireds < self.min_mireds: mireds = self.min_mireds kelvin = int(color_util.color_temperature_mired_to_kelvin(mireds)) await self.device.set_color_temperature(kelvin) + elif self.device.supports_tunable_white and \ + update_tunable_white: + await self.device.set_tunable_white(tunable_white) else: # no color/brightness change requested, so just turn it on await self.device.set_on() From ac432ab642a21b89aacbddf0a8b0e4c3d8f04269 Mon Sep 17 00:00:00 2001 From: farmio Date: Thu, 27 Dec 2018 15:19:04 +0100 Subject: [PATCH 11/12] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 753c426f5..1d698d1bc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ xknx.egg-info/ .coverage venv .idea/ +.vscode/ From 8b4d345a41b9cb4c50502718bf9d64c4c2eb76ad Mon Sep 17 00:00:00 2001 From: farmio Date: Thu, 27 Dec 2018 16:57:17 +0100 Subject: [PATCH 12/12] avoid multiple conversions from mired to kelvin and vice versa Avoid triple conversion while comparing against min / max color temperature when a new value is set. --- .../custom_components/light/xknx.py | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/home-assistant-plugin/custom_components/light/xknx.py b/home-assistant-plugin/custom_components/light/xknx.py index b0869537b..47e18ec80 100644 --- a/home-assistant-plugin/custom_components/light/xknx.py +++ b/home-assistant-plugin/custom_components/light/xknx.py @@ -36,6 +36,8 @@ DEFAULT_COLOR_TEMPERATURE = 333 # 3000 K DEFAULT_MIN_MIREDS = 166 # 6000 K DEFAULT_MAX_MIREDS = 370 # 2700 K +DEFAULT_MIN_KELVIN = 2700 +DEFAULT_MAX_KELVIN = 6000 DEPENDENCIES = ['xknx'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -187,6 +189,24 @@ def max_mireds(self): if kelvin is not None else DEFAULT_MAX_MIREDS return None + @property + def min_kelvin(self): + """Return the warmest color temperature this light supports in kelvin.""" + if self.device.supports_color_temperature: + kelvin = self.device.min_kelvin + return kelvin \ + if kelvin is not None else DEFAULT_MIN_KELVIN + return None + + @property + def max_kelvin(self): + """Return the coldest color temperature this light supports in kelvin.""" + if self.device.supports_color_temperature: + kelvin = self.device.max_kelvin + return kelvin \ + if kelvin is not None else DEFAULT_MAX_KELVIN + return None + @property def effect_list(self): """Return the list of supported effects.""" @@ -250,11 +270,11 @@ async def async_turn_on(self, **kwargs): elif self.device.supports_color_temperature and \ update_color_temp: # change color temperature without ON telegram - if mireds > self.max_mireds: - mireds = self.max_mireds - elif mireds < self.min_mireds: - mireds = self.min_mireds kelvin = int(color_util.color_temperature_mired_to_kelvin(mireds)) + if kelvin > self.max_kelvin: + kelvin = self.max_kelvin + elif kelvin < self.min_kelvin: + kelvin = self.min_kelvin await self.device.set_color_temperature(kelvin) elif self.device.supports_tunable_white and \ update_tunable_white: