Security fix & lock for HomeMatic (#11980)

* HomeMatic KeyMatic device become a real lock component

* Adds supported features to lock component.

Locks may are capable to open the door latch.
If component is support it, the SUPPORT_OPENING bitmask can be supplied in the supported_features property.

* hound improvements.

* Travis improvements.

* Improvements from review process

* Simplifies is_locked method

* Adds an openable lock in the lock demo component

* removes blank line

* Adds test for openable demo lock and lint and reviewer improvements.

* adds new line...

* Comment end with a period.

* Additional blank line.

* Mock service based testing, lint fixes

* Update description
This commit is contained in:
Patrick Hofmann 2018-03-25 23:25:28 +02:00 committed by Paulus Schoutsen
parent 8a204fd15b
commit 0d48a8eec6
5 changed files with 121 additions and 10 deletions

View file

@ -33,6 +33,7 @@ DISCOVER_SENSORS = 'homematic.sensor'
DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor' DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor'
DISCOVER_COVER = 'homematic.cover' DISCOVER_COVER = 'homematic.cover'
DISCOVER_CLIMATE = 'homematic.climate' DISCOVER_CLIMATE = 'homematic.climate'
DISCOVER_LOCKS = 'homematic.locks'
ATTR_DISCOVER_DEVICES = 'devices' ATTR_DISCOVER_DEVICES = 'devices'
ATTR_PARAM = 'param' ATTR_PARAM = 'param'
@ -59,7 +60,7 @@ SERVICE_SET_INSTALL_MODE = 'set_install_mode'
HM_DEVICE_TYPES = { HM_DEVICE_TYPES = {
DISCOVER_SWITCHES: [ DISCOVER_SWITCHES: [
'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren', 'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren',
'IPSwitchPowermeter', 'KeyMatic', 'HMWIOSwitch', 'Rain', 'EcoLogic'], 'IPSwitchPowermeter', 'HMWIOSwitch', 'Rain', 'EcoLogic'],
DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer'], DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer'],
DISCOVER_SENSORS: [ DISCOVER_SENSORS: [
'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion', 'MotionIP', 'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion', 'MotionIP',
@ -78,7 +79,8 @@ HM_DEVICE_TYPES = {
'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain', 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain',
'WiredSensor', 'PresenceIP'], 'WiredSensor', 'PresenceIP'],
DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'] DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'],
DISCOVER_LOCKS: ['KeyMatic']
} }
HM_IGNORE_DISCOVERY_NODE = [ HM_IGNORE_DISCOVERY_NODE = [
@ -464,7 +466,8 @@ def _system_callback_handler(hass, config, src, *args):
('cover', DISCOVER_COVER), ('cover', DISCOVER_COVER),
('binary_sensor', DISCOVER_BINARY_SENSORS), ('binary_sensor', DISCOVER_BINARY_SENSORS),
('sensor', DISCOVER_SENSORS), ('sensor', DISCOVER_SENSORS),
('climate', DISCOVER_CLIMATE)): ('climate', DISCOVER_CLIMATE),
('lock', DISCOVER_LOCKS)):
# Get all devices of a specific type # Get all devices of a specific type
found_devices = _get_devices( found_devices = _get_devices(
hass, discovery_type, addresses, interface) hass, discovery_type, addresses, interface)

View file

