-
Notifications
You must be signed in to change notification settings - Fork 81
Devices
An actor refers to an entity that participates in an activity. Typically, these refer to devices; however, there are two other types of actors: groups which combine actors accordingly to a logical relationship (e.g., 'and' or 'first/next') and pseudo actors which are software-only constructs (e.g., the place).
Support for devices is added by creating a module with a path and name that conform to the Device Taxonomy. This module is detected and loaded by the steward during startup. (Note that whenever you add a module to a running steward, you must restart the steward.)
When the module is loaded, the start() function is invoked which does two things:
-
It defines an object that is consulted when an instance of the device is discovered.
-
It defines how instances of the device are discovered.
A module consists of several parts. In reading this section, it is probably useful to also glance at one of the existing modules. For the examples that follow, let's say we're going to the define a module for a macguffin presence device manufactured by Yoyodyne Propulsion Systems.
The first step is to select the name of the device prototype. In this case, it's probably going to be:
/device/presence/yoyodyne/macguffin
The corresponding file name would be:
devices/device-presence/presence-yoyodyne-macguffin.js
The file should have six parts:
First, is the require section where external modules are loaded. By convention, system and third-party modules are loaded first, followed by any steward-provided modules, e.g.,
var util = require('util')
, devices = require('./../../core/device')
, steward = require('./../../core/steward')
, utility = require('./../../core/utility')
, presence = require('./../device-presence')
;
Second, is the logger section, which is usually just one line:
var logger = presence.logger;
Logging is done syslog-style, which means these functions are available
logger.crit
logger.error
logger.warning
logger.notice
logger.info
logger.debug
These functions take two arguments: a string and a property-list object, e.g.,
try {
...
} catch(ex) {
logger.error('device/' + self.deviceID,
{ event: 'perform', diagnostic: ex.message });
}
Third, is the prototype function that is invoked by the steward whenever an instance of this device is (re-)discovered:
Fourth, comes the optional observe section, that implements asynchronous observation of events.
Fifth, comes the optional perform section, that implements the performance of tasks.
Sixth, comes the start() function.
var Macguffin = exports.Device = function(deviceID, deviceUID, info) {
// begin boilerpate...
var self = this;
self.whatami = info.deviceType;
self.deviceID = deviceID.toString();
self.deviceUID = deviceUID;
self.name = info.device.name;
self.info = utility.clone(info);
delete(self.info.id);
delete(self.info.device);
delete(self.info.deviceType);
// end boilerplate...
self.status = '...';
self.changed();
// perform initialization here
utility.broker.subscribe('actors', function(request, taskID, actor, observe, parameter) {
if (request === 'ping') {
logger.info('device/' + self.deviceID, { status: self.status });
return;
}
if (actor !== ('device/' + self.deviceID)) return;
else if (request === 'observe') self.observer(self, taskID, observe, parameter);
else if (request === 'perform') self.perform(self, taskID, observe, parameter);
});
};
util.inherits(Macguffin, indicator.Device);
The prototype function invokes this whenever the steward module publishes a request to the actor asking that a particular event be monitored:
Macguffin.prototype.observe = function(self, eventID, observe, parameter) {
var params;
try { params = JSON.parse(parameter); } catch(ex) { params = {}; }
switch (observe) {
case '...':
// create a data structure to monitor for this observe/params pair
// whenever an event occurs, invoke
// steward.observed(eventID);
// now tell the steward that monitoring has started
steward.report(eventID);
break;
default:
break;
}
}
The prototype function invokes this whenever the steward module publishes a request to the actor asking that a particular task be performed:
Macguffin.prototype.perform = function(self, taskID, perform, parameter) {
var params;
try { params = JSON.parse(parameter); } catch(ex) { params = {}; }
if (perform === 'set') {
if (!!params.name) self.setName(params.name);
// other state variables may be set here
if (!!params.whatever) {
// may wish to range-check params.whatever here...
self.info.whatever = params.whatever;
self.setInfo();
}
return steward.performed(taskID);
}
// any other tasks allowed?
if (perform === '...') {
}
return false;
};
As noted earlier, this function performs two tasks. The first task is to link the device prototype into the steward:
exports.start = function() {
steward.actors.device.presence.yoyodyne =
steward.actors.device.presence.yoyodyne ||
{ $info : { type: '/device/presence/yoyodyne' } };
steward.actors.device.presence.yoyodyne.macguffin =
{ $info : { type : '/device/presence/yoyodyne/macguffin'
, observe : [ ... ]
, perform : [ ... ]
, properties : { name : true
, status : [ ... ]
...
}
}
, $observe : { observe : validate_observe }
, $validate : { perform : validate_perform }
};
devices.makers['/device/presence/yoyodyne/macguffin'] = MacGuffin;
...
The first assignment:
steward.actors.device.presence.yoyodyne =
steward.actors.device.presence.yoyodyne ||
{ $info : { type: '/device/presence/yoyodyne' } };
is there simply to make sure the naming tree already has a node for the parent of the device prototype. (The steward automatically creates naming nodes for the top-level categories).
The next assignment is the tricky one:
steward.actors.device.presence.yoyodyne.macguffin =
{ $info : { type : '/device/presence/yoyodyne/macguffin'
, observe : [ ... ]
, perform : [ ... ]
, properties : { name : true
, status : [ ... ]
...
}
}
, $list : function() { ... }
, $lookup : function(id) { ... }
, $validate : { create : validate_create
, observe : validate_observe
, perform : validate_perform
}
};
The $info field is mandatory, as are its four sub-fields:
-
type: the name of the device prototype.
-
observe: an array of events that the device observe. The array may be empty.
-
perform: an array of tasks that the device performs. At a minimum, Every device must support a "set" task in order to set the name of the device instance. The array may be empty (the "set" task does not appear in the array).
-
properties: a list of property names and syntaxes. Consult the Device Taxonomy section below for a list of defined properties and corresponding syntaxes.
The $list and $lookup fields are for "special" kinds of actors, and are described later.
The $validate field contains an object, with these sub-fields:
-
create: present for "special" kinds of actors, described later.
-
observe: a function that is used to evaluate an event and its associated parameter, to see if "it makes sense."
-
perform: a function that is used to evaluate a task and its associated parameter, to see if "it makes sense."
The second task performed by the start() function is to register with the appropriate discovery module. There are presently five modules:
-
SSDP: LAN multicast (not necessarily UPnP)
-
BLE: Bluetooth Low Energy
-
TCP port: TCP port number
-
MAC OUI: MAC prefix
For the first two discovery modules, the steward will automatically (re-)create device instances as appropriate. For the others, the module may need to do some additional processing before it decides that a device instance should (re-)created. Accordingly, the module calls device.discovery() directly.
Discovery via SSDP occurs when the steward encounters a local host with an SSDP server that advertises a particular "friendlyName" or "deviceType":
devices.makers['Yoyodye Propulsion Systems MacGuffin'] = Macguffin;
Discovery via BLE occurs when the steward identifies a BLE device from a particular manufacturer, and find a matching characteristic value:
devices.makers['/device/presence/yoyodyne/macguffing'] = Macguffin;
require('./../../discovery/discovery-ble').register(
{ 'Yoyodyne Propulsion' :
{ '2a24' :
{ 'MacGuffin 1.1' :
{ type : '/device/presence/yoyodyne/macguffin' }
}
}
});
Discovery via TCP port occurs when the steward is able to connect to a particular TCP port on a local host:
require('./../../discovery/discovery-portscan').pairing([ 1234 ],
function(socket, ipaddr, portno, macaddr, tag) {
var info = { };
...
info.deviceType = '/device/presence/yoyodyne/macguffin';
info.id = info.device.unit.udn;
if (devices.devices[info.id]) return socket.destroy();
utility.logger('discovery').info(tag, { ... });
devices.discover(info);
});
Discovery via MAC OUI occurs when the steward encounters a local host with a MAC address whose first 3 octets match a particular prefix:
require('./../../discovery/discovery-mac').pairing([ '01:23:45' ],
function(ipaddr, macaddr, tag) {
var info = { };
...
info.deviceType = '/device/presence/yoyodyne/macguffin';
info.id = info.device.unit.udn;
if (devices.devices[info.id]) return;
utility.logger('discovery').info(tag, { ... });
devices.discover(info);
});
Discovery via TSRP occurs when the steward receives a multicast message on the TSRP port.
There are three design patterns currently in use for device actors.
A standalone actor refers to a device that is discovered by the steward and doesn't discover anything on its own.
Examples of standalone actors include:
-
/device/lighting/blinkstick/led
-
/device/media/sonos/audio
-
/device/presence/fob/inrange
-
/device/sensor/wemo/motion
Typically the steward is able to discover a gateway for a particular technology, and the module for that gateway then discovers "interesting" devices. Examples of these kinds of actors include things like:
-
/device/gateway/insteon/hub and /device/switch/insteon/dimmer, etc.
-
/device/gateway/netatmo/cloud and device/climate/netatmo/sensor
When a gateway actor discovers an "interesting" device, it calls devices.discover() to tell the steward to (re-)create it.
These kind of actors aren't discoverable, so a client must make a specific API call to the steward in order to create an instance. Examples of creatable actors include:
- /device/indicator/text/xively
In general, these actors refer to software-only constructs: while it's the steward's job to discovery devices, only a user can decide whether they want sensor readings uploaded somewhere.
Devices are managed by authorized clients using the
/manage/api/v1/device/
path prefix, e.g.,
{ path : '/api/v1/actor/list'
, requestID : '1'
, options : { depth: all }
}
To create a device, an authorized client sends:
{ path : '/api/v1/actor/create/UUID'
, requestID : 'X'
, name : 'NAME'
, whatami : 'TAXONOMY'
, info : { PARAMS }
, comments : 'COMMENTS'
}
where UUID corresponds to an unpredictable string generated by the client, X is any non-empty string, NAME is a user-friendly name for this instance, INFO are any parameters associated with the device, and COMMENTS (if present) are textual, e.g.,
{ path : '/api/v1/actor/create/YPI'
, requestID : '1'
, name : 'OO'
, whatami : '/device/presence/yoyodune/macguffin'
, info : { beep: 'annoying' }
}
To list the properties of a single device, an authorized client sends:
{ path : '/api/v1/actor/list/ID'
, requestID : 'X'
, options : { depth: DEPTH }
}
where ID corresponds to the deviceID of the device to be defined, X is any non-empty string, and DEPTH is either 'flat', 'tree', or 'all'
If the ID is omitted, then all devices are listed, e.g., to find out anything about everything, an authorized client sends:
{ path : '/api/v1/actor/list'
, requestID : '2'
, options : { depth: 'all' }
}
To have a device perform a task, an authorized client sends:
{ path : '/api/v1/actor/perform/ID'
, requestID : 'X'
, perform : 'TASK'
, parameter : 'PARAM'
}
where ID corresponds to the deviceID of the device to perform the task, X is any non-empty string, TASK identifies a task to be performed, and PARAM (if present) provides parameters for the task, e.g.,
{ path : '/api/v1/actor/perform/7'
, requestID : '3'
, perform : 'on'
, parameter : '{"color":{"model":"cie1931","cie1931":{"x":0.5771,"y":0.3830}},"brightness":50}'
}
To define a device, an authorized client sends:
{ path : '/api/v1/actor/device/ID'
, requestID : 'X'
}
where ID corresponds to the deviceID of the device to be deleted, and X is any non-empty string, e.g.,
{ path : '/api/v1/actor/device/7'
, requestID : '4'
}
The steward's taxonomy consists of a hierarchical system for device prototypes along with a flat namespace for properties. Although the naming for device prototypes is hierarchical, based on primary function, a given property may appear in any device prototype in which "it makes sense".
Properties are expressed in a consistent set of units:
- percentage - [0 .. 100]
- degrees - [0 .. 360)
- mireds - [154 .. 500] (MK^-1, reciprocal mega-kelvins)
- meters/second - [0 .. N]
- coordinates - [latitude, longitude] -or- [latitude, longitude, elevation] -or- [latitude, longitude, elevation, accuracy]
- meters - [0 .. N]
- kilometers - [0 .. N]
- milliseconds - [0 .. N]
- id - [1 .. N]
- u8 - [0 .. 255]
- s8 - [-127 .. 128]
- fraction - [0 .. 1]
- timestamp - 2013-03-28T15:52:49.680Z
- celsius
- ppm
- decibels
- millibars
- sigmas
or as an entry from a fixed set of keywords.
At a minimum, three properties must be present in all devices:
-
name - a string
-
status - a keyword:
-
waiting - indicates that the steward is waiting for the device to communicate
-
busy - indicates that the device is busy "doing something"
-
ready - indicates that all is well between the steward and the device
-
error - indicates something else
-
reset - indicates that the device, sadly, requires intervention
-
on or off - for lighting and switches
-
motion or quiet - for motion sensors
-
idle, playing, or paused - for media players
-
present, absent, or recent - for presence and sensors
-
green, blue, orange, or red - for reporting steward health
-
-
updated - a timestamp
In addition, there are a few optional properties that might be supported by any device:
-
location - a coordinates array
-
placement - a string (e.g., "front yard")
-
batteryLevel - a percentage
-
lqi - an s8 value indicating proximity (higher is closer)
-
nextSample - a timestamp
-
text - a string
Certain properties are termed measurement properties in that model a physical characteristic that:
-
is measured in a particular set of units (e.g., 'celsius');
-
that may be related to the SI system; and,
-
and often expressed using a symbol (e.g., 'C').
If you wish for a measurement property to be automatically archived, then examine the file
steward/devices/device-sensor.js
to see which measurement properties are currently defined in the measures object, e.g.,
var measures = { temperature: { symbol: 'C', units: 'celsius', type: 'derivedSI' }
...
};
Add to this object as appropriate. The valid values for the type field are:
-
baseSI - one of the seven SI base units;
-
derivedSI - formed by the juxtaposition of two or more base units;
-
derivedUnits - a unit derived not derived from the SI, but frequently used in conjunction with the SI; and,
-
contextDependentUnits - anything else.
Now let's look at the ten categories of devices.
These are devices that monitor or control the "breathable environment". The naming pattern is:
/device/climate/XYZ/QUALITY
depending on whether control functions are available.
At a minimum, two properties must be present:
-
lastSample - a timestamp
-
temperature - in degrees centigrade (celsius)
In addition, depending on the capabilities of the device, additional properties may be present:
-
humidity - a percentage
-
co2 - in parts-per-million
-
noise - in decibels
-
pressure - in millibars
-
airQuality - in sigmas
-
smoke - in sigmas
-
co - in sigmas
-
no2 - in sigmas
-
hcho - in sigmas
-
needs* - true or false
-
advise* - a string (e.g., "front yard")
Please note that the updated and lastSample properties report different things: lastSample indicates when the climate properties where last measured, whilst updated indicates the last change in state for the device (i.e., it is possible for updated to change regardless of whether lastSample changes; however, whenever lastSample changes to reflect a more recent measurement, updated will also change to the current time).
Finally, control devices may have additional properties:
away - either off or on
hvac - either off, cool, heat, or fan
fan - either on, auto, or the number of milliseconds that it should run
goalTemperature - the desired temperature
leaf - either off or on
These are devices that interface to non-IP devices, or devices that talk to a cloud-based service to get information about a device in the home. Accordingly, there are two naming patterns, i.e.,
/device/gateway/TECHNOLOGY/MODEL
/device/gateway/TECHNOLOGY/cloud
For example:
/device/gateway/insteon/hub
/device/gateway/netatmo/cloud
The status property may be the only property present:
- status - waiting, ready, error, or reset
Note that gateways to cloud-based services require authentication information, which is typically set using either the
/manage/api/v1/device/create/uuid
or
/manage/api/v1/device/perform/id
APIs.
These are devices that provide an indication to the user that is related to neither the "lighting environment" or the "media environment". The naming pattern is:
/dev/indicator/XYZ/MEDIA
where MEDIA is usually text.
The status property may be the only property present:
- status - waiting, ready, or error
As with gateway devices for cloud-based services, once initialized, these devices are almost entirely uninteresting to the user.
These are devices that control the "lighting environment". The naming pattern is:
/device/lighting/XYZ/bulb
/device/lighting/XYZ/cfl
/device/lighting/XYZ/downlight
/device/lighting/XYZ/led
/device/lighting/XYZ/lightstrip
/device/lighting/XYZ/uplight
Given the range of physical properties, it is challenging to provide an abstraction which preserves the fidelity of the device-specific color model.
The status property indicates the current state of the bulb:
- status - waiting, on, or off
At a minimum, two tasks must be available:
-
on - turns the light on
-
off - turns the light off
Any of these properties may be present, which are set with the on task:
-
color.model - defines the model and parameters, any combination of:
-
color.rgb - with parameters r, g, and b, each a integer-value between 0 and 255
-
color.rgb16 - with parameters r, g, and b, each a integer-value between 0 and 65535
-
color.rgbow - with parameters r, g, b, o, and w, each a integer-value between 0 and 255
-
color.hue - with parameters hue (in degrees) and saturation (as a percentage)
-
color.temperature - with parameter temperature expressed in mireds
-
color.cie1931 - with parameters x and y expressed as a real-value between 0 and 1
-
With respect to the color model, the list above is presented starting at the least-desirable (rgb) to the most-desirable (cie1931). Clueful clients that manage the lighting environment should take note of which models are supported by a device and use the most desirable.
In addition, there are some optional properties:
-
brightness - an integer-value percentage of the bulb's possible output
-
color.fixed - true or false (if true, the color may not be changed)
-
pps - u16 the number of pixels in a pixel strip
These are devices that control the "media environment". The naming pattern is:
/device/media/XYZ/audio
/device/media/XYZ/netcam
/device/media/XYZ/video
At a minimum, these properties must be present:
-
status - one of idle, playing, paused, or busy
-
track - defines the track information:
-
title, artist, and album - strings
-
albumArtURI - a URI
-
-
position - an integer-value indicating the number of milliseconds
-
volume - an integer-value percentage of the device's possible output
-
muted - either on or off
At a minimum, these tasks must be available:
-
play - plays the url parameter (or resumes playback if the url isn't present)
-
stop - stops playback
-
pause - pauses playback
-
seek - to the position parameter
-
set - set any of these parameters: position, volume, and/or muted
These are devices that have some movement capability: either rotational or mobile. The naming pattern is:
/device/motive/XYZ/2d
/device/motive/XYZ/3d
/device/motive/XYZ/ptz
/device/motive/XYZ/automobile
under review
These are devices that report presence. The naming pattern is:
/device/presence/XYZ/fob
/device/presence/XYZ/mobile
At a minimum, two properties must be present:
-
status
-
present - the device is currently detected
-
absent - the device is no longer detected
-
recent - the device was recently detected (to save device power, the steward does not continuously probe the device)
-
-
lqi - an integer-value between -127 and 128 indicating proximity (higher is closer)
At a minimum, one task must be present:
-
alert - causes the presence device to emit a visual and/or audio signal
- level parameter: one of none, mild, or high
These are devices that measure one physical quality (such as motion). The naming pattern is:
/device/sensor/XYZ/QUALITY
At a minimum, one property must be present:
- lastSample - a timestamp
These are devices that control power either in binary (onoff), contiguously (dimmer), or as a group (strip). In addition, this category also includes devices that monitor power (meter). The naming pattern is:
/device/switch/XYZ/dimmer
/device/switch/XYZ/meter
/device/switch/XYZ/onoff
/device/switch/XYZ/strip
The status property may be the only property present:
- status - on, off, busy, or waiting
For devices that control power, at a minimum, two tasks must be available:
-
on - turns the power on
- level (dimmer only) - an integer-value percentage of the switch's possible output
-
off - turns the power off
These are devices that are similar to fob devices, but meant to be more personal. The naming pattern is:
/device/wearable/XYZ/watch
Please consult the section on Presence devices for further details. (In the future, it is likely that this device prototype will have additional features.)
There are a large number of technologies available for integration. The steward's architecture is agnostic with respect to the choice of communication and application protocols. However, many of these technologies compete (in the loose sense of the word). Here is the algorithm that the steward's developers use to determine whether something should go into the development queue.
-
Unless the device is going to solve a pressing problem for you, it really ought to be in mainstream use. One of the reasons that the open source/node.js ecosystem was selected is because it presently is the most accessible for developers. Similarly, it's desirable to integrate things that are going to reduce the pain for lots of people.
-
The mainstream use test consists of going to your national amazon site and seeing what, if anything, is for sale. Usually, the search page will reveal several bits of useful information.
-
Sponsored Links: often these are links to distributors, but sometimes there links to knowledge aggregators. The first kind link is useful for getting a better sense as to the range of products available, but the second kind link is usually more useful because it will direct you to places where you can find out more about the integration possibilities, e.g., community sites, developer forums, and so on.
-
Products for sale:
-
Frequently Bought Together:
-
Customers Who Bought This Item Also Bought:
-
One of the things that is quite perplexing is the lack of technical information on technical products. Although many developer forums have information, "code rules". So the obvious stop is github - search for the technology there. Look at each project:
-
Many projects often include pointers to community, forum, and documentation sources.
-
Some projects contain a documentation directory; if not, you can usually get a sense of things by looking at the "main" source file.
-
If you are fortunate, a large part of the integration may already be done in node.js (use "npm search"). If so, check the licensing to see if it is "MIT". If not, study it carefully to see whether it will work for you.
-
After reviewing the project, go up one level and look at the author's other projects. Often there are related projects that weren't returned by the first search.
Finally, you may have a choice of devices to integrate with, and you may even have the opportunity to build your own. If you go the "off-the-shelf" route, please consider what is going to be easiest for others to connect to:
-
If there is an ethernet-connected gateway to a device, then it is best to integrate to that gateway: others will be able to use the gateway fairly easily, because attaching devices to an ethernet is fairly simple.
-
Otherwise, if there is a USB stick that talks to a device, then use that: although USB sticks should be less expensive than devices with ethernet-connectivity, they also tend to require more expertise to configure.
-
Finally, if there is a serial connector that talks to a device, you can always use that. Good luck!