Skip to content
Draft
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
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.11"
dependencies = [
"zigpy>=1.5.0",
"zigpy>=2.4.1",
"voluptuous",
"coloredlogs",
"jsonschema",
Expand Down
3 changes: 3 additions & 0 deletions tests/application/test_joining.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ async def test_permit_join(device, mocker, make_application):
)

await app.startup(auto_form=False)

# Green Power commissioning also consumes sequence numbers
mocker.patch.object(app, "get_sequence", return_value=2)
await app.permit(time_s=10)

await permit_join_broadcast
Expand Down
95 changes: 79 additions & 16 deletions tests/application/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from zigpy.exceptions import DeliveryError
import zigpy.profiles
import zigpy.types as zigpy_t
import zigpy.zcl.foundation
import zigpy.zdo.types as zdo_t

import zigpy_znp.commands as c
Expand Down Expand Up @@ -68,16 +69,15 @@
DstEndpoint=1,
SrcEndpoint=1,
ClusterId=6,
TSN=TSN,
Data=bytes([0x01, TSN, 0x01]),
partial=True,
),
responses=[
c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS),
c.AF.DataConfirm.Callback(
lambda req: c.AF.DataConfirm.Callback(
Status=t.Status.SUCCESS,
Endpoint=1,
TSN=TSN,
TSN=req.TSN,
),
c.ZDO.SrcRtgInd.Callback(DstAddr=device.nwk, Relays=[]),
c.AF.IncomingMsg.Callback(
Expand Down Expand Up @@ -127,16 +127,15 @@
DstEndpoint=1,
SrcEndpoint=1,
ClusterId=6,
TSN=TSN,
Data=bytes([0x01, TSN, 0x01]),
partial=True,
),
responses=[
c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS),
c.AF.DataConfirm.Callback(
lambda req: c.AF.DataConfirm.Callback(
Status=t.Status.FAILURE,
Endpoint=1,
TSN=TSN,
TSN=req.TSN,
),
],
)
Expand Down Expand Up @@ -251,7 +250,6 @@
DstPanId=0x0000,
SrcEndpoint=1,
ClusterId=3,
TSN=1,
Radius=3,
Data=b"???",
partial=True,
Expand Down Expand Up @@ -341,6 +339,72 @@
await app.shutdown()


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

device1 = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAAAA)
device2 = app.add_initialized_device(ieee=t.EUI64(range(1, 9)), nwk=0xBBBB)

requests = {}
both_sent = asyncio.Event()

def delayed_confirm(req):
requests[req.DstAddrModeAddress.address] = req

if len(requests) == 2:
both_sent.set()

async def inner():
await both_sent.wait()

# The second device fails and its confirm arrives first
if req.DstAddrModeAddress.address == device1.nwk:
await asyncio.sleep(0.1)
status = t.Status.SUCCESS
else:
status = t.Status.MAC_NO_ACK

znp_server.send(
c.AF.DataConfirm.Callback(Status=status, Endpoint=1, TSN=req.TSN)
)

asyncio.create_task(inner())

znp_server.reply_to(
c.AF.DataRequestExt.Req(partial=True),
responses=[
c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS),
delayed_confirm,
],
)

# Both requests use the same ZCL TSN
rsp1, rsp2 = await asyncio.gather(
*[
app.request(
device=dev,
profile=260,
cluster=1,
src_ep=1,
dst_ep=1,
sequence=1,
data=b"\x00\x01\x00",
)
for dev in (device1, device2)
],
return_exceptions=True,
)

assert requests[device1.nwk].TSN != requests[device2.nwk].TSN
assert rsp1 == (zigpy.zcl.foundation.Status.SUCCESS, "")
assert isinstance(rsp2, DeliveryError)
assert rsp2.status == t.Status.MAC_NO_ACK

await app.shutdown()


