async def form_network(self, channels=[15], pan_id=None, extended_pan_id=None): # These options are read only on startup so we perform a soft reset right after await self._znp.nvram_write(NwkNvIds.STARTUP_OPTION, t.StartupOptions.ClearState) # XXX: the undocumented `znpBasicCfg` request can do this await self._znp.nvram_write(NwkNvIds.LOGICAL_TYPE, t.DeviceLogicalType.Coordinator) await self._reset() # If zgPreConfigKeys is set to TRUE, all devices should use the same # pre-configured security key. If zgPreConfigKeys is set to FALSE, the # pre-configured key is set only on the coordinator device, and is handed to # joining devices. The key is sent in the clear over the last hop. Upon reset, # the device will retrieve the pre-configured key from NV memory if the NV_INIT # compile option is defined (the NV item is called ZCD_NV_PRECFGKEY). await self.update_network( channel=None, channels=t.Channels.from_channel_list(channels), pan_id=0xFFFF if pan_id is None else pan_id, extended_pan_id=ExtendedPanId( os.urandom(8) if extended_pan_id is None else extended_pan_id), network_key=t.KeyData(os.urandom(16)), reset=False, ) # We do not want to receive verbose ZDO callbacks # Just pass ZDO callbacks back to Zigpy await self._znp.nvram_write(NwkNvIds.ZDO_DIRECT_CB, t.Bool(True)) await self._znp.request( c.APPConfigCommands.BDBStartCommissioning.Req( Mode=c.app_config.BDBCommissioningMode.NetworkFormation), RspStatus=t.Status.Success, ) # This may take a while because of some sort of background scanning. # This can probably be disabled. await self._znp.wait_for_response( c.ZDOCommands.StateChangeInd.Callback( State=t.DeviceState.StartedAsCoordinator)) await self._znp.request( c.APPConfigCommands.BDBStartCommissioning.Req( Mode=c.app_config.BDBCommissioningMode.NetworkSteering), RspStatus=t.Status.Success, )
async def form_network(self): # These options are read only on startup so we perform a soft reset right after await self._znp.nvram_write(NwkNvIds.STARTUP_OPTION, t.StartupOptions.ClearState) pan_id = self.config[conf.CONF_NWK][conf.CONF_NWK_PAN_ID] extended_pan_id = self.config[conf.CONF_NWK][ conf.CONF_NWK_EXTENDED_PAN_ID] await self.update_network( channels=self.config[conf.CONF_NWK][conf.CONF_NWK_CHANNELS], pan_id=0xFFFF if pan_id is None else pan_id, extended_pan_id=ExtendedPanId(os.urandom(8)) if extended_pan_id is None else extended_pan_id, network_key=t.KeyData(os.urandom(16)), reset=False, ) # We want to receive all ZDO callbacks to proxy them back go zipgy await self._znp.nvram_write(NwkNvIds.ZDO_DIRECT_CB, t.Bool(True)) # Reset now so that the changes take effect await self._reset() await self._znp.request( c.AppConfig.BDBStartCommissioning.Req( Mode=c.app_config.BDBCommissioningMode.NwkFormation), RspStatus=t.Status.SUCCESS, ) # This may take a while because of some sort of background scanning. # This can probably be disabled. await self._znp.wait_for_response( c.ZDO.StateChangeInd.Callback( State=t.DeviceState.StartedAsCoordinator)) # Create the NV item that keeps track of whether or not we're configured osal_create_rsp = await self._znp.request( c.SYS.OSALNVItemInit.Req(Id=NwkNvIds.HAS_CONFIGURED_ZSTACK3, ItemLen=1, Value=b"\x55")) if osal_create_rsp.Status not in (t.Status.SUCCESS, t.Status.NV_ITEM_UNINIT): raise RuntimeError( "Could not create HAS_CONFIGURED_ZSTACK3 NV item" ) # pragma: no cover # Initializing the item won't guarantee that it holds this exact value await self._znp.nvram_write(NwkNvIds.HAS_CONFIGURED_ZSTACK3, b"\x55")
async def test_update_network(mocker, caplog, application): app, znp_server = application await app.startup(auto_form=False) mocker.patch.object(app, "_reset", new=CoroutineMock()) channel = t.uint8_t(15) pan_id = t.PanId(0x1234) extended_pan_id = t.ExtendedPanId(range(8)) channels = t.Channels.from_channel_list([11, 15, 20]) network_key = t.KeyData(range(16)) channels_updated = znp_server.reply_once_to( request=c.Util.SetChannels.Req(Channels=channels), responses=[c.Util.SetChannels.Rsp(Status=t.Status.SUCCESS)], ) bdb_set_primary_channel = znp_server.reply_once_to( request=c.AppConfig.BDBSetChannel.Req(IsPrimary=True, Channel=channels), responses=[c.AppConfig.BDBSetChannel.Rsp(Status=t.Status.SUCCESS)], ) bdb_set_secondary_channel = znp_server.reply_once_to( request=c.AppConfig.BDBSetChannel.Req(IsPrimary=False, Channel=t.Channels.NO_CHANNELS), responses=[c.AppConfig.BDBSetChannel.Rsp(Status=t.Status.SUCCESS)], ) set_pan_id = znp_server.reply_once_to( request=c.Util.SetPanId.Req(PanId=pan_id), responses=[c.Util.SetPanId.Rsp(Status=t.Status.SUCCESS)], ) set_extended_pan_id = znp_server.reply_once_to( request=c.SYS.OSALNVWrite.Req(Id=NwkNvIds.EXTENDED_PAN_ID, Offset=0, Value=extended_pan_id.serialize()), responses=[c.SYS.OSALNVWrite.Rsp(Status=t.Status.SUCCESS)], ) set_network_key_util = znp_server.reply_once_to( request=c.Util.SetPreConfigKey.Req(PreConfigKey=network_key), responses=[c.Util.SetPreConfigKey.Rsp(Status=t.Status.SUCCESS)], ) set_network_key_nvram = znp_server.reply_once_to( request=c.SYS.OSALNVWrite.Req(Id=NwkNvIds.PRECFGKEYS_ENABLE, Offset=0, Value=t.Bool(True).serialize()), responses=[c.SYS.OSALNVWrite.Rsp(Status=t.Status.SUCCESS)], ) set_nib_nvram = znp_server.reply_once_to( request=c.SYS.OSALNVWrite.Req(Id=NwkNvIds.NIB, Offset=0, partial=True), responses=[c.SYS.OSALNVWrite.Rsp(Status=t.Status.SUCCESS)], ) # But it does succeed with a warning if you explicitly allow it with caplog.at_level(logging.WARNING): await app.update_network( channel=channel, channels=channels, extended_pan_id=extended_pan_id, network_key=network_key, pan_id=pan_id, tc_address=t.EUI64(range(8)), tc_link_key=t.KeyData(range(8)), update_id=0, reset=True, ) # We should receive a few warnings for `tc_` stuff assert len(caplog.records) >= 2 await channels_updated await bdb_set_primary_channel await bdb_set_secondary_channel await set_pan_id await set_extended_pan_id await set_network_key_util await set_network_key_nvram await set_nib_nvram app._reset.assert_called_once_with() # Ensure we set everything we could assert app.nwk_update_id is None # We can't use it assert app.channel == channel assert app.channels == channels assert app.pan_id == pan_id assert app.extended_pan_id == extended_pan_id
async def test_auto_form_necessary(application, mocker): app, znp_server = application nvram = {} mocker.patch.object(app, "update_network", new=CoroutineMock()) mocker.patch.object(app, "_reset", new=CoroutineMock()) def nvram_writer(req): nvram[req.Id] = req.Value return c.SYS.OSALNVWrite.Rsp(Status=t.Status.SUCCESS) def nvram_init(req): nvram[req.Id] = req.Value return c.SYS.OSALNVItemInit.Rsp(Status=t.Status.SUCCESS) # Prevent the fixture's default NVRAM responses, except for the NIB listeners = znp_server._response_listeners[c.SYS.OSALNVRead.Req.header] znp_server._response_listeners[c.SYS.OSALNVRead.Req.header] = [ listener for listener in listeners if listener.matching_commands[0] == c.SYS.OSALNVRead.Req(Id=NwkNvIds.NIB, Offset=0) ] read_zstack_configured = znp_server.reply_once_to( request=c.SYS.OSALNVRead.Req(Id=NwkNvIds.HAS_CONFIGURED_ZSTACK3, Offset=0), responses=[ c.SYS.OSALNVRead.Rsp(Status=t.Status.INVALID_PARAMETER, Value=b"") ], ) znp_server.reply_to(request=c.SYS.OSALNVWrite.Req(Offset=0, partial=True), responses=[nvram_writer]) znp_server.reply_to(request=c.SYS.OSALNVItemInit.Req(partial=True), responses=[nvram_init]) znp_server.reply_to( request=c.AppConfig.BDBStartCommissioning.Req( Mode=c.app_config.BDBCommissioningMode.NwkFormation), responses=[ c.AppConfig.BDBStartCommissioning.Rsp(Status=t.Status.SUCCESS), c.ZDO.StateChangeInd.Callback( State=t.DeviceState.StartedAsCoordinator), ], ) znp_server.reply_to( request=c.AppConfig.BDBStartCommissioning.Req( Mode=c.app_config.BDBCommissioningMode.NwkSteering), responses=[ c.AppConfig.BDBStartCommissioning.Rsp(Status=t.Status.SUCCESS) ], ) await app.startup(auto_form=True) await read_zstack_configured assert app.update_network.call_count == 1 assert app._reset.call_count == 2 assert nvram[NwkNvIds.HAS_CONFIGURED_ZSTACK3] == b"\x55" assert nvram[ NwkNvIds.STARTUP_OPTION] == t.StartupOptions.ClearState.serialize() assert nvram[ NwkNvIds.LOGICAL_TYPE] == t.DeviceLogicalType.Coordinator.serialize() assert nvram[NwkNvIds.ZDO_DIRECT_CB] == t.Bool(True).serialize()
async def update_network( self, *, channel: typing.Optional[t.uint8_t] = None, channels: typing.Optional[t.Channels] = None, extended_pan_id: typing.Optional[t.ExtendedPanId] = None, network_key: typing.Optional[t.KeyData] = None, pan_id: typing.Optional[t.PanId] = None, tc_address: typing.Optional[t.EUI64] = None, tc_link_key: typing.Optional[t.KeyData] = None, update_id: int = 0, reset: bool = True, ): if (channel is not None and channels is not None and not t.Channels.from_channel_list([channel]) & channels): raise ValueError("Channel does not overlap with channel mask") if tc_link_key is not None: LOGGER.warning( "Trust center link key in config is not yet supported") if tc_address is not None: LOGGER.warning( "Trust center address in config is not yet supported") if channels is not None: await self._znp.request( c.Util.SetChannels.Req(Channels=channels), RspStatus=t.Status.SUCCESS, ) await self._znp.request( c.AppConfig.BDBSetChannel.Req(IsPrimary=True, Channel=channels), RspStatus=t.Status.SUCCESS, ) await self._znp.request( c.AppConfig.BDBSetChannel.Req(IsPrimary=False, Channel=t.Channels.NO_CHANNELS), RspStatus=t.Status.SUCCESS, ) self._channels = channels if channel is not None: # We modify the logical channel value directly in the NIB nib = bytearray(await self._znp.nvram_read(NwkNvIds.NIB)) nib[24] = channel await self._znp.nvram_write(NwkNvIds.NIB, nib) self._channel = channel if pan_id is not None: await self._znp.request(c.Util.SetPanId.Req(PanId=pan_id), RspStatus=t.Status.SUCCESS) self._pan_id = pan_id if extended_pan_id is not None: # There is no Util request to do this await self._znp.nvram_write(NwkNvIds.EXTENDED_PAN_ID, extended_pan_id) self._ext_pan_id = extended_pan_id if network_key is not None: await self._znp.request( c.Util.SetPreConfigKey.Req(PreConfigKey=network_key), RspStatus=t.Status.SUCCESS, ) # XXX: The Util request does not actually write to this NV address await self._znp.nvram_write(NwkNvIds.PRECFGKEYS_ENABLE, t.Bool(True)) if reset: # We have to reset afterwards await self._reset()
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]