async def test_znp_auto_connect(mocker, event_loop, pingable_serial_port): AUTO_DETECTED_PORT = "/dev/ttyWorkingUSB1" uart_guess_port = mocker.patch( "zigpy_znp.uart.guess_port", return_value=AUTO_DETECTED_PORT ) async def fixed_uart_connect(config, api): protocol = await connect(config, api) protocol.transport.serial.name = AUTO_DETECTED_PORT return protocol mock = mocker.patch("zigpy_znp.uart.connect", side_effect=fixed_uart_connect) api = ZNP(config_for_port_path("auto")) await api.connect() assert uart_guess_port.call_count == 1 assert mock.call_count == 1 api.close() await api.connect() # We should not detect the port again # The user might have multiple matching devices assert uart_guess_port.call_count == 1 assert mock.call_count == 2
async def test_api_close(znp, event_loop): closed_future = event_loop.create_future() znp_connection_lost = znp.connection_lost def intercepted_connection_lost(exc): closed_future.set_result(exc) return znp_connection_lost(exc) znp._reconnect_task = Mock() znp._reconnect_task.done = lambda: False znp.connection_lost = intercepted_connection_lost znp.close() # connection_lost with no exc indicates the port was closed assert (await closed_future) is None # Make sure our UART was actually closed assert znp._uart is None # ZNP.close should not throw any errors znp2 = ZNP(TEST_APP_CONFIG) znp2.close() znp2.close() znp.close() znp.close()
async def test_znp_connect_without_test(mocker, event_loop, pingable_serial_port): api = ZNP(TEST_APP_CONFIG) api.request = mocker.Mock(wraps=api.request) await api.connect(test_port=False) # Nothing should have been sent assert api.request.call_count == 0
def znp(mocker): api = ZNP(TEST_APP_CONFIG) transport = mocker.Mock() transport.close = lambda: api._uart.connection_lost(exc=None) api._uart = ZnpMtProtocol(api) api._uart.send = mocker.Mock(wraps=api._uart.send) api._uart.connection_made(transport) return api
async def probe(cls, device_config: conf.ConfigType) -> bool: znp = ZNP(conf.CONFIG_SCHEMA({conf.CONF_DEVICE: device_config})) LOGGER.debug("Probing %s", znp._port_path) try: await znp.connect() return True except Exception as e: LOGGER.warning("Failed to probe ZNP radio with config %s", device_config, exc_info=e) return False finally: znp.close()
async def probe(cls, device_config: conf.ConfigType) -> bool: new_schema = conf.CONFIG_SCHEMA({ conf.CONF_DEVICE: device_config, conf.CONF_ZNP_CONFIG: { conf.CONF_AUTO_RECONNECT: False }, }) znp = ZNP(new_schema) LOGGER.debug("Probing %s", znp._port_path) try: await znp.connect() return True except Exception: return False finally: znp.close()
def pingable_serial_port(mocker): port_name = "/dev/ttyWorkingUSB1" transport = mocker.Mock() protocol = None api = ZNP(config_for_port_path(port_name)) api.set_application(mocker.Mock()) api._app.startup = Mock(return_value=lambda: asyncio.sleep(0)) def ping_responder(data): # XXX: this assumes that our UART will send packets perfectly framed if data == bytes.fromhex("FE 00 21 01 20"): protocol.data_received(bytes.fromhex("FE 02 61 01 00 01 63")) elif data == bytes.fromhex("FE 00 21 02 23"): protocol.data_received(b"\xFE\x0E\x61\x02\x02\x01\x02\x07\x01\xE1" b"\x3B\x34\x01\x00\xFF\xFF\xFF\xFF\x85") transport.write = mocker.Mock(side_effect=ping_responder) old_serial_connect = serial_asyncio.create_serial_connection def dummy_serial_conn(loop, protocol_factory, url, *args, **kwargs): # Only our virtual port is handled differently if url != port_name: return old_serial_connect(loop, protocol_factory, url, *args, **kwargs) fut = loop.create_future() assert url == port_name nonlocal protocol protocol = protocol_factory() protocol.connection_made(transport) fut.set_result((transport, protocol)) return fut mocker.patch("serial_asyncio.create_serial_connection", new=dummy_serial_conn) return port_name
async def restore(radio_path, backup): znp = ZNP(CONFIG_SCHEMA({"device": {"path": radio_path}})) await znp.connect() for nwk_nvid, value in backup["nwk"].items(): nvid = NwkNvIds[nwk_nvid] value = bytes.fromhex(value) # XXX: are any NVIDs not filled all the way? try: await znp.request( c.SYS.OSALNVItemInit.Req(Id=nvid, ItemLen=len(value), Value=value), RspStatus=t.Status.SUCCESS, ) await znp.nvram_write(nvid, value) except InvalidCommandResponse: LOGGER.warning("Write failed for %s = %s", nvid, value) for osal_nvid, value in backup["osal"].items(): nvid = OsalExNvIds[osal_nvid] value = bytes.fromhex(value) try: await znp.request( c.SYS.NVWrite.Req(SysId=1, ItemId=nvid, SubId=0, Offset=0, Value=value), RspStatus=t.Status.SUCCESS, ) except InvalidCommandResponse: LOGGER.warning("Write failed for %s = %s", nvid, value) # Reset afterwards to have the new values take effect await znp.request_callback_rsp( request=c.SYS.ResetReq.Req(Type=t.ResetType.Soft), callback=c.SYS.ResetInd.Callback(partial=True), )
async def read_firmware(radio_path: str) -> bytearray: znp = ZNP(CONFIG_SCHEMA({"device": {"path": radio_path}})) # The bootloader handshake must be the very first command await znp.connect(test_port=False) try: async with async_timeout.timeout(5): handshake_rsp = await znp.request_callback_rsp( request=c.UBL.HandshakeReq.Req(), callback=c.UBL.HandshakeRsp.Callback(partial=True), ) except asyncio.TimeoutError: raise RuntimeError( "Did not receive a bootloader handshake response!" " Make sure your adapter has just been plugged in and" " nothing else has had a chance to communicate with it.") if handshake_rsp.Status != c.ubl.BootloaderStatus.SUCCESS: raise RuntimeError( f"Bad bootloader handshake response: {handshake_rsp}") # All reads and writes are this size buffer_size = handshake_rsp.BufferSize data = bytearray() for offset in range(0, c.ubl.IMAGE_SIZE, buffer_size): address = offset // c.ubl.FLASH_WORD_SIZE LOGGER.info("Progress: %0.2f%%", (100.0 * offset) / c.ubl.IMAGE_SIZE) read_rsp = await znp.request_callback_rsp( request=c.UBL.ReadReq.Req(FlashWordAddr=address), callback=c.UBL.ReadRsp.Callback(partial=True), ) assert read_rsp.Status == c.ubl.BootloaderStatus.SUCCESS assert read_rsp.FlashWordAddr == address assert len(read_rsp.Data) == buffer_size data.extend(read_rsp.Data) return data
async def backup(radio_path): znp = ZNP(CONFIG_SCHEMA({"device": {"path": radio_path}})) await znp.connect() data = { "osal": {}, "nwk": {}, } for nwk_nvid in NwkNvIds: try: value = await znp.nvram_read(nwk_nvid) LOGGER.info("%s = %s", nwk_nvid, value) data["nwk"][nwk_nvid.name] = value.hex() except InvalidCommandResponse: LOGGER.warning("Read failed for %s", nwk_nvid) continue for osal_nvid in OsalExNvIds: length_rsp = await znp.request( c.SYS.NVLength.Req(SysId=1, ItemId=osal_nvid, SubId=0)) length = length_rsp.Length if length == 0: LOGGER.warning("Read failed for %s", osal_nvid) continue value = (await znp.request( c.SYS.NVRead.Req(SysId=1, ItemId=osal_nvid, SubId=0, Offset=0, Length=length), RspStatus=t.Status.SUCCESS, )).Value LOGGER.info("%s = %s", osal_nvid, value) data["osal"][osal_nvid.name] = value.hex() return data
async def test_request_callback_rsp(pingable_serial_port, event_loop): api = ZNP(TEST_APP_CONFIG) await api.connect() def send_responses(): api._uart.data_received( TransportFrame( c.AFCommands.DataRequest.Rsp(Status=t.Status.Success).to_frame() ).serialize() + TransportFrame( c.AFCommands.DataConfirm.Callback( Endpoint=56, TSN=1, Status=t.Status.Success ).to_frame() ).serialize() ) event_loop.call_later(0.1, send_responses) # The UART sometimes replies with a SRSP and an AREQ faster than # we can register callbacks for both. This method is a workaround. response = await api.request_callback_rsp( request=c.AFCommands.DataRequest.Req( DstAddr=0x1234, DstEndpoint=56, SrcEndpoint=78, ClusterId=90, TSN=1, Options=c.af.TransmitOptions.RouteDiscovery, Radius=30, Data=b"hello", ), RspStatus=t.Status.Success, callback=c.AFCommands.DataConfirm.Callback(partial=True, Endpoint=56, TSN=1), ) # Our response is the callback, not the confirmation response assert response == c.AFCommands.DataConfirm.Callback( Endpoint=56, TSN=1, Status=t.Status.Success )
async def test_znp_connect(mocker, event_loop, pingable_serial_port): api = ZNP(TEST_APP_CONFIG) await api.connect()
async def test_api_reconnect(event_loop, mocker): SREQ_TIMEOUT = 0.2 port_path = "/dev/ttyUSB1" config = conf.CONFIG_SCHEMA( { conf.CONF_DEVICE: {conf.CONF_DEVICE_PATH: port_path}, conf.CONF_ZNP_CONFIG: { conf.CONF_SREQ_TIMEOUT: SREQ_TIMEOUT, conf.CONF_AUTO_RECONNECT_RETRY_DELAY: 0.01, }, } ) transport = mocker.Mock() def dummy_serial_conn(loop, protocol_factory, url, *args, **kwargs): fut = loop.create_future() assert url == port_path protocol = protocol_factory() protocol.connection_made(transport) fut.set_result((transport, protocol)) return fut mocker.patch("serial_asyncio.create_serial_connection", new=dummy_serial_conn) mocker.patch("zigpy_znp.uart.connect", wraps=zigpy_znp.uart.connect) app = mocker.Mock() app.startup = Mock(return_value=asyncio.sleep(0)) api = ZNP(config) api.set_application(app) connect_fut = event_loop.create_future() connect_task = asyncio.create_task(api.connect()) connect_task.add_done_callback(lambda _: connect_fut.set_result(None)) while transport.write.call_count < 1: await asyncio.sleep(0.01) # XXX: not ideal # We should have receiving a ping transport.write.assert_called_once_with(bytes.fromhex("FE 00 21 01 20")) # Send a ping response api._uart.data_received(bytes.fromhex("FE 02 61 01 00 01 63")) # Wait to connect await connect_fut assert api._port_path == port_path transport.reset_mock() # Now that we're connected, close the connection due to an error assert transport.write.call_count == 0 api.connection_lost(RuntimeError("Uh oh")) # We should get another ping request soon while transport.write.call_count != 1: await asyncio.sleep(0.01) # XXX: not ideal transport.write.assert_called_once_with(bytes.fromhex("FE 00 21 01 20")) # Reply incorrectly to the ping request api._uart.data_received(b"bad response") # We should still have the old connection info assert api._port_path == port_path # Wait for the SREQ_TIMEOUT to pass, we should fail to reconnect await asyncio.sleep(SREQ_TIMEOUT + 0.1) transport.reset_mock() # We wait a bit again for another ping while transport.write.call_count != 1: await asyncio.sleep(0.01) # XXX: not ideal transport.write.assert_called_once_with(bytes.fromhex("FE 00 21 01 20")) # Our reconnect task should complete after we send the ping reply reconnect_fut = event_loop.create_future() api._reconnect_task.add_done_callback(lambda _: reconnect_fut.set_result(None)) # App re-startup should not have happened, we've never reconnected before assert api._app.startup.call_count == 0 api._uart.data_received(bytes.fromhex("FE 02 61 01 00 01 63")) # We should be reconnected soon and the app should have been restarted await reconnect_fut assert api._app.startup.call_count == 1
async def startup(self, auto_form=False): """Perform a complete application startup""" self._znp = ZNP(self.config) self._bind_callbacks(self._znp) await self._znp.connect() await self._reset(t.ResetType.Soft) if auto_form and False: # XXX: actually form a network await self.form_network() if self.config[conf.CONF_ZNP_CONFIG][conf.CONF_TX_POWER] is not None: dbm = self.config[conf.CONF_ZNP_CONFIG][conf.CONF_TX_POWER] await self._znp.request(c.SysCommands.SetTxPower.Req(TXPower=dbm), RspStatus=t.Status.Success) """ # Get our active endpoints endpoints = await self._znp.request_callback_rsp( request=c.ZDOCommands.ActiveEpReq.Req( DstAddr=0x0000, NWKAddrOfInterest=0x0000 ), RspStatus=t.Status.Success, callback=c.ZDOCommands.ActiveEpRsp.Callback(partial=True), ) # Clear out the list of active endpoints for endpoint in endpoints.ActiveEndpoints: await self._znp.request( c.AFCommands.Delete(Endpoint=endpoint), RspStatus=t.Status.Success ) """ # Register our endpoints await self._znp.request( c.AFCommands.Register.Req( Endpoint=1, ProfileId=zigpy.profiles.zha.PROFILE_ID, DeviceId=zigpy.profiles.zha.DeviceType.CONFIGURATION_TOOL, DeviceVersion=0x00, LatencyReq=c.af.LatencyReq.NoLatencyReqs, InputClusters=[], OutputClusters=[], ), RspStatus=t.Status.Success, ) await self._znp.request( c.AFCommands.Register.Req( Endpoint=8, ProfileId=zigpy.profiles.zha.PROFILE_ID, DeviceId=zigpy.profiles.zha.DeviceType.IAS_CONTROL, DeviceVersion=0x00, LatencyReq=c.af.LatencyReq.NoLatencyReqs, InputClusters=[], OutputClusters=[IasZone.cluster_id], ), RspStatus=t.Status.Success, ) await self._znp.request( c.AFCommands.Register.Req( Endpoint=11, ProfileId=zigpy.profiles.zha.PROFILE_ID, DeviceId=zigpy.profiles.zha.DeviceType.CONFIGURATION_TOOL, DeviceVersion=0x00, LatencyReq=c.af.LatencyReq.NoLatencyReqs, InputClusters=[], OutputClusters=[], ), RspStatus=t.Status.Success, ) await self._znp.request( c.AFCommands.Register.Req( Endpoint=12, ProfileId=zigpy.profiles.zha.PROFILE_ID, DeviceId=zigpy.profiles.zha.DeviceType.CONFIGURATION_TOOL, DeviceVersion=0x00, LatencyReq=c.af.LatencyReq.NoLatencyReqs, InputClusters=[], OutputClusters=[], ), RspStatus=t.Status.Success, ) await self._znp.request( c.AFCommands.Register.Req( Endpoint=100, ProfileId=zigpy.profiles.zll.PROFILE_ID, DeviceId=0x0005, DeviceVersion=0x00, LatencyReq=c.af.LatencyReq.NoLatencyReqs, InputClusters=[], OutputClusters=[], ), RspStatus=t.Status.Success, ) # Start commissioning and wait until it's done comm_notification = await self._znp.request_callback_rsp( request=c.APPConfigCommands.BDBStartCommissioning.Req( Mode=c.app_config.BDBCommissioningMode.NetworkFormation), RspStatus=t.Status.Success, callback=c.APPConfigCommands.BDBCommissioningNotification.Callback( partial=True, RemainingModes=c.app_config.BDBRemainingCommissioningModes. NONE, ), ) # XXX: Commissioning fails for me yet I experience no issues if comm_notification.Status != c.app_config.BDBCommissioningStatus.Success: LOGGER.warning("BDB commissioning did not succeed: %s", comm_notification.Status)
async def write_firmware(firmware: bytes, radio_path: str): if len(firmware) != c.ubl.IMAGE_SIZE: raise ValueError(f"Firmware is the wrong size." f" Expected {c.ubl.IMAGE_SIZE}, got {len(firmware)}") znp = ZNP(CONFIG_SCHEMA({"device": {"path": radio_path}})) # The bootloader handshake must be the very first command await znp.connect(test_port=False) try: async with async_timeout.timeout(5): handshake_rsp = await znp.request_callback_rsp( request=c.UBL.HandshakeReq.Req(), callback=c.UBL.HandshakeRsp.Callback(partial=True), ) except asyncio.TimeoutError: raise RuntimeError( "Did not receive a bootloader handshake response!" " Make sure your adapter has just been plugged in and" " nothing else has had a chance to communicate with it.") if handshake_rsp.Status != c.ubl.BootloaderStatus.SUCCESS: raise RuntimeError( f"Bad bootloader handshake response: {handshake_rsp}") # All reads and writes are this size buffer_size = handshake_rsp.BufferSize for offset in range(0, c.ubl.IMAGE_SIZE, buffer_size): address = offset // c.ubl.FLASH_WORD_SIZE LOGGER.info("Write progress: %0.2f%%", (100.0 * offset) / c.ubl.IMAGE_SIZE) write_rsp = await znp.request_callback_rsp( request=c.UBL.WriteReq.Req( FlashWordAddr=address, Data=t.TrailingBytes(firmware[offset:offset + buffer_size]), ), callback=c.UBL.WriteRsp.Callback(partial=True), ) assert write_rsp.Status == c.ubl.BootloaderStatus.SUCCESS # Now we have to read it all back # TODO: figure out how the CRC is computed! for offset in range(0, c.ubl.IMAGE_SIZE, buffer_size): address = offset // c.ubl.FLASH_WORD_SIZE LOGGER.info("Verification progress: %0.2f%%", (100.0 * offset) / c.ubl.IMAGE_SIZE) read_rsp = await znp.request_callback_rsp( request=c.UBL.ReadReq.Req(FlashWordAddr=address, ), callback=c.UBL.ReadRsp.Callback(partial=True), ) assert read_rsp.Status == c.ubl.BootloaderStatus.SUCCESS assert read_rsp.FlashWordAddr == address assert read_rsp.Data == firmware[offset:offset + buffer_size] # This seems to cause the firmware to compute and verify the CRC enable_rsp = await znp.request_callback_rsp( request=c.UBL.EnableReq.Req(), callback=c.UBL.EnableRsp.Callback(partial=True), ) assert enable_rsp.Status == c.ubl.BootloaderStatus.SUCCESS
async def startup(self, auto_form=False): """Perform a complete application startup""" znp = ZNP(self.config) znp.set_application(self) self._bind_callbacks(znp) await znp.connect() self._znp = znp # XXX: To make sure we don't switch to the wrong device upon reconnect, # update our config to point to the last-detected port. if self._config[conf.CONF_DEVICE][conf.CONF_DEVICE_PATH] == "auto": self._config[conf.CONF_DEVICE][ conf.CONF_DEVICE_PATH] = self._znp._uart.transport.serial.name # It's better to configure these explicitly than rely on the NVRAM defaults await self._znp.nvram_write(NwkNvIds.CONCENTRATOR_ENABLE, t.Bool(True)) await self._znp.nvram_write(NwkNvIds.CONCENTRATOR_DISCOVERY, t.uint8_t(120)) await self._znp.nvram_write(NwkNvIds.CONCENTRATOR_RC, t.Bool(True)) await self._znp.nvram_write(NwkNvIds.SRC_RTG_EXPIRY_TIME, t.uint8_t(255)) await self._znp.nvram_write(NwkNvIds.NWK_CHILD_AGE_ENABLE, t.Bool(False)) # XXX: the undocumented `znpBasicCfg` request can do this await self._znp.nvram_write(NwkNvIds.LOGICAL_TYPE, t.DeviceLogicalType.Coordinator) # Reset to make the above NVRAM writes take effect. # This also ensures any previously-started network joins don't continue. await self._reset() try: is_configured = (await self._znp.nvram_read( NwkNvIds.HAS_CONFIGURED_ZSTACK3)) == b"\x55" except InvalidCommandResponse as e: assert e.response.Status == t.Status.INVALID_PARAMETER is_configured = False if not is_configured and not auto_form: raise RuntimeError( "Cannot start application, network is not formed") elif auto_form and is_configured: LOGGER.info( "ZNP is already configured, no need to form a network.") elif auto_form and not is_configured: await self.form_network() if self.config[conf.CONF_ZNP_CONFIG][conf.CONF_TX_POWER] is not None: dbm = self.config[conf.CONF_ZNP_CONFIG][conf.CONF_TX_POWER] await self._znp.request(c.SYS.SetTxPower.Req(TXPower=dbm), RspStatus=t.Status.SUCCESS) device_info = await self._znp.request(c.Util.GetDeviceInfo.Req(), RspStatus=t.Status.SUCCESS) self._ieee = device_info.IEEE if device_info.DeviceState != t.DeviceState.StartedAsCoordinator: # Start the application and wait until it's ready await self._znp.request_callback_rsp( request=c.ZDO.StartupFromApp.Req(StartDelay=100), RspState=c.zdo.StartupState.RestoredNetworkState, callback=c.ZDO.StateChangeInd.Callback( State=t.DeviceState.StartedAsCoordinator), ) # Get our active endpoints endpoints = await self._znp.request_callback_rsp( request=c.ZDO.ActiveEpReq.Req(DstAddr=0x0000, NWKAddrOfInterest=0x0000), RspStatus=t.Status.SUCCESS, callback=c.ZDO.ActiveEpRsp.Callback(partial=True), ) # Clear out the list of active endpoints for endpoint in endpoints.ActiveEndpoints: await self._znp.request(c.AF.Delete.Req(Endpoint=endpoint), RspStatus=t.Status.SUCCESS) # Register our endpoints await self._register_endpoint(endpoint=1) await self._register_endpoint( endpoint=8, device_id=zigpy.profiles.zha.DeviceType.IAS_CONTROL, output_clusters=[clusters.security.IasZone.cluster_id], ) await self._register_endpoint(endpoint=11) await self._register_endpoint(endpoint=12) await self._register_endpoint( endpoint=13, input_clusters=[clusters.general.Ota.cluster_id]) await self._register_endpoint(endpoint=100, profile_id=zigpy.profiles.zll.PROFILE_ID, device_id=0x0005) # Structure is in `zstack/stack/nwk/nwk.h` nib = await self._znp.nvram_read(NwkNvIds.NIB) self._channel = nib[24] self._channels = t.Channels.deserialize(nib[40:44])[0]