Add SmartThings Sensor platform (#20848)

* Add Sensor platform and update pysmartthings 0.6.0

* Add tests for Sensor platform

* Redesigned capability subscription process

* Removed redundant Entity inheritance

* Updated per review feedback.
This commit is contained in:
Andrew Sayre 2019-02-07 22:51:17 -06:00 committed by Aaron Bach
parent c7df4cf092
commit 706810bbce
11 changed files with 457 additions and 65 deletions

View file

@ -23,7 +23,7 @@ from .const import (
from .smartapp import (
setup_smartapp, setup_smartapp_endpoint, validate_installed_app)
REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.5.0']
REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.6.0']
DEPENDENCIES = ['webhook']
_LOGGER = logging.getLogger(__name__)

View file

@ -22,25 +22,9 @@ SUPPORTED_PLATFORMS = [
'binary_sensor',
'fan',
'light',
'sensor',
'switch'
]
SUPPORTED_CAPABILITIES = [
'accelerationSensor',
'button',
'colorControl',
'colorTemperature',
'contactSensor',
'fanSpeed',
'filterStatus',
'motionSensor',
'presenceSensor',
'soundSensor',
'switch',
'switchLevel',
'tamperAlert',
'valve',
'waterSensor'
]
VAL_UID = "^(?:([0-9a-fA-F]{32})|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]" \
"{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}))$"
VAL_UID_MATCHER = re.compile(VAL_UID)

View file

@ -0,0 +1,218 @@
"""
Support for sensors through the SmartThings cloud API.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/smartthings.sensor/
"""
from collections import namedtuple
from homeassistant.const import (
DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, MASS_KILOGRAMS,
TEMP_CELSIUS, TEMP_FAHRENHEIT)
from . import SmartThingsEntity
from .const import DATA_BROKERS, DOMAIN
DEPENDENCIES = ['smartthings']
Map = namedtuple("map", "attribute name default_unit device_class")
CAPABILITY_TO_SENSORS = {
'activityLightingMode': [
Map('lightingMode', "Activity Lighting Mode", None, None)],
'airConditionerMode': [
Map('airConditionerMode', "Air Conditioner Mode", None, None)],
'airQualitySensor': [
Map('airQuality', "Air Quality", 'CAQI', None)],
'alarm': [
Map('alarm', "Alarm", None, None)],
'audioVolume': [
Map('volume', "Volume", "%", None)],
'battery': [
Map('battery', "Battery", "%", DEVICE_CLASS_BATTERY)],
'bodyMassIndexMeasurement': [
Map('bmiMeasurement', "Body Mass Index", "kg/m^2", None)],
'bodyWeightMeasurement': [
Map('bodyWeightMeasurement', "Body Weight", MASS_KILOGRAMS, None)],
'carbonDioxideMeasurement': [
Map('carbonDioxide', "Carbon Dioxide Measurement", "ppm", None)],
'carbonMonoxideDetector': [
Map('carbonMonoxide', "Carbon Monoxide Detector", None, None)],
'carbonMonoxideMeasurement': [
Map('carbonMonoxideLevel', "Carbon Monoxide Measurement", "ppm",
None)],
'dishwasherOperatingState': [
Map('machineState', "Dishwasher Machine State", None, None),
Map('dishwasherJobState', "Dishwasher Job State", None, None),
Map('completionTime', "Dishwasher Completion Time", None,
DEVICE_CLASS_TIMESTAMP)],
'doorControl': [
Map('door', "Door", None, None)],
'dryerMode': [
Map('dryerMode', "Dryer Mode", None, None)],
'dryerOperatingState': [
Map('machineState', "Dryer Machine State", None, None),
Map('dryerJobState', "Dryer Job State", None, None),
Map('completionTime', "Dryer Completion Time", None,
DEVICE_CLASS_TIMESTAMP)],
'dustSensor': [
Map('fineDustLevel', "Fine Dust Level", None, None),
Map('dustLevel', "Dust Level", None, None)],
'energyMeter': [
Map('energy', "Energy Meter", 'kWh', None)],
'equivalentCarbonDioxideMeasurement': [
Map('equivalentCarbonDioxideMeasurement',
'Equivalent Carbon Dioxide Measurement', 'ppm', None)],
'formaldehydeMeasurement': [
Map('formaldehydeLevel', 'Formaldehyde Measurement', 'ppm', None)],
'garageDoorControl': [
Map('door', 'Garage Door', None, None)],
'illuminanceMeasurement': [
Map('illuminance', "Illuminance", 'lux', DEVICE_CLASS_ILLUMINANCE)],
'infraredLevel': [
Map('infraredLevel', "Infrared Level", '%', None)],
'lock': [
Map('lock', "Lock", None, None)],
'mediaInputSource': [
Map('inputSource', "Media Input Source", None, None)],
'mediaPlaybackRepeat': [
Map('playbackRepeatMode', "Media Playback Repeat", None, None)],
'mediaPlaybackShuffle': [
Map('playbackShuffle', "Media Playback Shuffle", None, None)],
'mediaPlayback': [
Map('playbackStatus', "Media Playback Status", None, None)],
'odorSensor': [
Map('odorLevel', "Odor Sensor", None, None)],
'ovenMode': [
Map('ovenMode', "Oven Mode", None, None)],
'ovenOperatingState': [
Map('machineState', "Oven Machine State", None, None),
Map('ovenJobState', "Oven Job State", None, None),
Map('completionTime', "Oven Completion Time", None, None)],
'ovenSetpoint': [
Map('ovenSetpoint', "Oven Set Point", None, None)],
'powerMeter': [
Map('power', "Power Meter", 'W', None)],
'powerSource': [
Map('powerSource', "Power Source", None, None)],
'refrigerationSetpoint': [
Map('refrigerationSetpoint', "Refrigeration Setpoint", TEMP_CELSIUS,
DEVICE_CLASS_TEMPERATURE)],
'relativeHumidityMeasurement': [
Map('humidity', "Relative Humidity Measurement", '%',
DEVICE_CLASS_HUMIDITY)],
'robotCleanerCleaningMode': [
Map('robotCleanerCleaningMode', "Robot Cleaner Cleaning Mode",
None, None)],
'robotCleanerMovement': [
Map('robotCleanerMovement', "Robot Cleaner Movement", None, None)],
'robotCleanerTurboMode': [
Map('robotCleanerTurboMode', "Robot Cleaner Turbo Mode", None, None)],
'signalStrength': [
Map('lqi', "LQI Signal Strength", None, None),
Map('rssi', "RSSI Signal Strength", None, None)],
'smokeDetector': [
Map('smoke', "Smoke Detector", None, None)],
'temperatureMeasurement': [
Map('temperature', "Temperature Measurement", TEMP_CELSIUS,
DEVICE_CLASS_TEMPERATURE)],
'thermostatCoolingSetpoint': [
Map('coolingSetpoint', "Thermostat Cooling Setpoint", TEMP_CELSIUS,
DEVICE_CLASS_TEMPERATURE)],
'thermostatFanMode': [
Map('thermostatFanMode', "Thermostat Fan Mode", None, None)],
'thermostatHeatingSetpoint': [
Map('heatingSetpoint', "Thermostat Heating Setpoint", TEMP_CELSIUS,
DEVICE_CLASS_TEMPERATURE)],
'thermostatMode': [
Map('thermostatMode', "Thermostat Mode", None, None)],
'thermostatOperatingState': [
Map('thermostatOperatingState', "Thermostat Operating State",
None, None)],
'thermostatSetpoint': [
Map('thermostatSetpoint', "Thermostat Setpoint", TEMP_CELSIUS,
DEVICE_CLASS_TEMPERATURE)],
'tvChannel': [
Map('tvChannel', "Tv Channel", None, None)],
'tvocMeasurement': [
Map('tvocLevel', "Tvoc Measurement", 'ppm', None)],
'ultravioletIndex': [
Map('ultravioletIndex', "Ultraviolet Index", None, None)],
'voltageMeasurement': [
Map('voltage', "Voltage Measurement", 'V', None)],
'washerMode': [
Map('washerMode', "Washer Mode", None, None)],
'washerOperatingState': [
Map('machineState', "Washer Machine State", None, None),
Map('washerJobState', "Washer Job State", None, None),
Map('completionTime', "Washer Completion Time", None,
DEVICE_CLASS_TIMESTAMP)],
'windowShade': [
Map('windowShade', 'Window Shade', None, None)]
}
UNITS = {
'C': TEMP_CELSIUS,
'F': TEMP_FAHRENHEIT
}
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 binary sensors for a config entry."""
broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id]
sensors = []
for device in broker.devices.values():
for capability, maps in CAPABILITY_TO_SENSORS.items():
if capability in device.capabilities:
sensors.extend([
SmartThingsSensor(
device, m.attribute, m.name, m.default_unit,
m.device_class)
for m in maps])
async_add_entities(sensors)
class SmartThingsSensor(SmartThingsEntity):
"""Define a SmartThings Binary Sensor."""
def __init__(self, device, attribute: str, name: str,
default_unit: str, device_class: str):
"""Init the class."""
super().__init__(device)
self._attribute = attribute
self._name = name
self._device_class = device_class
self._default_unit = default_unit
@property
def name(self) -> str:
"""Return the name of the binary sensor."""
return '{} {}'.format(self._device.label, self._name)
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return '{}.{}'.format(self._device.device_id, self._attribute)
@property
def state(self):
"""Return the state of the sensor."""
return self._device.status.attributes[self._attribute].value
@property
def device_class(self):
"""Return the device class of the sensor."""
return self._device_class
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
unit = self._device.status.attributes[self._attribute].unit
return UNITS.get(unit, unit) if unit else self._default_unit

View file

@ -22,8 +22,7 @@ from homeassistant.helpers.typing import HomeAssistantType
from .const import (
APP_NAME_PREFIX, APP_OAUTH_SCOPES, CONF_APP_ID, CONF_INSTALLED_APP_ID,
CONF_INSTANCE_ID, CONF_LOCATION_ID, DATA_BROKERS, DATA_MANAGER, DOMAIN,
SETTINGS_INSTANCE_ID, SIGNAL_SMARTAPP_PREFIX, STORAGE_KEY, STORAGE_VERSION,
SUPPORTED_CAPABILITIES)
SETTINGS_INSTANCE_ID, SIGNAL_SMARTAPP_PREFIX, STORAGE_KEY, STORAGE_VERSION)
_LOGGER = logging.getLogger(__name__)
@ -176,6 +175,7 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType):
webhook.async_generate_path(config[CONF_WEBHOOK_ID]),
dispatcher=dispatcher)
manager.connect_install(functools.partial(smartapp_install, hass))
manager.connect_update(functools.partial(smartapp_update, hass))
manager.connect_uninstall(functools.partial(smartapp_uninstall, hass))
webhook.async_register(hass, DOMAIN, 'SmartApp',
@ -189,6 +189,45 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType):
}
async def smartapp_sync_subscriptions(
hass: HomeAssistantType, auth_token: str, location_id: str,
installed_app_id: str, *, skip_delete=False):
"""Synchronize subscriptions of an installed up."""
from pysmartthings import (
CAPABILITIES, SmartThings, SourceType, Subscription)
api = SmartThings(async_get_clientsession(hass), auth_token)
devices = await api.devices(location_ids=[location_id])
# Build set of capabilities and prune unsupported ones
capabilities = set()
for device in devices:
capabilities.update(device.capabilities)
capabilities.intersection_update(CAPABILITIES)
# Remove all (except for installs)
if not skip_delete:
await api.delete_subscriptions(installed_app_id)
# Create for each capability
async def create_subscription(target):
sub = Subscription()
sub.installed_app_id = installed_app_id
sub.location_id = location_id
sub.source_type = SourceType.CAPABILITY
sub.capability = target
try:
await api.create_subscription(sub)
_LOGGER.debug("Created subscription for '%s' under app '%s'",
target, installed_app_id)
except Exception: # pylint:disable=broad-except
_LOGGER.exception("Failed to create subscription for '%s' under "
"app '%s'", target, installed_app_id)
tasks = [create_subscription(c) for c in capabilities]
await asyncio.gather(*tasks)
async def smartapp_install(hass: HomeAssistantType, req, resp, app):
"""
Handle when a SmartApp is installed by the user into a location.
@ -199,30 +238,9 @@ async def smartapp_install(hass: HomeAssistantType, req, resp, app):
representing the installation if this is not the first installation under
the account.
"""
from pysmartthings import SmartThings, Subscription, SourceType
# This access token is a temporary 'SmartApp token' that expires in 5 min
# and is used to create subscriptions only.
api = SmartThings(async_get_clientsession(hass), req.auth_token)
async def create_subscription(target):
sub = Subscription()
sub.installed_app_id = req.installed_app_id
sub.location_id = req.location_id
sub.source_type = SourceType.CAPABILITY
sub.capability = target
try:
await api.create_subscription(sub)
_LOGGER.debug("Created subscription for '%s' under app '%s'",
target, req.installed_app_id)
except Exception: # pylint:disable=broad-except
_LOGGER.exception("Failed to create subscription for '%s' under "
"app '%s'", target, req.installed_app_id)
tasks = [create_subscription(c) for c in SUPPORTED_CAPABILITIES]
await asyncio.gather(*tasks)
_LOGGER.debug("SmartApp '%s' under parent app '%s' was installed",
req.installed_app_id, app.app_id)
await smartapp_sync_subscriptions(
hass, req.auth_token, req.location_id, req.installed_app_id,
skip_delete=True)
# The permanent access token is copied from another config flow with the
# same parent app_id. If one is not found, that means the user is within
@ -244,6 +262,19 @@ async def smartapp_install(hass: HomeAssistantType, req, resp, app):
})
async def smartapp_update(hass: HomeAssistantType, req, resp, app):
"""
Handle when a SmartApp is updated (reconfigured) by the user.
Synchronize subscriptions to ensure we're up-to-date.
"""
await smartapp_sync_subscriptions(
hass, req.auth_token, req.location_id, req.installed_app_id)
_LOGGER.debug("SmartApp '%s' under parent app '%s' was updated",
req.installed_app_id, app.app_id)
async def smartapp_uninstall(hass: HomeAssistantType, req, resp, app):
"""
Handle when a SmartApp is removed from a location by the user.

View file

@ -1231,7 +1231,7 @@ pysma==0.3.1
pysmartapp==0.3.0
# homeassistant.components.smartthings
pysmartthings==0.5.0
pysmartthings==0.6.0
# homeassistant.components.device_tracker.snmp
# homeassistant.components.sensor.snmp

View file

@ -217,7 +217,7 @@ pyqwikswitch==0.8
pysmartapp==0.3.0
# homeassistant.components.smartthings
pysmartthings==0.5.0
pysmartthings==0.6.0
# homeassistant.components.sonos
pysonos==0.0.6

View file

@ -10,9 +10,10 @@ from pysmartthings.api import Api
import pytest
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, DOMAIN, SETTINGS_INSTANCE_ID, STORAGE_KEY,
CONF_LOCATION_ID, DATA_BROKERS, DOMAIN, SETTINGS_INSTANCE_ID, STORAGE_KEY,
STORAGE_VERSION)
from homeassistant.config_entries import (
CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry)
@ -22,6 +23,23 @@ from homeassistant.setup import async_setup_component
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", {},
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, platform)
await hass.async_block_till_done()
return config_entry
@pytest.fixture(autouse=True)
async def setup_component(hass, config_file, hass_storage):
"""Load the SmartThing component."""

