1
mirror of https://github.com/home-assistant/core synced 2024-09-28 03:04:04 +02:00

Add Lock capability to SmartThings platform (#20977)

* Bumped pysmartthings version to 0.6.1

* Added Lock to supported platforms

* Added SmartThings Lock component

* Updated lock to eagerly set state

* Updated requirements_all.txt & requirements_test_all.txt with pysmartthings==0.6.1

* Added SmartThings Lock tests

* Removed inapplicable comment

* Removed unused import (STATE_UNLOCKED)

* Populated device_state_attributes with values provided by SmartThings

* Condensed if_lock assertion function

* Updated gathered attributes

* Fixed typo

* Updated tests to use new setup_platform

* Updated assignment of device state attributes

* Updated tests to utilise the LOCK_DOMAIN constant where suitable

* Fixed false positive for Switch test: (test_unload_config_entry)

* Implemented constant to contain expected SmartThings state for is_locked check

* Improved allocation of State Attributes

* Improved allocation of state attributes

* Fixed lint error (was running lint checks against the wrong file, whoops)

* Added test for unloading lock config

* Use isinstance instead of type()

* Updated device state to explicitly check for is not None instead of a truthy value
This commit is contained in:
Ben Dews 2019-02-14 11:36:49 +11:00 committed by Andrew Sayre
parent 02f207ea8e
commit c20e0b985a
4 changed files with 185 additions and 1 deletions

View File

@ -23,6 +23,7 @@ SUPPORTED_PLATFORMS = [
'climate',
'fan',
'light',
'lock',
'sensor',
'switch'
]

View File

@ -0,0 +1,73 @@
"""
Support for locks through the SmartThings cloud API.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/smartthings.lock/
"""
from homeassistant.components.lock import LockDevice
from . import SmartThingsEntity
from .const import DATA_BROKERS, DOMAIN
DEPENDENCIES = ['smartthings']
ST_STATE_LOCKED = 'locked'
ST_LOCK_ATTR_MAP = {
'method': 'method',
'codeId': 'code_id',
'timeout': 'timeout'
}
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 locks for a config entry."""
broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id]
async_add_entities(
[SmartThingsLock(device) for device in broker.devices.values()
if is_lock(device)])
def is_lock(device):
"""Determine if the device supports the lock capability."""
from pysmartthings import Capability
return Capability.lock in device.capabilities
class SmartThingsLock(SmartThingsEntity, LockDevice):
"""Define a SmartThings lock."""
async def async_lock(self, **kwargs):
"""Lock the device."""
await self._device.lock(set_status=True)
self.async_schedule_update_ha_state()
async def async_unlock(self, **kwargs):
"""Unlock the device."""
await self._device.unlock(set_status=True)
self.async_schedule_update_ha_state()
@property
def is_locked(self):
"""Return true if lock is locked."""
return self._device.status.lock == ST_STATE_LOCKED
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
from pysmartthings import Attribute
state_attrs = {}
status = self._device.status.attributes[Attribute.lock]
if status.value:
state_attrs['lock_state'] = status.value
if isinstance(status.data, dict):
for st_attr, ha_attr in ST_LOCK_ATTR_MAP.items():
data_val = status.data.get(st_attr)
if data_val is not None:
state_attrs[ha_attr] = data_val
return state_attrs

View File

@ -0,0 +1,110 @@
"""
Test for the SmartThings lock 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 homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.components.smartthings import lock
from homeassistant.components.smartthings.const import (
DOMAIN, SIGNAL_SMARTTHINGS_UPDATE)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .conftest import setup_platform
async def test_async_setup_platform():
"""Test setup platform does nothing (it uses config entries)."""
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):
"""Test the attributes of the entity are correct."""
# Arrange
device = device_factory('Lock_1', [Capability.lock],
{Attribute.lock: 'unlocked'})
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, LOCK_DOMAIN, device)
# Assert
entry = entity_registry.async_get('lock.lock_1')
assert entry
assert entry.unique_id == device.device_id
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_lock(hass, device_factory):
"""Test the lock locks successfully."""
# Arrange
device = device_factory('Lock_1', [Capability.lock],
{Attribute.lock: 'unlocked'})
await setup_platform(hass, LOCK_DOMAIN, device)
# Act
await hass.services.async_call(
LOCK_DOMAIN, 'lock', {'entity_id': 'lock.lock_1'},
blocking=True)
# Assert
state = hass.states.get('lock.lock_1')
assert state is not None
assert state.state == 'locked'
async def test_unlock(hass, device_factory):
"""Test the lock unlocks successfully."""
# Arrange
device = device_factory('Lock_1', [Capability.lock],
{Attribute.lock: 'locked'})
await setup_platform(hass, LOCK_DOMAIN, device)
# Act
await hass.services.async_call(
LOCK_DOMAIN, 'unlock', {'entity_id': 'lock.lock_1'},
blocking=True)
# Assert
state = hass.states.get('lock.lock_1')
assert state is not None
assert state.state == 'unlocked'
async def test_update_from_signal(hass, device_factory):
"""Test the lock updates when receiving a signal."""
# Arrange
device = device_factory('Lock_1', [Capability.lock],
{Attribute.lock: 'unlocked'})
await setup_platform(hass, LOCK_DOMAIN, device)
await device.lock(True)
# Act
async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE,
[device.device_id])
# Assert
await hass.async_block_till_done()
state = hass.states.get('lock.lock_1')
assert state is not None
assert state.state == 'locked'
async def test_unload_config_entry(hass, device_factory):
"""Test the lock is removed when the config entry is unloaded."""
# Arrange
device = device_factory('Lock_1', [Capability.lock],
{Attribute.lock: 'locked'})
config_entry = await setup_platform(hass, LOCK_DOMAIN, device)
# Act
await hass.config_entries.async_forward_entry_unload(
config_entry, 'lock')
# Assert
assert not hass.states.get('lock.lock_1')

View File

@ -126,7 +126,7 @@ async def test_update_from_signal(hass, device_factory):
async def test_unload_config_entry(hass, device_factory):
"""Test the switch is removed when the config entry is unloaded."""
# Arrange
device = device_factory('Switch', [Capability.switch],
device = device_factory('Switch 1', [Capability.switch],
{Attribute.switch: 'on'})
config_entry = await _setup_platform(hass, device)
# Act