Creating Plugins

Overview

Plugins are the mechanism through which the Edge server communicates with local devices.

Plugins are responsible for:

  1. Discovering local devices
  2. Coordinating communication between devices and the Edge server
  3. Tracking the state changes for each device.
  4. Relaying this information to the corresponding Droplit service.

A good plugin is one that is focused on a common application-level protocol. For example, WeMo devices all work over HTTP with a common SOAP structure behind them, so a single generic plugin can work. Attempting to make a general HTTP device plugin would be poor design, however, because the structure of data used to communicate with devices over HTTP varies widely. Plugins should be based around a principle of common structure, encapsulating common functionality.

Plugins may be written in both TypeScript and native JavaScript. The example plugins droplit-plugin-ts-example and droplit-plugin-js-example (for each language accordingly) are included with the Edge server. These exammples may be useful as starting points to create further custom plugins, if desired.

Node.js can use native libraries through a C++ addon module using node-gyp.

Plugin Pieces

Service Classes

A plugin author should be familiar with how service classes are built, and how to use their components.

localId

A localId uniquely identifies a device within a plugin. The Edge server is responsible for mapping between the device ID from the Droplit service and the localId. While a localId only needs to be unique within a plugin, globally unique identifiers that a device may have make for a good localId (e.g., MAC address or a UUID from SSDP).

address

An address is the identifier used to address the device on the network. The form the address takes depends on the network protocol the device uses. E.g., a WiFi device would use an IP address or URI for its address.

Discovery

Each plugin is responsible for discovering the devices that it is compatible with.

The Edge server starts discovery by calling the discover method on the plugin. This is first called after all plugins have been initialized. Later, it is called on a routine interval, to discover any devices added after the first call. Discovery calls to each plugin are staggered to avoid interference between plugins. Beyond the discover call, the discovery process is implementation agnostic.

Every time a new device is discovered, the plugin should call the onDeviceInfo method. This will register the device and its details with the Droplit service based on the provided localId. Subsequent calls will update the device details with the specified properties. Only make additional calls when something has actually changed.

Discovered devices should be cached upon discovery. This helps track which discoveries are new, and makes it easier to track property changes.

The following example illustrates discovering a new device, caching it, and registering the device to the Droplit service.

Javascript:

// setup device discoverer
const discoverer = new Discoverer(); 

// Cache of known devices
const devices = new Map();

// Handle device being discovered
discoverer.on('device discovered', device => {

  // Skip the device if discovered already
  if (devices.has(device.identifier)) return;

  // Cache the device to record discovery
  devices.set(device.identifier, device);

  // Send device info
  onDeviceInfo({
    localId: device.identifier,
    address: device.ipAddress,
    deviceMeta: {
      customName: device.name,
      manufacturer: 'Sirius Cybernetics Corp.',
      modelName: 'Nutrimatics Drinks Despenser'
    },
    services: [ 'BinarySwitch' ]
  });
});

// The Edge process calls the discover function
function discover() {
  discoverer.discover();    // do discovery  
}

Typescript:

// setup device discoverer
const discoverer = new Discoverer(); 

// Cache of known devices
const devices: Map<string, Device> = new Map<string, Device>();

// Handle device being discovered
discoverer.on('device discovered', (device: Device) => {

  // Skip the device if discovered already
  if (devices.has(device.identifier)) return;

  // Cache the device to record discovery
  devices.set(device.identifier, device);

  // Send device info
  onDeviceInfo({
    localId: device.identifier,
    address: device.ipAddress,
    deviceMeta: {
      customName: device.name,
      manufacturer: 'Sirius Cybernetics Corp.',
      modelName: 'Nutrimatics Drinks Despenser'
    },
    services: [ 'BinarySwitch' ]
  }); 
});

// The Edge process calls the discover function
function discover() {
  discoverer.discover();    // do discovery 
}

