Add nodes automatically to LoRaServer

Hello,

I am newbie in LoRaWAN world so I hope that you can help me with this topic. I wonder if it’s possible the action of adding nodes/end-devices in an automatic way by knowing the keys into LoRaServer. Actually I have already created an application and some device but in the manual way.

I want to do this because I have a big quantity of devices and it is not possible to add them manually. If this it’s possible and you can provide me information, maybe some steps to follow or something like this I would really appreaciate it.

Many thanks!

Hello, you can use grpc framework to do it.

Here an examle provided by @brocaar, in this example you will connect you to your loraserver with your credentials then you have to provide an xlsx file with all information concerning your devices.

package main

import (
	"context"
	"crypto/tls"
	"flag"
	"log"

	"github.com/pkg/errors"
	"github.com/tealeg/xlsx"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials"

	"github.com/brocaar/lora-app-server/api"
)

// JWTCredentials provides JWT credentials for gRPC
type JWTCredentials struct {
	token string
}

// GetRequestMetadata returns the meta-data for a request.
func (j *JWTCredentials) GetRequestMetadata(ctx context.Context, url ...string) (map[string]string, error) {
	return map[string]string{
		"authorization": j.token,
	}, nil
}

// RequireTransportSecurity ...
func (j *JWTCredentials) RequireTransportSecurity() bool {
	return false
}

// SetToken sets the JWT token.
func (j *JWTCredentials) SetToken(token string) {
	j.token = token
}

// DeviceImportRecord defines a record for a device to import.
type DeviceImportRecord struct {
	DevEUI          string
	ApplicationID   int64
	DeviceProfileID string
	Name            string
	Description     string
	NetworkKey      string
	ApplicationKey  string
}

var (
	username       string
	password       string
	file           string
	apiHost        string
	apiInsecure    bool
	jwtCredentials *JWTCredentials
)

func init() {
	jwtCredentials = &JWTCredentials{}

	flag.StringVar(&username, "username", "admin", "LoRa App Server username")
	flag.StringVar(&password, "password", "admin", "LoRa App Server password")
	flag.StringVar(&file, "file", "", "Path to Excel file")
	flag.StringVar(&apiHost, "api", "localhost:8080", "hostname:port to LoRa App Server API")
	flag.BoolVar(&apiInsecure, "api-insecure", false, "LoRa App Server API does not use TLS")
	flag.Parse()
}

func getGRPCConn() (*grpc.ClientConn, error) {
	dialOpts := []grpc.DialOption{
		grpc.WithBlock(),
		grpc.WithPerRPCCredentials(jwtCredentials),
	}

	if apiInsecure {
		log.Println("using insecure api")
		dialOpts = append(dialOpts, grpc.WithInsecure())
	} else {
		dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
			InsecureSkipVerify: true,
		})))
	}

	conn, err := grpc.Dial(apiHost, dialOpts...)
	if err != nil {
		return nil, errors.Wrap(err, "grpc dial error")
	}

	return conn, nil
}

func login(conn *grpc.ClientConn) error {
	internalClient := api.NewInternalServiceClient(conn)

	resp, err := internalClient.Login(context.Background(), &api.LoginRequest{
		Username: username,
		Password: password,
	})
	if err != nil {
		return errors.Wrap(err, "login error")
	}

	jwtCredentials.SetToken(resp.Jwt)

	return nil
}

func getDeviceImportList() ([]DeviceImportRecord, error) {
	xlFile, err := xlsx.OpenFile(file)
	if err != nil {
		return nil, errors.Wrap(err, "open excel file error")
	}

	var out []DeviceImportRecord

	for _, sheet := range xlFile.Sheets {
		for i, row := range sheet.Rows {
			if i == 0 {
				continue
			}

			if len(row.Cells) != 7 {
				log.Fatalf("expected exactly 7 columns (row %d)", i+1)
			}

			devEUI := row.Cells[0].String()
			applicationID, err := row.Cells[1].Int64()
			if err != nil {
				log.Fatalf("application id parse error (row %d): %s", i+1, err)
			}
			deviceProfileID := row.Cells[2].String()
			name := row.Cells[3].String()
			description := row.Cells[4].String()
			networkKey := row.Cells[5].String()
			applicationKey := row.Cells[6].String()

			out = append(out, DeviceImportRecord{
				DevEUI:          devEUI,
				ApplicationID:   applicationID,
				DeviceProfileID: deviceProfileID,
				Name:            name,
				Description:     description,
				NetworkKey:      networkKey,
				ApplicationKey:  applicationKey,
			})
		}
	}

	return out, nil
}

