Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync up with zigpy 0.60.0 #233

Merged
merged 9 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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