Skip to content

Building Controllers & Creating Entities

Preliminary/Draft

This documentation is in very preliminary draft form. Aside from the obvious formatting and consistency issues, it is likely to contain errors.

A controller in Reactor's terminology is an object that acts as a conduit and translator for data from a remote API or system, and publishes Reactor entities containing the data for use in Rules, Reactions, and Expressions.

This section describes the Controller base class and provides rules and reference material for it and its supporting classes that you may also need to develop Controller subclasses of your own.

Quite literally, a controller is a subclass of the Controller class. This abstract base class defines interfaces and behaviors, and implements default behaviors that would be expected of most subclasses. To create a new controller subclass, one must merely extend the base class and provide the necessary implementation methods.

The following are the expected behaviors and rules for Controller subclasses:

  • It must implement the async start() method, which must return a Promise that resolves when the controller successfully starts. "Starting" does not necessarily mean having successfully published all entities that may be available, but rather that the controller is sufficiently initialized and has started the work of publishing those entities. The start() Promise may resolve before publishing any entities, or wait until all have been updated and published.
  • It may implement the async stop() method, and stop any and all processes, disconnect from any persistent connections, and dispose of all acquired objects and data. If this method is overridden, it must return async super.stop() as its final action.
  • It may implement the run() method, if periodic calls to this method are convenient for the implementation of the controller. The run() method is the executive of a timer tick that can be controlled by the subclass.
  • It must call the base class online() method when it is functioning normally. This call must be made at least once, but may be called as often as needed, even if redundant.
  • It must call the base class offline() method when it can cannot publish updates for its entities for whatever reason (i.e. unable to connect to a remote hub or API). It may briefly delay calling offline(), to allow an opportunity to quickly reconnect a lost connection. It should not delay more than 60 seconds by default, however.
  • It must use the getEntity() method of its base class to create a new entity (i.e. an instance of the Entity class). This will ensure that the Entity object is properly tracked in the system.
  • It must use entity.markDead( false ) on each valid entity during initialization to notify the base class that the entities are still valid (i.e. the device has not been excluded from the Z-Wave mesh, deleted from the API, etc.).
  • It must apply only known, declared capabilities and attributes (i.e. those than can be successfully queried from the Capabilities class/object), or create extension capabilities in the x_ namespace (more on that below).
  • It should, as much as possible, use configuration to drive the translation of source objects to Reactor entities, so that user configuration of new or unexpected source objects might be possible.
  • It must follow further implementation rules for individual class methods and objects as further described in the reference section.

How Controllers Are Loaded and Run

A controller implementation is loaded when an entry in the controllers section of reactor.yaml declares that it uses the class. The class name to be used by that controller instance is set as the value of the implementation key (there are plenty of examples of this in the default config).

At startup, the Reactor core will load the controller implementation subclass. It finds the subclass by looking in the ext directory (same level as config and logs) for a same-named subdirectory containing a same-named JavaScript file. That is, MQTTController is loaded by finding the MQTTController.js file in ext/MQTTController. Once the class is loaded, the Controller base class creates an instance for it (see the getInstancePromise() method).

After all controllers have been instantiated, they are then started by calling their async start() methods. As an async function, this method will (must) implicitly return a Promise to the Reactor core, which the subclass must resolve when it has successfully started. The resolver must return the subclass instance (i.e. this) as its data (or it can return a Promise that eventually resolves to this).

The start() method is expected to do everything necessary to set up a free-running controller. There will be no further intervention from the core until the instance is later stopped (i.e. its async stop() method is called by the core), so the controller must set up any and all timers and other asynchronous functionality needed for its operation.

Your subclass must all implement all recovery behavior for failed connections, etc., including failures at startup. It should keep trying until it succeeds. How often you retry is up to you, but you are encouraged to make it fairly frequent, and preferably configurable by taking the retry interval from the controller configuration (see Controller reference). Many standard controllers like HassController and HubitatController implement decaying retry intervals, where after a certain number of retries at the normal interval (say 12 retries with 5 seconds between), the retry interval doubles on every subsequent retry to a maximum (perhaps 120 seconds). This is friendly to remote APIs especially.

Creating and Managing Entities

The Controller base class provides a framework by which entities can be created and tracked. The Entity class and its subclasses must not be instantiated directly; rather, the getEntity() method must be used. This method will either create a new entity with the given type and ID, or return an existing entity (see also findEntity() in the Controller class reference).

Entities are stored in a persistence cache. When an entity is modified, its cache entry is updated. When Reactor is shut down, the states of all entities are written to the cache. When a controller is instantiated, the base class restores the entities from the cache before calling the subclass' start() method. To identify and track entities that are no longer used, Controller enforces deadmarking — all entities are marked dead when restored, and the subclass implementation is expected to unmark those that are still valid during its initialization (using entity.markDead(false)). It is also highly recommended (virtually required) that the subclass update all entity attributes to current values during its startup.

During startup initialization, the subclass must extend any and all capabilities needed by the entity by calling entity.extendCapability( capabilityName ). This will make the capability available on the entity, and also initialize the attributes of the capability to default values if they do not already exist on the entity. The section below on Capabilities, Attributes and Actions describes where capabilities come from, and how Controller subclasses can define and load their own capabilities.

Controller subclasses may update any entity of theirs at any time. This is usually a simple matter of calling entity.setAttribute( name, value ) on every attribute that needs to be updated (see also the Entity class method setAttributes()). It is safe to assign a value to an attribute that is already present — that is not considered a change to the entity. Other methods such as entity.setName() can be called if the entity's name changes, etc. It is even possible to add or remove capabilities, although this is likely to not be synchronized with the UI without a full browser refresh.

Entities notify observers, other objects in the system and the UI that can watch for changes in entities they subscribe to. A change notification is sent by the base Entity class whenever an attribute or property (name, capabilities, etc.) changes. To prevent multiple changes from sending multiple notifications quickly, the Entity class provides a mechanism to defer updates when large numbers of changes are being made (see the Entity class deferNotifies() method).

Controllers typically do not, and probably should not, dispose of entities during stop(). Controller automatically saves and restores entity states across restarts, and this is important for the Engine and the user's rules and reactions: the Engine can start and initialize all of the rules and reactions with the full complement of dependent entities present, even though their controllers may not yet have established connection to the remote hub or API, for example. This provides faster startup of Reactor and is vital to proper function of the system, even if a controller fails at startup (e.g. can't access its remote hub or API).

Implementation as Data

It is often the case that a controller will have a large set of complex rules for turning objects from the remote hub or API into entities. In this case, it is preferable to use a data-driven approach, where the Controller subclass becomes more of a rules engine driven by data to perform these conversions. This is the way all current core controllers that interface to hubs operate. It is not required to use this mechanism, but for large numbers of varied and complex objects, it is highly recommended.

Controller provides a standard mechanism for storing and loading this data. The loadBaseImplementationData( name, dir ) method is available to load two data files, one named <name>_capabilities.yaml and one named <name>_devices.yaml. The former describes the additional capabilities needed by your Controller subclass. All subclass-specific capabilities are referred to as extension capabilites and must start with x_<controllername> (e.g. MQTTController uses x_mqtt as a prefix for its capabilities, and HubitatController uses x_hubitat as its prefix). The capabilities defined must reside within a capabilities key in the data file, which is the only key used by that file; these capabilities are automatically loaded into the runtime capability catalog (i.e. Capabilities class/instance) when loadBaseImplementationMethod() is called. The only other keys required in this file are version, revision, and format, where version is a version number applied to the file (I use two-digit year and Julian day of the year, like 22056 for the 56th day of 2022), revision is a revision number within the version (so if several changes are made in a day, revision starts at 1 and increments with each commit/change), and format is for later use and may either be omitted or, if specified, must be specified as 1 (digit one).

The <name>_devices.yaml file is used to contain your subclass-specific data for mapping source objects into Reactor entities. There is no definition for the form or content of this data, it is entirely up to you and your subclass. However, the version, revision, and format keys must exist and have the same requirements and semantics as those described for the capabilities file, above. When loadBaseImplementationData() is called, it will return to you the object that is the parsed data from this file, in its entirety. Do with it what you will.

No Code

For security reasons, it is required that the devices file never contain JavaScript code that your subclass may execute via eval().

Most of the core controller implementations use the data mapping method, and make extensive use of the expression parser to assist in mapping and converting data values. To give you an idea of how data and the devices file work together to drive an entity, let's look at a specific example from HassController. For a binary switch, Home Assistant will send an object like this during the inventory phase in response to HassControllers request for all objects:

{
    "entity_id": "switch.power_switch",
    "state": "off",
    "attributes": {
        "friendly_name": "Prusa Power Switch"
    },
    "last_changed": "2022-10-08T15:04:19.386098+00:00",
    "last_updated": "2022-10-08T15:04:19.386098+00:00",
    "context": {
        "id": "01GEW20ANTF8PBR62EB50YHPZ0",
        "parent_id": null,
        "user_id": null
    }
}

HassController, at initialization, takes each object from Home Assistant and tries to find a matching entity-mapping entry in the device data file, which looks like this (this entry is one of dozens of similar for different device "fingerprints"):

mapping:
  - match_domain: "switch"
    capability:
      - extend:
          - power_switch
          - toggle
    type: Switch
    primary_attribute: power_switch.state

HassController looks through all of the mapping entries and uses match_domain and other keys to see of the object from Home Assistant matches the mapping criteria. In this case, it would, so HassController then uses the other keys in this definition to know what capabilities to extend to the entity, what type the entity is, and what primary attribute the entity should have.

