SmartThings Component Enhancements/Fixes (#21085)

* Improve component setup error logging/notification

* Prevent capabilities from being represented my multiple platforms

* Improved logging of received updates

* Updates based on review feedback
This commit is contained in:
Andrew Sayre 2019-02-15 10:40:54 -06:00 committed by Martin Hjelmare
parent 7d0f847f83
commit 93f84a5cd1
20 changed files with 196 additions and 151 deletions

View File

@ -7,7 +7,8 @@
"token_already_setup": "The token has already been setup.", "token_already_setup": "The token has already been setup.",
"token_forbidden": "The token does not have the required OAuth scopes.", "token_forbidden": "The token does not have the required OAuth scopes.",
"token_invalid_format": "The token must be in the UID/GUID format", "token_invalid_format": "The token must be in the UID/GUID format",
"token_unauthorized": "The token is invalid or no longer authorized." "token_unauthorized": "The token is invalid or no longer authorized.",
"webhook_error": "SmartThings could not validate the endpoint configured in `base_url`. Please review the [component requirements]({component_url})."
}, },
"step": { "step": {
"user": { "user": {

View File

@ -1,5 +1,6 @@
"""Support for SmartThings Cloud.""" """Support for SmartThings Cloud."""
import asyncio import asyncio
import importlib
import logging import logging
from typing import Iterable from typing import Iterable
@ -22,7 +23,7 @@ from .const import (
from .smartapp import ( from .smartapp import (
setup_smartapp, setup_smartapp_endpoint, validate_installed_app) setup_smartapp, setup_smartapp_endpoint, validate_installed_app)
REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.6.1'] REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.6.2']
DEPENDENCIES = ['webhook'] DEPENDENCIES = ['webhook']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -132,9 +133,41 @@ class DeviceBroker:
"""Create a new instance of the DeviceBroker.""" """Create a new instance of the DeviceBroker."""
self._hass = hass self._hass = hass
self._installed_app_id = installed_app_id self._installed_app_id = installed_app_id
self.assignments = self._assign_capabilities(devices)
self.devices = {device.device_id: device for device in devices} self.devices = {device.device_id: device for device in devices}
self.event_handler_disconnect = None self.event_handler_disconnect = None
def _assign_capabilities(self, devices: Iterable):
"""Assign platforms to capabilities."""
assignments = {}
for device in devices:
capabilities = device.capabilities.copy()
slots = {}
for platform_name in SUPPORTED_PLATFORMS:
platform = importlib.import_module(
'.' + platform_name, self.__module__)
assigned = platform.get_capabilities(capabilities)
if not assigned:
continue
# Draw-down capabilities and set slot assignment
for capability in assigned:
if capability not in capabilities:
continue
capabilities.remove(capability)
slots[capability] = platform_name
assignments[device.device_id] = slots
return assignments
def get_assigned(self, device_id: str, platform: str):
"""Get the capabilities assigned to the platform."""
slots = self.assignments.get(device_id, {})
return [key for key, value in slots.items() if value == platform]
def any_assigned(self, device_id: str, platform: str):
"""Return True if the platform has any assigned capabilities."""
slots = self.assignments.get(device_id, {})
return any(value for value in slots.values() if value == platform)
async def event_handler(self, req, resp, app): async def event_handler(self, req, resp, app):
"""Broker for incoming events.""" """Broker for incoming events."""
from pysmartapp.event import EVENT_TYPE_DEVICE from pysmartapp.event import EVENT_TYPE_DEVICE
@ -167,10 +200,18 @@ class DeviceBroker:
} }
self._hass.bus.async_fire(EVENT_BUTTON, data) self._hass.bus.async_fire(EVENT_BUTTON, data)
_LOGGER.debug("Fired button event: %s", data) _LOGGER.debug("Fired button event: %s", data)
else:
data = {
'location_id': evt.location_id,
'device_id': evt.device_id,
'component_id': evt.component_id,
'capability': evt.capability,
'attribute': evt.attribute,
'value': evt.value,
}
_LOGGER.debug("Push update received: %s", data)
updated_devices.add(device.device_id) updated_devices.add(device.device_id)
_LOGGER.debug("Update received with %s events and updated %s devices",
len(req.events), len(updated_devices))
async_dispatcher_send(self._hass, SIGNAL_SMARTTHINGS_UPDATE, async_dispatcher_send(self._hass, SIGNAL_SMARTTHINGS_UPDATE,
updated_devices) updated_devices)

View File

@ -1,4 +1,6 @@
"""Support for binary sensors through the SmartThings cloud API.""" """Support for binary sensors through the SmartThings cloud API."""
from typing import Optional, Sequence
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from . import SmartThingsEntity from . import SmartThingsEntity
@ -41,12 +43,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id]
sensors = [] sensors = []
for device in broker.devices.values(): for device in broker.devices.values():
for capability, attrib in CAPABILITY_TO_ATTRIB.items(): for capability in broker.get_assigned(
if capability in device.capabilities: device.device_id, 'binary_sensor'):
sensors.append(SmartThingsBinarySensor(device, attrib)) attrib = CAPABILITY_TO_ATTRIB[capability]
sensors.append(SmartThingsBinarySensor(device, attrib))
async_add_entities(sensors) async_add_entities(sensors)
def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
"""Return all capabilities supported if minimum required are present."""
return [capability for capability in CAPABILITY_TO_ATTRIB
if capability in capabilities]
class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorDevice): class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorDevice):
"""Define a SmartThings Binary Sensor.""" """Define a SmartThings Binary Sensor."""