@pytest.mark.parametrize("device", FORMED_DEVICES)
async def test_nonstandard_profile(device, make_application):
app, znp_server = make_application(server_cls=device)
Expand Down Expand Up @@ -382,7 +446,7 @@
SecurityUse=t.Bool(False),
TimeStamp=12345678,
TSN=0,
Data=b"\x08" + bytes([req.TSN]) + b"\x0b\x00\x00",
Data=b"\x08" + bytes([req.Data[1]]) + b"\x0b\x00\x00",
MacSrcAddr=device.nwk,
MsgResultRadius=29,
),
Expand Down Expand Up @@ -554,7 +618,7 @@
return c.AF.DataConfirm.Callback(
Status=t.Status.SUCCESS if route_discovered else t.Status.NWK_NO_ROUTE,
Endpoint=1,
TSN=1,
TSN=req.TSN,
)

def set_route_discovered(req):
Expand Down Expand Up @@ -622,7 +686,7 @@
return c.AF.DataConfirm.Callback(
Status=next(data_confirm_statuses),
Endpoint=1,
TSN=1,
TSN=req.TSN,
)

znp_server.reply_to(
Expand Down Expand Up @@ -707,7 +771,7 @@
return c.AF.DataConfirm.Callback(
Status=t.Status.MAC_TRANSACTION_EXPIRED if bad_assoc else final_status,
Endpoint=1,
TSN=1,
TSN=req.TSN,
)

znp_server.reply_to(
Expand Down Expand Up @@ -785,7 +849,7 @@
await req
else:
with pytest.raises(DeliveryError):
await req

Check failure on line 852 in tests/application/test_requests.py

View workflow job for this annotation

GitHub Actions / shared-ci / Run tests Python 3.11

test_request_recovery_assoc_remove[Status.APS_NO_ACK-True-FormedZStack3CC2531] TimeoutError

if fw_assoc_remove:
assert len(did_assoc_remove.mock_calls) >= 1
Expand Down Expand Up @@ -871,17 +935,16 @@
DstEndpoint=packet.dst_ep,
# SrcEndpoint=packet.src_ep,
ClusterId=packet.cluster_id,
TSN=packet.tsn,
Data=packet.data.serialize(),
SourceRoute=packet.source_route,
partial=True,
),
responses=[
c.AF.DataRequestSrcRtg.Rsp(Status=t.Status.SUCCESS),
c.AF.DataConfirm.Callback(
lambda req: c.AF.DataConfirm.Callback(
Status=t.Status.SUCCESS,
Endpoint=packet.dst_ep,
TSN=packet.tsn,
TSN=req.TSN,
),
],
)
Expand Down Expand Up @@ -918,10 +981,10 @@
request=c.AF.DataRequestExt.Req(partial=True),
responses=[
c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS),
c.AF.DataConfirm.Callback(
lambda req: c.AF.DataConfirm.Callback(
Status=t.Status.MAC_NO_ACK,
Endpoint=packet.dst_ep,
TSN=packet.tsn,
TSN=req.TSN,
),
],
)
Expand Down
5 changes: 3 additions & 2 deletions tests/application/test_startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,11 @@ async def test_endpoints(device, make_application):

await app.startup(auto_form=False)

# We currently just register two endpoints
assert len(endpoints) == 2
# ZHA, ZLL, and Green Power
assert len(endpoints) == 3
assert 1 in app._device.endpoints
assert 2 in app._device.endpoints
assert 242 in app._device.endpoints

await app.shutdown()

Expand Down
27 changes: 20 additions & 7 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
from asynctest import CoroutineMock # type:ignore[no-redef] # noqa: F401

import zigpy.endpoint
from zigpy.zcl.clusters.greenpower import GreenPowerProxy
import zigpy.zdo.types as zdo_t
import zigpy.zgp

