The Z-Way core function engine is the so called JavaScript (JS) automation system. It uses the APIs of the technology-dependent 'drivers' and delivers all the functions and interface for running a Smart Home controller:
This chapter explains the different building blocks of the JS Engine:
Z-Way uses the JavaScript engine provided by Google referred to as V8. You find more information about this JavaScript implementation on
https://code.google.com/p/v8/.
V8 implements JavaScript according to the specification ECMA 5 .
Please note that this V8 core engine only implements the very basic JS functions and need to be extended to be usable in a Smart Home environment.
Z-Way extends the basic functionality provided by V8 with plenty of application-specific functions.
Javascript code can be executed on the server side and certain functions of the JS core are available on the client side as well since most modern web browsers have a built-in Javascript engine as well. The bridge between the server side JS and the web browser client is a built-in web server. This is the same embedded web server serving all the web browsers HTML pages etc.
There are three ways to run JavaScript code in Z-Way backend.
All options have their pros and cons. Running JS code via the browser is a very nice and convenient way to test things but the code is not persistent across Z-Way restarts.
Storing the it in a file allows to run it on Z-Way start (if 'executeFile("myfile.js")' is placed in main.js) but is not really convenient to distribute.
Writing a module requires more knowledge, but includes a nice graphical interface for App configuration. Upload your App in the Z-Way App Store for easy deployment and distribution of your App.
Check Z-Way App Store on
https://developer.z-wave.me/for more information. There are many other open source Apps made by the community.
Accessing a server side JS function from the web browsers client side is easy. Just call
http://YOURIP:8083/JS/Run/<any JS code>
Please note that all accesses using the embedded webserver require an authentication of the web browsers instance. Please refer to chapter 13.1 for details how to authenticate in the Z-Way web server.
Z-Way offers one central object with the name 'zway' . This object encapsulates all the Z-Wave variables and functions known from the Z-Wave Device API describes in chapter 11.3.
Hence its possible to use the very same functions of the Z-Wave Device API using the JS engine. The zway objects internal structure is shown in figure 11.2 and the data elements are describes in Annex 11.3.1.
The functions can be accessed using the web browsers function like
http://YOURIP:8083/JS/Run/zway.devices[x].*
Due to the scripting nature of JavaScript it is possible to 'inject' code at run time using the interface. Here a nice example how to use the Java Script setInterval function:
[Polling device \#2] /JS/Run/setInterval(function() { zway.devices[2].Basic.Get(); }, 300*1000);
This code will, once 'executed' as a URL within a web browser, calls the Get() command of the command class Basic of Node ID 2 every 300 seconds.
A very powerful function of the JS API is the ability to bind functions to certain values of the device tree. they get then executed when the value changes. Here an example for this binding. The device No. 3 has a command class SensorMultilevel that offers the variable level. The following call - both available on the client side and on the server side - will bind a simple alert function to the change of the variable.
[Bind a function] zway.devices[3].SensorMultilevel.data[1].val.bind(function() { debugPrint('CHANGED TO: ' + this.value + '\n'); });
Z-Way provides some extensions to the JS core that are not part of the ECMA functionality mentioned above.
The JavaScript implementation of Z-Way allows directly accessing HTTP objects.
The http request is much like jQuery.ajax(): r = http.request(options);
Here's the list of options:
headers: { "Content-Type": "text/xml", "X-Requested-With": "RaZberry/1.5.0" }
auth: { login: 'username', password: 'secret' }
Response (as stated above) is delivered either as function return value, or as callback parameter. It is always an object containing the following members:
Response data is handled differently depending on content type (if contentType on request is set, it takes priority over server content type):
http.request({ url: "http://server.com" (string, required), method: "GET" (GET/POST/HEAD, optional, default "GET"), headers: (object, optional) { "name": "value", ... }, auth: (object, optional) { "login": "xxx" (string, required), "password": "***" (string, required) }, data: (object, optional, for POST only) { "name": "value", ... } -- OR -- data: "name=value&..." (string, optional, for POST only), async: true (boolean, optional, default false), timeout: (number, optional, default 20000) success: function(rsp) {} (function, optional, for async only), error: function(rsp) {} (function, optional, for async only), complete: function(rsp) {} (function, optional, for async only) }); response: { status: 200 (integer, -1 for non-http errors), statusText: "OK" (string), url: "http://server.com" (string), contentType: "text/html" (string), headers: (object) { "name": "value" }, data: result (object or string, depending on content type) }
ZXmlDocument object allows converting any valid XML document into a JSON object and vice versa.
\{ name: "node_name", - mandatory text: "value", - optional, for text nodes attributes: { - optional name: "value", ... }, children: [ - optional, should contain a valid object of same type { ... } ] }
For example:
(new ZXmlDocument('<weather><city id="1"><name>Zwickau</name> <temp>2.6</temp></city> <city id="2"><name>Moscow</name><temp>-23.4</temp></city> </weather>')).root = { "children":[ { "children":[ { "text":"Zwickau", "name":"name" }, { "text":"2.6", "name":"temp" } ], "attributes":{ "id":"1" }, "name":"city" }, { "children":[ { "text":"Moscow", "name":"name" }, { "text":"-23.4", "name":"temp" } ], "attributes":{ "id":"2" }, "name":"city" } ], "name":"weather" }
x.findOne('/weather/city[@id="2"]') // returns only city tag for Moscow x.findOne('/weather/city[name="Moscow"]/temp/text()') // returns temperature in Moscow
x.findAll('/weather/city') // returns all city tags x.findAll('/weather/city/name/text()') // returns all city names
ZXmlDocument is returned from http.request() when content type is 'application/xml', 'text/xml' or any other ending with '+xml'. Namespaces are not yet supported.
crypto object provides access to some popular cryptographic functions such as SHA1, SHA256, SHA512, MD5, HMAC, and provides good random numbers.
rnd = (new Uint8Array(crypto.random(10)));
There are also a few shortcut functions for popular algorithms: 'md5', 'sha1', 'sha256', 'sha512'. For example, these calls are equivalent:
dgst = crypto.digest('sha256', data); dgst = crypto.sha256(data);
Key parameter is required.
If no data parameters specified, it returns a HMAC of an empty value. If more than one data parameter is specified, they're all used to calculate the result. Key and data parameters may be of different types (strings, arrays, ArrayBuffers). Return value is of type ArrayBuffer.
There are also a few shortcut functions for popular algorithms: 'hmac256', 'hmac512'. For example, these calls are equivalent:
dgst = crypto.hmac('sha256', key, data); dgst = crypto.hmac256(key, data);
Socket module allows easy access to TCP and UDP sockets from JavaScript. Both connection to distant ports and listening on local are available. This API fully mirrors into JavaScript POSIX TCP/IP sockets. This can be used to control third party devices like Global Cache or Sonos as well as emulating third party services.
To start communication, one needs to create socket and either connect it or listen it. onrecv method is called on data receive from remote, while send is used to send data to remote side.
The example below dumps to log file response to http://ya.ru:80/ (raw HTTP protocol is used as an example).
var sock = new sockets.tcp(); sock.onrecv = function(data) { debugPrint(data.byteLength); }; sock.connect('ya.ru', 80); sock.send("GET / HTTP/1.0\r\n\r\n");
Here is an example of TCP echo server on port 8888:
var sock = new sockets.tcp(); sock.bind(8888); sock.onrecv = function(data) { this.send(data); }; sock.listen();
And echo server for UDP:
var sock = new sockets.udp(); sock.bind(8888); sock.onrecv = function(data, host, port) { this.sendto(data, host, port); }; sock.listen();
Important! Callbacks can only be specified before the connection is established.
“this“ inside callbacks refers to the socket object itself.
Detailed description of Socket API:
Socket module also implements WebSockets (RFC 6455). WebSocket API is made to be compatible with browser implementations (some rarely used functions are not implemented, see below).
The example below implements basic application using the WebSockets client:
var sock = new sockets.websocket("ws://echo.websocket.org"); sock.onopen = function () { debugPrint('connected, sending ping'); sock.send('ping'); } sock.onmessage = function(ev) { debugPrint('recv', ev.data); } sock.onclose = function() { debugPrint('closed'); } sock.onerror = function(ev) { debugPrint('error', ev.data); }
Next example shows basic application using WebSockets server:
var sock = new sockets.websocket(9009); sock.onconnect = function () { debugPrint('client connected, sending ping'); } sock.onmessage = function(ev) { debugPrint('recv', ev.data); sock.send('pong'); } sock.onclose = function() { if (this === sock) { debugPrint('server websocket closed'); } else { debugPrint('client disconnected'); } } sock.onerror = function(ev) { debugPrint('error', ev.data); }
Detailed description of WebSocket API:
MQTT module allows to connect to MQTT broker from JavaScript. Both subscribtion to remote to topics and publishing own topics are possible. Unencrupted or TLS-encrypted TCP transport is supported (TLS requires libmosquitto 2.1.0 or upper).
The example below connects to a server using and publishes a topic.
var m = new mqtt("broker.emqx.io", 1883); m.onconnect = function() { debugPrint("Connected"); }; m.ondisconnect = function() { debugPrint("Disconnected"); }; m.onpublish = function() { debugPrint("Published"); }; it will work when the sent message is published; m.onsubscribe = function () { debugPrint("Subscribed"); }; m.onmessage = function (topic, message) { debugPrint("New topic " + topic + ": " + message); }; m.connect();
Important! Callbacks can only be specified before the connection is established.
“this“ inside callbacks refers to the mqtt object itself.
Detailed description of MQTT API:
You can to set the TSL settings before the connection is established. Configure the client for certificate based SSL/TLS support:
The MQTT object provides the following methods:
For debugging purposes there is an additional method:
This returns the list of items in the folder or undefined if the folder does not exist.
This returns one of the following values:
This function reads a file from the file system and loads it into the memory. The file must contain a valid JSON object. The only argument is the name of the file including relative pathname to the automation folder. Returns the full JSON object or null in case of error.
This function reads a file from the file system and returns its content as a string. The only argument is the name of the file including relative pathname to the automation folder. Returns null in case of error.
Loads and executes a particular JavaScript file from the local filesystem or executes JavaScript code represented in string (like eval in browsers).
The script is executed within the global namespace.
Remark: If an error occurrs during the execution, it won't stop from further execution, but erroneous scripts will not be executed completely. It will stop at the first error. Exceptions in the executed code can be trapped in the caller using standard try-catch mechanism.
The command system() allows executing any shell level command available on the operating system. It will return the shell output of the command. By default the execution of system commands is forbidden. Each command executed need to be permitted by putting one line with the starting commands in the file automation/.syscommands or in an different automation folder as specified in config.xml.
Data is saved in automation/storage folder. Filenames are made from object names by stripping characters but [a-ZA-Z0-9] and adding checksum from original name (to avoid name conflicts).
listExternalAccess returns array with names of all registered HTTP handlers.
Here is an example how to attach handlers for /what/timeisit and /what:
what = function() { return { status: 500, body: 'What do you want to know' }; }; what.timeisit = function() { return { status: 200, body: (new Date()).toString() } }; allowExternalAccess("what"); allowExternalAccess("what.timeisit");
Prints arguments converted to string to Z-Way console. Very useful for debugging. For convenience, one can map 'console.log()' to debugPrint().
This is how it was done in automation/main.js in Z-Way Home Automation engine:
var console = { log: debugPrint, warn: debugPrint, error: debugPrint, debug: debugPrint, logJS: function() { var arr = []; for (var key in arguments) arr.push(JSON.stringify(arguments[key])); debugPrint(arr); } };
<config> ... <debug-port>8183</debug-port> .... </config>
node-inspector debugger tool is required. It provides web-based UI for debugging similar to Google Chrome debug console.
You might want to run debugger tool on another machine (for example if it is not possible to install it on the same box as Z-Way is running on).
Use the following command to forward debugger port defined in config.xml to your local machine:
ssh -N USER@IP_OF_Z-WAY_MACHINE -L 8183:127.0.0.1:8183 (for RaZberry USER is pi)
Install node-inspector debugger tool and run it:
npm install -g node-inspector node-inspector –debug-port 8183
Then you can connect to
http://IP_OF_MACHINE_WITH_NODE_INSPECTOR:8080/debug?port=8183
If debugging is turned on, Z-Way gives you five seconds during startup to reconnect debugger to Z-Way (refresh the page of debugger Web UI within these five seconds). This allows you to debug startup code of Z-Way JavaScript engine from the very first line of code.
A virtual device is a data object within the JS engine. Virtual devices have properties and functions. Most virtual devices represent a physical device or a part of a physical device but virtual devices are not limited to this. Virtual devices can be pure dummy device doing nothing but pretenting to be a device (There is an app called 'DUMMY DEVICE' that works exactly like this). Virtual devices can also connect to services via TCP/IP.
The purpose of virtual devices is to unify the appearance on a graphical user interface and to unify the communication between them. At the level of virtual devices and EnOcean controller can switch a Z-Wave switch and trigger a rule in a cloud service.
Every virtual device is identified by a simple string type id. For all virtual devices that are related to physical Z-Wave devices the device name is auto-generated by the module (app) 'Z-Wave' following this logic:
The Node Id is the node id of the physical device, the Instance ID is the instance id of the device or '0' if there is only one instance. The command class ID refers to the command class the function is embedded in. The scale id is usually '0' unless the virtual device is generated from a Z-Wave device that supports multiple sensors with different scales in one single command class.
Virtual devices not generated by a Z-Wave device may have other Ids. They are either created by other physical device subsystems such as 433MHz or EnOcean or they are generated by a module (app).
Virtual devices can have a certain types. Table shows the different types plus the defines commands. Table 12.1 shows the list of current device types with their metrics and defines commands.
Before connecting, make sure to authenticate or save the token in Cookies.
[Connecting using WebSockets] var ws = new WebSocket("ws://localhost:8083"); ws.onopen = function() { console.log("connected"); }; ws.onclose = function() { console.log("disconnected"); }; ws.onmessage = function(event) { console.log(event.data); }; ws.onerror = function(event) { console.log(event); };
Once connected, the send command allows to send commands to the remote site. Convert the parameter object to a string first.
[Sending commands via WebSockets] ws.send(JSON.stringify({"event": "httpEncapsulatedRequest", "data": {"url": "/ZAutomation/api/v1/devices/DummyDevice_18/command/on", "method": "GET"}}))
Events are sent in the following format:
[Events format from WebSockets connection] {"type":"ws-reply","data":{"status":200,"body":"{\"data\":null,\"code\":200,\"message\":\"200 OK\",\"error\":null}","headers":{"Content-Type":"application/json; charset=utf-8","X-API-VERSION":"2.0.1"}}} {"type":"me.z-wave.devices.level","data":{"creationTime":1710555717,"creatorId":18,"customIcons":{},"deviceType":"switchBinary","firmware":"v4.1.2","h":-1669838584,"hasHistory":false,"id":"DummyDevice_18","location":0,"locationName":"globalRoom","manufacturer":"Z-Wave.Me","metrics":{"title":"Dummy Device Binary","icon":"switch","level"
Virtual devices can be access both on the server side using JS modules and on the client side using the JSON API. On the client they are encoded into a URL style for easier handling in AJAX code. A typical client side command in the vDev API looks like
http://YOURIP:8083/ZAutomation/api/v1/devices/ZWayVDev_6:0:37/command/off
'api' points to the vDev API function, 'v1' is just a constant to allow future extensions. The devices are referred by a name that is automatically generated from the Z-Wave Device API. The vDev also unifies the commands 'command' and the parameters, here 'off'.
On the server side the very same command would be encoded in a JavaScript style.
[Access vDevs] vdevId = vdev.id; vDev = this.controller.devices.get(vdevId); vDevList = this.controller.devices.filter(function(x) { return x.get("deviceType") === "switchBinary"; }); vDevTypes = this.controller.devices.map(function(x) { return x.get("deviceType"); });
In case the virtual device is an actor it will accept and execute a command using the syntax:
The name of the accepted command should depend on the device type and can again be defined free of restrictions when implementing the virtual device. For auto-generated devices derived from Z-Wave the following commands are typically implemented.
Virtual devices have inner values. They are called metrics. A metric can be set and get. Each virtual device can define its own metrics. Metrics can be level, title icon and other device specific values like scale (%, kWh, ...)
vDev.set("metrics:...", ...); vDev.get("metrics:...");
A Virtual Device (Vdev) is an instance of a VirtualDevice class' descendant which exposes set of metrics and commands (according to it's type/subtype). Virtual devices are the only runtime instances which is controllable and observable through the JS API.
Technically, VDev is a VirtualDevice subclass which concretize, overrides or extends superclass' methods.
// Important: constructor SHOULD always be successful BatteryPollingDevice = function (id, controller) { // Always call superconstructor first BatteryPollingDevice.super_.call(this, id, controller); // Define VDevs properties this.deviceType = "virtual"; this.deviceSubType = "batteryPolling"; this.widgetClass = "BatteryStatusWidget"; // Setup some additional metrics (many of them is setted up in a base class) this.setMetricValue("someMetric", "someValue"); } inherits(BatteryPollingDevice, VirtualDevice);
VDev class should always fill in the deviceType property and often fill in the deviceSubType property.
If the particular VDev class can be controller by the client-side widget, it should define widget's class name in the widgetClass property.
BatteryPollingDevice.prototype.performCommand = function (command) { var handled = true; if ("update" === command) { for (var id in zway.devices) { zway.devices[id].Battery && zway.devices[id].Battery.Get(); } } else { handled = false; } return handled ? true : BatteryPollingDevice.super_.prototype.performCommand.call(this, command); }
VDev itself mostly needed to handle commands, triggered by the events, system or the API.
In the example above you could see, that this VDev is capable of performing "update" command. But base class can be capable of performing some other commands, so the last l ine calls superclass' performCommand() method if the particular command wasn't handled by the VDev itself.
This extensibility provides the possibility to create a VDev class tree. Take a look at ZWaveGate module as an example of such tree.
// ...part of the BatteryPolling.init() method executeFile(this.moduleBasePath()+"/BatteryPollingDevice.js"); this.vdev = new BatteryPollingDevice("BatteryPolling", this.controller);
First line of code is loads and executes apropriate .js-file which provides BatteryPollingDevice class.
Secnd line instantiates this class.
The last line calls controller's registerDevice method to register and VDev instance.
[Register Device] vDev = this.controller.devices.create(vDevId, { deviceType: "deviceType", metrics: { level: "level", icon: "icon from lib or url" title: "Default title" } }, function (command, ...) { // handles actions with the widget });
Devices can be deleted or unregistered using the following command:
The metric - the inner variables of the vDev a changed by the system automatically. In order to perform certain functions on these changes the function needs to be bound to the change to the vdev. The syntax for this is
Unbinding then works as one can expect:
All communication from and to the automation modules is handled by events. An event is a structure containing certain information that is exchanged using a central distribution place, the event bus. This means that all modules can send events to the event bus and can listen to event in order to execute commands on them. All modules can 'see' all events but need to filter out their events of relevance. The core objects of the automation are written in JS and they are available as source code in the sub folder 'classes':
The file main.js is the startup file for the automation system and it is loading the three classes just mentioned. The subfolder /lib contains the key JS script for the Event handling: eventemitter.js.
The 'Event emitter' emits events into the central event bus. The event emitter can be called from all modules and scripts of the automation system. The syntax is:
controller.emit(eventName, args1, arg2,...argn)
The event name 'eventName' has to be noted in the form of 'XXX.YYY' where 'XXX' is the name of the event source (e.g. the name of the module issuing the event or the name of the module using the event) and 'YYY' is the name of the event itself. To allow a scalable system it makes sense to name the events by the name of the module that is supposed to receive and to manage events. This simplifies the filtering of these events by the receiver module(s).
Certain event names are forbidden for general use because they are already used in the existing modules. One example are events with the name cron.XXXX that are used by the cron module handling all timer related events.
Every event can have a list of arguments developers can decide on. For the events used by preloaded modules (first and foremost the cron module) this argument structure is predefined. For all other modules the developer is free to decide on structure and content. It is also possible to have list fields and or any other structure as argument for the event
One example of an issued event can be
emit(“mymodule.testevent”,”Test”,[“event1”,”event2”])
The controller object, part of every module, offers a function called 'on()' to catch events. The 'on(name, function())' function subscribes to events of a certain name type. If not all events of a certain name tag shall be processed a further filtering needs to be implemented processing the further arguments of the event. The function argument contains a reference to the implementation using the event to perform certain actions. The argument list of the event is handed over to this function in its order but need to be declared in the function call statement.
this.controller.on(“mymodule.testevent”, function (name,eventarray) )
The same way objects can unbind from events:
this.controller.off(“mymodule.testevent”, function (name,eventarray));
Notifications are a special kind of event to inform the user on the graphical user interface or out-of-band.. This means that normal events are typically describes with numbers or ids while notifications contain a human readable message.
The UI can be notified on the certain events.
The parameters define
The controller can act on notifications or disable them.
Beside the core functions encoded into the JS core there are extensions to this code called modules. Modules extend the JS core by providing internal or external ( visible to the user) functions.
Each modules code is located in a sub directory of the sub folder module as described
in chapter 11.4.1. The name of the subfolder equals the name of the module. The sub folder
contains files to define the behavior of the module.
This file contains the module meta-definition used by the AutomationController. It must be a valid JSON object with the following fields (all of them are required):
This script defines an automation module class which is descendant of AutomationModule base class. During initialization the module script must define the variable '_module' containing the particular module class.
Example of a minimal automation module:
[Minimal Module] function SampleModule (id, controller) { SampleModule.super_.call.init(this, id, controller); this.greeting = "Hello, World!"; } inherits(SampleModule, AutomationModule); _module = SampleModule; SampleModule.prototype.init = function () { this.sayHello(); // subscriptions and initializations }; SampleModule.prototype.stop = function () { // unsubscriptions and cleanup of allocated obkects }; SampleModule.prototype.sayHello = function () { debugPrint(this.greeting); };
The first part of the code illustrates how to define a class function named SampleModule that calls the superclass' constructor. Its highly recommended not to do further instantiations in the constructur. Initializations should be implemented within the 'init' function.
The second part of the code is almost immutable for any module. It calls prototypal inheritance support routine and it fills in _module variable.
The third part of the sample code defines module's init() method which is an instance initializer. This initializer must call the superclass's initializer prior to all other tasks. In the initializer module can setup it's private environment, subscribe to the events and do any other stuff. Sometimes, whole module code can be placed withing the initializer without creation of any other class's methods. As the reference of such approach you can examine AutoOff module source code.
After the init function a module may contain other functions. The 'sayHello' function of the Sample Module shows this as example.
All modules in Z-Way are designed the same way using the same file structure but they serve different purposes and they are of different importance:
All time driven actions need a timer. The Z-Way automation engine implement a cron-type timer system as a module as well. The basic function of the cron module is
The registration and deregistration of events is also handled using the event mechanism. The cron module is listening for events with the tags 'cron.addTask' and 'cron.removeTask'. The first argument of these events are the name of the event fired by the cron module. The second argument of the 'addTask' event is an array desricing the times when this event shall be issued. It has the format:
The object
{minute : null, hour : null, weekDay : null, day : null, month : null}
will fire every minute within every hour within every weekday on every day of the month every month. Another example of an event emitted towards the cron module for registering an timer event can be found in the Battery Polling Module:
[Registering a Battery Polling Command] this.controller.emit("cron.addTask", "batteryPolling.poll", { minute: 0, hour: 0, weekDay: this.config.launchWeekDay, day: null, month: null });
This call will cause the cron module to emit an event at night (00:00) on a day that is defined in the configuration variable this.config.launchWeekDay, e.g. 0 = Sunday.
The 'cron.removeTask' only needs the name of the registered event to deregister.
The whole mapping of Z-Wave devices into virtual devices is handled by a module called
'ZWAVE'. This module is quite powerful. It does not only manage the mapping but handles
various Z-Wave specific functions such as timing recording, etc.