Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 30 additions & 21 deletions homeassistant/components/lock/zwave.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
"""
# Because we do not compile openzwave on CI
# pylint: disable=import-error
import asyncio
import logging
from os import path

import voluptuous as vol

from homeassistant.components.lock import DOMAIN, LockDevice
from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv

Expand Down Expand Up @@ -53,7 +53,7 @@
'9': 'Deadbolt Jammed',
'18': 'Locked with Keypad by user ',
'19': 'Unlocked with Keypad by user ',
'21': 'Manually Locked by',
'21': 'Manually Locked by ',
'22': 'Manually Unlocked by Key or Inside thumb turn',
'24': 'Locked by RF',
'25': 'Unlocked by RF',
Expand Down Expand Up @@ -120,8 +120,12 @@
})


def get_device(hass, node, values, **kwargs):
"""Create zwave entity device."""
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Generic Z-Wave platform setup."""
yield from zwave.async_setup_platform(
hass, config, async_add_devices, discovery_info)

descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))

Expand All @@ -140,6 +144,7 @@ def set_usercode(service):
_LOGGER.error('Invalid code provided: (%s)'
' usercode must %s or less digits',
usercode, len(value.data))
break
value.data = str(usercode)
break

Expand Down Expand Up @@ -175,22 +180,25 @@ def clear_usercode(service):
_LOGGER.info('Usercode at slot %s is cleared', value.index)
break

if node.has_command_class(zwave.const.COMMAND_CLASS_USER_CODE):
hass.services.register(DOMAIN,
SERVICE_SET_USERCODE,
set_usercode,
descriptions.get(SERVICE_SET_USERCODE),
schema=SET_USERCODE_SCHEMA)
hass.services.register(DOMAIN,
SERVICE_GET_USERCODE,
get_usercode,
descriptions.get(SERVICE_GET_USERCODE),
schema=GET_USERCODE_SCHEMA)
hass.services.register(DOMAIN,
SERVICE_CLEAR_USERCODE,
clear_usercode,
descriptions.get(SERVICE_CLEAR_USERCODE),
schema=CLEAR_USERCODE_SCHEMA)
hass.services.async_register(DOMAIN,
SERVICE_SET_USERCODE,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line under-indented for visual indent

set_usercode,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line under-indented for visual indent

descriptions.get(SERVICE_SET_USERCODE),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line under-indented for visual indent

schema=SET_USERCODE_SCHEMA)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line under-indented for visual indent

hass.services.async_register(DOMAIN,
SERVICE_GET_USERCODE,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line under-indented for visual indent

get_usercode,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line under-indented for visual indent

descriptions.get(SERVICE_GET_USERCODE),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line under-indented for visual indent

schema=GET_USERCODE_SCHEMA)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line under-indented for visual indent

hass.services.async_register(DOMAIN,
SERVICE_CLEAR_USERCODE,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line under-indented for visual indent

clear_usercode,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line under-indented for visual indent

descriptions.get(SERVICE_CLEAR_USERCODE),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line under-indented for visual indent

schema=CLEAR_USERCODE_SCHEMA)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line under-indented for visual indent



def get_device(node, values, **kwargs):
"""Create zwave entity device."""
return ZwaveLock(values)


Expand Down Expand Up @@ -253,7 +261,8 @@ def update_properties(self):
self._lock_status = '{}{}'.format(
LOCK_ALARM_TYPE.get(str(alarm_type)),
MANUAL_LOCK_ALARM_LEVEL.get(str(alarm_level)))
if alarm_type in ALARM_TYPE_STD:
return
if str(alarm_type) in ALARM_TYPE_STD:
self._lock_status = '{}{}'.format(
LOCK_ALARM_TYPE.get(str(alarm_type)), str(alarm_level))
return
Expand Down
271 changes: 271 additions & 0 deletions tests/components/lock/test_zwave.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
"""Test Z-Wave locks."""
import asyncio

from unittest.mock import patch, MagicMock

from homeassistant.components.lock import zwave
from homeassistant.components.zwave import const

