Skip to content

Commit

Permalink
Merge 337b71a into 3009e43
Browse files Browse the repository at this point in the history
  • Loading branch information
Adminiuga committed Oct 15, 2019
2 parents 3009e43 + 337b71a commit b8a1bcb
Show file tree
Hide file tree
Showing 21 changed files with 616 additions and 178 deletions.
18 changes: 14 additions & 4 deletions README.md
Expand Up @@ -7,9 +7,12 @@

Zigbee Home Automation integration with zigpy allows you to connect one of many off-the-shelf Zigbee adapters using one of the available Zigbee radio library modules compatible with zigpy to control Zigbee based devices. There is currently support for controlling Zigbee device types such as binary sensors (e.g., motion and door sensors), sensors (e.g., temperature sensors), lightbulbs, switches, and fans. A working implementation of zigbe exist in **[Home Assistant](https://www.home-assistant.io)** (Python based open source home automation software) as part of its **[ZHA component](https://www.home-assistant.io/components/zha/)**

zigpy works with separate radio libraries which can each interface with multiple USB and GPIO radio adapters/modules over different native UART serial protocols. Such radio libraries includes **[bellows](https://github.com/zigpy/bellows)** (which communicates with EZSP/EmberZNet based radios) and **[zigpy-xbee](https://github.com/zigpy/zigpy-xbee)** (which communicates with XBee based Zigbee radios). There are also experimental radio libraries called **[zigpy-deconz](https://github.com/zigpy/zigpy-deconz)** and **[pyconz](https://github.com/Equidamoid/pyconz/)** available for deCONZ serial protocol (for communicating with ConBee and RaspBee USB and GPIO radios from Dresden-Elektronik).
## Compatible hardware

zigpy works with separate radio libraries which can each interface with multiple USB and GPIO radio hardware adapters/modules over different native UART serial protocols. Such radio libraries includes **[bellows](https://github.com/zigpy/bellows)** (which communicates with EZSP/EmberZNet based radios), **[zigpy-xbee](https://github.com/zigpy/zigpy-xbee)** (which communicates with XBee based Zigbee radios), and as **[zigpy-deconz](https://github.com/zigpy/zigpy-deconz)** for deCONZ serial protocol (for communicating with ConBee and RaspBee USB and GPIO radios from Dresden-Elektronik). There are also an experimental radio library called **[zigpy-zigate](https://github.com/doudz/zigpy-zigate)** for communicating with ZiGate based radios.

### Known working Zigbee radio modules

**Known working Zigbee radio modules:**
- EmberZNet based radios using the EZSP protocol (via the [bellows](https://github.com/zigpy/bellows) library for zigpy)
- [Nortek GoControl QuickStick Combo Model HUSBZB-1 (Z-Wave & Zigbee USB Adapter)](https://www.nortekcontrol.com/products/2gig/husbzb-1-gocontrol-quickstick-combo/)
- [Elelabs Zigbee USB Adapter](https://elelabs.com/products/elelabs_usb_adapter.html)
Expand All @@ -23,13 +26,20 @@ zigpy works with separate radio libraries which can each interface with multiple
- [ConBee](https://www.dresden-elektronik.de/conbee/) USB radio adapter from [Dresden-Elektronik](https://www.dresden-elektronik.de)
- [RaspBee](https://www.dresden-elektronik.de/raspbee/) GPIO radio adapter from [Dresden-Elektronik](https://www.dresden-elektronik.de)

**Release packages available via PyPI:**
### Experimental Zigbee radio modules
- ZiGate based radios (via the [zigpy-zigate](https://github.com/doudz/zigpy-zigate) library for zigpy)
- [ZiGate open source ZigBee adapter hardware](https://zigate.fr/)

## Release packages available via PyPI

Packages of tagged versions are also released via PyPI
- https://pypi.org/project/zigpy-homeassistant/
- https://pypi.org/project/bellows-homeassistant/
- https://pypi.org/project/zigpy-xbee-homeassistant/
- https://pypi.org/project/zigpy-deconz/
- https://pypi.org/project/zigpy-zigate/

**Related projects:**
## Related projects

### ZHA Device Handlers
ZHA deviation handling in Home Assistant relies on on the third-party [ZHA Device Handlers](https://github.com/dmulcahey/zha-device-handlers) project. Zigbee devices that deviate from or do not fully conform to the standard specifications set by the [Zigbee Alliance](https://www.zigbee.org) may require the development of custom [ZHA Device Handlers](https://github.com/dmulcahey/zha-device-handlers) (ZHA custom quirks handler implementation) to for all their functions to work properly with the ZHA component in Home Assistant. These ZHA Device Handlers for Home Assistant can thus be used to parse custom messages to and from non-compliant Zigbee devices. The custom quirks implementations for zigpy implemented as ZHA Device Handlers for Home Assistant are a similar concept to that of [Hub-connected Device Handlers for the SmartThings Classics platform](https://docs.smartthings.com/en/latest/device-type-developers-guide/) as well as that of [Zigbee-Shepherd Converters as used by Zigbee2mqtt](https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html), meaning they are each virtual representations of a physical device that expose additional functionality that is not provided out-of-the-box by the existing integration between these platforms.
2 changes: 1 addition & 1 deletion tests/test_appdb.py
Expand Up @@ -74,7 +74,7 @@ async def test_database(tmpdir, monkeypatch):
clus._update_attribute(4, bytes("Custom", "ascii"))
clus._update_attribute(5, bytes("Model", "ascii"))
clus.listener_event("cluster_command", 0)
clus.listener_event("zdo_command")
clus.listener_event("general_command")

# Test a CustomDevice
custom_ieee = make_ieee(1)
Expand Down
14 changes: 14 additions & 0 deletions tests/test_application.py
Expand Up @@ -250,3 +250,17 @@ def test_get_dst_address(app):
r = app.get_dst_address(mock.MagicMock())
assert r.addrmode == 3
assert r.endpoint == 1


@pytest.mark.asyncio
async def test_update_network(app):
with pytest.raises(NotImplementedError):
await app.update_network()


def test_props(app):
assert app.channel is None
assert app.channels is None
assert app.extended_pan_id is None
assert app.pan_id is None
assert app.nwk_update_id is None
26 changes: 15 additions & 11 deletions tests/test_device.py
Expand Up @@ -117,14 +117,10 @@ def test_handle_message_no_endpoint(dev):

def test_handle_message(dev):
ep = dev.add_endpoint(3)
dev.deserialize = mock.MagicMock(
return_value=[
mock.sentinel.tsn,
mock.sentinel.cmd_id,
mock.sentinel.is_reply,
mock.sentinel.args,
]
)
hdr = mock.MagicMock()
hdr.tsn = mock.sentinel.tsn
hdr.is_reply = mock.sentinel.is_reply
dev.deserialize = mock.MagicMock(return_value=[hdr, mock.sentinel.args])
ep.handle_message = mock.MagicMock()
dev.handle_message(99, 98, 3, 3, b"abcd")
assert ep.handle_message.call_count == 1
Expand All @@ -136,11 +132,19 @@ def test_handle_message_reply(dev):
tsn = mock.sentinel.tsn
req_mock = mock.MagicMock()
dev._pending[tsn] = req_mock
hdr_1 = mock.MagicMock()
hdr_1.tsn = tsn
hdr_1.command_id = mock.sentinel.command_id
hdr_1.is_reply = True
hdr_2 = mock.MagicMock()
hdr_2.tsn = mock.sentinel.another_tsn
hdr_2.command_id = mock.sentinel.command_id
hdr_2.is_reply = True
dev.deserialize = mock.MagicMock(
side_effect=(
(tsn, mock.sentinel.cmd_id, True, mock.sentinel.args),
(mock.sentinel.another_tsn, mock.sentinel.cmd_id, True, mock.sentinel.args),
(tsn, mock.sentinel.cmd_id, True, mock.sentinel.args),
(hdr_1, mock.sentinel.args),
(hdr_2, mock.sentinel.args),
(hdr_1, mock.sentinel.args),
)
)
dev.handle_message(99, 98, 3, 3, b"abcd")
Expand Down
28 changes: 23 additions & 5 deletions tests/test_endpoint.py
Expand Up @@ -115,19 +115,21 @@ def test_multiple_add_output_cluster(ep):
def test_handle_message(ep):
c = ep.add_input_cluster(0)
c.handle_message = mock.MagicMock()
ep.handle_message(0, 0, 0, 1, [])
c.handle_message.assert_called_once_with(0, 1, [])
ep.handle_message(mock.sentinel.profile, 0, mock.sentinel.hdr, mock.sentinel.data)
c.handle_message.assert_called_once_with(mock.sentinel.hdr, mock.sentinel.data)


def test_handle_message_output(ep):
c = ep.add_output_cluster(0)
c.handle_message = mock.MagicMock()
ep.handle_message(0, 0, 0, 1, [])
c.handle_message.assert_called_once_with(0, 1, [])
ep.handle_message(mock.sentinel.profile, 0, mock.sentinel.hdr, mock.sentinel.data)
c.handle_message.assert_called_once_with(mock.sentinel.hdr, mock.sentinel.data)


def test_handle_request_unknown(ep):
ep.handle_message(0, 99, 0, 0, [])
hdr = mock.MagicMock()
hdr.command_id = mock.sentinel.command_id
ep.handle_message(mock.sentinel.profile, 99, hdr, mock.sentinel.args)


def test_cluster_attr(ep):
Expand Down Expand Up @@ -159,6 +161,22 @@ def test_reply(ep):
assert ep._device.reply.call_count == 1


def test_reply_change_profile_id(ep):
ep.profile_id = 49246
ep.reply(0x1000, 8, b"", 0x3F)
assert ep._device.reply.call_count == 1
assert ep._device.reply.call_args[0][0] == ep.profile_id

ep.reply(0x1000, 8, b"", 0x40)
assert ep._device.reply.call_count == 2
assert ep._device.reply.call_args[0][0] == 0x0104

ep.profile_id = 0xBEEF
ep.reply(0x1000, 8, b"", 0x40)
assert ep._device.reply.call_count == 3
assert ep._device.reply.call_args[0][0] == ep.profile_id


def _mk_rar(attrid, value, status=0):
r = zcl.foundation.ReadAttributeRecord()
r.attrid = attrid
Expand Down
66 changes: 66 additions & 0 deletions tests/test_types.py
Expand Up @@ -232,3 +232,69 @@ def test_optional():
d, r = t.Optional(t.uint8_t).deserialize(b"\x001234aaa")
assert d == 0
assert r == b"1234aaa"


def test_nodata():
"""Test No Data ZCL data type."""
data = b"\xaa\x55\xbb"
r, rest = t.NoData.deserialize(data)
assert isinstance(r, t.NoData)
assert rest == data

assert t.NoData().serialize() == b""


def test_date():
"""Test Date ZCL data type."""
year = t.uint8_t(70)
month = t.uint8_t(1)
day = t.uint8_t(1)
dow = t.uint8_t(4)

data = year.serialize() + month.serialize() + day.serialize() + dow.serialize()
extra = b"\xaa\x55"

r, rest = t.Date.deserialize(data + extra)
assert rest == extra
assert r._year == 70
assert r.year == 1970
assert r.month == 1
assert r.day == 1
assert r.day_of_week == 4

assert r.serialize() == data
r.year = 2020
assert r.serialize()[0] == 2020 - 1900
assert t.Date().year is None


def test_eui64():
"""Test EUI64."""
data = b"\x01\x02\x03\x04\x05\x06\x07\x08"
extra = b"\xaa\x55"

ieee, rest = t.EUI64.deserialize(data + extra)
assert ieee[0] == 1
assert ieee[1] == 2
assert ieee[2] == 3
assert ieee[3] == 4
assert ieee[4] == 5
assert ieee[5] == 6
assert ieee[6] == 7
assert ieee[7] == 8
assert rest == extra
assert ieee.serialize() == data


def test_eui64_convert():
ieee = t.EUI64.convert("08:07:06:05:04:03:02:01")
assert ieee[0] == 1
assert ieee[1] == 2
assert ieee[2] == 3
assert ieee[3] == 4
assert ieee[4] == 5
assert ieee[5] == 6
assert ieee[6] == 7
assert ieee[7] == 8

assert t.EUI64.convert(None) is None
101 changes: 74 additions & 27 deletions tests/test_zcl.py
Expand Up @@ -17,31 +17,37 @@ def endpoint():


def test_deserialize_general(endpoint):
tsn, command_id, is_reply, args = endpoint.deserialize(0, b"\x00\x01\x00")
assert tsn == 1
assert command_id == 0
assert is_reply is False
hdr, args = endpoint.deserialize(0, b"\x00\x01\x00")
assert hdr.tsn == 1
assert hdr.command_id == 0
assert hdr.is_reply is False


def test_deserialize_general_unknown(endpoint):
tsn, command_id, is_reply, args = endpoint.deserialize(0, b"\x00\x01\xff")
assert tsn == 1
assert command_id == 255
assert is_reply is False
hdr, args = endpoint.deserialize(0, b"\x00\x01\xff")
assert hdr.tsn == 1
assert hdr.frame_control.is_general is True
assert hdr.frame_control.is_cluster is False
assert hdr.command_id == 255
assert hdr.is_reply is False


def test_deserialize_cluster(endpoint):
tsn, command_id, is_reply, args = endpoint.deserialize(0, b"\x01\x01\x00xxx")
assert tsn == 1
assert command_id == 256
assert is_reply is False
hdr, args = endpoint.deserialize(0, b"\x01\x01\x00xxx")
assert hdr.tsn == 1
assert hdr.frame_control.is_general is False
assert hdr.frame_control.is_cluster is True
assert hdr.command_id == 0
assert hdr.is_reply is False


def test_deserialize_cluster_client(endpoint):
tsn, command_id, is_reply, args = endpoint.deserialize(3, b"\x09\x01\x00AB")
assert tsn == 1
assert command_id == 256
assert is_reply is True
hdr, args = endpoint.deserialize(3, b"\x09\x01\x00AB")
assert hdr.tsn == 1
assert hdr.frame_control.is_general is False
assert hdr.frame_control.is_cluster is True
assert hdr.command_id == 0
assert hdr.is_reply is True
assert args == [0x4241]


Expand All @@ -51,10 +57,10 @@ def test_deserialize_cluster_unknown(endpoint):


def test_deserialize_cluster_command_unknown(endpoint):
tsn, command_id, is_reply, args = endpoint.deserialize(0, b"\x01\x01\xff")
assert tsn == 1
assert command_id == 255 + 256
assert is_reply is False
hdr, args = endpoint.deserialize(0, b"\x01\x01\xff")
assert hdr.tsn == 1
assert hdr.command_id == 255
assert hdr.is_reply is False


def test_unknown_cluster():
Expand Down Expand Up @@ -152,20 +158,49 @@ def test_attribute_report(cluster):
attr.attrid = 4
attr.value = zcl.foundation.TypeValue()
attr.value.value = 1
cluster.handle_message(0, 0x0A, [[attr]])
hdr = mock.MagicMock(auto_spec=foundation.ZCLHeader)
hdr.command_id = foundation.Command.Report_Attributes
hdr.frame_control.is_general = True
hdr.frame_control.is_cluster = False
cluster.handle_message(hdr, [[attr]])
assert cluster._attr_cache[4] == 1


def test_handle_request_unknown(cluster):
cluster.handle_message(0, 0xFF, [])
hdr = mock.MagicMock(auto_spec=foundation.ZCLHeader)
hdr.command_id = mock.sentinel.command_id
hdr.frame_control.is_general = True
hdr.frame_control.is_cluster = False
cluster.listener_event = mock.MagicMock()
cluster._update_attribute = mock.MagicMock()
cluster.handle_cluster_general_request = mock.MagicMock()
cluster.handle_cluster_request = mock.MagicMock()
cluster.handle_message(hdr, mock.sentinel.args)

assert cluster.listener_event.call_count == 1
assert cluster.listener_event.call_args[0][0] == "general_command"
assert cluster._update_attribute.call_count == 0
assert cluster.handle_cluster_general_request.call_count == 1
assert cluster.handle_cluster_request.call_count == 0


def test_handle_cluster_request(cluster):
cluster.handle_message(0, 256, [])


def test_handle_unexpected_reply(cluster):
cluster.handle_message(0, 0, [])
hdr = mock.MagicMock(auto_spec=foundation.ZCLHeader)
hdr.command_id = mock.sentinel.command_id
hdr.frame_control.is_general = False
hdr.frame_control.is_cluster = True
hdr.command_id.is_general = False
cluster.listener_event = mock.MagicMock()
cluster._update_attribute = mock.MagicMock()
cluster.handle_cluster_general_request = mock.MagicMock()
cluster.handle_cluster_request = mock.MagicMock()
cluster.handle_message(hdr, mock.sentinel.args)

assert cluster.listener_event.call_count == 1
assert cluster.listener_event.call_args[0][0] == "cluster_command"
assert cluster._update_attribute.call_count == 0
assert cluster.handle_cluster_general_request.call_count == 0
assert cluster.handle_cluster_request.call_count == 1


def _mk_rar(attrid, value, status=0):
Expand Down Expand Up @@ -451,3 +486,15 @@ def test_general_command_reply(cluster):
cluster.reply.assert_called_with(
True, cmd_id, mock.ANY, True, [], manufacturer=0x4567, tsn=mock.sentinel.tsn
)


def test_handle_cluster_request_handler(cluster):
cluster.handle_cluster_request(
mock.sentinel.tsn, mock.sentinel.command_id, mock.sentinel.args
)


def test_handle_cluster_general_request(cluster):
cluster.handle_cluster_general_request(
mock.sentinel.tsn, mock.sentinel.command_id, mock.sentinel.args
)
8 changes: 8 additions & 0 deletions tests/test_zcl_foundation.py
Expand Up @@ -298,3 +298,11 @@ def test_frame_header_cluster():
hdr.manufacturer = None
assert hdr.manufacturer is None
assert hdr.frame_control.is_manufacturer_specific is False


def test_data_types():
"""Test data types mappings."""
assert len(foundation.DATA_TYPES) == len(foundation.DATA_TYPE_IDX)
data_types_set = set([d[1] for d in foundation.DATA_TYPES.values()])
dt_2_id_set = set(foundation.DATA_TYPE_IDX.keys())
assert data_types_set == dt_2_id_set

0 comments on commit b8a1bcb

Please sign in to comment.