Overview
Plugins are the mechanism through which the Edge server communicates with local devices.
Plugins are responsible for:
- Discovering local devices
- Coordinating communication between devices and the Edge server
- Tracking the state changes for each device.
- 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