Skip to content

Commit 9c664a1

Browse files
authored
Sync up with zigpy 0.60.0 (#233)
* Load parsed device info into application state * Fix zigpy unit tests * Bump all pre-commit dependencies so pre-commit runs * Bump to unreleased zigpy * Use zigpy watchdog and connection closing * Use zigpy `device` config schema * Drop `permit_with_key` * Remove unnecessary unit tests * Add a unit test for watchdog feeding
1 parent 406b21f commit 9c664a1

13 files changed

+127
-335
lines changed

.pre-commit-config.yaml

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,47 @@
11
repos:
22
- repo: https://github.com/pre-commit/pre-commit-hooks
3-
rev: v4.4.0
3+
rev: v4.5.0
44
hooks:
55
- id: debug-statements
66

77
- repo: https://github.com/psf/black
8-
rev: 23.1.0
8+
rev: 23.10.1
99
hooks:
1010
- id: black
1111

1212
- repo: https://github.com/PyCQA/flake8
13-
rev: 6.0.0
13+
rev: 6.1.0
1414
hooks:
1515
- id: flake8
1616
entry: pflake8
1717
additional_dependencies:
18-
- pyproject-flake8==6.0.0.post1
18+
- pyproject-flake8==6.1.0
1919
- flake8-bugbear==23.1.20
2020
- flake8-comprehensions==3.10.1
2121
- flake8_2020==1.7.0
2222
- mccabe==0.7.0
23-
- pycodestyle==2.10.0
24-
- pyflakes==3.0.1
23+
- pycodestyle==2.11.1
24+
- pyflakes==3.1.0
2525

2626
- repo: https://github.com/PyCQA/isort
2727
rev: 5.12.0
2828
hooks:
2929
- id: isort
3030

3131
- repo: https://github.com/pre-commit/mirrors-mypy
32-
rev: v1.0.0
32+
rev: v1.6.1
3333
hooks:
3434
- id: mypy
3535
additional_dependencies:
3636
- zigpy
3737
- types-setuptools
3838

3939
- repo: https://github.com/asottile/pyupgrade
40-
rev: v3.3.1
40+
rev: v3.15.0
4141
hooks:
4242
- id: pyupgrade
4343

4444
- repo: https://github.com/fsouza/autoflake8
45-
rev: v0.4.0
45+
rev: v0.4.1
4646
hooks:
4747
- id: autoflake8

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ readme = "README.md"
1414
license = {text = "GPL-3.0"}
1515
requires-python = ">=3.8"
1616
dependencies = [
17-
"zigpy>=0.56.3",
17+
"zigpy>=0.60.0",
1818
"async_timeout",
1919
"voluptuous",
2020
"coloredlogs",

tests/api/test_network_state.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ async def test_state_transfer(from_device, to_device, make_connected_znp):
5252
metadata=formed_znp.network_info.metadata
5353
)
5454

55-
assert formed_znp.node_info == empty_znp.node_info
55+
assert formed_znp.node_info == empty_znp.node_info.replace(
56+
version=formed_znp.node_info.version,
57+
model=formed_znp.node_info.model,
58+
manufacturer=formed_znp.node_info.manufacturer,
59+
)
5660

5761

5862
@pytest.mark.parametrize("device", [FormedZStack3CC2531])

tests/application/test_connect.py

+16-138
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import asyncio
2-
from unittest.mock import patch
2+
from unittest.mock import AsyncMock, patch
33

44
import pytest
55

@@ -118,48 +118,6 @@ async def test_probe_multiple(device, make_znp_server):
118118
assert not any([t._is_connected for t in znp_server._transports])
119119

120120

121-
@pytest.mark.parametrize("device", FORMED_DEVICES)
122-
async def test_reconnect(device, make_application):
123-
app, znp_server = make_application(
124-
server_cls=device,
125-
client_config={
126-
# Make auto-reconnection happen really fast
127-
conf.CONF_ZNP_CONFIG: {
128-
conf.CONF_AUTO_RECONNECT_RETRY_DELAY: 0.01,
129-
conf.CONF_SREQ_TIMEOUT: 0.1,
130-
}
131-
},
132-
shorten_delays=False,
133-
)
134-
135-
# Start up the server
136-
await app.startup(auto_form=False)
137-
assert app._znp is not None
138-
139-
# Don't reply to anything for a bit
140-
with patch.object(znp_server, "frame_received", lambda _: None):
141-
# Now that we're connected, have the server close the connection
142-
znp_server._uart._transport.close()
143-
144-
# ZNP should be closed
145-
assert app._znp is None
146-
147-
# Wait for more than the SREQ_TIMEOUT to pass, we should still fail to reconnect
148-
await asyncio.sleep(0.3)
149-
150-
assert not app._reconnect_task.done()
151-
assert app._znp is None
152-
153-
# Our reconnect task should complete a moment after we send the ping reply
154-
while app._znp is None:
155-
await asyncio.sleep(0.1)
156-
157-
assert app._znp is not None
158-
assert app._znp._uart is not None
159-
160-
await app.shutdown()
161-
162-
163121
@pytest.mark.parametrize("device", FORMED_DEVICES)
164122
async def test_shutdown_from_app(device, mocker, make_application):
165123
app, znp_server = make_application(server_cls=device)
@@ -185,7 +143,6 @@ async def test_clean_shutdown(make_application):
185143
await app.shutdown()
186144

187145
assert app._znp is None
188-
assert app._reconnect_task.cancelled()
189146

190147

191148
async def test_multiple_shutdown(make_application):
@@ -197,100 +154,6 @@ async def test_multiple_shutdown(make_application):
197154
await app.shutdown()
198155

199156

200-
@pytest.mark.parametrize("device", FORMED_DEVICES)
201-
async def test_reconnect_lockup(device, make_application, mocker):
202-
mocker.patch("zigpy_znp.zigbee.application.WATCHDOG_PERIOD", 0.1)
203-
204-
app, znp_server = make_application(
205-
server_cls=device,
206-
client_config={
207-
# Make auto-reconnection happen really fast
208-
conf.CONF_ZNP_CONFIG: {
209-
conf.CONF_AUTO_RECONNECT_RETRY_DELAY: 0.01,
210-
conf.CONF_SREQ_TIMEOUT: 0.1,
211-
}
212-
},
213-
)
214-
215-
# Start up the server
216-
await app.startup(auto_form=False)
217-
218-
# Stop responding
219-
with patch.object(znp_server, "frame_received", lambda _: None):
220-
assert app._znp is not None
221-
assert app._reconnect_task.done()
222-
223-
# Wait for more than the SREQ_TIMEOUT to pass, the watchdog will notice
224-
await asyncio.sleep(0.3)
225-
226-
# We will treat this as a disconnect
227-
assert app._znp is None
228-
assert app._watchdog_task.done()
229-
assert not app._reconnect_task.done()
230-
231-
# Our reconnect task should complete after that
232-
while app._znp is None:
233-
await asyncio.sleep(0.1)
234-
235-
assert app._znp is not None
236-
assert app._znp._uart is not None
237-
238-
await app.shutdown()
239-
240-
241-
@pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1])
242-
async def test_reconnect_lockup_pyserial(device, make_application, mocker):
243-
mocker.patch("zigpy_znp.zigbee.application.WATCHDOG_PERIOD", 0.1)
244-
245-
app, znp_server = make_application(
246-
server_cls=device,
247-
client_config={
248-
conf.CONF_ZNP_CONFIG: {
249-
conf.CONF_AUTO_RECONNECT_RETRY_DELAY: 0.01,
250-
conf.CONF_SREQ_TIMEOUT: 0.1,
251-
}
252-
},
253-
)
254-
255-
# Start up the server
256-
await app.startup(auto_form=False)
257-
258-
# On Linux, a connection error during read with queued writes will cause PySerial to
259-
# swallow the exception. This makes it appear like we intentionally closed the
260-
# connection.
261-
262-
# We are connected
263-
assert app._znp is not None
264-
265-
did_start_network = asyncio.get_running_loop().create_future()
266-
267-
async def patched_start_network(old_start_network=app.start_network, **kwargs):
268-
try:
269-
return await old_start_network(**kwargs)
270-
finally:
271-
did_start_network.set_result(True)
272-
273-
with patch.object(app, "start_network", patched_start_network):
274-
# "Drop" the connection like PySerial
275-
app._znp._uart.connection_lost(exc=None)
276-
277-
# Wait until we are reconnecting
278-
await did_start_network
279-
280-
# "Drop" the connection like PySerial again, but during connect
281-
app._znp._uart.connection_lost(exc=None)
282-
283-
# We should reconnect soon
284-
mocker.spy(app, "_watchdog_loop")
285-
286-
while app._watchdog_loop.call_count == 0:
287-
await asyncio.sleep(0.1)
288-
289-
assert app._znp and app._znp._uart
290-
291-
await app.shutdown()
292-
293-
294157
@pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1])
295158
async def test_disconnect(device, make_application):
296159
app, znp_server = make_application(
@@ -335,3 +198,18 @@ async def test_disconnect_failure(device, make_application):
335198
await app.disconnect()
336199

337200
assert app._znp is None
201+
202+
203+
@pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1])
204+
async def test_watchdog(device, make_application):
205+
app, znp_server = make_application(server_cls=device)
206+
await app.startup(auto_form=False)
207+
208+
app._watchdog_feed = AsyncMock(wraps=app._watchdog_feed)
209+
210+
with patch("zigpy.application.ControllerApplication._watchdog_period", new=0.1):
211+
await asyncio.sleep(0.6)
212+
213+
assert len(app._watchdog_feed.mock_calls) >= 5
214+
215+
await app.shutdown()

