Skip to content

Connect multiple times on startup and speed up initial connect #104

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 4 commits into from
Nov 30, 2021
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ zha:

# Delay between auto-reconnect attempts in case the device gets disconnected
auto_reconnect_retry_delay: 5

# Pin states for skipping the bootloader
connect_rts_pin_states: [off, on, off]
connect_dtr_pin_states: [off, off, off]
```

# Tools
Expand Down
119 changes: 102 additions & 17 deletions tests/api/test_connect.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,126 @@
import asyncio
from unittest.mock import call

import pytest

import zigpy_znp.types as t
import zigpy_znp.commands as c
from zigpy_znp.api import ZNP

from ..conftest import FAKE_SERIAL_PORT, BaseServerZNP, config_for_port_path
from ..conftest import BaseServerZNP, CoroutineMock, config_for_port_path

pytestmark = [pytest.mark.asyncio]

async def test_connect_no_test(make_znp_server):
znp_server = make_znp_server(server_cls=BaseServerZNP)
znp = ZNP(config_for_port_path(znp_server.port_path))

async def test_connect_no_communication(connected_znp):
znp, znp_server = connected_znp
await znp.connect(test_port=False)

# Nothing will be sent
assert znp_server._uart.data_received.call_count == 0

znp.close()


async def test_connect_skip_bootloader(make_znp_server):
@pytest.mark.parametrize("work_after_attempt", [1, 2, 3])
async def test_connect_skip_bootloader(make_znp_server, mocker, work_after_attempt):
znp_server = make_znp_server(server_cls=BaseServerZNP)
znp = ZNP(config_for_port_path(FAKE_SERIAL_PORT))
znp = ZNP(config_for_port_path(znp_server.port_path))

mocker.patch.object(znp.nvram, "determine_alignment", new=CoroutineMock())
mocker.patch.object(znp, "detect_zstack_version", new=CoroutineMock())

num_pings = 0

def ping_rsp(req):
nonlocal num_pings
num_pings += 1

# Ignore the first few pings
if num_pings >= work_after_attempt:
return c.SYS.Ping.Rsp(Capabilities=t.MTCapabilities.SYS)

znp_server.reply_to(c.SYS.Ping.Req(), responses=[ping_rsp])

await znp.connect(test_port=True)

znp.close()


async def test_connect_skip_bootloader_batched_rsp(make_znp_server, mocker):
znp_server = make_znp_server(server_cls=BaseServerZNP)
znp = ZNP(config_for_port_path(znp_server.port_path))

mocker.patch.object(znp.nvram, "determine_alignment", new=CoroutineMock())
mocker.patch.object(znp, "detect_zstack_version", new=CoroutineMock())

num_pings = 0

def ping_rsp(req):
nonlocal num_pings
num_pings += 1

if num_pings == 3:
# CC253x radios sometimes buffer requests until they send a `ResetInd`
return (
[
c.SYS.ResetInd.Callback(
Reason=t.ResetReason.PowerUp,
TransportRev=0x00,
ProductId=0x12,
MajorRel=0x01,
MinorRel=0x02,
MaintRel=0x03,
)
]
+ [c.SYS.Ping.Rsp(Capabilities=t.MTCapabilities.SYS)] * num_pings,
)
elif num_pings >= 3:
return c.SYS.Ping.Rsp(Capabilities=t.MTCapabilities.SYS)

znp_server.reply_to(c.SYS.Ping.Req(), responses=[ping_rsp])

await znp.connect(test_port=True)

znp.close()

await znp.connect(test_port=False)

# Nothing should have been sent except for bootloader skip bytes
# NOTE: `c[-2][0]` is `c.args[0]`, just compatible with all Python versions
data_written = b"".join(c[-2][0] for c in znp_server._uart.data_received.mock_calls)
assert set(data_written) == {0xEF}
assert len(data_written) >= 167
async def test_connect_skip_bootloader_failure(make_znp_server):
znp_server = make_znp_server(server_cls=BaseServerZNP)
znp = ZNP(config_for_port_path(znp_server.port_path))

with pytest.raises(asyncio.TimeoutError):
await znp.connect(test_port=True)

znp.close()


async def wait_for_spy(spy):
while True:
if spy.called:
return
async def test_connect_skip_bootloader_rts_dtr_pins(make_znp_server, mocker):
znp_server = make_znp_server(server_cls=BaseServerZNP)
znp = ZNP(config_for_port_path(znp_server.port_path))

mocker.patch.object(znp.nvram, "determine_alignment", new=CoroutineMock())
mocker.patch.object(znp, "detect_zstack_version", new=CoroutineMock())

await asyncio.sleep(0.01)
num_pings = 0

def ping_rsp(req):
nonlocal num_pings
num_pings += 1

if num_pings >= 3:
return c.SYS.Ping.Rsp(Capabilities=t.MTCapabilities.SYS)

# Ignore the first few pings so we trigger the pin toggling
znp_server.reply_to(c.SYS.Ping.Req(), responses=ping_rsp)

await znp.connect(test_port=True)

serial = znp._uart._transport
assert serial._mock_dtr_prop.mock_calls == [call(False), call(False), call(False)]
assert serial._mock_rts_prop.mock_calls == [call(False), call(True), call(False)]

znp.close()


async def test_api_close(connected_znp, mocker):
Expand Down
2 changes: 0 additions & 2 deletions tests/api/test_listeners.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import zigpy_znp.commands as c
from zigpy_znp.api import OneShotResponseListener, CallbackResponseListener

pytestmark = [pytest.mark.asyncio]


async def test_resolve(event_loop, mocker):
callback = mocker.Mock()
Expand Down
2 changes: 0 additions & 2 deletions tests/api/test_network_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

from ..conftest import ALL_DEVICES, FORMED_DEVICES, BaseZStack1CC2531

pytestmark = [pytest.mark.asyncio]


@pytest.mark.parametrize("to_device", ALL_DEVICES)
@pytest.mark.parametrize("from_device", FORMED_DEVICES)
Expand Down
2 changes: 0 additions & 2 deletions tests/api/test_nvram.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
from zigpy_znp.types import nvids
from zigpy_znp.exceptions import SecurityError

pytestmark = [pytest.mark.asyncio]


async def test_osal_writes_invalid(connected_znp):
znp, _ = connected_znp
Expand Down
24 changes: 11 additions & 13 deletions tests/api/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
from zigpy_znp.frames import GeneralFrame
from zigpy_znp.exceptions import CommandNotRecognized, InvalidCommandResponse

pytestmark = [pytest.mark.asyncio]


async def test_callback_rsp(connected_znp, event_loop):
znp, znp_server = connected_znp
Expand Down Expand Up @@ -52,33 +50,33 @@ async def test_cleanup_timeout_internal(connected_znp):
znp._config[conf.CONF_ZNP_CONFIG][conf.CONF_SREQ_TIMEOUT] = 0.1
znp._config[conf.CONF_ZNP_CONFIG][conf.CONF_ARSP_TIMEOUT] = 0.1

assert not znp._listeners
assert not any(znp._listeners.values())

with pytest.raises(asyncio.TimeoutError):
await znp.request(c.UTIL.TimeAlive.Req())

# We should be cleaned up
assert not znp._listeners
assert not any(znp._listeners.values())


async def test_cleanup_timeout_external(connected_znp):
znp, znp_server = connected_znp

assert not znp._listeners
assert not any(znp._listeners.values())

# This request will timeout because we didn't send anything back
with pytest.raises(asyncio.TimeoutError):
async with async_timeout.timeout(0.1):
await znp.request(c.UTIL.TimeAlive.Req())

# We should be cleaned up
assert not znp._listeners
assert not any(znp._listeners.values())


async def test_callback_rsp_cleanup_timeout_external(connected_znp):
znp, znp_server = connected_znp

assert not znp._listeners
assert not any(znp._listeners.values())

# This request will timeout because we didn't send anything back
with pytest.raises(asyncio.TimeoutError):
Expand All @@ -89,7 +87,7 @@ async def test_callback_rsp_cleanup_timeout_external(connected_znp):
)

# We should be cleaned up
assert not znp._listeners
assert not any(znp._listeners.values())


@pytest.mark.parametrize("background", [False, True])
Expand All @@ -98,7 +96,7 @@ async def test_callback_rsp_cleanup_timeout_internal(background, connected_znp):
znp._config[conf.CONF_ZNP_CONFIG][conf.CONF_SREQ_TIMEOUT] = 0.1
znp._config[conf.CONF_ZNP_CONFIG][conf.CONF_ARSP_TIMEOUT] = 0.1

assert not znp._listeners
assert not any(znp._listeners.values())

# This request will timeout because we didn't send anything back
with pytest.raises(asyncio.TimeoutError):
Expand All @@ -109,7 +107,7 @@ async def test_callback_rsp_cleanup_timeout_internal(background, connected_znp):
)

# We should be cleaned up
assert not znp._listeners
assert not any(znp._listeners.values())


async def test_callback_rsp_background_timeout(connected_znp, mocker):
Expand Down Expand Up @@ -146,7 +144,7 @@ async def replier(req):
await reply

# We should be cleaned up
assert not znp._listeners
assert not any(znp._listeners.values())

# Command was properly handled
assert len(znp._unhandled_command.mock_calls) == 0
Expand All @@ -157,7 +155,7 @@ async def test_callback_rsp_cleanup_concurrent(connected_znp, event_loop, mocker

mocker.spy(znp, "_unhandled_command")

assert not znp._listeners
assert not any(znp._listeners.values())

def send_responses():
znp_server.send(c.UTIL.TimeAlive.Rsp(Seconds=123))
Expand All @@ -173,7 +171,7 @@ def send_responses():
)

# We should be cleaned up
assert not znp._listeners
assert not any(znp._listeners.values())

assert callback_rsp == c.SYS.OSALTimerExpired.Callback(Id=0xAB)

Expand Down
16 changes: 7 additions & 9 deletions tests/api/test_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,15 @@
import zigpy_znp.commands as c
from zigpy_znp.utils import deduplicate_commands

pytestmark = [pytest.mark.asyncio]


async def test_responses(connected_znp):
znp, znp_server = connected_znp

assert not znp._listeners
assert not any(znp._listeners.values())

future = znp.wait_for_response(c.SYS.Ping.Rsp(partial=True))

assert znp._listeners
assert any(znp._listeners.values())

response = c.SYS.Ping.Rsp(Capabilities=t.MTCapabilities.SYS)
znp_server.send(response)
Expand All @@ -26,13 +24,13 @@ async def test_responses(connected_znp):

# Our listener will have been cleaned up after a step
await asyncio.sleep(0)
assert not znp._listeners
assert not any(znp._listeners.values())


async def test_responses_multiple(connected_znp):
znp, _ = connected_znp

assert not znp._listeners
assert not any(znp._listeners.values())

future1 = znp.wait_for_response(c.SYS.Ping.Rsp(partial=True))
future2 = znp.wait_for_response(c.SYS.Ping.Rsp(partial=True))
Expand All @@ -49,7 +47,7 @@ async def test_responses_multiple(connected_znp):
assert not future2.done()
assert not future3.done()

assert znp._listeners
assert any(znp._listeners.values())


async def test_response_timeouts(connected_znp):
Expand All @@ -68,7 +66,7 @@ async def send_soon(delay):

# The response was successfully received so we should have no outstanding listeners
await asyncio.sleep(0)
assert not znp._listeners
assert not any(znp._listeners.values())

asyncio.create_task(send_soon(0.6))

Expand All @@ -80,7 +78,7 @@ async def send_soon(delay):

# Our future still completed, albeit unsuccessfully.
# We should have no leaked listeners here.
assert not znp._listeners
assert not any(znp._listeners.values())


async def test_response_matching_partial(connected_znp):
Expand Down
9 changes: 4 additions & 5 deletions tests/application/test_connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@

from ..conftest import FORMED_DEVICES, FormedLaunchpadCC26X2R1, swap_attribute

pytestmark = [pytest.mark.asyncio]


async def test_no_double_connect(make_znp_server, mocker):
znp_server = make_znp_server(server_cls=FormedLaunchpadCC26X2R1)
Expand Down Expand Up @@ -60,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):
znp_server = make_znp_server(server_cls=device)
znp_server = make_znp_server(server_cls=device, shorten_delays=False)

# Don't respond to anything
znp_server._listeners.clear()
Expand All @@ -78,7 +76,7 @@ async def test_probe_unsuccessful_slow(device, make_znp_server, mocker):

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

assert await ControllerApplication.probe(
conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: znp_server.serial_port})
Expand All @@ -89,7 +87,7 @@ async def test_probe_successful(device, make_znp_server):
@pytest.mark.parametrize("device", FORMED_DEVICES)
async def test_probe_multiple(device, make_znp_server):
# Make sure that our listeners don't get cleaned up after each probe
znp_server = make_znp_server(server_cls=device)
znp_server = make_znp_server(server_cls=device, shorten_delays=False)
znp_server.close = lambda: None

config = conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: znp_server.serial_port})
Expand All @@ -112,6 +110,7 @@ async def test_reconnect(device, event_loop, make_application):
conf.CONF_SREQ_TIMEOUT: 0.1,
}
},
shorten_delays=False,
)

# Start up the server
Expand Down
2 changes: 0 additions & 2 deletions tests/application/test_joining.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
FormedLaunchpadCC26X2R1,
)

pytestmark = [pytest.mark.asyncio]


@pytest.mark.parametrize(
"device,fixed_joining_bug",
Expand Down
Loading