Skip to content

Commit d1756a6

Browse files
authored
Merge pull request #35 from zigpy/new-zigpy-init/dev
New zigpy init/dev
2 parents 7c4f7c3 + 68896ac commit d1756a6

File tree

10 files changed

+171
-49
lines changed

10 files changed

+171
-49
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def is_raspberry_pi(raise_on_errors=False):
4949

5050

5151
requires = ['pyserial-asyncio',
52-
'zigpy-homeassistant>=0.10.0', # https://github.com/zigpy/zigpy/issues/190
52+
'zigpy>=0.20.1.a3',
5353
]
5454
if is_raspberry_pi():
5555
requires.append('RPi.GPIO')

tests/test_api.py

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
from unittest import mock
1+
import asyncio
22

33
import pytest
4+
import serial
45
import serial_asyncio
6+
from asynctest import CoroutineMock, mock
7+
8+
import zigpy_zigate.config as config
9+
import zigpy_zigate.uart
510
from zigpy_zigate import api as zigate_api
611

12+
DEVICE_CONFIG = config.SCHEMA_DEVICE({config.CONF_DEVICE_PATH: "/dev/null"})
13+
714

815
@pytest.fixture
916
def api():
10-
api = zigate_api.ZiGate()
17+
api = zigate_api.ZiGate(DEVICE_CONFIG)
1118
api._uart = mock.MagicMock()
1219
return api
1320

@@ -19,19 +26,68 @@ def test_set_application(api):
1926

2027
@pytest.mark.asyncio
2128
async def test_connect(monkeypatch):
22-
api = zigate_api.ZiGate()
23-
portmock = mock.MagicMock()
29+
api = zigate_api.ZiGate(DEVICE_CONFIG)
2430

2531
async def mock_conn(loop, protocol_factory, **kwargs):
2632
protocol = protocol_factory()
2733
loop.call_soon(protocol.connection_made, None)
2834
return None, protocol
29-
monkeypatch.setattr(serial_asyncio, 'create_serial_connection', mock_conn)
3035

31-
await api.connect(portmock, 115200)
36+
monkeypatch.setattr(serial_asyncio, "create_serial_connection", mock_conn)
37+
38+
await api.connect()
3239

3340

3441
def test_close(api):
3542
api._uart.close = mock.MagicMock()
43+
uart = api._uart
3644
api.close()
37-
assert api._uart.close.call_count == 1
45+
assert uart.close.call_count == 1
46+
assert api._uart is None
47+
48+
49+
@pytest.mark.asyncio
50+
@mock.patch.object(zigpy_zigate.uart, "connect")
51+
async def test_api_new(conn_mck):
52+
"""Test new class method."""
53+
api = await zigate_api.ZiGate.new(DEVICE_CONFIG, mock.sentinel.application)
54+
assert isinstance(api, zigate_api.ZiGate)
55+
assert conn_mck.call_count == 1
56+
assert conn_mck.await_count == 1
57+
58+
59+
@pytest.mark.asyncio
60+
@mock.patch.object(zigate_api.ZiGate, "set_raw_mode", new_callable=CoroutineMock)
61+
@mock.patch.object(zigpy_zigate.uart, "connect")
62+
async def test_probe_success(mock_connect, mock_raw_mode):
63+
"""Test device probing."""
64+
65+
res = await zigate_api.ZiGate.probe(DEVICE_CONFIG)
66+
assert res is True
67+
assert mock_connect.call_count == 1
68+
assert mock_connect.await_count == 1
69+
assert mock_connect.call_args[0][0] == DEVICE_CONFIG
70+
assert mock_raw_mode.call_count == 1
71+
assert mock_connect.return_value.close.call_count == 1
72+
73+
74+
@pytest.mark.asyncio
75+
@mock.patch.object(zigate_api.ZiGate, "set_raw_mode", side_effect=asyncio.TimeoutError)
76+
@mock.patch.object(zigpy_zigate.uart, "connect")
77+
@pytest.mark.parametrize(
78+
"exception",
79+
(asyncio.TimeoutError, serial.SerialException, zigate_api.NoResponseError),
80+
)
81+
async def test_probe_fail(mock_connect, mock_raw_mode, exception):
82+
"""Test device probing fails."""
83+
84+
mock_raw_mode.side_effect = exception
85+
mock_connect.reset_mock()
86+
mock_raw_mode.reset_mock()
87+
res = await zigate_api.ZiGate.probe(DEVICE_CONFIG)
88+
assert res is False
89+
assert mock_connect.call_count == 1
90+
assert mock_connect.await_count == 1
91+
assert mock_connect.call_args[0][0] == DEVICE_CONFIG
92+
assert mock_raw_mode.call_count == 1
93+
assert mock_connect.return_value.close.call_count == 1