@ -18,7 +18,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED,
STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK) STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK, SERVICE_OPEN)
from homeassistant.components import group from homeassistant.components import group
ATTR_CHANGED_BY = 'changed_by' ATTR_CHANGED_BY = 'changed_by'
@ -39,6 +39,9 @@ LOCK_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_CODE): cv.string, vol.Optional(ATTR_CODE): cv.string,
}) })
# Bitfield of features supported by the lock entity
SUPPORT_OPEN = 1
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PROP_TO_ATTR = { PROP_TO_ATTR = {
@ -78,6 +81,18 @@ def unlock(hass, entity_id=None, code=None):
hass.services.call(DOMAIN, SERVICE_UNLOCK, data) hass.services.call(DOMAIN, SERVICE_UNLOCK, data)
@bind_hass
def open_lock(hass, entity_id=None, code=None):
"""Open all or specified locks."""
data = {}
if code:
data[ATTR_CODE] = code
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_OPEN, data)
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Track states and offer events for locks.""" """Track states and offer events for locks."""
@ -97,6 +112,8 @@ def async_setup(hass, config):
for entity in target_locks: for entity in target_locks:
if service.service == SERVICE_LOCK: if service.service == SERVICE_LOCK:
yield from entity.async_lock(code=code) yield from entity.async_lock(code=code)
elif service.service == SERVICE_OPEN:
yield from entity.async_open(code=code)
else: else:
yield from entity.async_unlock(code=code) yield from entity.async_unlock(code=code)
@ -113,6 +130,9 @@ def async_setup(hass, config):
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_LOCK, async_handle_lock_service, DOMAIN, SERVICE_LOCK, async_handle_lock_service,
schema=LOCK_SERVICE_SCHEMA) schema=LOCK_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_OPEN, async_handle_lock_service,
schema=LOCK_SERVICE_SCHEMA)
return True return True
@ -158,6 +178,17 @@ class LockDevice(Entity):
""" """
return self.hass.async_add_job(ft.partial(self.unlock, **kwargs)) return self.hass.async_add_job(ft.partial(self.unlock, **kwargs))
def open(self, **kwargs):
"""Open the door latch."""
raise NotImplementedError()
def async_open(self, **kwargs):
"""Open the door latch.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(ft.partial(self.open, **kwargs))
@property @property
def state_attributes(self): def state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""

View file

@ -4,7 +4,7 @@ Demo lock platform that has two fake locks.
For more details about this platform, please refer to the documentation For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/ https://home-assistant.io/components/demo/
""" """
from homeassistant.components.lock import LockDevice from homeassistant.components.lock import LockDevice, SUPPORT_OPEN
from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED) from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED)
@ -13,17 +13,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo lock platform.""" """Set up the Demo lock platform."""
add_devices([ add_devices([
DemoLock('Front Door', STATE_LOCKED), DemoLock('Front Door', STATE_LOCKED),
DemoLock('Kitchen Door', STATE_UNLOCKED) DemoLock('Kitchen Door', STATE_UNLOCKED),
DemoLock('Openable Lock', STATE_LOCKED, True)
]) ])
class DemoLock(LockDevice): class DemoLock(LockDevice):
"""Representation of a Demo lock.""" """Representation of a Demo lock."""
def __init__(self, name, state): def __init__(self, name, state, openable=False):
"""Initialize the lock.""" """Initialize the lock."""
self._name = name self._name = name
self._state = state self._state = state
self._openable = openable
@property @property
def should_poll(self): def should_poll(self):
@ -49,3 +51,14 @@ class DemoLock(LockDevice):
"""Unlock the device.""" """Unlock the device."""
self._state = STATE_UNLOCKED self._state = STATE_UNLOCKED
self.schedule_update_ha_state() self.schedule_update_ha_state()
def open(self, **kwargs):
"""Open the door latch."""
self._state = STATE_UNLOCKED
self.schedule_update_ha_state()
@property
def supported_features(self):
"""Flag supported features."""
if self._openable:
return SUPPORT_OPEN

View file

@ -0,0 +1,58 @@
"""
Support for Homematic lock.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/lock.homematic/
"""
import logging
from homeassistant.components.lock import LockDevice, SUPPORT_OPEN
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
from homeassistant.const import STATE_UNKNOWN
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['homematic']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Homematic lock platform."""
if discovery_info is None:
return
devices = []
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
devices.append(HMLock(conf))
add_devices(devices)
class HMLock(HMDevice, LockDevice):
"""Representation of a Homematic lock aka KeyMatic."""
@property
def is_locked(self):
"""Return true if the lock is locked."""
return not bool(self._hm_get_state())
def lock(self, **kwargs):
"""Lock the lock."""
self._hmdevice.lock()
def unlock(self, **kwargs):
"""Unlock the lock."""
self._hmdevice.unlock()
def open(self, **kwargs):
"""Open the door latch."""
self._hmdevice.open()
def _init_data_struct(self):
"""Generate the data dictionary (self._data) from metadata."""
self._state = "STATE"
self._data.update({self._state: STATE_UNKNOWN})
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_OPEN

View file

@ -4,11 +4,10 @@ import unittest
from homeassistant.setup import setup_component from homeassistant.setup import setup_component
from homeassistant.components import lock from homeassistant.components import lock
from tests.common import get_test_home_assistant from tests.common import get_test_home_assistant, mock_service
FRONT = 'lock.front_door' FRONT = 'lock.front_door'
KITCHEN = 'lock.kitchen_door' KITCHEN = 'lock.kitchen_door'
OPENABLE_LOCK = 'lock.openable_lock'
class TestLockDemo(unittest.TestCase): class TestLockDemo(unittest.TestCase):
@ -48,3 +47,10 @@ class TestLockDemo(unittest.TestCase):
self.hass.block_till_done() self.hass.block_till_done()
self.assertFalse(lock.is_locked(self.hass, FRONT)) self.assertFalse(lock.is_locked(self.hass, FRONT))
def test_opening(self):
"""Test the opening of a lock."""
calls = mock_service(self.hass, lock.DOMAIN, lock.SERVICE_OPEN)
lock.open_lock(self.hass, OPENABLE_LOCK)
self.hass.block_till_done()
self.assertEqual(1, len(calls))