diff --git a/pytradfri/command.py b/pytradfri/command.py index bd376bd4..81652fd1 100644 --- a/pytradfri/command.py +++ b/pytradfri/command.py @@ -1,5 +1,7 @@ """Command implementation.""" +from copy import deepcopy + class Command(object): """The object for coap commands.""" @@ -71,3 +73,36 @@ def url(self, host): """Generate url for coap client.""" path = '/'.join(str(v) for v in self._path) return 'coaps://{}:5684/{}'.format(host, path) + + def _merge(self, a, b): + """Merges a into b.""" + for k, v in a.items(): + if isinstance(v, dict): + item = b.setdefault(k, {}) + self._merge(v, item) + elif isinstance(v, list): + item = b.setdefault(k, [{}]) + if len(v) == 1 and isinstance(v[0], dict): + self._merge(v[0], item[0]) + else: + b[k] = v + else: + b[k] = v + return b + + def combine_data(self, command2): + """Combines the data for this command with another.""" + if command2 is None: + return + self._data = self._merge(command2._data, self._data) + + def __add__(self, other): + if other is None: + return deepcopy(self) + if isinstance(other, self.__class__): + newObj = deepcopy(self) + newObj.combine_data(other) + return newObj + else: + raise (TypeError("unsupported operand type(s) for +: " + "'{}' and '{}'").format(self.__class__, type(other))) diff --git a/pytradfri/device.py b/pytradfri/device.py index 0a7d70b9..02c246df 100644 --- a/pytradfri/device.py +++ b/pytradfri/device.py @@ -163,9 +163,22 @@ def __init__(self, device): if ATTR_LIGHT_COLOR_HUE in self.raw[0]: self.can_set_color = True + # Currently uncertain which bulbs are capable of setting + # multiple values simultaneously. As of gateway firmware + # 1.3.14 1st party bulbs do not seem to support this properly, + # but (at least some) hue bulbs do. + if 'Philips' in self._device.device_info.manufacturer: + self.can_combine_commands = True + self.min_mireds = RANGE_MIREDS[0] self.max_mireds = RANGE_MIREDS[1] + self.min_hue = RANGE_HUE[0] + self.max_hue = RANGE_HUE[1] + + self.min_saturation = RANGE_SATURATION[0] + self.max_saturation = RANGE_SATURATION[1] + @property def raw(self): """Return raw data that it represents.""" diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 31a59077..6c7a8dba --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ with open(path.join(here, 'README.md'), encoding='utf-8') as f: long_description = f.read() -VERSION = "5.4.2" +VERSION = "5.5" DOWNLOAD_URL = \ 'https://github.com/ggravlingen/pytradfri/archive/{}.zip'.format(VERSION) diff --git a/tests/test_command.py b/tests/test_command.py new file mode 100644 index 00000000..c09d8e27 --- /dev/null +++ b/tests/test_command.py @@ -0,0 +1,133 @@ +from pytradfri.command import Command + + +def test_combining_mutates(): + DATA_INT = {'key_int': 0} + DATA_INT2 = {'key_int_2': 1} + COMBINED_INT = {'key_int': 0, 'key_int_2': 1} + + command1 = Command('method', 'path', DATA_INT) + command2 = Command('method', 'path', DATA_INT2) + combined = command1 + command2 + + # Adding shouldn't mutate the original commands + assert command1._data == DATA_INT + assert command2._data == DATA_INT2 + assert combined._data == COMBINED_INT + + # Combining should mutate the original command + command1.combine_data(command2) + assert command1._data == COMBINED_INT + assert command2._data == DATA_INT2 + + +def test_combining_with_none(): + DATA_INT = {'key_int': 0} + + command1 = Command('method', 'path', DATA_INT) + combined = command1 + None + + assert combined._data == DATA_INT + + # Combining should mutate the original command + command1.combine_data(None) + assert command1._data == DATA_INT + + +def test_combining_integer_keys(): + DATA_INT = {'key_int': 0} + DATA_INT_SAME_KEY = {'key_int': 1} + DATA_INT2 = {'key_int_2': 1} + COMBINED_INT = {'key_int': 0, 'key_int_2': 1} + + command1 = Command('method', 'path', DATA_INT) + command2 = Command('method', 'path', DATA_INT2) + combined = command1 + command2 + assert combined._data == COMBINED_INT + + command1 = Command('method', 'path', DATA_INT) + command2 = Command('method', 'path', DATA_INT_SAME_KEY) + # We should always take the last key if we can't merge + combined = command1 + command2 + assert combined._data == DATA_INT_SAME_KEY + + +def test_combining_string_keys(): + DATA_STRING = {'key_string': 'a'} + DATA_STRING_SAME_KEY = {'key_string': 'same'} + DATA_STRING2 = {'key_string_2': 'b'} + COMBINED_STRING = {'key_string': 'a', 'key_string_2': 'b'} + + command1 = Command('method', 'path', DATA_STRING) + command2 = Command('method', 'path', DATA_STRING2) + combined = command1 + command2 + assert combined._data == COMBINED_STRING + + command1 = Command('method', 'path', DATA_STRING) + command2 = Command('method', 'path', DATA_STRING_SAME_KEY) + # We should always take the last key if we can't merge + combined = command1 + command2 + assert combined._data == DATA_STRING_SAME_KEY + + +def test_combining_dict_keys(): + DATA_EMPTY_DICT = {'key_dict': {}} + DATA_DICT_INT = {'key_dict': {'key_int': 0}} + DATA_DICT_STRING = {'key_dict': {'key_string': 'a'}} + DATA_DICT_STRING2 = {'key_dict': {'key_string': 'b'}} + DATA_DICT_INTSTRING = {'key_dict': {'key_int': 0, 'key_string': 'a'}} + + command1 = Command('method', 'path', DATA_EMPTY_DICT) + command2 = Command('method', 'path', DATA_DICT_INT) + combined = command1 + command2 + assert combined._data == DATA_DICT_INT + + command1 = Command('method', 'path', DATA_DICT_INT) + command2 = Command('method', 'path', DATA_DICT_STRING) + combined = command1 + command2 + assert combined._data == DATA_DICT_INTSTRING + + command1 = Command('method', 'path', DATA_DICT_STRING) + command2 = Command('method', 'path', DATA_DICT_STRING2) + combined = command1 + command2 + assert combined._data == DATA_DICT_STRING2 + + command1 = Command('method', 'path', DATA_DICT_INT) + command2 = Command('method', 'path', DATA_DICT_STRING2) + command3 = Command('method', 'path', DATA_DICT_STRING) + combined = command1 + command2 + command3 + assert combined._data == DATA_DICT_INTSTRING + + +def test_combining_list_keys(): + DATA_EMPTY_LIST = {'key_list': []} + DATA_INT_LIST1 = {'key_list': [0, 1, 2]} + DATA_INT_LIST2 = {'key_list': [10, 11, 12]} + + command1 = Command('method', 'path', DATA_EMPTY_LIST) + command2 = Command('method', 'path', DATA_INT_LIST1) + combined = command1 + command2 + assert combined._data == DATA_INT_LIST1 + + # Duplicated keys are replaced if not dicts + command1 = Command('method', 'path', DATA_INT_LIST1) + command2 = Command('method', 'path', DATA_INT_LIST2) + combined = command1 + command2 + assert combined._data == DATA_INT_LIST2 + + +def test_combining_listed_dict_keys(): + DATA_EMPTY_DICT = {'key_ldict': [{}]} + DATA_DICT_INT = {'key_ldict': [{'key_int': 0}]} + DATA_DICT_STRING = {'key_ldict': [{'key_string': 'a'}]} + DATA_DICT_INTSTRING = {'key_ldict': [{'key_int': 0, 'key_string': 'a'}]} + + command1 = Command('method', 'path', DATA_EMPTY_DICT) + command2 = Command('method', 'path', DATA_DICT_INT) + combined = command1 + command2 + assert combined._data == DATA_DICT_INT + + command1 = Command('method', 'path', DATA_DICT_INT) + command2 = Command('method', 'path', DATA_DICT_STRING) + combined = command1 + command2 + assert combined._data == DATA_DICT_INTSTRING