from zigpy_znp.api import ZNP
import zigpy_znp.commands as c
Expand Down Expand Up @@ -288,6 +290,7 @@ def zdo_request_matcher(
zdo_kwargs = {k: v for k, v in kwargs.items() if k.startswith("zdo_")}

kwargs = {k: v for k, v in kwargs.items() if not k.startswith("zdo_")}
zdo_tsn = kwargs.pop("TSN")
kwargs.setdefault("DstEndpoint", 0x00)
kwargs.setdefault("DstPanId", 0x0000)
kwargs.setdefault("SrcEndpoint", 0x00)
Expand All @@ -297,7 +300,7 @@ def zdo_request_matcher(
return c.AF.DataRequestExt.Req(
DstAddrModeAddress=dst_addr,
ClusterId=command_id,
Data=bytes([kwargs["TSN"]]) + serialize_zdo_command(command_id, **zdo_kwargs),
Data=bytes([zdo_tsn]) + serialize_zdo_command(command_id, **zdo_kwargs),
**kwargs,
partial=True,
)
Expand Down Expand Up @@ -594,6 +597,16 @@ def permit_join(self, request):
c.ZDO.MgmtPermitJoinRsp.Callback(Src=0x0000, Status=t.ZDOStatus.SUCCESS),
]

@reply_to(
c.AF.DataRequestExt.Req(
partial=True,
DstEndpoint=zigpy.zgp.GP_ENDPOINT,
ClusterId=GreenPowerProxy.cluster_id,
)
)
def on_gp_broadcast(self, req):
return [c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS)]

@reply_to(c.AF.DataRequestExt.Req(partial=True, DstEndpoint=0))
def on_zdo_request(self, req):
kwargs = deserialize_zdo_command(req.ClusterId, req.Data[1:])
Expand Down Expand Up @@ -623,7 +636,7 @@ def on_zdo_mgmt_permit_joining_req(self, req, PermitDuration, TC_Significant):
IsBroadcast=t.Bool.false,
ClusterId=zdo_t.ZDOCmd.Mgmt_Permit_Joining_rsp,
SecurityUse=0,
TSN=req.TSN,
TSN=req.Data[0],
MacDst=0x0000,
Data=serialize_zdo_command(
command_id=zdo_t.ZDOCmd.Mgmt_Permit_Joining_rsp,
Expand All @@ -648,7 +661,7 @@ def on_zdo_mgmt_nwk_update_req(self, req, NwkUpdate):
IsBroadcast=t.Bool.false,
ClusterId=zdo_t.ZDOCmd.Mgmt_NWK_Update_rsp,
SecurityUse=0,
TSN=req.TSN,
TSN=req.Data[0],
MacDst=0x0000,
Data=serialize_zdo_command(
command_id=zdo_t.ZDOCmd.Mgmt_NWK_Update_rsp,
Expand Down Expand Up @@ -683,7 +696,7 @@ def on_zdo_active_ep_req(self, req, NWKAddrOfInterest):
IsBroadcast=t.Bool.false,
ClusterId=zdo_t.ZDOCmd.Active_EP_rsp,
SecurityUse=0,
TSN=req.TSN,
TSN=req.Data[0],
MacDst=0x0000,
Data=serialize_zdo_command(
command_id=zdo_t.ZDOCmd.Active_EP_rsp,
Expand Down Expand Up @@ -743,7 +756,7 @@ def on_zdo_simple_desc_req(self, req, NWKAddrOfInterest, EndPoint):
IsBroadcast=t.Bool.false,
ClusterId=zdo_t.ZDOCmd.Simple_Desc_rsp,
SecurityUse=0,
TSN=req.TSN,
TSN=req.Data[0],
MacDst=0x0000,
Data=serialize_zdo_command(
command_id=zdo_t.ZDOCmd.Simple_Desc_rsp,
Expand Down Expand Up @@ -1026,7 +1039,7 @@ def on_zdo_node_desc_req(self, req, NWKAddrOfInterest):
IsBroadcast=t.Bool.false,
ClusterId=zdo_t.ZDOCmd.Node_Desc_rsp,
SecurityUse=0,
TSN=req.TSN,
TSN=req.Data[0],
MacDst=0x0000,
Data=serialize_zdo_command(
command_id=zdo_t.ZDOCmd.Node_Desc_rsp,
Expand Down Expand Up @@ -1281,7 +1294,7 @@ def on_zdo_node_desc_req(self, req, NWKAddrOfInterest):
IsBroadcast=t.Bool.false,
ClusterId=zdo_t.ZDOCmd.Node_Desc_rsp,
SecurityUse=0,
TSN=req.TSN,
TSN=req.Data[0],
MacDst=0x0000,
Data=serialize_zdo_command(
command_id=zdo_t.ZDOCmd.Node_Desc_rsp,
Expand Down
Loading
Loading