tests/test_application.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,30 @@
33
import pytest
44
import zigpy.types as zigpy_types
55

6+
import zigpy_zigate.config as config
67
import zigpy_zigate.types as t
78
import zigpy_zigate.zigbee.application
89

10+
APP_CONFIG = zigpy_zigate.zigbee.application.ControllerApplication.SCHEMA(
11+
{
12+
config.CONF_DEVICE: {config.CONF_DEVICE_PATH: "/dev/null"},
13+
config.CONF_DATABASE: None,
14+
}
15+
)
16+
917

1018
@pytest.fixture
1119
def app():
12-
api = mock.MagicMock()
13-
return zigpy_zigate.zigbee.application.ControllerApplication(api)
20+
return zigpy_zigate.zigbee.application.ControllerApplication(APP_CONFIG)
1421

1522

1623
def test_zigpy_ieee(app):
1724
cluster = mock.MagicMock()
1825
cluster.cluster_id = 0x0000
19-
data = b'\x01\x02\x03\x04\x05\x06\x07\x08'
26+
data = b"\x01\x02\x03\x04\x05\x06\x07\x08"
2027

2128
zigate_ieee, _ = t.EUI64.deserialize(data)
2229
app._ieee = zigpy_types.EUI64(zigate_ieee)
2330

2431
dst_addr = app.get_dst_address(cluster)
25-
assert dst_addr.serialize() == b'\x03' + data[::-1] + b'\x01'
32+
assert dst_addr.serialize() == b"\x03" + data[::-1] + b"\x01"

tests/test_uart.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
33
import pytest
44
import serial_asyncio
55

6+
import zigpy_zigate.config
67
from zigpy_zigate import uart
78

9+
DEVICE_CONFIG = zigpy_zigate.config.SCHEMA_DEVICE(
10+
{zigpy_zigate.config.CONF_DEVICE_PATH: "/dev/null"}
11+
)
12+
813

914
@pytest.fixture
1015
def gw():
@@ -16,15 +21,14 @@ def gw():
1621
@pytest.mark.asyncio
1722
async def test_connect(monkeypatch):
1823
api = mock.MagicMock()
19-
portmock = mock.MagicMock()
2024

2125
async def mock_conn(loop, protocol_factory, **kwargs):
2226
protocol = protocol_factory()
2327
loop.call_soon(protocol.connection_made, None)
2428
return None, protocol
2529
monkeypatch.setattr(serial_asyncio, 'create_serial_connection', mock_conn)
2630

27-
await uart.connect(portmock, 115200, api)
31+
await uart.connect(DEVICE_CONFIG, api)
2832

2933

3034
def test_send(gw):

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ setenv = PYTHONPATH = {toxinidir}
1212
install_command = pip install {opts} {packages}
1313
commands = py.test --cov --cov-report=
1414
deps =
15+
asynctest
1516
coveralls
1617
pytest
1718
pytest-cov

zigpy_zigate/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
MAJOR_VERSION = 0
2-
MINOR_VERSION = 5
3-
PATCH_VERSION = '1'
2+
MINOR_VERSION = 6
3+
PATCH_VERSION = '0'
44
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
55
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)

zigpy_zigate/api.py

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
import logging
21
import asyncio
32
import binascii
43
import functools
4+
import logging
5+
from typing import Any, Dict
6+
7+
import serial
8+
import zigpy.exceptions
9+
10+
import zigpy_zigate.config
11+
import zigpy_zigate.uart
512

6-
from . import uart
713
from . import types as t
814

915
LOGGER = logging.getLogger(__name__)
1016

11-
COMMAND_TIMEOUT = 3.0
12-
ZIGATE_BAUDRATE = 115200
17+
COMMAND_TIMEOUT = 1.5
1318

1419
RESPONSES = {
1520
0x004D: (t.NWK, t.EUI64, t.uint8_t),
@@ -34,25 +39,35 @@
3439
}
3540

3641

37-
class NoResponseError(Exception):
42+
class NoResponseError(zigpy.exceptions.APIException):
3843
pass
3944

4045

4146
class ZiGate:
42-
def __init__(self):
47+
def __init__(self, device_config: Dict[str, Any]):
48+
self._app = None
49+
self._config = device_config
4350
self._uart = None
44-
self._callbacks = {}
4551
self._awaiting = {}
4652
self._status_awaiting = {}
4753

4854
self.network_state = None
4955