Further data in the devices file tells HassController how to drive the attributes in the power_switch capability, and how to make the actions work:

implementation:
  power_switch:
    attributes:
      state:
        source: state
        map:
          'off': false
          'on': true
        map_unmatched: null
    actions:
      # Use the most generic service
      'on':
        hass_service: homeassistant.turn_on
      'off':
        hass_service: homeassistant.turn_off
      'set':
        remap:
          - condition: bool( parameters.state )
            action: power_switch.on
          - action: power_switch.off
  toggle:
    actions:
      toggle:
        hass_service: homeassistant.toggle

Here, HassController is told the power_switch.state attribute is determined (via the key named by source) that the state key from the Home Assistant response object has the current value of our switch state. The map entry that follows is something HassController knows how to use to turn the words off and on (seen in the state of the object) into the boolean false and true values required by the power_switch.state attribute. The actions section tells HassController what Home Assistant needs to be told to do when each action is invoked.

Example Only

Remember that this is just an example to illustrate how data-based rules are used in preference to hard-coding in many of Reactor's native controllers. It is not a guideline for how you should do it. Your choices should be driven entirely by your subclass' needs.

Using data in this way prevents hard-coding of such mappings in your controller. An added benefit is that it's possible to distribute a ZIP file containing updated versions of the data files to improve mappings without the need to publish a new version of the controller. If the ZIP file is placed in the user's config directory, loadBaseImplementationData() will know to use it in preference (maybe) to the versions included with the controller install. It checks version and revision and makes sure the more recent of the ZIP or installed file is used, so it's safe for the user who may forget to remove the ZIP file after upgrading Reactor or an extension controller (the user won't end up using old data with a new version of the controller subclass).

Capabilities, Attributes and Actions

Controllers that create entities will need to declare on those entities which capabilities the entity supports. The system_capabilities.yaml file (located in server/lib in the Reactor install) is the catalog of system-defined capabilities. The most common on these are documented here.

Controllers may also define their own capabilities, and should when data is not represented by the system-defined capabilities. It should generally be an objective of your controller to present as much data about the source objects and possible, to avoid restricting the user from access to it. You can see this philosophy play out in the various hub Controllers like VeraController, where every state variable on a Vera device is represented in the x_vera_svc... capabilities and attributes.

Capabilities define attributes and actions, although it is not required that a capability have either (i.e. it can have no attributes or no actions). The binary_sensor capability, for example, has a state attribute, but has no actions. Its related capability security_sensor, however, defines attributes for its state and its arming state, and actions to set its arming state. The toggle capability has a single action and no attributes.

Controllers must use any and all available system capabilities that align with available data and actions. This is, at the moment, a highly subjective area, but it should go without saying that if your controller is interfacing with an object/device it knows to be a motion sensor, then the entity in Reactor should implement the motion_sensor capability. User expectation is at work here. When in doubt, that's the first question to ask: what would the user expect? Further, the semantics of all system capabilities used must follow their definition (see Standard Capabilities).

Some entities may implement a part of a capability; for example, a media player may have the ability to play and pause, but not stop. In this case, the Controller implementation can choose to declare the stop action unsupported, or it can emulate the stop action by handling it in the same manner as pause. The Controller base class has a couple of these implemented as defaults as well: if an entity with the dimming service doesn't support up and down, the base class will emulate it using set and an offset applied to the current dimming.level on the entity; if an entity supports power_switch but not toggle, the Controller class will emulate it by examining power_switch.state and performing power_switch.off or power_switch.on as needed. Again, go with user expectation: if an action can be implemented, it should be.

Likewise, not all attributes of a capability may be supported. In this case, it is generally preferable to leave them null, unless it is reasonable, logical, and deterministic to emulate them. Again, manage the user's expectations.

When no system capability aligns with your Controller's need, an extension capability can be defined. Extension capabilities must begin with x_<controllername>, thus defining a namespace that avoids collisions with system capabilities and the extension capabilities of other controllers. For example, all extension capabilities defined by MQTTController are prefixed x_mqtt, while all those required by HubitatController are prefixed x_hubitat.

The extension capabilities defined by a Controller may be static or dynamic. Static capabilities, those that are known to be needed and can be defined before runtime, are usually defined in a Controller-specific capabilities file (<controllername>_capabilities.yaml) that will be loaded when your Controller subclass calls loadBaseImplementationData(). Dynamic capabilities, those that could only be generated from information gathered at runtime, can be defined by calling Capabilities.loadCapabilityData() in runtime.

Packaging Controllers

Actually, Don't Do This... Yet

There are still deployment issues to be worked out with how npm packages work in this context. Ignore the strikethough parts of this section for now. For the time being, I recommend using GitHub to publish your Controller subclass. Please publish clear installation instructions for your users in the repository's README.md file.

The best way to package your controller is as a standard npm package. As a convention, "official" Reactor controllers use reactor- as the prefix for the package name; for community-developed/contributed controllers, please use reactor-contrib- as a prefix. For example, reactor-contrib-modbuscontroller.