View file

@ -4,12 +4,12 @@ Test for the SmartThings binary_sensor platform.
The only mocking required is of the underlying SmartThings API object so
real HTTP calls are not initiated during testing.
"""
from pysmartthings import Attribute, Capability
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.smartthings.const import (
DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_CAPABILITIES)
DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE)
from homeassistant.config_entries import (
CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry)
from homeassistant.const import ATTR_FRIENDLY_NAME
@ -35,14 +35,16 @@ async def _setup_platform(hass, *devices):
async def test_mapping_integrity():
"""Test ensures the map dicts have proper integrity."""
# Ensure every CAPABILITY_TO_ATTRIB key is in SUPPORTED_CAPABILITIES
# Ensure every CAPABILITY_TO_ATTRIB key is in CAPABILITIES
# Ensure every CAPABILITY_TO_ATTRIB value is in ATTRIB_TO_CLASS keys
for capability, attrib in binary_sensor.CAPABILITY_TO_ATTRIB.items():
assert capability in SUPPORTED_CAPABILITIES, capability
assert capability in CAPABILITIES, capability
assert attrib in ATTRIBUTES, attrib
assert attrib in binary_sensor.ATTRIB_TO_CLASS.keys(), attrib
# Ensure every ATTRIB_TO_CLASS value is in DEVICE_CLASSES
for device_class in binary_sensor.ATTRIB_TO_CLASS.values():
assert device_class in DEVICE_CLASSES
for attrib, device_class in binary_sensor.ATTRIB_TO_CLASS.items():
assert attrib in ATTRIBUTES, attrib
assert device_class in DEVICE_CLASSES, device_class
async def test_async_setup_platform():

View file

@ -162,7 +162,7 @@ async def test_event_handler_dispatches_updated_devices(
assert called
for device in devices:
assert device.status.attributes['Updated'] == 'Value'
assert device.status.values['Updated'] == 'Value'
async def test_event_handler_ignores_other_installed_app(

View file

@ -0,0 +1,97 @@
"""
Test for the SmartThings sensors platform.
The only mocking required is of the underlying SmartThings API object so
real HTTP calls are not initiated during testing.
"""
from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability
from homeassistant.components.sensor import (
DEVICE_CLASSES, DOMAIN as SENSOR_DOMAIN)
from homeassistant.components.smartthings import sensor
from homeassistant.components.smartthings.const import (
DOMAIN, SIGNAL_SMARTTHINGS_UPDATE)
from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .conftest import setup_platform
async def test_mapping_integrity():
"""Test ensures the map dicts have proper integrity."""
for capability, maps in sensor.CAPABILITY_TO_SENSORS.items():
assert capability in CAPABILITIES, capability
for sensor_map in maps:
assert sensor_map.attribute in ATTRIBUTES, sensor_map.attribute
if sensor_map.device_class:
assert sensor_map.device_class in DEVICE_CLASSES, \
sensor_map.device_class
async def test_async_setup_platform():
"""Test setup platform does nothing (it uses config entries)."""
await sensor.async_setup_platform(None, None, None)
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)
state = hass.states.get('sensor.sensor_1_battery')
assert state.state == '100'
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == '%'
assert state.attributes[ATTR_FRIENDLY_NAME] ==\
device.label + " Battery"
async def test_entity_and_device_attributes(hass, device_factory):
"""Test the attributes of the entity are correct."""
# Arrange
device = device_factory('Sensor 1', [Capability.battery],
{Attribute.battery: 100})
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)
# Assert
entry = entity_registry.async_get('sensor.sensor_1_battery')
assert entry
assert entry.unique_id == device.device_id + '.' + Attribute.battery
entry = device_registry.async_get_device(
{(DOMAIN, device.device_id)}, [])
assert entry
assert entry.name == device.label
assert entry.model == device.device_type_name
assert entry.manufacturer == 'Unavailable'
async def test_update_from_signal(hass, device_factory):
"""Test the binary_sensor updates when receiving a signal."""
# Arrange
device = device_factory('Sensor 1', [Capability.battery],
{Attribute.battery: 100})
await setup_platform(hass, SENSOR_DOMAIN, device)
device.status.apply_attribute_update(
'main', Capability.battery, Attribute.battery, 75)
# Act
async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE,
[device.device_id])
# Assert
await hass.async_block_till_done()
state = hass.states.get('sensor.sensor_1_battery')
assert state is not None
assert state.state == '75'
async def test_unload_config_entry(hass, device_factory):
"""Test the binary_sensor is removed when the config entry is unloaded."""
# Arrange
device = device_factory('Sensor 1', [Capability.battery],
{Attribute.battery: 100})
config_entry = await setup_platform(hass, SENSOR_DOMAIN, device)
# Act
await hass.config_entries.async_forward_entry_unload(
config_entry, 'sensor')
# Assert
assert not hass.states.get('sensor.sensor_1_battery')

View file

@ -2,11 +2,10 @@
from unittest.mock import Mock, patch
from uuid import uuid4
from pysmartthings import AppEntity
from pysmartthings import AppEntity, Capability
from homeassistant.components.smartthings import smartapp
from homeassistant.components.smartthings.const import (
DATA_MANAGER, DOMAIN, SUPPORTED_CAPABILITIES)
from homeassistant.components.smartthings.const import DATA_MANAGER, DOMAIN
from tests.common import mock_coro
@ -36,8 +35,10 @@ async def test_update_app_updated_needed(hass, app):
assert mock_app.classifications == app.classifications
async def test_smartapp_install_abort_if_no_other(hass, smartthings_mock):
async def test_smartapp_install_abort_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()
@ -46,17 +47,23 @@ async def test_smartapp_install_abort_if_no_other(hass, smartthings_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)
# 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 == \
len(SUPPORTED_CAPABILITIES)
assert api.create_subscription.call_count == 3
async def test_smartapp_install_creates_flow(
hass, smartthings_mock, config_entry, location):
hass, smartthings_mock, config_entry, location, device_factory):
"""Test installation creates flow."""
# Arrange
setattr(hass.config_entries, '_entries', [config_entry])
@ -68,14 +75,20 @@ async def test_smartapp_install_creates_flow(
request.installed_app_id = str(uuid4())
request.auth_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_install(hass, request, None, app)
# Assert
await hass.async_block_till_done()
entries = hass.config_entries.async_entries('smartthings')
assert len(entries) == 2
assert api.create_subscription.call_count == \
len(SUPPORTED_CAPABILITIES)
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
@ -84,6 +97,35 @@ async def test_smartapp_install_creates_flow(
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."""
# Arrange
setattr(hass.config_entries, '_entries', [config_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()
request = Mock()
request.installed_app_id = str(uuid4())
request.auth_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
async def test_smartapp_uninstall(hass, config_entry):
"""Test the config entry is unloaded when the app is uninstalled."""
setattr(hass.config_entries, '_entries', [config_entry])