View File

@ -1,11 +1,12 @@
"""Support for climate devices through the SmartThings cloud API.""" """Support for climate devices through the SmartThings cloud API."""
import asyncio import asyncio
from typing import Optional, Sequence
from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, SUPPORT_FAN_MODE,
SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.const import ( from homeassistant.const import (
ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
@ -49,30 +50,37 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id]
async_add_entities( async_add_entities(
[SmartThingsThermostat(device) for device in broker.devices.values() [SmartThingsThermostat(device) for device in broker.devices.values()
if is_climate(device)]) if broker.any_assigned(device.device_id, 'climate')])
def is_climate(device): def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
"""Determine if the device should be represented as a climate entity.""" """Return all capabilities supported if minimum required are present."""
from pysmartthings import Capability from pysmartthings import Capability
supported = [
Capability.thermostat,
Capability.temperature_measurement,
Capability.thermostat_cooling_setpoint,
Capability.thermostat_heating_setpoint,
Capability.thermostat_mode,
Capability.relative_humidity_measurement,
Capability.thermostat_operating_state,
Capability.thermostat_fan_mode
]
# Can have this legacy/deprecated capability # Can have this legacy/deprecated capability
if Capability.thermostat in device.capabilities: if Capability.thermostat in capabilities:
return True return supported
# Or must have all of these # Or must have all of these
climate_capabilities = [ climate_capabilities = [
Capability.temperature_measurement, Capability.temperature_measurement,
Capability.thermostat_cooling_setpoint, Capability.thermostat_cooling_setpoint,
Capability.thermostat_heating_setpoint, Capability.thermostat_heating_setpoint,
Capability.thermostat_mode] Capability.thermostat_mode]
if all(capability in device.capabilities if all(capability in capabilities
for capability in climate_capabilities): for capability in climate_capabilities):
return True return supported
# Optional capabilities:
# relative_humidity_measurement -> state attribs return None
# thermostat_operating_state -> state attribs
# thermostat_fan_mode -> SUPPORT_FAN_MODE
return False
class SmartThingsThermostat(SmartThingsEntity, ClimateDevice): class SmartThingsThermostat(SmartThingsEntity, ClimateDevice):

View File

@ -1,7 +1,7 @@
"""Config flow to configure SmartThings.""" """Config flow to configure SmartThings."""
import logging import logging
from aiohttp.client_exceptions import ClientResponseError from aiohttp import ClientResponseError
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@ -50,7 +50,7 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow):
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Get access token and validate it.""" """Get access token and validate it."""
from pysmartthings import SmartThings from pysmartthings import APIResponseError, SmartThings
errors = {} errors = {}
if not self.hass.config.api.base_url.lower().startswith('https://'): if not self.hass.config.api.base_url.lower().startswith('https://'):
@ -87,6 +87,14 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow):
app = await create_app(self.hass, self.api) app = await create_app(self.hass, self.api)
setup_smartapp(self.hass, app) setup_smartapp(self.hass, app)
self.app_id = app.app_id self.app_id = app.app_id
except APIResponseError as ex:
if ex.is_target_error():
errors['base'] = 'webhook_error'
else:
errors['base'] = "app_setup_error"
_LOGGER.exception("API error setting up the SmartApp: %s",
ex.raw_error_response)
return self._show_step_user(errors)
except ClientResponseError as ex: except ClientResponseError as ex:
if ex.status == 401: if ex.status == 401:
errors[CONF_ACCESS_TOKEN] = "token_unauthorized" errors[CONF_ACCESS_TOKEN] = "token_unauthorized"
@ -94,6 +102,7 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow):
errors[CONF_ACCESS_TOKEN] = "token_forbidden" errors[CONF_ACCESS_TOKEN] = "token_forbidden"
else: else:
errors['base'] = "app_setup_error" errors['base'] = "app_setup_error"
_LOGGER.exception("Unexpected error setting up the SmartApp")
return self._show_step_user(errors) return self._show_step_user(errors)
except Exception: # pylint:disable=broad-except except Exception: # pylint:disable=broad-except
errors['base'] = "app_setup_error" errors['base'] = "app_setup_error"

View File

@ -18,14 +18,16 @@ SIGNAL_SMARTAPP_PREFIX = 'smartthings_smartap_'
SETTINGS_INSTANCE_ID = "hassInstanceId" SETTINGS_INSTANCE_ID = "hassInstanceId"
STORAGE_KEY = DOMAIN STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1 STORAGE_VERSION = 1
# Ordered 'specific to least-specific platform' in order for capabilities
# to be drawn-down and represented by the appropriate platform.
SUPPORTED_PLATFORMS = [ SUPPORTED_PLATFORMS = [
'binary_sensor',
'climate', 'climate',
'fan', 'fan',
'light', 'light',
'lock', 'lock',
'sensor', 'switch',
'switch' 'binary_sensor',
'sensor'
] ]
VAL_UID = "^(?:([0-9a-fA-F]{32})|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]" \ 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}))$" "{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}))$"

View File

@ -1,4 +1,6 @@
"""Support for fans through the SmartThings cloud API.""" """Support for fans through the SmartThings cloud API."""
from typing import Optional, Sequence
from homeassistant.components.fan import ( from homeassistant.components.fan import (
SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED,
FanEntity) FanEntity)
@ -29,15 +31,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id]
async_add_entities( async_add_entities(
[SmartThingsFan(device) for device in broker.devices.values() [SmartThingsFan(device) for device in broker.devices.values()
if is_fan(device)]) if broker.any_assigned(device.device_id, 'fan')])
def is_fan(device): def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
"""Determine if the device should be represented as a fan.""" """Return all capabilities supported if minimum required are present."""
from pysmartthings import Capability from pysmartthings import Capability
supported = [Capability.switch, Capability.fan_speed]
# Must have switch and fan_speed # Must have switch and fan_speed
return all(capability in device.capabilities if all(capability in capabilities for capability in supported):
for capability in [Capability.switch, Capability.fan_speed]) return supported
class SmartThingsFan(SmartThingsEntity, FanEntity): class SmartThingsFan(SmartThingsEntity, FanEntity):

View File

@ -1,5 +1,6 @@
"""Support for lights through the SmartThings cloud API.""" """Support for lights through the SmartThings cloud API."""
import asyncio import asyncio
from typing import Optional, Sequence
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION,
@ -24,29 +25,32 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id]
async_add_entities( async_add_entities(
[SmartThingsLight(device) for device in broker.devices.values() [SmartThingsLight(device) for device in broker.devices.values()
if is_light(device)], True) if broker.any_assigned(device.device_id, 'light')], True)
def is_light(device): def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
"""Determine if the device should be represented as a light.""" """Return all capabilities supported if minimum required are present."""
from pysmartthings import Capability from pysmartthings import Capability
supported = [
Capability.switch,
Capability.switch_level,
Capability.color_control,
Capability.color_temperature,
]
# Must be able to be turned on/off. # Must be able to be turned on/off.
if Capability.switch not in device.capabilities: if Capability.switch not in capabilities:
return False return None
# Not a fan (which might also have switch_level)
if Capability.fan_speed in device.capabilities:
return False
# Must have one of these # Must have one of these
light_capabilities = [ light_capabilities = [
Capability.color_control, Capability.color_control,
Capability.color_temperature, Capability.color_temperature,
Capability.switch_level Capability.switch_level
] ]
if any(capability in device.capabilities if any(capability in capabilities
for capability in light_capabilities): for capability in light_capabilities):
return True return supported
return False return None
def convert_scale(value, value_scale, target_scale, round_digits=4): def convert_scale(value, value_scale, target_scale, round_digits=4):

View File

@ -1,4 +1,6 @@
"""Support for locks through the SmartThings cloud API.""" """Support for locks through the SmartThings cloud API."""
from typing import Optional, Sequence
from homeassistant.components.lock import LockDevice from homeassistant.components.lock import LockDevice
from . import SmartThingsEntity from . import SmartThingsEntity
@ -25,13 +27,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id]
async_add_entities( async_add_entities(
[SmartThingsLock(device) for device in broker.devices.values() [SmartThingsLock(device) for device in broker.devices.values()
if is_lock(device)]) if broker.any_assigned(device.device_id, 'lock')])
def is_lock(device): def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
"""Determine if the device supports the lock capability.""" """Return all capabilities supported if minimum required are present."""
from pysmartthings import Capability from pysmartthings import Capability
return Capability.lock in device.capabilities
if Capability.lock in capabilities:
return [Capability.lock]
return None
class SmartThingsLock(SmartThingsEntity, LockDevice): class SmartThingsLock(SmartThingsEntity, LockDevice):

View File

@ -1,5 +1,6 @@
"""Support for sensors through the SmartThings cloud API.""" """Support for sensors through the SmartThings cloud API."""
from collections import namedtuple from collections import namedtuple
from typing import Optional, Sequence
from homeassistant.const import ( from homeassistant.const import (
DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE,
@ -164,16 +165,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id]
sensors = [] sensors = []
for device in broker.devices.values(): for device in broker.devices.values():
for capability, maps in CAPABILITY_TO_SENSORS.items(): for capability in broker.get_assigned(device.device_id, 'sensor'):
if capability in device.capabilities: maps = CAPABILITY_TO_SENSORS[capability]
sensors.extend([ sensors.extend([
SmartThingsSensor( SmartThingsSensor(
device, m.attribute, m.name, m.default_unit, device, m.attribute, m.name, m.default_unit,
m.device_class) m.device_class)
for m in maps]) for m in maps])
async_add_entities(sensors) async_add_entities(sensors)
def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
"""Return all capabilities supported if minimum required are present."""
return [capability for capability in CAPABILITY_TO_SENSORS
if capability in capabilities]
class SmartThingsSensor(SmartThingsEntity): class SmartThingsSensor(SmartThingsEntity):
"""Define a SmartThings Binary Sensor.""" """Define a SmartThings Binary Sensor."""

View File

@ -21,7 +21,8 @@
"token_already_setup": "The token has already been setup.", "token_already_setup": "The token has already been setup.",
"app_setup_error": "Unable to setup the SmartApp. Please try again.", "app_setup_error": "Unable to setup the SmartApp. Please try again.",
"app_not_installed": "Please ensure you have installed and authorized the Home Assistant SmartApp and try again.", "app_not_installed": "Please ensure you have installed and authorized the Home Assistant SmartApp and try again.",
"base_url_not_https": "The `base_url` for the `http` component must be configured and start with `https://`." "base_url_not_https": "The `base_url` for the `http` component must be configured and start with `https://`.",
"webhook_error": "SmartThings could not validate the endpoint configured in `base_url`. Please review the [component requirements]({component_url})."
} }
} }
} }

View File

@ -1,4 +1,6 @@
"""Support for switches through the SmartThings cloud API.""" """Support for switches through the SmartThings cloud API."""
from typing import Optional, Sequence
from homeassistant.components.switch import SwitchDevice from homeassistant.components.switch import SwitchDevice
from . import SmartThingsEntity from . import SmartThingsEntity
@ -18,28 +20,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id]
async_add_entities( async_add_entities(
[SmartThingsSwitch(device) for device in broker.devices.values() [SmartThingsSwitch(device) for device in broker.devices.values()
if is_switch(device)]) if broker.any_assigned(device.device_id, 'switch')])
def is_switch(device): def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
"""Determine if the device should be represented as a switch.""" """Return all capabilities supported if minimum required are present."""
from pysmartthings import Capability from pysmartthings import Capability
# Must be able to be turned on/off. # Must be able to be turned on/off.
if Capability.switch not in device.capabilities: if Capability.switch in capabilities:
return False return [Capability.switch]
# Must not have a capability represented by other types. return None
non_switch_capabilities = [
Capability.color_control,
Capability.color_temperature,
Capability.fan_speed,
Capability.switch_level
]
if any(capability in device.capabilities
for capability in non_switch_capabilities):
return False
return True
class SmartThingsSwitch(SmartThingsEntity, SwitchDevice): class SmartThingsSwitch(SmartThingsEntity, SwitchDevice):

View File

@ -1244,7 +1244,7 @@ pysma==0.3.1
pysmartapp==0.3.0 pysmartapp==0.3.0
# homeassistant.components.smartthings # homeassistant.components.smartthings
pysmartthings==0.6.1 pysmartthings==0.6.2
# homeassistant.components.device_tracker.snmp # homeassistant.components.device_tracker.snmp
# homeassistant.components.sensor.snmp # homeassistant.components.sensor.snmp

View File

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

View File

@ -100,19 +100,6 @@ async def test_async_setup_platform():
await climate.async_setup_platform(None, None, None) await climate.async_setup_platform(None, None, None)
def test_is_climate(device_factory, legacy_thermostat,
basic_thermostat, thermostat):
"""Test climate devices are correctly identified."""
other_devices = [
device_factory('Unknown', ['Unknown']),
device_factory("Switch 1", [Capability.switch])
]
for device in [legacy_thermostat, basic_thermostat, thermostat]:
assert climate.is_climate(device), device.name
for device in other_devices:
assert not climate.is_climate(device), device.name
async def test_legacy_thermostat_entity_state(hass, legacy_thermostat): async def test_legacy_thermostat_entity_state(hass, legacy_thermostat):
"""Tests the state attributes properly match the thermostat type.""" """Tests the state attributes properly match the thermostat type."""
await setup_platform(hass, CLIMATE_DOMAIN, legacy_thermostat) await setup_platform(hass, CLIMATE_DOMAIN, legacy_thermostat)

View File

@ -1,8 +1,9 @@
"""Tests for the SmartThings config flow module.""" """Tests for the SmartThings config flow module."""
from unittest.mock import patch from unittest.mock import Mock, patch
from uuid import uuid4 from uuid import uuid4
from aiohttp.client_exceptions import ClientResponseError from aiohttp import ClientResponseError
from pysmartthings import APIResponseError
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.components.smartthings.config_flow import ( from homeassistant.components.smartthings.config_flow import (
@ -103,13 +104,50 @@ async def test_token_forbidden(hass, smartthings_mock):
assert result['errors'] == {'access_token': 'token_forbidden'} assert result['errors'] == {'access_token': 'token_forbidden'}
async def test_webhook_error(hass, smartthings_mock):
"""Test an error is when there's an error with the webhook endpoint."""
flow = SmartThingsFlowHandler()
flow.hass = hass
data = {'error': {}}
error = APIResponseError(None, None, data=data, status=422)
error.is_target_error = Mock(return_value=True)
smartthings_mock.return_value.apps.return_value = mock_coro(
exception=error)
result = await flow.async_step_user({'access_token': str(uuid4())})
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'user'
assert result['errors'] == {'base': 'webhook_error'}
async def test_api_error(hass, smartthings_mock):
"""Test an error is shown when other API errors occur."""
flow = SmartThingsFlowHandler()
flow.hass = hass
data = {'error': {}}
error = APIResponseError(None, None, data=data, status=400)
smartthings_mock.return_value.apps.return_value = mock_coro(
exception=error)
result = await flow.async_step_user({'access_token': str(uuid4())})
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'user'
assert result['errors'] == {'base': 'app_setup_error'}
async def test_unknown_api_error(hass, smartthings_mock): async def test_unknown_api_error(hass, smartthings_mock):
"""Test an error is shown when there is an unknown API error.""" """Test an error is shown when there is an unknown API error."""
flow = SmartThingsFlowHandler() flow = SmartThingsFlowHandler()
flow.hass = hass flow.hass = hass
smartthings_mock.return_value.apps.return_value = mock_coro( smartthings_mock.return_value.apps.return_value = mock_coro(
exception=ClientResponseError(None, None, status=500)) exception=ClientResponseError(None, None, status=404))
result = await flow.async_step_user({'access_token': str(uuid4())}) result = await flow.async_step_user({'access_token': str(uuid4())})