There are multiple tutorials online on how to build an NPM package, so those details won't be repeated here. But there are a couple of specifics you need to know:

  • The main key in your package.json file must point to the controller class file.
  • The controller class file should be named using the Reactor Controller convention: Controller (e.g. VeraController, HubitatController, or building on the example above, ModbusController.
  • Your package must specify all dependencies not otherwise available in the default Reactor execution environment (basically express, node-fetch, and ws).

Here is what the MQTTController package looks like when installed:

$ ls ext/MQTTController
0-README.txt  config-example.yaml  mqtt_capabilities.yaml  mqtt_devices.yaml  package.json
CHANGELOG.md  install.sh           MQTTController.js       node_modules

Please publish your package to npmjs, so that customers can install it by running npm i <package-name> (ideally, in their home directory, rather than in the Reactor install directory, so that the package install survives Reactor upgrades).

It is recommended that your controller have a package.json file even if you do not publish it on npmjs.com. The information in this file is used to assist the installation of dependencies (i.e. npm i --no-package-lock --omit dev by your user will install all dependencies listed in the file), and provide information about your controller that is displayed in Reactor's About page. At a minimum, your package.json should define name, version, description, author, and license, in addition to any dependencies. You can also specify homepage with a URL that will be shown as a link next to your package information on the About page. To avoid confusing users, it's a good idea to ensure that the version in package.json bears some relation to the const version value in your controller class file.

As an example/template, here is the package.json file for a recent version of the example WhiteBitController:

{
  "name": "reactor-whitebit",
  "version": "0.1.22305",
  "description": "Reactor Add-on Demo/Sample Code: WhiteBit Interface",
  "homepage": "https://reactor.toggledbits.com/docs/",
  "private": true,
  "author": "Patrick Rigney/Kedron Holdings LLC",
  "license": "MIT"
}

A package.json file will be built for you if you run npm init. You can add dependencies to your package by running npm install <package> --no-package-lock. Refer to the documentation for the npm command-line tool for more information.

Finding Files and Paths

In your Controller implementation, generally speaking __dirname will refer to the path of the current source file. But it is inappropriate to use this value when constructing paths to find other elements of the system in Reactor, like configuration files or log files. This would fail under docker, for example, where unlike bare-metal installs, their location is under a separate root that is bind-mounted to a virtual directory not in the Reactor install tree. And although it is little-known and little-used, the paths for configuration files, log files, data storage, etc. are all user-configurable in reactor.yaml, so there really is no correct way to construct them from __dirname.

To assist in finding the correct directories regardless of platform, install method, or user configuration, when Reactor starts it figures out where these directories are, and makes them available through the Configuration class. Using Configuration.getConfig( "reactor.basedir", "string" ), for example, will give you the base directory path (that is, where Reactor is installed). Here are the available paths:

  • reactor.basedir — install directory for Reactor;
  • reactor.confdir — configuration file directory;
  • reactor.logsdir — directory where log files are written;
  • reactor.plugindir — directory where extensions are installed.

You are strongly encouraged to use path.resolve() and path.join() exclusively rather than string concatenation when constructing pathnames/filenames, as these functions will correctly construct a path for the running platform. Differences in file systems are abstracted for you by these functions: Windows uses \ as a directory name separator, where Linux uses /, and some future platform could use anything else. If you must know the directory separator, use path.sep rather than hard-coding a character.

Loading Data from JSON/YAML

See the util.loadData() function to load a JSON or YAML data file from a search path you can provide (or your install directory), if Controller.loadBaseImplementationData isn't enough. Controllers like ZWaveJSController use this function to load per-manufacturer/device data from a manufacturer subdirectory in its install directory, which is more convenient that having a monolithic, large zwavejs_device.yaml data file alone.

Don't Rely on Reactor's Installed Directory Structure

You must not rely on (what you assume to be) Reactor's installed directory structure. That is, you must not assume that reactor.confdir is the same as reactor.basedir concatenated with /config. Always request reactor.confdir to get the correct directory. Further, you must not assume that any directories or files exist other than those documented and available in server/lib and common for loading class dependencies/public core modules. This will assure maximum portability of your work without the need for coding per-platform exceptions.

Using MQTT in Controllers

The MQTTController extension provides a mechanism by which other controllers can subscribe to and publish topics. This frees your controller from managing its own connection to the broker. See the documentation for MQTTController for details on how to use these facilities.

Class Reference

NOTE: Any class not described in this section is considered a private system implementation class and must not be used. The behaviors, APIs, names, and future existence of these private classes are not guaranteed. Use only the classes documented below, in the manner prescribed.

class Controller

Controller is the base class for all controllers in Reactor.

Controller implements the Observer and Observable interfaces (see below).

constructor()

The subclass constructor must call the superclass constructor, passing all arguments received in the same order received. Thereafter, it may declare and initialize any data it requires for its implementation. It should not, however, initiate any communication or begin its entities (except that the superclass constructor will create some system-defined entities by default, and this is normal/allowed).

The constructor is not called directly. It must only be called by the Controller.getInstancePromise() method, which itself must only be called by the system's Structure object.

async start()

Arguments: None

Returns: this (via a Promise, either explicitly or implicitly).

The start() method is called by the Structure when a configured Controller subclass object is required to start. This may be at any time after its creation by getInstancePromise().

  • Subclasses must override this method;
  • Subclasses do not need to call super.start();
  • Subclass overrides must return a Promise, either explicitly or implicitly (i.e. because async);
  • The Promise must resolve to this (the Controller instance itself).

This method is expected to take all necessary steps to initiate communications with remote APIs, or do any other work needed to begin the process of publishing entities. It must return a promise that resolves when successful. It may define success as completing publishing all known entities, or it may at any other time, such as following an asynchronous HTTP request but before the response is received and parsed.

It is recommended that all expected entities be published if possible and the time required to do so is brief (a few seconds). If the process may be protracted and lengthy, the Promise should resolve earlier and the entities published as discovered.

async stop()

Arguments: None

Returns: this (via a Promise, either explicitly or implicitly).

The stop() method will be called by the Structure when the application is shutting down.

Subclasses may override this method. If overridden, the override:

  • must return a Promise, either explicitly or implicitly (i.e. because async);
  • must resolve the Promise to this (the Controller instance itself);
  • must properly clean up and release/destroy all objects, private data, etc. that it created;
  • must call the superclass method, and should either await its completion (if async), or if final in the method, return the Promise returned by the superclass method (i.e. return await super.stop()).

init()

Arguments: None

Returns: None

The init() method will be called before a scheduled timing delay period begins. The default implementation does nothing, but subclasses may override this method to perform any tasks required prior to going into a period of "sleep", effectively.

DEPRECATED. It turns out this method is never used; initializations made prior to timing events usually are handled in other places like the constructor or start(), and this method name is misleading and may lead one to believe it's part of the subclass initialization that's called only once. In fact, it's just part of the timer subfunction and will be called many times, so a misunderstanding of its function could lead to some really confusing behavior.

run()

Arguments: None

Returns: None

The run() method is called when a placed time delay (see startDelay(), startInterval(), and startDaily()) is met. The default implementation is empty. Subclasses are only required to provide an implementation if the Controller (base class) delay mechanism is used.

NOTE: I'm considering deprecating the Controller delay/timer mechanism, which is a vestige of the foundation layer's history as part of my dashboard. I'm thinking it's better to use Timer objects sourced from TimerBroker. For the moment, the legacy mechanism stands, as most of the current controllers use it, for better or worse.

async loadBaseImplementationData( namespace, dirname )

Arguments:
namespace — (string) the lowerbase basename of your controller.
dirname — (string) the directory in which the data files are found.

Returns: an object containing the implementation data loaded from the data file.

Loads two data files from the controller's installation directory: the controller-specific capabilities (i.e. extension capabilities), and the controller's implementation data for rules-based implementation.

The namespace should simply be a lower-case version of your controller name. For example, MQTTController uses mqtt, and ZWaveJSController uses zwavejs. This will be used as the prefix for the data files, which are expected to be named <namespace>_capabilities.yaml and <namespace>_devices.yaml.

The dirname is an unfortunate necessity of the way nodejs loads modules; it should always and only be specified as the node constant __dirname. See the example code below.

This method also looks for a ZIP file in the user's configuration (config) subdirectory of the name <namespace>_data.zip. If found, it is opened and its contents examined. If it contains a capabilities file, the version and revision of which are higher-numbered than the files included in the controller's install directory, the file from the ZIP archive is used in preference. The implementation data (<namespace>_devices.yaml) file is handled the same way. This allows the easy distribution of updated capabilities and mapping rules without the need to release an entire new version of your Controller. In addition, it is safe for the user: if the user later upgrades to a new version of your Controller with even newer data files, those in the ZIP file will then be ignored. That is, whichever source, be it the install directory or the ZIP archive, has the newest data, that's the data that is used.

The format of the capabilities file is simple. It must contain a capabilities key whose value is an object. The keys of that object are capability names, and the values are the capability objects (see the form defined in Capabilities.loadCapabilityData). The only other keys required in this file are version, revision, and format, where version is a version number applied to the file (I use two-digit year and Julian day of the year, like 22056 for the 56th day of 2022), revision is a revision number within the version (so if several changes are made in a day, revision starts at 1 and increments with each commit/change), and format is for later use and may either be omitted or, if specified, must be specified as 1 (digit one).

# This is an example <namespace>_capabilities.yaml file
---
version: 22284
revision: 1
format: 1

capabilities:
  x_foobar:
    attributes:
      state:
        type: string
    actions:
      do_it:
        arguments:
          what:
            type: string
          when:
            type: ui8
          how_much:
            type: int

The <name>_devices.yaml file is used to contain your subclass-specific data for mapping source objects into Reactor entities, if your Controller uses data-driven rules for this purpose. There is no definition for the form or content of this data, it is entirely up to you and your subclass. However, the version, revision, and format keys must exist and have the same requirements and semantics as those described for the capabilities file, above. When loadBaseImplementationData() is called, it will return to you the object that is the parsed data from this file, in its entirety. Do with it what you will.

# This is the standard preamble for <namespace>_devices.yaml
---
version: 22284
revision: 1

# Whatever data you want here...

Typically, the base implementation data and extension capabilities only need to loaded once. The capabilities data loaded is automatically shared by all system objects that need it. The implementation data loaded from the devices file also usually applies to all instances of your Controller (if there are multiple in the user's configuration), so saving the RAM by keeping only one copy is a good idea. The following illustrates the typical loading of implementation data used by Reactor's core Controllers:

var impl = false;

class MQTTController extends Controller {
    // ...

    async start() {
        if ( false === impl ) {
            impl = await this.loadBaseImplementationData( 'mqtt', __dirname );
        }
        // etc...

Here you see, when an instance of the Controller gets called to start, it will check if the implementation data has been loaded. The impl variable is defined at the module level (outside the class definition), so it applies to all instances of the class. With this mechanism, the first instance to be started loads the implementation data, and from there, all instances benefit from its single presence.

Take special note that since loadBaseImplementationData() is itself an async function, we must use await to ensure that we wait for its completion and assign the return value to the module-level variable.

Also, there's a little trick at work here. The impl module variable is initialized to (boolean) false. The test in start(), rather than simply using if ( ! impl ), more specifically matches to the boolean type and the value false using ===. That is because, if the implementation data cannot be loaded or isn't used/available, loadBaseImplementationData() will return undefined, which will then become the value of impl. Future calls to start() (including calls by other instances) will then not match the if ( false === impl ) test (because undefined is not boolean false), and so they won't continuously try to load data that isn't there on every call.

notify( event, ... )

Arguments: event - an classless object containing the event data.

Returns: None

The notify() method satisfies the implementation of the Observer interface (see below). This method is called when a subscribed Observable object publishes a message.

The default implementation simply propagates the event to any observers of the controller (usually the Structure, but also other system objects such as the WebSocket API). Since the controller is by definition subscribed to all of its managed entities, any update to an entity results in a notification to the controller, which is then passed up by this method. Thus a subsystem may watch any or all entities of a controller without having to know they exist and subscribe to each individually.

An event contains the following key fields: source (the object sending the event), type (the type of message, e.g. entity-changed), and data (additional data about the event).

This method is not normally overridden. It's default implementation is usually sufficient for all purposes. If, however, a subclass finds reason to override it, the override must call the superclass method. This is vital to proper event processing for the controller's entities; any override should take special care that the superclass method is called no matter what kind of error occurs otherwise in the subclass override (e.g. in finally of an all-enclosing try block).

Overrides must always return super.notify(...)

If you find a need to implement notify() in your subclass, your override method must return super.notify( event ) as its final action, no matter what other actions your override performs, or what errors it encounters. Failing to do so will interrupt event processing for entities throughout the entire system and cause it to stop running rules and responding to their conditions, updating the UI, etc. See the foregoing paragraph for details.

getSystemEntity()

Returns the Controller's system entity.

findEntity( id )

Arguments:
id — (string) the ID of the entity to be found.

Returns: the located Entity object, or undefined

This method finds and returns the Entity with the given ID. The Entity must be owned by the controller (this). The argument is not a canonical ID, but rather the controller-local ID. If no entity matching is found, undefined is returned.

  • This method must not be overridden.
  • This method must be called in preference to accessing this.entities from the base class directly.

getEntity( className, id )

Arguments:
className — (string) name of Entity class to use if the object needs to be created;
id — (string) local ID of the Entity class to be found, or assigned if created.

Returns: the found/created Entity object.

This method is the only valid way to create an Entity object, and must be used exclusively for that purpose. Direct construction of Entity objects is not permitted.

If the entity is created, the new object is automatically added to the controller's list of managed entities, and the controller is subscribed to the entity. The new entity also has change events deferred, so your implementation must at some point call entity.deferNotifies( false ) to reverse this. See Entity below.

When creating entities, entity IDs must contain only alphanumeric (A-Z, 0-9) characters and underscore (_), and be between 1 and 128 characters in length. Other characters, including Unicode, are not permitted. Entity IDs are case-sensitive.

removeEntity( entity )

Arguments:
entity — (Entity) the Entity object to be removed.

Removes the specified entity and destroys it. The entity will be unsubscribed from all Observers in the process.

async performOnEntity( entity, actionName, parameters )

Arguments:

entity — (Entity object) the entity on which to perform the action;

actionName — (string) the action name, in "capability.action" form (e.g. "power_switch.on")

parameters — (classless object, optional) the parameters required by the action

Returns: Promise (explicitly or implicitly)

This action, which may be overridden by subclasses, is expected to do what is necessary to perform the requested action on the specified entity. This method must return a Promise that resolves when the action request has been sent to the remote system. It may defer settling the Promise until the request is acknowledged or not by the remote system (recommended); it further may defer settling until a remote service completes the request and returns its status or update (not recommended).

Subsystems outside of the Controller branch must not call this method. The correct way to perform an action on an entity is to call perform() on the entity itself (see Entity below).

By default, the base class method will look for a method named action_actionname( entity, parameters ), where actionname is in the form capability_action (the usual dot separator between the capability name and action name are replaced with an underscore). If found, this method will be called with only entity and parameters arguments (the action name is implied by the method name so not passed). All action_ methods declared by Controller subclasses may be declared async and must return a Promise (explicitly or implicitly).

The action_methods are not intended to be the primary mechanism by which actions are handled in a Controller subclass. That is, they are not expected to provide what would likely be a long list of action_ methods for every capability and action required by any entity they support. Rather, a subclass override of performOnEntity() is more commonly used to implement rule-based mapping of actions to whatever the remote interface requires, the mapping being data-driven by implementation hints loaded using loadBaseImplementationData(), and then a few, if any, action_ functions used only for those actions that are too complex to implement in data-driven rules. Few core Reactor Controllers use action_ functions at all, and the ability of most of these Controllers to implement their complex behavior almost entirely in data and avoid hard-coding is matter of pride to me. However, there are cases where their use has merit.

getID()

Arguments: None

Returns: (string) the ID of the Controller, as given in the controllers configuration section.

This method must not be overridden.

toString()

Arguments: None

Returns: (string) returns a string with the subclass name and controller ID (e.g. "HassController#home")

This method must not be overridden.

isEqual( obj )

Arguments:
obj — an object, presumably another Controller instance.

Returns: (boolean) true if obj is the same controller as the one on which the method is called; false otherwise.

getConfig()

Arguments: None

Returns: (classless object) the configuration object passed to the Controller when it was created. This must be used in preference to accessing this.config (Controller private data) directly.

  • This method must not be overridden.
  • The subclass may modify data in the returned object, and those changes will be persistent, but it is not required for the Controller base class to honor any changes made (the base class usually does everything it needs to do with config during construction, so there's no opportunity to modify values before or during in a way that the base class constructor would see).

getLogger()

Arguments: None

Returns: (Logger) the logger established for the controller.

This method must not be overridden.

For brevity, most controller implementations simply use this.log, accessing the base class property directly. While this isn't perfect style, it's so practical that this will probably never change.

    this.getLogger().debug( 5, "This is a debug message" );

    this.log.debug( 5, "This is also a debug message" );

getStructure()

Arguments: None

Returns: (Structure) the structure object that created the controller.

This method must not be overridden.

isReady()

Arguments: None

Returns: (boolean) whether the controller is currently online.

This method must not be overridden.

extract()

Arguments: None

Returns: (classless object) an object representation of the class data

This method is used by the various APIs to transmit the controller data to a client. It creates a classless-object representation of the controller's data.

This method must not be overridden.

serialize()

Argument: None

Returns: (string) JSON string representing the controller data.

This method is used to create a JSON representation of the controller. It is effectively implemented by returning JSON.stringify( this.extract() ).

This method must not be overridden.

isRunning()

Arguments: None

Returns: (boolean) true if a timer is in effect; false otherwise;

This method must not be overridden.

online()

Arguments: None

Returns: Nothing

This method is used by Controller subclasses to signal the base class that the subclass is operational, or not.

This method must be called by a subclass at least once. This method must not be overridden.

offline()

Arguments: None

Returns: Nothing

This method is used by Controller subclasses to signal the base class that the subclass is not operational (e.g. it has lost a connection to a remote data source, etc.).

Requirements:

  • This method may be called when communications or other source strategies fail;
  • This method may be called after a reasonable number of retries/small delay during retries;
  • This method must not be overridden.

startDelay( milliseconds )

This method starts a delay on the Controller legacy timer of the specified number of milliseconds. After at least that many milliseconds (i.e. timing not guaranteed), the run() method will be invoked. Thereafter, no timer will be in effect. The implementation of the run() method may call startDelay() again, or another timer function, to reinstate timing.

The Controller legacy timing mechanism supports only one timer at a time. For this reason, this mechanism is deprecated. Use Timer objects from TimerBroker instead.

This method must not be overridden.

startInterval( milliseconds )

Starts the legacy timer with an interval of milliseconds. The run() method, which you should override, is called on that interval until stopTimer() is called or the Controller itself is stopped (i.e. stop() is called). The interval can be cancelled at any time by calling stopTimer().

The Controller legacy timing mechanism supports only one timer at a time. For this reason, this mechanism is deprecated. Use Timer objects from TimerBroker instead.

This method must not be overridden.

startDaily()

DEPRECATED — Do not use.

This method must not be overridden.

stopTimer()

This method stops the Controller legacy timer. If the legacy timer is not running, it returns without action or error.

This method must not be overridden.

run()

This method will be called when the legacy timer expires, to handle any period actions required by your subclass.

This method is synchronous, has no arguments, and returns no value.

If triggered by the end of an interval started by startInterval(), Controller will issue a warning to the log if your run() method takes longer to execute than the interval (i.e. it puts your intervals behind schedule).

tick()

This is an internal private implementation method.

This method must not be overridden. Override the run() method to handle legacy timer expiry.

fetchText( url [, options] )

Arguments: url - URL to be fetched; options - (optional) object containing option key/value pairs.

Returns: Promise that resolves to response text.

This is a convenience method to help streamline subclass implemenations. It returns a Promise that resolves when the specified url has been fetched. The resolution object is a text string containing the entire response from the server.

This method must not be overridden.

The options argument is an object that contains the following known keys/values:
timeout — (integer > 0) the timeout period for the request, in milliseconds.
reactor_ignore_certs — (boolean) for HTTPS requests only, if true the certificate of the server will not be verified (e.g. for connecting to servers using self-signed certificates).
reactor_username and reactor_password — (strings) for requests requiring authentication, the username and password.
reactor_basic_auth — (boolean) for requests requiring authorization, true HTTP Basic authentication, or false (default) for HTTP Digest authentication.
all other values are passed through to node-fetch, such as method, body, headers and so forth. Please refer to the node-fetch documentation for details.

fetchJSON( url, [, options] )

Arguments: url - URL to be fetched; options - (optional) object containing option key/value pairs. See fetchText for meaning.

Returns: Promise that resolves to an object containing the parsed JSON response.

This is a convenience method to help streamline subclass implemenations. It returns a Promise that resolves when the specified url has been fetched. The resolution object is the parsed JSON data of the response.

This method must not be overridden.

getNotificationURL()

Arguments: None

Returns: (string) URL at which the application can receive HTTP requests from a remote system and pass them to the controller instance.

The system offers a method by which, if necessary, a remote system may send a message to a Controller instance asynchronously via HTTP (i.e. a callback). This method returns the constructed URL at which the system is prepared to receive such requests. All request methods are allowed and passed through.

The URL is of the fixed form http://reactor-ip-address:reactor-port/api/v1/notify_controller/id, where id is the ID of the target controller. Requests to this URL are passed to the controller by the HTTP API calling getNotificationMiddlewre() and executing the function chain it returns.

See getNotificationMiddleware(), below.

This method must not be overridden.

getNotificationMiddleware()

Arguments: None

Returns: (array) An array of Express-compatible middleware functions to be used in handling the request.

The HTTP API and Controller subsystems provide a mechanism by which a remote endpoint may initiate a connection to the Reactor HTTP API and send a message to a specified controller. The URL at which this is done is created dynamically and must be fetched by calling getNotificationURL(). When the HTTP API receives a request at this URL (any method), it determines which controller the message is for, and then calls that controller's getNotificationMiddleware() method. The default (superclass) implementation simple returns boolean false, indicating that the request should not be processed (a 404 is returned to the requestor). Otherwise, the return value is expected to be an array of Express-compatible middleware used for processing and handling the request.

Without going into too much detail about the operation of Express (refer to its documentation separately), the array of middleware that is returned is run in sequence until one of the middleware functions handles the request. Typically, a subclass method is the last element in the array, in the form this.subclass_method_name.bind(this). It may be preceded by one or more other middleware methods, in particular, any of the Express-included middleware. For example, a common set for processing a JSON payload posted from a remote system might be [ express.json(), this.subclass_method_name.bind(this) ].

Since the subclass method follows the design pattern of Express middleware, it receives three arguments: request, response, and next. The request argument is the Express Request object for the request. The body of the request can be accessed using request.body(). The response argument is the Express Response object. It must be used, at a minimum, to define the response to the remote endpoint: response.status(200).send("OK"). The next argument is a function that can be called if the current middleware function wishes to allow any subsequent middleware in the array to be run (i.e. the request may have been modified, but is not completely handled). Usually, Controller subclass implementation middleware functions will be last in the array, and thus may ignore and not call next(), which indicates that it has handled the request and no further work other than sending the required response is to be done.

For reference, look at the implementation of HubitatController, which is the reason this mechanism exists.

This method must not be overridden.

sendError( message, ...args )

sendWarning( message, ...args )

sendNotice( message, ...args )

Arguments:
message — (string) Text of message
...args - additional arguments, comma-separated as usual, for values used in substitutions.

Sends an error/warning/notice alert. This allows your Controller instance to alert the user to specific conditions you need them to see.

The message string can perform substitutions to bring values in from the argument list. These substitutions work as described in the Localization section, and will not be described in full detail here. The Cliff's Notes version is that substrings of the form {n}, where n in a number starting from 0, will substitute the nth argument value into the string. For example, sendNotice( "I say {0} {1}", "Hello", "World" ) would send the result string I say Hello World as a notice-level alert. There are specific formatting options that can also be used, as further described in Localization.

These methods must not be overridden.

getInstancePromise()

This method is used to create an instance of a Controller subclass, and is the only way. It is used by the Structure object during startup, and must not be used otherwise. It keeps the instance on a map indexed by controller ID, assuring that only one instance with that ID can exist.

This method must not be overridden.

WebSocket Subsystem - ws_ methods

The WebSocket subsystem provides additional methods on Controller to manage a single connection to an endpoint. It simplifies the process of creating and managing WebSockets in Controller implementations, handling connection and upgrade, ping/pong, errors, and closure. Received messages are passed to a subclass method for handling.

To use the WebSocket subsystem, the subclass will usually:

  • Call ws_open to open a connection to a server;
  • Implement a ws_closing() method override to be notified when the connection is closing for any reason;
  • Implement a ws_message() method override to handle incoming messages from the server;
  • Call ws_send as needed to send data to the server;
  • Call ws_close as needed to close the connection to the server.

The async ws_open( url, options ) method is used to open a WebSocket connection to a given URL. It returns a Promise that resolves when the connection succeeds, or rejects if the connection fails. The options argument is an object (dictionary) that currently has only three keys available: connectTimeout — the maximum time (integer ms) to attempt connection; ignoreCertificatestrue/false (boolean) to ignore certificates when making secure connections to servers with self-signed certificates; and pingInterval — the maximum interval (integer ms) between pings on the connection. This method must not be overridden.

The async ws_close( code, reason ) method can be called by your subclass to close the WebSocket. This would normally only be done as part of a stop() implementation, and the base class will do it if your subclass does not. This method returns a Promise that resolves when the connection is fully closed. This method must not be overridden (see ws_closing() below if you need to do things when the socket closes).

The more urgent ws_terminate() is also available, which summarily slams the connection shut with no fanfare. This method must not be overridden.

The ws_send( payload ) method of the base class can be called to send payload data (String or Buffer) to the endpoint. This method must not be overridden.

The ws_connected() method returns (boolean) true if the WebSocket is currently connected; false otherwise. This method must not be overridden.

Your Controller subclass must implement a ws_message( data ) method, which is called when data is received on the socket. No interpretation or transformation of the data is performed; it is handed to your implementation exactly as it is received (text or binary).

Your subclass may also implement ws_closing( code, reason ), which is an advisory method to notify your subclass that the connection is being closed. The connection can close at any time for any reason. The most typical use of this method is to start a delay timer that, when expired, attempts reconnection to the endpoint, which is not automatic. The state of the connection when called is undefined, so implementations must not send any final messages or perform any other functions (e.g. ping) on the WebSocket connection.

Note

It is very common for this.offline() to be called in response to a close, so that other parts of the system know that the controller is offline. Often, however, some hysteresis is desirable, and the call to offline() is made elsewhere after several consecutive attempts to reconnect have failed. The default implementation of ws_closing() calls offline(). If you don't want this, just don't call super.ws_closing() from your override method.

class Entity

Entity implements Observable and publishes entity-changed events when its properties or attributes are modified. The entity-delete event is sent before an entity is destroyed.

private constructor

Entity's constructor is private and must not be called directly. The (only) correct way to retrieve, and if necessary create, an instance of an entity is to use Controller's getEntity() method. This ensures that there is exactly one object that represents an entity with a given ID for a given controller.

static isValidID( str )

Arguments:
str — a string to validate as an Entity ID.

Returns: (boolean) true if the string is valid entity ID; false otherwise.

Valid entity IDs are one or more characters in length and contain only alphanumeric characters (A-Z and 0-9) and underscore (_).

getID()

Returns the controller-local ID of the entity.

getCanonicalID()

Returns the canonical ID of the entity. An entity's canonical ID is the ID of its parent controller and its local ID concatented with a > (greater than) between (e.g. vera>device_188 or hass>switch_bathroom_light).

getName()

Returns the name of the object.

isEqual( obj )

Arguments:
obj — another object, presumably another Entity instance.

Returns: (boolean) true if obj is the same entity as that on which the method is called; false otherwise.

markDead( isDead )

Arguments:
isDead — (boolean) true if this entity instance is dead, false otherwise.

deferNotifies( defer )

Arguments:
defer — (boolean) new state of notification deferral (true means defer notifications, false means send them immediately as the entity changes).

Returns: (boolean) the previous state of entity deferral.

Every change to an Entity causes an event to be published, by default. But typically, during inventory of a hub by a Controller subclass, for example, an entity will need to have many attributes and properties set, and it would be undesirable to have a large number of events sent; one event for the entire setup would be sufficient (and desirable). When deferNotifies( true ) is called, publication of change events is suspended until deferNotifies( false ) is called later (at which point, a single change event is sent, if changes were made).

deferNotifies() returns the prior state as the function value. This can be saved and used to pass to a later deferNotifies() call in situations where there may be nested functions, each of which may or may not defer notifications based on their implementations.

setName( name )

Set the entity name. If different from the current name, a change event will be published.

extendCapability( capability_name )

Extends the named capability. It is safe to call this method if the entity is already extended to the entity (no action occurs).

A change event is published if the entity's capability list is modified.

hasCapability( capability_name )

Returns true if the named capability is extended to the entity; false otherwise.

setAttribute( attribute_name, new_value )

Sets the value of an attribute. A change in value updates the metadata for the attribute.

A change event is published if the attribute's value is modified (i.e. the new value is different from the current value).

setAttributes( dict [, default_capability ] )

Arguments:
dict — (object) Object whose keys are attribute names and values are the attribute values to be assigned.
default_capability — (optional, string) name of the default capability for the assignments.

Sets a number of attributes at the same time. The keys of dict can be fully-qualified attribute names (e.g. power_switch.state, dimming.level), or can be just attribute names. Any key encountered that is just an attribute name will be assumed to be defined by the named default_capability, so if simple names are used as keys, the default_capability argument must be supplied.

Example:

/* Set three attributes in the rgb_color capability, and the dimming level and power state */
setAttributes({
    'red': 255,
    'green': 0,
    'blue': 128,
    'power_switch.state': true,
    'dimming.level': 0.5
}, 'rgb_color');

/* The example below is invalid, because default_capability is not specified when simple names are used */
setAttributes({
    'hue': 30,
    'saturation': 1
});

A change event is sent if any attribute is modified by this action.

getAttribute( attribute_name )

Returns the current value of the named attribute.

hasAttribute( attribute_name )

Returns true if the named attribute (in capabilityname.attributename form) is available on the entity.

getAttributeMeta( attribute_name )

Returns the attribute metadata for the attribute.

setPrimaryAttribute( attribute_name )

Arguments:
attribute_name — (string) canonical name (capability.attribute form) of the new primary attribute

The capability owning the attribute must have been previously extended to this entity or an exception will be thrown.

A change event is sent if the primary attribute is changed.

setPrimaryValue( new_value )

Arguments:
new_value — (varies) new value to be assigned to the primary attribute

Sets the value of the primary attribute.

A change event is sent if the value is changed (i.e. the new value is different from the current value).

enumAttributes( [capability] )

Arguments:
capability — (string, optional) capability name.

Returns: Array of attribute names in canonical form (i.e. capabilityname.attributename, like power_switch.state).

If capability is given, return only those attribute names in that capability. Otherwise, returns all attribute names for all capabilities.

registerAction( name, func )

Arguments:
name — (string) action name.
func — (boolean) whether or not the action is supported by the entity.

Registers the named (capability.actionName form) action. If func is given as false, it signals to the system that this entity in incapable of performing the named action (the intent being that this removes the option from the Reaction editor's list of available actions for the entity).

A change event is sent if the entity's action list is modified.

hasAction( name )

Returns true if the entity has the named action and it is supported (see registerAction() above).

perform( actionName, parameters )

Performs the named action on this Entity with the given parameters. Returns a Promise. Subsystems outside of Controller implementations must call this method and must not call Controller.performOnEntity() directly.

class TaskQueue

TaskQueue implements a FIFO queue of actions to performed at a limited pace. It is often used with Controllers when the hub is incapable of managing a large number of requests in a short period of time.

constructor()

Returns a new TaskQueue.

start()

Start the TaskQueue. This must be called at least once to get things moving, and it may be called when the task queue is empty, so it is recommended that your subclass constructor create the TaskQueue instance and called its start() method immediately. Once started, the TaskQueue will run enqueued tasks as they come, subject to pacing restrictions (see setPace() below).

stop()

Stop the TaskQueue. This may be called, and when used, is often done in the stop() implementation of a parent Controller. Any queued tasks not yet completed fail immediately.

finish()

Returns a Promise that resolves when all queued tasks complete.

setPace( milliseconds )

Set the pace at which tasks may execute (i.e. not less than milliseconds apart). If the queue contains multiple tasks, the next task may be delayed.

enqueueTask( func [ , opts [ , ...args ] ] )

Enqueues the given function as a task to be run. The function will be called with the listed args (if any). Returns a Promise that resolves when the task completes, or rejects if the task throws an error.

The opts object currently only has one key, name, which may be set to a name identifying the task (helps debugging). If not given, the name is the task ID, which is internally generated.

When a task is enqueued, if no tasks are currently running, the newly-enqueued task is started as soon as possible (it may be delayed by the pace restriction). Whenever a task completes, if another task is available in the queue, it is started after the pace delay. This continues until the queue is empty.

interface Observable

Observable is an interface (mixin) class that can be applied to a class to indicate that it sends events.

Use the util.mixin() function to apply Observable. Note that it is not necesary to apply Observable to your Controller subclass because it is inherited from Controller.

const util = require("server/lib/util.js");

class MyController extends Controller {
    // ...
}

util.mixin( MyController, Observable );

publish( data, eventType, ...args )

Publishes an event with the given eventType and data. Most objects will simply publish this as the data (i.e. they publish themselves to announce they have changed), but the data can be anything, or nothing.

disconnect()

Removes all observers for this object. Usually called when the object is about to be destroyed.

interface Observer

Observer is an interface (mixin) class applied to a class to indicate that it can receive events from Observable objects.

Use util.mixin() to apply Observer. Note that it is not necessary to apply Observer to your Controller subclass because it is inherited from Controller.

const util = require("server/lib/util.js");

class MyController extends Controller {
    // ...
}

util.mixin( MyController, Observer );

notify( event )

Called by the events subsystem to pass an event from an Observable to which this Observer has previously subscribed. The event is a structure/object containing keys eventType, data, and sender. If the event was propagated by another object, the currentSender field identifies that sender (so sender is always the original sender of the event, and currentSender is the last object to propagate it).

propagate( event )

Typically used in an Observer's notify() implementation when the Observer itself is Observable, propagate() passes an event up the event hierarchy. The currentSender on the event is set to the current object calling propagate() (i.e. it's set to this).

subscribe( observable [, ident ] )

Subscribes this Observer to an Observable. The optional ident may be used to manage multiple subscriptions to the same Observable for different purposes.

unsubscribe( [observed] )

Unsubscribe from the specified Observable, or if not provided, all objects.

class TimerBroker

TimerBroker is a singleton that manages multiple timers for a subsystem.

getTimer( id [, resolution [, callback ] ] )

Arguments:
id — (string) Timer ID (see below).
resolution — (optional, integer) timer resolution in milliseconds; recommend using 1 in all circumstances (default: 1).
callback — (optional, function) function to be called when the timer expires; see Timer below.

Returns: A Timer instance with the given ID (existing or created).

Timer IDs must be unique system-wide. It is recommended that if you request timers for your Controller subclass, you prefix the timer IDs with ctrl-<id>, where <id> is the ID of your controller instance (use getID()).

releaseTimer( id_or_timer )

Arguments:
id_or_timer — (string or Timer instance) - The timer ID or a Timer instance.

Releases the timer. If running, it is first cancelled (it will send no more events).

class Timer

A Timer is an Observable object that will send a timer-trigger event to its observers when the time is met.

Use TimerBroker for new Timers

Timers should not be instantiated directly; use TimerBroker instead.

constructor( id [, callback ] )

Arguments:
id — (string) Timer ID.
callback — (optional, function) a function to be called when the timer expires (optional); see below.

Timer IDs must be unique system-wide. It is recommended that if you request timers for your Controller subclass, you prefix the timer IDs with ctrl-<id>, where <id> is the ID of your controller instance (use getID()).

If the callback is given, it is called in addition to sending a timer-trigger event to any observers. This allows you to use either mechanism to be notified when a timer expires, or both (often a callback is simpler than implementing Observer's notify()).

Use TimerBroker for new Timers

Timers should not be instantiated directly; use TimerBroker instead.

delayms( ms )

Arguments:
ms — (integer) delay in milliseconds

Sets the timer's expiry to the current time plus the indicated delay. This will override any prior expiry on a running timer; it is not necessary to cancel() the timer first.

earlierDelay( ms )

Arguments:
ms — (integer) delay in milliseconds

The requested delay is set if it would trigger the time earlier than its current expiry. If the timer is not running, it is started with the given delay.

at( absolute_time )

Arguments:
absolute_time — (integer, Epoch timestamp) time at which to trigger (no earlier than)

Sets the timer's expiry to the given time. This will override any prior expiry on a running timer; it is not necessary to cancel() the timer first.

earlier( absolute_time )

Arguments:
absolute_time — (integer, Epoch timestamp) time at which to trigger (no earlier than)

If the given time is earlier than the timer's current expiry, the timer will be set to trigger at the given time. If the timer is not running, it is started with the given time as its expiry.

isRunning()

Returns true if the timer is running, false otherwise.

expires()

Returns the current expiry of the timer, or (boolean) false if the timer is not running.

cancel()

Cancels the current timer. The timer is not destroyed, it just becomes dormant. The expiration callback, if any, is not called, nor is a timer-trigger event sent to any observers.

destroy()

Destroys the timer. Usually, you don't call this function directly. If you acquired the timer through TimerBroker, use its releaseTimer() method instead.

class Logger

Messages sent via Logger are stored in the Reactor logs per the rules defined by logging.yaml. Messages can be sent at various log levels to minimize the chatter/clutter or focus a log on a particular class or object/instance.

Message text may include substitution of arguments. The %n form is used, where n is a number (e.g. %1, %2, etc.); these represent the nth argument passed. This mechanism should be used in favor of string construction (e.g. "my name is" + name) because, if the log message does not meet the log level configured as minimal for output to the log file(s), the processing of the substitution will not occur (i.e. it's more efficient for messages that may or may not be output at all, especially for the many debug-level messages that tend to persist in code).

this.log.debug( Logger.DEBUG0, "Loaded %1 objects, %2 valid.", obj_count, valid_count);

The substitution will handle parameters that are arrays, objects, Sets, Maps, and Dates. They will be formatted to a human-readable form. Numbers that look (to Logger) like Epoch timestamps will also be output both numerically and as a formatted date/time. You do not need to do any of these conversions before passing them to Logger's output methods.

Care should be taken with debug messages in particular. Large numbers of debug messages in loops can be executed hundreds and thousands of times per minute, and if the log level would prevent them from being output, you're wasting CPU cycles. The determination of whether a message meets level filtering is fast and done early, so it's not too bad, but you don't want to create "collateral work" in building your messages — remember that JavaScript must evaluate every argument you pass before passing them into Loggers method, so if you use an expression as an argument, the work of evaluating that expression will be done before the Logger gets to decide if the message will even make it to the output. See isLoggable().

Where needed, the log levels are defined as constants on the Logger class. In order of descending priority: Logger.CRIT (critical), Logger.ERROR, Logger.WARNING, Logger.NOTICE, Logger.INFO, Logger.DEBUG0 .. Logger.DEBUG4. These identifiers have numeric values 0 through 9 respectively, and that is not expected to change ever, so while it's not great style, the numeric values are more often used in practice when calling the debug() method. The level names aren't needed for other levels because you are typically calling the method with the same name as the level (e.g. error(), notice(), etc.).

In default configurations, the default log level is 4 (INFO), but the settings in logging.yaml can change this for any class or instance.

constructor

Logger's constructor is private and must not be called directly. Use the static getLogger() class method instead.

static getLogger( id [, parent ] )

Arguments:
id — (string or object) ID of the logger to fetch or create.
parent — (optional, Logger or string) parent logger.

Gets a logger with the given ID. If an object is passed for id, it must be an object that implements a toString() method that gives a unique result for each instance; a logger will be created for just this instance, and the instance can be logged separately from all other instances (by configuration in logging.yaml).

The parent can also be a string or object (that implements toString() as above). For hierarchies of classes/objects, this is useful as it allows you define a parent logger. This then allows you to set, through logging.yaml, the log level and output streams for all loggers having the same parent.

It is customary, when implementing a Controller subclass, to have the following in the preamble before the class definition:

const version = YYJJJ;  // Two-digit year with julian date as a version number

const Logger = require( "server/lib/Logger" );
Logger.getLogger( "MyControllerClass", "Controller" ).always( "MyControllerClass version %1", version );

This logs your class name and version to the log file at the time the class is loaded by Structure (at startup).

It is usually otherwise unnecessary to call this function, as the Controller base class constructor creates a per-instance logger for you. This is the logger you should use for all messages emitted by your Controller subclass. It is available by calling the base class method this.getLogger(), or for cleanliness and brevity, you can simply refer to the base class (public) property this.log.

However, you can, if you wish, create additional loggers for subsystems (for example) of your subclass. These should be given an ID sufficiently unique as to not collide with other objects, so the usual recommendation is to prefix the ID of your new logger with your subclass name. For example, you might create a logger for your action handling, so the string MyControllerClass-Actions could be an appropriate ID. That would make it a class-wide logger, applying to all instances of your subclass. To create a per-instance logger, you could create the ID using this.getID() + "-Actions" (where this refers to the running instance of your Controller subclass).

static getDefaultLogger()

Gets the system default logger. This must only be used in contexts where a class or object logger cannot be determined or applied.

setLevel( new_level )

Changes the log level of this logger. If null is set, the log takes on the level of its parent.

getLevel()

Returns the log level of this logger. If null, the parent log level is in effect.

getEffectiveLevel()

Returns the effective log level for this logger. If this log has a specific level set, it is returned. Otherwise, this method ascends the tree of loggers until an ancestor is found with a specific level set, and that is returned.

isLoggable( level )

Returns true if and only if a message at level would pass this logger's level filtering (i.e. its priority is at least as high as the effective level).

For example, if this logger is at level 4, isLoggable(5) would return false, while isLoggable(4) (and all levels lower) would return true.

This method is often used when more complex information needs to be output, but only if the effective log level would allow it. For example, converting a large websocket payload to JSON could be pretty expensive, so calling logger.debug( Logger.DEBUG1, "The message was %1", JSON.stringify( wsdata ) ) is not ideal, because the JSON-ification of the data has to be done before the logger can determine if the message is eligible for output at all. To save that work, this could be structured as:

logger.debug0( "websocket message received" );
if ( logger.isLoggable( Logger.DEBUG1 ) ) {
    logger.debug( Logger.DEBUG1, "The message was %1", JSON.stringify( wsdata ) )
}

In this way, the work of converting the message to a JSON string for logging is only done if the string would be logged.

info( msg, ... )

Sends an information level message.

notice( msg, ... )

Sends a notice level message.

warn( msg, ... )

Sends a warning level message.

err( msg, ... )

Sends an error level message.

exception( error_object )

Sends a stack trace for the given exception (an instance of JavaScript's Error).

debug( level, msg, ... )

Sends a debug-level message. Debug levels are 5-9 (Logger.DEBUG0 through .DEBUG4).

always( msg, ... )

Sends a message to the log unconditionally, bypassing all level filters.

raw( line )

Sends line (string) to all logs unconditionally, bypassing all level filters and formatting (i.e. %n substititions are not performed).

class Semaphore

A Semaphore is a locking object that allows access to a restricted number of resources. For example, a semaphore created with a resource count of 2 can be acquired by two tasks. At that point, a third attempt to acquire the semaphore will block until either of the first two tasks releases the semaphore.

Read more about semaphores here.

constructor( resourceCount )

Constructs a new counting semaphore with the given resource count (integer >= 0).

acquire( [block] )

Each call to acquire() decrements the semaphore's counter and returns a Promise. If the resource counter is greater than 0 prior to decrementing, the Promise resolves; otherwise, the Promise waits, effectively blocking the calling process, until another process that has previosly acquired the semaphore releases it.

If block is given and false, the request will not block if the semaphore is not available (count <= 0), but rather reject the returned Promise immediately.

The data passed by the resolved Promise is a release function that must be called to advise the semaphore that you are done with it. Great care must be taken all acquired semaphores are released, or deadlocks may result.

    const sem = new Semaphore( 1 );
    sem.acquire().then( release => {
        console.log("I have acquired and am now releasing");
        release();
    }).catch( err => {
        console.error("An error occurred: ", err );
    });

isLocked()

Returns true if the semaphore is currently locked (has count <= 0).

count()

Returns the semaphore's current count.

wait( timeout )

Like acquire(), but will time out after timeout milliseconds if the semaphore cannot be acquired (the returned Promise is rejected in this case).

drain()

"Drains" the semaphore by settling all waiting Promises and restoring the semaphore's count to its initial value.

class Mutex

Implements a mutual-exclusion lock. This is a special case of Semaphore that prevents execution of a protected code block by more than one thread of execution simultaneously.

constructor()

Makes a new lock.

acquire()

Acquires the lock. A promise is returned that resolves when the lock is acquired. The data passed to the resolver is a release function that must be called when the code if finished with the lock. Care must be taken that the lock is released under any and all conditions, or deadlocks will result.

isLocked()

Returns true if the lock is currently locked (i.e. calling acquire() would block).

release()

Releases the lock unconditionally. It is not recommended that this method ever be called; it is provided only for exigent circumstances (that I have not yet identified concretely, so this method may be removed in future).

class Capabilities

The Capabilities class (and its singleton object) handle the runtime catalog of capabilities. By default, the catalog contains the complement of capabilities defined by server/lib/system_capabilities.yaml. Controller subclasses and other subsystems can expand the catalog by adding their own extension capabilities (see, for example, the Controller method loadBaseImplementationData()).

When loading the Capabilities module (as shown below), the default export is a singleton (one instance for the entire system) instance of the Capabilities class. It is not a class, and you should not attempt to create a new instance using new Capabilities() — that won't work. Use the constant directly (see example code in the loadCapabilityData() method, below).

const Capabilities = require( "server/lib/Capabilities" );

getCapabilities()

Returns the current catalog of capabilities as an object whose keys are capability names. The values are capability objects, which as of this writing are objects with keys attributes and actions that define what attributes and actions, respectively, the capability offers.

getCapability( name )

Arguments:
name — (string) the name of the capability to be returned.

Returns the capability object (see getCapabilities()) for the named capability, or undefined if it is not defined.

listCapabilities()

Returns an Array containing all defined capability names.

loadCapabilityData( data )

Arguments:
data — (object) an object (dictionary) of capabilities to be defined.

The data object is of the same form as that return by getCapabilities(): an object the keys of which are capability names, and the values of which are capability objects.

It is not usually necessary for Controller subclasses to call this method. Usually, the subclass defines its own capabilities in an _capabilities.yaml file that is loaded by the Controller base class when Controller's loadBaseImplementationData() method is called. However, there are controllers that create capabililities on the fly in addition to those defined that can be defined in capability data files.

const Capabilities = require( "server/lib/Capabilities" );

Capabilities.loadCapabilityData({
    x_my_capability: {
        attributes: {
            flavor: {
                type: "string"
            },
            count: {
                type: "int"
                default: 0
            }
        },
        actions: {
            set_flavor: {
                arguments: {
                    new_flavor: {
                        type: "string"
                    }
                }
            },
            set_count: {
                arguments: {
                    new_count: {
                        type: "int"
                    }
                }
            }
        }
    }
});

util library

The util library contains a number of utility functions that have various uses.

getUID( [ prefix ] )

Returns a (string) unique identifier with the given prefix (if any). The string is guaranteed to be unique as long as the system time does not slew backwards significantly (and even if it does, a restart of Reactor would have to occur during the overlap period for any risk of duplication — not likely).

shallowCopy( object ) **DEPRECATED**

Creates a shallow copy of the passed object. This is equivalent to the { ...object } (spread) construct in newer versions of JavaScript and is in fact the implementation used by this function. It is kept for compatiblity with legacy code in Reactor. Consider this function DEPRECATED.

clone( object )

Creates and returns a deep copy of the given object.

This is a simple implementation for data objects; as such, the source object may not contain functions or JavaScript objects that have class definitions and/or constructors (i.e. Set, Map, Date, Generator, Promise, RegExp, etc.), except Array (always permitted). Rule of thumb: if you can't convert it to JSON and back symmetrically, you can't use this function to clone it.

hasAnyProperty( object )

Returns true if the given object has any (enumerable) properties (e.g. hasAnyProperty( {} ) returns false ).

deepCompare( val1, val2 )

Compares val1 and val2 for semantic equality. Both should have the same type, and may be or contain arrays and/or objects. That is, you can use this function to compare arbitrarily complex structures.

Arrays are considered equal if they have the same number of elements and the element values are the same and in the same order. Objects are considered equal if they have the same enumerable properties and the values of all properties are the same (object prototypes are not considered).

binarySearch( arr, val )

Quickly find the element of a sorted Array arr having value val. The array must be sorted (ascending) or this function will malfunction. Returns the index of the element or -1 if not found.

Binary search has O(logn) complexity: finding an element in an array of 8 elements requires at most 3 iterations, but in an array of 1024 elements at most just 10 iterations. It is therefore much more efficient than seeking sequentially in the array, but it requires that the array be sorted (ascending).

mixin( baseClass, mixinClass )

Applies mixinClass (which must be a class) to baseClass: all methods of mixinClass except its constructor are applied to baseClass, if they are not already defined.

coalesce( ... )

Returns the first non-undefined, non-null, non-NaN argument in the argument list, or null. Its typical use is in applying default values:

 /* If the config value "interval" is given and can be parsed to an integer,
  * use it. Otherwise, use 60. */
const interval = util.coalesce( parseInt( this.getConfig().interval ), 60 );

isEmpty( string )

Returns true if the given string is undefined, null, or the empty string ("").

loadData( pathArray, fileroot, sysroot )

Arguments:

pathArray — (array of strings, or string) paths to search for the data file. These can be relative paths, or absolute. If a string is specified, multiple paths can be separated by using : or ; as a delimiter; if null or undefined, the sysroot alone is scanned for the file;

fileroot — (string) base name of the data file (i.e. without a suffix);

sysroot — (string, optional) path of the root for relatives paths in pathArray; the most common (and recommended) usage is to supply __dirname as the argument value.

Returns: (object) the contents of the data file, if found; otherwise undefined.

Loads a data file (JSON or YAML) and returns its contents. A search of the elements of pathArray is conducted by calling path.resolve( sysroot, pathArray[i] ) for each element, and seeing if a JSON or YAML file with fileroot as its basename (i.e. basename plus .json, .yaml, or .yml) exists in that directory. If so, the data file is loaded and the data returned. If the data file cannot be parsed, an exception is thrown, so this function should be used in a try block. If no file is found, undefined is returned.

    mfg = util.loadData( ["manufacturer", "mfg", "."], mfg_id, __dirname );

In the above example, loadData searches for a JSON or YAML file named by the manufacturer's (hypothetical) ID, first in the manufacturer subdirectory underneath the running module's directory (__dirname), then the mfg subdirectory, and then in the module's directory itself. That is, if __dirname resolves to /home/reactor/ext/MyController, then /home/reactor/ext/MyController/manufacturer is first searched, then /home/reactor/ext/MyController/mfg, and finally /home/reactor/ext/MyController.

A slightly more complex, and perhaps useful, example: the following would first search for the data file in the user's configuration directory under a mycontroller_data subdirectory (if it exists), and then in the subdirectories used in the previous example:

    mfg = util.loadData( [
        path.resolve( Configuration.getConfig( "reactor.confdir", "string" ), "mycontroller_data" ),
        "manufacturer", "mfg", "."
        ], mfg_id, __dirname
    );

This shows how a mix of relative and absolute path names can be used in the search to facilitate user-configured exceptions or overrides to package data.

Note that you should not use this utility function to load the <name>_capabilities.yaml or <name>_devices.yaml files typically used as implementation data for a Controller subclass. The use of Controller.loadBaseImplementationData() alone is correct for that purpose. However, it is perfectly acceptable for a Controller subclass to use this function to load embellishment data, where the amount of such data would be large and inconvenient to store monolithically in either of these files. For example, ZWaveJSController uses this method to load per-manufacturer device exceptions, because the number of supported manufacturers, and the number of total devices they offer, is so large that it would be inconvenient to store all of that data in zwavejs_devices.yaml alone. At the moment, however, this places the onus on the Controller developer to detect if a ZIP override file is available and determine if its contents should be used. See Controller.getZipper().

async asyncForEach( array, callback )

There are sometimes cases where forEach() would be used but the callback function must be async, and it is probably desirable, then, to await on the completion of all iterations before execution continues. This isn't hard to code in JavaScript, but can be a bit sloppy, so this function provides a tidy one-call implementation. The array is that over which the iteration will occur, and the callback is an async function whose arguments are the current array element and its index.

async notifyServer() {
    const arr = someApi.getListOfNotices();  /* array of message strings */

    /* We want to wait until all notices have been sent */
    await util.asyncForEach( arr, async ( str, id ) => {
        /* util.fetch_http() is async, but we want to wait for it */
        await util.fetch_http( "http://www.example.com/notify?msg=" + str );
    });
}

TimedPromise( implementation, timeout )

Given a function (implementation) that takes resolve, reject arguments (i.e. a Promise implementation), returns a Promise that runs the implementation, and rejects if the implementation does not resolve within the given timeout (milliseconds). If the implementation succeeds before timeout, the returned Promise resolves to whatever the implementation function passed to resolve(). If the implementation itself reject()s before timeout, the returned Promise also rejects with whatever value the implementation rejected with.

Note that nature of JavaScript Promises is such that they cannot be cancelled/stopped, so timing out does not (and can not) actually stop the implementation from finishing. The implementation will run to its conclusion regardless. That will not change the rejection of the returned Promise, however (i.e. the returned Promise will not reject with timeout and then later resolve if the implementation successfully resolves after the timeout has occurred). Users of this function are cautioned to consider this fact when choosing activities in their implementation function before resolve() is called. Any and all work that relies on the result of the implementation should be done only in the .then() of the returned Promise and never in the implementation function itself.

sequencePromises( implementationArray )

Given an array of functions that take resolve, reject arguments (i.e. Promise implementations), runs the implementations sequentially (each must resolve() before the next is started). Returns a Promise that resolves when the last resolves, or rejects if any rejects/fails.

promiseWrapper( function, ...args )

Runs a plain function that is not in Promise form asynchronously as a Promise that resolves with the function value if successful, or rejects with a JavaScript Error otherwise. It's primary use is to wrap functions to be used with sequencePromises().

Although this is a bit of a nonsense example, here's a sort of an array of numbers using JavaScript's Array sort() method as a Promise that resolves when the sorting is done and returns the sorted array:

    let arr = [ 9, 3, 4, 1, 2 ];
    util.promiseWrapper( arr.sort.bind( arr ) ).then( sorted_array => {
        console.log( "The sorted array is", sorted_array );
    }).catch( err => {
        console.error( "The array sort failed: ", err );
    });

promiseWrapperAsync( async function, ...args )

This is a version of promiseWrapper() that accepts an async function to be wrapped.

http_fetch( url, req )

A wrapper for node-fetch that also handles HTTP Basic and Digest authentication. Parameters are identical to that of node-fetch, with the following optional additions to req:

  • reactor_ignore_certs: boolean (default: false)
  • reactor_username: string
  • reactor_password: string
  • reactor_basic_auth: boolean (default: false, use Digest authentication)

For developers implementing Controller subclasses, use Controller's fetchText() and fetchJSON() methods instead.

lexpjs library

lexpjs is the expression parser used in Reactor expressions, and is also used to handle expressions for Controller implementation data, when data-driven rules are used.

I'm working on a better interface for lexpjs, and improved documentation. Until then, the docs for lexpjs can be read here.

const lexp = require( "common/lexp" );

console.log( "8 squared is", lexp.evaluate( '8 * 8' ) );

Base Controller Template and Examples

For a working example controller that uses HTTP request polling, see OctoPrintController. For an example controller that uses a WebSocket for access to a remote API and live updates, see WhiteBITController. For an example controller that uses an installed (npm) API package for its interface, see NUTController. These are extension controllers that can be found in the extras folder of the download server.

/** Your Controller Title/Name
 *  Copyright (c) your information, All Rights Reserved.
 *  Link to documentation, license, etc.
 */

/** NOTE: Replace $$$ in this template with the base of your subclass name.
 *  The class name must end with Controller. This file's name should be
 *  your <classname>.js (i.e. <something>Controller.js).
 */

const version = 22283;

const Controller = require("server/lib/Controller");

const Logger = require( "server/lib/Logger" );
Logger.getLogger( '$$$Controller', 'Controller' ).always( "Module $$$Controller v%1", version );

const Configuration = require("server/lib/Configuration");
const logsdir = Configuration.getConfig( "reactor.logsdir" );  /* logs directory path if you need it */

/* Another other modules we need? */
const util = require("server/lib/util");

var impl = false;  /* Implementation data, one copy for all instances, will be loaded by start() later */

/** Place any other global/class-wide declarations here. They are not exported
 *  unless you export them explicitly.
 */

/** Declare your subclass */

module.exports = class $$$Controller extends Controller {
    constructor( struct, id, config ) {
        super( struct, id, config );  /* required */

        /* Place your instance property initializations here. *
        this.retries = 0;
    }

    /** Start the controller. */
    async start() {
        /** Load implementation data if not yet loaded. Remove this if you don't
         *  use implementation data files.
         */
        if ( !impl ) {
            /*  The string passed below should be the **lowercase** translation
             *  of your class basename.
             */
            impl = await this.loadBaseImplementationData( '$$$', __dirname );
        }

        /** Do what you need to do to start your controller here. How you
         *  structure this code is up to you. Here, for example purposes,
         *  we'll just call our run() method and kick of a train of timer
         *  events.
         */
        this.run();

        /* You have to do this somewhere, when everything is working. */
        this.online();

        /** Must return `this` or a Promise that resolves to this. If this
         *  is declared async (as it should be), the return of a Promise
         *  is implicit (async functions always return Promise).
         */
        return this;
    }

    /** Stop the controller.
     *  You only need to supply this method if your subclass has something
     *  specific it needs to do.
     */
    async stop() {
        this.log.notice( "%1 stopping", this );

        /* Required ending */
        return await super.stop();
    }

    /** run() is called when Controller's single-simple timer expires.
     *  If you don't use Controller's simple timer, you don't need this.
     */
    run() {
        this.log.debug( Logger.DEBUG5, "%1 in run() for timer tick", this );
        this.startDelay( 60000 );  /* run again in 60 seconds */
    }

    /** performOnEntity() is used to implement actions on entities. You usually
     *  must implement this, if any of your entities has any actions.
     */
    async performOnEntity( entity, actionName, params ) {
        this.log.debug( 5, "%1 perform %2 on %3 with %4", this, actionName, entity, params );

        /** If you can't find anything to do yourself, you can try to let the
         *  base class handle it. That usually means the base class will try to
         *  call a method (of yours) called action_actionname (for example, for
         *  power_switch.on it would look for action_power_switch_on( entity, params )
         *  and for toggle.toggle it would look for action_toggle_toggle( ... ).
         *  See the example action function below.
         */
        if ( no_specfic_implementation ) {
            return super.performOnEntity( entity, actionName, params );
        }
    }

    /** ---------------- ALL OF YOUR CODE BELOW ------------------ */

    /** ---------------- DEFAULT ACTION IMPLEMENTATIONS ------------------ */

    /** Here's an example of an action function that could be used as a default.
     *  It simply uses the current power_switch.state attribute on the entity
     *  to implement the toggle.toggle action.
     */
    action_toggle_toggle( entity, params ) {
        return this.performOnEntity( entity, "power_switch.set",
            { state: ! entity.getAttribute( "power_switch.state" ) } );
    }

};

Updated: 2022-Nov-23