# ThinCloud Node SDK

The ThinCloud Node SDK allows developers to write Node applications that utilizes the ThinCloud MQTT API.

- [ThinCloud Node SDK](#thincloud-node-sdk)
  - [Installation](#installation)
  - [Initialization](#initialization)
    - [Usage](#usage)
    - [Configuration](#configuration)
      - [MQTT Authentication](#mqtt-authentication)
      - [Commissioned Device](#commissioned-device)
      - [Optional Fields](#optional-fields)
    - [Connecting](#connecting)
    - [Disconnecting](#disconnecting)
  - [Core Functionality](#core-functionality)
    - [Events](#events)
    - [RPC Requests](#rpc-requests)
      - [Commission](#commission)
      - [Decommission](#decommission)
      - [Get Invitation Code](#get-invitation-code)
      - [Product Information](#product-information)
    - [State](#state)
      - [Get State](#get-state)
      - [Set State](#set-state)
    - [Child Devices](#child-devices)
      - [Create And Pair New Child Device](#create-and-pair-new-child-device)
      - [Pair Existing Child Device](#pair-existing-child-device)
      - [List Child Devices](#list-child-devices)
      - [Remove Child Device](#remove-child-device)
    - [Download Firmware](#download-firmware)


## Installation

```sh
npm install @yonomi/thincloud-v2-node-sdk
```

## Initialization
 
### Usage

To properly authenticate and connect, either pass the configuration into the client's constructor

```js
const { Client } = require('@yonomi/thincloud-v2-node-sdk')

// Passing in configuration into client constructor
const client = new Client({
    host: IOT_HOST,
    ca: CA_FILE,
    key: KEY,
    cert: CERT,
    clientId: CLIENT_ID,
    deviceId: DEVICE_ID,
    isCommissioned: !!DEVICE_ID,
})

// Then call the connect method
await client.connect()
```

or initialize with an empty constructor and pass the configuration into the connect method.

**Note**: The configuration passed into the connect method will override the one passed into the client's constructor.

```js
const { Client } = require('@yonomi/thincloud-v2-node-sdk')

// Initialize with empty constructor
const client = new Client()

// Then call the connect method with configuration
await client.connect({
    host: IOT_HOST,
    ca: CA_FILE,
    key: KEY,
    cert: CERT,
    clientId: CLIENT_ID,
    deviceId: DEVICE_ID,
    isCommissioned: !!DEVICE_ID,
})
```


 
### Configuration

In order to use the SDK's core functionality, pass a config into the client constructor or to the connect method. The fields that can be passed in are as follows:
	
   * host
   * ca
   * key
   * cert
   * clientId
   * deviceId
   * isCommissioned
   * productId 
   * rpcRequestTimeout
   * debugLevel

#### MQTT Authentication

These fields are all required and are used to connect and authenticate to the MQTT API. Follow the [MQTT Authentication docs](https://github.com/Yonomi/thincloud/blob/master/docs/devices/devices.md#mqtt-authentication) to generate these.

* **host** is the URL of the MQTT API endpoint
* **ca** is the [AWS Root CA](https://www.amazontrust.com/repository/AmazonRootCA1.pem)
* **key** is the device key
* **cert** is the device certificate
* **clientId** is the client ID generated from the public key's fingerprint

#### Commissioned Device

After a device has been commissioned, if the client disconnects and must be initialized, these fields are required to use the already-commissioned device.

* **deviceId** 

	While this is optional for authentication purposes, it is a *required* field if persisting the device ID for all requests is required. After commissioning a device, the device ID can be stored locally and passed in here if the client needs to reconnect.
    
* **isCommissioned** 

	If the device has been commissioned, pass in **true** when initializing the client.

#### Optional Fields

* **productId**

	While this field is optional, in order to commission a device, either pass in a product ID here or in the commission method.
    
* **rpcRequestTimeout**

	Timeout in milliseconds for RPC requests (commission, decommission, and getInvitationCode) made from the device to the cloud. By default, this is set to *4500 milliseconds*.
    
* **debugLevel**

	Level of logs for debugging. Possible values to set this to are: `fatal`, `error`, `warn`, `info`, `debug`, and `trace`. By default, this is set to `fatal`. 
    
### Connecting

To connect to the ThinCloud MQTT API, use the initialized client to call `client.connect()`.

**`client.isConnected`** will be `true` on successful connection.

**client.connect([config])**

* `config` is an optional configuration argument. If the client configuration is passed in here, it will override the one passed into the client's constructor.
  
```js
// Assume client has been initialized with configuration
    
// Connect to MQTT API
await client.connect()
    
if(client.isConnected) {
	// Use SDK core functionality
}	
```

### Disconnecting

To disconnect from the Thincloud MQTT API, call `client.disconnect()`. This does not decommission the device.

**`client.isConnected`** will be `false` after successful disconnection.

**client.disconnect()**

```js
await client.disconnect()

if(!client.isConnected) {
	console.log('Client is disconnected')
}
```

## Core Functionality

Once the client is authenticated and connected to the MQTT API, the client can use the core functionality to listen to events and make requests.

### Events

Using `client.on()` and `client.off()`, attach and detach listeners on events. Use exported `Event` constants.

```js
const { Event } = require('@yonomi/thincloud-v2-node-sdk')

// Assume client has been initialized and connected

client.on(Event.OFFLINE, () => {
	console.log('Device went offline')
})

client.off(Event.OFFLINE, () => {
	console.log('Removed event listener for OFFLINE event')
})
```
* **`Event.CONNECT`**

	`function (connack) {}`
    
    Emitted on successful (re)connection (i.e. connack rc=0).

* **`Event.RECONNECT`**

	`function () {}`
    
	Emitted when a reconnect starts.
	
* **`Event.CLOSE`**

	`function () {}`

	Emitted after a disconnection.
    
* **`Event.DISCONNECT`**

	`function (packet) {}`

	Emitted after receiving disconnect packet from MQTT broker.
    
* **`Event.OFFLINE`**

	`function () {}`

	Emitted when the client goes offline.
    
* **`Event.ERROR`**

	`function (error) {}`

	Emitted when a message parsing error occurs.
    
* **`Event.END`**

	`function (error) {}`

	Emitted when `client.disconnect()` is called.

* **`Event.COMMAND`**

	`function (command) {}`
    
    Emitted when a command is sent from the cloud. See [Send Command](https://github.com/Yonomi/thincloud/blob/master/docs/devices/devices.md#send-command). Use `command.success()` or `command.error()` to send back a response.
    
    * `command.name` is the name of the command.
    * `command.payload` is the command payload. This is a String and may need to be deserialized.
    * `command.childDeviceId` is the child device ID. If this command is for the gateway itself, this will be `undefined`. 
    * `command.success([payload])` send back a success response with an optional payload. The payload can be anything.
    * `command.error([payload])` send back an error response with an optional payload. The payload can be anything.

  If this request is sent from the cloud

  ```gql
  mutation {
    sendCommand(input: {
      deviceId: "559307a8-24af-49ff-bfe0-ff4ffcab14ad"
      name: "ping"
      payload: "ping"
    }) {
      requestId
    }
  }
  ```
  
  The device can listen on the command event and respond appropriately
  
  ```js
  client.on(Event.COMMAND, (command) => {
    console.log(command.name) // 'ping'
    
    const payload = JSON.parse(command.payload)
    console.log(payload) // { command: 'ping' }
    
    if(command.name === 'ping') {
    	command.success({ echo: 'pong' })
    } else {
    	command.error({ error: 'Received unsupported command'})
    }
  })
  ```

* **`Event.STATE`**
	
    `function (state) {}`
    
	Emitted when state is sent from the cloud. See [Send State](https://github.com/Yonomi/thincloud/blob/master/docs/devices/devices.md#send-state). To persist state changes to the cloud, send a response using `state.confirm()`
    
    * `state.params` contains contents of the state request
    * `state.childDeviceId` is the child device ID. This will be `undefined` is the state event is meant for the gateway itself.
    * `state.confirm([payload])`
    	
    	Send a response to persist state changes. If `state.confirm()` is called with no payload, the SDK will send the contents of `state.params` by default. Any attributes that are part of the device type definition will not be changed.
        
  If this request is sent from the cloud

  ```gql
  mutation {
    sendState(input: {
      id: "559307a8-24af-49ff-bfe0-ff4ffcab14ad"
      type: "Light"
      on: true
    }) {
      requestId
    }
  }
  ```
  
  The device can listen on the state event and respond appropriately.
  
  ```js
  client.on(Event.STATE, (state) => {
    console.log(state.params) // { id: '559307a8-24af-49ff-bfe0-ff4ffcab14ad', on: true }
    /* < Code to change actual device state > */
    state.confirm()
  })
  ```
  
  A use case where passing in a payload to `state.confirm` makes sense would be if this request is sent from the cloud
  
  ```gql
  mutation {
    sendState(input: {
      id: "559307a8-24af-49ff-bfe0-ff4ffcab14ad"
      type: "Light"
      rainbowMode: true
    }) {
      requestId
    }
  }
  ```
  
  If the device is off, then it ignores a state change request to turn on rainbowMode. In that case, the device would respond with
  
   ```js
  client.on(Event.STATE, (state) => {
    console.log(state.params) // { id: '559307a8-24af-49ff-bfe0-ff4ffcab14ad', rainbowMode: true }
    /* < Code that checks device state > */
    state.confirm({ on: false, rainbowMode: false })
  })
  ```

* **`Event.PRODUCT`**
  
  `function (product) {}`
  
  Emitted when product updates are sent from the cloud.
  
  * `product.params` contains content of the product update

  If this request is sent from the cloud from the Admin GraphQL API

  ```gql
  mutation {
    updateProduct(
      id: "eb7c2710-7c55-4a9e-9c8e-059b2b53ef47",
      tenantId: "af65595d-7b80-4051-9bdd-caf19e1a2aec",
      updatePayload: {
        manufacturer: 'Yonomi',
      }
    ) {
      id
      manufacturer
    }
  }
  ```
  
  The device can listen on the product event and handle appropriately. A response is not needed.
  
  ```js
  client.on(Event.PRODUCT, (product) => {
    console.log(product.params) // {  manufacturer: 'Yonomi' }
    /* < Code to handle the device updating its product information > */
  })
  ```

* **`Event.FIRMWARE_UPDATE`**

  `function (firmwareUpdate){}`

  Emitted when firmware releases are sent from the cloud.

  * `firmwareUpdate.params` contains content of the firmware update

  The device can listen on the firmwareUpdate event and handle appropriately. A response is not needed.

  ```js
  client.on(Event.FIRMWARE_UPDATE, (firmwareUpdate) => {
    const { firmwareVersion, firmwareUrl } = firmwareUpdate.params
    console.log(`A release, ${firmwareVersion}, is now available at ${firmwareUrl}`)
  })
  ```


### RPC Requests

All RPC requests are made using the `rpcRequestTimeout` specified in the client configuration. By default, this is *4500 milliseconds*. Each individual RPC request can take an optional timeout argument in milliseconds that will override `rpcRequestTimeout` for that request.



#### Commission

Commission a device. To receive commands and state requests, a device must be commissioned. The response contains the device ID.

**`client.isCommissioned`** will be set to `true` and **`client.deviceId`** will be set to the device ID returned in the response.

```js
 const { deviceId, productId } = await client.commission({ productId: '16e424b0-ca99-40d2-af5d-f22c8664bc33' }, 3000)
```

**client.commission([params], [timeout])**

* `params` is optional but is required if `productId` is not set in the client configuration
	* `params.productId` is required.
	* `params.force` is optional and when set to `true` will force commissioning
* `timeout` is optional.

**Response**

```js
{
    deviceId: '559307a8-24af-49ff-bfe0-ff4ffcab14ad',
    productId: '16e424b0-ca99-40d2-af5d-f22c8664bc33'
}
```

* `deviceId` is the device ID. This is used to communicate between device and cloud.
* `productId` is the device's productId. It should be the same as the one used to commission the device.

#### Decommission


Decommissions a device. The device ID will no longer be valid for commands and state requests.

```js
await client.decommission(3000)

if(!client.deviceId && !client.isCommissioned) {
    console.log('Device is decommissioned')
}
```

**client.decommission([timeout])**

* `timeout` is optional

**Response**

An empty Object
```js
{}
```

#### Get Invitation Code

Get device invitation code, a JWT. This is used for user-device association in the cloud. See [Associate to a Device](https://github.com/Yonomi/thincloud/blob/master/docs/devices/devices.md#associate-to-a-device).

```js
const { AccessLevel } = require('@yonomi/thincloud-v2-node-sdk')
const { ADMIN, GUEST, OWNER } = AccessLevel

const { invitationCode } = await client.getInvitationCode({
    userId: '248b8e07-211a-4937-829e-80549e315168',
    accessLevel: GUEST,
}, 3000)
```

**client.getInvitationCode(params, [timeout])**

* `params` is required
	* `params.userId` is the user ID of the user in the user-device association
	* `params.accessLevel` is the access level granted to the user for this device. `AccessLevel` is exported as a constant from the SDK. `AccessLevel.ADMIN`, `AccessLevel.GUEST`, and `AccessLevel.OWNER` are valid values.
	* `params.expiration` is optional, defaults to 3600 ms, and must be between 3600 ms and 18000 ms. It is the number of milliseconds before the generated invitation code expires.

* `timeout` is optional

**Response**

```js
{ invitationCode: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzY290Y2guaW8iLCJleHAiOjEzMDA4MTkzODAsIm5hbWUiOiJDaHJpcyBTZXZpbGxlamEiLCJhZG1pbiI6dHJ1ZX0.03f329983b86f7d9a9f5fef85305880101d5e302afafa20154d094b229f75773' }
```

#### Product Information

To get a device's product information, use `client.getProductInfo()`.


```js
 const productInfo = await client.getProductInfo({ productId: '16e424b0-ca99-40d2-af5d-f22c8664bc33' }, 3000)
```

**client.getProductInfo([params], [timeout])**

* `params` 
	* `params.productId` is optional if the request is for the gateway device and has been passed into the client config. If this request is being made for a child device or the product ID for gateway has not been passed into the client config, it is required.
* `timeout` is optional.

**Response**

```js
{ 
  deviceType: 'Lock',
  name: 'LockProduct',
  manufacturer: 'Yonomi',
  brand: 'Yonomi',
  model: 'latest',
  createdAt: '10-20-2019',
  updatedAt:  '10-21-2019',
  firmwareRelease: [{
      productId: '16e424b0-ca99-40d2-af5d-f22c8664bc33',
      version: '1.0.1',
      status: 'READY',
      labels: ['beta'],
      md5: '<md5 hash>', 
      crc: '<crc32 hash>',     
  }]
  customKey: 'customValue',   
}
```
This response returns all the standard product information for the device including the product's firmware release information.


### State

#### Get State

To get device state from the cloud, use `client.getState()`.

**client.getState([deviceId], [timeout])**

* `deviceId` is optional, but necessary if attempting to get the state of a child device
*  `timeout` is optional

To get the gateway device state

```js
await client.getState()
```

To get a child device's state, pass in the child device id.
```js
const childDeviceId = '3529ea24-dd68-4b8c-9269-325427beb3ec'
await client.getState(childDeviceId)
```

#### Set State

To send device state to the cloud, use `client.setState()`. 

**client.setState(deviceState, [deviceId])**

* `deviceState` is required and should be an Object representing the device state. Any attributes that are part of the device type definition will not be changed.
* `deviceId` is optional, but necessary if attempting to set the state of a child device 

To set the gateway device state

```js
await client.setState({
    on: true,
    rainbowMode: true,
})
```

To set child device state, pass in the child device id.

```js
const childDeviceId = '3529ea24-dd68-4b8c-9269-325427beb3ec'
await client.setState({
    on: true,
    rainbowMode: true,
}, childDeviceId)
```

### Child Devices

#### Create And Pair New Child Device

To create and pair a new child device, use `client.createAndPair`.

```js
await client.createAndPair({
	productId: '16e424b0-ca99-40d2-af5d-f22c8664bc33',
})
```

**client.createAndPair(params, [timeout])**

* `params` is required
	* `params.productId` is required.
* `timeout` is optional

**Response**

```js
{
    deviceId: 'a1736e0f-c16f-4635-8914-a19cb90e3a4e',
    productId: '16e424b0-ca99-40d2-af5d-f22c8664bc33' 
}
```
* `deviceId` is the device ID of the child device
* `productId` is the device's productId. 

#### Pair Existing Child Device

To pair a child device that exists in the cloud already, use `client.pairChild`.

```js
const pairedDevice = await client.pairChild({
    deviceId: '7a744e83-c19b-489c-a708-6037bae39a8f'
})
```

**client.pairChild(params, [timeout])**

* `params` is required
	* `params.deviceId` is required and is the deviceId of the child device to pair
* `timeout` is optional

**Response**

```js
{   
    deviceId: '7a744e83-c19b-489c-a708-6037bae39a8f',
    productId: '16e424b0-ca99-40d2-af5d-f22c8664bc33' 
}
```
* `deviceId` is the device ID of the child device
* `productId` is the device's product ID. 

#### List Child Devices

To list child devices, use `client.listChildren`.

```js
const childDevices = await client.listChildren()
```

**client.listChildren([timeout])**

* `timeout` is optional

**Response**

The response is an array of Objects with ids representing child device IDs.

```js
[
    { id: '7a744e83-c19b-489c-a708-6037bae39a8f' },
    { id: 'a1736e0f-c16f-4635-8914-a19cb90e3a4e' },
    ...
]
```
* `id` is the child device ID

#### Remove Child Device

Removes a child device. The request includes a deviceId and the response is void.

```js
  await client.removeChild({ deviceId: 'a1736e0f-c16f-4635-8914-a19cb90e3a4e' }, 3000)
```

**client.removeChild(params, [timeout])**
* `params` is required
  * `params.deviceId` is required and should be equal to the deviceId of the child that should be removed
* `timeout` is optional

**Response**

The response is `void`

# Download Firmware

Once the device has a provided URL to download its firmware, the initialized SDK can be used to download the firmware image. To request the device's firmware URL, use the `getFirmwareInfo` RPC method.

```js
  const firmwareImage = await client.getFirmware({ url: 'https://...' }, 3000)
```

**client.getFirmware(params, [timeout])**
* `params` is required
  * `params.url` is required and should be the URL where the device firmware is hosted
* `timeout` is optional. By default, it will be 4500 ms.
