Skip to content

Commit

Permalink
Sync up with zigpy 0.60.0 (#233)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
puddly authored Nov 16, 2023
1 parent 406b21f commit 9c664a1
Show file tree
Hide file tree
Showing 13 changed files with 127 additions and 335 deletions.
18 changes: 9 additions & 9 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,47 +1,47 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.5.0
hooks:
- id: debug-statements

- repo: https://github.com/psf/black
rev: 23.1.0
rev: 23.10.1
hooks:
- id: black

- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
rev: 6.1.0
hooks:
- id: flake8
entry: pflake8
additional_dependencies:
- pyproject-flake8==6.0.0.post1
- pyproject-flake8==6.1.0
- flake8-bugbear==23.1.20
- flake8-comprehensions==3.10.1
- flake8_2020==1.7.0
- mccabe==0.7.0
- pycodestyle==2.10.0
- pyflakes==3.0.1
- pycodestyle==2.11.1
- pyflakes==3.1.0

- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.0.0
rev: v1.6.1
hooks:
- id: mypy
additional_dependencies:
- zigpy
- types-setuptools

- repo: https://github.com/asottile/pyupgrade
rev: v3.3.1
rev: v3.15.0
hooks:
- id: pyupgrade

- repo: https://github.com/fsouza/autoflake8
rev: v0.4.0
rev: v0.4.1
hooks:
- id: autoflake8
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ readme = "README.md"
license = {text = "GPL-3.0"}
requires-python = ">=3.8"
dependencies = [
"zigpy>=0.56.3",
"zigpy>=0.60.0",
"async_timeout",
"voluptuous",
"coloredlogs",
Expand Down
6 changes: 5 additions & 1 deletion tests/api/test_network_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ async def test_state_transfer(from_device, to_device, make_connected_znp):
metadata=formed_znp.network_info.metadata
)

assert formed_znp.node_info == empty_znp.node_info
assert formed_znp.node_info == empty_znp.node_info.replace(
version=formed_znp.node_info.version,
model=formed_znp.node_info.model,
manufacturer=formed_znp.node_info.manufacturer,
)


@pytest.mark.parametrize("device", [FormedZStack3CC2531])
Expand Down
154 changes: 16 additions & 138 deletions tests/application/test_connect.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import asyncio
from unittest.mock import patch
from unittest.mock import AsyncMock, patch

import pytest

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


@pytest.mark.parametrize("device", FORMED_DEVICES)
async def test_reconnect(device, make_application):
app, znp_server = make_application(
server_cls=device,
client_config={
# Make auto-reconnection happen really fast
conf.CONF_ZNP_CONFIG: {
conf.CONF_AUTO_RECONNECT_RETRY_DELAY: 0.01,
conf.CONF_SREQ_TIMEOUT: 0.1,
}
},
shorten_delays=False,
)

# Start up the server
await app.startup(auto_form=False)
assert app._znp is not None

# Don't reply to anything for a bit
with patch.object(znp_server, "frame_received", lambda _: None):
# Now that we're connected, have the server close the connection
znp_server._uart._transport.close()

# ZNP should be closed
assert app._znp is None

# Wait for more than the SREQ_TIMEOUT to pass, we should still fail to reconnect
await asyncio.sleep(0.3)

assert not app._reconnect_task.done()
assert app._znp is None

# Our reconnect task should complete a moment after we send the ping reply
while app._znp is None:
await asyncio.sleep(0.1)

assert app._znp is not None
assert app._znp._uart is not None

await app.shutdown()


@pytest.mark.parametrize("device", FORMED_DEVICES)
async def test_shutdown_from_app(device, mocker, make_application):
app, znp_server = make_application(server_cls=device)
Expand All @@ -185,7 +143,6 @@ async def test_clean_shutdown(make_application):
await app.shutdown()

assert app._znp is None
assert app._reconnect_task.cancelled()


async def test_multiple_shutdown(make_application):
Expand All @@ -197,100 +154,6 @@ async def test_multiple_shutdown(make_application):
await app.shutdown()


@pytest.mark.parametrize("device", FORMED_DEVICES)
async def test_reconnect_lockup(device, make_application, mocker):
mocker.patch("zigpy_znp.zigbee.application.WATCHDOG_PERIOD", 0.1)

app, znp_server = make_application(
server_cls=device,
client_config={
# Make auto-reconnection happen really fast
conf.CONF_ZNP_CONFIG: {
conf.CONF_AUTO_RECONNECT_RETRY_DELAY: 0.01,
conf.CONF_SREQ_TIMEOUT: 0.1,
}
},
)

# Start up the server
await app.startup(auto_form=False)

# Stop responding
with patch.object(znp_server, "frame_received", lambda _: None):
assert app._znp is not None
assert app._reconnect_task.done()

# Wait for more than the SREQ_TIMEOUT to pass, the watchdog will notice
await asyncio.sleep(0.3)

# We will treat this as a disconnect
assert app._znp is None
assert app._watchdog_task.done()
assert not app._reconnect_task.done()

# Our reconnect task should complete after that
while app._znp is None:
await asyncio.sleep(0.1)

assert app._znp is not None
assert app._znp._uart is not None

await app.shutdown()


@pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1])
async def test_reconnect_lockup_pyserial(device, make_application, mocker):
mocker.patch("zigpy_znp.zigbee.application.WATCHDOG_PERIOD", 0.1)