from tests.mock.zwave import (
MockNode, MockValue, MockEntityValues, value_changed)


def test_get_device_detects_lock(mock_openzwave):
"""Test get_device returns a Z-Wave lock."""
node = MockNode()
values = MockEntityValues(
primary=MockValue(data=None, node=node),
access_control=None,
alarm_type=None,
alarm_level=None,
)

device = zwave.get_device(node=node, values=values, node_config={})
assert isinstance(device, zwave.ZwaveLock)


def test_lock_turn_on_and_off(mock_openzwave):
"""Test turning on a Z-Wave lock."""
node = MockNode()
values = MockEntityValues(
primary=MockValue(data=None, node=node),
access_control=None,
alarm_type=None,
alarm_level=None,
)
device = zwave.get_device(node=node, values=values, node_config={})

assert not values.primary.data

device.lock()
assert values.primary.data

device.unlock()
assert not values.primary.data


def test_lock_value_changed(mock_openzwave):
"""Test value changed for Z-Wave lock."""
node = MockNode()
values = MockEntityValues(
primary=MockValue(data=None, node=node),
access_control=None,
alarm_type=None,
alarm_level=None,
)
device = zwave.get_device(node=node, values=values, node_config={})

assert not device.is_locked

values.primary.data = True
value_changed(values.primary)

assert device.is_locked


def test_v2btze_value_changed(mock_openzwave):
"""Test value changed for v2btze Z-Wave lock."""
node = MockNode(manufacturer_id='010e', product_id='0002')
values = MockEntityValues(
primary=MockValue(data=None, node=node),
v2btze_advanced=MockValue(data='Advanced', node=node),
access_control=MockValue(data=19, node=node),
alarm_type=None,
alarm_level=None,
)
device = zwave.get_device(node=node, values=values, node_config={})
assert device._v2btze

assert not device.is_locked

values.access_control.data = 24
value_changed(values.primary)

assert device.is_locked


def test_lock_access_control(mock_openzwave):
"""Test access control for Z-Wave lock."""
node = MockNode()
values = MockEntityValues(
primary=MockValue(data=None, node=node),
access_control=MockValue(data=11, node=node),
alarm_type=None,
alarm_level=None,
)
device = zwave.get_device(node=node, values=values, node_config={})

assert device.device_state_attributes[zwave.ATTR_NOTIFICATION] == \
'Lock Jammed'


def test_lock_alarm_type(mock_openzwave):
"""Test alarm type for Z-Wave lock."""
node = MockNode()
values = MockEntityValues(
primary=MockValue(data=None, node=node),
access_control=None,
alarm_type=MockValue(data=None, node=node),
alarm_level=None,
)
device = zwave.get_device(node=node, values=values, node_config={})

assert zwave.ATTR_LOCK_STATUS not in device.device_state_attributes

values.alarm_type.data = 21
value_changed(values.alarm_type)
assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \
'Manually Locked by None'

values.alarm_type.data = 18
value_changed(values.alarm_type)
assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \
'Locked with Keypad by user None'

values.alarm_type.data = 161
value_changed(values.alarm_type)
assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \
'Tamper Alarm: None'

values.alarm_type.data = 9
value_changed(values.alarm_type)
assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \
'Deadbolt Jammed'


def test_lock_alarm_level(mock_openzwave):
"""Test alarm level for Z-Wave lock."""
node = MockNode()
values = MockEntityValues(
primary=MockValue(data=None, node=node),
access_control=None,
alarm_type=MockValue(data=None, node=node),
alarm_level=MockValue(data=None, node=node),
)
device = zwave.get_device(node=node, values=values, node_config={})

assert zwave.ATTR_LOCK_STATUS not in device.device_state_attributes

values.alarm_type.data = 21
values.alarm_level.data = 1
value_changed(values.alarm_type)
value_changed(values.alarm_level)
assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \
'Manually Locked by Key Cylinder or Inside thumb turn'

values.alarm_type.data = 18
values.alarm_level.data = 'alice'
value_changed(values.alarm_type)
value_changed(values.alarm_level)
assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \
'Locked with Keypad by user alice'

