From 9a69f9cfe804eeb98772d032fd57f016f1840bd8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 30 Jul 2018 18:11:24 +0200 Subject: [PATCH 1/8] Initial commit for deCONZ switch support --- homeassistant/components/deconz/__init__.py | 2 +- homeassistant/components/light/deconz.py | 4 +- homeassistant/components/switch/deconz.py | 94 +++++++++++++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/switch/deconz.py diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index e0982c65f3344..8682d5cbec158 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -96,7 +96,7 @@ def async_add_device_callback(device_type, device): hass.data[DATA_DECONZ_EVENT] = [] hass.data[DATA_DECONZ_UNSUB] = [] - for component in ['binary_sensor', 'light', 'scene', 'sensor']: + for component in ['binary_sensor', 'light', 'scene', 'sensor', 'switch']: hass.async_create_task(hass.config_entries.async_forward_entry_setup( config_entry, component)) diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 08d7f5773f757..06dd72755fcfc 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -12,6 +12,7 @@ ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, Light) +from homeassistant.components.switch.deconz import SWITCH_TYPES from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util @@ -32,7 +33,8 @@ def async_add_light(lights): """Add light from deCONZ.""" entities = [] for light in lights: - entities.append(DeconzLight(light)) + if light.type not in SWITCH_TYPES: + entities.append(DeconzLight(light)) async_add_devices(entities, True) hass.data[DATA_DECONZ_UNSUB].append( diff --git a/homeassistant/components/switch/deconz.py b/homeassistant/components/switch/deconz.py new file mode 100644 index 0000000000000..6aa545e71dfb0 --- /dev/null +++ b/homeassistant/components/switch/deconz.py @@ -0,0 +1,94 @@ +""" +Support for deCONZ switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.deconz/ +""" +from homeassistant.components.deconz.const import ( + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) +from homeassistant.components.switch import SwitchDevice +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +DEPENDENCIES = ['deconz'] + +SWITCH_TYPES = ["On/Off plug-in unit", "Smart plug"] + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Old way of setting up deCONZ switches.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up switches for deCONZ component. + + Switches are based same device class as lights in deCONZ. + """ + @callback + def async_add_switch(lights): + """Add switch from deCONZ.""" + entities = [] + for light in lights: + if light.type in SWITCH_TYPES: + entities.append(DeconzSwitch(light)) + async_add_devices(entities, True) + + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_light', async_add_switch)) + + async_add_switch(hass.data[DATA_DECONZ].lights.values()) + + +class DeconzSwitch(SwitchDevice): + """Representation of a deCONZ switch.""" + + def __init__(self, switch): + """Set up switch and add update callback to get data from websocket.""" + self._switch = switch + + async def async_added_to_hass(self): + """Subscribe to switches events.""" + self._switch.register_async_callback(self.async_update_callback) + self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._switch.deconz_id + + @callback + def async_update_callback(self, reason): + """Update the switch's state.""" + self.async_schedule_update_ha_state() + + @property + def state(self): + """Return the state of the switch.""" + return self._switch.state + + @property + def is_on(self): + """Return true if switch is on.""" + return self._switch.state + + @property + def is_standby(self): + """Return true if switch is in standby.""" + return self._switch.state == False + + @property + def name(self): + """Return the name of the switch.""" + return self._switch.name + + @property + def unique_id(self): + """Return a unique identifier for this switch.""" + return self._switch.uniqueid + + async def async_turn_on(self, **kwargs): + """Turn on switch.""" + data = {'on': True} + await self._switch.async_set_state(data) + + async def async_turn_off(self, **kwargs): + """Turn off switch.""" + data = {'on': False} + await self._switch.async_set_state(data) From b1c2dcc17da23fef913cbcc8786b6084591feeba Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 31 Jul 2018 09:27:36 +0200 Subject: [PATCH 2/8] Fix hound comment --- homeassistant/components/switch/deconz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/deconz.py b/homeassistant/components/switch/deconz.py index 6aa545e71dfb0..aadea911ffdce 100644 --- a/homeassistant/components/switch/deconz.py +++ b/homeassistant/components/switch/deconz.py @@ -71,7 +71,7 @@ def is_on(self): @property def is_standby(self): """Return true if switch is in standby.""" - return self._switch.state == False + return self._switch.state is False @property def name(self): From d23271fdd61eb9cbb8df9df56e85d9f13d94e0e6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 31 Jul 2018 10:09:53 +0200 Subject: [PATCH 3/8] Fix martins comment; platforms shouldn't depend on another platform --- homeassistant/components/deconz/const.py | 2 ++ homeassistant/components/light/deconz.py | 7 +++---- homeassistant/components/switch/deconz.py | 4 +--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 6deee322a15e3..7e16a9d7f107e 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -14,3 +14,5 @@ ATTR_DARK = 'dark' ATTR_ON = 'on' + +SWITCH_TYPES = ["On/Off plug-in unit", "Smart plug"] diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 06dd72755fcfc..1ec8d208dc623 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -4,15 +4,14 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.deconz/ """ -from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) -from homeassistant.components.deconz.const import CONF_ALLOW_DECONZ_GROUPS +from homeassistant.components.deconz.const import ( + CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DATA_DECONZ, + DATA_DECONZ_ID, DATA_DECONZ_UNSUB, SWITCH_TYPES) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, Light) -from homeassistant.components.switch.deconz import SWITCH_TYPES from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util diff --git a/homeassistant/components/switch/deconz.py b/homeassistant/components/switch/deconz.py index aadea911ffdce..d6a5be949d55c 100644 --- a/homeassistant/components/switch/deconz.py +++ b/homeassistant/components/switch/deconz.py @@ -5,15 +5,13 @@ https://home-assistant.io/components/switch.deconz/ """ from homeassistant.components.deconz.const import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, SWITCH_TYPES) from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['deconz'] -SWITCH_TYPES = ["On/Off plug-in unit", "Smart plug"] - async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): From b3faee8010c9363d87a78099bee16db92d4bcd59 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 31 Jul 2018 10:10:58 +0200 Subject: [PATCH 4/8] Fix existing tests --- tests/components/deconz/test_init.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 8f5342de1e3ad..c6fc130a4a41a 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -99,8 +99,8 @@ async def test_setup_entry_successful(hass): assert hass.data[deconz.DOMAIN] assert hass.data[deconz.DATA_DECONZ_ID] == {} assert len(hass.data[deconz.DATA_DECONZ_UNSUB]) == 1 - assert len(mock_add_job.mock_calls) == 4 - assert len(mock_config_entries.async_forward_entry_setup.mock_calls) == 4 + assert len(mock_add_job.mock_calls) == 5 + assert len(mock_config_entries.async_forward_entry_setup.mock_calls) == 5 assert mock_config_entries.async_forward_entry_setup.mock_calls[0][1] == \ (entry, 'binary_sensor') assert mock_config_entries.async_forward_entry_setup.mock_calls[1][1] == \ @@ -109,6 +109,8 @@ async def test_setup_entry_successful(hass): (entry, 'scene') assert mock_config_entries.async_forward_entry_setup.mock_calls[3][1] == \ (entry, 'sensor') + assert mock_config_entries.async_forward_entry_setup.mock_calls[4][1] == \ + (entry, 'switch') async def test_unload_entry(hass): From b9932cf89915fd33aec5edaaeb0ca8483a9484d4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 31 Jul 2018 13:52:42 +0200 Subject: [PATCH 5/8] New tests --- tests/components/light/test_deconz.py | 16 +++++ tests/components/switch/test_deconz.py | 90 ++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 tests/components/switch/test_deconz.py diff --git a/tests/components/light/test_deconz.py b/tests/components/light/test_deconz.py index d7d609f820eb5..df088d7a1b5dd 100644 --- a/tests/components/light/test_deconz.py +++ b/tests/components/light/test_deconz.py @@ -37,6 +37,15 @@ }, } +SWITCH = { + "1": { + "id": "Switch 1 id", + "name": "Switch 1 name", + "type": "On/Off plug-in unit", + "state": {} + } +} + async def setup_bridge(hass, data, allow_deconz_groups=True): """Load the deCONZ light platform.""" @@ -112,3 +121,10 @@ async def test_do_not_add_deconz_groups(hass): async_dispatcher_send(hass, 'deconz_new_group', [group]) await hass.async_block_till_done() assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + + +async def test_no_switch(hass): + """Test that a switch doesn't get created as a light entity.""" + await setup_bridge(hass, {"lights": SWITCH}) + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + assert len(hass.states.async_all()) == 0 diff --git a/tests/components/switch/test_deconz.py b/tests/components/switch/test_deconz.py new file mode 100644 index 0000000000000..490a0e67c9dd5 --- /dev/null +++ b/tests/components/switch/test_deconz.py @@ -0,0 +1,90 @@ +"""deCONZ switch platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz +from homeassistant.components.deconz.const import SWITCH_TYPES +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from tests.common import mock_coro + +SUPPORTED_SWITCHES = { + "1": { + "id": "Switch 1 id", + "name": "Switch 1 name", + "type": "On/Off plug-in unit", + "state": {} + }, + "2": { + "id": "Switch 2 id", + "name": "Switch 2 name", + "type": "Smart plug", + "state": {} + } +} + +UNSUPPORTED_SWITCH = { + "1": { + "id": "Switch id", + "name": "Unsupported switch", + "type": "Not a smart plug", + "state": {} + } +} + + +async def setup_bridge(hass, data): + """Load the deCONZ switch platform.""" + from pydeconz import DeconzSession + loop = Mock() + session = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + bridge = DeconzSession(loop, session, **entry.data) + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(data)): + await bridge.async_load_parameters() + hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_UNSUB] = [] + hass.data[deconz.DATA_DECONZ_ID] = {} + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + await hass.config_entries.async_forward_entry_setup(config_entry, 'switch') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_no_switches(hass): + """Test that no switch entities are created.""" + data = {} + await setup_bridge(hass, data) + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + assert len(hass.states.async_all()) == 0 + + +async def test_switch(hass): + """Test that all supported switch entities and switch group are created.""" + await setup_bridge(hass, {"lights": SUPPORTED_SWITCHES}) + assert "switch.switch_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "switch.switch_2_name" in hass.data[deconz.DATA_DECONZ_ID] + assert len(SUPPORTED_SWITCHES) == len(SWITCH_TYPES) + assert len(hass.states.async_all()) == 3 + + +async def test_add_new_switch(hass): + """Test successful creation of switch entity.""" + data = {} + await setup_bridge(hass, data) + switch = Mock() + switch.name = 'name' + switch.type = "Smart plug" + switch.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_light', [switch]) + await hass.async_block_till_done() + assert "switch.name" in hass.data[deconz.DATA_DECONZ_ID] + + +async def test_unsupported_switch(hass): + """Test that unsupported switches are not created.""" + await setup_bridge(hass, {"lights": UNSUPPORTED_SWITCH}) + assert len(hass.states.async_all()) == 0 From e2fbd692a10c2349c780d95a46fa5e3bf64097e9 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 31 Jul 2018 17:37:00 +0200 Subject: [PATCH 6/8] Clean up unnecessary methods --- homeassistant/components/switch/deconz.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/homeassistant/components/switch/deconz.py b/homeassistant/components/switch/deconz.py index d6a5be949d55c..95e7d7367392d 100644 --- a/homeassistant/components/switch/deconz.py +++ b/homeassistant/components/switch/deconz.py @@ -56,21 +56,11 @@ def async_update_callback(self, reason): """Update the switch's state.""" self.async_schedule_update_ha_state() - @property - def state(self): - """Return the state of the switch.""" - return self._switch.state - @property def is_on(self): """Return true if switch is on.""" return self._switch.state - @property - def is_standby(self): - """Return true if switch is in standby.""" - return self._switch.state is False - @property def name(self): """Return the name of the switch.""" From 2b42c56358c4fa02bdac926ff9189d4d0101bad1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 1 Aug 2018 00:15:42 +0200 Subject: [PATCH 7/8] Bump requirement to v43 --- homeassistant/components/deconz/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 8682d5cbec158..eacb31e3f8b17 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -22,7 +22,7 @@ CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==42'] +REQUIREMENTS = ['pydeconz==43'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ diff --git a/requirements_all.txt b/requirements_all.txt index eda2b60411575..75828ecd2a403 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -790,7 +790,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==42 +pydeconz==43 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff95cd3be25fe..3d8402842f1a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -136,7 +136,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==42 +pydeconz==43 # homeassistant.components.zwave pydispatcher==2.0.5 From 9c181d920efbbb3a718535e447a29d47690092b2 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 1 Aug 2018 09:22:58 +0200 Subject: [PATCH 8/8] Added device state attributes to light --- homeassistant/components/light/deconz.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 1ec8d208dc623..4e1f5d8f15f9d 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -187,3 +187,12 @@ async def async_turn_off(self, **kwargs): del data['on'] await self._light.async_set_state(data) + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = {} + attributes['is_deconz_group'] = self._light.type == 'LightGroup' + if self._light.type == 'LightGroup': + attributes['all_on'] = self._light.all_on + return attributes