zigpy-zboss 2.0.3__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.
Files changed (56) hide show
  1. {zigpy_zboss-2.0.3/zigpy_zboss.egg-info → zigpy_zboss-2.0.4}/PKG-INFO +1 -1
  2. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/__init__.py +1 -1
  3. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/zigbee/application.py +33 -4
  4. zigpy_zboss-2.0.4/zigpy_zboss/zigbee/device.py +461 -0
  5. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4/zigpy_zboss.egg-info}/PKG-INFO +1 -1
  6. zigpy_zboss-2.0.3/zigpy_zboss/zigbee/device.py +0 -331
  7. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/COPYING +0 -0
  8. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/LICENSE +0 -0
  9. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/README.md +0 -0
  10. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/pyproject.toml +0 -0
  11. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/setup.cfg +0 -0
  12. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/setup.py +0 -0
  13. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/tests/test_commands.py +0 -0
  14. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/tests/test_config.py +0 -0
  15. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/tests/test_frame.py +0 -0
  16. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/tests/test_nvids.py +0 -0
  17. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/tests/test_types_basic.py +0 -0
  18. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/tests/test_types_cstruct.py +0 -0
  19. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/tests/test_types_named.py +0 -0
  20. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/tests/test_uart.py +0 -0
  21. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/tests/test_utils.py +0 -0
  22. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/api.py +0 -0
  23. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/checksum.py +0 -0
  24. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/commands/__init__.py +0 -0
  25. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/commands/af.py +0 -0
  26. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/commands/aps.py +0 -0
  27. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/commands/ncp_config.py +0 -0
  28. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/commands/nwk_mgmt.py +0 -0
  29. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/commands/security.py +0 -0
  30. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/commands/zdo.py +0 -0
  31. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/config.py +0 -0
  32. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/debug.py +0 -0
  33. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/exceptions.py +0 -0
  34. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/frames.py +0 -0
  35. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/logger.py +0 -0
  36. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/nvram.py +0 -0
  37. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/tools/__init__.py +0 -0
  38. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/tools/config.py +0 -0
  39. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/tools/factory_reset_ncp.py +0 -0
  40. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/tools/get_ncp_version.py +0 -0
  41. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/tools/get_nwk_keys.py +0 -0
  42. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/tools/log2pcap.py +0 -0
  43. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/types/__init__.py +0 -0
  44. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/types/basic.py +0 -0
  45. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/types/commands.py +0 -0
  46. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/types/cstruct.py +0 -0
  47. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/types/named.py +0 -0
  48. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/types/nvids.py +0 -0
  49. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/types/structs.py +0 -0
  50. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/uart.py +0 -0
  51. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/utils.py +0 -0
  52. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss/zigbee/__init__.py +0 -0
  53. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss.egg-info/SOURCES.txt +0 -0
  54. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss.egg-info/dependency_links.txt +0 -0
  55. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss.egg-info/requires.txt +0 -0
  56. {zigpy_zboss-2.0.3 → zigpy_zboss-2.0.4}/zigpy_zboss.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zigpy-zboss
3
- Version: 2.0.3
3
+ Version: 2.0.4
4
4
  Summary: A library for zigpy which communicates with Nordic nRF52 radios running ZBOSS NCP
5
5
  Author-email: Damien Kastner <damien.kastner@kardia.no>
6
6
  License: GPL-3.0
@@ -2,7 +2,7 @@
2
2
 
3
3
  MAJOR_VERSION = 2
4
4
  MINOR_VERSION = 0
5
- PATCH_VERSION = 3
5
+ PATCH_VERSION = 4
6
6
 
7
7
  __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
8
8
  __version__ = f"{__short_version__}.{PATCH_VERSION}"
@@ -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.handle_join(nwk=msg.NWK, ieee=msg.IEEE, parent_nwk=None)
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.handle_join(msg.Nwk, msg.IEEE, 0x0000)
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
- self.handle_join(msg.Nwk, msg.IEEE, 0x0000)
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.handle_join(msg.Nwk, msg.IEEE, 0x0000)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zigpy-zboss
3
- Version: 2.0.3
3
+ Version: 2.0.4
4
4
  Summary: A library for zigpy which communicates with Nordic nRF52 radios running ZBOSS NCP
5
5
  Author-email: Damien Kastner <damien.kastner@kardia.no>
6
6
  License: GPL-3.0
@@ -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, 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 = dst_address.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, 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