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 }