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:
parent
c7df4cf092
commit
706810bbce
11 changed files with 457 additions and 65 deletions
|
@ -23,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.5.0']
|
REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.6.0']
|
||||||
DEPENDENCIES = ['webhook']
|
DEPENDENCIES = ['webhook']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
|
@ -22,25 +22,9 @@ SUPPORTED_PLATFORMS = [
|
||||||
'binary_sensor',
|
'binary_sensor',
|
||||||
'fan',
|
'fan',
|
||||||
'light',
|
'light',
|
||||||
|
'sensor',
|
||||||
'switch'
|
'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]" \
|
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}))$"
|
||||||
VAL_UID_MATCHER = re.compile(VAL_UID)
|
VAL_UID_MATCHER = re.compile(VAL_UID)
|
||||||
|
|
218
homeassistant/components/smartthings/sensor.py
Normal file
218
homeassistant/components/smartthings/sensor.py
Normal 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
|
|
@ -22,8 +22,7 @@ from homeassistant.helpers.typing import HomeAssistantType
|
||||||
from .const import (
|
from .const import (
|
||||||
APP_NAME_PREFIX, APP_OAUTH_SCOPES, CONF_APP_ID, CONF_INSTALLED_APP_ID,
|
APP_NAME_PREFIX, APP_OAUTH_SCOPES, CONF_APP_ID, CONF_INSTALLED_APP_ID,
|
||||||
CONF_INSTANCE_ID, CONF_LOCATION_ID, DATA_BROKERS, DATA_MANAGER, DOMAIN,
|
CONF_INSTANCE_ID, CONF_LOCATION_ID, DATA_BROKERS, DATA_MANAGER, DOMAIN,
|
||||||
SETTINGS_INSTANCE_ID, SIGNAL_SMARTAPP_PREFIX, STORAGE_KEY, STORAGE_VERSION,
|
SETTINGS_INSTANCE_ID, SIGNAL_SMARTAPP_PREFIX, STORAGE_KEY, STORAGE_VERSION)
|
||||||
SUPPORTED_CAPABILITIES)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -176,6 +175,7 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType):
|
||||||
webhook.async_generate_path(config[CONF_WEBHOOK_ID]),
|
webhook.async_generate_path(config[CONF_WEBHOOK_ID]),
|
||||||
dispatcher=dispatcher)
|
dispatcher=dispatcher)
|
||||||
manager.connect_install(functools.partial(smartapp_install, hass))
|
manager.connect_install(functools.partial(smartapp_install, hass))
|
||||||
|
manager.connect_update(functools.partial(smartapp_update, hass))
|
||||||
manager.connect_uninstall(functools.partial(smartapp_uninstall, hass))
|
manager.connect_uninstall(functools.partial(smartapp_uninstall, hass))
|
||||||
|
|
||||||
webhook.async_register(hass, DOMAIN, 'SmartApp',
|
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):
|
async def smartapp_install(hass: HomeAssistantType, req, resp, app):
|
||||||
"""
|
"""
|
||||||
Handle when a SmartApp is installed by the user into a location.
|
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
|
representing the installation if this is not the first installation under
|
||||||
the account.
|
the account.
|
||||||
"""
|
"""
|
||||||
from pysmartthings import SmartThings, Subscription, SourceType
|
await smartapp_sync_subscriptions(
|
||||||
|
hass, req.auth_token, req.location_id, req.installed_app_id,
|
||||||
# This access token is a temporary 'SmartApp token' that expires in 5 min
|
skip_delete=True)
|
||||||
# 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)
|
|
||||||
|
|
||||||
# The permanent access token is copied from another config flow with the
|
# 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
|
# 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):
|
async def smartapp_uninstall(hass: HomeAssistantType, req, resp, app):
|
||||||
"""
|
"""
|
||||||
Handle when a SmartApp is removed from a location by the user.
|
Handle when a SmartApp is removed from a location by the user.
|
||||||
|
|
|
@ -1231,7 +1231,7 @@ pysma==0.3.1
|
||||||
pysmartapp==0.3.0
|
pysmartapp==0.3.0
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartthings==0.5.0
|
pysmartthings==0.6.0
|
||||||
|
|
||||||
# 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.5.0
|
pysmartthings==0.6.0
|
||||||
|
|
||||||
# homeassistant.components.sonos
|
# homeassistant.components.sonos
|
||||||
pysonos==0.0.6
|
pysonos==0.0.6
|
||||||
|
|
|
@ -10,9 +10,10 @@ from pysmartthings.api import Api
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components import webhook
|
from homeassistant.components import webhook
|
||||||
|
from homeassistant.components.smartthings import DeviceBroker
|
||||||
from homeassistant.components.smartthings.const import (
|
from homeassistant.components.smartthings.const import (
|
||||||
APP_NAME_PREFIX, CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_INSTANCE_ID,
|
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)
|
STORAGE_VERSION)
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import (
|
||||||
CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry)
|
CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry)
|
||||||
|
@ -22,6 +23,23 @@ from homeassistant.setup import async_setup_component
|
||||||
from tests.common import mock_coro
|
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)
|
@pytest.fixture(autouse=True)
|
||||||
async def setup_component(hass, config_file, hass_storage):
|
async def setup_component(hass, config_file, hass_storage):
|
||||||
"""Load the SmartThing component."""
|
"""Load the SmartThing component."""
|
||||||
|
|
|
@ -4,12 +4,12 @@ Test for the SmartThings binary_sensor platform.
|
||||||
The only mocking required is of the underlying SmartThings API object so
|
The only mocking required is of the underlying SmartThings API object so
|
||||||
real HTTP calls are not initiated during testing.
|
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.binary_sensor import DEVICE_CLASSES
|
||||||
from homeassistant.components.smartthings import DeviceBroker, binary_sensor
|
from homeassistant.components.smartthings import DeviceBroker, binary_sensor
|
||||||
from homeassistant.components.smartthings.const import (
|
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 (
|
from homeassistant.config_entries import (
|
||||||
CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry)
|
CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry)
|
||||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||||
|
@ -35,14 +35,16 @@ async def _setup_platform(hass, *devices):
|
||||||
|
|
||||||
async def test_mapping_integrity():
|
async def test_mapping_integrity():
|
||||||
"""Test ensures the map dicts have proper 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
|
# Ensure every CAPABILITY_TO_ATTRIB value is in ATTRIB_TO_CLASS keys
|
||||||
for capability, attrib in binary_sensor.CAPABILITY_TO_ATTRIB.items():
|
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
|
assert attrib in binary_sensor.ATTRIB_TO_CLASS.keys(), attrib
|
||||||
# Ensure every ATTRIB_TO_CLASS value is in DEVICE_CLASSES
|
# Ensure every ATTRIB_TO_CLASS value is in DEVICE_CLASSES
|
||||||
for device_class in binary_sensor.ATTRIB_TO_CLASS.values():
|
for attrib, device_class in binary_sensor.ATTRIB_TO_CLASS.items():
|
||||||
assert device_class in DEVICE_CLASSES
|
assert attrib in ATTRIBUTES, attrib
|
||||||
|
assert device_class in DEVICE_CLASSES, device_class
|
||||||
|
|
||||||
|
|
||||||
async def test_async_setup_platform():
|
async def test_async_setup_platform():
|
||||||
|
|
|
@ -162,7 +162,7 @@ async def test_event_handler_dispatches_updated_devices(
|
||||||
|
|
||||||
assert called
|
assert called
|
||||||
for device in devices:
|
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(
|
async def test_event_handler_ignores_other_installed_app(
|
||||||
|
|
97
tests/components/smartthings/test_sensor.py
Normal file
97
tests/components/smartthings/test_sensor.py
Normal 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')
|
|
@ -2,11 +2,10 @@
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from pysmartthings import AppEntity
|
from pysmartthings import AppEntity, Capability
|
||||||
|
|
||||||
from homeassistant.components.smartthings import smartapp
|
from homeassistant.components.smartthings import smartapp
|
||||||
from homeassistant.components.smartthings.const import (
|
from homeassistant.components.smartthings.const import DATA_MANAGER, DOMAIN
|
||||||
DATA_MANAGER, DOMAIN, SUPPORTED_CAPABILITIES)
|
|
||||||
|
|
||||||
from tests.common import mock_coro
|
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
|
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."""
|
"""Test aborts if no other app was configured already."""
|
||||||
|
# Arrange
|
||||||
api = smartthings_mock.return_value
|
api = smartthings_mock.return_value
|
||||||
api.create_subscription.return_value = mock_coro()
|
api.create_subscription.return_value = mock_coro()
|
||||||
app = Mock()
|
app = Mock()
|
||||||
|
@ -46,17 +47,23 @@ async def test_smartapp_install_abort_if_no_other(hass, smartthings_mock):
|
||||||
request.installed_app_id = uuid4()
|
request.installed_app_id = uuid4()
|
||||||
request.auth_token = uuid4()
|
request.auth_token = uuid4()
|
||||||
request.location_id = 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)
|
await smartapp.smartapp_install(hass, request, None, app)
|
||||||
|
# Assert
|
||||||
entries = hass.config_entries.async_entries('smartthings')
|
entries = hass.config_entries.async_entries('smartthings')
|
||||||
assert not entries
|
assert not entries
|
||||||
assert api.create_subscription.call_count == \
|
assert api.create_subscription.call_count == 3
|
||||||
len(SUPPORTED_CAPABILITIES)
|
|
||||||
|
|
||||||
|
|
||||||
async def test_smartapp_install_creates_flow(
|
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."""
|
"""Test installation creates flow."""
|
||||||
# Arrange
|
# Arrange
|
||||||
setattr(hass.config_entries, '_entries', [config_entry])
|
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.installed_app_id = str(uuid4())
|
||||||
request.auth_token = str(uuid4())
|
request.auth_token = str(uuid4())
|
||||||
request.location_id = location.location_id
|
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
|
# Act
|
||||||
await smartapp.smartapp_install(hass, request, None, app)
|
await smartapp.smartapp_install(hass, request, None, app)
|
||||||
# Assert
|
# Assert
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
entries = hass.config_entries.async_entries('smartthings')
|
entries = hass.config_entries.async_entries('smartthings')
|
||||||
assert len(entries) == 2
|
assert len(entries) == 2
|
||||||
assert api.create_subscription.call_count == \
|
assert api.create_subscription.call_count == 3
|
||||||
len(SUPPORTED_CAPABILITIES)
|
|
||||||
assert entries[1].data['app_id'] == app.app_id
|
assert entries[1].data['app_id'] == app.app_id
|
||||||
assert entries[1].data['installed_app_id'] == request.installed_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['location_id'] == request.location_id
|
||||||
|
@ -84,6 +97,35 @@ async def test_smartapp_install_creates_flow(
|
||||||
assert entries[1].title == location.name
|
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):
|
async def test_smartapp_uninstall(hass, config_entry):
|
||||||
"""Test the config entry is unloaded when the app is uninstalled."""
|
"""Test the config entry is unloaded when the app is uninstalled."""
|
||||||
setattr(hass.config_entries, '_entries', [config_entry])
|
setattr(hass.config_entries, '_entries', [config_entry])
|
||||||
|
|
Loading…
Add table
Reference in a new issue