mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
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:
parent
7d0f847f83
commit
93f84a5cd1
@ -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": {
|
||||||
|
@ -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)
|
||||||
|
@ -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'):
|
||||||
|
attrib = CAPABILITY_TO_ATTRIB[capability]
|
||||||
sensors.append(SmartThingsBinarySensor(device, attrib))
|
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."""
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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"
|
||||||
|
@ -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}))$"
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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,8 +165,8 @@ 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,
|
||||||
@ -174,6 +175,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
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."""
|
||||||
|
|
||||||
|
@ -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})."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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())})
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user