DLI ISO32 User's Guide
1.9.16.0
External APIs

The controller can be accessed programmatically using a number of protocols and APIS, including:

  • the REST-like API, in the following variants:
    • regular HTTP,
    • CoAP over WebSockets over HTTP,
    • command-line via SSH,
  • JSON-RPC (over HTTP),
  • SNMP,
  • MQTT,
  • UPnP.

Additionally, a Lua access library is provided for standalone scripts.

Common external API settings

apis_apis.png
Common external API settings

Each of the external APIs can be enabled separately. Command-line REST-like API, or the standalone Lua access library cannot be disabled, except by e.g. disabling SSH access.

HTTP APIs perform cross-site request forgery checking to make sure they are not called by a misguided browser without JavaScript, bypassing browser security checks (a custom header needs to be present in the requests). Browsers can normally issue GET and POST requests with URL-encoded or multipart content types; you can tick the corresponding "relax ... checks" checkboxes to skip the checks in cases where the method or content type indicates that the request couldn't have been sent by a browser without JavaScript.

REST-like API over HTTP

apis_rest_demo.png
REST-like API demo

The REST-style API is based on the REST architectural style. It presents the state and configuration as an hierarchy of resources, and relies on HTTP to perform action signaling and content negotiation. Requests with different HTTP headers yield different representations of resources (e.g. plain text, HTML, JSON, etc.). A type description system is used to outline the object model.

Refer to the REST-style API reference for details.

REST-like API over CoAP over WebSockets

This REST-style API variant shares the object hierarchy with the plain HTTP variant; however, it relies on a WebSocket connection for transport and CoAP for action signaling and content negotiation. The only supported content type is JSON; the only supported content patch type is JSON patch. To use it, a WebSocket connection with protocol 'coap' must be made to the '/.well-known/coap' URL. From there, CoAP requests can be sent via binary WebSocket packets; the CoAP URL structure matches one of the regular REST-style API (in particular, the first Uri-Path segment is 'restapi', and the trailing segment must be empty for regular data requests). CoAP Observe option is not yet implemented. REST API matrix URI fragments are not implemented as there's no consensus on how they should be encoded. Depth-limited queries are not implemented.

REST-like API on the command line

This REST-style API variant shares the object hierarchy with the plain HTTP variant; however, it is usable from the command line, e.g. over SSH. The only supported (and implied) content type is JSON, and the set of CRUD operations is a bit different from the conventional REST-like API. Resources are identified by paths similar to HTTP REST API ones, but with the '/restapi/' part omitted, and needn't be terminated by a '/'; instead, retrieving description of a resource is performed using a designated verb. The API is accessed through the 'uom' command, which has several subcommands. The 'get' and 'set' subcommands correspond roughly to 'GET' and 'PUT' requests, except that 'set' is not to be used to create new resources:

uom get <uom/path>
uom set <uom/path> <value>

For example:

# uom get /relay/name

yields:

"DLI Controller"

Both the 'get' response and the 'set' argument are proper JSON values, so need to be properly quoted independently of shell's own quoting:

# uom set /relay/name '"Power Controller"'

The 'insert' subcommand is intended for collection element creation; it is different from a 'POST' request as its path argument must point to the element intended to be created, and different from a 'PUT' request as it shifts elements when inserting in the middle of an array:

uom insert <uom/path> <value>

Use a '-' instead of the last path element for specifying "the element position after the last one" (i.e. appending to an array):

uom insert config/links/- '{"href":"http://example.com/","description":"Example link"}'

The 'remove' subcommand is intended for collection element deletion; it is similar to a 'DELETE' request; use a '-' instead of the last path element for specifying "the last element's position" (i.e. deleting the last array element):

uom remove <uom/path>

The 'invoke' subcommand is intended for calling object methods; it is similar to a 'POST' request; however, the argument list is specified as a number of positional arguments (as is common in shell scripts) instead of a JSON array.

uom invoke <uom/path> [arg [arg ...]]

The 'describe' subcommand retrieves an element's type description; it corresponds to a 'GET' request with a trailing '/description' URI fragment.