50-
async def connect(self, device, baudrate=ZIGATE_BAUDRATE):
56+
@classmethod
57+
async def new(cls, config: Dict[str, Any], application=None) -> "ZiGate":
58+
api = cls(config)
59+
await api.connect()
60+
api.set_application(application)
61+
return api
62+
63+
async def connect(self):
5164
assert self._uart is None
52-
self._uart = await uart.connect(device, ZIGATE_BAUDRATE, self)
65+
self._uart = await zigpy_zigate.uart.connect(self._config, self)
5366

5467
def close(self):
55-
return self._uart.close()
68+
if self._uart:
69+
self._uart.close()
70+
self._uart = None
5671

5772
def set_application(self, app):
5873
self._app = app
@@ -141,19 +156,37 @@ async def raw_aps_data_request(self, addr, src_ep, dst_ep, profile,
141156
security, radius, payload], COMMANDS[0x0530])
142157
return await self.command(0x0530, data)
143158

144-
def add_callback(self, cb):
145-
id_ = hash(cb)
146-
while id_ in self._callbacks:
147-
id_ += 1
148-
self._callbacks[id_] = cb
149-
return id_
150-
151-
def remove_callback(self, id_):
152-
return self._callbacks.pop(id_)
153-
154159
def handle_callback(self, *args):
155-
for handler in self._callbacks.values():
160+
"""run application callback handler"""
161+
if self._app:
156162
try:
157-
handler(*args)
163+
self._app.zigate_callback_handler(*args)
158164
except Exception as e:
159165
LOGGER.exception("Exception running handler", exc_info=e)
166+
167+
@classmethod
168+
async def probe(cls, device_config: Dict[str, Any]) -> bool:
169+
"""Probe port for the device presence."""
170+
api = cls(zigpy_zigate.config.SCHEMA_DEVICE(device_config))
171+
try:
172+
await asyncio.wait_for(api._probe(), timeout=COMMAND_TIMEOUT)
173+
return True
174+
except (
175+
asyncio.TimeoutError,
176+
serial.SerialException,
177+
zigpy.exceptions.ZigbeeException,
178+
) as exc:
179+
LOGGER.debug(
180+
"Unsuccessful radio probe of '%s' port",
181+
device_config[zigpy_zigate.config.CONF_DEVICE_PATH],
182+
exc_info=exc,
183+
)
184+
finally:
185+
api.close()
186+
187+
return False
188+
189+
async def _probe(self) -> None:
190+
"""Open port and try sending a command"""
191+
await self.connect()
192+
await self.set_raw_mode()

zigpy_zigate/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from zigpy.config import ( # noqa: F401 pylint: disable=unused-import
2+
CONF_DATABASE,
3+
CONF_DEVICE,
4+
CONF_DEVICE_PATH,
5+
CONFIG_SCHEMA,
6+
SCHEMA_DEVICE,
7+
)

zigpy_zigate/uart.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import asyncio
2-
import logging
3-
import serial # noqa
4-
import serial.tools.list_ports
52
import binascii
3+
import logging
64
import struct
5+
from typing import Any, Dict
76

7+
import serial # noqa
8+
import serial.tools.list_ports
89
import serial_asyncio
910

11+
from zigpy_zigate.config import CONF_DEVICE_PATH
12+
1013
LOGGER = logging.getLogger(__name__)
14+
ZIGATE_BAUDRATE = 115200
1115

1216

1317
class Gateway(asyncio.Protocol):
@@ -108,13 +112,14 @@ def _length(self, frame):
108112
return length
109113

110114

111-
async def connect(port, baudrate, api, loop=None):
115+
async def connect(device_config: Dict[str, Any], api, loop=None):
112116
if loop is None:
113117
loop = asyncio.get_event_loop()
114118

115119
connected_future = asyncio.Future()
116120
protocol = Gateway(api, connected_future)
117121

122+
port = device_config[CONF_DEVICE_PATH]
118123
if port.startswith('pizigate:'):
119124
await set_pizigate_running_mode()
120125
port = port.split(':', 1)[1]
@@ -130,12 +135,13 @@ async def connect(port, baudrate, api, loop=None):
130135
LOGGER.info('ZiGate probably found at %s', port)
131136
else:
132137
LOGGER.error('Unable to find ZiGate using auto mode')
138+
raise serial.SerialException("Unable to find Zigate using auto mode")
133139

134140
_, protocol = await serial_asyncio.create_serial_connection(
135141
loop,
136142
lambda: protocol,
137143
url=port,
138-
baudrate=baudrate,
144+
baudrate=ZIGATE_BAUDRATE,
139145
parity=serial.PARITY_NONE,
140146
stopbits=serial.STOPBITS_ONE,
141147
xonxoff=False,

0 commit comments

Comments
 (0)