diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index e0982c65f3344b61cf705565ea062e806c67b0e3..eacb31e3f8b17156712a8fbf90855d1b62c56826 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -22,7 +22,7 @@ from .const import ( 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({ @@ -96,7 +96,7 @@ async def async_setup_entry(hass, config_entry): 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/deconz/const.py b/homeassistant/components/deconz/const.py index 6deee322a15e310da26a11c5c12a763e785743ba..7e16a9d7f107e0c3c9bba362d80d948889209f48 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -14,3 +14,5 @@ CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups' 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 08d7f5773f75798620861b81a8cc1abec3bd864b..4e1f5d8f15f9d53e81dcf86728e86322fe527095 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -4,9 +4,9 @@ Support for deCONZ light. 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, @@ -32,7 +32,8 @@ async def async_setup_entry(hass, config_entry, async_add_devices): """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( @@ -186,3 +187,12 @@ class DeconzLight(Light): 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 diff --git a/homeassistant/components/switch/deconz.py b/homeassistant/components/switch/deconz.py new file mode 100644 index 0000000000000000000000000000000000000000..95e7d7367392db676aa49bd922d947b3823aa3af --- /dev/null +++ b/homeassistant/components/switch/deconz.py @@ -0,0 +1,82 @@ +""" +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, SWITCH_TYPES) +from homeassistant.components.switch import SwitchDevice +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +DEPENDENCIES = ['deconz'] + + +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 is_on(self): + """Return true if switch is on.""" + return self._switch.state + + @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) diff --git a/requirements_all.txt b/requirements_all.txt index e498b833bd2b830a5cdaadd120ab8f57c84bed60..6388842e84d94c1a82a3c461b027f38d1e8125ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -793,7 +793,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 ff95cd3be25fefcde4cdb60cf859a4bf4273d29b..3d8402842f1a12194b71a7521e3676af48334b1c 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 diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 8f5342de1e3ad5276bcc7519a458845a24cca23d..c6fc130a4a41a691d4869becf623cea0ef8bcc5f 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): diff --git a/tests/components/light/test_deconz.py b/tests/components/light/test_deconz.py index d7d609f820eb5f5ed778708ce4a94b56b5f6ab6f..df088d7a1b5ddefed17ed406652c1ddff1e217e7 100644 --- a/tests/components/light/test_deconz.py +++ b/tests/components/light/test_deconz.py @@ -37,6 +37,15 @@ GROUP = { }, } +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 0000000000000000000000000000000000000000..490a0e67c9dd591eb410d8e79d0c297cc3164594 --- /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