From c1edbbbe9d80c1a5174fb64b99f43fe431c7bd1b Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 5 Jun 2020 20:13:24 -0400 Subject: [PATCH 1/9] 0.18.0.dev0 version bump --- bellows/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bellows/__init__.py b/bellows/__init__.py index 9a483f95..561ba11c 100644 --- a/bellows/__init__.py +++ b/bellows/__init__.py @@ -1,5 +1,5 @@ MAJOR_VERSION = 0 -MINOR_VERSION = 17 +MINOR_VERSION = 18 PATCH_VERSION = "0.dev0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) From 1dfe9c79ef6daea8aa06c4ddde4cf33f6c863033 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 8 Jun 2020 11:52:19 -0400 Subject: [PATCH 2/9] Set CONFIG_MAX_END_DEVICE_CHILDREN default to 24 (#269) --- bellows/config/ezsp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bellows/config/ezsp.py b/bellows/config/ezsp.py index 8b04b5bf..3309b989 100644 --- a/bellows/config/ezsp.py +++ b/bellows/config/ezsp.py @@ -74,7 +74,7 @@ vol.Optional(c.CONFIG_MAX_HOPS.name): vol.All(int, vol.Range(min=0, max=30)), # # The maximum number of end device children that a router will support - vol.Optional(c.CONFIG_MAX_END_DEVICE_CHILDREN.name, default=32): vol.All( + vol.Optional(c.CONFIG_MAX_END_DEVICE_CHILDREN.name, default=24): vol.All( int, vol.Range(min=0, max=32) ), # From 6e66bffe8d49a6a36f41a12b2aebdf1ed7ee384e Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 23 Jun 2020 18:26:19 -0400 Subject: [PATCH 3/9] Send commands for a request under a lock (#270) Make sure setExtendedTimeout, setSourceRoute and sendUnicast commands are sent together and not mixed with other requests. --- bellows/zigbee/application.py | 75 ++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 9f578127..11c740f8 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -55,6 +55,7 @@ def __init__(self, config: Dict): | t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY ) self.use_source_routing = self.config[CONF_PARAM_SRC_RTG] + self._req_lock = asyncio.Lock() @property def controller_event(self): @@ -422,9 +423,10 @@ async def mrequest( with self._pending.new(message_tag) as req: async with self._in_flight_msg: - res = await self._ezsp.sendMulticast( - aps_frame, hops, non_member_radius, message_tag, data - ) + async with self._req_lock: + res = await self._ezsp.sendMulticast( + aps_frame, hops, non_member_radius, message_tag, data + ) if res[0] != t.EmberStatus.SUCCESS: return res[0], "EZSP sendMulticast failure: %s" % (res[0],) @@ -476,33 +478,43 @@ async def request( ) with self._pending.new(message_tag) as req: async with self._in_flight_msg: - if expect_reply and device.node_desc.is_end_device in (True, None): - LOGGER.debug( - "Extending timeout for %s/0x%04x", device.ieee, device.nwk - ) - await self._ezsp.setExtendedTimeout(device.ieee, True) - if self.use_source_routing and device.relays is not None: - res = await self._ezsp.setSourceRoute(device.nwk, device.relays) - if res[0] != t.EmberStatus.SUCCESS: - LOGGER.warning( - "Couldn't set source route for %s: %s", device.nwk, res - ) - else: - aps_frame.options = t.EmberApsOption( - aps_frame.options - ^ t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY - ) - LOGGER.debug( - "Set source route for %s to %s: %s", - device.nwk, - device.relays, - res, - ) delays = [0.5, 1.0, 1.5] while True: - status, _ = await self._ezsp.sendUnicast( - self.direct, device.nwk, aps_frame, message_tag, data - ) + async with self._req_lock: + if expect_reply and device.node_desc.is_end_device in ( + True, + None, + ): + LOGGER.debug( + "Extending timeout for %s/0x%04x", + device.ieee, + device.nwk, + ) + await self._ezsp.setExtendedTimeout(device.ieee, True) + if self.use_source_routing and device.relays is not None: + res = await self._ezsp.setSourceRoute( + device.nwk, device.relays + ) + if res[0] != t.EmberStatus.SUCCESS: + LOGGER.warning( + "Couldn't set source route for %s: %s", + device.nwk, + res, + ) + else: + aps_frame.options = t.EmberApsOption( + aps_frame.options + ^ t.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY + ) + LOGGER.debug( + "Set source route for %s to %s: %s", + device.nwk, + device.relays, + res, + ) + status, _ = await self._ezsp.sendUnicast( + self.direct, device.nwk, aps_frame, message_tag, data + ) if not ( status == t.EmberStatus.MAX_MESSAGE_LIMIT_REACHED and delays ): @@ -590,9 +602,10 @@ async def broadcast( with self._pending.new(message_tag) as req: async with self._in_flight_msg: - res = await self._ezsp.sendBroadcast( - broadcast_address, aps_frame, radius, message_tag, data - ) + async with self._req_lock: + res = await self._ezsp.sendBroadcast( + broadcast_address, aps_frame, radius, message_tag, data + ) if res[0] != t.EmberStatus.SUCCESS: return res[0], "broadcast send failure" From e97e370794cf97ef9e801726b360a63b2b9423c0 Mon Sep 17 00:00:00 2001 From: T1toss77 Date: Sun, 12 Jul 2020 18:18:51 +0200 Subject: [PATCH 4/9] Feature efr32 support - EZSP v8 framing (#272) * Added Support for EZSP v8 framing * EZSP v8 Frame ID MSB handling added * Fixed balck tests * Added Tests for EZSP 8 * Fixed Lint related errors --- bellows/ezsp.py | 14 +++++++++++--- tests/test_ezsp.py | 11 +++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/bellows/ezsp.py b/bellows/ezsp.py index 210a5ea0..bee0cdbb 100644 --- a/bellows/ezsp.py +++ b/bellows/ezsp.py @@ -107,8 +107,12 @@ def _ezsp_frame(self, name, *args): data = t.serialize(args, c[1]) frame = [self._seq & 0xFF, 0, c[0]] # Frame control. TODO. # Frame ID if self.ezsp_version >= 5: - frame.insert(1, 0xFF) # Legacy Frame ID - frame.insert(1, 0x00) # Ext frame control. TODO. + frame.insert(1, 0xFF) # Legacy Frame + frame.insert(1, 0x00) # Frame control low byte + if self.ezsp_version >= 8: + frame[2] = 0x01 # EZSP v8 - FC High Byte + frame[3] = c[0] & 0x00FF # Extended Frame ID - LSB + frame[4] = (c[0] & 0xFF00) >> 8 # Extended Frame ID - MSB return bytes(frame) + data @@ -219,7 +223,11 @@ def frame_received(self, data): just have EZSP application stuff here, with all escaping/stuffing and data randomization removed. """ - sequence, frame_id, data = data[0], data[2], data[3:] + if self.ezsp_version >= 8: + sequence, frame_id, data = data[0], ((data[4] << 8) | data[3]), data[5:] + else: + sequence, frame_id, data = data[0], data[2], data[3:] + if frame_id == 0xFF: frame_id = 0 if len(data) > 1: diff --git a/tests/test_ezsp.py b/tests/test_ezsp.py index a7cb4802..baab5161 100644 --- a/tests/test_ezsp.py +++ b/tests/test_ezsp.py @@ -187,6 +187,13 @@ def test_receive_protocol_5(ezsp_f): assert ezsp_f.handle_callback.call_count == 1 +def test_receive_protocol_8(ezsp_f): + ezsp_f._ezsp_version = 8 + ezsp_f.handle_callback = mock.MagicMock() + ezsp_f.frame_received(b"\x01\x80\x01\x00\x00\x06\x02\x00") + assert ezsp_f.handle_callback.call_count == 1 + + def test_receive_reply(ezsp_f): ezsp_f.handle_callback = mock.MagicMock() callback_mock = mock.MagicMock(spec_set=asyncio.Future) @@ -306,6 +313,10 @@ def test_ezsp_frame(ezsp_f): data = ezsp_f._ezsp_frame("version", 6) assert data == b"\x22\x00\xff\x00\x00\x06" + ezsp_f._ezsp_version = 8 + data = ezsp_f._ezsp_frame("version", 8) + assert data == b"\x22\x00\x01\x00\x00\x08" + @pytest.mark.asyncio @mock.patch.object(ezsp.EZSP, "reset", new_callable=CoroutineMock) From a9a6f0d548dda2d4b0a88fb96d962bc4b265b7e9 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 12 Jul 2020 12:19:55 -0400 Subject: [PATCH 5/9] Pin isort version --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 13a86387..9f0f0379 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ basepython = python3 deps = flake8==3.5.0 pep8-naming==0.4.1 - isort + isort=4.3.21 commands = flake8 isort --check -rc {toxinidir}/bellows {toxinidir}/tests {toxinidir}/setup.py From 85ae7ff23ff260824905ec79aca39f9b7dc8e1c0 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 12 Jul 2020 21:31:32 -0400 Subject: [PATCH 6/9] Doh. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 9f0f0379..a7abf2de 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ basepython = python3 deps = flake8==3.5.0 pep8-naming==0.4.1 - isort=4.3.21 + isort==4.3.21 commands = flake8 isort --check -rc {toxinidir}/bellows {toxinidir}/tests {toxinidir}/setup.py From d71f30735c26b6ff229a65e8a5f7d420294ba096 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 21 Jul 2020 12:52:28 -0400 Subject: [PATCH 7/9] handle ID conflict reports (#274) * Try looking up NWK id for a device by IEEE on NWK conflict report, try too lookup NWK * Iterate over devices * Update NWK address based on TC callback. * Don't lookup device's NWK address. * Update tests. * Lint --- bellows/zigbee/application.py | 49 ++++++++++++++++++++++++- tests/test_application.py | 69 +++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 11c740f8..ad8e64a3 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -264,14 +264,19 @@ def ezsp_callback_handler(self, frame_name, args): else: self._handle_frame_sent(*args) elif frame_name == "trustCenterJoinHandler": - if args[2] == t.EmberDeviceUpdate.DEVICE_LEFT: - self.handle_leave(args[0], args[1]) + nwk, ieee, dev_update_status, decision, parent_nwk = args + if dev_update_status == t.EmberDeviceUpdate.DEVICE_LEFT: + self.handle_leave(nwk, ieee) + else: + self._update_device(nwk, ieee, dev_update_status, decision, parent_nwk) elif frame_name == "incomingRouteRecordHandler": self.handle_route_record(*args) elif frame_name == "incomingRouteErrorHandler": self.handle_route_error(*args) elif frame_name == "_reset_controller_application": self._handle_reset_request(*args) + elif frame_name == "idConflictHandler": + self._handle_id_conflict(*args) def _handle_frame( self, @@ -378,6 +383,32 @@ async def _reset_controller(self): await asyncio.sleep(0.5) await self.startup() + def _update_device( + self, + nwk: t.EmberNodeId, + ieee: t.EmberEUI64, + dev_update_status: t.EmberDeviceUpdate, + decision: t.EmberJoinDecision, + parent_nwk: t.EmberNodeId, + ) -> None: + """Handle Trust Center join callback. + + If this is an existing device, then update the NWK address, otherwise it is a + nop + """ + try: + device = self.get_device(ieee=ieee) + if device.nwk != nwk: + LOGGER.info( + "Updating NWK address for %s device. New NWK address: %04x", + ieee, + nwk, + ) + device.nwk = nwk + except KeyError: + # new device joining. wait till ZDO announce + pass + async def mrequest( self, group_id, @@ -560,6 +591,20 @@ async def permit_with_key(self, node, code, time_s=60): return await self.permit(time_s) + def _handle_id_conflict(self, nwk: t.EmberNodeId) -> None: + LOGGER.warning("NWK conflict is reported for 0x%04x", nwk) + for device in self.devices.values(): + if device.nwk != nwk: + continue + LOGGER.warning( + "Found %s device for 0x%04x NWK conflict: %s %s", + device.ieee, + nwk, + device.manufacturer, + device.model, + ) + self.handle_leave(nwk, device.ieee) + async def broadcast( self, profile, diff --git a/tests/test_application.py b/tests/test_application.py index 821d1985..5cdf447a 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1080,3 +1080,72 @@ async def test_probe_success(mock_connect, mock_reset): assert mock_connect.await_count == 1 assert mock_reset.call_count == 1 assert mock_connect.return_value.close.call_count == 1 + + +def test_handle_id_conflict(app, ieee): + """Test handling of an ID confict report.""" + nwk = t.EmberNodeId(0x1234) + app.add_device(ieee, nwk) + app.handle_leave = mock.MagicMock() + + app.ezsp_callback_handler("idConflictHandler", [nwk + 1]) + assert app.handle_leave.call_count == 0 + + app.ezsp_callback_handler("idConflictHandler", [nwk]) + assert app.handle_leave.call_count == 1 + assert app.handle_leave.call_args[0][0] == nwk + + +def test_handle_tc_join_handler(app, ieee): + """Test updating device NWK on TC join/rejoin callbacks.""" + nwk = t.EmberNodeId(0x1234) + new_nwk = t.EmberNodeId(0x4321) + + app.ezsp_callback_handler( + "trustCenterJoinHandler", + ( + nwk, + ieee, + t.EmberDeviceUpdate.STANDARD_SECURITY_SECURED_REJOIN, + mock.sentinel.decision, + mock.sentinel.parent, + ), + ) + + dev = app.add_device(ieee, nwk) + app.ezsp_callback_handler( + "trustCenterJoinHandler", + ( + nwk, + ieee, + t.EmberDeviceUpdate.STANDARD_SECURITY_SECURED_REJOIN, + mock.sentinel.decision, + mock.sentinel.parent, + ), + ) + assert dev.nwk == nwk + + app.ezsp_callback_handler( + "trustCenterJoinHandler", + ( + new_nwk, + ieee, + t.EmberDeviceUpdate.STANDARD_SECURITY_SECURED_REJOIN, + mock.sentinel.decision, + mock.sentinel.parent, + ), + ) + assert dev.nwk == new_nwk + + ieee[0] = 0x22 + app.ezsp_callback_handler( + "trustCenterJoinHandler", + ( + nwk, + ieee, + t.EmberDeviceUpdate.STANDARD_SECURITY_SECURED_REJOIN, + mock.sentinel.decision, + mock.sentinel.parent, + ), + ) + assert dev.nwk == new_nwk From 9cd71ec64d8870c14c2117ebc9c31b090575482b Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 21 Jul 2020 15:06:45 -0400 Subject: [PATCH 8/9] Trigger handle_join on TC join call backs (#275) * Trigger handle_join on TC join call backs. * Fix tests. --- bellows/zigbee/application.py | 28 +-------------- tests/test_application.py | 64 ++++------------------------------- 2 files changed, 8 insertions(+), 84 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index ad8e64a3..9293edb4 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -268,7 +268,7 @@ def ezsp_callback_handler(self, frame_name, args): if dev_update_status == t.EmberDeviceUpdate.DEVICE_LEFT: self.handle_leave(nwk, ieee) else: - self._update_device(nwk, ieee, dev_update_status, decision, parent_nwk) + self.handle_join(nwk, ieee, parent_nwk) elif frame_name == "incomingRouteRecordHandler": self.handle_route_record(*args) elif frame_name == "incomingRouteErrorHandler": @@ -383,32 +383,6 @@ async def _reset_controller(self): await asyncio.sleep(0.5) await self.startup() - def _update_device( - self, - nwk: t.EmberNodeId, - ieee: t.EmberEUI64, - dev_update_status: t.EmberDeviceUpdate, - decision: t.EmberJoinDecision, - parent_nwk: t.EmberNodeId, - ) -> None: - """Handle Trust Center join callback. - - If this is an existing device, then update the NWK address, otherwise it is a - nop - """ - try: - device = self.get_device(ieee=ieee) - if device.nwk != nwk: - LOGGER.info( - "Updating NWK address for %s device. New NWK address: %04x", - ieee, - nwk, - ) - device.nwk = nwk - except KeyError: - # new device joining. wait till ZDO announce - pass - async def mrequest( self, group_id, diff --git a/tests/test_application.py b/tests/test_application.py index 5cdf447a..baf94348 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -226,9 +226,14 @@ def test_dup_send_success(app, aps, ieee): def test_join_handler(app, ieee): # Calls device.initialize, leaks a task app.handle_join = mock.MagicMock() - app.ezsp_callback_handler("trustCenterJoinHandler", [1, ieee, None, None, None]) + app.ezsp_callback_handler( + "trustCenterJoinHandler", [1, ieee, None, None, mock.sentinel.parent] + ) assert ieee not in app.devices - assert app.handle_join.call_count == 0 + assert app.handle_join.call_count == 1 + assert app.handle_join.call_args[0][0] == 1 + assert app.handle_join.call_args[0][1] == ieee + assert app.handle_join.call_args[0][2] is mock.sentinel.parent def test_leave_handler(app, ieee): @@ -1094,58 +1099,3 @@ def test_handle_id_conflict(app, ieee): app.ezsp_callback_handler("idConflictHandler", [nwk]) assert app.handle_leave.call_count == 1 assert app.handle_leave.call_args[0][0] == nwk - - -def test_handle_tc_join_handler(app, ieee): - """Test updating device NWK on TC join/rejoin callbacks.""" - nwk = t.EmberNodeId(0x1234) - new_nwk = t.EmberNodeId(0x4321) - - app.ezsp_callback_handler( - "trustCenterJoinHandler", - ( - nwk, - ieee, - t.EmberDeviceUpdate.STANDARD_SECURITY_SECURED_REJOIN, - mock.sentinel.decision, - mock.sentinel.parent, - ), - ) - - dev = app.add_device(ieee, nwk) - app.ezsp_callback_handler( - "trustCenterJoinHandler", - ( - nwk, - ieee, - t.EmberDeviceUpdate.STANDARD_SECURITY_SECURED_REJOIN, - mock.sentinel.decision, - mock.sentinel.parent, - ), - ) - assert dev.nwk == nwk - - app.ezsp_callback_handler( - "trustCenterJoinHandler", - ( - new_nwk, - ieee, - t.EmberDeviceUpdate.STANDARD_SECURITY_SECURED_REJOIN, - mock.sentinel.decision, - mock.sentinel.parent, - ), - ) - assert dev.nwk == new_nwk - - ieee[0] = 0x22 - app.ezsp_callback_handler( - "trustCenterJoinHandler", - ( - nwk, - ieee, - t.EmberDeviceUpdate.STANDARD_SECURITY_SECURED_REJOIN, - mock.sentinel.decision, - mock.sentinel.parent, - ), - ) - assert dev.nwk == new_nwk From 2c29bf080b3022f7001852ea9e847c20effed5ea Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 21 Jul 2020 15:07:58 -0400 Subject: [PATCH 9/9] 0.18.0 Version bump. --- bellows/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bellows/__init__.py b/bellows/__init__.py index 561ba11c..0fed60ea 100644 --- a/bellows/__init__.py +++ b/bellows/__init__.py @@ -1,5 +1,5 @@ MAJOR_VERSION = 0 MINOR_VERSION = 18 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION)