Skip to content

Implement new zigpy radio API #97

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

Merged
merged 43 commits into from
Jun 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
251fc77
Fix `GetExtAddr` request schema
puddly Dec 12, 2021
059bffa
Implement provisional zigpy network management API
puddly Nov 30, 2021
02c45c9
Handle global TC link keys
puddly Nov 30, 2021
c826fc4
Move NVRAM migration into `ZNP`
puddly Nov 30, 2021
bbc96fb
TCLK cannot be read on Z-Stack Home 1.2, only written
puddly Nov 30, 2021
3538608
Override `probe` method to use shorter probe timeout
puddly Nov 30, 2021
8306270
Rename `tc_frame_counter` to `nwk_frame_counter`
puddly Dec 1, 2021
6726158
Use new `network_info.children` structure
puddly Dec 12, 2021
3ee8c2c
Fix failing unit tests after rebase
puddly Dec 12, 2021
4759543
Fix wrong value being appended to the `children` list
puddly Dec 26, 2021
2e8b179
Merge branch 'dev' into puddly/new-radio-settings-api
puddly Jan 24, 2022
bc1e273
Merge branch 'dev' into puddly/new-radio-settings-api
puddly Feb 10, 2022
e7cfc91
Handle the expected "formation failure" status code on startup
puddly Feb 23, 2022
7cca13a
Handle both formation status codes (brand new network and existing)
puddly Feb 25, 2022
9e439f6
Make network state compatible with Zigbee2MQTT
puddly Feb 28, 2022
741a6ab
Allow the node IEEE address to be deleted, resetting it to the default
puddly Mar 15, 2022
46645b4
Use `t.EUI64.UNKNOWN` instead of `None` to represent an unset IEEE addr
puddly Mar 15, 2022
6e2851b
Reset after `write_network_info`
puddly Mar 29, 2022
9aa2c97
Fix black's click dependency problem
puddly Mar 29, 2022
7ecbaa3
Fix inconsistent zigpy network state on startup
puddly Mar 29, 2022
95bbba3
Reset before starting the network
puddly Mar 31, 2022
4961596
Remove unneeded function signature
puddly Apr 1, 2022
6cb154b
Add mypy pre-commit hook to perform minimal checks
puddly Apr 1, 2022
bfc5a08
Bump `pyproject-flake8` pre-commit dependency
puddly Apr 1, 2022
635dfbc
Improve type annotations
puddly Apr 1, 2022
d8c3aea
Add `typing_extensions` as an installation dependency
puddly Apr 1, 2022
85bb403
Merge branch 'dev' into puddly/new-radio-settings-api
puddly Apr 1, 2022
dd9bb54
Use the coordinator's IEEE address as the `partner_ieee` for the TCLK
puddly Apr 14, 2022
440711d
Rely on `tc_link_key` always being populated
puddly Apr 14, 2022
cfc5e50
Register endpoints using the new zigpy API
puddly Apr 16, 2022
fa1f11e
Rename `register_endpoint` to `add_endpoint`
puddly Apr 18, 2022
4b75a2e
Use zigpy's new `register_endpoints` method
puddly Apr 19, 2022
7bad3e7
Merge branch 'dev' into puddly/new-radio-settings-api
puddly May 2, 2022
a1ab639
Use the node's NWK address instead of `0x0000`
puddly May 13, 2022
450cb77
Move `permit(0)` call out of the radio library and into zigpy
puddly May 13, 2022
4a512ad
Use zigpy's `_device` instead of ZNP-specific `zigpy_device`
puddly May 13, 2022
345513d
Include radio library metadata in network info
puddly May 15, 2022
a080d9d
Fix unit tests broken by new `metadata` key
puddly May 15, 2022
f60d410
Bump minimum required zigpy version to 0.47.0
puddly Jun 15, 2022
f69c407
Speed up `disconnect`
puddly Jun 17, 2022
6ad0366
Replace `swap_attribute` with `patch.object`
puddly Jun 19, 2022
b545d07
Update reconnect tests and don't use zigpy `startup` during reconnect
puddly Jun 19, 2022
9dce57e
Increase diff test coverage
puddly Jun 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ repos:
- id: flake8
entry: pflake8
additional_dependencies:
- pyproject-flake8==0.0.1a2
- pyproject-flake8==0.0.1a3
- flake8-bugbear==22.1.11
- flake8-comprehensions==3.8.0
- flake8_2020==1.6.1
Expand All @@ -29,11 +29,11 @@ repos:
- id: isort

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.931
rev: v0.942
hooks:
- id: mypy
additional_dependencies:
- zigpy==0.43.0
- zigpy

- repo: https://github.com/asottile/pyupgrade
rev: v2.31.0
Expand All @@ -43,4 +43,4 @@ repos:
- repo: https://github.com/fsouza/autoflake8
rev: v0.3.1
hooks:
- id: autoflake8
- id: autoflake8
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ python_requires = >=3.7
install_requires =
pyserial-asyncio; platform_system!="Windows"
pyserial-asyncio!=0.5; platform_system=="Windows" # 0.5 broke writes
zigpy>=0.40.0
zigpy>=0.47.0
async_timeout
voluptuous
coloredlogs
Expand Down
43 changes: 38 additions & 5 deletions tests/api/test_network_state.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import logging
import dataclasses

import pytest

import zigpy_znp.types as t
from zigpy_znp.types.nvids import ExNvIds, OsalNvIds

from ..conftest import (
Expand Down Expand Up @@ -31,16 +31,22 @@ async def test_state_transfer(from_device, to_device, make_connected_znp):

# Z-Stack 1 devices can't have some security info read out
if issubclass(from_device, BaseZStack1CC2531):
assert formed_znp.network_info == dataclasses.replace(
empty_znp.network_info, stack_specific={}
assert formed_znp.network_info == empty_znp.network_info.replace(
stack_specific={},
metadata=formed_znp.network_info.metadata,
)
elif issubclass(to_device, BaseZStack1CC2531):
assert (
dataclasses.replace(formed_znp.network_info, stack_specific={})
formed_znp.network_info.replace(
stack_specific={},
metadata=empty_znp.network_info.metadata,
)
== empty_znp.network_info
)
else:
assert formed_znp.network_info == empty_znp.network_info
assert formed_znp.network_info == empty_znp.network_info.replace(
metadata=formed_znp.network_info.metadata
)

assert formed_znp.node_info == empty_znp.node_info

Expand All @@ -59,3 +65,30 @@ async def test_broken_cc2531_load_state(device, make_connected_znp, caplog):
assert "inconsistent" in caplog.text

znp.close()


@pytest.mark.parametrize("device", [FormedZStack3CC2531])
async def test_state_write_tclk_zstack3(device, make_connected_znp, caplog):
formed_znp, _ = await make_connected_znp(server_cls=device)

await formed_znp.load_network_info()
formed_znp.close()

empty_znp, _ = await make_connected_znp(server_cls=device)

caplog.set_level(logging.WARNING)
await empty_znp.write_network_info(
network_info=formed_znp.network_info.replace(
tc_link_key=formed_znp.network_info.tc_link_key.replace(
# Non-standard TCLK
key=t.KeyData.convert("AA:BB:CC:DD:AA:BB:CC:DD:AA:BB:CC:DD:AA:BB:CC:DD")
)
),
node_info=formed_znp.node_info,
)
assert "TC link key is configured at build time in Z-Stack 3" in caplog.text

await empty_znp.load_network_info()

# TCLK was not changed
assert formed_znp.network_info == empty_znp.network_info
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async def test_addrmgr_empty_entries(make_connected_znp, device):


@pytest.mark.parametrize("device", [FormedZStack3CC2531])
async def test_addrmgr_rewrite_fix(device, make_application, mocker):
async def test_addrmgr_rewrite_fix(device, make_connected_znp):
# Keep track of reads
addrmgr_reads = []

Expand All @@ -60,7 +60,7 @@ async def test_addrmgr_rewrite_fix(device, make_application, mocker):
extAddr=t.EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF"),
)

app, znp_server = make_application(server_cls=device)
znp, znp_server = await make_connected_znp(server_cls=device)
znp_server.callback_for_response(
c.SYS.OSALNVReadExt.Req(Id=OsalNvIds.ADDRMGR, Offset=0), addrmgr_reads.append
)
Expand All @@ -81,8 +81,7 @@ async def test_addrmgr_rewrite_fix(device, make_application, mocker):
assert old_addrmgr != nvram[OsalNvIds.ADDRMGR]

assert len(addrmgr_reads) == 0
await app.startup()
await app.shutdown()
await znp.migrate_nvram()
assert len(addrmgr_reads) == 2

# Bad entries have been fixed
Expand All @@ -94,8 +93,7 @@ async def test_addrmgr_rewrite_fix(device, make_application, mocker):

# Will not be read again
assert len(addrmgr_reads) == 2
await app.startup()
await app.shutdown()
await znp.migrate_nvram()
assert len(addrmgr_reads) == 2

# Will be migrated again if the migration NVID is deleted
Expand All @@ -104,8 +102,7 @@ async def test_addrmgr_rewrite_fix(device, make_application, mocker):
old_addrmgr2 = nvram[OsalNvIds.ADDRMGR]

assert len(addrmgr_reads) == 2
await app.startup()
await app.shutdown()
await znp.migrate_nvram()
assert len(addrmgr_reads) == 3

# But nothing will change
Expand Down
108 changes: 87 additions & 21 deletions tests/application/test_connect.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import asyncio
from unittest.mock import patch

import pytest

import zigpy_znp.config as conf
from zigpy_znp.uart import connect as uart_connect
from zigpy_znp.zigbee.application import ControllerApplication

from ..conftest import FORMED_DEVICES, FormedLaunchpadCC26X2R1, swap_attribute
from ..conftest import FORMED_DEVICES, FormedLaunchpadCC26X2R1


async def test_no_double_connect(make_znp_server, mocker):
Expand Down Expand Up @@ -57,7 +58,7 @@ async def test_probe_unsuccessful():


@pytest.mark.parametrize("device", FORMED_DEVICES)
async def test_probe_unsuccessful_slow(device, make_znp_server, mocker):
async def test_probe_unsuccessful_slow1(device, make_znp_server, mocker):
znp_server = make_znp_server(server_cls=device, shorten_delays=False)

# Don't respond to anything
Expand All @@ -74,6 +75,24 @@ async def test_probe_unsuccessful_slow(device, make_znp_server, mocker):
assert not any([t._is_connected for t in znp_server._transports])


@pytest.mark.parametrize("device", FORMED_DEVICES)
async def test_probe_unsuccessful_slow2(device, make_znp_server, mocker):
znp_server = make_znp_server(server_cls=device, shorten_delays=False)

# Don't respond to anything
znp_server._listeners.clear()

mocker.patch("zigpy_znp.zigbee.application.PROBE_TIMEOUT", new=0.1)

assert not (
await ControllerApplication.probe(
conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: znp_server.serial_port})
)
)

