zigpy-ziggurat 1.2.0__tar.gz → 1.2.2__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_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/PKG-INFO +2 -2
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/pyproject.toml +1 -1
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/tests/test_api.py +25 -0
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/tests/test_application.py +73 -2
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/tests/test_transport.py +4 -4
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/zigpy_ziggurat/zigbee/api.py +8 -1
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/zigpy_ziggurat/zigbee/application.py +47 -11
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/zigpy_ziggurat/zigbee/transport.py +10 -25
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/zigpy_ziggurat.egg-info/PKG-INFO +2 -2
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/zigpy_ziggurat.egg-info/requires.txt +1 -1
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/README.md +0 -0
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/setup.cfg +0 -0
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/tests/test_protocol.py +0 -0
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/zigpy_ziggurat/__init__.py +0 -0
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/zigpy_ziggurat/config.py +0 -0
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/zigpy_ziggurat/zigbee/__init__.py +0 -0
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/zigpy_ziggurat/zigbee/protocol.py +0 -0
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/zigpy_ziggurat/zigbee/wire.py +0 -0
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/zigpy_ziggurat.egg-info/SOURCES.txt +0 -0
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/zigpy_ziggurat.egg-info/dependency_links.txt +0 -0
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/zigpy_ziggurat.egg-info/entry_points.txt +0 -0
- {zigpy_ziggurat-1.2.0 → zigpy_ziggurat-1.2.2}/zigpy_ziggurat.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zigpy-ziggurat
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Zigpy radio library for the Ziggurat server
|
|
5
5
|
Author-email: puddly <puddly3@gmail.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -9,7 +9,7 @@ Requires-Python: >=3.12
|
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
Requires-Dist: zigpy>=2.4.0
|
|
11
11
|
Requires-Dist: aiohttp
|
|
12
|
-
Requires-Dist: aiospinel>=1.
|
|
12
|
+
Requires-Dist: aiospinel>=1.3.0
|
|
13
13
|
|
|
14
14
|
# zigpy-ziggurat
|
|
15
15
|
|
|
@@ -488,12 +488,37 @@ async def test_connection_lost_fails_pending_requests(
|
|
|
488
488
|
assert api.disconnects == [None]
|
|
489
489
|
|
|
490
490
|
|
|
491
|
+
async def test_startup_hello_ignored(
|
|
492
|
+
api: RecordingApi, transport: SyntheticBinaryTransport
|
|
493
|
+
) -> None:
|
|
494
|
+
async def start_then_reply(request: p.Request, request_id: int) -> None:
|
|
495
|
+
transport.notify(
|
|
496
|
+
p.NotificationCommand.HELLO,
|
|
497
|
+
0,
|
|
498
|
+
p.Hello(
|
|
499
|
+
protocol_version=t.uint8_t(p.PROTOCOL_VERSION),
|
|
500
|
+
configured=t.Bool(False),
|
|
501
|
+
),
|
|
502
|
+
)
|
|
503
|
+
transport.ok(request.command, request_id)
|
|
504
|
+
|
|
505
|
+
# Embedded firmware starts its stack on the first host frame and announces it
|
|
506
|
+
# with a `hello` before the reply: that opens the session, it is not a reboot
|
|
507
|
+
transport.handlers[p.RequestCommand.RESET] = start_then_reply
|
|
508
|
+
assert await api.request(p.Reset(hard=t.Bool(False))) is None
|
|
509
|
+
|
|
510
|
+
assert api.disconnects == []
|
|
511
|
+
assert api.notifications == []
|
|
512
|
+
|
|
513
|
+
|
|
491
514
|
async def test_hello_reported_as_disconnect(
|
|
492
515
|
api: RecordingApi, transport: SyntheticBinaryTransport
|
|
493
516
|
) -> None:
|
|
494
517
|
async def withhold(request: p.Request, request_id: int) -> None:
|
|
495
518
|
return None
|
|
496
519
|
|
|
520
|
+
assert await api.request(p.Shutdown()) is None
|
|
521
|
+
|
|
497
522
|
transport.handlers[p.RequestCommand.SHUTDOWN] = withhold
|
|
498
523
|
request = asyncio.ensure_future(api.request(p.Shutdown()))
|
|
499
524
|
await asyncio.sleep(0)
|
|
@@ -471,6 +471,59 @@ async def test_write_network_info_restores_children(
|
|
|
471
471
|
assert [entry.nwk for entry in entries] == list(nwk_addresses.values())
|
|
472
472
|
# The backup carries no capability, so the device type is unknown
|
|
473
473
|
assert {entry.device_type for entry in entries} == {p.ChildDeviceType.UNKNOWN}
|
|
474
|
+
assert {entry.rx_on_when_idle for entry in entries} == {0}
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
async def test_write_network_info_restores_children_capability(
|
|
478
|
+
connected_app: ControllerApplication, server: SyntheticZiggurat
|
|
479
|
+
) -> None:
|
|
480
|
+
"""A child's capability comes from its node descriptor, so an end device that
|
|
481
|
+
keeps its receiver on is not restored as sleepy."""
|
|
482
|
+
app = connected_app
|
|
483
|
+
await app.load_network_info()
|
|
484
|
+
|
|
485
|
+
def child(
|
|
486
|
+
n: int, mac_capability_flags: int | None, logical_type: zdo_t.LogicalType | None
|
|
487
|
+
) -> t.EUI64:
|
|
488
|
+
ieee = t.EUI64([n] * 8)
|
|
489
|
+
device = app.add_device(ieee, t.NWK(0x1000 + n))
|
|
490
|
+
if mac_capability_flags is not None:
|
|
491
|
+
device.node_desc = zdo_t.NodeDescriptor(
|
|
492
|
+
logical_type=logical_type,
|
|
493
|
+
mac_capability_flags=zdo_t.NodeDescriptor.MACCapabilityFlags(
|
|
494
|
+
mac_capability_flags
|
|
495
|
+
),
|
|
496
|
+
)
|
|
497
|
+
return ieee
|
|
498
|
+
|
|
499
|
+
children = [
|
|
500
|
+
# A Sengled bulb: an end device with its receiver on
|
|
501
|
+
child(1, 0x8C, zdo_t.LogicalType.EndDevice),
|
|
502
|
+
# A battery sensor
|
|
503
|
+
child(2, 0x80, zdo_t.LogicalType.EndDevice),
|
|
504
|
+
child(3, 0x8E, zdo_t.LogicalType.Router),
|
|
505
|
+
# Not interviewed
|
|
506
|
+
child(4, None, None),
|
|
507
|
+
# A node descriptor with no logical type
|
|
508
|
+
child(5, 0x80, None),
|
|
509
|
+
]
|
|
510
|
+
|
|
511
|
+
await app.write_network_info(
|
|
512
|
+
network_info=app.state.network_info.replace(
|
|
513
|
+
children=children,
|
|
514
|
+
nwk_addresses={ieee: app.get_device(ieee=ieee).nwk for ieee in children},
|
|
515
|
+
),
|
|
516
|
+
node_info=app.state.node_info,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
(load,) = server.sent(p.LoadChildren)
|
|
520
|
+
assert [(entry.device_type, entry.rx_on_when_idle) for entry in load.entries] == [
|
|
521
|
+
(p.ChildDeviceType.END_DEVICE, 1),
|
|
522
|
+
(p.ChildDeviceType.END_DEVICE, 0),
|
|
523
|
+
(p.ChildDeviceType.ROUTER, 1),
|
|
524
|
+
(p.ChildDeviceType.UNKNOWN, 0),
|
|
525
|
+
(p.ChildDeviceType.UNKNOWN, 0),
|
|
526
|
+
]
|
|
474
527
|
|
|
475
528
|
|
|
476
529
|
async def test_permit_ncp(
|
|
@@ -745,6 +798,19 @@ async def test_send_packet_encrypted_without_eui64(
|
|
|
745
798
|
assert server.sent(p.SendUnicast) == []
|
|
746
799
|
|
|
747
800
|
|
|
801
|
+
async def test_send_packet_disconnected(
|
|
802
|
+
app: ControllerApplication, server: SyntheticZiggurat
|
|
803
|
+
) -> None:
|
|
804
|
+
await app.disconnect()
|
|
805
|
+
|
|
806
|
+
with pytest.raises(DeliveryError, match="Not connected"):
|
|
807
|
+
await app.send_packet(
|
|
808
|
+
aps_packet(t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=DEVICE_NWK))
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
assert server.sent(p.SendUnicast) == []
|
|
812
|
+
|
|
813
|
+
|
|
748
814
|
async def test_send_packet_broadcast(
|
|
749
815
|
app: ControllerApplication, server: SyntheticZiggurat
|
|
750
816
|
) -> None:
|
|
@@ -1127,12 +1193,13 @@ async def test_on_notification_received_aps_zdo_reply_encryption(
|
|
|
1127
1193
|
|
|
1128
1194
|
|
|
1129
1195
|
@pytest.mark.parametrize(
|
|
1130
|
-
("destination", "group", "expected_dst"),
|
|
1196
|
+
("destination", "group", "expected_dst", "expected_dst_ep"),
|
|
1131
1197
|
[
|
|
1132
1198
|
(
|
|
1133
1199
|
t.NWK(0x0000),
|
|
1134
1200
|
2,
|
|
1135
1201
|
t.AddrModeAddress(addr_mode=t.AddrMode.Group, address=t.Group(2)),
|
|
1202
|
+
None,
|
|
1136
1203
|
),
|
|
1137
1204
|
(
|
|
1138
1205
|
t.NWK(0xFFFD),
|
|
@@ -1140,11 +1207,13 @@ async def test_on_notification_received_aps_zdo_reply_encryption(
|
|
|
1140
1207
|
t.AddrModeAddress(
|
|
1141
1208
|
addr_mode=t.AddrMode.Broadcast, address=t.BroadcastAddress(0xFFFD)
|
|
1142
1209
|
),
|
|
1210
|
+
1,
|
|
1143
1211
|
),
|
|
1144
1212
|
(
|
|
1145
1213
|
t.NWK(0x0000),
|
|
1146
1214
|
None,
|
|
1147
1215
|
t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=t.NWK(0x0000)),
|
|
1216
|
+
1,
|
|
1148
1217
|
),
|
|
1149
1218
|
],
|
|
1150
1219
|
)
|
|
@@ -1153,9 +1222,10 @@ async def test_on_notification_received_aps_address_modes(
|
|
|
1153
1222
|
destination: t.NWK,
|
|
1154
1223
|
group: int | None,
|
|
1155
1224
|
expected_dst: t.AddrModeAddress,
|
|
1225
|
+
expected_dst_ep: int | None,
|
|
1156
1226
|
) -> None:
|
|
1157
1227
|
"""A group id, a broadcast address and a plain network address each decode into
|
|
1158
|
-
their own zigpy address mode."""
|
|
1228
|
+
their own zigpy address mode. A group frame has no destination endpoint."""
|
|
1159
1229
|
app = RecordingApplication(make_app_config(server.url))
|
|
1160
1230
|
await app.connect()
|
|
1161
1231
|
await app.start_network()
|
|
@@ -1184,6 +1254,7 @@ async def test_on_notification_received_aps_address_modes(
|
|
|
1184
1254
|
assert len(app.packets) == 1
|
|
1185
1255
|
packet = app.packets[0]
|
|
1186
1256
|
assert packet.dst == expected_dst
|
|
1257
|
+
assert packet.dst_ep == expected_dst_ep
|
|
1187
1258
|
assert packet.src == t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=DEVICE_NWK)
|
|
1188
1259
|
assert packet.lqi == 200
|
|
1189
1260
|
assert packet.rssi == -70
|
|
@@ -3,7 +3,7 @@ the transports it returns."""
|
|
|
3
3
|
|
|
4
4
|
import asyncio
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
from aiospinel import PropertyStatusError, Status
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
9
|
from tests.common import (
|
|
@@ -97,7 +97,7 @@ async def test_spinel_stream_frame_handler_error(
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
async def test_spinel_connect_rejects_foreign_firmware() -> None:
|
|
100
|
-
rcp = SyntheticSpinelRcp(
|
|
100
|
+
rcp = SyntheticSpinelRcp(get_status=Status.PROP_NOT_FOUND)
|
|
101
101
|
await rcp.start()
|
|
102
102
|
try:
|
|
103
103
|
with pytest.raises(ConnectionError, match="does not embed"):
|
|
@@ -107,11 +107,11 @@ async def test_spinel_connect_rejects_foreign_firmware() -> None:
|
|
|
107
107
|
|
|
108
108
|
|
|
109
109
|
async def test_spinel_tunnel_write_rejected() -> None:
|
|
110
|
-
rcp = SyntheticSpinelRcp(
|
|
110
|
+
rcp = SyntheticSpinelRcp(set_status=Status.INVALID_ARGUMENT)
|
|
111
111
|
await rcp.start()
|
|
112
112
|
transport = await connect_transport(rcp.url, lambda frame: None, lambda exc: None)
|
|
113
113
|
try:
|
|
114
|
-
with pytest.raises(
|
|
114
|
+
with pytest.raises(PropertyStatusError):
|
|
115
115
|
await transport.send_frame(b"\x01")
|
|
116
116
|
finally:
|
|
117
117
|
await transport.disconnect()
|
|
@@ -56,6 +56,7 @@ class ZigguratApi:
|
|
|
56
56
|
] = {}
|
|
57
57
|
self._awaiting_aps_ack: set[int] = set()
|
|
58
58
|
self._transport: Transport | None = None
|
|
59
|
+
self._received_frame = False
|
|
59
60
|
|
|
60
61
|
async def connect(self) -> None:
|
|
61
62
|
self._transport = await connect_transport(
|
|
@@ -201,6 +202,8 @@ class ZigguratApi:
|
|
|
201
202
|
_LOGGER.debug("Received notification (id=%d): %r", request_id, notification)
|
|
202
203
|
self._handle_notification(request_id, notification)
|
|
203
204
|
|
|
205
|
+
self._received_frame = True
|
|
206
|
+
|
|
204
207
|
def _handle_response(self, request_id: int, body: bytes) -> None:
|
|
205
208
|
pending = self._pending.get(request_id)
|
|
206
209
|
if pending is None or pending.response.done():
|
|
@@ -261,7 +264,11 @@ class ZigguratApi:
|
|
|
261
264
|
if confirm is not None and not confirm.done():
|
|
262
265
|
confirm.set_result(notification)
|
|
263
266
|
elif isinstance(notification, p.Hello):
|
|
264
|
-
#
|
|
267
|
+
# Embedded firmware starts its stack on the first frame the host sends, and
|
|
268
|
+
# the stack announces itself before it answers. So a `hello` that opens the
|
|
269
|
+
# session is that start. Any later `hello` means the firmware rebooted.
|
|
270
|
+
if not self._received_frame:
|
|
271
|
+
return
|
|
265
272
|
self._connection_lost(ConnectionError("Ziggurat firmware reset"))
|
|
266
273
|
elif isinstance(notification, p.LastReset):
|
|
267
274
|
logging.getLogger("ziggurat.fw").warning(
|
|
@@ -541,16 +541,9 @@ class ControllerApplication(zigpy.application.ControllerApplication):
|
|
|
541
541
|
|
|
542
542
|
async def _restore_children(self, network_info: zigpy.state.NetworkInfo) -> None:
|
|
543
543
|
assert self._api is not None
|
|
544
|
-
#
|
|
545
|
-
# sleepy end device); children without a known NWK address can't be loaded.
|
|
544
|
+
# Children without a known NWK address can't be loaded
|
|
546
545
|
entries = [
|
|
547
|
-
|
|
548
|
-
ieee=ieee,
|
|
549
|
-
nwk=network_info.nwk_addresses[ieee],
|
|
550
|
-
rx_on_when_idle=t.uint1_t(1),
|
|
551
|
-
device_type=p.ChildDeviceType.UNKNOWN,
|
|
552
|
-
reserved=t.uint5_t(0),
|
|
553
|
-
)
|
|
546
|
+
self._child_entry(ieee, network_info.nwk_addresses[ieee])
|
|
554
547
|
for ieee in network_info.children
|
|
555
548
|
if ieee in network_info.nwk_addresses
|
|
556
549
|
]
|
|
@@ -563,6 +556,41 @@ class ControllerApplication(zigpy.application.ControllerApplication):
|
|
|
563
556
|
)
|
|
564
557
|
)
|
|
565
558
|
|
|
559
|
+
def _child_entry(self, ieee: t.EUI64, nwk: t.NWK) -> p.ChildEntry:
|
|
560
|
+
# The backup carries no capability, so it comes from the child's node
|
|
561
|
+
# descriptor. Some end devices keep their receiver on and never poll (Sengled
|
|
562
|
+
# bulbs), so they must not be restored as sleepy. Without a node descriptor
|
|
563
|
+
# the child is restored as a sleepy end device, as most end devices are.
|
|
564
|
+
try:
|
|
565
|
+
device = self.get_device(ieee=ieee)
|
|
566
|
+
except KeyError:
|
|
567
|
+
node_desc = None
|
|
568
|
+
else:
|
|
569
|
+
# A child joined the network, so it is never a GPD
|
|
570
|
+
assert isinstance(device, zigpy.device.ZigbeeDevice)
|
|
571
|
+
node_desc = device.node_desc
|
|
572
|
+
|
|
573
|
+
if node_desc is None:
|
|
574
|
+
device_type = p.ChildDeviceType.UNKNOWN
|
|
575
|
+
rx_on_when_idle = False
|
|
576
|
+
else:
|
|
577
|
+
if node_desc.is_router:
|
|
578
|
+
device_type = p.ChildDeviceType.ROUTER
|
|
579
|
+
elif node_desc.is_end_device:
|
|
580
|
+
device_type = p.ChildDeviceType.END_DEVICE
|
|
581
|
+
else:
|
|
582
|
+
device_type = p.ChildDeviceType.UNKNOWN
|
|
583
|
+
|
|
584
|
+
rx_on_when_idle = bool(node_desc.is_receiver_on_when_idle)
|
|
585
|
+
|
|
586
|
+
return p.ChildEntry(
|
|
587
|
+
ieee=ieee,
|
|
588
|
+
nwk=nwk,
|
|
589
|
+
rx_on_when_idle=t.uint1_t(rx_on_when_idle),
|
|
590
|
+
device_type=device_type,
|
|
591
|
+
reserved=t.uint5_t(0),
|
|
592
|
+
)
|
|
593
|
+
|
|
566
594
|
async def reset_network_info(self) -> None:
|
|
567
595
|
assert self._api is not None
|
|
568
596
|
await self._api.request(p.Shutdown())
|
|
@@ -778,11 +806,16 @@ class ControllerApplication(zigpy.application.ControllerApplication):
|
|
|
778
806
|
|
|
779
807
|
def _handle_received_aps_command(self, command: p.ReceivedAps) -> None:
|
|
780
808
|
group = command.group_id
|
|
809
|
+
dst_ep: t.uint8_t | None = command.dst_ep
|
|
810
|
+
|
|
781
811
|
if group is not None:
|
|
782
812
|
dst = t.AddrModeAddress(
|
|
783
813
|
addr_mode=t.AddrMode.Group,
|
|
784
814
|
address=t.Group(group),
|
|
785
815
|
)
|
|
816
|
+
# A group frame carries no destination endpoint, so `dst_ep` is filler.
|
|
817
|
+
# Passing it on would make zigpy treat a filler of 0 as ZDO.
|
|
818
|
+
dst_ep = None
|
|
786
819
|
elif command.destination >= 0xFFF8:
|
|
787
820
|
dst = t.AddrModeAddress(
|
|
788
821
|
addr_mode=t.AddrMode.Broadcast,
|
|
@@ -801,7 +834,7 @@ class ControllerApplication(zigpy.application.ControllerApplication):
|
|
|
801
834
|
),
|
|
802
835
|
dst=dst,
|
|
803
836
|
src_ep=command.src_ep,
|
|
804
|
-
dst_ep=
|
|
837
|
+
dst_ep=dst_ep,
|
|
805
838
|
profile_id=command.profile_id,
|
|
806
839
|
cluster_id=command.cluster_id,
|
|
807
840
|
lqi=command.lqi,
|
|
@@ -865,7 +898,6 @@ class ControllerApplication(zigpy.application.ControllerApplication):
|
|
|
865
898
|
# Resolves once the send is confirmed: passive-ack quorum for a broadcast or
|
|
866
899
|
# groupcast, next-hop acceptance for a no-ack unicast, or the end-to-end APS
|
|
867
900
|
# ack. A rejected or failed send raises `DeliveryError`.
|
|
868
|
-
assert self._api is not None
|
|
869
901
|
priority = packet.priority if packet.priority is not None else 0
|
|
870
902
|
radius = packet.radius or 30
|
|
871
903
|
asdu = packet.data.serialize()
|
|
@@ -942,4 +974,8 @@ class ControllerApplication(zigpy.application.ControllerApplication):
|
|
|
942
974
|
asdu=asdu,
|
|
943
975
|
)
|
|
944
976
|
|
|
977
|
+
# The connection can close while the send waits for a concurrency slot
|
|
978
|
+
if self._api is None:
|
|
979
|
+
raise DeliveryError("Not connected to Ziggurat")
|
|
980
|
+
|
|
945
981
|
await self._api.request_confirmed(send)
|
|
@@ -8,14 +8,14 @@ import logging
|
|
|
8
8
|
from typing import Any, Protocol, cast
|
|
9
9
|
|
|
10
10
|
import aiohttp
|
|
11
|
-
import
|
|
11
|
+
from aiospinel import PropertyStatusError, SpinelProtocol
|
|
12
12
|
import zigpy.serial
|
|
13
13
|
|
|
14
14
|
_LOGGER = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
16
|
WEBSOCKET_HEARTBEAT = 15
|
|
17
17
|
|
|
18
|
-
PROP_VENDOR_ZIGGURAT =
|
|
18
|
+
PROP_VENDOR_ZIGGURAT = 0x3D5A
|
|
19
19
|
|
|
20
20
|
# The callback the API installs to receive a device -> host binary frame.
|
|
21
21
|
OnFrame = Callable[[bytes], None]
|
|
@@ -56,7 +56,7 @@ async def connect_transport(
|
|
|
56
56
|
# -- serial (Spinel tunnel) ------------------------------------------------------
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
class _SpinelProtocol(
|
|
59
|
+
class _SpinelProtocol(SpinelProtocol):
|
|
60
60
|
"""Tunnels binary frames over the vendor Spinel stream property."""
|
|
61
61
|
|
|
62
62
|
def __init__(self, on_frame: OnFrame, on_lost: OnLost) -> None:
|
|
@@ -80,32 +80,18 @@ class _SpinelProtocol(aiospinel.SpinelProtocol):
|
|
|
80
80
|
_LOGGER.exception("Failed to handle frame: %r", data)
|
|
81
81
|
|
|
82
82
|
async def start_ziggurat(self) -> None:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
prop_id, _ = aiospinel.PackedUInt21.deserialize(rsp.data)
|
|
88
|
-
if prop_id != PROP_VENDOR_ZIGGURAT:
|
|
89
|
-
raise ConnectionError(
|
|
90
|
-
f"Firmware does not embed the Ziggurat stack: {rsp!r}"
|
|
91
|
-
)
|
|
83
|
+
try:
|
|
84
|
+
await self.get_property(PROP_VENDOR_ZIGGURAT)
|
|
85
|
+
except PropertyStatusError as exc:
|
|
86
|
+
raise ConnectionError("Firmware does not embed the Ziggurat stack") from exc
|
|
92
87
|
_LOGGER.debug("Embedded Ziggurat firmware detected")
|
|
93
88
|
|
|
94
89
|
async def tunnel_send(self, frame: bytes) -> None:
|
|
95
90
|
# No retries: a timed-out tunnel write must not resend the request (the first
|
|
96
91
|
# copy may already have been processed).
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
(
|
|
100
|
-
PROP_VENDOR_ZIGGURAT.serialize()
|
|
101
|
-
+ len(frame).to_bytes(2, "little")
|
|
102
|
-
+ frame
|
|
103
|
-
),
|
|
104
|
-
retries=0,
|
|
92
|
+
await self.set_property(
|
|
93
|
+
PROP_VENDOR_ZIGGURAT, len(frame).to_bytes(2, "little") + frame, retries=0
|
|
105
94
|
)
|
|
106
|
-
prop_id, _ = aiospinel.PackedUInt21.deserialize(rsp.data)
|
|
107
|
-
if prop_id != PROP_VENDOR_ZIGGURAT:
|
|
108
|
-
raise ConnectionError(f"Tunnel write rejected: {rsp!r}")
|
|
109
95
|
|
|
110
96
|
|
|
111
97
|
class SpinelTransport:
|
|
@@ -141,8 +127,7 @@ class SpinelTransport:
|
|
|
141
127
|
|
|
142
128
|
async def disconnect(self) -> None:
|
|
143
129
|
if self._protocol is not None:
|
|
144
|
-
self._protocol.
|
|
145
|
-
await self._protocol.wait_until_closed()
|
|
130
|
+
await self._protocol.disconnect()
|
|
146
131
|
self._protocol = None
|
|
147
132
|
|
|
148
133
|
async def send_frame(self, frame: bytes) -> None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zigpy-ziggurat
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Zigpy radio library for the Ziggurat server
|
|
5
5
|
Author-email: puddly <puddly3@gmail.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -9,7 +9,7 @@ Requires-Python: >=3.12
|
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
Requires-Dist: zigpy>=2.4.0
|
|
11
11
|
Requires-Dist: aiohttp
|
|
12
|
-
Requires-Dist: aiospinel>=1.
|
|
12
|
+
Requires-Dist: aiospinel>=1.3.0
|
|
13
13
|
|
|
14
14
|
# zigpy-ziggurat
|
|
15
15
|
|
|
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
|