app, znp_server = make_application(
server_cls=device,
client_config={
conf.CONF_ZNP_CONFIG: {
conf.CONF_AUTO_RECONNECT_RETRY_DELAY: 0.01,
conf.CONF_SREQ_TIMEOUT: 0.1,
}
},
)

# Start up the server
await app.startup(auto_form=False)

# On Linux, a connection error during read with queued writes will cause PySerial to
# swallow the exception. This makes it appear like we intentionally closed the
# connection.

# We are connected
assert app._znp is not None

did_start_network = asyncio.get_running_loop().create_future()

async def patched_start_network(old_start_network=app.start_network, **kwargs):
try:
return await old_start_network(**kwargs)
finally:
did_start_network.set_result(True)

with patch.object(app, "start_network", patched_start_network):
# "Drop" the connection like PySerial
app._znp._uart.connection_lost(exc=None)

# Wait until we are reconnecting
await did_start_network

# "Drop" the connection like PySerial again, but during connect
app._znp._uart.connection_lost(exc=None)

# We should reconnect soon
mocker.spy(app, "_watchdog_loop")

while app._watchdog_loop.call_count == 0:
await asyncio.sleep(0.1)

assert app._znp and app._znp._uart

await app.shutdown()


@pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1])
async def test_disconnect(device, make_application):
app, znp_server = make_application(
Expand Down Expand Up @@ -335,3 +198,18 @@ async def test_disconnect_failure(device, make_application):
await app.disconnect()

assert app._znp is None


@pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1])
async def test_watchdog(device, make_application):
app, znp_server = make_application(server_cls=device)
await app.startup(auto_form=False)

app._watchdog_feed = AsyncMock(wraps=app._watchdog_feed)

with patch("zigpy.application.ControllerApplication._watchdog_period", new=0.1):
await asyncio.sleep(0.6)

assert len(app._watchdog_feed.mock_calls) >= 5