View File

@ -39,26 +39,6 @@ async def test_async_setup_platform():
await fan.async_setup_platform(None, None, None) await fan.async_setup_platform(None, None, None)
def test_is_fan(device_factory):
"""Test fans are correctly identified."""
non_fans = [
device_factory('Unknown', ['Unknown']),
device_factory("Switch 1", [Capability.switch]),
device_factory("Non-Switchable Fan", [Capability.fan_speed]),
device_factory("Color Light",
[Capability.switch, Capability.switch_level,
Capability.color_control,
Capability.color_temperature])
]
fan_device = device_factory(
"Fan 1", [Capability.switch, Capability.switch_level,
Capability.fan_speed])
assert fan.is_fan(fan_device), fan_device.name
for device in non_fans:
assert not fan.is_fan(device), device.name
async def test_entity_state(hass, device_factory): async def test_entity_state(hass, device_factory):
"""Tests the state attributes properly match the fan types.""" """Tests the state attributes properly match the fan types."""
device = device_factory( device = device_factory(

View File

@ -65,25 +65,6 @@ async def test_async_setup_platform():
await light.async_setup_platform(None, None, None) await light.async_setup_platform(None, None, None)
def test_is_light(device_factory, light_devices):
"""Test lights are correctly identified."""
non_lights = [
device_factory('Unknown', ['Unknown']),
device_factory("Fan 1",
[Capability.switch, Capability.switch_level,
Capability.fan_speed]),
device_factory("Switch 1", [Capability.switch]),
device_factory("Can't be turned off",
[Capability.switch_level, Capability.color_control,
Capability.color_temperature])
]
for device in light_devices:
assert light.is_light(device), device.name
for device in non_lights:
assert not light.is_light(device), device.name
async def test_entity_state(hass, light_devices): async def test_entity_state(hass, light_devices):
"""Tests the state attributes properly match the light types.""" """Tests the state attributes properly match the light types."""
await _setup_platform(hass, *light_devices) await _setup_platform(hass, *light_devices)

View File

@ -20,12 +20,6 @@ async def test_async_setup_platform():
await lock.async_setup_platform(None, None, None) await lock.async_setup_platform(None, None, None)
def test_is_lock(device_factory):
"""Test locks are correctly identified."""
lock_device = device_factory('Lock', [Capability.lock])
assert lock.is_lock(lock_device)
async def test_entity_and_device_attributes(hass, device_factory): async def test_entity_and_device_attributes(hass, device_factory):
"""Test the attributes of the entity are correct.""" """Test the attributes of the entity are correct."""
# Arrange # Arrange

View File

@ -35,23 +35,6 @@ async def test_async_setup_platform():
await switch.async_setup_platform(None, None, None) await switch.async_setup_platform(None, None, None)
def test_is_switch(device_factory):
"""Test switches are correctly identified."""
switch_device = device_factory('Switch', [Capability.switch])
non_switch_devices = [
device_factory('Light', [Capability.switch, Capability.switch_level]),
device_factory('Fan', [Capability.switch, Capability.fan_speed]),
device_factory('Color Light', [Capability.switch,
Capability.color_control]),
device_factory('Temp Light', [Capability.switch,
Capability.color_temperature]),
device_factory('Unknown', ['Unknown']),
]
assert switch.is_switch(switch_device)
for non_switch_device in non_switch_devices:
assert not switch.is_switch(non_switch_device)
async def test_entity_and_device_attributes(hass, device_factory): async def test_entity_and_device_attributes(hass, device_factory):
"""Test the attributes of the entity are correct.""" """Test the attributes of the entity are correct."""
# Arrange # Arrange