values.alarm_type.data = 161
values.alarm_level.data = 1
value_changed(values.alarm_type)
value_changed(values.alarm_level)
assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \
'Tamper Alarm: Too many keypresses'


@asyncio.coroutine
def test_lock_set_usercode_service(hass, mock_openzwave):
"""Test the zwave lock set_usercode service."""
node = MockNode(node_id=12)
value0 = MockValue(data=None, node=node, index=0)
value1 = MockValue(data=None, node=node, index=1)
yield from zwave.async_setup_platform(
hass, {}, MagicMock())

node.get_values.return_value = {
value0.value_id: value0,
value1.value_id: value1,
}

with patch.object(zwave.zwave, 'NETWORK') as mock_network:
mock_network.nodes = {
node.node_id: node
}
yield from hass.services.async_call(
zwave.DOMAIN, zwave.SERVICE_SET_USERCODE, {
const.ATTR_NODE_ID: node.node_id,
zwave.ATTR_USERCODE: '1234',
zwave.ATTR_CODE_SLOT: 1,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line missing indentation or outdented

yield from hass.async_block_till_done()

assert value1.data == '1234'

with patch.object(zwave.zwave, 'NETWORK') as mock_network:
mock_network.nodes = {
node.node_id: node
}
yield from hass.services.async_call(
zwave.DOMAIN, zwave.SERVICE_SET_USERCODE, {
const.ATTR_NODE_ID: node.node_id,
zwave.ATTR_USERCODE: '12345',
zwave.ATTR_CODE_SLOT: 1,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line missing indentation or outdented

yield from hass.async_block_till_done()

assert value1.data == '1234'


@asyncio.coroutine
def test_lock_get_usercode_service(hass, mock_openzwave):
"""Test the zwave lock get_usercode service."""
node = MockNode(node_id=12)
value0 = MockValue(data=None, node=node, index=0)
value1 = MockValue(data='1234', node=node, index=1)
yield from zwave.async_setup_platform(
hass, {}, MagicMock())

node.get_values.return_value = {
value0.value_id: value0,
value1.value_id: value1,
}

with patch.object(zwave.zwave, 'NETWORK') as mock_network:
with patch.object(zwave, '_LOGGER') as mock_logger:
mock_network.nodes = {
node.node_id: node
}
yield from hass.services.async_call(
zwave.DOMAIN, zwave.SERVICE_GET_USERCODE, {
const.ATTR_NODE_ID: node.node_id,
zwave.ATTR_CODE_SLOT: 1,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line missing indentation or outdented

yield from hass.async_block_till_done()

# This service only seems to write to the log
assert mock_logger.info.called
assert len(mock_logger.info.mock_calls) == 1
assert mock_logger.info.mock_calls[0][1][2] == '1234'


@asyncio.coroutine
def test_lock_clear_usercode_service(hass, mock_openzwave):
"""Test the zwave lock clear_usercode service."""
node = MockNode(node_id=12)
value0 = MockValue(data=None, node=node, index=0)
value1 = MockValue(data='123', node=node, index=1)
yield from zwave.async_setup_platform(
hass, {}, MagicMock())

node.get_values.return_value = {
value0.value_id: value0,
value1.value_id: value1,
}

with patch.object(zwave.zwave, 'NETWORK') as mock_network:
mock_network.nodes = {
node.node_id: node
}
yield from hass.services.async_call(
zwave.DOMAIN, zwave.SERVICE_CLEAR_USERCODE, {
const.ATTR_NODE_ID: node.node_id,
zwave.ATTR_CODE_SLOT: 1,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continuation line missing indentation or outdented

yield from hass.async_block_till_done()

assert value1.data == '\0\0\0'
2 changes: 1 addition & 1 deletion tests/mock/zwave.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def __init__(self, *,
self.data_items = data_items
self.node = node
self.instance = instance
self.index = 0
self.index = index
self.command_class = command_class
self.units = units
if value_id is None:
Expand Down