zigpy-zboss 2.0.2__tar.gz → 2.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {zigpy_zboss-2.0.2/zigpy_zboss.egg-info → zigpy_zboss-2.0.4}/PKG-INFO +1 -1
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/tests/test_uart.py +36 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/__init__.py +1 -1
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/uart.py +7 -1
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/zigbee/application.py +33 -4
- zigpy_zboss-2.0.4/zigpy_zboss/zigbee/device.py +461 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4/zigpy_zboss.egg-info}/PKG-INFO +1 -1
- zigpy_zboss-2.0.2/zigpy_zboss/zigbee/device.py +0 -331
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/COPYING +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/LICENSE +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/README.md +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/pyproject.toml +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/setup.cfg +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/setup.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/tests/test_commands.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/tests/test_config.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/tests/test_frame.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/tests/test_nvids.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/tests/test_types_basic.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/tests/test_types_cstruct.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/tests/test_types_named.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/tests/test_utils.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/api.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/checksum.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/commands/__init__.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/commands/af.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/commands/aps.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/commands/ncp_config.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/commands/nwk_mgmt.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/commands/security.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/commands/zdo.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/config.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/debug.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/exceptions.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/frames.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/logger.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/nvram.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/tools/__init__.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/tools/config.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/tools/factory_reset_ncp.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/tools/get_ncp_version.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/tools/get_nwk_keys.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/tools/log2pcap.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/types/__init__.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/types/basic.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/types/commands.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/types/cstruct.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/types/named.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/types/nvids.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/types/structs.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/utils.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss/zigbee/__init__.py +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss.egg-info/SOURCES.txt +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss.egg-info/dependency_links.txt +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss.egg-info/requires.txt +0 -0
- {zigpy_zboss-2.0.2 → zigpy_zboss-2.0.4}/zigpy_zboss.egg-info/top_level.txt +0 -0
|
@@ -280,6 +280,42 @@ async def test_connection_lost(dummy_serial_conn, mocker, event_loop):
|
|
|
280
280
|
assert (await conn_lost_fut) == exception
|
|
281
281
|
|
|
282
282
|
|
|
283
|
+
@pytest.mark.asyncio
|
|
284
|
+
async def test_connect_url_port_skips_path_check(mocker, event_loop):
|
|
285
|
+
"""A URL-style port (``socket://``/``tcp://``) has no filesystem path.
|
|
286
|
+
|
|
287
|
+
``connect()`` must not gate on ``os.path.exists`` for such ports, otherwise
|
|
288
|
+
it spins in the retry loop until the timeout and never opens the connection
|
|
289
|
+
(regression for ser2net bridges and native-TCP NCPs).
|
|
290
|
+
"""
|
|
291
|
+
target_url = "socket://127.0.0.1:6638"
|
|
292
|
+
|
|
293
|
+
def create_serial_conn(loop, protocol_factory, url, *args, **kwargs):
|
|
294
|
+
assert url == target_url
|
|
295
|
+
fut = event_loop.create_future()
|
|
296
|
+
protocol = protocol_factory()
|
|
297
|
+
event_loop.add_writer = lambda *a, **k: None
|
|
298
|
+
event_loop.add_reader = lambda *a, **k: None
|
|
299
|
+
event_loop.remove_writer = lambda *a, **k: None
|
|
300
|
+
event_loop.remove_reader = lambda *a, **k: None
|
|
301
|
+
transport = mocker.Mock()
|
|
302
|
+
transport.serial = mocker.Mock()
|
|
303
|
+
protocol.connection_made(transport)
|
|
304
|
+
fut.set_result((transport, protocol))
|
|
305
|
+
return fut
|
|
306
|
+
|
|
307
|
+
mocker.patch("serialx.create_serial_connection", new=create_serial_conn)
|
|
308
|
+
# A socket:// path never exists on disk; the check must be skipped for it.
|
|
309
|
+
exists = mocker.patch("zigpy_zboss.uart.os.path.exists", return_value=False)
|
|
310
|
+
|
|
311
|
+
protocol = await zboss_uart.connect(
|
|
312
|
+
conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: target_url}), api=mocker.Mock()
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
assert protocol is not None
|
|
316
|
+
exists.assert_not_called()
|
|
317
|
+
|
|
318
|
+
|
|
283
319
|
# ToFix: this is not testing the uart test_connection_made method
|
|
284
320
|
# @pytest.mark.asyncio
|
|
285
321
|
# async def test_connection_made(dummy_serial_conn, mocker):
|
|
@@ -288,8 +288,14 @@ async def connect(config: conf.ConfigType, api) -> ZbossNcpProtocol:
|
|
|
288
288
|
last_exc: OSError | None = None
|
|
289
289
|
protocol = None
|
|
290
290
|
|
|
291
|
+
# URL-style ports (``socket://``, ``tcp://``, ``rfc2217://``, …) are not
|
|
292
|
+
# filesystem paths, so the local-device existence check below must be
|
|
293
|
+
# skipped for them; otherwise the loop spins until the timeout and the
|
|
294
|
+
# connection is never opened (e.g. ser2net or a native TCP NCP).
|
|
295
|
+
is_local_device = "://" not in port
|
|
296
|
+
|
|
291
297
|
while loop.time() < deadline:
|
|
292
|
-
if not os.path.exists(port):
|
|
298
|
+
if is_local_device and not os.path.exists(port):
|
|
293
299
|
await asyncio.sleep(CONNECT_OPEN_RETRY_DELAY)
|
|
294
300
|
continue
|
|
295
301
|
|
|
@@ -652,20 +652,42 @@ class ControllerApplication(zigpy.application.ControllerApplication):
|
|
|
652
652
|
if dev:
|
|
653
653
|
self.handle_leave(nwk=dev.nwk, ieee=msg.IEEE)
|
|
654
654
|
|
|
655
|
+
def _should_handle_join(self, nwk: t.NWK, ieee: t.EUI64) -> bool:
|
|
656
|
+
"""Whether a join signal is worth passing on to zigpy.
|
|
657
|
+
|
|
658
|
+
`Device_annce` is a broadcast that routers rebroadcast, so the same
|
|
659
|
+
join arrives several times, and the NCP reports its own indications
|
|
660
|
+
alongside it. `handle_join` restarts a running interview, so the
|
|
661
|
+
repeats have to be dropped while one is still in flight.
|
|
662
|
+
"""
|
|
663
|
+
device = self.devices.get(ieee)
|
|
664
|
+
return device is None or device.nwk != nwk or device.is_initialized
|
|
665
|
+
|
|
655
666
|
def on_zdo_device_announcement(self, msg: c.ZDO.DevAnnceInd.Ind):
|
|
656
667
|
"""ZDO Device announcement command received."""
|
|
657
|
-
self.
|
|
668
|
+
if self._should_handle_join(msg.NWK, msg.IEEE):
|
|
669
|
+
self.handle_join(nwk=msg.NWK, ieee=msg.IEEE, parent_nwk=None)
|
|
658
670
|
|
|
659
671
|
def on_dev_update(self, msg: c.ZDO.DevUpdateInd.Ind):
|
|
660
672
|
"""Device update indication."""
|
|
661
673
|
if msg.Status == t_zboss.DeviceUpdateStatus.secured_rejoin:
|
|
662
|
-
self.
|
|
674
|
+
if self._should_handle_join(msg.Nwk, msg.IEEE):
|
|
675
|
+
self.handle_join(msg.Nwk, msg.IEEE, 0x0000)
|
|
663
676
|
elif msg.Status == t_zboss.DeviceUpdateStatus.unsecured_join:
|
|
664
|
-
|
|
677
|
+
# A fresh association: the device has no network key yet and
|
|
678
|
+
# cannot answer, so interviewing it now only adds traffic while
|
|
679
|
+
# it is still finishing the handshake. The announcement or the
|
|
680
|
+
# authorization indication brings it in once it is really on.
|
|
681
|
+
LOGGER.debug(
|
|
682
|
+
"Device %s associated as 0x%04x, waiting for it to join",
|
|
683
|
+
msg.IEEE,
|
|
684
|
+
msg.Nwk,
|
|
685
|
+
)
|
|
665
686
|
elif msg.Status == t_zboss.DeviceUpdateStatus.device_left:
|
|
666
687
|
self.handle_leave(msg.Nwk, msg.IEEE)
|
|
667
688
|
elif msg.Status == t_zboss.DeviceUpdateStatus.tc_rejoin:
|
|
668
|
-
self.
|
|
689
|
+
if self._should_handle_join(msg.Nwk, msg.IEEE):
|
|
690
|
+
self.handle_join(msg.Nwk, msg.IEEE, 0x0000)
|
|
669
691
|
|
|
670
692
|
def on_dev_authorized(self, msg: c.ZDO.DevAuthorizedInd.Ind):
|
|
671
693
|
"""TC device authorized indication."""
|
|
@@ -677,6 +699,13 @@ class ControllerApplication(zigpy.application.ControllerApplication):
|
|
|
677
699
|
)
|
|
678
700
|
# ZBOSS NCP will kick the device and reset; proactively clean up
|
|
679
701
|
self.handle_leave(msg.Nwk, msg.IEEE)
|
|
702
|
+
return
|
|
703
|
+
|
|
704
|
+
# `Device_annce` is a broadcast and can be lost, while this
|
|
705
|
+
# indication is generated locally by the NCP, so it has to be able
|
|
706
|
+
# to bring the device in on its own.
|
|
707
|
+
if self._should_handle_join(msg.Nwk, msg.IEEE):
|
|
708
|
+
self.handle_join(msg.Nwk, msg.IEEE, 0x0000)
|
|
680
709
|
|
|
681
710
|
def on_apsde_indication(self, msg):
|
|
682
711
|
"""APSDE-DATA.indication handler."""
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
"""Zigbee device object."""
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import zigpy.device
|
|
6
|
+
import zigpy.endpoint
|
|
7
|
+
import zigpy.exceptions
|
|
8
|
+
import zigpy.types as t
|
|
9
|
+
import zigpy.util
|
|
10
|
+
from zigpy.zdo import ZDO as ZigpyZDO
|
|
11
|
+
from zigpy.zdo import types as zdo_t
|
|
12
|
+
|
|
13
|
+
import zigpy_zboss.types as t_zboss
|
|
14
|
+
from zigpy_zboss import commands as c
|
|
15
|
+
|
|
16
|
+
LOGGER = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# A ZDO command is only answered once the on-air transaction resolves,
|
|
19
|
+
# which takes the NCP up to a minute for an unreachable device. zigpy
|
|
20
|
+
# cancels its own requests long before this, so it is only a backstop
|
|
21
|
+
# for paths that impose no deadline of their own, such as broadcasts.
|
|
22
|
+
ZDO_TIMEOUT = 70
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ZbossZDO(ZigpyZDO):
|
|
26
|
+
"""The ZDO endpoint of a ZBOSS device.
|
|
27
|
+
|
|
28
|
+
The ZBOSS NCP has no APSDE path for ZDO: every ZDO request has to be sent
|
|
29
|
+
as its own NCP command. Rather than overriding the individual `*_req`
|
|
30
|
+
methods, which quirks discard when they replace a device object, the
|
|
31
|
+
translation happens in `zboss_specific_cmd` on the outgoing packet. That
|
|
32
|
+
hook lives on the coordinator, which is never quirked, so every device
|
|
33
|
+
takes the same path and gets standard zigpy return values.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def handle_mgmt_permit_joining_req(
|
|
37
|
+
self,
|
|
38
|
+
permit_duration: int,
|
|
39
|
+
tc_significance: int,
|
|
40
|
+
):
|
|
41
|
+
"""Handle ZDO permit joining request."""
|
|
42
|
+
hdr = zdo_t.ZDOHeader(zdo_t.ZDOCmd.Mgmt_Permit_Joining_req, 0)
|
|
43
|
+
dst_addressing = t.Addressing.IEEE
|
|
44
|
+
|
|
45
|
+
self.listener_event("permit_duration", permit_duration)
|
|
46
|
+
self.listener_event(
|
|
47
|
+
"zdo_mgmt_permit_joining_req",
|
|
48
|
+
self._device,
|
|
49
|
+
dst_addressing,
|
|
50
|
+
hdr,
|
|
51
|
+
(permit_duration, tc_significance),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
async def Mgmt_NWK_Update_req(self, nwkUpdate):
|
|
55
|
+
"""Issue a ZDO Mgmt_NWK_Update (energy scan / channel change).
|
|
56
|
+
|
|
57
|
+
Kept as an override: an energy scan can take longer than zigpy's APS
|
|
58
|
+
reply timeout, so it must not go through `Device.request`. It is only
|
|
59
|
+
ever issued on the coordinator, which quirks never replace.
|
|
60
|
+
"""
|
|
61
|
+
res = await self._device._application._api.request(
|
|
62
|
+
c.ZDO.MgmtNwkUpdate.Req(
|
|
63
|
+
TSN=self._device._application.get_sequence(),
|
|
64
|
+
ScanChannelMask=nwkUpdate.ScanChannels,
|
|
65
|
+
ScanDuration=nwkUpdate.ScanDuration,
|
|
66
|
+
ScanCount=nwkUpdate.ScanCount or 0,
|
|
67
|
+
MgrAddr=self._device.nwk,
|
|
68
|
+
DstNWK=t.NWK(0x0000),
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
if res.StatusCode != 0:
|
|
72
|
+
raise zigpy.exceptions.DeliveryError(
|
|
73
|
+
f"Mgmt_NWK_Update_req failed: {res.StatusCode}",
|
|
74
|
+
status=res.StatusCode,
|
|
75
|
+
)
|
|
76
|
+
return (None, res.ScannedChannels, None, None, res.EnergyValues)
|
|
77
|
+
|
|
78
|
+
async def zboss_specific_cmd(self, packet: t.ZigbeePacket) -> None:
|
|
79
|
+
"""Reroute ZDO packets sent over APSDE to ZBOSS ZDO commands."""
|
|
80
|
+
try:
|
|
81
|
+
zdo_hdr, zdo_args = self.deserialize(
|
|
82
|
+
cluster_id=packet.cluster_id, data=packet.data.serialize()
|
|
83
|
+
)
|
|
84
|
+
except ValueError:
|
|
85
|
+
LOGGER.debug("Could not parse ZDO message from packet")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
handler = self._ZDO_PACKET_HANDLERS.get(zdo_hdr.command_id)
|
|
89
|
+
|
|
90
|
+
if handler is not None:
|
|
91
|
+
await handler(self, packet, zdo_hdr, zdo_args)
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
is_response = bool(zdo_hdr.command_id & 0x8000)
|
|
95
|
+
is_unicast = packet.dst.addr_mode in (t.AddrMode.NWK, t.AddrMode.IEEE)
|
|
96
|
+
|
|
97
|
+
# Only unicast requests have a caller waiting on a reply. Dropping one
|
|
98
|
+
# silently would stall that caller until it times out.
|
|
99
|
+
if is_response or not is_unicast:
|
|
100
|
+
LOGGER.debug("Dropping unsupported ZDO packet: %r", packet)
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
raise zigpy.exceptions.DeliveryError(
|
|
104
|
+
f"ZBOSS NCP cannot send ZDO command {zdo_hdr.command_id!r}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def _api(self):
|
|
109
|
+
"""Return the NCP API of the application this ZDO belongs to."""
|
|
110
|
+
return self._device._application._api
|
|
111
|
+
|
|
112
|
+
def _next_tsn(self) -> int:
|
|
113
|
+
"""Return the next NCP transmission sequence number."""
|
|
114
|
+
return self._device._application.get_sequence()
|
|
115
|
+
|
|
116
|
+
def _target_nwk(self, packet: t.ZigbeePacket) -> t.NWK:
|
|
117
|
+
"""Return the NWK address a ZDO packet is addressed to."""
|
|
118
|
+
if packet.dst.addr_mode != t.AddrMode.IEEE:
|
|
119
|
+
return t.NWK(packet.dst.address)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
device = self._device._application.get_device(
|
|
123
|
+
ieee=packet.dst.address
|
|
124
|
+
)
|
|
125
|
+
except KeyError as exc:
|
|
126
|
+
raise zigpy.exceptions.DeliveryError(
|
|
127
|
+
f"Cannot send ZDO request to unknown device {packet.dst!r}"
|
|
128
|
+
) from exc
|
|
129
|
+
|
|
130
|
+
return t.NWK(device.nwk)
|
|
131
|
+
|
|
132
|
+
def _send_zdo_rsp(
|
|
133
|
+
self,
|
|
134
|
+
packet: t.ZigbeePacket,
|
|
135
|
+
zdo_hdr: zdo_t.ZDOHeader,
|
|
136
|
+
*values: Any,
|
|
137
|
+
src: t.AddrModeAddress | None = None) -> None:
|
|
138
|
+
"""Feed a synthesized ZDO response back into zigpy.
|
|
139
|
+
|
|
140
|
+
`src` defaults to the request's destination, which is what zigpy
|
|
141
|
+
matches the pending request against. Broadcast requests have to
|
|
142
|
+
override it with the address that actually answered.
|
|
143
|
+
"""
|
|
144
|
+
rsp_cmd = zdo_t.ZDOCmd(zdo_hdr.command_id | 0x8000)
|
|
145
|
+
_, schema = zdo_t.CLUSTERS[rsp_cmd]
|
|
146
|
+
|
|
147
|
+
args = list(values)
|
|
148
|
+
while args and args[-1] is None:
|
|
149
|
+
args.pop()
|
|
150
|
+
|
|
151
|
+
data = t.uint8_t(zdo_hdr.tsn).serialize() + t.serialize(args, schema)
|
|
152
|
+
|
|
153
|
+
self._device._application.packet_received(
|
|
154
|
+
t.ZigbeePacket(
|
|
155
|
+
src=packet.dst if src is None else src,
|
|
156
|
+
src_ep=0,
|
|
157
|
+
dst=t.AddrModeAddress(
|
|
158
|
+
addr_mode=t.AddrMode.NWK,
|
|
159
|
+
address=t.NWK(0x0000),
|
|
160
|
+
),
|
|
161
|
+
dst_ep=0,
|
|
162
|
+
tsn=zdo_hdr.tsn,
|
|
163
|
+
profile_id=0,
|
|
164
|
+
cluster_id=rsp_cmd,
|
|
165
|
+
data=t.SerializableBytes(data),
|
|
166
|
+
tx_options=t.TransmitOptions.NONE,
|
|
167
|
+
lqi=None,
|
|
168
|
+
rssi=None,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _bind_dst_address(dst_address: zdo_t.MultiAddress):
|
|
174
|
+
"""Convert a ZDO bind destination into ZBOSS addressing."""
|
|
175
|
+
if dst_address.addrmode == t.AddrMode.IEEE:
|
|
176
|
+
return (
|
|
177
|
+
t_zboss.BindAddrMode.IEEE,
|
|
178
|
+
dst_address.ieee,
|
|
179
|
+
dst_address.endpoint,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# ZBOSS does not support the NWK mode for binding
|
|
183
|
+
if dst_address.addrmode != t.AddrMode.Group:
|
|
184
|
+
raise zigpy.exceptions.DeliveryError(
|
|
185
|
+
f"Unsupported bind address mode: {dst_address.addrmode!r}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# A group destination carries no endpoint, but ZBOSS always wants one
|
|
189
|
+
return (
|
|
190
|
+
t_zboss.BindAddrMode.Group,
|
|
191
|
+
t.EUI64(
|
|
192
|
+
[
|
|
193
|
+
dst_address.nwk % 0x100,
|
|
194
|
+
dst_address.nwk >> 8,
|
|
195
|
+
0,
|
|
196
|
+
0,
|
|
197
|
+
0,
|
|
198
|
+
0,
|
|
199
|
+
0,
|
|
200
|
+
0,
|
|
201
|
+
]
|
|
202
|
+
),
|
|
203
|
+
t.uint8_t(0),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
async def _bind_req(
|
|
207
|
+
self,
|
|
208
|
+
packet: t.ZigbeePacket,
|
|
209
|
+
zdo_hdr: zdo_t.ZDOHeader,
|
|
210
|
+
zdo_args: tuple[Any]) -> None:
|
|
211
|
+
"""Send ZDO bind request and handle the response."""
|
|
212
|
+
src_ieee, src_ep, cluster_id, dst_address = zdo_args
|
|
213
|
+
addr_mode, dst_addr, dst_ep = self._bind_dst_address(dst_address)
|
|
214
|
+
|
|
215
|
+
res = await self._api.request(
|
|
216
|
+
c.ZDO.BindReq.Req(
|
|
217
|
+
TSN=self._next_tsn(),
|
|
218
|
+
TargetNwkAddr=self._target_nwk(packet),
|
|
219
|
+
SrcIEEE=src_ieee,
|
|
220
|
+
SrcEndpoint=src_ep,
|
|
221
|
+
ClusterId=cluster_id,
|
|
222
|
+
DstAddrMode=addr_mode,
|
|
223
|
+
DstAddr=dst_addr,
|
|
224
|
+
DstEndpoint=dst_ep,
|
|
225
|
+
),
|
|
226
|
+
timeout=ZDO_TIMEOUT,
|
|
227
|
+
)
|
|
228
|
+
self._send_zdo_rsp(packet, zdo_hdr, res.StatusCode)
|
|
229
|
+
|
|
230
|
+
async def _unbind_req(
|
|
231
|
+
self,
|
|
232
|
+
packet: t.ZigbeePacket,
|
|
233
|
+
zdo_hdr: zdo_t.ZDOHeader,
|
|
234
|
+
zdo_args: tuple[Any]) -> None:
|
|
235
|
+
"""Send ZDO unbind request and handle the response."""
|
|
236
|
+
src_ieee, src_ep, cluster_id, dst_address = zdo_args
|
|
237
|
+
addr_mode, dst_addr, dst_ep = self._bind_dst_address(dst_address)
|
|
238
|
+
|
|
239
|
+
res = await self._api.request(
|
|
240
|
+
c.ZDO.UnbindReq.Req(
|
|
241
|
+
TSN=self._next_tsn(),
|
|
242
|
+
TargetNwkAddr=self._target_nwk(packet),
|
|
243
|
+
SrcIEEE=src_ieee,
|
|
244
|
+
SrcEndpoint=src_ep,
|
|
245
|
+
ClusterId=cluster_id,
|
|
246
|
+
DstAddrMode=addr_mode,
|
|
247
|
+
DstAddr=dst_addr,
|
|
248
|
+
DstEndpoint=dst_ep,
|
|
249
|
+
),
|
|
250
|
+
timeout=ZDO_TIMEOUT,
|
|
251
|
+
)
|
|
252
|
+
self._send_zdo_rsp(packet, zdo_hdr, res.StatusCode)
|
|
253
|
+
|
|
254
|
+
async def _node_desc_req(
|
|
255
|
+
self,
|
|
256
|
+
packet: t.ZigbeePacket,
|
|
257
|
+
zdo_hdr: zdo_t.ZDOHeader,
|
|
258
|
+
zdo_args: tuple[Any]) -> None:
|
|
259
|
+
"""Send ZDO node descriptor request and handle the response."""
|
|
260
|
+
(nwk,) = zdo_args
|
|
261
|
+
res = await self._api.request(
|
|
262
|
+
c.ZDO.NodeDescReq.Req(TSN=self._next_tsn(), NwkAddr=nwk),
|
|
263
|
+
timeout=ZDO_TIMEOUT,
|
|
264
|
+
)
|
|
265
|
+
node_desc = None if res.StatusCode else res.NodeDesc
|
|
266
|
+
|
|
267
|
+
self._send_zdo_rsp(packet, zdo_hdr, res.StatusCode, nwk, node_desc)
|
|
268
|
+
|
|
269
|
+
async def _active_ep_req(
|
|
270
|
+
self,
|
|
271
|
+
packet: t.ZigbeePacket,
|
|
272
|
+
zdo_hdr: zdo_t.ZDOHeader,
|
|
273
|
+
zdo_args: tuple[Any]) -> None:
|
|
274
|
+
"""Send ZDO active endpoints request and handle the response."""
|
|
275
|
+
(nwk,) = zdo_args
|
|
276
|
+
res = await self._api.request(
|
|
277
|
+
c.ZDO.ActiveEpReq.Req(TSN=self._next_tsn(), NwkAddr=nwk),
|
|
278
|
+
timeout=ZDO_TIMEOUT,
|
|
279
|
+
)
|
|
280
|
+
# Active_EP_rsp has no optional endpoint list, so it cannot be omitted
|
|
281
|
+
endpoints = [] if res.StatusCode else res.ActiveEpList
|
|
282
|
+
|
|
283
|
+
self._send_zdo_rsp(packet, zdo_hdr, res.StatusCode, nwk, endpoints)
|
|
284
|
+
|
|
285
|
+
async def _simple_desc_req(
|
|
286
|
+
self,
|
|
287
|
+
packet: t.ZigbeePacket,
|
|
288
|
+
zdo_hdr: zdo_t.ZDOHeader,
|
|
289
|
+
zdo_args: tuple[Any]) -> None:
|
|
290
|
+
"""Send ZDO simple descriptor request and handle the response."""
|
|
291
|
+
nwk, endpoint = zdo_args
|
|
292
|
+
res = await self._api.request(
|
|
293
|
+
c.ZDO.SimpleDescriptorReq.Req(
|
|
294
|
+
TSN=self._next_tsn(), NwkAddr=nwk, Endpoint=endpoint
|
|
295
|
+
),
|
|
296
|
+
timeout=ZDO_TIMEOUT,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if res.StatusCode:
|
|
300
|
+
desc = None
|
|
301
|
+
else:
|
|
302
|
+
desc = zdo_t.SizePrefixedSimpleDescriptor(
|
|
303
|
+
endpoint=res.SimpleDesc.endpoint,
|
|
304
|
+
profile=res.SimpleDesc.profile,
|
|
305
|
+
device_type=res.SimpleDesc.device_type,
|
|
306
|
+
device_version=res.SimpleDesc.device_version,
|
|
307
|
+
input_clusters=res.SimpleDesc.input_clusters,
|
|
308
|
+
output_clusters=res.SimpleDesc.output_clusters,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
self._send_zdo_rsp(packet, zdo_hdr, res.StatusCode, nwk, desc)
|
|
312
|
+
|
|
313
|
+
async def _mgmt_lqi_req(
|
|
314
|
+
self,
|
|
315
|
+
packet: t.ZigbeePacket,
|
|
316
|
+
zdo_hdr: zdo_t.ZDOHeader,
|
|
317
|
+
zdo_args: tuple[Any]) -> None:
|
|
318
|
+
"""Send ZDO LQI request and handle the response."""
|
|
319
|
+
(start_index,) = zdo_args
|
|
320
|
+
res = await self._api.request(
|
|
321
|
+
c.ZDO.MgmtLqi.Req(
|
|
322
|
+
TSN=self._next_tsn(),
|
|
323
|
+
DestNWK=self._target_nwk(packet),
|
|
324
|
+
Index=start_index,
|
|
325
|
+
),
|
|
326
|
+
timeout=ZDO_TIMEOUT,
|
|
327
|
+
)
|
|
328
|
+
neighbors = None if res.StatusCode else res.Neighbors
|
|
329
|
+
|
|
330
|
+
self._send_zdo_rsp(packet, zdo_hdr, res.StatusCode, neighbors)
|
|
331
|
+
|
|
332
|
+
async def _mgmt_leave_req(
|
|
333
|
+
self,
|
|
334
|
+
packet: t.ZigbeePacket,
|
|
335
|
+
zdo_hdr: zdo_t.ZDOHeader,
|
|
336
|
+
zdo_args: tuple[Any]) -> None:
|
|
337
|
+
"""Send ZDO leave request and handle the response."""
|
|
338
|
+
ieee, options = zdo_args
|
|
339
|
+
res = await self._api.request(
|
|
340
|
+
c.ZDO.MgtLeave.Req(
|
|
341
|
+
TSN=self._next_tsn(),
|
|
342
|
+
DestNWK=self._target_nwk(packet),
|
|
343
|
+
IEEE=t.EUI64(ieee),
|
|
344
|
+
Flags=t.uint8_t(options),
|
|
345
|
+
),
|
|
346
|
+
timeout=ZDO_TIMEOUT,
|
|
347
|
+
)
|
|
348
|
+
self._send_zdo_rsp(packet, zdo_hdr, res.StatusCode)
|
|
349
|
+
|
|
350
|
+
async def _mgmt_permit_joining_req(
|
|
351
|
+
self,
|
|
352
|
+
packet: t.ZigbeePacket,
|
|
353
|
+
zdo_hdr: zdo_t.ZDOHeader,
|
|
354
|
+
zdo_args: tuple[Any]) -> None:
|
|
355
|
+
"""Send ZDO permit joining request and handle the response.
|
|
356
|
+
|
|
357
|
+
`app.permit()` already broadcasts this and then opens the coordinator
|
|
358
|
+
via `permit_ncp`, so forwarding the broadcast here would send it
|
|
359
|
+
twice. Only the targeted form is translated.
|
|
360
|
+
"""
|
|
361
|
+
if packet.dst.addr_mode not in (t.AddrMode.NWK, t.AddrMode.IEEE):
|
|
362
|
+
LOGGER.debug("Dropping broadcast permit joining request")
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
duration, tc_significance = zdo_args
|
|
366
|
+
res = await self._api.request(
|
|
367
|
+
c.ZDO.PermitJoin.Req(
|
|
368
|
+
TSN=self._next_tsn(),
|
|
369
|
+
# Targeting `self._target_nwk(packet)` would be correct, but
|
|
370
|
+
# this has always gone out as a broadcast and changing where
|
|
371
|
+
# joins are opened deserves its own change.
|
|
372
|
+
DestNWK=t.NWK(t.BroadcastAddress.RX_ON_WHEN_IDLE),
|
|
373
|
+
PermitDuration=t.uint8_t(duration),
|
|
374
|
+
TCSignificance=t.uint8_t(tc_significance),
|
|
375
|
+
),
|
|
376
|
+
timeout=ZDO_TIMEOUT,
|
|
377
|
+
)
|
|
378
|
+
self._send_zdo_rsp(packet, zdo_hdr, res.StatusCode)
|
|
379
|
+
|
|
380
|
+
async def _IEEE_addr_req(
|
|
381
|
+
self,
|
|
382
|
+
packet: t.ZigbeePacket,
|
|
383
|
+
zdo_hdr: zdo_t.ZDOHeader,
|
|
384
|
+
zdo_args: tuple[Any]) -> None:
|
|
385
|
+
"""Send ZDO IEEE addr request and handle the response."""
|
|
386
|
+
nwki, req_type, index = zdo_args
|
|
387
|
+
res = await self._api.request(
|
|
388
|
+
c.ZDO.IeeeAddrReq.Req(
|
|
389
|
+
TSN=zdo_hdr.tsn,
|
|
390
|
+
DstNWK=packet.dst.address,
|
|
391
|
+
NWKtoMatch=nwki,
|
|
392
|
+
RequestType=req_type,
|
|
393
|
+
StartIndex=index,
|
|
394
|
+
),
|
|
395
|
+
timeout=ZDO_TIMEOUT,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
if res.StatusCode:
|
|
399
|
+
# ZDO command failed, use dummy values.
|
|
400
|
+
ieee = t.EUI64([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
|
|
401
|
+
nwki = t.NWK(0xffff)
|
|
402
|
+
src_ad = t.NWK(0x0000)
|
|
403
|
+
else:
|
|
404
|
+
ieee = res.RemoteDevIEEE
|
|
405
|
+
nwki = res.RemoteDevNWK
|
|
406
|
+
src_ad = res.RemoteDevNWK
|
|
407
|
+
|
|
408
|
+
# This request is broadcast, so the reply comes from the device that
|
|
409
|
+
# matched it rather than from the packet's destination.
|
|
410
|
+
self._send_zdo_rsp(
|
|
411
|
+
packet,
|
|
412
|
+
zdo_hdr,
|
|
413
|
+
res.StatusCode,
|
|
414
|
+
ieee,
|
|
415
|
+
nwki,
|
|
416
|
+
src=t.AddrModeAddress(
|
|
417
|
+
addr_mode=t.AddrMode.NWK,
|
|
418
|
+
address=src_ad,
|
|
419
|
+
),
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
_ZDO_PACKET_HANDLERS = {
|
|
423
|
+
zdo_t.ZDOCmd.IEEE_addr_req: _IEEE_addr_req,
|
|
424
|
+
zdo_t.ZDOCmd.Node_Desc_req: _node_desc_req,
|
|
425
|
+
zdo_t.ZDOCmd.Simple_Desc_req: _simple_desc_req,
|
|
426
|
+
zdo_t.ZDOCmd.Active_EP_req: _active_ep_req,
|
|
427
|
+
zdo_t.ZDOCmd.Bind_req: _bind_req,
|
|
428
|
+
zdo_t.ZDOCmd.Unbind_req: _unbind_req,
|
|
429
|
+
zdo_t.ZDOCmd.Mgmt_Lqi_req: _mgmt_lqi_req,
|
|
430
|
+
zdo_t.ZDOCmd.Mgmt_Leave_req: _mgmt_leave_req,
|
|
431
|
+
zdo_t.ZDOCmd.Mgmt_Permit_Joining_req: _mgmt_permit_joining_req,
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class ZbossDevice(zigpy.device.Device):
|
|
436
|
+
"""Class representing an nRF device."""
|
|
437
|
+
|
|
438
|
+
def __init__(self, *args, **kwargs):
|
|
439
|
+
"""Initialize instance."""
|
|
440
|
+
super().__init__(*args, **kwargs)
|
|
441
|
+
assert hasattr(self, "zdo")
|
|
442
|
+
self.zdo = ZbossZDO(self)
|
|
443
|
+
self.endpoints[0] = self.zdo
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class ZbossCoordinator(ZbossDevice):
|
|
447
|
+
"""Zigpy Device representing the controller."""
|
|
448
|
+
|
|
449
|
+
def __init__(self, *args, **kwargs):
|
|
450
|
+
"""Initialize instance."""
|
|
451
|
+
super().__init__(*args, **kwargs)
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def manufacturer(self):
|
|
455
|
+
"""Return manufacturer."""
|
|
456
|
+
return "Nordic Semiconductor"
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def model(self):
|
|
460
|
+
"""Return model."""
|
|
461
|
+
return "nRF52840"
|
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
"""Zigbee device object."""
|
|
2
|
-
import logging
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
import zigpy.device
|
|
6
|
-
import zigpy.endpoint
|
|
7
|
-
import zigpy.types as t
|
|
8
|
-
import zigpy.util
|
|
9
|
-
from zigpy.zdo import ZDO as ZigpyZDO
|
|
10
|
-
from zigpy.zdo import types as zdo_t
|
|
11
|
-
|
|
12
|
-
import zigpy_zboss.types as t_zboss
|
|
13
|
-
from zigpy_zboss import commands as c
|
|
14
|
-
|
|
15
|
-
LOGGER = logging.getLogger(__name__)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class ZbossZDO(ZigpyZDO):
|
|
19
|
-
"""The ZDO endpoint of a ZBOSS device."""
|
|
20
|
-
|
|
21
|
-
def handle_mgmt_permit_joining_req(
|
|
22
|
-
self,
|
|
23
|
-
permit_duration: int,
|
|
24
|
-
tc_significance: int,
|
|
25
|
-
):
|
|
26
|
-
"""Handle ZDO permit joining request."""
|
|
27
|
-
hdr = zdo_t.ZDOHeader(zdo_t.ZDOCmd.Mgmt_Permit_Joining_req, 0)
|
|
28
|
-
dst_addressing = t.Addressing.IEEE
|
|
29
|
-
|
|
30
|
-
self.listener_event("permit_duration", permit_duration)
|
|
31
|
-
self.listener_event(
|
|
32
|
-
"zdo_mgmt_permit_joining_req",
|
|
33
|
-
self._device,
|
|
34
|
-
dst_addressing,
|
|
35
|
-
hdr,
|
|
36
|
-
(permit_duration, tc_significance),
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
async def Bind_req(self, eui64, ep, cluster, dst_address):
|
|
40
|
-
"""Binding request."""
|
|
41
|
-
if dst_address.addrmode == t.AddrMode.IEEE:
|
|
42
|
-
addr_mode = t_zboss.BindAddrMode.IEEE
|
|
43
|
-
dst_eui64 = dst_address.ieee
|
|
44
|
-
# ZBOSS does not support the NWK mode for binding
|
|
45
|
-
elif dst_address.addrmode == t.AddrMode.NWK:
|
|
46
|
-
self.log(
|
|
47
|
-
logging.WARNING,
|
|
48
|
-
"Nwk address mode is not supported for the Bind request."
|
|
49
|
-
)
|
|
50
|
-
elif dst_address.addrmode == t.AddrMode.Group:
|
|
51
|
-
addr_mode = t_zboss.BindAddrMode.Group
|
|
52
|
-
dst_eui64 = [
|
|
53
|
-
dst_address.nwk % 0x100,
|
|
54
|
-
dst_address.nwk >> 8,
|
|
55
|
-
0,
|
|
56
|
-
0,
|
|
57
|
-
0,
|
|
58
|
-
0,
|
|
59
|
-
0,
|
|
60
|
-
0,
|
|
61
|
-
]
|
|
62
|
-
|
|
63
|
-
res = await self._device._application._api.request(
|
|
64
|
-
c.ZDO.BindReq.Req(
|
|
65
|
-
TSN=self._device._application.get_sequence(),
|
|
66
|
-
TargetNwkAddr=self._device.nwk,
|
|
67
|
-
SrcIEEE=eui64,
|
|
68
|
-
SrcEndpoint=ep,
|
|
69
|
-
ClusterId=cluster,
|
|
70
|
-
DstAddrMode=addr_mode,
|
|
71
|
-
DstAddr=dst_eui64,
|
|
72
|
-
DstEndpoint=dst_address.endpoint,
|
|
73
|
-
)
|
|
74
|
-
)
|
|
75
|
-
if res.StatusCode != 0:
|
|
76
|
-
return (res.StatusCode % 0xFF, dst_address, cluster)
|
|
77
|
-
|
|
78
|
-
return (zdo_t.Status.SUCCESS, dst_address, cluster)
|
|
79
|
-
|
|
80
|
-
async def Unbind_req(self, eui64, ep, cluster, dst_address):
|
|
81
|
-
"""Unbinding request."""
|
|
82
|
-
if dst_address.addrmode == t.AddrMode.IEEE:
|
|
83
|
-
addr_mode = t_zboss.BindAddrMode.IEEE
|
|
84
|
-
dst_eui64 = t.Addressing.IEEE
|
|
85
|
-
# ZBOSS does not support the NWK mode for binding
|
|
86
|
-
elif dst_address.addrmode == t.AddrMode.NWK:
|
|
87
|
-
self.log(
|
|
88
|
-
logging.WARNING,
|
|
89
|
-
"Nwk address mode is not supported for the Unbind request."
|
|
90
|
-
)
|
|
91
|
-
elif dst_address.addrmode == t.AddrMode.Group:
|
|
92
|
-
addr_mode = t_zboss.BindAddrMode.Group
|
|
93
|
-
dst_eui64 = [
|
|
94
|
-
dst_address.nwk % 0x100,
|
|
95
|
-
dst_address.nwk >> 8,
|
|
96
|
-
0,
|
|
97
|
-
0,
|
|
98
|
-
0,
|
|
99
|
-
0,
|
|
100
|
-
0,
|
|
101
|
-
0,
|
|
102
|
-
]
|
|
103
|
-
|
|
104
|
-
res = await self._device._application._api.request(
|
|
105
|
-
c.ZDO.UnbindReq.Req(
|
|
106
|
-
TSN=self._device._application.get_sequence(),
|
|
107
|
-
TargetNwkAddr=self._device.nwk,
|
|
108
|
-
SrcIEEE=eui64,
|
|
109
|
-
SrcEndpoint=ep,
|
|
110
|
-
ClusterId=cluster,
|
|
111
|
-
DstAddrMode=addr_mode,
|
|
112
|
-
DstAddr=dst_eui64,
|
|
113
|
-
DstEndpoint=dst_address.endpoint,
|
|
114
|
-
)
|
|
115
|
-
)
|
|
116
|
-
if res.StatusCode != 0:
|
|
117
|
-
return (res.StatusCode % 0xFF, dst_address, cluster)
|
|
118
|
-
|
|
119
|
-
return (zdo_t.Status.SUCCESS, dst_address, cluster)
|
|
120
|
-
|
|
121
|
-
def request(self, command, *args, use_ieee=False):
|
|
122
|
-
"""Request overwrite for Bind/Unbind requests."""
|
|
123
|
-
if command == zdo_t.ZDOCmd.Bind_req:
|
|
124
|
-
return self.Bind_req(*args)
|
|
125
|
-
if command == zdo_t.ZDOCmd.Unbind_req:
|
|
126
|
-
return self.Unbind_req(*args)
|
|
127
|
-
return super().request(command, *args, use_ieee=use_ieee)
|
|
128
|
-
|
|
129
|
-
async def Node_Desc_req(self, nwk):
|
|
130
|
-
"""Node descriptor request."""
|
|
131
|
-
res = await self._device._application._api.request(
|
|
132
|
-
c.ZDO.NodeDescReq.Req(
|
|
133
|
-
TSN=self._device._application.get_sequence(),
|
|
134
|
-
NwkAddr=nwk
|
|
135
|
-
)
|
|
136
|
-
)
|
|
137
|
-
if res.StatusCode != 0:
|
|
138
|
-
return (res.StatusCode, None, None)
|
|
139
|
-
|
|
140
|
-
return (zdo_t.Status.SUCCESS, None, res.NodeDesc)
|
|
141
|
-
|
|
142
|
-
async def Simple_Desc_req(self, nwk, ep):
|
|
143
|
-
"""Request simple descriptor."""
|
|
144
|
-
res = await self._device._application._api.request(
|
|
145
|
-
c.ZDO.SimpleDescriptorReq.Req(
|
|
146
|
-
TSN=self._device._application.get_sequence(),
|
|
147
|
-
NwkAddr=nwk,
|
|
148
|
-
Endpoint=ep
|
|
149
|
-
)
|
|
150
|
-
)
|
|
151
|
-
if res.StatusCode != 0:
|
|
152
|
-
return (res.StatusCode, None, None)
|
|
153
|
-
|
|
154
|
-
desc = zdo_t.SimpleDescriptor(
|
|
155
|
-
endpoint=res.SimpleDesc.endpoint,
|
|
156
|
-
profile=res.SimpleDesc.profile,
|
|
157
|
-
device_type=res.SimpleDesc.device_type,
|
|
158
|
-
device_version=res.SimpleDesc.device_version,
|
|
159
|
-
input_clusters=res.SimpleDesc.input_clusters,
|
|
160
|
-
output_clusters=res.SimpleDesc.output_clusters,
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
return (zdo_t.Status.SUCCESS, None, desc)
|
|
164
|
-
|
|
165
|
-
async def Active_EP_req(self, nwk):
|
|
166
|
-
"""Request active end points."""
|
|
167
|
-
res = await self._device._application._api.request(
|
|
168
|
-
c.ZDO.ActiveEpReq.Req(
|
|
169
|
-
TSN=self._device._application.get_sequence(),
|
|
170
|
-
NwkAddr=nwk
|
|
171
|
-
)
|
|
172
|
-
)
|
|
173
|
-
if res.StatusCode != 0:
|
|
174
|
-
return (res.StatusCode, None, None)
|
|
175
|
-
|
|
176
|
-
return (zdo_t.Status.SUCCESS, None, res.ActiveEpList)
|
|
177
|
-
|
|
178
|
-
async def Mgmt_Lqi_req(self, idx):
|
|
179
|
-
"""Request Link Quality Index."""
|
|
180
|
-
res = await self._device._application._api.request(
|
|
181
|
-
c.ZDO.MgmtLqi.Req(
|
|
182
|
-
TSN=self._device._application.get_sequence(),
|
|
183
|
-
DestNWK=self._device.nwk,
|
|
184
|
-
Index=idx,
|
|
185
|
-
)
|
|
186
|
-
)
|
|
187
|
-
if res.StatusCode != 0:
|
|
188
|
-
return (res.StatusCode, None)
|
|
189
|
-
|
|
190
|
-
return (res.StatusCode, res.Neighbors)
|
|
191
|
-
|
|
192
|
-
async def Mgmt_Leave_req(self, ieee, flags):
|
|
193
|
-
"""Request device leaving the network."""
|
|
194
|
-
res = await self._device._application._api.request(
|
|
195
|
-
c.ZDO.MgtLeave.Req(
|
|
196
|
-
TSN=self._device._application.get_sequence(),
|
|
197
|
-
DestNWK=t.NWK(self._device._application.devices[ieee].nwk),
|
|
198
|
-
IEEE=t.EUI64(ieee),
|
|
199
|
-
Flags=t.uint8_t(flags),
|
|
200
|
-
)
|
|
201
|
-
)
|
|
202
|
-
return res.StatusCode
|
|
203
|
-
|
|
204
|
-
async def Mgmt_Permit_Joining_req(self, duration, tc_significance):
|
|
205
|
-
"""Request join permition."""
|
|
206
|
-
res = await self._device._application._api.request(
|
|
207
|
-
c.ZDO.PermitJoin.Req(
|
|
208
|
-
TSN=self._device._application.get_sequence(),
|
|
209
|
-
DestNWK=t.NWK(t.BroadcastAddress.RX_ON_WHEN_IDLE),
|
|
210
|
-
PermitDuration=t.uint8_t(duration),
|
|
211
|
-
TCSignificance=t.uint8_t(tc_significance),
|
|
212
|
-
)
|
|
213
|
-
)
|
|
214
|
-
return res.StatusCode
|
|
215
|
-
|
|
216
|
-
async def Mgmt_NWK_Update_req(self, nwkUpdate):
|
|
217
|
-
"""Issue a ZDO Mgmt_NWK_Update (energy scan / channel change)."""
|
|
218
|
-
res = await self._device._application._api.request(
|
|
219
|
-
c.ZDO.MgmtNwkUpdate.Req(
|
|
220
|
-
TSN=self._device._application.get_sequence(),
|
|
221
|
-
ScanChannelMask=nwkUpdate.ScanChannels,
|
|
222
|
-
ScanDuration=nwkUpdate.ScanDuration,
|
|
223
|
-
ScanCount=nwkUpdate.ScanCount or 0,
|
|
224
|
-
MgrAddr=self._device.nwk,
|
|
225
|
-
DstNWK=t.NWK(0x0000),
|
|
226
|
-
)
|
|
227
|
-
)
|
|
228
|
-
if res.StatusCode != 0:
|
|
229
|
-
raise zigpy.exceptions.DeliveryError(
|
|
230
|
-
f"Mgmt_NWK_Update_req failed: {res.StatusCode}",
|
|
231
|
-
status=res.StatusCode,
|
|
232
|
-
)
|
|
233
|
-
return (None, res.ScannedChannels, None, None, res.EnergyValues)
|
|
234
|
-
|
|
235
|
-
async def zboss_specific_cmd(self, packet: t.ZigbeePacket) -> None:
|
|
236
|
-
"""Reroute ZDO packet sent over APSDE to custom ZBOSS ZDO commands."""
|
|
237
|
-
try:
|
|
238
|
-
zdo_hdr, zdo_args = self.deserialize(
|
|
239
|
-
cluster_id=packet.cluster_id, data=packet.data.serialize()
|
|
240
|
-
)
|
|
241
|
-
except ValueError:
|
|
242
|
-
LOGGER.debug("Could not parse ZDO message from packet")
|
|
243
|
-
return
|
|
244
|
-
|
|
245
|
-
if zdo_hdr.command_id == zdo_t.ZDOCmd.IEEE_addr_req:
|
|
246
|
-
await self._IEEE_addr_req(packet, zdo_hdr, zdo_args)
|
|
247
|
-
|
|
248
|
-
async def _IEEE_addr_req(
|
|
249
|
-
self,
|
|
250
|
-
packet: t.ZigbeePacket,
|
|
251
|
-
zdo_hdr: zdo_t.ZDOHeader,
|
|
252
|
-
zdo_args: tuple[Any]) -> None:
|
|
253
|
-
"""Send ZDO IEEE addr request and handle the response."""
|
|
254
|
-
tsn = zdo_hdr.tsn
|
|
255
|
-
nwki, req_type, index = zdo_args
|
|
256
|
-
res = await self._device._application._api.request(
|
|
257
|
-
c.ZDO.IeeeAddrReq.Req(
|
|
258
|
-
TSN=tsn,
|
|
259
|
-
DstNWK=packet.dst.address,
|
|
260
|
-
NWKtoMatch=nwki,
|
|
261
|
-
RequestType=req_type,
|
|
262
|
-
StartIndex=index,
|
|
263
|
-
),
|
|
264
|
-
timeout=70,
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
if res.StatusCode:
|
|
268
|
-
# ZDO command failed, use dummy values.
|
|
269
|
-
ieee = t.EUI64([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
|
|
270
|
-
nwki = t.NWK(0xffff)
|
|
271
|
-
src_ad = t.NWK(0x0000)
|
|
272
|
-
else:
|
|
273
|
-
ieee = res.RemoteDevIEEE
|
|
274
|
-
nwki = res.RemoteDevNWK
|
|
275
|
-
src_ad = res.RemoteDevNWK
|
|
276
|
-
|
|
277
|
-
status = zdo_t.Status(res.StatusCode)
|
|
278
|
-
data = tsn.serialize() \
|
|
279
|
-
+ status.serialize() \
|
|
280
|
-
+ ieee.serialize() \
|
|
281
|
-
+ nwki.serialize()
|
|
282
|
-
|
|
283
|
-
packet = t.ZigbeePacket(
|
|
284
|
-
src=t.AddrModeAddress(
|
|
285
|
-
addr_mode=t.AddrMode.NWK,
|
|
286
|
-
address=src_ad,
|
|
287
|
-
),
|
|
288
|
-
src_ep=0,
|
|
289
|
-
dst=t.AddrModeAddress(
|
|
290
|
-
addr_mode=t.AddrMode.NWK,
|
|
291
|
-
address=t.NWK(0x0000),
|
|
292
|
-
),
|
|
293
|
-
dst_ep=0,
|
|
294
|
-
tsn=tsn,
|
|
295
|
-
profile_id=0,
|
|
296
|
-
cluster_id=zdo_t.ZDOCmd.IEEE_addr_rsp,
|
|
297
|
-
data=t.SerializableBytes(data),
|
|
298
|
-
tx_options=t.TransmitOptions.NONE,
|
|
299
|
-
lqi=None,
|
|
300
|
-
rssi=None
|
|
301
|
-
)
|
|
302
|
-
self._device._application.packet_received(packet)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
class ZbossDevice(zigpy.device.Device):
|
|
306
|
-
"""Class representing an nRF device."""
|
|
307
|
-
|
|
308
|
-
def __init__(self, *args, **kwargs):
|
|
309
|
-
"""Initialize instance."""
|
|
310
|
-
super().__init__(*args, **kwargs)
|
|
311
|
-
assert hasattr(self, "zdo")
|
|
312
|
-
self.zdo = ZbossZDO(self)
|
|
313
|
-
self.endpoints[0] = self.zdo
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
class ZbossCoordinator(ZbossDevice):
|
|
317
|
-
"""Zigpy Device representing the controller."""
|
|
318
|
-
|
|
319
|
-
def __init__(self, *args, **kwargs):
|
|
320
|
-
"""Initialize instance."""
|
|
321
|
-
super().__init__(*args, **kwargs)
|
|
322
|
-
|
|
323
|
-
@property
|
|
324
|
-
def manufacturer(self):
|
|
325
|
-
"""Return manufacturer."""
|
|
326
|
-
return "Nordic Semiconductor"
|
|
327
|
-
|
|
328
|
-
@property
|
|
329
|
-
def model(self):
|
|
330
|
-
"""Return model."""
|
|
331
|
-
return "nRF52840"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|