Every plugin must handle the case of an address change (e.g., the IP address for a WiFi address). Changes in the device addresses are often found in subsequent discovery calls by comparing the known unique identifier with the address. When this happens, the Droplit service should be notified with an onDeviceInfo call containing only the localId and address.

The following example illustrates updating the device address after it has been changed:

Javascript:

discoverer.on('ipChange', data => {
  const device =
    this.devices.get(data.identifier);
  // If the device isn't defined, return
  if (!device)
    return;
  // Update the address in the cache
  device.address = data.address;

  // Only send the localId and changed data
  onDeviceInfo({
    localId: device.identifier,
    address: device.address
  });
});

Typescript:

discoverer.on('ipChange', (data: Device) => {
  const device =
    this.devices.get(data.identifier);
  // If the device isn't defined, return
  if (!device)
    return;
  // Update the address in the cache
  device.address = data.address;

  // Only send the localId and changed data
  onDeviceInfo({
    localId: device.identifier,
    address: device.address
  });
});

The Edge server will call the plugin’s dropDevice method when a device is deleted (e.g., through the Droplit service’s REST API). On dropDevice, the plugin should clear the specified device from its local cache. Dropped devices may be rediscovered on subsequent discovery calls.

The following example illustrates a dropDevice implementation.

Javascript:

function dropDevice(localId) {
   // The device is not previously known
   const device = devices.get(localId);
   if (!device)
     return false;

   // Remove the device from the cache
   this.devices.delete(device.identifier);
   return true;
}

Typescript:

function dropDevice(localId: string): boolean {
    // The device is not previously known
    const device = devices.get(localId);
    if (!device)
      return false;

    // Remove the device from the cache
    this.devices.delete(device.identifier);
    return true;
}

Edge as a Device

The Droplit Edge software connects remote devices to the Droplit.io cloud for monitoring and control. Devices connected through the edge software are referred to as downstream devices.

Downstream devices are independently addressable and controlled without needing a specialized knowledge of how they are connected up to the cloud. The node running the Droplit Edge software is also represented as a device in its respective environment.

Edge devices can expose services that can be monitored or controlled as well. Services local to the edge device are referred to as local services.

Local Services

Local services are exposed by configuring a plugin to handle requests for a particular service class. All requests made to the edge device accessing that service class will be routed to that plugin. This does not include requests addressed to downstream devices.

Ex., if you wish to handle the hypothetical DoStuff service with the hypothetical plugin-stuff plugin, it would be configured as:

{
  "plugins": {
    "plugin-stuff": {
      "enabled": true,
      "localServices": [ "DoStuff" ]
    }
  }
}

From within the plugin the localId used for services is ..

This localId will come in on any property accessor or method call and should be set for any upstream message such as an event or property changed message.

Ex., sending a notifyStuff event on DoStuff.

this.onEvents([{ localId: '.', service: 'DoStuff', member: 'notifyStuff' }])

Data Structures

There are common data structures used by various plugin interfaces.

DeviceInfo

The DeviceInfo structure is an object representing all of the data about a device, except for service property states. This structure is used by onDeviceInfo.

localId

The localId of the device represented by a string.

address

The address of the device represented by a string.

services

The services supported by the device represented by an array of strings.

deviceMeta

The deviceMeta property is an object representing device metadata. This device metadata is directly mapped to general device metadata. The explicit properties on deviceMeta get mapped to system metadata keys while custom properties get mapped to regular metadata keys.

System keys:

  • customName: A custom name stored on device. Many devices support the ability for a user to set a name through their first-party device — for such devices, this name should be used for custom name.
  • manufacturer: The device manufacturer.
  • modelDescription: A brief description of the device.
  • modelName: The name of the device’s model.
  • modelNumber: The device model number.

DeviceServiceMember

The DeviceServiceMember structure is an object used by property setters/getters, method calls, and events.

localId

The localId of the device represented by a string.

address

