Client library in Swift for Disruptive Technologies' REST API

Disruptive - Swift API

Swift library for accessing data from Disruptive Technologies.

Installation

This library is currently only available through the Swift Package Manager (SPM).

To add this Swift Package as a dependency in Xcode:

  1. Go to File -> Swift Packages -> Add Package Dependency...
  2. Enter the following repository URL: https://github.com/vegather/Disruptive
  3. Specify the version of the API you want. The default should be fine
  4. Make sure your app target is selected, and click "Finish"
  5. You can now import the Disruptive Swift API with import Disruptive

If you want to add it manually to your Swift project, you can add the following dependency to your Package.swift:

dependencies: [
    .package(url: "https://github.com/vegather/Disruptive.git", from: "1.0.0")
]

Guides

Overview

To use this Swift library, you start by initializing an instance of the Disruptive struct with the credentials for a Service Account (see the Authentication section below). This will be your entry-point for all the requests to the Disruptive Technologies service. This Disruptive instance will automatically handle things such as authentication, pagination, re-sending of events after rate-limiting, and other recoverable errors.

The endpoints implemented on the Disruptive struct are asynchronous, and will return its results in a closure you provide with an argument of type Result (read more about the Result type on Apple's developer site). This Result will contain the value you requested on .success (Void if no values makes sense), or a DisruptiveError on .failure.

Note: The callback with the Result will always be called on the main queue, even if networking/processing is done in a background queue.

The following sections will provide a brief guide to the most common use-cases of the API. Check out the full Swift API documentation for more.

Authentication

Authentication is done by initializing the Disruptive instance with a type that conforms to the Authenticator protocol. The recommended type for this is OAuth2Authenticator which will authenticate a Service Account using the OAuth2 flow. Once authenticated, the Disruptive instance will make sure it always has a non-expired access token, and will add that to the Authorization header of the request before sending it. If the access token is expired when a request is made, a new access token will be fetched automatically before sending the request.

A service account can be created in DT Studio by clicking the Service Account tab under API Integrations in the side menu. Create a new key for the Service Account and make sure to note down the key id, secret, and email for the Service Account. Note that by default, the Service Account will not have access to any resources. See the section below about Permissions to learn how to grant the Service Account access to your resources.

Here's an example of how to authenticate a service account with the OAuth2 flow:

import Disruptive

let credentials = ServiceAccountCredentials(email: "<EMAIL>", keyID: "<KEY_ID>", secret: "<SECRET>")
let authenticator = OAuth2Authenticator(credentials: credentials)
let disruptive = Disruptive(authenticator: authenticator)

// All methods called on the disruptive instance will be authenticated

OAuth2Authenticator documentation

Permissions

Access levels for the Disruptive API can be described in terms of members, roles, and permissions. For an account (Service Account or user) to have access to a resource, it has to be a member in the project or organization that is a parent of that resource. A member will always have a role for the project/organization it's a member of (such as project user, project admin, etc). Each of those roles as a list of permissions that describes which CRUD (create, read, update, delete) operations it can perform on various resources. Examples of permissions would be "project.read", "membership.create", "serviceaccount.key.delete", etc. To list the available roles and permissions, use the getRoles and getPermissions functions.

In order for a Service Account to be able to access a given resource, it must have sufficient permissions for that resource. By default, a Service Account does not have access to any resources. The easiest way to get started with a Service Account is by granting access for the relevant projects/organizations in DT Studio. You can give it a role in the current project by selecting Role in current Project when viewing the Service Account under API Integrations -> Service Accounts. You can also give it access to other projects/organizations by going to the list of members (in Project Settings for project members), and then adding the Service Account as a member using the Service Account's email address, and selecting an appropriate role.

Once you have the credentials for a Service Account and have created a Disruptive instance, you can use the API to create new Service Accounts and add them as a members to your projects. See the createServiceAccount and inviteMember functions for more details.

See the Service Accounts article on the developer website for more details about Service Accounts in general.

Making Requests

Once an instance of the Disruptive struct has been created, it will be the main entry point to make requests against the Disruptive API. See the API reference for an overview of all the functionality available on the Disruptive struct.

Lists & Pagination

There are two main approaches to fetching a list of resources (such as a list of Devices or Projects): You can either fetch them all at once, or one page at a time. Fetching all the items of a resource at once is more convenient, but if there are a lot of items this can take a long time as multiple network requests might be made in the background to get all the pages automatically.

Fetching one page of items at a time is slightly more cumbersome to implement, but provides full control of how many items are fetched at a time and when to fetch the next page of items. Fetching one page at a time is available for Organization, Project, Device, DataConnector, Member, ServiceAccount, and ServiceAccount.Key. It is not available for Role and Permission as those have a well-known, small number of items to list. It is also not available for fetching events as events can be paged by specifying start and end timestamps to fetch events between.

Here are examples of both of these approaches for fetching a list of Devices.

Fetching all Devices in a project at once:

disruptive.getAllDevices(projectID: "<PROJECT_ID>") { result in
    switch result {
        case .success(let devices):
            print(devices)
        case .failure(let error):
            print("Failed to get devices: \(error)")
    }
}

getAllDevices documentation

Fetching Devices one page at a time:

var fetchedDevices = [Device]()
var nextPageToken: String?

func fetchNextPage(pageToken: String?) {
    disruptive.getDevicesPage(projectID: "<PROJECT_ID>", pageSize: 25, pageToken: pageToken) { result in
        switch result {
            case .success(let page):
                // Keep track of the page token to use for the next page.
                // Note that this will be `nil` when the last page is received.
                nextPageToken = page.nextPageToken
                
                // Update the list of all the devices we have found so far
                fetchedDevices.append(contentsOf: page.devices)
                
                print("Fetched \(page.devices.count) more devices. \(fetchedDevices.count) devices fetched in total")
            case .failure(let error):
                print("Failed to get devices: \(error)")
        }
    }
}

// Fetch the first page
fetchNextPage(pageToken: nil)

// Fetch subsequent pages when it makes sense (for example when pre-fetching data
// for a UITableView). Note that `nextPageToken` will be set to `nil` when the last
// page is received.
if let pageToken = nextPageToken {
    fetchNextPage(pageToken: pageToken)
}

getDevicesPage documentation

Fetching Historical Events

Fetching historical events for a device is similar to fetching other lists of data (like getOrganizations or getProjects). You need to specify the identifier of the project and the device, and optionally the start/end time and which events to fetch (certain event types are only available for certain device types, eg. temperature events are only available for temperature sensors). If the Result returned in the callback was .success, you will receive a value of type Events that contains an optional array of events for each event type. Only the event types that were actually returned will be non-nil, not necessarily the one specified in the eventTypes parameter.

Example of fetching just temperature events for a temperature sensor (defaults to last 24 hours):

disruptive.getEvents(projectID: "<PROJECT_ID>", deviceID: "<DEVICE_ID>", eventTypes: [.temperature]) { result in
    switch result {
        case .success(let events):
            if let temperatureEvents = events.temperature {
                print(temperatureEvents)
            }
        case .failure(let error):
            print("Failed to get temperature events: \(error)")
    }
}

getEvents documentation

Subscribing to Device Events

When subscribing to device events you have two options: Either subscribe to a single device, or to a list of devices. If you want to subscribe to a list of devices, you can filter on which devices to subscribe to based on both device types and labels. Either way, you will get a value of type DeviceEventStream back that will let you set up a callbacks for each the various event types. Only the event types specified in the eventTypes parameter will actually receive callbacks.

Example of subscribing to temperature events for a single temperature sensor:

let stream = disruptive.subscribeToDevice(
    projectID  : "<PROJECT_ID>", 
    deviceID   : "<DEVICE_ID>", 
    eventTypes : [.temperature] // optional
)
stream?.onTemperature = { deviceID, temperatureEvent in
    print("Got temperature \(temperatureEvent) for device with id \(deviceID)")
}
stream?.onError = { error in
    print("Got stream error: \(error)")
}

subscribeToDevice documentation

Other Common Requests

Search / Filter Devices

The requests to fetch devices has various parameters to search and/or filter devices. All of these parameters are optional (except for projectID), and can be mixed and matched as desired.

When specifying the order to retrieve the devices in, a field as well as an ascending/descending flag is included in a tuple. The value of this field is based on the JSON structure of the devices. Examples of fields to use include id (identifier), type (device type), labels.name (displayName). All events will have the format reported.<event_type>.<field>, eg. reported.networkStatus.signalStrength. See the REST API documentation for the GET Devices endpoint to get hints for which fields are available.

Here is an example of how to use all the parameters:

disruptive.getAllDevices(
    projectID    : "<PROJECT_ID>",
    query        : "Air Vent",
    deviceIDs    : ["<DEVICE_ID>", "<DEVICE_ID>"],
    deviceTypes  : [.temperature],
    labelFilters : ["kit": "perform-compare-establish"],
    orderBy      : (field: "reported.networkStatus.updateTime", ascending: false))
{ result in
    ...
}
Single Device Lookup

A single device can be looked up just by the identifier of the device. This is useful if you got a device identifier by scanning a QR code for example. Here's an example:

disruptive.getDevice(deviceID: "<DEVICE_ID>") { result in
    ...
}

getDevice documentation

Emulated Devices

Emulated devices can be created and used to publish events using the API. This enables testing out other parts of the API (such as listing devices) and developing a solution around the API without having access to physical devices.

Here's an example for how to create a new emulated temperature sensor:

disruptive.createEmulatedDevice(
    projectID   : "<PROJECT_ID>",
    deviceType  : .temperature,
    displayName : "Emulated Temperature Sensor")
{ result in
    ...
}

createEmulatedDevice documentation

Here's an example for how to publish a TemperatureEvent for an emulated sensor:

disruptive.publishEmulatedEvent(
    projectID : "<PROJECT_ID>",
    deviceID  : "<DEVICE_ID>",
    event     : TemperatureEvent(celsius: 42, timestamp: Date()))
{ result in
    ...
}

publishEmulatedEvent documentation

Fetch Projects & Organizations

Fetching projects lets you optionally filter on both the organization (by identifier) as well as a keyword based query. You can also leave both of those parameters out to fetch all projects available to the authenticated account. The following example will search for projects with a specified organization id (fetched from the getOrganizations endpoint for example) that has Building 1 in its name:

disruptive.getAllProjects(organizationID: "<ORG_ID>", query: "Building 1") { result in
    ...
}

getAllProjects documentation

Here's an example of fetching all the organizations available to the authenticated account:

disruptive.getAllOrganizations { result in
    ...
}

getAllOrganizations documentation

Misc Tips

  • Some basic debug logs can be enabled by setting Disruptive.loggingEnabled = true

GitHub