func importDevices(conn *grpc.ClientConn, devices []DeviceImportRecord) error {
	deviceClient := api.NewDeviceServiceClient(conn)

	for i, dev := range devices {
		d := api.Device{
			DevEui:          dev.DevEUI,
			Name:            dev.Name,
			ApplicationId:   dev.ApplicationID,
			Description:     dev.Description,
			DeviceProfileId: dev.DeviceProfileID,
		}

		dk := api.DeviceKeys{
			DevEui: dev.DevEUI,
			NwkKey: dev.NetworkKey,
			AppKey: dev.ApplicationKey,
		}

		_, err := deviceClient.Create(context.Background(), &api.CreateDeviceRequest{
			Device: &d,
		})
		if err != nil {
			if grpc.Code(err) == codes.AlreadyExists {
				log.Printf("device %s already exists (row %d)", d.DevEui, i+2)
				continue
			}
			log.Fatalf("import error (device %s row %d): %s", d.DevEui, i+2, err)
		}

		_, err = deviceClient.CreateKeys(context.Background(), &api.CreateDeviceKeysRequest{
			DeviceKeys: &dk,
		})
		if err != nil {
			if grpc.Code(err) == codes.AlreadyExists {
				log.Printf("device-keys for device %s already exists (row %d)", d.DevEui, i+2)
				continue
			}
			log.Fatalf("import error (device %s) (row %d): %s", d.DevEui, i+2, err)
		}
	}

	return nil
}

func main() {
	conn, err := getGRPCConn()
	if err != nil {
		log.Fatal("error connecting to api", err)
	}

	if err := login(conn); err != nil {
		log.Fatal("login error", err)
	}

	rows, err := getDeviceImportList()
	if err != nil {
		log.Fatal("get device import records error", err)
	}

	if err := importDevices(conn, rows); err != nil {
		log.Fatal("import error", err)
	}
}

Thanks for your reply! If I understand you, you have to provide to LoRaServer a xlsx file with the devices’ information, isn’t it?

The problem is that I will add end-nodes but not at the same time, for example I can add 20 end-nodes today but in 5 months add another 10 more and I won’t be able to access to the LoRaServer and consequentily the xlsx file because it will be in another country! . So I hope something like if I know the app key that the LoRaServer add the devices that have the same app key!

Many thanks!

Hi @CarlosGD,
you can use the RESTful API [1] e.g. in a Python script to realize it.

[1] RESTful JSON API

Best regards
DeHi

ok, thank you. Can you provide me more information about how to achive this?

Regards!

I’ve suggested the RESTful API because I’m much more familiar with it than with the gprc.
I think it makes sense to take a csv or xlsx file and add all devices with keys and EUIs (like @julien said).

Then, after receiving your JWT token (for example via the requests module), you can compare the list of your previously integrated devices on the app server with the current csv or xlsx list. Here you could, for example, check if there are new DevEUIs in the csv or xlsx list.
The URL to obtain information about devices for an application with the ID Y would be e.g.

https://[SERVER_URL]/api/devices?limit=X&applicationID=Y

Hi , i modified the import github.com/brocaar/lora-app-server/api that does not exist anymore to github.com/brocaar/chirpstack-api/go/as/external/api and I have this error
.\Chirpstack import device.go:97:3: unknown field ‘Username’ in struct literal of type api.LoginRequest

func login(conn *grpc.ClientConn) error {

internalClient := api.NewInternalServiceClient(conn)

resp, err := internalClient.Login(context.Background(), &api.LoginRequest{
    **Username: username,**
    Password: password,
})
if err != nil {
    return errors.Wrap(err, "login error")
}
jwtCredentials.SetToken(resp.Jwt)
return nil