The address of the device represented by a string.

service

The name of the service class represented by a string

index

The index on the service class. Omit for service class implementations that are not indexed. Represented by a string.

member

The name of the member on the service class represented by a string.

value

A value to pass with the Edge event. For properties, this is the value of the property. For method calls, this is that method arguments. For events, this is an event value. this value may be any valid JS type.

Plugin interface

A plugin is expected to expose an object implementing a particular set of interfaces in order to work with the Edge server. For the convenience, Droplit provides a droplit-plugin module (supporting both TypeScript and native JavaScript) to make implementing a plugin as easy as extending a ES6 class; however, using the module is not strictly necessary for implementing a plugin (if for example you do not wish to use ES6+).

The following are plugin methods (signatures defined in TypeScript notation):

discover

discover(): void — discover all devices
discover is called by the Edge server to tell the plugin to attempt to find new devices (e.g., invoking SSDP).

drop device

dropDevice(localId: string): boolean — clear device information from memory
dropDevice is called by the Edge server to tell the plugin to remove knowledge of a specified device from any sort of cache. The device is specified with the localId parameter. The return value is a boolean signifying whether the device was successfully removed or not (e.g., the specified id is not one that the plugin is aware of).

onDeviceInfo

onDeviceInfo(deviceInfo: DeviceInfo, callback?: (deviceInfo: DeviceInfo) => {}): void
onDeviceInfo is called by the Edge server to update the non-service data of a device. It is expected that onDeviceInfo is called on device discovery in order to first initialize device data; subsequent calls may be made to modify this data.

The callback will be called with the complete deviceInfo data as it is known by the server in response to the update. This is useful if there is existing known data cached in the droplit.io service that is not easily discoverable from the plugin; on discovery, the plugin may send its incomplete information and fill-in the remainder with what the server already knows.

onEvents

onEvents(events: DeviceServiceMember[]): void
onEvents is called within the plugin to communicate to the Edge server when an event occurs. The events parameter is in the form of an array to allow for reporting multiple events being raised simultaneously.

ex. report MotionSensor.motion event occurring for device 1

this.onEvents({
  localId: '1',
  service: 'MotionSensor',
  member: 'motion'
});

onPropertiesChanged

onPropertiesChanged(properties: DeviceServiceMember[]): void
onPropertiesChanged is called within the plugin to communicate to the Edge server when a property’s value has changed. This method should not be called unless the underlying state value is actually different than the previously known value. Initial property values after device discovery should be reported with this method. The properties parameter is in the form of an array to allow for reporting multiple property changes simultaneously.

ex. report BinarySwitch.switch being set to on for device 1

this.onPropertyChanged({
  localId: '1',
  service: 'BinarySwitch',
  member: 'switch',
  value: 'on'
});

services

services: any — maps service class members to functions
services is a property of the plugin object that maps service class members to functions with an object of key-value pairs with members as keys and functions as values.

ex. maps the members of the BinarySwitch service class

this.services = {
    BinarySwitch: {
        get_switch: this.getSwitch,
        set_switch: this.setSwitch,
        switchOff: this.switchOff,
        switchOn: this.switchOn
    }
};

For members that are methods, the mapping key is the member name. For members that are properties, a mapping is expected for the property getter and setter (read-only properties may omit a setter). The format of these mapping keys is to prefix the member name with get_ and set_ for getters and setters accordingly.

The mapped functions have difference signatures depending on the type of member.
property getter: function(localId: string, callback: (value: any) => void, index: string): boolean
property setter: function(localId: string, value: any, index: string): boolean
method: function(localId: string, value: any, callback: (value: any) => void, index: string): boolean

localId — the id of the device to invoke the getter/setter/method call on
index — for indexed devices, this specifies the index to use on the device specified by the localId
value — the value to set a property to or call a method with
callback — invoke to return the get value for a getter or a value to return in response to a method call when it is a request rather than a call