Skip to content

Commit e110bd7

Browse files
authored
Merge pull request #108 from TheThingsNetwork/feature/wanesy
Support Migration from Wanesy
2 parents f029876 + 7a17539 commit e110bd7

12 files changed

Lines changed: 696 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@
1717
# Build files
1818
/build/
1919
/dist/
20+
21+
# macOS related
22+
*.DS_store

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88

99
### Added
1010

11+
- Support to convert Kerlink Wanesy CSV format to The Things Stack JSON.
12+
1113
### Changed
1214

1315
### Deprecated

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,47 @@ $ ttn-lw-migrate firefly application --all --verbose > devices.json
307307
$ ttn-lw-migrate firefly application --all --invalidate-keys > devices.json
308308
```
309309

310+
## Wanesy
311+
312+
Migration from Kerlink's Wanesy requires exporting the device data into a [CSV](./pkg/source/wanesy/testdata/test.csv) file and feeding it into this tool. Please reach out to Kerlink to get an export of the devices that need to be migrated.
313+
314+
### Configuration
315+
316+
Configure with environment variables, or command-line arguments.
317+
318+
See `ttn-lw-migrate wanesy {device|application} --help` for more details.
319+
320+
The following example shows how to set options via environment variables.
321+
322+
```bash
323+
$ export APP_ID=my-test-app # Application ID for the exported devices
324+
$ export FREQUENCY_PLAN_ID=EU_863_870 # Frequency Plan ID for the exported devices
325+
$ export CSV_PATH=<path> # Local path to the exported CSV file.
326+
```
327+
328+
### Notes
329+
330+
- The export process will halt if any error occurs.
331+
- Since the migration tool uses a CSV file that's exported from WMC and does not interact with the API, make sure to remove/clean up the devices on WMC once the migration is completed.
332+
333+
### Export Devices
334+
335+
To export a single device using its Device EUI (e.g. `1111111111111112`):
336+
337+
```bash
338+
# Export a device from the CSV to TTS format.
339+
$ ttn-lw-migrate wanesy device 1111111111111112 > devices.json
340+
```
341+
342+
### Export All Devices
343+
344+
In order to export all devices from the CSV file, use the `application` command.
345+
346+
```bash
347+
# Export all devices from the CSV.
348+
$ ttn-lw-migrate wanesy application --all
349+
```
350+
310351
## Development Environment
311352

312353
Requires Go version 1.16 or higher. [Download Go](https://golang.org/dl/).

cmd/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"go.thethings.network/lorawan-stack-migrate/cmd/firefly"
2424
"go.thethings.network/lorawan-stack-migrate/cmd/ttnv2"
2525
"go.thethings.network/lorawan-stack-migrate/cmd/tts"
26+
"go.thethings.network/lorawan-stack-migrate/cmd/wanesy"
2627
"go.thethings.network/lorawan-stack-migrate/pkg/export"
2728
"go.thethings.network/lorawan-stack-migrate/pkg/source"
2829
)
@@ -81,4 +82,5 @@ func init() {
8182
rootCmd.AddCommand(tts.TTSCmd)
8283
rootCmd.AddCommand(chirpstack.ChirpStackCmd)
8384
rootCmd.AddCommand(firefly.FireflyCmd)
85+
rootCmd.AddCommand(wanesy.WanesyCmd)
8486
}

cmd/wanesy/wanesy.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright © 2023 The Things Network Foundation, The Things Industries B.V.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package wanesy
16+
17+
import (
18+
"go.thethings.network/lorawan-stack-migrate/pkg/commands"
19+
_ "go.thethings.network/lorawan-stack-migrate/pkg/source/wanesy"
20+
)
21+
22+
const sourceName = "wanesy"
23+
24+
// WanesyCmd represents the Wanesy source.
25+
var WanesyCmd = commands.Source(sourceName,
26+
"Migrate from Wanesy Management Center",
27+
)

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/brocaar/chirpstack-api/go/v3 v3.12.5
1414
github.com/mdempsky/unconvert v0.0.0-20230125054757-2661c2c99a9b
1515
github.com/mgechev/revive v1.3.6
16+
github.com/smarty/assertions v1.15.1
1617
github.com/smartystreets/assertions v1.13.1
1718
github.com/spf13/cobra v1.8.0
1819
github.com/spf13/pflag v1.0.5

pkg/source/wanesy/config.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright © 2024 The Things Network Foundation, The Things Industries B.V.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package wanesy
16+
17+
import (
18+
"encoding/csv"
19+
"encoding/json"
20+
"net/http"
21+
"os"
22+
"strings"
23+
24+
"github.com/spf13/pflag"
25+
26+
"go.thethings.network/lorawan-stack-migrate/pkg/source"
27+
"go.thethings.network/lorawan-stack/v3/pkg/fetch"
28+
"go.thethings.network/lorawan-stack/v3/pkg/frequencyplans"
29+
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
30+
)
31+
32+
type Config struct {
33+
src source.Config
34+
35+
appID string
36+
frequencyPlanID string
37+
csvPath string
38+
all bool
39+
40+
derivedMacVersion ttnpb.MACVersion
41+
derivedPhyVersion ttnpb.PHYVersion
42+
43+
flags *pflag.FlagSet
44+
fpStore *frequencyplans.Store
45+
}
46+
47+
// NewConfig returns a new Wanesy configuration.
48+
func NewConfig() *Config {
49+
config := &Config{
50+
flags: &pflag.FlagSet{},
51+
}
52+
config.flags.StringVar(&config.frequencyPlanID,
53+
"frequency-plan-id",
54+
"",
55+
"Frequency Plan ID for the exported devices")
56+
config.flags.StringVar(&config.appID,
57+
"app-id",
58+
"",
59+
"Application ID for the exported devices")
60+
config.flags.StringVar(&config.csvPath,
61+
"csv-path",
62+
"",
63+
"Path to the CSV file exported from Wanesy Management Center")
64+
config.flags.BoolVar(&config.all,
65+
"all",
66+
false,
67+
"Export all devices in the CSV. This is only used by the application command")
68+
return config
69+
}
70+
71+
// Initialize the configuration.
72+
func (c *Config) Initialize(src source.Config) error {
73+
c.src = src
74+
75+
if c.appID = os.Getenv("APP_ID"); c.appID == "" {
76+
return errNoAppID.New()
77+
}
78+
if c.frequencyPlanID = os.Getenv("FREQUENCY_PLAN_ID"); c.frequencyPlanID == "" {
79+
return errNoFrequencyPlanID.New()
80+
}
81+
if c.csvPath = os.Getenv("CSV_PATH"); c.csvPath == "" {
82+
return errNoCSVFileProvided.New()
83+
}
84+
85+
fpFetcher, err := fetch.FromHTTP(http.DefaultClient, src.FrequencyPlansURL)
86+
if err != nil {
87+
return err
88+
}
89+
c.fpStore = frequencyplans.NewStore(fpFetcher)
90+
91+
return nil
92+
}
93+
94+
// Flags returns the flags for the configuration.
95+
func (c *Config) Flags() *pflag.FlagSet {
96+
return c.flags
97+
}
98+
99+
// ImportDevices imports the devices from the provided CSV file.
100+
func ImportDevices(csvPath string) (Devices, error) {
101+
raw, err := os.ReadFile(csvPath)
102+
if err != nil {
103+
return nil, err
104+
}
105+
reader := csv.NewReader(strings.NewReader(string(raw)))
106+
readValues, err := reader.ReadAll()
107+
if err != nil {
108+
return nil, err
109+
}
110+
if len(readValues) < 2 {
111+
return nil, errNoValuesInCSV.New()
112+
}
113+
values := make([]map[string]string, 0)
114+
for i := 1; i < len(readValues); i++ {
115+
keys := readValues[0]
116+
value := make(map[string]string)
117+
for j := 0; j < len(keys); j++ {
118+
noOfcolumns := len(readValues[i])
119+
if j >= noOfcolumns {
120+
value[keys[j]] = "" // Fill empty columns.
121+
continue
122+
}
123+
value[keys[j]] = readValues[i][j]
124+
}
125+
values = append(values, value)
126+
}
127+
j, err := json.Marshal(values)
128+
if err != nil {
129+
return nil, err
130+
}
131+
devices := make(Devices)
132+
err = devices.UnmarshalJSON(j)
133+
if err != nil {
134+
return nil, err
135+
}
136+
return devices, nil
137+
}

0 commit comments

Comments
 (0)