tests/application/test_joining.py

+3-16
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,13 @@ async def test_permit_join_with_key(device, permit_result, make_application, moc
140140
# Consciot bulb
141141
ieee = t.EUI64.convert("EC:1B:BD:FF:FE:54:4F:40")
142142
code = bytes.fromhex("17D1856872570CEB7ACB53030C5D6DA368B1")
143+
link_key = t.KeyData(zigpy.util.convert_install_code(code))
143144

144145
bdb_add_install_code = znp_server.reply_once_to(
145146
c.AppConfig.BDBAddInstallCode.Req(
146147
InstallCodeFormat=c.app_config.InstallCodeFormat.KeyDerivedFromInstallCode,
147148
IEEE=ieee,
148-
InstallCode=t.Bytes(zigpy.util.convert_install_code(code)),
149+
InstallCode=t.Bytes(link_key),
149150
),
150151
responses=[c.AppConfig.BDBAddInstallCode.Rsp(Status=t.Status.SUCCESS)],
151152
)
@@ -171,7 +172,7 @@ async def test_permit_join_with_key(device, permit_result, make_application, moc
171172
with contextlib.nullcontext() if permit_result is None else pytest.raises(
172173
asyncio.TimeoutError
173174
):
174-
await app.permit_with_key(node=ieee, code=code, time_s=1)
175+
await app.permit_with_link_key(node=ieee, link_key=link_key, time_s=1)
175176

176177
await bdb_add_install_code
177178
await join_enable_install_code
@@ -183,20 +184,6 @@ async def test_permit_join_with_key(device, permit_result, make_application, moc
183184
await app.shutdown()
184185

185186

186-
@pytest.mark.parametrize("device", FORMED_ZSTACK3_DEVICES)
187-
async def test_permit_join_with_invalid_key(device, make_application):
188-
app, znp_server = make_application(server_cls=device)
189-
190-
# Consciot bulb
191-
ieee = t.EUI64.convert("EC:1B:BD:FF:FE:54:4F:40")
192-
code = bytes.fromhex("17D1856872570CEB7ACB53030C5D6DA368B1")[:-1] # truncate it
193-
194-
with pytest.raises(ValueError):
195-
await app.permit_with_key(node=ieee, code=code, time_s=1)
196-
197-
await app.shutdown()
198-
199-
200187
@pytest.mark.parametrize("device", FORMED_DEVICES)
201188
async def test_on_zdo_device_join(device, make_application, mocker):
202189
app, znp_server = make_application(server_cls=device)

tests/application/test_startup.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,26 @@
2222

2323
DEV_NETWORK_SETTINGS = {
2424
FormedLaunchpadCC26X2R1: (
25-
f"CC1352/CC2652, Z-Stack 3.30+ (build {FormedLaunchpadCC26X2R1.code_revision})",
25+
"CC2652",
26+
f"Z-Stack {FormedLaunchpadCC26X2R1.code_revision}",
2627
15,
2728
t.Channels.from_channel_list([15]),
2829
0x4402,
2930
t.EUI64.convert("A2:BA:38:A8:B5:E6:83:A0"),
3031
t.KeyData.convert("4C:4E:72:B8:41:22:51:79:9A:BF:35:25:12:88:CA:83"),
3132
),
3233
FormedZStack3CC2531: (
33-
f"CC2531, Z-Stack 3.0.x (build {FormedZStack3CC2531.code_revision})",
34+
"CC2531",
35+
f"Z-Stack 3.0.x {FormedZStack3CC2531.code_revision}",
3436
15,
3537
t.Channels.from_channel_list([15]),
3638
0xB6AB,
3739
t.EUI64.convert("62:92:32:46:3C:77:2D:B2"),
3840
t.KeyData.convert("6D:DE:24:EA:E2:85:52:B6:DE:29:56:EB:05:85:1A:FA"),
3941
),
4042
FormedZStack1CC2531: (
41-
f"CC2531, Z-Stack Home 1.2 (build {FormedZStack1CC2531.code_revision})",
43+
"CC2531",
44+
f"Z-Stack Home 1.2 {FormedZStack1CC2531.code_revision}",
4245
11,
4346
t.Channels.from_channel_list([11]),
4447
0x1A62,
@@ -50,12 +53,13 @@
5053

5154
# These settings were extracted from beacon requests and key exchanges in Wireshark
5255
@pytest.mark.parametrize(
53-
"device,model,channel,channels,pan_id,ext_pan_id,network_key",
56+
"device,model,version,channel,channels,pan_id,ext_pan_id,network_key",
5457
[(device_cls,) + settings for device_cls, settings in DEV_NETWORK_SETTINGS.items()],
5558
)
5659
async def test_info(
5760
device,
5861
model,
62+
version,
5963
channel,
6064
channels,
6165
pan_id,
@@ -80,8 +84,9 @@ async def test_info(
8084
assert app.state.network_info.network_key.key == network_key
8185
assert app.state.network_info.network_key.seq == 0
8286

83-
assert app._device.manufacturer == "Texas Instruments"
84-
assert app._device.model == model
87+
assert app.state.node_info.manufacturer == "Texas Instruments"
88+
assert app.state.node_info.model == model
89+
assert app.state.node_info.version == version
8590

8691
# Anything to make sure it's set
8792
assert app._device.node_desc.maximum_outgoing_transfer_size == 160

0 commit comments

Comments
 (0)