From 521cc7247dadf37cfc96498863b356219e3fad10 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 4 Mar 2020 22:05:39 -0800 Subject: [PATCH] Add Dynalite switch platform (#32389) * added presets for switch devices * added channel type to __init and const * ran pylint on library so needed a few changes in names * removed callback * bool -> cv.boolean --- homeassistant/components/dynalite/__init__.py | 52 ++++++++++++++++--- homeassistant/components/dynalite/bridge.py | 6 +-- .../components/dynalite/config_flow.py | 4 +- homeassistant/components/dynalite/const.py | 11 +++- homeassistant/components/dynalite/light.py | 7 +-- .../components/dynalite/manifest.json | 2 +- homeassistant/components/dynalite/switch.py | 29 +++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/dynalite/common.py | 2 +- tests/components/dynalite/test_bridge.py | 6 +-- tests/components/dynalite/test_init.py | 8 ++- tests/components/dynalite/test_switch.py | 34 ++++++++++++ 13 files changed, 134 insertions(+), 31 deletions(-) create mode 100755 homeassistant/components/dynalite/switch.py create mode 100755 tests/components/dynalite/test_switch.py diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index 1c4ba99d1a4..e27bdfbb142 100755 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -1,4 +1,7 @@ """Support for the Dynalite networks.""" + +import asyncio + import voluptuous as vol from homeassistant import config_entries @@ -10,18 +13,26 @@ from homeassistant.helpers import config_validation as cv from .bridge import DynaliteBridge from .const import ( CONF_ACTIVE, + CONF_ACTIVE_INIT, + CONF_ACTIVE_OFF, + CONF_ACTIVE_ON, CONF_AREA, CONF_AUTO_DISCOVER, CONF_BRIDGES, CONF_CHANNEL, + CONF_CHANNEL_TYPE, CONF_DEFAULT, CONF_FADE, CONF_NAME, + CONF_NO_DEFAULT, CONF_POLLTIMER, CONF_PORT, + CONF_PRESET, + DEFAULT_CHANNEL_TYPE, DEFAULT_NAME, DEFAULT_PORT, DOMAIN, + ENTITY_PLATFORMS, LOGGER, ) @@ -35,16 +46,31 @@ def num_string(value): CHANNEL_DATA_SCHEMA = vol.Schema( - {vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_FADE): vol.Coerce(float)} + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_FADE): vol.Coerce(float), + vol.Optional(CONF_CHANNEL_TYPE, default=DEFAULT_CHANNEL_TYPE): vol.Any( + "light", "switch" + ), + } ) CHANNEL_SCHEMA = vol.Schema({num_string: CHANNEL_DATA_SCHEMA}) +PRESET_DATA_SCHEMA = vol.Schema( + {vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_FADE): vol.Coerce(float)} +) + +PRESET_SCHEMA = vol.Schema({num_string: vol.Any(PRESET_DATA_SCHEMA, None)}) + + AREA_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_FADE): vol.Coerce(float), + vol.Optional(CONF_NO_DEFAULT): vol.Coerce(bool), vol.Optional(CONF_CHANNEL): CHANNEL_SCHEMA, + vol.Optional(CONF_PRESET): PRESET_SCHEMA, }, ) @@ -62,7 +88,10 @@ BRIDGE_SCHEMA = vol.Schema( vol.Optional(CONF_POLLTIMER, default=1.0): vol.Coerce(float), vol.Optional(CONF_AREA): AREA_SCHEMA, vol.Optional(CONF_DEFAULT): PLATFORM_DEFAULTS_SCHEMA, - vol.Optional(CONF_ACTIVE, default=False): vol.Coerce(bool), + vol.Optional(CONF_ACTIVE, default=False): vol.Any( + CONF_ACTIVE_ON, CONF_ACTIVE_OFF, CONF_ACTIVE_INIT, cv.boolean + ), + vol.Optional(CONF_PRESET): PRESET_SCHEMA, } ) @@ -120,14 +149,17 @@ async def async_setup_entry(hass, entry): """Set up a bridge from a config entry.""" LOGGER.debug("Setting up entry %s", entry.data) bridge = DynaliteBridge(hass, entry.data) + # need to do it before the listener + hass.data[DOMAIN][entry.entry_id] = bridge entry.add_update_listener(async_entry_changed) if not await bridge.async_setup(): LOGGER.error("Could not set up bridge for entry %s", entry.data) + hass.data[DOMAIN][entry.entry_id] = None raise ConfigEntryNotReady - hass.data[DOMAIN][entry.entry_id] = bridge - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "light") - ) + for platform in ENTITY_PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) return True @@ -135,5 +167,9 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" LOGGER.debug("Unloading entry %s", entry.data) hass.data[DOMAIN].pop(entry.entry_id) - result = await hass.config_entries.async_forward_entry_unload(entry, "light") - return result + tasks = [ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in ENTITY_PLATFORMS + ] + results = await asyncio.gather(*tasks) + return False not in results diff --git a/homeassistant/components/dynalite/bridge.py b/homeassistant/components/dynalite/bridge.py index 85a187249df..fa0a91bfab1 100755 --- a/homeassistant/components/dynalite/bridge.py +++ b/homeassistant/components/dynalite/bridge.py @@ -20,8 +20,8 @@ class DynaliteBridge: self.host = config[CONF_HOST] # Configure the dynalite devices self.dynalite_devices = DynaliteDevices( - newDeviceFunc=self.add_devices_when_registered, - updateDeviceFunc=self.update_device, + new_device_func=self.add_devices_when_registered, + update_device_func=self.update_device, ) self.dynalite_devices.configure(config) @@ -31,7 +31,7 @@ class DynaliteBridge: LOGGER.debug("Setting up bridge - host %s", self.host) return await self.dynalite_devices.async_setup() - async def reload_config(self, config): + def reload_config(self, config): """Reconfigure a bridge when config changes.""" LOGGER.debug("Reloading bridge - host %s, config %s", self.host, config) self.dynalite_devices.configure(config) diff --git a/homeassistant/components/dynalite/config_flow.py b/homeassistant/components/dynalite/config_flow.py index 10d66c82d52..ca95c0754a6 100755 --- a/homeassistant/components/dynalite/config_flow.py +++ b/homeassistant/components/dynalite/config_flow.py @@ -3,7 +3,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_HOST from .bridge import DynaliteBridge -from .const import DOMAIN, LOGGER # pylint: disable=unused-import +from .const import DOMAIN, LOGGER class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -12,8 +12,6 @@ class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - def __init__(self): """Initialize the Dynalite flow.""" self.host = None diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py index 2e86d49c825..267b5727b83 100755 --- a/homeassistant/components/dynalite/const.py +++ b/homeassistant/components/dynalite/const.py @@ -4,21 +4,28 @@ import logging LOGGER = logging.getLogger(__package__) DOMAIN = "dynalite" -ENTITY_PLATFORMS = ["light"] +ENTITY_PLATFORMS = ["light", "switch"] + CONF_ACTIVE = "active" +CONF_ACTIVE_INIT = "init" +CONF_ACTIVE_OFF = "off" +CONF_ACTIVE_ON = "on" CONF_ALL = "ALL" CONF_AREA = "area" CONF_AUTO_DISCOVER = "autodiscover" CONF_BRIDGES = "bridges" CONF_CHANNEL = "channel" +CONF_CHANNEL_TYPE = "type" CONF_DEFAULT = "default" CONF_FADE = "fade" CONF_HOST = "host" CONF_NAME = "name" +CONF_NO_DEFAULT = "nodefault" CONF_POLLTIMER = "polltimer" CONF_PORT = "port" +CONF_PRESET = "preset" - +DEFAULT_CHANNEL_TYPE = "light" DEFAULT_NAME = "dynalite" DEFAULT_PORT = 12345 diff --git a/homeassistant/components/dynalite/light.py b/homeassistant/components/dynalite/light.py index caa39ad573a..a5b7139803c 100755 --- a/homeassistant/components/dynalite/light.py +++ b/homeassistant/components/dynalite/light.py @@ -1,6 +1,5 @@ """Support for Dynalite channels as lights.""" from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light -from homeassistant.core import callback from .dynalitebase import DynaliteBase, async_setup_entry_base @@ -8,12 +7,8 @@ from .dynalitebase import DynaliteBase, async_setup_entry_base async def async_setup_entry(hass, config_entry, async_add_entities): """Record the async_add_entities function to add them later when received from Dynalite.""" - @callback - def light_from_device(device, bridge): - return DynaliteLight(device, bridge) - async_setup_entry_base( - hass, config_entry, async_add_entities, "light", light_from_device + hass, config_entry, async_add_entities, "light", DynaliteLight ) diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json index 18f1ebed919..d6351db17b2 100755 --- a/homeassistant/components/dynalite/manifest.json +++ b/homeassistant/components/dynalite/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/dynalite", "dependencies": [], "codeowners": ["@ziv1234"], - "requirements": ["dynalite_devices==0.1.30"] + "requirements": ["dynalite_devices==0.1.32"] } diff --git a/homeassistant/components/dynalite/switch.py b/homeassistant/components/dynalite/switch.py new file mode 100755 index 00000000000..84be74cee36 --- /dev/null +++ b/homeassistant/components/dynalite/switch.py @@ -0,0 +1,29 @@ +"""Support for the Dynalite channels and presets as switches.""" +from homeassistant.components.switch import SwitchDevice + +from .dynalitebase import DynaliteBase, async_setup_entry_base + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Record the async_add_entities function to add them later when received from Dynalite.""" + + async_setup_entry_base( + hass, config_entry, async_add_entities, "switch", DynaliteSwitch + ) + + +class DynaliteSwitch(DynaliteBase, SwitchDevice): + """Representation of a Dynalite Channel as a Home Assistant Switch.""" + + @property + def is_on(self): + """Return true if switch is on.""" + return self._device.is_on + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + await self._device.async_turn_on() + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + await self._device.async_turn_off() diff --git a/requirements_all.txt b/requirements_all.txt index fad600e918d..5f134d6d9c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -466,7 +466,7 @@ dsmr_parser==0.18 dweepy==0.3.0 # homeassistant.components.dynalite -dynalite_devices==0.1.30 +dynalite_devices==0.1.32 # homeassistant.components.rainforest_eagle eagle200_reader==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff37f3a9ba3..ec2608fe644 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -174,7 +174,7 @@ distro==1.4.0 dsmr_parser==0.18 # homeassistant.components.dynalite -dynalite_devices==0.1.30 +dynalite_devices==0.1.32 # homeassistant.components.ee_brightbox eebrightbox==0.0.4 diff --git a/tests/components/dynalite/common.py b/tests/components/dynalite/common.py index 97750140811..56554efaa07 100755 --- a/tests/components/dynalite/common.py +++ b/tests/components/dynalite/common.py @@ -41,7 +41,7 @@ async def create_entity_from_device(hass, device): mock_dyn_dev().async_setup = CoroutineMock(return_value=True) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - new_device_func = mock_dyn_dev.mock_calls[1][2]["newDeviceFunc"] + new_device_func = mock_dyn_dev.mock_calls[1][2]["new_device_func"] new_device_func([device]) await hass.async_block_till_done() diff --git a/tests/components/dynalite/test_bridge.py b/tests/components/dynalite/test_bridge.py index 0c9ea517992..ee6baaa7561 100755 --- a/tests/components/dynalite/test_bridge.py +++ b/tests/components/dynalite/test_bridge.py @@ -20,7 +20,7 @@ async def test_update_device(hass): mock_dyn_dev().async_setup = CoroutineMock(return_value=True) assert await hass.config_entries.async_setup(entry.entry_id) # Not waiting so it add the devices before registration - update_device_func = mock_dyn_dev.mock_calls[1][2]["updateDeviceFunc"] + update_device_func = mock_dyn_dev.mock_calls[1][2]["update_device_func"] device = Mock() device.unique_id = "abcdef" wide_func = Mock() @@ -50,7 +50,7 @@ async def test_add_devices_then_register(hass): mock_dyn_dev().async_setup = CoroutineMock(return_value=True) assert await hass.config_entries.async_setup(entry.entry_id) # Not waiting so it add the devices before registration - new_device_func = mock_dyn_dev.mock_calls[1][2]["newDeviceFunc"] + new_device_func = mock_dyn_dev.mock_calls[1][2]["new_device_func"] # Now with devices device1 = Mock() device1.category = "light" @@ -73,7 +73,7 @@ async def test_register_then_add_devices(hass): mock_dyn_dev().async_setup = CoroutineMock(return_value=True) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - new_device_func = mock_dyn_dev.mock_calls[1][2]["newDeviceFunc"] + new_device_func = mock_dyn_dev.mock_calls[1][2]["new_device_func"] # Now with devices device1 = Mock() device1.category = "light" diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py index 6c9309cb4e5..b74fcd64da0 100755 --- a/tests/components/dynalite/test_init.py +++ b/tests/components/dynalite/test_init.py @@ -83,5 +83,9 @@ async def test_unload_entry(hass): ) as mock_unload: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - mock_unload.assert_called_once() - assert mock_unload.mock_calls == [call(entry, "light")] + assert mock_unload.call_count == len(dynalite.ENTITY_PLATFORMS) + expected_calls = [ + call(entry, platform) for platform in dynalite.ENTITY_PLATFORMS + ] + for cur_call in mock_unload.mock_calls: + assert cur_call in expected_calls diff --git a/tests/components/dynalite/test_switch.py b/tests/components/dynalite/test_switch.py new file mode 100755 index 00000000000..7c0c5d632d3 --- /dev/null +++ b/tests/components/dynalite/test_switch.py @@ -0,0 +1,34 @@ +"""Test Dynalite switch.""" + +from dynalite_devices_lib.switch import DynalitePresetSwitchDevice +import pytest + +from .common import ( + ATTR_METHOD, + ATTR_SERVICE, + create_entity_from_device, + create_mock_device, + run_service_tests, +) + + +@pytest.fixture +def mock_device(): + """Mock a Dynalite device.""" + return create_mock_device("switch", DynalitePresetSwitchDevice) + + +async def test_switch_setup(hass, mock_device): + """Test a successful setup.""" + await create_entity_from_device(hass, mock_device) + entity_state = hass.states.get("switch.name") + assert entity_state.attributes["friendly_name"] == mock_device.name + await run_service_tests( + hass, + mock_device, + "switch", + [ + {ATTR_SERVICE: "turn_on", ATTR_METHOD: "async_turn_on"}, + {ATTR_SERVICE: "turn_off", ATTR_METHOD: "async_turn_off"}, + ], + )