diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index df14ab680550..25cd9e8305f2 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -23,6 +23,7 @@ SUPPORTED_PLATFORMS = [ 'climate', 'fan', 'light', + 'lock', 'sensor', 'switch' ] diff --git a/homeassistant/components/smartthings/lock.py b/homeassistant/components/smartthings/lock.py new file mode 100644 index 000000000000..6dfff0bd02c6 --- /dev/null +++ b/homeassistant/components/smartthings/lock.py @@ -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 diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py new file mode 100644 index 000000000000..c73f4ff549ee --- /dev/null +++ b/tests/components/smartthings/test_lock.py @@ -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') diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index a8013105291f..15ff3adce86c 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -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