diff --git a/README.md b/README.md index f8d625f6..e31915e9 100644 --- a/README.md +++ b/README.md @@ -43,22 +43,22 @@ Devices currently present on Arduino IoT Cloud can be retrieved by using this co ## Thing commands -Things can be created starting from a template or by cloning another thing. Additionally, a thing name should be specified. +Things can be created starting from a template or by cloning another thing. -Create a thing from a template: +Create a thing from a thing template. The name parameter is optional. If it is provided then it overrides the name retrieved from the template: `$ iot-cloud-cli thing create --name --template ` -Create a thing by cloning another thing: +Create a thing by cloning another thing, here the *name is mandatory*: -`$ iot-cloud-cli thing create --name --clone-id ` +`$ iot-cloud-cli thing clone --name --clone-id ` Things can be printed thanks to a list command. -Print a list of available things and their properties by using this command: +Print a list of available things and their variables by using this command: -`$ iot-cloud-cli thing list --properties` +`$ iot-cloud-cli thing list --show-variables` Print a *filtered* list of available things, print only things belonging to the ids list: diff --git a/cli/thing/clone.go b/cli/thing/clone.go new file mode 100644 index 00000000..c976f69b --- /dev/null +++ b/cli/thing/clone.go @@ -0,0 +1,44 @@ +package thing + +import ( + "fmt" + + "github.com/arduino/iot-cloud-cli/command/thing" + "github.com/spf13/cobra" +) + +var cloneFlags struct { + name string + cloneID string +} + +func initCloneCommand() *cobra.Command { + cloneCommand := &cobra.Command{ + Use: "clone", + Short: "Clone a thing", + Long: "Clone a thing for Arduino IoT Cloud", + RunE: runCloneCommand, + } + cloneCommand.Flags().StringVarP(&cloneFlags.name, "name", "n", "", "Thing name") + cloneCommand.Flags().StringVarP(&cloneFlags.cloneID, "clone-id", "c", "", "ID of Thing to be cloned") + cloneCommand.MarkFlagRequired("name") + cloneCommand.MarkFlagRequired("clone-id") + return cloneCommand +} + +func runCloneCommand(cmd *cobra.Command, args []string) error { + fmt.Printf("Cloning thing %s into a new thing called %s\n", cloneFlags.cloneID, cloneFlags.name) + + params := &thing.CloneParams{ + Name: cloneFlags.name, + CloneID: cloneFlags.cloneID, + } + + thingID, err := thing.Clone(params) + if err != nil { + return err + } + + fmt.Printf("IoT Cloud thing created with ID: %s\n", thingID) + return nil +} diff --git a/cli/thing/create.go b/cli/thing/create.go index 1f48c44f..290929d8 100644 --- a/cli/thing/create.go +++ b/cli/thing/create.go @@ -9,34 +9,28 @@ import ( var createFlags struct { name string - deviceID string template string - cloneID string } func initCreateCommand() *cobra.Command { createCommand := &cobra.Command{ Use: "create", - Short: "Create a thing", - Long: "Create a thing for Arduino IoT Cloud", + Short: "Create a thing from a template", + Long: "Create a thing from a template for Arduino IoT Cloud", RunE: runCreateCommand, } createCommand.Flags().StringVarP(&createFlags.name, "name", "n", "", "Thing name") - createCommand.Flags().StringVarP(&createFlags.deviceID, "device-id", "d", "", "ID of Device to bind to the new thing") - createCommand.Flags().StringVarP(&createFlags.cloneID, "clone-id", "c", "", "ID of Thing to be cloned") createCommand.Flags().StringVarP(&createFlags.template, "template", "t", "", "File containing a thing template") - createCommand.MarkFlagRequired("name") + createCommand.MarkFlagRequired("template") return createCommand } func runCreateCommand(cmd *cobra.Command, args []string) error { - fmt.Printf("Creating thing with name %s\n", createFlags.name) + fmt.Printf("Creating thing from template %s\n", createFlags.template) params := &thing.CreateParams{ Name: createFlags.name, - DeviceID: createFlags.deviceID, Template: createFlags.template, - CloneID: createFlags.cloneID, } thingID, err := thing.Create(params) diff --git a/cli/thing/extract.go b/cli/thing/extract.go index 0d82a90a..5260f293 100644 --- a/cli/thing/extract.go +++ b/cli/thing/extract.go @@ -15,8 +15,8 @@ var extractFlags struct { func initExtractCommand() *cobra.Command { extractCommand := &cobra.Command{ Use: "extract", - Short: "Extract and save a thing", - Long: "Extract a thing from Arduino IoT Cloud and save it in a template file", + Short: "Extract a template from a thing", + Long: "Extract a template from a Arduino IoT Cloud thing and save it in a file", RunE: runExtractCommand, } extractCommand.Flags().StringVarP(&extractFlags.id, "id", "i", "", "Thing ID") @@ -26,7 +26,7 @@ func initExtractCommand() *cobra.Command { } func runExtractCommand(cmd *cobra.Command, args []string) error { - fmt.Printf("Extracting thing %s\n", extractFlags.id) + fmt.Printf("Extracting template from thing %s\n", extractFlags.id) params := &thing.ExtractParams{ID: extractFlags.id} if extractFlags.outfile != "" { @@ -38,6 +38,6 @@ func runExtractCommand(cmd *cobra.Command, args []string) error { return err } - fmt.Println("Thing successfully extracted") + fmt.Println("Template successfully extracted") return nil } diff --git a/cli/thing/list.go b/cli/thing/list.go index 5212af2c..961e1168 100644 --- a/cli/thing/list.go +++ b/cli/thing/list.go @@ -11,9 +11,9 @@ import ( ) var listFlags struct { - ids []string - deviceID string - properties bool + ids []string + deviceID string + variables bool } func initListCommand() *cobra.Command { @@ -27,7 +27,7 @@ func initListCommand() *cobra.Command { listCommand.Flags().StringSliceVarP(&listFlags.ids, "ids", "i", []string{}, "List of thing IDs to be retrieved") // list only the thing associated to the passed device id listCommand.Flags().StringVarP(&listFlags.deviceID, "device-id", "d", "", "ID of Device associated to the thing to be retrieved") - listCommand.Flags().BoolVarP(&listFlags.properties, "properties", "p", false, "Show thing properties") + listCommand.Flags().BoolVarP(&listFlags.variables, "show-variables", "s", false, "Show thing variables") return listCommand } @@ -35,8 +35,8 @@ func runListCommand(cmd *cobra.Command, args []string) error { fmt.Println("Listing things") params := &thing.ListParams{ - IDs: listFlags.ids, - Properties: listFlags.properties, + IDs: listFlags.ids, + Variables: listFlags.variables, } if listFlags.deviceID != "" { params.DeviceID = &listFlags.deviceID @@ -66,15 +66,15 @@ func (r result) String() string { t := table.New() h := []interface{}{"Name", "ID", "Device"} - if listFlags.properties { - h = append(h, "Properties") + if listFlags.variables { + h = append(h, "Variables") } t.SetHeader(h...) for _, thing := range r.things { r := []interface{}{thing.Name, thing.ID, thing.DeviceID} - if listFlags.properties { - r = append(r, strings.Join(thing.Properties, ", ")) + if listFlags.variables { + r = append(r, strings.Join(thing.Variables, ", ")) } t.AddRow(r...) } diff --git a/cli/thing/thing.go b/cli/thing/thing.go index a95dd358..59edf3a9 100644 --- a/cli/thing/thing.go +++ b/cli/thing/thing.go @@ -12,6 +12,7 @@ func NewCommand() *cobra.Command { } thingCommand.AddCommand(initCreateCommand()) + thingCommand.AddCommand(initCloneCommand()) thingCommand.AddCommand(initListCommand()) thingCommand.AddCommand(initDeleteCommand()) thingCommand.AddCommand(initExtractCommand()) diff --git a/command/thing/clone.go b/command/thing/clone.go new file mode 100644 index 00000000..f42dbedb --- /dev/null +++ b/command/thing/clone.go @@ -0,0 +1,66 @@ +package thing + +import ( + "fmt" + + iotclient "github.com/arduino/iot-client-go" + "github.com/arduino/iot-cloud-cli/internal/config" + "github.com/arduino/iot-cloud-cli/internal/iot" +) + +// CloneParams contains the parameters needed to clone a thing. +type CloneParams struct { + // Mandatory - contains the name of the thing + Name string + // Mandatory - specifies ID of thing to be cloned + CloneID string +} + +// Clone allows to create a new thing from an already existing one +func Clone(params *CloneParams) (string, error) { + conf, err := config.Retrieve() + if err != nil { + return "", err + } + iotClient, err := iot.NewClient(conf.Client, conf.Secret) + if err != nil { + return "", err + } + + thing, err := retrieve(iotClient, params.CloneID) + if err != nil { + return "", err + } + + thing.Name = params.Name + force := true + thingID, err := iotClient.AddThing(thing, force) + if err != nil { + return "", err + } + + return thingID, nil +} + +func retrieve(client iot.Client, thingID string) (*iotclient.Thing, error) { + clone, err := client.GetThing(thingID) + if err != nil { + return nil, fmt.Errorf("%s: %w", "retrieving the thing to be cloned", err) + } + + thing := &iotclient.Thing{} + + // Copy variables + for _, p := range clone.Properties { + thing.Properties = append(thing.Properties, iotclient.Property{ + Name: p.Name, + Permission: p.Permission, + UpdateParameter: p.UpdateParameter, + UpdateStrategy: p.UpdateStrategy, + Type: p.Type, + VariableName: p.VariableName, + }) + } + + return thing, nil +} diff --git a/command/thing/create.go b/command/thing/create.go index b4537154..afda2897 100644 --- a/command/thing/create.go +++ b/command/thing/create.go @@ -2,12 +2,11 @@ package thing import ( "encoding/json" + "errors" "fmt" "io/ioutil" "os" - "errors" - iotclient "github.com/arduino/iot-client-go" "github.com/arduino/iot-cloud-cli/internal/config" "github.com/arduino/iot-cloud-cli/internal/iot" @@ -15,22 +14,14 @@ import ( // CreateParams contains the parameters needed to create a new thing. type CreateParams struct { - // Mandatory - contains the name of the thing + // Optional - contains the name of the thing Name string - // Optional - contains the ID of the device to be bound to the thing - DeviceID string - // Mandatory if device is empty - contains the path of the template file + // Mandatory - contains the path of the template file Template string - // Mandatory if template is empty- name of things to be cloned - CloneID string } // Create allows to create a new thing func Create(params *CreateParams) (string, error) { - if params.Template == "" && params.CloneID == "" { - return "", fmt.Errorf("%s", "provide either a thing(ID) to clone (--clone) or a thing template file (--template)\n") - } - conf, err := config.Retrieve() if err != nil { return "", err @@ -40,29 +31,21 @@ func Create(params *CreateParams) (string, error) { return "", err } - var thing *iotclient.Thing - - if params.CloneID != "" { - thing, err = clone(iotClient, params.CloneID) - if err != nil { - return "", err - } - - } else if params.Template != "" { - thing, err = loadTemplate(params.Template) - if err != nil { - return "", err - } + thing, err := loadTemplate(params.Template) + if err != nil { + return "", err + } - } else { - return "", errors.New("provide either a thing(ID) to clone (--clone) or a thing template file (--template)") + // Name passed as parameter has priority over name from template + if params.Name != "" { + thing.Name = params.Name + } + // If name is not specified in the template, it should be passed as parameter + if thing.Name == "" { + return "", errors.New("thing name not specified") } - thing.Name = params.Name force := true - if params.DeviceID != "" { - thing.DeviceId = params.DeviceID - } thingID, err := iotClient.AddThing(thing, force) if err != nil { return "", err @@ -71,38 +54,6 @@ func Create(params *CreateParams) (string, error) { return thingID, nil } -func clone(client iot.Client, thingID string) (*iotclient.Thing, error) { - clone, err := client.GetThing(thingID) - if err != nil { - return nil, fmt.Errorf("%s: %w", "retrieving the thing to be cloned", err) - } - - thing := &iotclient.Thing{} - - // Copy device id - if clone.DeviceId != "" { - thing.DeviceId = clone.DeviceId - } - - // Copy properties - for _, p := range clone.Properties { - thing.Properties = append(thing.Properties, iotclient.Property{ - Name: p.Name, - MinValue: p.MinValue, - MaxValue: p.MaxValue, - Permission: p.Permission, - UpdateParameter: p.UpdateParameter, - UpdateStrategy: p.UpdateStrategy, - Type: p.Type, - VariableName: p.VariableName, - Persist: p.Persist, - Tag: p.Tag, - }) - } - - return thing, nil -} - func loadTemplate(file string) (*iotclient.Thing, error) { templateFile, err := os.Open(file) if err != nil { @@ -115,11 +66,29 @@ func loadTemplate(file string) (*iotclient.Thing, error) { return nil, err } - thing := &iotclient.Thing{} - err = json.Unmarshal([]byte(templateBytes), thing) + template := make(map[string]interface{}) + err = json.Unmarshal([]byte(templateBytes), &template) if err != nil { return nil, fmt.Errorf("%s: %w", "reading template file: template not valid: ", err) } + // Adapt thing template to thing structure + delete(template, "id") + template["properties"] = template["variables"] + delete(template, "variables") + + // Convert template into thing structure exploiting json marshalling/unmarshalling + thing := &iotclient.Thing{} + + t, err := json.Marshal(template) + if err != nil { + return nil, fmt.Errorf("%s: %w", "extracting template", err) + } + + err = json.Unmarshal(t, &thing) + if err != nil { + return nil, fmt.Errorf("%s: %w", "creating thing structure from template", err) + } + return thing, nil } diff --git a/command/thing/extract.go b/command/thing/extract.go index 2cefa446..f625c211 100644 --- a/command/thing/extract.go +++ b/command/thing/extract.go @@ -57,24 +57,20 @@ func Extract(params *ExtractParams) error { func templateFromThing(thing *iotclient.ArduinoThing) ([]byte, error) { template := make(map[string]interface{}) - template["properties_count"] = thing.PropertiesCount + template["name"] = thing.Name var props []map[string]interface{} for _, p := range thing.Properties { prop := make(map[string]interface{}) - prop["max_value"] = p.MaxValue - prop["min_value"] = p.MinValue prop["name"] = p.Name prop["permission"] = p.Permission - prop["persist"] = p.Persist - prop["tag"] = p.Tag prop["type"] = p.Type prop["update_parameter"] = p.UpdateParameter prop["update_strategy"] = p.UpdateStrategy prop["variable_name"] = p.VariableName props = append(props, prop) } - template["properties"] = props + template["variables"] = props // Extract json template from thing structure file, err := json.MarshalIndent(template, "", " ") diff --git a/command/thing/list.go b/command/thing/list.go index caa21286..3c44bd16 100644 --- a/command/thing/list.go +++ b/command/thing/list.go @@ -8,21 +8,21 @@ import ( // ThingInfo contains the main parameters of // an Arduino IoT Cloud thing. type ThingInfo struct { - Name string - ID string - DeviceID string - Properties []string + Name string + ID string + DeviceID string + Variables []string } // ListParams contains the optional parameters needed // to filter the things to be listed. // If IDs is valid, only things belonging to that list are listed. // If DeviceID is provided, only things associated to that device are listed. -// If Properties is true, properties names are retrieved. +// If Variables is true, variables names are retrieved. type ListParams struct { - IDs []string - DeviceID *string - Properties bool + IDs []string + DeviceID *string + Variables bool } // List command is used to list @@ -37,22 +37,22 @@ func List(params *ListParams) ([]ThingInfo, error) { return nil, err } - foundThings, err := iotClient.ListThings(params.IDs, params.DeviceID, params.Properties) + foundThings, err := iotClient.ListThings(params.IDs, params.DeviceID, params.Variables) if err != nil { return nil, err } var things []ThingInfo for _, foundThing := range foundThings { - var props []string + var vars []string for _, p := range foundThing.Properties { - props = append(props, p.Name) + vars = append(vars, p.Name) } th := ThingInfo{ - Name: foundThing.Name, - ID: foundThing.Id, - DeviceID: foundThing.DeviceId, - Properties: props, + Name: foundThing.Name, + ID: foundThing.Id, + DeviceID: foundThing.DeviceId, + Variables: vars, } things = append(things, th) }