Enhance SmartThings component subscription (#21124)

* Move to config v2 to store SmartApp oauth keys

* Add migration functionality.

* Regenerate refresh token on periodic basis

* Fix regenerate and misc. optimizations

* Review feedback

* Subscription sync logic now performs a difference operation

* Removed config entry reloading.
This commit is contained in:
Andrew Sayre authored and Martin Hjelmare committed 2019-02-22 20:35:12 +01:00
1 parent d9712027e8
commit 8b38b82e73
14 files changed
+529 -275

No files matched your search

+33 -8
View File
@@ -4,8 +4,8 @@ from unittest.mock import Mock, patch
from uuid import uuid4
from pysmartthings import (
CLASSIFICATION_AUTOMATION, AppEntity, AppSettings, DeviceEntity,
InstalledApp, Location)
CLASSIFICATION_AUTOMATION, AppEntity, AppOAuthClient, AppSettings,
DeviceEntity, InstalledApp, Location, Subscription)
from pysmartthings.api import Api
import pytest
@@ -13,8 +13,9 @@ from homeassistant.components import webhook
from homeassistant.components.smartthings import DeviceBroker
from homeassistant.components.smartthings.const import (
APP_NAME_PREFIX, CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_INSTANCE_ID,
CONF_LOCATION_ID, DATA_BROKERS, DOMAIN, SETTINGS_INSTANCE_ID, STORAGE_KEY,
STORAGE_VERSION)
CONF_LOCATION_ID, CONF_OAUTH_CLIENT_ID, CONF_OAUTH_CLIENT_SECRET,
CONF_REFRESH_TOKEN, DATA_BROKERS, DOMAIN, SETTINGS_INSTANCE_ID,
STORAGE_KEY, STORAGE_VERSION)
from homeassistant.config_entries import (
CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry)
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_WEBHOOK_ID
@@ -26,9 +27,11 @@ from tests.common import mock_coro
async def setup_platform(hass, platform: str, *devices):
"""Set up the SmartThings platform and prerequisites."""
hass.config.components.add(DOMAIN)
broker = DeviceBroker(hass, devices, '')
config_entry = ConfigEntry("1", DOMAIN, "Test", {},
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)
hass.data[DOMAIN] = {
DATA_BROKERS: {
config_entry.entry_id: broker
@@ -98,6 +101,15 @@ def app_fixture(hass, config_file):
return app
@pytest.fixture(name="app_oauth_client")
def app_oauth_client_fixture():
"""Fixture for a single app's oauth."""
return AppOAuthClient({
'oauthClientId': str(uuid4()),
'oauthClientSecret': str(uuid4())
})
@pytest.fixture(name='app_settings')
def app_settings_fixture(app, config_file):
"""Fixture for an app settings."""
@@ -225,12 +237,25 @@ def config_entry_fixture(hass, installed_app, location):
CONF_ACCESS_TOKEN: str(uuid4()),
CONF_INSTALLED_APP_ID: installed_app.installed_app_id,
CONF_APP_ID: installed_app.app_id,
CONF_LOCATION_ID: location.location_id
CONF_LOCATION_ID: location.location_id,
CONF_REFRESH_TOKEN: str(uuid4()),
CONF_OAUTH_CLIENT_ID: str(uuid4()),
CONF_OAUTH_CLIENT_SECRET: str(uuid4())
}
return ConfigEntry("1", DOMAIN, location.name, data, SOURCE_USER,
return ConfigEntry(2, DOMAIN, location.name, data, SOURCE_USER,
CONN_CLASS_CLOUD_PUSH)
@pytest.fixture(name="subscription_factory")
def subscription_factory_fixture():
"""Fixture for creating mock subscriptions."""
def _factory(capability):
sub = Subscription()
sub.capability = capability
return sub
return _factory
@pytest.fixture(name="device_factory")
def device_factory_fixture():
"""Fixture for creating mock devices."""
@@ -6,31 +6,15 @@ real HTTP calls are not initiated during testing.
"""
from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability
from homeassistant.components.binary_sensor import DEVICE_CLASSES
from homeassistant.components.smartthings import DeviceBroker, binary_sensor
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES, DOMAIN as BINARY_SENSOR_DOMAIN)
from homeassistant.components.smartthings import binary_sensor
from homeassistant.components.smartthings.const import (
DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE)
from homeassistant.config_entries import (
CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry)
DOMAIN, SIGNAL_SMARTTHINGS_UPDATE)
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.helpers.dispatcher import async_dispatcher_send
async def _setup_platform(hass, *devices):
"""Set up the SmartThings binary_sensor platform and prerequisites."""
hass.config.components.add(DOMAIN)
broker = DeviceBroker(hass, devices, '')
config_entry = ConfigEntry("1", DOMAIN, "Test", {},
SOURCE_USER, CONN_CLASS_CLOUD_PUSH)
hass.data[DOMAIN] = {
DATA_BROKERS: {
config_entry.entry_id: broker
}
}
await hass.config_entries.async_forward_entry_setup(
config_entry, 'binary_sensor')
await hass.async_block_till_done()
return config_entry
from .conftest import setup_platform
async def test_mapping_integrity():
@@ -56,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, device)
await setup_platform(hass, BINARY_SENSOR_DOMAIN, device)
state = hass.states.get('binary_sensor.motion_sensor_1_motion')
assert state.state == 'off'
assert state.attributes[ATTR_FRIENDLY_NAME] ==\
@@ -71,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, device)
await setup_platform(hass, BINARY_SENSOR_DOMAIN, device)
# Assert
entry = entity_registry.async_get('binary_sensor.motion_sensor_1_motion')
assert entry
@@ -89,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, device)
await setup_platform(hass, BINARY_SENSOR_DOMAIN, device)
device.status.apply_attribute_update(
'main', Capability.motion_sensor, Attribute.motion, 'active')
# Act
@@ -107,7 +91,7 @@ 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, device)
config_entry = await setup_platform(hass, BINARY_SENSOR_DOMAIN, device)
# Act
await hass.config_entries.async_forward_entry_unload(
config_entry, 'binary_sensor')
@@ -8,6 +8,9 @@ from pysmartthings import APIResponseError
from homeassistant import data_entry_flow
from homeassistant.components.smartthings.config_flow import (
SmartThingsFlowHandler)
from homeassistant.components.smartthings.const import (
CONF_INSTALLED_APP_ID, CONF_INSTALLED_APPS, CONF_LOCATION_ID,
CONF_REFRESH_TOKEN, DOMAIN)
from homeassistant.config_entries import ConfigEntry
from tests.common import mock_coro
@@ -171,14 +174,16 @@ async def test_unknown_error(hass, smartthings_mock):
assert result['errors'] == {'base': 'app_setup_error'}
async def test_app_created_then_show_wait_form(hass, app, smartthings_mock):
async def test_app_created_then_show_wait_form(
hass, app, app_oauth_client, smartthings_mock):
"""Test SmartApp is created when one does not exist and shows wait form."""
flow = SmartThingsFlowHandler()
flow.hass = hass
smartthings = smartthings_mock.return_value
smartthings.apps.return_value = mock_coro(return_value=[])
smartthings.create_app.return_value = mock_coro(return_value=(app, None))
smartthings.create_app.return_value = \
mock_coro(return_value=(app, app_oauth_client))
smartthings.update_app_settings.return_value = mock_coro()
smartthings.update_app_oauth.return_value = mock_coro()
@@ -189,13 +194,15 @@ async def test_app_created_then_show_wait_form(hass, app, smartthings_mock):
async def test_app_updated_then_show_wait_form(
hass, app, smartthings_mock):
hass, app, app_oauth_client, smartthings_mock):
"""Test SmartApp is updated when an existing is already created."""
flow = SmartThingsFlowHandler()
flow.hass = hass
api = smartthings_mock.return_value
api.apps.return_value = mock_coro(return_value=[app])
api.generate_app_oauth.return_value = \
mock_coro(return_value=app_oauth_client)
result = await flow.async_step_user({'access_token': str(uuid4())})
@@ -219,8 +226,6 @@ async def test_wait_form_displayed_after_checking(hass, smartthings_mock):
flow = SmartThingsFlowHandler()
flow.hass = hass
flow.access_token = str(uuid4())
flow.api = smartthings_mock.return_value
flow.api.installed_apps.return_value = mock_coro(return_value=[])
result = await flow.async_step_wait_install({})
@@ -235,19 +240,29 @@ async def test_config_entry_created_when_installed(
flow = SmartThingsFlowHandler()
flow.hass = hass
flow.access_token = str(uuid4())
flow.api = smartthings_mock.return_value
flow.app_id = installed_app.app_id
flow.api.installed_apps.return_value = \
mock_coro(return_value=[installed_app])
flow.api = smartthings_mock.return_value
flow.oauth_client_id = str(uuid4())
flow.oauth_client_secret = str(uuid4())
data = {
CONF_REFRESH_TOKEN: str(uuid4()),
CONF_LOCATION_ID: installed_app.location_id,
CONF_INSTALLED_APP_ID: installed_app.installed_app_id
}
hass.data[DOMAIN][CONF_INSTALLED_APPS].append(data)
result = await flow.async_step_wait_install({})
assert not hass.data[DOMAIN][CONF_INSTALLED_APPS]
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result['data']['app_id'] == installed_app.app_id
assert result['data']['installed_app_id'] == \
installed_app.installed_app_id
assert result['data']['location_id'] == installed_app.location_id
assert result['data']['access_token'] == flow.access_token
assert result['data']['refresh_token'] == data[CONF_REFRESH_TOKEN]
assert result['data']['client_secret'] == flow.oauth_client_secret
assert result['data']['client_id'] == flow.oauth_client_id
assert result['title'] == location.name
@@ -259,17 +274,31 @@ async def test_multiple_config_entry_created_when_installed(
flow.access_token = str(uuid4())
flow.app_id = app.app_id
flow.api = smartthings_mock.return_value
flow.api.installed_apps.return_value = \
mock_coro(return_value=installed_apps)
flow.oauth_client_id = str(uuid4())
flow.oauth_client_secret = str(uuid4())
for installed_app in installed_apps:
data = {
CONF_REFRESH_TOKEN: str(uuid4()),
CONF_LOCATION_ID: installed_app.location_id,
CONF_INSTALLED_APP_ID: installed_app.installed_app_id
}
hass.data[DOMAIN][CONF_INSTALLED_APPS].append(data)
install_data = hass.data[DOMAIN][CONF_INSTALLED_APPS].copy()
result = await flow.async_step_wait_install({})
assert not hass.data[DOMAIN][CONF_INSTALLED_APPS]
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result['data']['app_id'] == installed_apps[0].app_id
assert result['data']['installed_app_id'] == \
installed_apps[0].installed_app_id
assert result['data']['location_id'] == installed_apps[0].location_id
assert result['data']['access_token'] == flow.access_token
assert result['data']['refresh_token'] == \
install_data[0][CONF_REFRESH_TOKEN]
assert result['data']['client_secret'] == flow.oauth_client_secret
assert result['data']['client_id'] == flow.oauth_client_id
assert result['title'] == locations[0].name
await hass.async_block_till_done()
@@ -280,4 +309,6 @@ async def test_multiple_config_entry_created_when_installed(
installed_apps[1].installed_app_id
assert entries[0].data['location_id'] == installed_apps[1].location_id
assert entries[0].data['access_token'] == flow.access_token
assert entries[0].data['client_secret'] == flow.oauth_client_secret
assert entries[0].data['client_id'] == flow.oauth_client_id
assert entries[0].title == locations[1].name
+14 -31
View File
@@ -7,31 +7,15 @@ real HTTP calls are not initiated during testing.
from pysmartthings import Attribute, Capability
from homeassistant.components.fan import (
ATTR_SPEED, ATTR_SPEED_LIST, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM,
SPEED_OFF, SUPPORT_SET_SPEED)
from homeassistant.components.smartthings import DeviceBroker, fan
ATTR_SPEED, ATTR_SPEED_LIST, DOMAIN as FAN_DOMAIN, SPEED_HIGH, SPEED_LOW,
SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED)
from homeassistant.components.smartthings import fan
from homeassistant.components.smartthings.const import (
DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE)
from homeassistant.config_entries import (
CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry)
DOMAIN, SIGNAL_SMARTTHINGS_UPDATE)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES
from homeassistant.helpers.dispatcher import async_dispatcher_send
async def _setup_platform(hass, *devices):
"""Set up the SmartThings fan platform and prerequisites."""
hass.config.components.add(DOMAIN)
broker = DeviceBroker(hass, devices, '')
config_entry = ConfigEntry("1", DOMAIN, "Test", {},
SOURCE_USER, CONN_CLASS_CLOUD_PUSH)
hass.data[DOMAIN] = {
DATA_BROKERS: {
config_entry.entry_id: broker
}
}
await hass.config_entries.async_forward_entry_setup(config_entry, 'fan')
await hass.async_block_till_done()
return config_entry
from .conftest import setup_platform
async def test_async_setup_platform():
@@ -45,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, device)
await setup_platform(hass, FAN_DOMAIN, device)
# Dimmer 1
state = hass.states.get('fan.fan_1')
@@ -63,11 +47,10 @@ async def test_entity_and_device_attributes(hass, device_factory):
"Fan 1",
capabilities=[Capability.switch, Capability.fan_speed],
status={Attribute.switch: 'on', Attribute.fan_speed: 2})
await _setup_platform(hass, device)
# Act
await setup_platform(hass, FAN_DOMAIN, device)
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, device)
# Assert
entry = entity_registry.async_get("fan.fan_1")
assert entry
@@ -88,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, device)
await setup_platform(hass, FAN_DOMAIN, device)
# Act
await hass.services.async_call(
'fan', 'turn_off', {'entity_id': 'fan.fan_1'},
@@ -106,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, device)
await setup_platform(hass, FAN_DOMAIN, device)
# Act
await hass.services.async_call(
'fan', 'turn_on', {ATTR_ENTITY_ID: "fan.fan_1"},
@@ -124,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, device)
await setup_platform(hass, FAN_DOMAIN, device)
# Act
await hass.services.async_call(
'fan', 'turn_on',
@@ -145,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, device)
await setup_platform(hass, FAN_DOMAIN, device)
# Act
await hass.services.async_call(
'fan', 'set_speed',
@@ -166,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, device)
await setup_platform(hass, FAN_DOMAIN, device)
await device.switch_on(True)
# Act
async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE,
@@ -185,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, device)
config_entry = await setup_platform(hass, FAN_DOMAIN, device)
# Act
await hass.config_entries.async_forward_entry_unload(
config_entry, 'fan')
+86 -17
View File
@@ -8,14 +8,33 @@ import pytest
from homeassistant.components import smartthings
from homeassistant.components.smartthings.const import (
DATA_BROKERS, DOMAIN, EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE,
SUPPORTED_PLATFORMS)
CONF_INSTALLED_APP_ID, CONF_REFRESH_TOKEN, DATA_BROKERS, DOMAIN,
EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS)
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from tests.common import mock_coro
async def test_migration_creates_new_flow(
hass, smartthings_mock, config_entry):
"""Test migration deletes app and creates new flow."""
config_entry.version = 1
setattr(hass.config_entries, '_entries', [config_entry])
api = smartthings_mock.return_value
api.delete_installed_app.return_value = mock_coro()
await smartthings.async_migrate_entry(hass, config_entry)
assert api.delete_installed_app.call_count == 1
await hass.async_block_till_done()
assert not hass.config_entries.async_entries(DOMAIN)
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]['handler'] == 'smartthings'
assert flows[0]['context'] == {'source': 'import'}
async def test_unrecoverable_api_errors_create_new_flow(
hass, config_entry, smartthings_mock):
"""
@@ -101,14 +120,22 @@ async def test_unauthorized_installed_app_raises_not_ready(
async def test_config_entry_loads_platforms(
hass, config_entry, app, installed_app,
device, smartthings_mock):
device, smartthings_mock, subscription_factory):
"""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.return_value = mock_coro(return_value=[device])
api.devices.side_effect = \
lambda *args, **kwargs: mock_coro(return_value=[device])
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:
@@ -120,8 +147,12 @@ async def test_config_entry_loads_platforms(
async def test_unload_entry(hass, config_entry):
"""Test entries are unloaded correctly."""
broker = Mock()
broker.event_handler_disconnect = Mock()
connect_disconnect = Mock()
smart_app = Mock()
smart_app.connect_event.return_value = connect_disconnect
broker = smartthings.DeviceBroker(
hass, config_entry, Mock(), smart_app, [])
broker.connect()
hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] = broker
with patch.object(hass.config_entries, 'async_forward_entry_unload',
@@ -129,15 +160,41 @@ async def test_unload_entry(hass, config_entry):
return_value=True
)) as forward_mock:
assert await smartthings.async_unload_entry(hass, config_entry)
assert broker.event_handler_disconnect.call_count == 1
assert connect_disconnect.call_count == 1
assert config_entry.entry_id not in hass.data[DOMAIN][DATA_BROKERS]
# Assert platforms unloaded
await hass.async_block_till_done()
assert forward_mock.call_count == len(SUPPORTED_PLATFORMS)
async def test_broker_regenerates_token(
hass, config_entry):
"""Test the device broker regenerates the refresh token."""
token = Mock()
token.refresh_token = str(uuid4())
token.refresh.return_value = mock_coro()
stored_action = None
def async_track_time_interval(hass, action, interval):
nonlocal stored_action
stored_action = action
with patch('homeassistant.components.smartthings'
'.async_track_time_interval',
new=async_track_time_interval):
broker = smartthings.DeviceBroker(
hass, config_entry, token, Mock(), [])
broker.connect()
assert stored_action
await stored_action(None) # pylint:disable=not-callable
assert token.refresh.call_count == 1
assert config_entry.data[CONF_REFRESH_TOKEN] == token.refresh_token
async def test_event_handler_dispatches_updated_devices(
hass, device_factory, event_request_factory):
hass, config_entry, device_factory, event_request_factory):
"""Test the event handler dispatches updated devices."""
devices = [
device_factory('Bedroom 1 Switch', ['switch']),
@@ -147,6 +204,7 @@ async def test_event_handler_dispatches_updated_devices(
device_ids = [devices[0].device_id, devices[1].device_id,
devices[2].device_id]
request = event_request_factory(device_ids)
config_entry.data[CONF_INSTALLED_APP_ID] = request.installed_app_id
called = False
def signal(ids):
@@ -154,10 +212,13 @@ async def test_event_handler_dispatches_updated_devices(
called = True
assert device_ids == ids
async_dispatcher_connect(hass, SIGNAL_SMARTTHINGS_UPDATE, signal)
broker = smartthings.DeviceBroker(
hass, devices, request.installed_app_id)
await broker.event_handler(request, None, None)
broker = smartthings.DeviceBroker(
hass, config_entry, Mock(), Mock(), devices)
broker.connect()
# pylint:disable=protected-access
await broker._event_handler(request, None, None)
await hass.async_block_till_done()
assert called
@@ -166,7 +227,7 @@ async def test_event_handler_dispatches_updated_devices(
async def test_event_handler_ignores_other_installed_app(
hass, device_factory, event_request_factory):
hass, config_entry, device_factory, event_request_factory):
"""Test the event handler dispatches updated devices."""
device = device_factory('Bedroom 1 Switch', ['switch'])
request = event_request_factory([device.device_id])
@@ -176,21 +237,26 @@ async def test_event_handler_ignores_other_installed_app(
nonlocal called
called = True
async_dispatcher_connect(hass, SIGNAL_SMARTTHINGS_UPDATE, signal)
broker = smartthings.DeviceBroker(hass, [device], str(uuid4()))
broker = smartthings.DeviceBroker(
hass, config_entry, Mock(), Mock(), [device])
broker.connect()
await broker.event_handler(request, None, None)
# pylint:disable=protected-access
await broker._event_handler(request, None, None)
await hass.async_block_till_done()
assert not called
async def test_event_handler_fires_button_events(
hass, device_factory, event_factory, event_request_factory):
hass, config_entry, device_factory, event_factory,
event_request_factory):
"""Test the event handler fires button events."""
device = device_factory('Button 1', ['button'])
event = event_factory(device.device_id, capability='button',
attribute='button', value='pushed')
request = event_request_factory(events=[event])
config_entry.data[CONF_INSTALLED_APP_ID] = request.installed_app_id
called = False
def handler(evt):
@@ -205,8 +271,11 @@ async def test_event_handler_fires_button_events(
}
hass.bus.async_listen(EVENT_BUTTON, handler)
broker = smartthings.DeviceBroker(
hass, [device], request.installed_app_id)
await broker.event_handler(request, None, None)
hass, config_entry, Mock(), Mock(), [device])
broker.connect()
# pylint:disable=protected-access
await broker._event_handler(request, None, None)
await hass.async_block_till_done()
assert called
+17 -32
View File
@@ -9,15 +9,16 @@ import pytest
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION)
from homeassistant.components.smartthings import DeviceBroker, light
DOMAIN as LIGHT_DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_COLOR,
SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION)
from homeassistant.components.smartthings import light
from homeassistant.components.smartthings.const import (
DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE)
from homeassistant.config_entries import (
CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry)
DOMAIN, SIGNAL_SMARTTHINGS_UPDATE)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .conftest import setup_platform
@pytest.fixture(name="light_devices")
def light_devices_fixture(device_factory):
@@ -44,22 +45,6 @@ def light_devices_fixture(device_factory):
]
async def _setup_platform(hass, *devices):
"""Set up the SmartThings light platform and prerequisites."""
hass.config.components.add(DOMAIN)
broker = DeviceBroker(hass, devices, '')
config_entry = ConfigEntry("1", DOMAIN, "Test", {},
SOURCE_USER, CONN_CLASS_CLOUD_PUSH)
hass.data[DOMAIN] = {
DATA_BROKERS: {
config_entry.entry_id: broker
}
}
await hass.config_entries.async_forward_entry_setup(config_entry, 'light')
await hass.async_block_till_done()
return config_entry
async def test_async_setup_platform():
"""Test setup platform does nothing (it uses config entries)."""
await light.async_setup_platform(None, None, None)
@@ -67,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_devices)
await setup_platform(hass, LIGHT_DOMAIN, *light_devices)
# Dimmer 1
state = hass.states.get('light.dimmer_1')
@@ -101,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, device)
await setup_platform(hass, LIGHT_DOMAIN, device)
# Assert
entry = entity_registry.async_get("light.light_1")
assert entry
@@ -118,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_devices)
await setup_platform(hass, LIGHT_DOMAIN, *light_devices)
# Act
await hass.services.async_call(
'light', 'turn_off', {'entity_id': 'light.color_dimmer_2'},
@@ -132,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_devices)
await setup_platform(hass, LIGHT_DOMAIN, *light_devices)
# Act
await hass.services.async_call(
'light', 'turn_off',
@@ -147,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_devices)
await setup_platform(hass, LIGHT_DOMAIN, *light_devices)
# Act
await hass.services.async_call(
'light', 'turn_on', {ATTR_ENTITY_ID: "light.color_dimmer_1"},
@@ -161,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_devices)
await setup_platform(hass, LIGHT_DOMAIN, *light_devices)
# Act
await hass.services.async_call(
'light', 'turn_on',
@@ -185,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_devices)
await setup_platform(hass, LIGHT_DOMAIN, *light_devices)
# Act
await hass.services.async_call(
'light', 'turn_on',
@@ -203,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_devices)
await setup_platform(hass, LIGHT_DOMAIN, *light_devices)
# Act
await hass.services.async_call(
'light', 'turn_on',
@@ -220,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_devices)
await setup_platform(hass, LIGHT_DOMAIN, *light_devices)
# Act
await hass.services.async_call(
'light', 'turn_on',
@@ -244,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, device)
await setup_platform(hass, LIGHT_DOMAIN, device)
await device.switch_on(True)
# Act
async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE,
@@ -266,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, device)
config_entry = await setup_platform(hass, LIGHT_DOMAIN, device)
# Act
await hass.config_entries.async_forward_entry_unload(
config_entry, 'light')
+113 -36
View File
@@ -5,7 +5,9 @@ from uuid import uuid4
from pysmartthings import AppEntity, Capability
from homeassistant.components.smartthings import smartapp
from homeassistant.components.smartthings.const import DATA_MANAGER, DOMAIN
from homeassistant.components.smartthings.const import (
CONF_INSTALLED_APP_ID, CONF_INSTALLED_APPS, CONF_LOCATION_ID,
CONF_REFRESH_TOKEN, DATA_MANAGER, DOMAIN)
from tests.common import mock_coro
@@ -35,31 +37,26 @@ async def test_update_app_updated_needed(hass, app):
assert mock_app.classifications == app.classifications
async def test_smartapp_install_abort_if_no_other(
async def test_smartapp_install_store_if_no_other(
hass, smartthings_mock, device_factory):
"""Test aborts if no other app was configured already."""
# Arrange
api = smartthings_mock.return_value
api.create_subscription.return_value = mock_coro()
app = Mock()
app.app_id = uuid4()
request = Mock()
request.installed_app_id = uuid4()
request.auth_token = uuid4()
request.location_id = uuid4()
devices = [
device_factory('', [Capability.battery, 'ping']),
device_factory('', [Capability.switch, Capability.switch_level]),
device_factory('', [Capability.switch])
]
api.devices = Mock()
api.devices.return_value = mock_coro(return_value=devices)
request.installed_app_id = str(uuid4())
request.auth_token = str(uuid4())
request.location_id = str(uuid4())
request.refresh_token = str(uuid4())
# Act
await smartapp.smartapp_install(hass, request, None, app)
# Assert
entries = hass.config_entries.async_entries('smartthings')
assert not entries
assert api.create_subscription.call_count == 3
data = hass.data[DOMAIN][CONF_INSTALLED_APPS][0]
assert data[CONF_REFRESH_TOKEN] == request.refresh_token
assert data[CONF_LOCATION_ID] == request.location_id
assert data[CONF_INSTALLED_APP_ID] == request.installed_app_id
async def test_smartapp_install_creates_flow(
@@ -68,12 +65,12 @@ async def test_smartapp_install_creates_flow(
# Arrange
setattr(hass.config_entries, '_entries', [config_entry])
api = smartthings_mock.return_value
api.create_subscription.return_value = mock_coro()
app = Mock()
app.app_id = config_entry.data['app_id']
request = Mock()
request.installed_app_id = str(uuid4())
request.auth_token = str(uuid4())
request.refresh_token = str(uuid4())
request.location_id = location.location_id
devices = [
device_factory('', [Capability.battery, 'ping']),
@@ -88,42 +85,42 @@ async def test_smartapp_install_creates_flow(
await hass.async_block_till_done()
entries = hass.config_entries.async_entries('smartthings')
assert len(entries) == 2
assert api.create_subscription.call_count == 3
assert entries[1].data['app_id'] == app.app_id
assert entries[1].data['installed_app_id'] == request.installed_app_id
assert entries[1].data['location_id'] == request.location_id
assert entries[1].data['access_token'] == \
config_entry.data['access_token']
assert entries[1].data['refresh_token'] == request.refresh_token
assert entries[1].data['client_secret'] == \
config_entry.data['client_secret']
assert entries[1].data['client_id'] == config_entry.data['client_id']
assert entries[1].title == location.name
async def test_smartapp_update_syncs_subs(
hass, smartthings_mock, config_entry, location, device_factory):
"""Test update synchronizes subscriptions."""
async def test_smartapp_update_saves_token(
hass, smartthings_mock, location, device_factory):
"""Test update saves token."""
# Arrange
setattr(hass.config_entries, '_entries', [config_entry])
entry = Mock()
entry.data = {
'installed_app_id': str(uuid4()),
'app_id': str(uuid4())
}
entry.domain = DOMAIN
setattr(hass.config_entries, '_entries', [entry])
app = Mock()
app.app_id = config_entry.data['app_id']
api = smartthings_mock.return_value
api.delete_subscriptions = Mock()
api.delete_subscriptions.return_value = mock_coro()
api.create_subscription.return_value = mock_coro()
app.app_id = entry.data['app_id']
request = Mock()
request.installed_app_id = str(uuid4())
request.installed_app_id = entry.data['installed_app_id']
request.auth_token = str(uuid4())
request.refresh_token = str(uuid4())
request.location_id = location.location_id
devices = [
device_factory('', [Capability.battery, 'ping']),
device_factory('', [Capability.switch, Capability.switch_level]),
device_factory('', [Capability.switch])
]
api.devices = Mock()
api.devices.return_value = mock_coro(return_value=devices)
# Act
await smartapp.smartapp_update(hass, request, None, app)
# Assert
assert api.create_subscription.call_count == 3
assert api.delete_subscriptions.call_count == 1
assert entry.data[CONF_REFRESH_TOKEN] == request.refresh_token
async def test_smartapp_uninstall(hass, config_entry):
@@ -152,3 +149,83 @@ async def test_smartapp_webhook(hass):
result = await smartapp.smartapp_webhook(hass, '', request)
assert result.body == b'{}'
async def test_smartapp_sync_subscriptions(
hass, smartthings_mock, device_factory, subscription_factory):
"""Test synchronization adds and removes."""
api = smartthings_mock.return_value
api.delete_subscription.side_effect = lambda loc_id, sub_id: mock_coro()
api.create_subscription.side_effect = lambda sub: mock_coro()
subscriptions = [
subscription_factory(Capability.thermostat),
subscription_factory(Capability.switch),
subscription_factory(Capability.switch_level)
]
api.subscriptions.return_value = mock_coro(return_value=subscriptions)
devices = [
device_factory('', [Capability.battery, 'ping']),
device_factory('', [Capability.switch, Capability.switch_level]),
device_factory('', [Capability.switch])
]
await smartapp.smartapp_sync_subscriptions(
hass, str(uuid4()), str(uuid4()), str(uuid4()), devices)
assert api.subscriptions.call_count == 1
assert api.delete_subscription.call_count == 1
assert api.create_subscription.call_count == 1
async def test_smartapp_sync_subscriptions_up_to_date(
hass, smartthings_mock, device_factory, subscription_factory):
"""Test synchronization does nothing when current."""
api = smartthings_mock.return_value
api.delete_subscription.side_effect = lambda loc_id, sub_id: mock_coro()
api.create_subscription.side_effect = lambda sub: mock_coro()
subscriptions = [
subscription_factory(Capability.battery),
subscription_factory(Capability.switch),
subscription_factory(Capability.switch_level)
]
api.subscriptions.return_value = mock_coro(return_value=subscriptions)
devices = [
device_factory('', [Capability.battery, 'ping']),
device_factory('', [Capability.switch, Capability.switch_level]),
device_factory('', [Capability.switch])
]
await smartapp.smartapp_sync_subscriptions(
hass, str(uuid4()), str(uuid4()), str(uuid4()), devices)
assert api.subscriptions.call_count == 1
assert api.delete_subscription.call_count == 0
assert api.create_subscription.call_count == 0
async def test_smartapp_sync_subscriptions_handles_exceptions(
hass, smartthings_mock, device_factory, subscription_factory):
"""Test synchronization does nothing when current."""
api = smartthings_mock.return_value
api.delete_subscription.side_effect = \
lambda loc_id, sub_id: mock_coro(exception=Exception)
api.create_subscription.side_effect = \
lambda sub: mock_coro(exception=Exception)
subscriptions = [
subscription_factory(Capability.battery),
subscription_factory(Capability.switch),
subscription_factory(Capability.switch_level)
]
api.subscriptions.return_value = mock_coro(return_value=subscriptions)
devices = [
device_factory('', [Capability.thermostat, 'ping']),
device_factory('', [Capability.switch, Capability.switch_level]),
device_factory('', [Capability.switch])
]
await smartapp.smartapp_sync_subscriptions(
hass, str(uuid4()), str(uuid4()), str(uuid4()), devices)
assert api.subscriptions.call_count == 1
assert api.delete_subscription.call_count == 1
assert api.create_subscription.call_count == 1
+9 -24
View File
@@ -6,28 +6,13 @@ real HTTP calls are not initiated during testing.
"""
from pysmartthings import Attribute, Capability
from homeassistant.components.smartthings import DeviceBroker, switch
from homeassistant.components.smartthings import switch
from homeassistant.components.smartthings.const import (
DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE)
from homeassistant.config_entries import (
CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry)
DOMAIN, SIGNAL_SMARTTHINGS_UPDATE)
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.helpers.dispatcher import async_dispatcher_send
async def _setup_platform(hass, *devices):
"""Set up the SmartThings switch platform and prerequisites."""
hass.config.components.add(DOMAIN)
broker = DeviceBroker(hass, devices, '')
config_entry = ConfigEntry("1", DOMAIN, "Test", {},
SOURCE_USER, CONN_CLASS_CLOUD_PUSH)
hass.data[DOMAIN] = {
DATA_BROKERS: {
config_entry.entry_id: broker
}
}
await hass.config_entries.async_forward_entry_setup(config_entry, 'switch')
await hass.async_block_till_done()
return config_entry
from .conftest import setup_platform
async def test_async_setup_platform():
@@ -43,7 +28,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, device)
await setup_platform(hass, SWITCH_DOMAIN, device)
# Assert
entry = entity_registry.async_get('switch.switch_1')
assert entry
@@ -62,7 +47,7 @@ async def test_turn_off(hass, device_factory):
# Arrange
device = device_factory('Switch_1', [Capability.switch],
{Attribute.switch: 'on'})
await _setup_platform(hass, device)
await setup_platform(hass, SWITCH_DOMAIN, device)
# Act
await hass.services.async_call(
'switch', 'turn_off', {'entity_id': 'switch.switch_1'},
@@ -78,7 +63,7 @@ async def test_turn_on(hass, device_factory):
# Arrange
device = device_factory('Switch_1', [Capability.switch],
{Attribute.switch: 'off'})
await _setup_platform(hass, device)
await setup_platform(hass, SWITCH_DOMAIN, device)
# Act
await hass.services.async_call(
'switch', 'turn_on', {'entity_id': 'switch.switch_1'},
@@ -94,7 +79,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, device)
await setup_platform(hass, SWITCH_DOMAIN, device)
await device.switch_on(True)
# Act
async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE,
@@ -111,7 +96,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, device)
config_entry = await setup_platform(hass, SWITCH_DOMAIN, device)
# Act
await hass.config_entries.async_forward_entry_unload(
config_entry, 'switch')