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 (
|
||||
setup_smartapp, setup_smartapp_endpoint, validate_installed_app)
|
||||
|
||||
REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.5.0']
|
||||
REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.6.0']
|
||||
DEPENDENCIES = ['webhook']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
|
|
@ -22,25 +22,9 @@ SUPPORTED_PLATFORMS = [
|
|||
'binary_sensor',
|
||||
'fan',
|
||||
'light',
|
||||
'sensor',
|
||||
'switch'
|
||||
]
|
||||
SUPPORTED_CAPABILITIES = [
|
||||
'accelerationSensor',
|
||||
'button',
|
||||
'colorControl',
|
||||
'colorTemperature',
|
||||
'contactSensor',
|
||||
'fanSpeed',
|
||||
'filterStatus',
|
||||
'motionSensor',
|
||||
'presenceSensor',
|
||||
'soundSensor',
|
||||
'switch',
|
||||
'switchLevel',
|
||||
'tamperAlert',
|
||||
'valve',
|
||||
'waterSensor'
|
||||
]
|
||||
VAL_UID = "^(?:([0-9a-fA-F]{32})|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]" \
|
||||
"{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}))$"
|
||||
VAL_UID_MATCHER = re.compile(VAL_UID)
|
||||
|
|
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 (
|
||||
APP_NAME_PREFIX, APP_OAUTH_SCOPES, CONF_APP_ID, CONF_INSTALLED_APP_ID,
|
||||
CONF_INSTANCE_ID, CONF_LOCATION_ID, DATA_BROKERS, DATA_MANAGER, DOMAIN,
|
||||
SETTINGS_INSTANCE_ID, SIGNAL_SMARTAPP_PREFIX, STORAGE_KEY, STORAGE_VERSION,
|
||||
SUPPORTED_CAPABILITIES)
|
||||
SETTINGS_INSTANCE_ID, SIGNAL_SMARTAPP_PREFIX, STORAGE_KEY, STORAGE_VERSION)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -176,6 +175,7 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType):
|
|||
webhook.async_generate_path(config[CONF_WEBHOOK_ID]),
|
||||
dispatcher=dispatcher)
|
||||
manager.connect_install(functools.partial(smartapp_install, hass))
|
||||
manager.connect_update(functools.partial(smartapp_update, hass))
|
||||
manager.connect_uninstall(functools.partial(smartapp_uninstall, hass))
|
||||
|
||||
webhook.async_register(hass, DOMAIN, 'SmartApp',
|
||||
|
@ -189,6 +189,45 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType):
|
|||
}
|
||||
|
||||
|
||||
async def smartapp_sync_subscriptions(
|
||||
hass: HomeAssistantType, auth_token: str, location_id: str,
|
||||
installed_app_id: str, *, skip_delete=False):
|
||||
"""Synchronize subscriptions of an installed up."""
|
||||
from pysmartthings import (
|
||||
CAPABILITIES, SmartThings, SourceType, Subscription)
|
||||
|
||||
api = SmartThings(async_get_clientsession(hass), auth_token)
|
||||
devices = await api.devices(location_ids=[location_id])
|
||||
|
||||
# Build set of capabilities and prune unsupported ones
|
||||
capabilities = set()
|
||||
for device in devices:
|
||||
capabilities.update(device.capabilities)
|
||||
capabilities.intersection_update(CAPABILITIES)
|
||||
|
||||
# Remove all (except for installs)
|
||||
if not skip_delete:
|
||||
await api.delete_subscriptions(installed_app_id)
|
||||
|
||||
# Create for each capability
|
||||
async def create_subscription(target):
|
||||
sub = Subscription()
|
||||
sub.installed_app_id = installed_app_id
|
||||
sub.location_id = location_id
|
||||
sub.source_type = SourceType.CAPABILITY
|
||||
sub.capability = target
|
||||
try:
|
||||
await api.create_subscription(sub)
|
||||
_LOGGER.debug("Created subscription for '%s' under app '%s'",
|
||||
target, installed_app_id)
|
||||
except Exception: # pylint:disable=broad-except
|
||||
_LOGGER.exception("Failed to create subscription for '%s' under "
|
||||
"app '%s'", target, installed_app_id)
|
||||
|
||||
tasks = [create_subscription(c) for c in capabilities]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
|
||||
async def smartapp_install(hass: HomeAssistantType, req, resp, app):
|
||||
"""
|
||||
Handle when a SmartApp is installed by the user into a location.
|
||||
|
@ -199,30 +238,9 @@ async def smartapp_install(hass: HomeAssistantType, req, resp, app):
|
|||
representing the installation if this is not the first installation under
|
||||
the account.
|
||||
"""
|
||||
from pysmartthings import SmartThings, Subscription, SourceType
|
||||
|
||||
# This access token is a temporary 'SmartApp token' that expires in 5 min
|
||||
# and is used to create subscriptions only.
|
||||
api = SmartThings(async_get_clientsession(hass), req.auth_token)
|
||||
|
||||
async def create_subscription(target):
|
||||
sub = Subscription()
|
||||
sub.installed_app_id = req.installed_app_id
|
||||
sub.location_id = req.location_id
|
||||
sub.source_type = SourceType.CAPABILITY
|
||||
sub.capability = target
|
||||
try:
|
||||
await api.create_subscription(sub)
|
||||
_LOGGER.debug("Created subscription for '%s' under app '%s'",
|
||||
target, req.installed_app_id)
|
||||
except Exception: # pylint:disable=broad-except
|
||||
_LOGGER.exception("Failed to create subscription for '%s' under "
|
||||
"app '%s'", target, req.installed_app_id)
|
||||
|
||||
tasks = [create_subscription(c) for c in SUPPORTED_CAPABILITIES]
|
||||
await asyncio.gather(*tasks)
|
||||
_LOGGER.debug("SmartApp '%s' under parent app '%s' was installed",
|
||||
req.installed_app_id, app.app_id)
|
||||
await smartapp_sync_subscriptions(
|
||||
hass, req.auth_token, req.location_id, req.installed_app_id,
|
||||
skip_delete=True)
|
||||
|
||||
# The permanent access token is copied from another config flow with the
|
||||
# same parent app_id. If one is not found, that means the user is within
|
||||
|
@ -244,6 +262,19 @@ async def smartapp_install(hass: HomeAssistantType, req, resp, app):
|
|||
})
|
||||
|
||||
|
||||
async def smartapp_update(hass: HomeAssistantType, req, resp, app):
|
||||
"""
|
||||
Handle when a SmartApp is updated (reconfigured) by the user.
|
||||
|
||||
Synchronize subscriptions to ensure we're up-to-date.
|
||||
"""
|
||||
await smartapp_sync_subscriptions(
|
||||
hass, req.auth_token, req.location_id, req.installed_app_id)
|
||||
|
||||
_LOGGER.debug("SmartApp '%s' under parent app '%s' was updated",
|
||||
req.installed_app_id, app.app_id)
|
||||
|
||||
|
||||
async def smartapp_uninstall(hass: HomeAssistantType, req, resp, app):
|
||||
"""
|
||||
Handle when a SmartApp is removed from a location by the user.
|
||||
|
|
|
@ -1231,7 +1231,7 @@ pysma==0.3.1
|
|||
pysmartapp==0.3.0
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==0.5.0
|
||||
pysmartthings==0.6.0
|
||||
|
||||
# homeassistant.components.device_tracker.snmp
|
||||
# homeassistant.components.sensor.snmp
|
||||
|
|
|
@ -217,7 +217,7 @@ pyqwikswitch==0.8
|
|||
pysmartapp==0.3.0
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==0.5.0
|
||||
pysmartthings==0.6.0
|
||||
|
||||
# homeassistant.components.sonos
|
||||
pysonos==0.0.6
|
||||
|
|
|
@ -10,9 +10,10 @@ from pysmartthings.api import Api
|
|||
import pytest
|
||||
|
||||
from homeassistant.components import webhook
|
||||
from homeassistant.components.smartthings import DeviceBroker
|
||||
from homeassistant.components.smartthings.const import (
|
||||
APP_NAME_PREFIX, CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_INSTANCE_ID,
|
||||
CONF_LOCATION_ID, DOMAIN, SETTINGS_INSTANCE_ID, STORAGE_KEY,
|
||||
CONF_LOCATION_ID, DATA_BROKERS, DOMAIN, SETTINGS_INSTANCE_ID, STORAGE_KEY,
|
||||
STORAGE_VERSION)
|
||||
from homeassistant.config_entries import (
|
||||
CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry)
|
||||
|
@ -22,6 +23,23 @@ from homeassistant.setup import async_setup_component
|
|||
from tests.common import mock_coro
|
||||
|
||||
|
||||
async def setup_platform(hass, platform: str, *devices):
|
||||
"""Set up the SmartThings platform and prerequisites."""
|
||||
hass.config.components.add(DOMAIN)
|
||||
broker = DeviceBroker(hass, devices, '')
|
||||
config_entry = ConfigEntry("1", DOMAIN, "Test", {},
|
||||
SOURCE_USER, CONN_CLASS_CLOUD_PUSH)
|
||||
hass.data[DOMAIN] = {
|
||||
DATA_BROKERS: {
|
||||
config_entry.entry_id: broker
|
||||
}
|
||||
}
|
||||
await hass.config_entries.async_forward_entry_setup(
|
||||
config_entry, platform)
|
||||
await hass.async_block_till_done()
|
||||
return config_entry
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_component(hass, config_file, hass_storage):
|
||||
"""Load the SmartThing component."""
|
||||
|
|
|
@ -4,12 +4,12 @@ Test for the SmartThings binary_sensor platform.
|
|||
The only mocking required is of the underlying SmartThings API object so
|
||||
real HTTP calls are not initiated during testing.
|
||||
"""
|
||||
from pysmartthings import Attribute, Capability
|
||||
from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability
|
||||
|
||||
from homeassistant.components.binary_sensor import DEVICE_CLASSES
|
||||
from homeassistant.components.smartthings import DeviceBroker, binary_sensor
|
||||
from homeassistant.components.smartthings.const import (
|
||||
DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_CAPABILITIES)
|
||||
DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE)
|
||||
from homeassistant.config_entries import (
|
||||
CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry)
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
|
@ -35,14 +35,16 @@ async def _setup_platform(hass, *devices):
|
|||
|
||||
async def test_mapping_integrity():
|
||||
"""Test ensures the map dicts have proper integrity."""
|
||||
# Ensure every CAPABILITY_TO_ATTRIB key is in SUPPORTED_CAPABILITIES
|
||||
# Ensure every CAPABILITY_TO_ATTRIB key is in CAPABILITIES
|
||||
# Ensure every CAPABILITY_TO_ATTRIB value is in ATTRIB_TO_CLASS keys
|
||||
for capability, attrib in binary_sensor.CAPABILITY_TO_ATTRIB.items():
|
||||
assert capability in SUPPORTED_CAPABILITIES, capability
|
||||
assert capability in CAPABILITIES, capability
|
||||
assert attrib in ATTRIBUTES, attrib
|
||||
assert attrib in binary_sensor.ATTRIB_TO_CLASS.keys(), attrib
|
||||
# Ensure every ATTRIB_TO_CLASS value is in DEVICE_CLASSES
|
||||
for device_class in binary_sensor.ATTRIB_TO_CLASS.values():
|
||||
assert device_class in DEVICE_CLASSES
|
||||
for attrib, device_class in binary_sensor.ATTRIB_TO_CLASS.items():
|
||||
assert attrib in ATTRIBUTES, attrib
|
||||
assert device_class in DEVICE_CLASSES, device_class
|
||||
|
||||
|
||||
async def test_async_setup_platform():
|
||||
|
|
|
@ -162,7 +162,7 @@ async def test_event_handler_dispatches_updated_devices(
|
|||
|
||||
assert called
|
||||
for device in devices:
|
||||
assert device.status.attributes['Updated'] == 'Value'
|
||||
assert device.status.values['Updated'] == 'Value'
|
||||
|
||||
|
||||
async def test_event_handler_ignores_other_installed_app(
|
||||
|
|
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 uuid import uuid4
|
||||
|
||||
from pysmartthings import AppEntity
|
||||
from pysmartthings import AppEntity, Capability
|
||||
|
||||
from homeassistant.components.smartthings import smartapp
|
||||
from homeassistant.components.smartthings.const import (
|
||||
DATA_MANAGER, DOMAIN, SUPPORTED_CAPABILITIES)
|
||||
from homeassistant.components.smartthings.const import DATA_MANAGER, DOMAIN
|
||||
|
||||
from tests.common import mock_coro
|
||||
|
||||
|
@ -36,8 +35,10 @@ async def test_update_app_updated_needed(hass, app):
|
|||
assert mock_app.classifications == app.classifications
|
||||
|
||||
|
||||
async def test_smartapp_install_abort_if_no_other(hass, smartthings_mock):
|
||||
async def test_smartapp_install_abort_if_no_other(
|
||||
hass, smartthings_mock, device_factory):
|
||||
"""Test aborts if no other app was configured already."""
|
||||
# Arrange
|
||||
api = smartthings_mock.return_value
|
||||
api.create_subscription.return_value = mock_coro()
|
||||
app = Mock()
|
||||
|
@ -46,17 +47,23 @@ async def test_smartapp_install_abort_if_no_other(hass, smartthings_mock):
|
|||
request.installed_app_id = uuid4()
|
||||
request.auth_token = uuid4()
|
||||
request.location_id = uuid4()
|
||||
|
||||
devices = [
|
||||
device_factory('', [Capability.battery, 'ping']),
|
||||
device_factory('', [Capability.switch, Capability.switch_level]),
|
||||
device_factory('', [Capability.switch])
|
||||
]
|
||||
api.devices = Mock()
|
||||
api.devices.return_value = mock_coro(return_value=devices)
|
||||
# Act
|
||||
await smartapp.smartapp_install(hass, request, None, app)
|
||||
|
||||
# Assert
|
||||
entries = hass.config_entries.async_entries('smartthings')
|
||||
assert not entries
|
||||
assert api.create_subscription.call_count == \
|
||||
len(SUPPORTED_CAPABILITIES)
|
||||
assert api.create_subscription.call_count == 3
|
||||
|
||||
|
||||
async def test_smartapp_install_creates_flow(
|
||||
hass, smartthings_mock, config_entry, location):
|
||||
hass, smartthings_mock, config_entry, location, device_factory):
|
||||
"""Test installation creates flow."""
|
||||
# Arrange
|
||||
setattr(hass.config_entries, '_entries', [config_entry])
|
||||
|
@ -68,14 +75,20 @@ async def test_smartapp_install_creates_flow(
|
|||
request.installed_app_id = str(uuid4())
|
||||
request.auth_token = str(uuid4())
|
||||
request.location_id = location.location_id
|
||||
devices = [
|
||||
device_factory('', [Capability.battery, 'ping']),
|
||||
device_factory('', [Capability.switch, Capability.switch_level]),
|
||||
device_factory('', [Capability.switch])
|
||||
]
|
||||
api.devices = Mock()
|
||||
api.devices.return_value = mock_coro(return_value=devices)
|
||||
# Act
|
||||
await smartapp.smartapp_install(hass, request, None, app)
|
||||
# Assert
|
||||
await hass.async_block_till_done()
|
||||
entries = hass.config_entries.async_entries('smartthings')
|
||||
assert len(entries) == 2
|
||||
assert api.create_subscription.call_count == \
|
||||
len(SUPPORTED_CAPABILITIES)
|
||||
assert api.create_subscription.call_count == 3
|
||||
assert entries[1].data['app_id'] == app.app_id
|
||||
assert entries[1].data['installed_app_id'] == request.installed_app_id
|
||||
assert entries[1].data['location_id'] == request.location_id
|
||||
|
@ -84,6 +97,35 @@ async def test_smartapp_install_creates_flow(
|
|||
assert entries[1].title == location.name
|
||||
|
||||
|
||||
async def test_smartapp_update_syncs_subs(
|
||||
hass, smartthings_mock, config_entry, location, device_factory):
|
||||
"""Test update synchronizes subscriptions."""
|
||||
# Arrange
|
||||
setattr(hass.config_entries, '_entries', [config_entry])
|
||||
app = Mock()
|
||||
app.app_id = config_entry.data['app_id']
|
||||
api = smartthings_mock.return_value
|
||||
api.delete_subscriptions = Mock()
|
||||
api.delete_subscriptions.return_value = mock_coro()
|
||||
api.create_subscription.return_value = mock_coro()
|
||||
request = Mock()
|
||||
request.installed_app_id = str(uuid4())
|
||||
request.auth_token = str(uuid4())
|
||||
request.location_id = location.location_id
|
||||
devices = [
|
||||
device_factory('', [Capability.battery, 'ping']),
|
||||
device_factory('', [Capability.switch, Capability.switch_level]),
|
||||
device_factory('', [Capability.switch])
|
||||
]
|
||||
api.devices = Mock()
|
||||
api.devices.return_value = mock_coro(return_value=devices)
|
||||
# Act
|
||||
await smartapp.smartapp_update(hass, request, None, app)
|
||||
# Assert
|
||||
assert api.create_subscription.call_count == 3
|
||||
assert api.delete_subscriptions.call_count == 1
|
||||
|
||||
|
||||
async def test_smartapp_uninstall(hass, config_entry):
|
||||
"""Test the config entry is unloaded when the app is uninstalled."""
|
||||
setattr(hass.config_entries, '_entries', [config_entry])
|
||||
|
|
Loading…
Add table
Reference in a new issue