assert not any([t._is_connected for t in znp_server._transports])


@pytest.mark.parametrize("device", FORMED_DEVICES)
async def test_probe_successful(device, make_znp_server):
znp_server = make_znp_server(server_cls=device, shorten_delays=False)
Expand All @@ -100,8 +119,8 @@ async def test_probe_multiple(device, make_znp_server):


@pytest.mark.parametrize("device", FORMED_DEVICES)
async def test_reconnect(device, event_loop, make_application):
app, znp_server = make_application(
async def test_reconnect(device, make_application):
app, znp_server = await make_application(
server_cls=device,
client_config={
# Make auto-reconnection happen really fast
Expand All @@ -118,7 +137,7 @@ async def test_reconnect(device, event_loop, make_application):
assert app._znp is not None

# Don't reply to anything for a bit
with swap_attribute(znp_server, "frame_received", lambda _: None):
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()

Expand All @@ -143,7 +162,7 @@ async def test_reconnect(device, event_loop, make_application):

@pytest.mark.parametrize("device", FORMED_DEVICES)
async def test_shutdown_from_app(device, mocker, make_application):
app, znp_server = make_application(server_cls=device)
app, znp_server = await make_application(server_cls=device)

await app.startup(auto_form=False)

Expand All @@ -159,7 +178,7 @@ async def test_shutdown_from_app(device, mocker, make_application):


async def test_clean_shutdown(make_application):
app, znp_server = make_application(server_cls=FormedLaunchpadCC26X2R1)
app, znp_server = await make_application(server_cls=FormedLaunchpadCC26X2R1)
await app.startup(auto_form=False)

# This should not throw
Expand All @@ -170,7 +189,7 @@ async def test_clean_shutdown(make_application):


async def test_multiple_shutdown(make_application):
app, znp_server = make_application(server_cls=FormedLaunchpadCC26X2R1)
app, znp_server = await make_application(server_cls=FormedLaunchpadCC26X2R1)
await app.startup(auto_form=False)

await app.shutdown()
Expand All @@ -179,10 +198,10 @@ async def test_multiple_shutdown(make_application):


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

app, znp_server = make_application(
app, znp_server = await make_application(
server_cls=device,
client_config={
# Make auto-reconnection happen really fast
Expand All @@ -197,7 +216,7 @@ async def test_reconnect_lockup(device, event_loop, make_application, mocker):
await app.startup(auto_form=False)

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

Expand All @@ -219,15 +238,16 @@ async def test_reconnect_lockup(device, event_loop, make_application, mocker):
await app.shutdown()


@pytest.mark.parametrize("device", FORMED_DEVICES)
async def test_reconnect_lockup_pyserial(device, event_loop, make_application, mocker):
@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(
app, znp_server = await make_application(
server_cls=device,
client_config={
conf.CONF_ZNP_CONFIG: {
conf.CONF_AUTO_RECONNECT_RETRY_DELAY: 0.1,
conf.CONF_AUTO_RECONNECT_RETRY_DELAY: 0.01,
conf.CONF_SREQ_TIMEOUT: 0.1,
}
},
)
Expand All @@ -242,20 +262,20 @@ async def test_reconnect_lockup_pyserial(device, event_loop, make_application, m
# We are connected
assert app._znp is not None

did_load_info = asyncio.get_running_loop().create_future()
did_start_network = asyncio.get_running_loop().create_future()

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

with swap_attribute(app, "load_network_info", patched_load_network_info):
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_load_info
await did_start_network

# "Drop" the connection like PySerial again, but during connect
app._znp._uart.connection_lost(exc=None)
Expand All @@ -269,3 +289,49 @@ async def patched_load_network_info(*, old_load=app.load_network_info):
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 = await make_application(
server_cls=device,
client_config={
conf.CONF_ZNP_CONFIG: {
conf.CONF_SREQ_TIMEOUT: 0.1,
}
},
)

assert app._znp is None
await app.connect()

assert app._znp is not None

await app.disconnect()
assert app._znp is None

await app.disconnect()
await app.disconnect()


@pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1])
async def test_disconnect_failure(device, make_application):
app, znp_server = await make_application(
server_cls=device,
client_config={
conf.CONF_ZNP_CONFIG: {
conf.CONF_SREQ_TIMEOUT: 0.1,
}
},
)

assert app._znp is None
await app.connect()

assert app._znp is not None

with patch.object(app._znp, "reset", side_effect=RuntimeError("An error")):
# Runs without error
await app.disconnect()

assert app._znp is None
Loading