uom describe <uom/path>

The 'keys' subcommand retrieves a JSON array corresponding to the current set of an element's keys.

uom keys <uom/path>

It can be used on arrays and objects alike:

# uom keys relay/outlets/
[0,1,2,3,4,5,6,7]

# uom keys meter/values
["relay_voltage","environment.temperature",...]

The 'dump' subcommand prints out a deep text representation of the element, possibly including read or iteration error indicators:

uom dump <uom/path>

The 'subscribe' subcommand prints the current state of the element, as if obtained via 'get', and waits for changes, printing them as they are received:

uom subscribe <uom/path>

It does not return unless an error reading the object occurs (e.g. if it's gone).

Depth-limited queries, as well as matrix URI fragments, are not implemented (the 'keys' subcommand can be used to perform depth-limited queries manually).

If you maintain nontrivial command-line scripts, you may want to store them under the /storage directory, which is reserved for the unit administrator and is persisted across upgrades.

Refer to the REST-style API reference for details.

Lua API access library

A library for accessing the object model in standalone Lua scripts (run by e.g. cron or otherwise configured via SSH, i.e. not by the scripting server) is provided. It shares the object hierarchy with the REST-like API; however, note that Lua conventions have array indices start with 1, not 0. The library is accessed from Lua via a require("uom") expression, e.g.

local uom=require("uom")

As usual in Lua, parentheses are optional.

Members of the uom table correspond to top-level object model entities (including uom.cred representing the credential structure); additionally, the following members are provided:

  • uom.null represents the JSON null, and is true in a boolean context; for convenience, however, simple object fields instead evaluate to Lua nil instead of null, and are therefore false in a boolean context;
  • uom.weak_key_table(), uom.weak_value_table() and uom.weak_key_value_table() functions construct tables with weak keys and/or values as if by using setmetatable({},{__mode="k"}), setmetatable({},{__mode="v"}) and setmetatable({},{__mode="kv"}), respectively; unlike the user scripting environment, system-level scripts run with setmetatable accessible, so the functions are provided mostly for consistency;
  • uom.pairs(object), uom.ipairs(object), uom.insert(object[,idx],value) and uom.remove(object[,idx]) should be used on object model members instead of pairs, ipairs, table.insert and table.remove, respectively, and have similar semantics (they work on regular tables fine as well; uom.remove doesn't return the removed element as that would make no sense in our case; additionally, uom.pairs and uom.ipairs may return nil values corresponding to nulls);
  • uom.timer(callback, timeout) and uom.periodic_timer(callback, interval) schedule a callback to be run, respectively, after timeout milliseconds elapse and each interval milliseconds; both return a callback that can be used to cancel the timer, or otherwise must be held to keep the timer active;
  • uom.publish(object,...) allows process-wide publishing of, and uom.subscribe(object,callback) subscribing to, messages for a topic identified by a particular Lua value, including object model members; callback receives all the arguments of the corresponding uom.publish in the same order, starting with the object; object model members emit messages on member changes; uom.subscribe returns a callback that can be used to cancel subscription, or otherwise must be held to keep the subscription active;
  • uom.dump(object[,sink]) returns a deep text representation of object, possibly including read or iteration error indicators if you pass it an object with members you don't have read access to; if a sink function is supplied, uom.dump calls sink with each of the lines it would have returned normally and returns nil;
  • uom.json.encode and uom.json.decode utility functions perform JSON encoding and decoding of tables (but not object model entities; use uom.copy first);
  • uom.copy(obj), uom.equals(a,b) and uom.merge(to,from) utility functions perform deep copying, equality check and merging of tables (including object model entities);
  • uom.context(partial) creates a new entity similar to the top-level uom object (containing top-level object model entities) but having a separate update queue; if partial is true, the resulting entities will be optimized for code retrieving only a subset of an entity's properties; additionally, such entities will not receive uom.subscribe() notifications for properties you haven't previously read or written;
  • uom.update fetches any pending changes to the object model into the local proxy representation (the default one, or the context created by uom.context() and supplied as the argument), and runs handlers for any changes you may have subscribed for using uom.subscribe;
  • uom.vacuum runs a cleanup step for unused object model proxies;
  • uom.run() starts running an event loop, which is useful if you have previously subscribed to messages or set up timers;
  • uom.stop_running(), often called from a subscription or timer callback, causes the active uom.run() to stop.

The following example scripts are meant to be saved as files to the power controller's filesystem and executed with the standalone lua interpreter. If you want to just enter them into the interpreter line by line (possibly interactively as a means of experimentation or convenient configuration tool), you'll need to keep your variables global (e.g. omit local) as each interpreter line has its own locals context.

For example, running the following script:

local uom=require("uom")
print(uom.relay.name)

yields:

DLI Controller

Setting simple values works the same way:

local uom=require("uom")
uom.relay.name="Power Controller"

Insertion and removal work as well:

local uom=require("uom")
uom.insert(uom.config.links,{href="http://example.com/",description="Example link"})
uom.remove(uom.config.links)

You can hold object model members in local variables:

local uom=require("uom")
local relay1=uom.relay.outlets[1]
relay1.state=true
relay1.state=false

Note that attempts to change a table after it has been assigned to an object model field or inserted into an object model container will not work as expected:

local uom=require("uom")
local link={href="http://example.com/",description="Example link"}
uom.insert(uom.config.links,link)
link.href="http://bad.example.com/" -- Don't do this

It is possible to partially dump objects you don't have full access to, for instance:

local uom=require"uom"
print(uom.dump(uom.auth.users[1]))

results in:

{
  is_admin = true,
  is_allowed = true,
  name = "admin",
  outlet_access = {
    true,
    true,
    true,
    true,
    true,
    true,
    true,
    true
  },
  password = <read failed>
}

and an appropriate access log entry.

Object model members send change notification messages on eligible fields (generally, those not being marked as volatile in their type descriptions). Change notifications contain arguments of the form key, value, key_adjustment. For example, the following script outputs the physical state of outlet 1:

local uom = require("uom")
local count = 10
local subscription = uom.subscribe(
    uom.relay.outlets[1],
    function(o,key,value,key_adjustment)
        if key == "physical_state" then
            print(value and "ON" or "OFF")
            if count == 0 then
                print("Enough!")
                uom.stop_running()
            else
                count = count-1
            end
        end
    end
)
print(uom.relay.outlets[1].physical_state and "ON" or "OFF")
uom.run()

uom.json.decode produces, and uom.json.encode accepts a second argument which is the 'empty array table' consulted to resolve the Lua empty table encoding ambiguity (i.e. should a particular {} be encoded as a JSON array or an object). For instance,

local array={}
local object={}
print(uom.json.encode({array,object},{[array]=true}))

produces the output:

[[],{}]

and the script:

print(uom.dump({uom.json.decode("[[],{}]")}))

produces output similar to:

{
  {{},{}},
  {[value[1][1]] = true}
}

Note that uom.dump can produce meaningful output for tables with table keys and even key-recursive tables, but uom.json.encode, uom.copy, uom.equals and uom.merge only work with JSON-compatible tables (string or integer keys only).

uom.vacuum and uom.update may only be useful for long-running scripts which don't rely on uom.run (as the latter performs updates and cleanups automatically).

If you maintain nontrivial OS-level Lua scripts, you may want to store them under the /storage directory, which is reserved for the unit administrator and is persisted across upgrades.

Refer to the REST-style API reference for details.

JSON-RPC

apis_jsonrpc_demo.png
JSON-RPC demo

JSON-RPC allows to access an object model similar to the one of the REST-like API, but in a different manner which may be more suitable for some integration environments. All composite objects are visible using JSON-RPC, with their field values accessible using "get" (with the field name in the argument) and "set" methods (with the field name and value as arguments). Additionally, containers support "add", "remove" and "list" methods. The "describe" method can be used to output a type description for the object (similar to the REST API "description" relative URI).

UPnP settings

apis_upnp.png
UPnP settings

The unit's outlets can be exposed via UPnP as devices with different profiles. The currently supported profile is a Belkin WeMo socket.

SNMP settings

SNMP (simple network management protocol) exposes the control variables as a set of hierarchical resources identified by object identifiers (OIDs). An object identifier is roughly a sequence of non-negative integers (called arcs), separated by dots ('.'). A leading dot may be used to emphasize that it's an absolute OID; however, all of the OIDs configurable in ISO32 are absolute unless otherwise stated explicitly, and the leading dot is not needed, therefore, it's not supported.

apis_snmp_subtrees.png
SNMP OID subtree properties

SNMP v3 introduces a user-based security model, where a number of different users can exist whose requests can be signed, and possibly encrypted, and who can have different access rights to the OID tree.

The specified root OIDs and their children will be exposed over SNMP. All OIDs must be absolute but not preceded by a dot.

The root OIDs are actually treated as masks, indicating to set of roots to apply the permission to. In addition to the standard OID syntax, all but the first two arcs of an OID mask may contain:

  • an asterisk "*", which means that any value in this position will match, e.g. "1.2.*.1" will match both "1.2.1.1" and "1.2.100.1";
  • a dash-delimited range, e.g. "1.2.8.1-3" will match both "1.2.8.1" and "1.2.8.2";
  • a comma-separated list of arcs, possibly including ranges, e.g. "1.2.8,9" will match both "1.2.8" and "1.2.9", and "1.2.1,6-8" will match both "1.2.1" and "1.2.7".

This can be used to implement fine-grained access to states of individual outlets (see below).

apis_snmp_users.png
SNMP user table

The engine ID identifies the device, and plays an important role in SNMPv3, in particular in authentication and encryption. It will normally be autodetected by management software (SNMP clients), but you may save it for future reference (the default value is based on the device factory MAC address). You can even change it; however, if you do, all passwords for SNMPv3 users will be invalidated as they are stored in a localized form to improve security.

SNMP v1 and v2c do not have a notion of 'users'. Instead, a 'community string', acting as a shared secret, is transferred in requests in plain text. The following table allows to configure how community strings are mapped to the above users.

apis_snmp_communities.png
SNMP community-to-user mapping configuration

In this example, requests with the 'private' community string will be serviced as though they were made by the 'powerAdmin' user if they come from the 192.168.0.x subnet, and denied otherwise. Likewise, requests with the 'public' community string coming from the same subnet will be served as the 'powerReader' user.

SNMP energy object MIB support overview

The Net-SNMP agent included in ISO32 has built-in support for several well-known MIBs, but none of them deal with power control. The power-control-related ENERGY-OBJECT-MIB is described in RFC 7460, and is supported in the following manner:

  • the root of the OID tree is at 1.3.6.1.2.1.229 as per RFC;
  • the objects are outlets, with indices starting at 1 ;
  • the current actual power states are indicated in the eoPowerOperState (1.3.6.1.2.1.229.1.2.1.9) table;
  • the assigned (expected) power states can be manipulated in the eoPowerAdminState (1.3.6.1.2.1.229.1.2.1.8) table;
  • supported power states are ieee1621Off (257) and ieee1621On (259) only

Additionally, among others, the following potentially useful parts of the above MIB are implemented:

  • eoPowerStateTotalTime;
  • eoPowerStateEnterCount.

These accumulate outlet state statistics. Note that those don't persist across device reboots.

The following parts of the above MIB are NOT implemented:

  • eoEnergyParametersTable;
  • eoEnergyTable;
  • eoMeterCapabilitiesTable.

The following related MIBs are NOT supported:

  • ENTITY-MIB;
  • ENERGY-OBJECT-CONTEXT-MIB.

Additionally, modifying the user permissions via SNMP is NOT supported as they are generated from the configuration described above and the process is not easily reversible.

In the default configuration, the security level for accessing the energy object MIB subtree is high. You can set the access level to 'Minimal' to interact with the device using SNMPv2c and SNMPv1, or use SNMPv3 instead, which is the recommended and more secure alternative.

SNMP sample commands

These examples assume you have your ISO32 at 192.168.0.100 with SNMPv3 user powerAdmin configured with SHA1 for authentication and AES for encryption, with password powerAdminPassword for both authentication and encryption. Requests with the private community string are assumed to be serviced as though they were made by the powerAdmin user.

You'll need Net-SNMP to run these samples; analogous commands should be available for other management software. The matching of requests vs SNMP protocol version is really arbitrary and is only used to demonstrate different ways of performing requests. Lines are broken using \\ for readability. We use -On to force numeric OID output, and omit the leading '.' in output OIDs for simplicity.

An SNMPv2 SET to turn outlet #3 on:

$ snmpset -On -v 2c -c private 192.168.0.100 1.3.6.1.2.1.229.1.2.1.8.3 i 259

Output:

1.3.6.1.2.1.229.1.2.1.8.3 = INTEGER: 259

An SNMPv3 GET to get outlet #5 status:

$ snmpget -On -v 3 -u powerAdmin -l authPriv -a SHA -x AES \
-A powerAdminPassword -X powerAdminPassword 192.168.0.100 \
1.3.6.1.2.1.229.1.2.1.9.5

Output:

1.3.6.1.2.1.229.1.2.1.9.5 = INTEGER: 257

257 is ieee1621Off, so now you know the outlet is physically off.

An SNMPv3 SET to turn outlet #5 on:

$ snmpset -On -v 3 -u powerAdmin -l authPriv -a SHA -x AES \
-A powerAdminPassword -X powerAdminPassword 192.168.0.100 \
1.3.6.1.2.1.229.1.2.1.8.5 i 259

Output:

1.3.6.1.2.1.229.1.2.1.8.5 = INTEGER: 259

Using SNMPv1 to enumerate the actual power states table:

$ snmpwalk -On -v 1 \
-c private \
192.168.0.100 \
1.3.6.1.2.1.229.1.2.1.9

Output:

1.3.6.1.2.1.229.1.2.1.9.1 = INTEGER: 257
1.3.6.1.2.1.229.1.2.1.9.2 = INTEGER: 257
1.3.6.1.2.1.229.1.2.1.9.3 = INTEGER: 259
1.3.6.1.2.1.229.1.2.1.9.4 = INTEGER: 257
1.3.6.1.2.1.229.1.2.1.9.5 = INTEGER: 259
1.3.6.1.2.1.229.1.2.1.9.6 = INTEGER: 257
1.3.6.1.2.1.229.1.2.1.9.7 = INTEGER: 257
1.3.6.1.2.1.229.1.2.1.9.8 = INTEGER: 257

You see that outlets 3 and 5 are on, and all others are off ; you can change outlet states as described above.

MQTT API

MQTT is an event-oriented protocol with a centralized publish/subscribe model, which makes it a bit awkward to use for controlling devices; however, an implementation is included due to its popularity.

MQTT client settings

apis_mqtt_config.png
General MQTT settings

ISO32 can function as an MQTT client, so you need to have a configured MQTT broker which it could connect to; then, other MQTT clients connected to the same broker could communicate with it (multi-broker configurations are also possible but out of scope of this document). SSL and username/password authentication are supported (leave empty to disable). The default port is 8883 when SSL is enabled, and 1883 otherwise.

Setting and reporting outlet state are performed by means of MQTT messages. MQTT messages carry topic markers to identify their type. The topics are arranged in a '/'-separated hierarchy.

The 'Topic root' setting allows to prepend a common string to topics related to all outlets, e.g. to group messages related to the same controller. It is advisable that you set a different topic root for every controller that you connect to the same MQTT broker; otherwise, you'll get collisions unless you set different topic subtrees for their outlets.

MQTT messages may have different QoS (quality of service) levels, which affect their handling by the broker:

  • "At most once" — no delivery guarantee or retransmission attempts;
  • "At least once" — acknowledgment is required, but the receiver may receive several copies of the message;
  • "Exactly once" — care is taken to make sure receiver receives exactly one copy of each message (highest overhead).

The following connection-related messages can be configured (i.e. have their topic, payload, QoS and retain bit set):

  • the connection message is sent after the client has connected to the broker and sent the initial outlet state data, if any;
  • the reconnection message is sent after the client has reconnected to the broker after an unintentional disconnection (e.g. network outage), and sent the initial outlet state data, if any (distinction between this and the above initial connection message may be used to check continuous connectivity over a period of time);
  • the disconnection message is sent before the client starts an intentional disconnection (in particular, before outlet data clearing is performed), e.g. when MQTT client is disabled or the broker address is changed;
  • the Last Will and Testament is a message that will be sent by the broker to notify you if the ISO32 unexpectedly goes offline.

Connection-related messages with an empty topic are not sent. Note that the topics of these messages are not prefixed by the topic root.

Changes to the connection, reconnection and disconnection message properties start affecting corresponding events immediately after the change; such changes do not by themselves cause changes to connection status. Unlike them, changes to the Last Will and Testament only take effect on the next (re)connection, i.e. if you have a connection active, change the Last Will and Testament and interrupt that connection, the old message will be sent in that particular occasion, but subsequent unexpected disconnections will result in the new Last Will and Testament sent. It may be useful to disconnect from the broker manually (e.g. by disabling MQTT) before changing connection-related message properties.

MQTT outlet bindings

apis_mqtt_binding.png
MQTT outlet bindings

Every outlet can be mapped to an MQTT topic subtree, which will be prefixed by the topic root and '/'; the outlet will report its state with a message with that topic if 'Allow read' is checked and honor requests to change its state if it receives a message with that topic suffixed by '/set' and 'Allow write' is checked. The quality of service determines the mode of delivery to request from the broker for messages on reporting outlet state; not all brokers may support all delivery modes; the default should be sufficient for most purposes.

Note that there is no explicit way to request the states of outlets. MQTT brokers are expected to keep track of the most recent payload of the outlet state topics, since the related messages have the retain bit set. Conversely, outlet state setting messages (ones with the '/set' topic suffix) must be sent by clients with the retain bit cleared: the API treats any such messages with the retain bit set as illegal and ignores them, since they could have been sent long ago and the current outlet state might have been overridden by other means since then.

MQTT payload formats

MQTT payload format is not defined by a standard, so we explicitly define it here. Everything that can be controlled via MQTT in ISO32 is an outlet state, which can be ON or OFF. We encode ON as '1' (the single ASCII character '1') and OFF as '0'. For compatibility, in addition to decoding '1' as ON and '0' as OFF, we accept '\0' (the ASCII NUL character) and the strings 'off' and 'false' as OFF, and '\1' (the ASCII SOH character) and the strings 'on' and 'true' as ON. Additionally, the empty string '' is used to indicate topic erasure (on topic changes or read access revocation).

MQTT sample commands

These examples assume you have an MQTT broker at 192.168.0.5, it doesn't require authentication and you have the ISO32 set up like shown on screenshots above.

You'll need the mosquitto MQTT client to run these samples; analogous commands should be available for other clients. mosquitto also has an MQTT broker implementation.

If you run the command:

mosquitto_sub -h 192.168.0.5 -C 1 -t pcr12345/outlets/0

it'll print the current state of the first outlet as known to the broker (as '0' or '1') and exit. The '-C 1' flags disable waiting for more state changes; if you run the command with them removed:

mosquitto_sub -h 192.168.0.5 -t pcr12345/outlets/0

you'll see the current state, but the program will wait for more state messages and print the states as they arrive; if you flip the outlet, you'll see output like this:

0
1
0
1

etc.

To change the state of the first outlet, use the mosquitto_pub command, e.g. to switch it on use:

mosquitto_pub -h 192.168.0.5 -t pcr12345/outlets/0/set -m 1

and to switch it off use:

mosquitto_pub -h 192.168.0.5 -t pcr12345/outlets/0/set -m 0

As mentioned above, alternate value forms, e.g.

mosquitto_pub -h 192.168.0.5 -t pcr12345/outlets/0/set -m true
mosquitto_pub -h 192.168.0.5 -t pcr12345/outlets/0/set -m on

will also work as expected.