await app.shutdown()
19 changes: 3 additions & 16 deletions tests/application/test_joining.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,13 @@ async def test_permit_join_with_key(device, permit_result, make_application, moc
# Consciot bulb
ieee = t.EUI64.convert("EC:1B:BD:FF:FE:54:4F:40")
code = bytes.fromhex("17D1856872570CEB7ACB53030C5D6DA368B1")
link_key = t.KeyData(zigpy.util.convert_install_code(code))

bdb_add_install_code = znp_server.reply_once_to(
c.AppConfig.BDBAddInstallCode.Req(
InstallCodeFormat=c.app_config.InstallCodeFormat.KeyDerivedFromInstallCode,
IEEE=ieee,
InstallCode=t.Bytes(zigpy.util.convert_install_code(code)),
InstallCode=t.Bytes(link_key),
),
responses=[c.AppConfig.BDBAddInstallCode.Rsp(Status=t.Status.SUCCESS)],
)
Expand All @@ -171,7 +172,7 @@ async def test_permit_join_with_key(device, permit_result, make_application, moc
with contextlib.nullcontext() if permit_result is None else pytest.raises(
asyncio.TimeoutError
):
await app.permit_with_key(node=ieee, code=code, time_s=1)
await app.permit_with_link_key(node=ieee, link_key=link_key, time_s=1)

await bdb_add_install_code
await join_enable_install_code
Expand All @@ -183,20 +184,6 @@ async def test_permit_join_with_key(device, permit_result, make_application, moc
await app.shutdown()


@pytest.mark.parametrize("device", FORMED_ZSTACK3_DEVICES)
async def test_permit_join_with_invalid_key(device, make_application):
app, znp_server = make_application(server_cls=device)

# Consciot bulb
ieee = t.EUI64.convert("EC:1B:BD:FF:FE:54:4F:40")
code = bytes.fromhex("17D1856872570CEB7ACB53030C5D6DA368B1")[:-1] # truncate it

with pytest.raises(ValueError):
await app.permit_with_key(node=ieee, code=code, time_s=1)

await app.shutdown()


@pytest.mark.parametrize("device", FORMED_DEVICES)
async def test_on_zdo_device_join(device, make_application, mocker):
app, znp_server = make_application(server_cls=device)
Expand Down
17 changes: 11 additions & 6 deletions tests/application/test_startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,26 @@

DEV_NETWORK_SETTINGS = {
FormedLaunchpadCC26X2R1: (
f"CC1352/CC2652, Z-Stack 3.30+ (build {FormedLaunchpadCC26X2R1.code_revision})",
"CC2652",
f"Z-Stack {FormedLaunchpadCC26X2R1.code_revision}",
15,
t.Channels.from_channel_list([15]),
0x4402,
t.EUI64.convert("A2:BA:38:A8:B5:E6:83:A0"),
t.KeyData.convert("4C:4E:72:B8:41:22:51:79:9A:BF:35:25:12:88:CA:83"),
),
FormedZStack3CC2531: (
f"CC2531, Z-Stack 3.0.x (build {FormedZStack3CC2531.code_revision})",
"CC2531",
f"Z-Stack 3.0.x {FormedZStack3CC2531.code_revision}",
15,
t.Channels.from_channel_list([15]),
0xB6AB,
t.EUI64.convert("62:92:32:46:3C:77:2D:B2"),
t.KeyData.convert("6D:DE:24:EA:E2:85:52:B6:DE:29:56:EB:05:85:1A:FA"),
),
FormedZStack1CC2531: (
f"CC2531, Z-Stack Home 1.2 (build {FormedZStack1CC2531.code_revision})",
"CC2531",
f"Z-Stack Home 1.2 {FormedZStack1CC2531.code_revision}",
11,
t.Channels.from_channel_list([11]),
0x1A62,
Expand All @@ -50,12 +53,13 @@

# These settings were extracted from beacon requests and key exchanges in Wireshark
@pytest.mark.parametrize(
"device,model,channel,channels,pan_id,ext_pan_id,network_key",
"device,model,version,channel,channels,pan_id,ext_pan_id,network_key",
[(device_cls,) + settings for device_cls, settings in DEV_NETWORK_SETTINGS.items()],
)
async def test_info(
device,
model,
version,
channel,
channels,
pan_id,
Expand All @@ -80,8 +84,9 @@ async def test_info(
assert app.state.network_info.network_key.key == network_key
assert app.state.network_info.network_key.seq == 0

assert app._device.manufacturer == "Texas Instruments"
assert app._device.model == model
assert app.state.node_info.manufacturer == "Texas Instruments"
assert app.state.node_info.model == model
assert app.state.node_info.version == version

# Anything to make sure it's set
assert app._device.node_desc.maximum_outgoing_transfer_size == 160
Expand Down
Loading

0 comments on commit 9c664a1

Please sign in to comment.