github.com/arduino/arduino-cloud-cli@v0.0.0-20240517070944-e7a449561083/internal/iot/client.go (about)

     1  // This file is part of arduino-cloud-cli.
     2  //
     3  // Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/)
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published
     7  // by the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <https://www.gnu.org/licenses/>.
    17  
    18  package iot
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"os"
    24  
    25  	"github.com/antihax/optional"
    26  	"github.com/arduino/arduino-cloud-cli/config"
    27  	iotclient "github.com/arduino/iot-client-go"
    28  	"golang.org/x/oauth2"
    29  )
    30  
    31  var ErrOtaAlreadyInProgress = fmt.Errorf("ota already in progress")
    32  
    33  // Client can perform actions on Arduino IoT Cloud.
    34  type Client struct {
    35  	api   *iotclient.APIClient
    36  	token oauth2.TokenSource
    37  }
    38  
    39  // NewClient returns a new client implementing the Client interface.
    40  // It needs client Credentials for cloud authentication.
    41  func NewClient(cred *config.Credentials) (*Client, error) {
    42  	cl := &Client{}
    43  	err := cl.setup(cred.Client, cred.Secret, cred.Organization)
    44  	if err != nil {
    45  		err = fmt.Errorf("instantiate new iot client: %w", err)
    46  		return nil, err
    47  	}
    48  	return cl, nil
    49  }
    50  
    51  // DeviceCreate allows to create a new device on Arduino IoT Cloud.
    52  // It returns the newly created device, and an error.
    53  func (cl *Client) DeviceCreate(ctx context.Context, fqbn, name, serial, dType string, cType *string) (*iotclient.ArduinoDevicev2, error) {
    54  	ctx, err := ctxWithToken(ctx, cl.token)
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	payload := iotclient.CreateDevicesV2Payload{
    60  		Fqbn:   fqbn,
    61  		Name:   name,
    62  		Serial: serial,
    63  		Type:   dType,
    64  	}
    65  
    66  	if cType != nil {
    67  		payload.ConnectionType = *cType
    68  	}
    69  
    70  	dev, _, err := cl.api.DevicesV2Api.DevicesV2Create(ctx, payload, nil)
    71  	if err != nil {
    72  		err = fmt.Errorf("creating device, %w", errorDetail(err))
    73  		return nil, err
    74  	}
    75  	return &dev, nil
    76  }
    77  
    78  // DeviceLoraCreate allows to create a new LoRa device on Arduino IoT Cloud.
    79  // It returns the LoRa information about the newly created device, and an error.
    80  func (cl *Client) DeviceLoraCreate(ctx context.Context, name, serial, devType, eui, freq string) (*iotclient.ArduinoLoradevicev1, error) {
    81  	ctx, err := ctxWithToken(ctx, cl.token)
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  
    86  	payload := iotclient.CreateLoraDevicesV1Payload{
    87  		App:           "defaultApp",
    88  		Eui:           eui,
    89  		FrequencyPlan: freq,
    90  		Name:          name,
    91  		Serial:        serial,
    92  		Type:          devType,
    93  		UserId:        "me",
    94  	}
    95  
    96  	dev, _, err := cl.api.LoraDevicesV1Api.LoraDevicesV1Create(ctx, payload, nil)
    97  	if err != nil {
    98  		err = fmt.Errorf("creating lora device: %w", errorDetail(err))
    99  		return nil, err
   100  	}
   101  	return &dev, nil
   102  }
   103  
   104  // DevicePassSet sets the device password to the one suggested by Arduino IoT Cloud.
   105  // Returns the set password.
   106  func (cl *Client) DevicePassSet(ctx context.Context, id string) (*iotclient.ArduinoDevicev2Pass, error) {
   107  	ctx, err := ctxWithToken(ctx, cl.token)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	// Fetch suggested password
   113  	opts := &iotclient.DevicesV2PassGetOpts{SuggestedPassword: optional.NewBool(true)}
   114  	pass, _, err := cl.api.DevicesV2PassApi.DevicesV2PassGet(ctx, id, opts)
   115  	if err != nil {
   116  		err = fmt.Errorf("fetching device suggested password: %w", errorDetail(err))
   117  		return nil, err
   118  	}
   119  
   120  	// Set password to the suggested one
   121  	p := iotclient.Devicev2Pass{Password: pass.SuggestedPassword}
   122  	pass, _, err = cl.api.DevicesV2PassApi.DevicesV2PassSet(ctx, id, p)
   123  	if err != nil {
   124  		err = fmt.Errorf("setting device password: %w", errorDetail(err))
   125  		return nil, err
   126  	}
   127  	return &pass, nil
   128  }
   129  
   130  // DeviceDelete deletes the device corresponding to the passed ID
   131  // from Arduino IoT Cloud.
   132  func (cl *Client) DeviceDelete(ctx context.Context, id string) error {
   133  	ctx, err := ctxWithToken(ctx, cl.token)
   134  	if err != nil {
   135  		return err
   136  	}
   137  
   138  	_, err = cl.api.DevicesV2Api.DevicesV2Delete(ctx, id, nil)
   139  	if err != nil {
   140  		err = fmt.Errorf("deleting device: %w", errorDetail(err))
   141  		return err
   142  	}
   143  	return nil
   144  }
   145  
   146  // DeviceList retrieves and returns a list of all Arduino IoT Cloud devices
   147  // belonging to the user performing the request.
   148  func (cl *Client) DeviceList(ctx context.Context, tags map[string]string) ([]iotclient.ArduinoDevicev2, error) {
   149  	ctx, err := ctxWithToken(ctx, cl.token)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	opts := &iotclient.DevicesV2ListOpts{}
   155  	if tags != nil {
   156  		t := make([]string, 0, len(tags))
   157  		for key, val := range tags {
   158  			// Use the 'key:value' format required from the backend
   159  			t = append(t, key+":"+val)
   160  		}
   161  		opts.Tags = optional.NewInterface(t)
   162  	}
   163  
   164  	devices, _, err := cl.api.DevicesV2Api.DevicesV2List(ctx, opts)
   165  	if err != nil {
   166  		err = fmt.Errorf("listing devices: %w", errorDetail(err))
   167  		return nil, err
   168  	}
   169  	return devices, nil
   170  }
   171  
   172  // DeviceShow allows to retrieve a specific device, given its id,
   173  // from Arduino IoT Cloud.
   174  func (cl *Client) DeviceShow(ctx context.Context, id string) (*iotclient.ArduinoDevicev2, error) {
   175  	ctx, err := ctxWithToken(ctx, cl.token)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	dev, _, err := cl.api.DevicesV2Api.DevicesV2Show(ctx, id, nil)
   181  	if err != nil {
   182  		err = fmt.Errorf("retrieving device, %w", errorDetail(err))
   183  		return nil, err
   184  	}
   185  	return &dev, nil
   186  }
   187  
   188  // DeviceOTA performs an OTA upload request to Arduino IoT Cloud, passing
   189  // the ID of the device to be updated and the actual file containing the OTA firmware.
   190  func (cl *Client) DeviceOTA(ctx context.Context, id string, file *os.File, expireMins int) error {
   191  	ctx, err := ctxWithToken(ctx, cl.token)
   192  	if err != nil {
   193  		return err
   194  	}
   195  
   196  	opt := &iotclient.DevicesV2OtaUploadOpts{
   197  		ExpireInMins: optional.NewInt32(int32(expireMins)),
   198  		Async:        optional.NewBool(true),
   199  	}
   200  	resp, err := cl.api.DevicesV2OtaApi.DevicesV2OtaUpload(ctx, id, file, opt)
   201  	if err != nil {
   202  		// 409 (Conflict) is the status code for an already existing OTA in progress for the same device. Handling it in a different way.
   203  		if resp.StatusCode == 409 {
   204  			return ErrOtaAlreadyInProgress
   205  		}
   206  		return fmt.Errorf("uploading device ota: %w", errorDetail(err))
   207  	}
   208  	return nil
   209  }
   210  
   211  // DeviceTagsCreate allows to create or overwrite tags on a device of Arduino IoT Cloud.
   212  func (cl *Client) DeviceTagsCreate(ctx context.Context, id string, tags map[string]string) error {
   213  	ctx, err := ctxWithToken(ctx, cl.token)
   214  	if err != nil {
   215  		return err
   216  	}
   217  
   218  	for key, val := range tags {
   219  		t := iotclient.Tag{Key: key, Value: val}
   220  		_, err := cl.api.DevicesV2TagsApi.DevicesV2TagsUpsert(ctx, id, t)
   221  		if err != nil {
   222  			err = fmt.Errorf("cannot create tag %s: %w", key, errorDetail(err))
   223  			return err
   224  		}
   225  	}
   226  	return nil
   227  }
   228  
   229  // DeviceTagsDelete deletes the tags of a device of Arduino IoT Cloud,
   230  // given the device id and the keys of the tags.
   231  func (cl *Client) DeviceTagsDelete(ctx context.Context, id string, keys []string) error {
   232  	ctx, err := ctxWithToken(ctx, cl.token)
   233  	if err != nil {
   234  		return err
   235  	}
   236  
   237  	for _, key := range keys {
   238  		_, err := cl.api.DevicesV2TagsApi.DevicesV2TagsDelete(ctx, id, key)
   239  		if err != nil {
   240  			err = fmt.Errorf("cannot delete tag %s: %w", key, errorDetail(err))
   241  			return err
   242  		}
   243  	}
   244  	return nil
   245  }
   246  
   247  // LoraFrequencyPlansList retrieves and returns the list of all supported
   248  // LoRa frequency plans.
   249  func (cl *Client) LoraFrequencyPlansList(ctx context.Context) ([]iotclient.ArduinoLorafreqplanv1, error) {
   250  	ctx, err := ctxWithToken(ctx, cl.token)
   251  	if err != nil {
   252  		return nil, err
   253  	}
   254  
   255  	freqs, _, err := cl.api.LoraFreqPlanV1Api.LoraFreqPlanV1List(ctx)
   256  	if err != nil {
   257  		err = fmt.Errorf("listing lora frequency plans: %w", errorDetail(err))
   258  		return nil, err
   259  	}
   260  	return freqs.FrequencyPlans, nil
   261  }
   262  
   263  // CertificateCreate allows to upload a certificate on Arduino IoT Cloud.
   264  // It returns the certificate parameters populated by the cloud.
   265  func (cl *Client) CertificateCreate(ctx context.Context, id, csr string) (*iotclient.ArduinoCompressedv2, error) {
   266  	ctx, err := ctxWithToken(ctx, cl.token)
   267  	if err != nil {
   268  		return nil, err
   269  	}
   270  
   271  	cert := iotclient.CreateDevicesV2CertsPayload{
   272  		Ca:      "Arduino",
   273  		Csr:     csr,
   274  		Enabled: true,
   275  	}
   276  
   277  	newCert, _, err := cl.api.DevicesV2CertsApi.DevicesV2CertsCreate(ctx, id, cert)
   278  	if err != nil {
   279  		err = fmt.Errorf("creating certificate, %w", errorDetail(err))
   280  		return nil, err
   281  	}
   282  
   283  	return &newCert.Compressed, nil
   284  }
   285  
   286  // ThingCreate adds a new thing on Arduino IoT Cloud.
   287  func (cl *Client) ThingCreate(ctx context.Context, thing *iotclient.ThingCreate, force bool) (*iotclient.ArduinoThing, error) {
   288  	ctx, err := ctxWithToken(ctx, cl.token)
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  
   293  	opt := &iotclient.ThingsV2CreateOpts{Force: optional.NewBool(force)}
   294  	newThing, _, err := cl.api.ThingsV2Api.ThingsV2Create(ctx, *thing, opt)
   295  	if err != nil {
   296  		return nil, fmt.Errorf("%s: %w", "adding new thing", errorDetail(err))
   297  	}
   298  	return &newThing, nil
   299  }
   300  
   301  // ThingUpdate updates a thing on Arduino IoT Cloud.
   302  func (cl *Client) ThingUpdate(ctx context.Context, id string, thing *iotclient.ThingUpdate, force bool) error {
   303  	ctx, err := ctxWithToken(ctx, cl.token)
   304  	if err != nil {
   305  		return err
   306  	}
   307  
   308  	opt := &iotclient.ThingsV2UpdateOpts{Force: optional.NewBool(force)}
   309  	_, _, err = cl.api.ThingsV2Api.ThingsV2Update(ctx, id, *thing, opt)
   310  	if err != nil {
   311  		return fmt.Errorf("%s: %v", "updating thing", errorDetail(err))
   312  	}
   313  	return nil
   314  }
   315  
   316  // ThingDelete deletes a thing from Arduino IoT Cloud.
   317  func (cl *Client) ThingDelete(ctx context.Context, id string) error {
   318  	ctx, err := ctxWithToken(ctx, cl.token)
   319  	if err != nil {
   320  		return err
   321  	}
   322  
   323  	_, err = cl.api.ThingsV2Api.ThingsV2Delete(ctx, id, nil)
   324  	if err != nil {
   325  		err = fmt.Errorf("deleting thing: %w", errorDetail(err))
   326  		return err
   327  	}
   328  	return nil
   329  }
   330  
   331  // ThingShow allows to retrieve a specific thing, given its id,
   332  // from Arduino IoT Cloud.
   333  func (cl *Client) ThingShow(ctx context.Context, id string) (*iotclient.ArduinoThing, error) {
   334  	ctx, err := ctxWithToken(ctx, cl.token)
   335  	if err != nil {
   336  		return nil, err
   337  	}
   338  
   339  	thing, _, err := cl.api.ThingsV2Api.ThingsV2Show(ctx, id, nil)
   340  	if err != nil {
   341  		err = fmt.Errorf("retrieving thing, %w", errorDetail(err))
   342  		return nil, err
   343  	}
   344  	return &thing, nil
   345  }
   346  
   347  // ThingList returns a list of things on Arduino IoT Cloud.
   348  func (cl *Client) ThingList(ctx context.Context, ids []string, device *string, props bool, tags map[string]string) ([]iotclient.ArduinoThing, error) {
   349  	ctx, err := ctxWithToken(ctx, cl.token)
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  
   354  	opts := &iotclient.ThingsV2ListOpts{}
   355  	opts.ShowProperties = optional.NewBool(props)
   356  
   357  	if ids != nil {
   358  		opts.Ids = optional.NewInterface(ids)
   359  	}
   360  
   361  	if device != nil {
   362  		opts.DeviceId = optional.NewString(*device)
   363  	}
   364  
   365  	if tags != nil {
   366  		t := make([]string, 0, len(tags))
   367  		for key, val := range tags {
   368  			// Use the 'key:value' format required from the backend
   369  			t = append(t, key+":"+val)
   370  		}
   371  		opts.Tags = optional.NewInterface(t)
   372  	}
   373  
   374  	things, _, err := cl.api.ThingsV2Api.ThingsV2List(ctx, opts)
   375  	if err != nil {
   376  		err = fmt.Errorf("retrieving things, %w", errorDetail(err))
   377  		return nil, err
   378  	}
   379  	return things, nil
   380  }
   381  
   382  // ThingTagsCreate allows to create or overwrite tags on a thing of Arduino IoT Cloud.
   383  func (cl *Client) ThingTagsCreate(ctx context.Context, id string, tags map[string]string) error {
   384  	ctx, err := ctxWithToken(ctx, cl.token)
   385  	if err != nil {
   386  		return err
   387  	}
   388  
   389  	for key, val := range tags {
   390  		t := iotclient.Tag{Key: key, Value: val}
   391  		_, err := cl.api.ThingsV2TagsApi.ThingsV2TagsUpsert(ctx, id, t)
   392  		if err != nil {
   393  			err = fmt.Errorf("cannot create tag %s: %w", key, errorDetail(err))
   394  			return err
   395  		}
   396  	}
   397  	return nil
   398  }
   399  
   400  // ThingTagsDelete deletes the tags of a thing of Arduino IoT Cloud,
   401  // given the thing id and the keys of the tags.
   402  func (cl *Client) ThingTagsDelete(ctx context.Context, id string, keys []string) error {
   403  	ctx, err := ctxWithToken(ctx, cl.token)
   404  	if err != nil {
   405  		return err
   406  	}
   407  
   408  	for _, key := range keys {
   409  		_, err := cl.api.ThingsV2TagsApi.ThingsV2TagsDelete(ctx, id, key)
   410  		if err != nil {
   411  			err = fmt.Errorf("cannot delete tag %s: %w", key, errorDetail(err))
   412  			return err
   413  		}
   414  	}
   415  	return nil
   416  }
   417  
   418  // DashboardCreate adds a new dashboard on Arduino IoT Cloud.
   419  func (cl *Client) DashboardCreate(ctx context.Context, dashboard *iotclient.Dashboardv2) (*iotclient.ArduinoDashboardv2, error) {
   420  	ctx, err := ctxWithToken(ctx, cl.token)
   421  	if err != nil {
   422  		return nil, err
   423  	}
   424  
   425  	newDashboard, _, err := cl.api.DashboardsV2Api.DashboardsV2Create(ctx, *dashboard, nil)
   426  	if err != nil {
   427  		return nil, fmt.Errorf("%s: %w", "adding new dashboard", errorDetail(err))
   428  	}
   429  	return &newDashboard, nil
   430  }
   431  
   432  // DashboardShow allows to retrieve a specific dashboard, given its id,
   433  // from Arduino IoT Cloud.
   434  func (cl *Client) DashboardShow(ctx context.Context, id string) (*iotclient.ArduinoDashboardv2, error) {
   435  	ctx, err := ctxWithToken(ctx, cl.token)
   436  	if err != nil {
   437  		return nil, err
   438  	}
   439  
   440  	dashboard, _, err := cl.api.DashboardsV2Api.DashboardsV2Show(ctx, id, nil)
   441  	if err != nil {
   442  		err = fmt.Errorf("retrieving dashboard, %w", errorDetail(err))
   443  		return nil, err
   444  	}
   445  	return &dashboard, nil
   446  }
   447  
   448  // DashboardList returns a list of dashboards on Arduino IoT Cloud.
   449  func (cl *Client) DashboardList(ctx context.Context) ([]iotclient.ArduinoDashboardv2, error) {
   450  	ctx, err := ctxWithToken(ctx, cl.token)
   451  	if err != nil {
   452  		return nil, err
   453  	}
   454  
   455  	dashboards, _, err := cl.api.DashboardsV2Api.DashboardsV2List(ctx, nil)
   456  	if err != nil {
   457  		err = fmt.Errorf("listing dashboards: %w", errorDetail(err))
   458  		return nil, err
   459  	}
   460  	return dashboards, nil
   461  }
   462  
   463  // DashboardDelete deletes a dashboard from Arduino IoT Cloud.
   464  func (cl *Client) DashboardDelete(ctx context.Context, id string) error {
   465  	ctx, err := ctxWithToken(ctx, cl.token)
   466  	if err != nil {
   467  		return err
   468  	}
   469  
   470  	_, err = cl.api.DashboardsV2Api.DashboardsV2Delete(ctx, id, nil)
   471  	if err != nil {
   472  		err = fmt.Errorf("deleting dashboard: %w", errorDetail(err))
   473  		return err
   474  	}
   475  	return nil
   476  }
   477  
   478  func (cl *Client) setup(client, secret, organization string) error {
   479  	baseURL := GetArduinoAPIBaseURL()
   480  
   481  	// Configure a token source given the user's credentials.
   482  	cl.token = NewUserTokenSource(client, secret, baseURL)
   483  
   484  	config := iotclient.NewConfiguration()
   485  	if organization != "" {
   486  		config.DefaultHeader = map[string]string{"X-Organization": organization}
   487  	}
   488  	config.BasePath = baseURL + "/iot"
   489  	cl.api = iotclient.NewAPIClient(config)
   490  
   491  	return nil
   492  }