Scripts

Sfera scripting language is a simple and efficient way to bind events conditions to your customized control logic.
It supports on-the-fly modification of code while Sfera is running allowing you to see the effects of your script as soon as you save it.

To create a script, add a file with extension .ev into the scripts directory. It is possible (and recommended) to create a different script file for different sections of your control logic.

Syntax and semantics

A script file can contain several rules, each rule has the following structure:

<trigger_condition> : { <action> }

Whenever an event is generated, all the trigger conditions referring to that event are evaluated and, if true, the corresponding action (JavaScript code) is executed.

Some examples:

myLights.light(1).level > 0 : { log.info("Light 1 is ON"); }

myLights.light(1).level >= 20 && myLights.light(2).level == 0 : {
    // something...
}

Trigger conditions

A trigger condition can be an event condition or a logical combination of conditions.

Event conditions can be of two types: persistent or transient.
Persistent conditions are comparisons on nodes’ properties values, for instance:

node.numberProperty == 1
node.numberProperty < 3
node.booleanProperty == true
node.stringProperty != 'foo'

They evaluate to true if the current value of the specified node property (i.e. the value of the latest event with such ID) respects the specified condition.

Values used in comparisons can be of the following types:

  • Number: equivalent to the double Java type
  • Boolean: true or false
  • String: quoted or double-quoted sequence of characters (e.g. “Hello world” or ‘Ciao mondo!’)
  • unknown: special value indicating an unknown state

The available comparison operators available are:

  • ==: equivalence
  • !=: different from
  • >: greater than
  • <: less than
  • >=: greater or equal to
  • <=: less or equal to

The == and != operators can be used with any of the above types. The others can be used only with numbers and Strings; for numbers they indicate the usual mathematical comparisons, for strings they represent lexicographical comparisons.

On the other hand, transient conditions are simply event IDs and evaluate to true whenever an event with that ID is triggered.
For instance:

node.numberProperty : { ... }

is a transient condition that triggers every time an event with ID node.numberProperty is generated.

Here are some examples of the use of persistent and transient conditions:

node.foo > 1

True when an event with ID node.foo is triggered and its value is a number greater than 1.

node.foo != "error"

True when a node.foo event is triggered and its value is a String different from “error”.

node.foo != unknown

True when a node.foo event is triggered and its value is known (of any type).

node.foo == 1 && node.bar == 0

True when node.foo or node.bar events are triggered and their last values are respectively 1 and 0.

node.foo && node.bar == 0

True only when a node.foo event is triggered and the last value of node.bar is 0.

node.foo || node.bar == 0

True whenever node.foo triggers or when node.bar triggers with a value of 0.

node.foo && node.bar

Never true. Two transient conditions can never be true at the same time.

node.foo || node.bar

True whenever node.foo or node.bar trigger with any value.

node.foo && (node.foo == 1 && node.bar == 0)

True only when node.foo triggers with a value of 1 and the last value of node.bar is 0.

A transient event can also specify higher level nodes IDs to subscribe to a range of events.
For instance, let’s consider again the lighting system driver example that generated events with IDs like:

myLights.light(1).level
myLights.light(2).healthy

Here are some transient conditions you can use:

  • myLights: triggered for all the events generated by the myLights driver instance
  • myLights.light: triggered for any event on any light of the system
  • myLights.light(2): triggered for any event on light 2

Logical combinations of conditions follow the same syntax and rules of the JavaScript language.

The nodes and respective events generated by drivers and apps are described in the drivers/apps’ documentation.
Refer to Utility nodes for details on available system nodes.

Actions

Actions are pure JavaScript snippets of code with access to the underlying Java framework of Sfera. The variables scope of actions is by default populated with some bindings to nodes and utility objects.

Let’s start with a Hello World action:

system.state == 'ready' : { log.info("Hello, World!"); }

The variable log is bounded to the system logger, which is your best friend when debugging your code and when inspecting what happened last Friday at 5 AM in your installation…

The above code just adds an INFO-level log entry to the logs, with “Hello, World!” as message.

Another important variable is _e, which is bounded to the event object that triggered the action:

myLights.light(1).level : {
    if (_e.value < 40 && _e.value > 60) {
        log.warn(_e.id + " is in an impossible state!!");
    }
}

Further, all driver instances are bounded to a variable whose name is equal to the instance ID:

myLights.light(1).level : {
    myLights.light(2).level = _e.value;
}

In the above action myLights is the driver instance Java object.

As for triggers, the actions (methods) that can be called on driver instances are described in the driver’s documentation.

