From 3b9db880656a8d74516e915fbbe3439f49d40a2c Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Tue, 26 Feb 2019 15:12:24 -0600 Subject: [PATCH] Add SmartThings Scene platform (#21405) * Add SmartThings Scene platform * Fixed failing tests after rebase * Update cover tests. --- .../components/smartthings/__init__.py | 26 ++++++++- homeassistant/components/smartthings/const.py | 3 +- homeassistant/components/smartthings/scene.py | 50 ++++++++++++++++ tests/components/smartthings/conftest.py | 33 ++++++++++- .../smartthings/test_binary_sensor.py | 9 +-- tests/components/smartthings/test_climate.py | 24 ++++---- tests/components/smartthings/test_cover.py | 16 +++--- tests/components/smartthings/test_fan.py | 16 +++--- tests/components/smartthings/test_init.py | 57 ++++++++++++++++--- tests/components/smartthings/test_light.py | 22 +++---- tests/components/smartthings/test_lock.py | 10 ++-- tests/components/smartthings/test_scene.py | 54 ++++++++++++++++++ tests/components/smartthings/test_sensor.py | 8 +-- tests/components/smartthings/test_switch.py | 10 ++-- 14 files changed, 266 insertions(+), 72 deletions(-) create mode 100644 homeassistant/components/smartthings/scene.py create mode 100644 tests/components/smartthings/test_scene.py diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 53ff6169c0a..e64988b2697 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -19,7 +19,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .config_flow import SmartThingsFlowHandler # noqa from .const import ( - CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_OAUTH_CLIENT_ID, + CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_LOCATION_ID, CONF_OAUTH_CLIENT_ID, CONF_OAUTH_CLIENT_SECRET, CONF_REFRESH_TOKEN, DATA_BROKERS, DATA_MANAGER, DOMAIN, EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS, TOKEN_REFRESH_INTERVAL) @@ -93,6 +93,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): installed_app = await validate_installed_app( api, entry.data[CONF_INSTALLED_APP_ID]) + # Get scenes + scenes = await async_get_entry_scenes(entry, api) + # Get SmartApp token to sync subscriptions token = await api.generate_tokens( entry.data[CONF_OAUTH_CLIENT_ID], @@ -123,7 +126,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): installed_app.installed_app_id, devices) # Setup device broker - broker = DeviceBroker(hass, entry, token, smart_app, devices) + broker = DeviceBroker(hass, entry, token, smart_app, devices, scenes) broker.connect() hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker @@ -156,6 +159,20 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): return True +async def async_get_entry_scenes(entry: ConfigEntry, api): + """Get the scenes within an integration.""" + try: + return await api.scenes(location_id=entry.data[CONF_LOCATION_ID]) + except ClientResponseError as ex: + if ex.status == 403: + _LOGGER.exception("Unable to load scenes for config entry '%s' " + "because the access token does not have the " + "required access", entry.title) + else: + raise + return [] + + async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS].pop(entry.entry_id, None) @@ -171,7 +188,7 @@ class DeviceBroker: """Manages an individual SmartThings config entry.""" def __init__(self, hass: HomeAssistantType, entry: ConfigEntry, - token, smart_app, devices: Iterable): + token, smart_app, devices: Iterable, scenes: Iterable): """Create a new instance of the DeviceBroker.""" self._hass = hass self._entry = entry @@ -182,6 +199,7 @@ class DeviceBroker: self._regenerate_token_remove = None self._assignments = self._assign_capabilities(devices) self.devices = {device.device_id: device for device in devices} + self.scenes = {scene.scene_id: scene for scene in scenes} def _assign_capabilities(self, devices: Iterable): """Assign platforms to capabilities.""" @@ -192,6 +210,8 @@ class DeviceBroker: for platform_name in SUPPORTED_PLATFORMS: platform = importlib.import_module( '.' + platform_name, self.__module__) + if not hasattr(platform, 'get_capabilities'): + continue assigned = platform.get_capabilities(capabilities) if not assigned: continue diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index 5da43203e4f..105c9760e12 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -34,7 +34,8 @@ SUPPORTED_PLATFORMS = [ 'cover', 'switch', 'binary_sensor', - 'sensor' + 'sensor', + 'scene' ] TOKEN_REFRESH_INTERVAL = timedelta(days=14) VAL_UID = "^(?:([0-9a-fA-F]{32})|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]" \ diff --git a/homeassistant/components/smartthings/scene.py b/homeassistant/components/smartthings/scene.py new file mode 100644 index 00000000000..9bf3211d8e3 --- /dev/null +++ b/homeassistant/components/smartthings/scene.py @@ -0,0 +1,50 @@ +"""Support for scenes through the SmartThings cloud API.""" +from homeassistant.components.scene import Scene + +from .const import DATA_BROKERS, DOMAIN + +DEPENDENCIES = ['smartthings'] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add switches for a config entry.""" + broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] + async_add_entities( + [SmartThingsScene(scene) for scene in broker.scenes.values()]) + + +class SmartThingsScene(Scene): + """Define a SmartThings scene.""" + + def __init__(self, scene): + """Init the scene class.""" + self._scene = scene + + async def async_activate(self): + """Activate scene.""" + await self._scene.execute() + + @property + def device_state_attributes(self): + """Get attributes about the state.""" + return { + 'icon': self._scene.icon, + 'color': self._scene.color, + 'location_id': self._scene.location_id + } + + @property + def name(self) -> str: + """Return the name of the device.""" + return self._scene.name + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._scene.scene_id diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 4622e49b0c6..27e833bff25 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -5,7 +5,7 @@ from uuid import uuid4 from pysmartthings import ( CLASSIFICATION_AUTOMATION, AppEntity, AppOAuthClient, AppSettings, - DeviceEntity, InstalledApp, Location, Subscription) + DeviceEntity, InstalledApp, Location, SceneEntity, Subscription) from pysmartthings.api import Api import pytest @@ -24,13 +24,15 @@ from homeassistant.setup import async_setup_component from tests.common import mock_coro -async def setup_platform(hass, platform: str, *devices): +async def setup_platform(hass, platform: str, *, + devices=None, scenes=None): """Set up the SmartThings platform and prerequisites.""" hass.config.components.add(DOMAIN) config_entry = ConfigEntry(2, DOMAIN, "Test", {CONF_INSTALLED_APP_ID: str(uuid4())}, SOURCE_USER, CONN_CLASS_CLOUD_PUSH) - broker = DeviceBroker(hass, config_entry, Mock(), Mock(), devices) + broker = DeviceBroker(hass, config_entry, Mock(), Mock(), + devices or [], scenes or []) hass.data[DOMAIN] = { DATA_BROKERS: { @@ -295,6 +297,31 @@ def device_factory_fixture(): return _factory +@pytest.fixture(name="scene_factory") +def scene_factory_fixture(location): + """Fixture for creating mock devices.""" + api = Mock(spec=Api) + api.execute_scene.side_effect = \ + lambda *args, **kwargs: mock_coro(return_value={}) + + def _factory(name): + scene_data = { + 'sceneId': str(uuid4()), + 'sceneName': name, + 'sceneIcon': '', + 'sceneColor': '', + 'locationId': location.location_id + } + return SceneEntity(api, scene_data) + return _factory + + +@pytest.fixture(name="scene") +def scene_fixture(scene_factory): + """Fixture for an individual scene.""" + return scene_factory('Test Scene') + + @pytest.fixture(name="event_factory") def event_factory_fixture(): """Fixture for creating mock devices.""" diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 6e60ee49ca6..d1de9f8f020 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -40,7 +40,7 @@ async def test_entity_state(hass, device_factory): """Tests the state attributes properly match the light types.""" device = device_factory('Motion Sensor 1', [Capability.motion_sensor], {Attribute.motion: 'inactive'}) - await setup_platform(hass, BINARY_SENSOR_DOMAIN, device) + await setup_platform(hass, BINARY_SENSOR_DOMAIN, devices=[device]) state = hass.states.get('binary_sensor.motion_sensor_1_motion') assert state.state == 'off' assert state.attributes[ATTR_FRIENDLY_NAME] ==\ @@ -55,7 +55,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await setup_platform(hass, BINARY_SENSOR_DOMAIN, device) + await setup_platform(hass, BINARY_SENSOR_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get('binary_sensor.motion_sensor_1_motion') assert entry @@ -73,7 +73,7 @@ async def test_update_from_signal(hass, device_factory): # Arrange device = device_factory('Motion Sensor 1', [Capability.motion_sensor], {Attribute.motion: 'inactive'}) - await setup_platform(hass, BINARY_SENSOR_DOMAIN, device) + await setup_platform(hass, BINARY_SENSOR_DOMAIN, devices=[device]) device.status.apply_attribute_update( 'main', Capability.motion_sensor, Attribute.motion, 'active') # Act @@ -91,7 +91,8 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory('Motion Sensor 1', [Capability.motion_sensor], {Attribute.motion: 'inactive'}) - config_entry = await setup_platform(hass, BINARY_SENSOR_DOMAIN, device) + config_entry = await setup_platform(hass, BINARY_SENSOR_DOMAIN, + devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'binary_sensor') diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 481e43266fa..29134d6ba6a 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -122,7 +122,7 @@ async def test_async_setup_platform(): async def test_legacy_thermostat_entity_state(hass, legacy_thermostat): """Tests the state attributes properly match the thermostat type.""" - await setup_platform(hass, CLIMATE_DOMAIN, legacy_thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[legacy_thermostat]) state = hass.states.get('climate.legacy_thermostat') assert state.state == STATE_AUTO assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ @@ -141,7 +141,7 @@ async def test_legacy_thermostat_entity_state(hass, legacy_thermostat): async def test_basic_thermostat_entity_state(hass, basic_thermostat): """Tests the state attributes properly match the thermostat type.""" - await setup_platform(hass, CLIMATE_DOMAIN, basic_thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[basic_thermostat]) state = hass.states.get('climate.basic_thermostat') assert state.state == STATE_OFF assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ @@ -155,7 +155,7 @@ async def test_basic_thermostat_entity_state(hass, basic_thermostat): async def test_thermostat_entity_state(hass, thermostat): """Tests the state attributes properly match the thermostat type.""" - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) state = hass.states.get('climate.thermostat') assert state.state == STATE_HEAT assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ @@ -174,7 +174,7 @@ async def test_thermostat_entity_state(hass, thermostat): async def test_buggy_thermostat_entity_state(hass, buggy_thermostat): """Tests the state attributes properly match the thermostat type.""" - await setup_platform(hass, CLIMATE_DOMAIN, buggy_thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[buggy_thermostat]) state = hass.states.get('climate.buggy_thermostat') assert state.state == STATE_UNKNOWN assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ @@ -190,14 +190,14 @@ async def test_buggy_thermostat_invalid_mode(hass, buggy_thermostat): buggy_thermostat.status.update_attribute_value( Attribute.supported_thermostat_modes, ['heat', 'emergency heat', 'other']) - await setup_platform(hass, CLIMATE_DOMAIN, buggy_thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[buggy_thermostat]) state = hass.states.get('climate.buggy_thermostat') assert state.attributes[ATTR_OPERATION_LIST] == {'heat'} async def test_set_fan_mode(hass, thermostat): """Test the fan mode is set successfully.""" - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -209,7 +209,7 @@ async def test_set_fan_mode(hass, thermostat): async def test_set_operation_mode(hass, thermostat): """Test the operation mode is set successfully.""" - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_OPERATION_MODE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -222,7 +222,7 @@ async def test_set_operation_mode(hass, thermostat): async def test_set_temperature_heat_mode(hass, thermostat): """Test the temperature is set successfully when in heat mode.""" thermostat.status.thermostat_mode = 'heat' - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -237,7 +237,7 @@ async def test_set_temperature_heat_mode(hass, thermostat): async def test_set_temperature_cool_mode(hass, thermostat): """Test the temperature is set successfully when in cool mode.""" thermostat.status.thermostat_mode = 'cool' - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -250,7 +250,7 @@ async def test_set_temperature_cool_mode(hass, thermostat): async def test_set_temperature(hass, thermostat): """Test the temperature is set successfully.""" thermostat.status.thermostat_mode = 'auto' - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -264,7 +264,7 @@ async def test_set_temperature(hass, thermostat): async def test_set_temperature_with_mode(hass, thermostat): """Test the temperature and mode is set successfully.""" - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -280,7 +280,7 @@ async def test_set_temperature_with_mode(hass, thermostat): async def test_entity_and_device_attributes(hass, thermostat): """Test the attributes of the entries are correct.""" - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 6e7844e521c..7e41237e3e7 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -32,7 +32,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await setup_platform(hass, COVER_DOMAIN, device) + await setup_platform(hass, COVER_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get('cover.garage') assert entry @@ -57,7 +57,7 @@ async def test_open(hass, device_factory): device_factory('Shade', [Capability.window_shade], {Attribute.window_shade: 'closed'}) } - await setup_platform(hass, COVER_DOMAIN, *devices) + await setup_platform(hass, COVER_DOMAIN, devices=devices) entity_ids = [ 'cover.door', 'cover.garage', @@ -86,7 +86,7 @@ async def test_close(hass, device_factory): device_factory('Shade', [Capability.window_shade], {Attribute.window_shade: 'open'}) } - await setup_platform(hass, COVER_DOMAIN, *devices) + await setup_platform(hass, COVER_DOMAIN, devices=devices) entity_ids = [ 'cover.door', 'cover.garage', @@ -113,7 +113,7 @@ async def test_set_cover_position(hass, device_factory): Capability.switch_level], {Attribute.window_shade: 'opening', Attribute.battery: 95, Attribute.level: 10}) - await setup_platform(hass, COVER_DOMAIN, device) + await setup_platform(hass, COVER_DOMAIN, devices=[device]) # Act await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, @@ -136,7 +136,7 @@ async def test_set_cover_position_unsupported(hass, device_factory): 'Shade', [Capability.window_shade], {Attribute.window_shade: 'opening'}) - await setup_platform(hass, COVER_DOMAIN, device) + await setup_platform(hass, COVER_DOMAIN, devices=[device]) # Act await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, @@ -152,7 +152,7 @@ async def test_update_to_open_from_signal(hass, device_factory): # Arrange device = device_factory('Garage', [Capability.garage_door_control], {Attribute.door: 'opening'}) - await setup_platform(hass, COVER_DOMAIN, device) + await setup_platform(hass, COVER_DOMAIN, devices=[device]) device.status.update_attribute_value(Attribute.door, 'open') assert hass.states.get('cover.garage').state == STATE_OPENING # Act @@ -170,7 +170,7 @@ async def test_update_to_closed_from_signal(hass, device_factory): # Arrange device = device_factory('Garage', [Capability.garage_door_control], {Attribute.door: 'closing'}) - await setup_platform(hass, COVER_DOMAIN, device) + await setup_platform(hass, COVER_DOMAIN, devices=[device]) device.status.update_attribute_value(Attribute.door, 'closed') assert hass.states.get('cover.garage').state == STATE_CLOSING # Act @@ -188,7 +188,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory('Garage', [Capability.garage_door_control], {Attribute.door: 'open'}) - config_entry = await setup_platform(hass, COVER_DOMAIN, device) + config_entry = await setup_platform(hass, COVER_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, COVER_DOMAIN) diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 644c0823fd5..dffffa7b340 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -29,7 +29,7 @@ async def test_entity_state(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'on', Attribute.fan_speed: 2}) - await setup_platform(hass, FAN_DOMAIN, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Dimmer 1 state = hass.states.get('fan.fan_1') @@ -48,7 +48,7 @@ async def test_entity_and_device_attributes(hass, device_factory): capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'on', Attribute.fan_speed: 2}) # Act - await setup_platform(hass, FAN_DOMAIN, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Assert @@ -71,7 +71,7 @@ async def test_turn_off(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'on', Attribute.fan_speed: 2}) - await setup_platform(hass, FAN_DOMAIN, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'fan', 'turn_off', {'entity_id': 'fan.fan_1'}, @@ -89,7 +89,7 @@ async def test_turn_on(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - await setup_platform(hass, FAN_DOMAIN, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'fan', 'turn_on', {ATTR_ENTITY_ID: "fan.fan_1"}, @@ -107,7 +107,7 @@ async def test_turn_on_with_speed(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - await setup_platform(hass, FAN_DOMAIN, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'fan', 'turn_on', @@ -128,7 +128,7 @@ async def test_set_speed(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - await setup_platform(hass, FAN_DOMAIN, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'fan', 'set_speed', @@ -149,7 +149,7 @@ async def test_update_from_signal(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - await setup_platform(hass, FAN_DOMAIN, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) await device.switch_on(True) # Act async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, @@ -168,7 +168,7 @@ async def test_unload_config_entry(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - config_entry = await setup_platform(hass, FAN_DOMAIN, device) + config_entry = await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'fan') diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 0e35ef80fc2..ec0b3982517 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -77,6 +77,19 @@ async def test_recoverable_api_errors_raise_not_ready( await smartthings.async_setup_entry(hass, config_entry) +async def test_scenes_api_errors_raise_not_ready( + hass, config_entry, app, installed_app, smartthings_mock): + """Test if scenes are unauthorized we continue to load platforms.""" + setattr(hass.config_entries, '_entries', [config_entry]) + api = smartthings_mock.return_value + api.app.return_value = mock_coro(return_value=app) + api.installed_app.return_value = mock_coro(return_value=installed_app) + api.scenes.return_value = mock_coro( + exception=ClientResponseError(None, None, status=500)) + with pytest.raises(ConfigEntryNotReady): + await smartthings.async_setup_entry(hass, config_entry) + + async def test_connection_errors_raise_not_ready( hass, config_entry, smartthings_mock): """Test config entry not ready raised for connection errors.""" @@ -118,17 +131,45 @@ async def test_unauthorized_installed_app_raises_not_ready( await smartthings.async_setup_entry(hass, config_entry) -async def test_config_entry_loads_platforms( +async def test_scenes_unauthorized_loads_platforms( hass, config_entry, app, installed_app, device, smartthings_mock, subscription_factory): - """Test config entry loads properly and proxies to platforms.""" + """Test if scenes are unauthorized we continue to load platforms.""" setattr(hass.config_entries, '_entries', [config_entry]) - api = smartthings_mock.return_value api.app.return_value = mock_coro(return_value=app) api.installed_app.return_value = mock_coro(return_value=installed_app) api.devices.side_effect = \ lambda *args, **kwargs: mock_coro(return_value=[device]) + api.scenes.return_value = mock_coro( + exception=ClientResponseError(None, None, status=403)) + mock_token = Mock() + mock_token.access_token.return_value = str(uuid4()) + mock_token.refresh_token.return_value = str(uuid4()) + api.generate_tokens.return_value = mock_coro(return_value=mock_token) + subscriptions = [subscription_factory(capability) + for capability in device.capabilities] + api.subscriptions.return_value = mock_coro(return_value=subscriptions) + + with patch.object(hass.config_entries, 'async_forward_entry_setup', + return_value=mock_coro()) as forward_mock: + assert await smartthings.async_setup_entry(hass, config_entry) + # Assert platforms loaded + await hass.async_block_till_done() + assert forward_mock.call_count == len(SUPPORTED_PLATFORMS) + + +async def test_config_entry_loads_platforms( + hass, config_entry, app, installed_app, + device, smartthings_mock, subscription_factory, scene): + """Test config entry loads properly and proxies to platforms.""" + setattr(hass.config_entries, '_entries', [config_entry]) + api = smartthings_mock.return_value + api.app.return_value = mock_coro(return_value=app) + api.installed_app.return_value = mock_coro(return_value=installed_app) + api.devices.side_effect = \ + lambda *args, **kwargs: mock_coro(return_value=[device]) + api.scenes.return_value = mock_coro(return_value=[scene]) mock_token = Mock() mock_token.access_token.return_value = str(uuid4()) mock_token.refresh_token.return_value = str(uuid4()) @@ -151,7 +192,7 @@ async def test_unload_entry(hass, config_entry): smart_app = Mock() smart_app.connect_event.return_value = connect_disconnect broker = smartthings.DeviceBroker( - hass, config_entry, Mock(), smart_app, []) + hass, config_entry, Mock(), smart_app, [], []) broker.connect() hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] = broker @@ -184,7 +225,7 @@ async def test_broker_regenerates_token( '.async_track_time_interval', new=async_track_time_interval): broker = smartthings.DeviceBroker( - hass, config_entry, token, Mock(), []) + hass, config_entry, token, Mock(), [], []) broker.connect() assert stored_action @@ -214,7 +255,7 @@ async def test_event_handler_dispatches_updated_devices( async_dispatcher_connect(hass, SIGNAL_SMARTTHINGS_UPDATE, signal) broker = smartthings.DeviceBroker( - hass, config_entry, Mock(), Mock(), devices) + hass, config_entry, Mock(), Mock(), devices, []) broker.connect() # pylint:disable=protected-access @@ -238,7 +279,7 @@ async def test_event_handler_ignores_other_installed_app( called = True async_dispatcher_connect(hass, SIGNAL_SMARTTHINGS_UPDATE, signal) broker = smartthings.DeviceBroker( - hass, config_entry, Mock(), Mock(), [device]) + hass, config_entry, Mock(), Mock(), [device], []) broker.connect() # pylint:disable=protected-access @@ -271,7 +312,7 @@ async def test_event_handler_fires_button_events( } hass.bus.async_listen(EVENT_BUTTON, handler) broker = smartthings.DeviceBroker( - hass, config_entry, Mock(), Mock(), [device]) + hass, config_entry, Mock(), Mock(), [device], []) broker.connect() # pylint:disable=protected-access diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index d31507925d6..6efd88d7237 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -52,7 +52,7 @@ async def test_async_setup_platform(): async def test_entity_state(hass, light_devices): """Tests the state attributes properly match the light types.""" - await setup_platform(hass, LIGHT_DOMAIN, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Dimmer 1 state = hass.states.get('light.dimmer_1') @@ -86,7 +86,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await setup_platform(hass, LIGHT_DOMAIN, device) + await setup_platform(hass, LIGHT_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get("light.light_1") assert entry @@ -103,7 +103,7 @@ async def test_entity_and_device_attributes(hass, device_factory): async def test_turn_off(hass, light_devices): """Test the light turns of successfully.""" # Arrange - await setup_platform(hass, LIGHT_DOMAIN, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_off', {'entity_id': 'light.color_dimmer_2'}, @@ -117,7 +117,7 @@ async def test_turn_off(hass, light_devices): async def test_turn_off_with_transition(hass, light_devices): """Test the light turns of successfully with transition.""" # Arrange - await setup_platform(hass, LIGHT_DOMAIN, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_off', @@ -132,7 +132,7 @@ async def test_turn_off_with_transition(hass, light_devices): async def test_turn_on(hass, light_devices): """Test the light turns of successfully.""" # Arrange - await setup_platform(hass, LIGHT_DOMAIN, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_on', {ATTR_ENTITY_ID: "light.color_dimmer_1"}, @@ -146,7 +146,7 @@ async def test_turn_on(hass, light_devices): async def test_turn_on_with_brightness(hass, light_devices): """Test the light turns on to the specified brightness.""" # Arrange - await setup_platform(hass, LIGHT_DOMAIN, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_on', @@ -170,7 +170,7 @@ async def test_turn_on_with_minimal_brightness(hass, light_devices): set the level to zero, which turns off the lights in SmartThings. """ # Arrange - await setup_platform(hass, LIGHT_DOMAIN, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_on', @@ -188,7 +188,7 @@ async def test_turn_on_with_minimal_brightness(hass, light_devices): async def test_turn_on_with_color(hass, light_devices): """Test the light turns on with color.""" # Arrange - await setup_platform(hass, LIGHT_DOMAIN, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_on', @@ -205,7 +205,7 @@ async def test_turn_on_with_color(hass, light_devices): async def test_turn_on_with_color_temp(hass, light_devices): """Test the light turns on with color temp.""" # Arrange - await setup_platform(hass, LIGHT_DOMAIN, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_on', @@ -229,7 +229,7 @@ async def test_update_from_signal(hass, device_factory): status={Attribute.switch: 'off', Attribute.level: 100, Attribute.hue: 76.0, Attribute.saturation: 55.0, Attribute.color_temperature: 4500}) - await setup_platform(hass, LIGHT_DOMAIN, device) + await setup_platform(hass, LIGHT_DOMAIN, devices=[device]) await device.switch_on(True) # Act async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, @@ -251,7 +251,7 @@ async def test_unload_config_entry(hass, device_factory): status={Attribute.switch: 'off', Attribute.level: 100, Attribute.hue: 76.0, Attribute.saturation: 55.0, Attribute.color_temperature: 4500}) - config_entry = await setup_platform(hass, LIGHT_DOMAIN, device) + config_entry = await setup_platform(hass, LIGHT_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'light') diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 922abbb161f..1d98e5f9bdb 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -29,7 +29,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await setup_platform(hass, LOCK_DOMAIN, device) + await setup_platform(hass, LOCK_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get('lock.lock_1') assert entry @@ -55,7 +55,7 @@ async def test_lock(hass, device_factory): 'lockName': 'Front Door', 'usedCode': 'Code 2' }) - await setup_platform(hass, LOCK_DOMAIN, device) + await setup_platform(hass, LOCK_DOMAIN, devices=[device]) # Act await hass.services.async_call( LOCK_DOMAIN, 'lock', {'entity_id': 'lock.lock_1'}, @@ -77,7 +77,7 @@ async def test_unlock(hass, device_factory): # Arrange device = device_factory('Lock_1', [Capability.lock], {Attribute.lock: 'locked'}) - await setup_platform(hass, LOCK_DOMAIN, device) + await setup_platform(hass, LOCK_DOMAIN, devices=[device]) # Act await hass.services.async_call( LOCK_DOMAIN, 'unlock', {'entity_id': 'lock.lock_1'}, @@ -93,7 +93,7 @@ async def test_update_from_signal(hass, device_factory): # Arrange device = device_factory('Lock_1', [Capability.lock], {Attribute.lock: 'unlocked'}) - await setup_platform(hass, LOCK_DOMAIN, device) + await setup_platform(hass, LOCK_DOMAIN, devices=[device]) await device.lock(True) # Act async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, @@ -110,7 +110,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory('Lock_1', [Capability.lock], {Attribute.lock: 'locked'}) - config_entry = await setup_platform(hass, LOCK_DOMAIN, device) + config_entry = await setup_platform(hass, LOCK_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'lock') diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py new file mode 100644 index 00000000000..2d4990675f8 --- /dev/null +++ b/tests/components/smartthings/test_scene.py @@ -0,0 +1,54 @@ +""" +Test for the SmartThings scene platform. + +The only mocking required is of the underlying SmartThings API object so +real HTTP calls are not initiated during testing. +""" +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN +from homeassistant.components.smartthings import scene as scene_platform +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON + +from .conftest import setup_platform + + +async def test_async_setup_platform(): + """Test setup platform does nothing (it uses config entries).""" + await scene_platform.async_setup_platform(None, None, None) + + +async def test_entity_and_device_attributes(hass, scene): + """Test the attributes of the entity are correct.""" + # Arrange + entity_registry = await hass.helpers.entity_registry.async_get_registry() + # Act + await setup_platform(hass, SCENE_DOMAIN, scenes=[scene]) + # Assert + entry = entity_registry.async_get('scene.test_scene') + assert entry + assert entry.unique_id == scene.scene_id + + +async def test_scene_activate(hass, scene): + """Test the scene is activated.""" + await setup_platform(hass, SCENE_DOMAIN, scenes=[scene]) + await hass.services.async_call( + SCENE_DOMAIN, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: 'scene.test_scene'}, + blocking=True) + state = hass.states.get('scene.test_scene') + assert state.attributes['icon'] == scene.icon + assert state.attributes['color'] == scene.color + assert state.attributes['location_id'] == scene.location_id + # pylint: disable=protected-access + assert scene._api.execute_scene.call_count == 1 # type: ignore + + +async def test_unload_config_entry(hass, scene): + """Test the scene is removed when the config entry is unloaded.""" + # Arrange + config_entry = await setup_platform(hass, SCENE_DOMAIN, scenes=[scene]) + # Act + await hass.config_entries.async_forward_entry_unload( + config_entry, SCENE_DOMAIN) + # Assert + assert not hass.states.get('scene.test_scene') diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 773f157dd87..879aae1994d 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -37,7 +37,7 @@ async def test_entity_state(hass, device_factory): """Tests the state attributes properly match the light types.""" device = device_factory('Sensor 1', [Capability.battery], {Attribute.battery: 100}) - await setup_platform(hass, SENSOR_DOMAIN, device) + await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) state = hass.states.get('sensor.sensor_1_battery') assert state.state == '100' assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == '%' @@ -53,7 +53,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await setup_platform(hass, SENSOR_DOMAIN, device) + await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get('sensor.sensor_1_battery') assert entry @@ -71,7 +71,7 @@ async def test_update_from_signal(hass, device_factory): # Arrange device = device_factory('Sensor 1', [Capability.battery], {Attribute.battery: 100}) - await setup_platform(hass, SENSOR_DOMAIN, device) + await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) device.status.apply_attribute_update( 'main', Capability.battery, Attribute.battery, 75) # Act @@ -89,7 +89,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory('Sensor 1', [Capability.battery], {Attribute.battery: 100}) - config_entry = await setup_platform(hass, SENSOR_DOMAIN, device) + config_entry = await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'sensor') diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 7d21db00460..e3b1f46bf39 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -29,7 +29,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await setup_platform(hass, SWITCH_DOMAIN, device) + await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get('switch.switch_1') assert entry @@ -48,7 +48,7 @@ async def test_turn_off(hass, device_factory): # Arrange device = device_factory('Switch_1', [Capability.switch], {Attribute.switch: 'on'}) - await setup_platform(hass, SWITCH_DOMAIN, device) + await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'switch', 'turn_off', {'entity_id': 'switch.switch_1'}, @@ -69,7 +69,7 @@ async def test_turn_on(hass, device_factory): {Attribute.switch: 'off', Attribute.power: 355, Attribute.energy: 11.422}) - await setup_platform(hass, SWITCH_DOMAIN, device) + await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'switch', 'turn_on', {'entity_id': 'switch.switch_1'}, @@ -87,7 +87,7 @@ async def test_update_from_signal(hass, device_factory): # Arrange device = device_factory('Switch_1', [Capability.switch], {Attribute.switch: 'off'}) - await setup_platform(hass, SWITCH_DOMAIN, device) + await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) await device.switch_on(True) # Act async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, @@ -104,7 +104,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory('Switch 1', [Capability.switch], {Attribute.switch: 'on'}) - config_entry = await setup_platform(hass, SWITCH_DOMAIN, device) + config_entry = await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'switch')