Building Controllers & Creating Entities
Effective Version and Change Log
This document has been updated to cover classes and methods available in version 24273. Updates to this document (which will reflect changes in the classes and methods described) can be found in the Change Log at the end.
Preliminary/Draft
This documentation is in very preliminary draft form. Aside from the obvious formatting and consistency issues, it is likely to contain errors. Please report any errors that you find.
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. Thestart()
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 mustreturn 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. Therun()
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 callingoffline()
, 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 thex_
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 (specifically the Application and Structure singleton objects) 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).
erDiagram
STRUCTURE ||--o{ CONTROLLER : has
CONTROLLER ||--o{ ENTITY : has
STRUCTURE ||--|| ENGINE : has
ENGINE ||--o{ RULE : has
ENGINE ||--o{ REACTION : has
ENGINE ||--o{ GLOBAL-VAR : has
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).
All Controller instances will have a system entity (which is an interface to the Controller itself), and a system group entity, which is a container for all other entities that the controller may create (except the system group and itself).
erDiagram
CONTROLLER ||--|{ ENTITIES : owns
ENTITIES ||--|| SYSTEM-ENTITY : has
ENTITIES ||--|| SYSTEM-GROUP-ENTITY : has
SYSTEM-GROUP-ENTITY ||--o{ ENTITIES : members
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.extendCapabilities( capabilityNameArray )
or entity.extendCapability( capabilityName )
repeatedly as needed. This will make the capabilities available on the entity, and also initialize the attributes of each 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.
erDiagram
ENTITY ||--o{ CAPABILITY : extends
CAPABILITY ||--o{ ATTRIBUTE : provides
CAPABILITY ||--o{ ACTION : provides
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.
Typical Attribute Definition in Capability Definitions
General form:
dimming:
attributes:
attrname1: # attribute name, required
type: real # optional value type -- see below; default is string
default: vvv # optional, value assigned when the attribute is first created (i.e. the capability is first extended)
writable: bool # optional, set true if writable attribute (DO NOT set true for Controller-controlled attributes)
minimum: nnn # optional, when type is numeric, hint to UI for good value in range (for writable attributes)
maximum: nnn # optional, when type is numeric, hint to UI for good value in range (for writable attributes)
values: # optional, array of expected values
- value1
- value2
attrname2:
# etc.
The type
can be one of the following:
string
— a string value of any length;real
— a real number (i.e. floating point value);bool
— a boolean value (true or false);int
oruint
— an signed or unsigned integer of system-defined size (i.e. 32 bits on 32-bit architectures, and 64 bits on 64-bit architectures);i1
,i2
,i4
,i8
,ui1
,ui2
,ui4
,ui8
— signed or unsigned integers of 1, 2, 4, or 8-byte size. A one-byte signed integer will have values between -128 and 127, inclusive; a two-byte unsigned integer will have values from 0 to 65535 inclusive.object
— object data as a JSON string.
Guidelines, not Rules
The type
, minimum
, maximum
and values
are all hints for the UI to improve the user experience, but they are not strictly enforced. An attribute can be given any value of any type, or given a value not specified in the values
list. This allows your Controller latitude when the hub produces a value newly introduced or not previously known to Reactor, for which Reactor has no analog/equivalent. Rather than limit your Controller (i.e. cause it to start getting exceptions thrown at it), Reactor will accept any value. It is up to your Controller to determine what's reasonable and how to use it.
The special value null is a valid value for any type.
Note that there is no data type for dates/times. Reactor typically stores these as JavaScript Epoch times (milliseconds since midnight January 1, 1970 UTC). Therefore, ui8
should always be used for attributes representing dates/times.
Do Not Use int
or uint
for Dates/Times
Do not use int
or uint
to represent dates/times, as these will overflow when Reactor is installed on 32-bit architectures. Today's date in JavaScript Epoch representation is already a much larger value than can be stored in a 32-bit integer.
Typical Action Definition in Capability Definition
General form:
dimming:
attributes:
# redacted for brevity/clarity
actions:
actionname1:
arguments:
argumentname1: # action name, required
type: ttt # optional, data type, same as attribute definition (see above)
minimum: nnn # optional, when type is numeric, hint to UI for good value in range
maximum: nnn # optional, when type is numeric, hint to UI for good value in range
default: vvv # optional, value assigned when no value is given for the argument
optional: bool # optional, hint to UI if value is not required; default *false*
values: # optional, array of expected values
- value1
- value2
sort: nnn # optional, order of field display (lowest to highest values)
response: bool # optional, if *true* this action returns a response; default *false*
argumentname2:
# etc.
actionName2:
arguments:
# etc.
The sort
value is optional but recommended for actions having more than one argument. The values are not significant except that their magitutude determines the sort order of the argument field display in the UI. That is, you can use 1, 2, 3, ..., or 100, 110, 120, ..., etc.
Guidelines, not Rules
Just as for attributes, the type
, minimum
, maximum
and values
for arguments are all hints for the UI to improve the user experience, but they are not strictly enforced. This allows the user latitude when the hub will accept a value for which your Controller has not provided an equivalent. Rather than limit the user (or your Controller), Reactor will accept the value.
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:
Themain
key in yourpackage.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 (basicallyexpress
,node-fetch
, andws
).
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. To create a custom Controller, you must extend
this class.
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 to be called directly. It must only be called by the getInstancePromise()
static method, which itself must only be called by the system's Structure
object.
class MyController extends Controller {
constructor( ...args ) {
super( ...args );
// any other initialization you need/want goes here
// You can access (read only) the following properties of Controller ONLY:
// this.id -- the designated ID of your controller
// this.config -- the "config" section of reactor.yaml for your controller
// Everything else must be access through methods provided only.
}
// etc
}
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 (ifasync
), 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 its parent 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
object contains the following key fields: sender
(the original object sending the event), currentSender
(the current object sending the event), type
(the type of message, e.g. entity-changed), data
(additional data about the event). Normally, sender
and currentSender
will be the same object, but if another Observer propagates the event, currentSender
will be that Observer (i.e. different from sender
, which is always the originator of the event).
Overrides must always return super.notify( event, ... )
If you find a need to override this method for your own event handling, your implementation must always 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 and other objects throughout the entire system and cause it to stop running rules and responding to their conditions, updating the UI, etc.
notify( event, ...args ) {
try {
// your custom code goes here
} catch ( err ) {
// your error handling goes here
} finally {
super.notify( event, ...args );
}
}
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, action_name, parameters )
- Arguments:
-
entity
— (Entity object) the entity on which to perform the action; -
action_name
— (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).
Effect of entity.registerAction()
Because actions are always launched with a call to perform()
on the target entity, the entity object will first check to see if it was given a function when its registerAction
was called. If so, that function is called and expected to complete the task, and performOnAction()
will not be called. If entity.registerAction()
was called with boolean true
, then performOnEntity()
is called. If your subclass provides an override for this method, it gets the first chance to handle the action, or may hand off to the base class (super
) method for the behavior described above.
There are a number of actions for which the Controller base class provides default implementations. For example, the dimming.up
and dimming.down
actions' default implemetation will take the current level and apply the value of the attribute dimming.step
on the object, passing the result value to dimming.set
. It is therefore only necessary to support the dimming.set
action. See the separate section below for a list of default implementations provided by the Controller base class.
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.
As of Reactor build 24273, actions can return responses. The value passed to your Promise's resolve()
function will be returned to the Engine as the action response, but only if the action is declared with response: true
in its definition.
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
— (string) URL to be fetched;options
— (object, 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. If not supplied, the default is 15000ms (15 seconds).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). The default is false, certificates will be verified on secure connections.reactor_username
andreactor_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 keys/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: Same as fetchText()
above.
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. See fetchText()
.
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; ignoreCertificates
— true/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.
extendCapabilities( capability_name_array, add_only )
Extends the capabilities in the given array of names. Any capability on the entity that is not in the array will be removed, unless add_only
is true
. Any capability that is already extended to the entity is left untouched. New capabilities are initialized as described in extendCapability()
below.
A single change event is published if the entity's capability list is modified.
This method returns three arrays: capabilities added, capabilities removed, and capabilities that could not be added due to an error.
let [ addCaps, delCaps, errCaps ] = entity.extendCapabilities( capNames );
if ( errCaps.length > 0 ) {
// Log the unrecognized capabilities
this.log.err( "%1 entity %2 unknown capabilities could not be extended: %3",
this, entity, errCaps );
// other handling
}
extendCapability( capability_name [, def [, throwOnError ] ] ] )
Extends the named capability. It is safe to call this method if the entity is already extended to the entity (no action occurs). If the capability is new to the entity, the attributes of the new capability are initialized on the entity to the default, initial values (or null
if none). Actions defined by the capability are marked as available as well (their individual availability can be later changed by calling registerAction()
on the entity).
If def
is passed, it must be a capability definition in object form, or null
. This is meant for internal use only and should not be used or needed normally; system capability definitions are automatically loaded by Reactor, and Controller-specific capability definitions should be loaded using the mechanisms described in the Controller class documentation only. That said, this may have other uses in the future. For now, just pass null
if you are trying to pass throwOnError
(below).
If the throwOnError
(boolean) argument is given and true, an error will be thrown if the capability is not defined. If not given or false, an attempt to extend an undefined capability will be logged and this method will simply return (i.e. the attempt is noted but ignored).
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.
refreshCapabilities()
Refreshes all extended capabilities by pulling their current definitions from the Capabilities
singleton and copying them into the entity. Any attributes from the previous definition of the capability that still exist in the new definition will have their values preserved across the refresh.
Because Controllers can modify the definition of a capability on a per-entity basis, entities maintain copies of all capabilities extended to them. This allows, for example, one entity to have an implementation for an action in a particular capability, where another entity may have the same capability but is unable to perform that action.
It is up to the Controller to determine when capabilities need to be refreshed. Many of Reactor's included controllers keep track of the version information for the Controller implementation data files and the Controller subclass itself, and when those change compared to what is stored in the entity, refresh the entity's capabilities.
This method is new in build 24273.
refreshCapability( capability_name )
Refreshes a single, specified capability. See refreshCapabilities()
above for more background information.
This method is new in build 24273.
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 [, default_value ] )
Returns the current value of the named attribute. If the attribute does not exist on the entity, the default_value
is returned (or undefined if default_value
is not provided).
hasAttribute( attribute_name [, capability_name] )
Returns true if the named attribute (in capabilityname.attributename form) is available on the entity.
The attribute_name
can be just the attribute name without the capability prefix, in which case the capability_name
argument is used.
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_name] )
- Arguments:
capability_name
— (string, optional) capability name.
Returns: Array of attribute names in canonical form (i.e. capabilityname.attributename, like power_switch.state
).
If capability_name
is given, returns only those attribute names in that capability. Otherwise, returns all attribute names for all capabilities extended to the entity.
registerAction( name [, pact ] )
- Arguments:
name
— (string) action name.pact
— (boolean or function) whether or not the action is supported by the entity. Default: true.
Registers the named (capability.action_name
form) action.
If pact
is given as false, it signals to Reactor 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 that entity). If pact
is true or not given, then the Controller subclass must provide an implementation for the action in your Controller.performOnEntity()
method override, or rely on Controller's default action handling mechanism.
If pact
is given as a function, then a call to perform()
on the entity for the named action will cause the provided function to be called. The parent Controller's performOnEntity()
method is not called. The provided function is expected to take two objects as arguments: the entity on which to perform the action, and the parameters for the action as passed to perform()
; the action name passed to perform()
is not passed to this function.
Deprecation Notice — Passing Function for pact
Passing of functions via pact
is deprecated and will be removed from a future release. This is because functions cannot be stored in persistent entity storage, and that requires the function to be reconnected for the entity/action every time the Controller starts. Provide your action implementations in/via Controller.performOnEntity()
.
Note that it is not required to call registerAction()
. When a capability is extended to an entity, extendCapability()
will register all of the actions defined for the capability with default handling: performOnEntity()
will be called on the parent Controller instance. It is only necessary to call registerAction()
if a special function is to be used for the action, or if the action is not supported by the entity at all.
A change event is sent if the entity's action list is modified.
hasAction( name [, capability_name ] )
Returns true if the entity has the named action and it is supported (see registerAction()
above). If capability_name
is not given, the name
must include a capability name prefix (e.g. dimming.set
); otherwise, the name
can be just the plain action name.
redefineAction( action_name, newDef [, capability_name ] )
Redefines an action with the given definition data. When a capability is extended to any Entity, the action definitions are installed from the system capabilities (via the Capabilities object). The action may later be given a new definition using this method. See below for more information.
- Arguments:
action_name
— (string) A fully-qualified action name (i.e.<capability_name>.<action_name
, likepower_switch.set
), or just a plain action name (e.g.set
); if the latter, the capability name is expected in the third argument.newDef
— (object) action definition, in the form seen insystem_capabilities.yaml
.capability_name
— (string, optional) The capability name that defines the action (need only be specified ifaction_name
is a plain name).
Returns: nothing
Throws: an exception if the capability is not extended to the object, or the action is not defined by the capability.
The following two calls are equivalent. Use whichever form best suits the structure of your code.
entity.redefineAction( 'power_switch.set', psdef );
entity.redefineAction( 'set', psdef, 'power_switch' );
As an example of when and why to use this method, let's examine the implementation of the Z-Wave thermostat operating mode. The mode contains many values, some of which do not have mappings to Reactor's predefined values in the default definition of the hvac_control.set
action. In addition, not all thermostats support all modes, so a particular thermostat may only advertise an even more limited subset of the possible values. In order to help the Reactor UI provide good hints to the user for possible values in action arguments or Rule conditions, this method can be used by ZWaveJSController to provide a modified definition of the action hvac_control.set_mode
containing only those values that are fully mapped by the device.
perform( action_name, 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.
perform()
looks to see if the action was registered with a function; if so, that function is called, as described in registerAction()
above. Otherwise, the entity, action name, and parameters are passed to the parent Controller instance's performOnEntity()
method for handling.
class TaskQueue
TaskQueue implements a FIFO queue of actions to performed, optionally at a limited pace. It is often used with Controllers when the hub is incapable of managing simultaneous requests, or 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 currently-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.
The default pace is 0, meaning tasks are sent as quickly as they are completed.
enqueueTask( func [ , opts [ , ...args ] ] )
Enqueues the given function as a task to be run. It will be called with the listed args
(if any), and must return a Promise.
The opts
object currently supports the following keys:
Option | Description |
---|---|
name | (string) A name for the task to help identify it in debugging. If not given, the task name will be the task ID, which is internally generated. |
retries | (number) An integer number of retries to attempt if the task fails. When a task is retried, it is not retried immediately if there are other queued tasks waiting; it goes to the back of the line. |
retryOnSuspend | (boolean) Normally, if the queue is suspended while a task is running, and the task fails, the task is automatically requeued so it is retried when the queue resumes. This is meant to handle situations where a Controller suspends the queue upon losing its connection to a hub, and resumes the queue on reconnect. The default is true ; setting this option to false disables the automatic requeueing of the task. |
Returns a Promise that resolves when the task completes, or rejects if the task throws an error.
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, if any. This continues until the queue is empty.
suspend()
Suspends the task queue. Any currently-running task will complete, but no new tasks will be launched until resume()
is called.
resume()
Resumes a previously-suspended task queue.
abort( name )
Aborts the first waiting task found with the matching name
. For this to work properly, the task must have been enqueued using the name
option, and the name given for each task must be unique. If a task is running that would match name
, it is not stopped, and will be allowed to settle.
The Promise associated with the task is rejected; the rejection handler will be called with an Error
object containing the message aborted
.
Returns true if a matching waiting (not running) task was found; false otherwise.
clear()
Aborts all tasks waiting in the queue. The running entry is allowed to settle, but if it rejects/fails, it will not be retried, even if it was configured for retries.
This call does not stop or suspend the queue, it merely empties it.
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 classless 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).
Overrides must always return super.notify( event, ...)
Overrides of this method must always 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 and other objects throughout the entire system and cause it to stop running rules and responding to their conditions, updating the UI, etc. Developers are advised to place their entire handler inside an enclosing try
block with a finally
block satisfying this requirement.
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).
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).
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 most 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).
As of 24138, this function supports JavaScript Map
, Set
, Buffer
, and RegExp
objects.
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.
As of version 24138, val
may be a closure/function that is passed an element of the array to test; it must return 0 if the element matches the value being sought, a value >0 if the value being sought sorts after the element passed, and <0 if the value being sought sorts before the element being passed.
Returns the index of the matching element or -1 if no match.
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).
// Here's a sorted array we'll search through.
var array = [
14, 126, 127, 278, 645, 842, 1223, 1515,
1769, 2049, 2263, 2309, 2336, 2371, 2495, 2634
];
var seek = 842; // the value we're seeking in array
var ix = util.binarySearch( array, (val) => {
console.log("Compare", seek, val);
if ( seek == val )
return 0; // found it
else if ( seek < val )
return -1; // return <0, seek sorts before test value
return 1; // return >0, seek sorts after test value
});
console.log( ix ); // prints 5 because arr[5]=842
The closure above can be written more compactly as (val) => seek-el
, but for clarity, the expanded form above is presented. The example is also trivial. The closure is intended for use in more complex cases, such as when the array contains objects and the value being tested is a field inside the object.
Danger
If you implement the closure incorrectly, this function will recurse until the stack overflows, causing a crash. If that happens, your closure is probably returning backwards values (i.e. it's returning <0 when it should be returning >0, and >0 when it should be returning <0).
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 (""
).
isEmpty( undefined ) # true
isEmpty( null ) # true
isEmpty( "" ) # true
isEmpty( " " ) # false
isEmpty( false ) # false
isVoid( val )
Returns true if the given value undefined
or null
.
isVoid( undefined ) # true
isVoid( null ) # true
isVoid( "" ) # false
isVoid( false ) # false
isVoid( 0 ) # false
isUndef( val )
Returns true if the given value is undefined
.
isUndef( undefined ) # true
isUndef( null ) # false
isUndef( "" ) # false
isUndef( false ) # false
isUndef( 0 ) # false
searchPath( pathArray, fileArray [, sysroot ] )
Given a search path and filename(s), search the path for matching files. Filenames are matched exactly. To find files matching a pattern, use findFiles()
.
- 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, thesysroot
alone is scanned for the file; -
fileArray
— (array of strings, or string) filenames for which to search the path. If a string is specified, multiple filenames can be separated by using:
or;
as a delimited; -
sysroot
— (string, optional) path of the root for relatives paths inpathArray
; the most common (and recommended) usage is to supply__dirname
as the argument value.
Returns: (array) of matching files as full pathnames; the array has zero length of there are no matches.
Note that only the specified directory paths are searched; subdirectories are not traversed.
findFiles( pathArray, pattern [, sysroot ] )
Given a search path and pattern (a regular expression), search the path for matching files.
- 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, thesysroot
alone is scanned for the file; -
pattern
— (string, RegExp, or closure) a JavaScript regular expression to match filenames, or a closure that returns a boolean (filename passed in); -
sysroot
— (string, optional) path of the root for relatives paths inpathArray
; the most common (and recommended) usage is to supply__dirname
as the argument value.
Returns: (array) of objects containing matching file information; the array has zero length of there are no matches. Each object contains the following keys:
Key | Value |
---|---|
pathname | (string) Full pathname of the matched file. |
stat | (fs.Stats object) File statistics (see fs.Stats) |
directory | (boolean) true if the pathname is a directory; false otherwise. |
Note that only the specified directory paths are searched; subdirectories are not traversed.
// Find all files in user Documents and Downloads directory ending in .tmp
const fileInfo = findFiles( "./Documents:./Downloads", /\.tmp$/i );
// Alternate using a closure
const fileInfo = findFiles( "./Documents:./Downloads", (name) => name.endsWith( '.tmp' ) );
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, thesysroot
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 inpathArray
; 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.
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 );
});
}
hash( str [, algorithm ] )
- Arguments:
-
str
— (string or Buffer) data to be hashed; -
algorithm
— (string) hashing algorithm to use. If not specified,sha256
.
Returns: (string) hash of input encoded as a hexadecimal string.
See also: crypto.createHash
stringOrRegexp( s [, throwOnError ] )
Determines if the given string contains a regular expression, or is just a plain string. This is often used for handling data from configuration files where it's possible that either a string or regular expression could be specified (the built-in YAML parser doesn't handle regular expressions natively). To be a regular expression, the string must match the pattern /pattern/flags
or m!pattern!flags
. The flags
are optional. For the second form, the character after the m
, which must be a non-alphanumeric, is used as the delimiter of the pattern (i.e. basically Perl style).
- Arguments:
-
s
— (string) string to test; -
throwOnError
— (boolean, optional) if true, an error will be thrown if the string looks like a RegExp but can't be compiled as one; if false or not passed, a string that looks like a RegExp but doesn't compile will simply be silently returned as a string.
Returns a string if the argument doesn't look like it's a pattern; returns a compiled JavaScript RegExp otherwise.
See also: JavaScript RegExp
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
: stringreactor_password
: stringreactor_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.
Base Controller Default Actions
The following actions have default implementations in the Controller base class. It is not necessary to provide your own implementations for these actions, but if you intend to allow your Controller subclass to use them, you must access them through the use of super.performOnEntity()
. For example, if you don't have your own implementation for dimming.up
and dimming.down
, your Controller's performOnEntity()
should just call super.performOnEntity()
(the base class method).
dimming.up
anddimming.down
— will offset the current dimming level (attributedimming.level
) bydimming.step
(writable attribute) and pass it to yourdimming.set
.power_switch.set
— will invoke yourpower_switch.on
orpower_switch.off
actions based on the value of thestate
parameter given.lock.set
— will invoke yourlock.lock
orlock.unlock
based on the value of thestate
parameter given.valve.set
— will invoke yourvalve.open
orvalve.close
based on the value of thestate
parameter given.passage.set
— will invoke yourpassage.open
orpassage.close
based on the value of thestate
parameter given.cover.set
— will invoke yourcover.open
orcover.close
based on the value of thestate
parameter given.muting.set
— will invoke yourmuting.mute
ormuting.unmute
based on the value of thestate
parameter given.toggle.toggle
— will toggle the state for capabilitiespower_switch
,lock
,cover
,valve
,passage
, andmuting
.rgb_color.set_rgb
— will transform the parametersred
,green
andblue
to a 24-bit color value and pass that to yourrgb_color.set
.position.increase
,position.decrease
, andposition.relative
— applies the value of theamount
parameter, or the entity attributeposition.step
(writable attribute) if anamount
is not provided, to the current position value (position.level
), and passes that to yourposition.set
.volume.increase
,volume.decrease
, andvolume.relative
— applies the value of theamount
parameter, or the entity attributevolume.step
(writable attribute) if anamount
is not provided, to the current position value (volume.level
), and passes that to yourvolume.set
.tag.reset_count
— resets thetag.scan_count
attribute to zero (0) on the entity.
Controller Subclass 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, action_name, params ) {
this.log.debug( 5, "%1 perform %2 on %3 with %4", this, action_name, 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, action_name, 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" ) } );
}
};
Change Log
This section describes changes in this document, which will also reflect changes in the classes and methods it describes.
Reactor build 24273 (Sep 29 2024)
DEPRECATION NOTICE
The passing of a function as the second argument to Entity.registerAction()
is now deprecated and will be removed from a future release. Reactor will issue a warning in the log if this method is being used for an action. See Entity.registerAction()
above for more information.
- Add
Entity.refreshCapabilities()
and.refreshCapability()
- Add description/section for Controller default action implementations.
- Provide some detail about attribute and action definitions (YAML) in capabilities.
- Add description of action response handling in
Controller.performOnEntity()
.
Reactor Build 24267 (Sep 23 2024)
- Add
Entity.redefineAction()
Reactor Build 24138 (May 17 2024)
- Add new
util
functionshash()
,isVoid()
,stringOrRegexp()
. util.searchPath()
now takes arrays or strings containing delimited paths for first two arguments (paths to search and files to be located).util.findFiles()
now takes array for first argument (paths to search), and string or regular expression (i.e. an instance of RegExp) for second argument (match filename).util.deepCompare()
now works with JavaScriptSet
,Map
,Buffer
,RegExp
objects.util.binarySearch()
now accepts a function for its second argument (search value).- Add new
Entity.extendCapabilities()
method. Entity.extendCapability()
now takes additional arguments.- Clarify use and effect of
Entity.registerAction()
. - Clarify flow of control for
Entity.perform()
andController.performOnEntity()
, and the operation of default action handlers. - Add new
TaskQueue
methodssuspend()
,resume()
,abort()
andclear()
. - Clarify use of
TaskQueue
methodsfinish()
andsetPace()
.
Updated: 2024-Oct-05