Sfera uses the Oracle Nashorn embedded JavaScript scripting engine to run the action snippets.

From the scripts it is possible to access to all the Java classes available to the Sfera environment. For simplicity, some variables in the global scope of the engine are bound to Sfera nodes and utility objects.

Logging

As anticipated above, each script file has a global log variable bound to an instance of a logger specific to the file.
The log object is an instance of the Logger interface. Here are some examples of its use:

log.info("Hello, World!");
log.info("Hello, {}. My name is {}", yourName, myName);
log.debug("Technical message here");
log.warn("Something weird happened...");
log.error("This is really bad!");

Calls to the above methods will create log entry with the specified level and with a logger name set to scripts.<path>.<to>.<file>.<file_name>.

Trigger event

Each action has the local variable _e bound to the Event object that triggered that action.
Among others, the Event interface exposes the following methods:

  • getId(): returns the ID of the event
  • getSource(): returns the Node that generated that event
  • getTimestamp(): returns the timestamp (in milliseconds) of the moment the event was generated
  • getValue(): returns the value of this event

The actual event that triggers an action will be an instance of a class implementing the Event interface thus, other than the above methods, it may expose any other method to provide customized information about the event and/or utility methods to perform some action in response to the event.

For instance an hypothetical MessageEvent generated by a GSM driver instance may provide a method getSender() to retrieve the number of the sender and a reply() method to quickly send a reply.

Nodes

Each node (e.g. driver instance) is referenced by a global variable named as the node’s ID.
You can call all the public methods exposed by the node, which generally have names similar to the corresponding events IDs it generates.

Here is an example of a possible driver instance:

myLights.light(1).level : {
    myLights.light(2).setLevel(_e.getValue());
}

In the above action myLights is the driver instance Java object that exposes a method light() which returns an object (e.g. an instance of a Light class) that, in turn, has a method setLevel() accepting an integer value as parameter.
This script simply aligns the level of light 2 to the level of light 1.

The methods exposed by driver instances are described in the driver’s documentation.

Syntactic sugar

The script engine infers properties from getters and setters methods of Java objects. All getXY() and setXY(value) methods are treated as properties.

For instance:

myLights.light(2).setLevel(_e.getValue());

can be written as:

myLights.light(2).level = _e.value;

Creating Nodes

You can define your own nodes that trigger custom events and/or expose methods like any driver instance.
Here is an example:

import sfera.js;

init {
    var myNode = new ScriptNode("myNode");
    myNode._num = 852;

    myNode.doSomething = function() {
        log.info("{} is doing something. Here is my number: {}", this.id, this._num);
        return true;
    };
}

system.state == "ready" : {
    myNode.postEvent("hello", "world");
}

myNode.hello : {
    log.info(_e.source.id + ": " + _e.value);
    myNode.doSomething();
}

As you can see, you need to import the sfera.js library that includes the definition of the ScriptNode class. This is a simple base class for all the nodes created in scripts. The constructor requires the ID of the node to be created and, when called, takes care of registering the node in the system. It only has two functions: getId() that returns the ID of the node and postEvent(id, value) used to trigger events on the system Bus.
Once you have instantiated your node you may add any property or function you need.

Bus

The Bus variable is bound to the system Bus class.
It can be used, for instance, to retrieve the state of the nodes:

var level = Bus.getValueOf("myLights.light(2).level");

Scopes

Other than the variables described above, every variable defined in an action has a scope limited to that snippet of code.

some.event : {
    var x = 10;
    log.info("x is " + x);
}

another.event : {
    log.info("x is " + x);
}

In the above example the variable x is only available in the first action. The second action will yield a reference error.

It is possible to define variables and functions that are available to all the actions contained in the same file. To this end, you can add an init block at the beginning of the script file:

init {
    var x = 10;

    function add(p1, p2) {
        return p1 + p2;
    }
}

some.event : {
    log.info("x is " + x);
    x = 20;
}

another.event : {
    log.info("3 + 5 = " + add(3, 5));
}

The above code will yield no error.

Moreover, it is possible to import external JavaScript libraries:

scripts/myLib.js

function add(p1, p2) {
    return p1 + p2;
}

scripts/myRules.ev

import myLib.js;

some.event : {
    log.info("3 + 5 = " + add(3, 5));
}

The import keyword creates a reference to the bindings of the specified JavaScript file. Note that in a imported file it is possible to define variables too, which will be accessible (read/write) to all the scripts importing it.

The path of imported files is considered relative to the location of the current file.