Shelly Script Language Features
Shelly Scripts run on a modified version of Espruino. While the previous JS interpreter running on Shelly supported a subset of JavaScript, Espruino interpreter is closer to the JavaScript standard.
The interpreter is feature compatible with one it's replacing. There should be no visible problem with scripts written and working before v 1.0.
More information about the API for managing Scripts on a Shelly device can be found here.
Supported
- global scope variables,
let
,var
- function binding
String
objectNumber
objectFunction
Array
objectMath
objectDate
objectnew
,delete
operatorsObject.keys
- Exceptions
Not supported
- Hoisting
- Classes as in ES6, function prototypes are supported
- Promises and async functions
ArrayBuffer
Specifics
arguments.length
will return number of arguments passed if more than defined, or number of defined, if the number of arguments passed to function are less than defineddelete
operator works without brackets onlyFunction
supports an additional methodreplaceWith
For detailed information on which methods are supported you can consult Espruino documentation. Please consider that we are only using the language interpreter and many of the system and hardware specific Espruino functions are not and will never be implemented in Shelly.
Strings
TL;DR
Strings support encoding bytes in the form \xHH
escape sequence. Strings don't support encoding using \u
escape sequence. UTF-8 works.
String encoding
String literals and objects in regular JS implementations use 16-bit words for character storage and are treated as UTF16-encoded text. In contrast, Shelly Scripts use byte arrays for string storage. This difference is not noticeable if strings are used to hold plain ASCII-encoded text but becomes important when strings are used to store characters outside of the ASCII set or arbitrary binary data.
The philosophy implemented in Shelly Scripts is to allow arbitrary binary data to be stored in strings and use UTF8 whenever encoding to byte streams or decoding input is involved. We thus optimize for memory usage, provide backward compatibility and Unicode support (in the form of UTF8 encoding) at the cost of some differences with standard JavaScript.
String literals
Script source code must be UTF8-encoded. String literals can thus contain Unicode text encoded in the UTF8 source. It is possible to encode arbitrary bytes in string literals using the \xHH
escape sequence, where HH
is the hexadecimal representation of a byte. Note, that this differs from regular JS implementations where the value is interpreted as a Unicode codepoint between 0 and 255.
String literals do not support the \uHHHH
and \u{HHHHHH}
escapes. Placing Unicode text in literals should be done by just using it's UTF8 representation in the source code.
Examples:
// a 4-byte long binary blob
let some_bytes = "\x00\x0a\xa0\xaa";
// UTF8-encoded Cyrillic string
let some_name = "Шели";
// UTF8 sequences as raw bytes
let an_emoji = "\xF0\x9F\x98\xB9"
// the following is not supported
let no_emoji = "\u{1F4A9}";
let a_letter = "\u0448";
Strings in JSON
JSON documents are UTF8-encoded, as defined in RFC 8259. Shelly Scripts follow the standard rules for parsing and serializing json.
JSON.parse()
- expects UTF8-encoded input and will raise an exception when users attempt to parse an invalid UTF8 string or strings containing invalid
\uHHHH\uHHHH
surrogate pairs \uHHHH
escapes and surrogate pairs encoded as\uHHHH\uHHHH
are transcoded to their UTF8 representation in the corresponding string variable.
- expects UTF8-encoded input and will raise an exception when users attempt to parse an invalid UTF8 string or strings containing invalid
JSON.stringify()
- encodes control characters as required by RFC 8259
- will raise an exception if it is asked to stringify a string with invalid UTF-8 contents
- will return a valid UTF-8 encoded string
To transfer binary data with JSON, use atob()
and btoa()
to encode the binary blobs to base-64 and back.
Shelly APIs
Shelly.call()
To interact with the local device, JS code can invoke RPC methods using a "local" RPC channel:
Shelly.call(method, params[, callback[, userdata]]) -> undefined
Property | Type | Description | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| string | Name of the method to invoke | |||||||||||||||
| object or string | Parameters | |||||||||||||||
| function or null or undefined | If it is a function, will be invoked when the call completes
| |||||||||||||||
| any type | Can be used to pass data to the callback |
- This method doesn't return a value. If invoked with invalid arguments the script is aborted.
Shelly.addEventHandler()
and Shelly.addStatusHandler()
These methods allow JS code to react to internal events. These are identical to the events reported through RPC notifications as NotifyStatus
and NotifyEvent
. The signatures are identical:
Shelly.addEventHandler(callback[, userdata]) -> subscription_handle
Shelly.addStatusHandler(callback[, userdata]) -> subscription_handle
Property | Type | Description | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
| function | Will be invoked when the respective event occurs
| |||||||||
| any type | Can be used to pass data to the callback |
- Return value: On success, returns a handle which can be used to remove the listener with
Shelly.removeEventHandler(subscription_handle)
orShelly.removeStatusHandler(subscription_handle)
respectively. If invoked with invalid arguments the script is aborted.
Shelly.removeEventHandler()
and Shelly.removeStatusHandler()
Shelly.removeEventHandler(subscription_handle) -> boolean
Shelly.removeStatusHandler(subscription_handle) -> boolean
Property | Type | Description |
---|---|---|
| number | The handle returned by previously called corresponding |
- Return value: Returns
true
if the handle is found and the listener is removed,false
if handle is not found orundefined
if the handle is invalid.
Shelly.emitEvent()
This method creates an event which is broadcasted to all persistent RPC channels.
Shelly.emitEvent(name, data) -> undefined
Property | Type | Description |
---|---|---|
| string | Name of the event |
| scalar or object or array | payload of the event. Any valid JSON value is allowed. |
- This method doesn't return a value. If invoked with invalid arguments the script is aborted.
Shelly.emitEvent("this_happened", {"what":"when", "why": 42});
The above code will trigger a notification to be emitted:
{
"component": "script:1",
"id": 1,
"event": "this_happened",
"data": {
"why": 42,
"what": "when"
},
"ts": 1657878122.44
}
Shelly.getComponentConfig()
Shelly.getComponentConfig(type_or_key, id) -> object or null
Property | Type | Description |
---|---|---|
| string | Component type or key |
| number | Numeric |
- Return value: an object with the current configuration of the component,
null
if component was not found. If invoked with invalid arguments the script is aborted.
let time_server = Shelly.getComponentConfig("sys").sntp.server;
print("My time comes from", time_server);
Shelly.getComponentStatus()
Shelly.getComponentStatus(type_or_key, id) -> object or null
Property | Type | Description |
---|---|---|
| string | Component type or key("component:id"). Component type must be in lowercase. |
| number | Numeric |
- Return value: an object with the current status of the component,
null
if component was not found. If invoked with invalid arguments the script is aborted.
let uptime = Shelly.getComponentStatus("sys").uptime;
print("I was awake", uptime, "seconds");
Shelly.getDeviceInfo()
- Return value: the
DeviceInfo
object.
Shelly.getCurrentScriptId()
- Return value: number - the
id
of the current script.
Virtual
APIs
Via an instance of Virtual
users obtain a handle to a virtual component instance. The handle can be used for:
- reading and writing the value of a virtual component
- reading the status
- reading and updating configuration
Quick example:
let a_number = Virtual.getHandle("number:200");
let some_text = Virtual.getHandle("text:200");
a_number.setValue(42);
some_text.setValue("Hello World!");
if (a_number.getConfig().persisted != true) {
a_number.setConfig({persisted: true});
}
Virtual.getHandle(key)
Returns an instance of Virtual
or null
on errors.
Virtual.setValue(new_value)
Set the value of the virtual component. Note, that the value will be applied asynchronously as soon as the current execution context of the script exits.
Button
and Group
components do not support this method. If invoked on a handle on those types an exception will be raised.
Virtual.getValue()
Returns the value of the virtual component or undefined
on error. Not supported for Button
and Group
.
Virtual.getStatus()
Returns the status of the virtual component. undefined
is returned if the component no longer exists. Identical to the corresponding GetStatus(id)
RPC method.
Virtual.getConfig()
Returns the configuration of the virtual component. undefined
is returned if the component no longer exists. Identical to the corresponding GetConfig(id)
RPC method.
Virtual.setConfig(config_obj)
Sets the configuration of the virtual component. undefined
is returned if the component no longer exists. Identical to the corresponding SetConfig(id)
RPC method.
Virtual.on(event, callback)
Attach an event handler to a virtual component instance. Callback is given an object with event information. For all events, it contains a source
property, indicating where the action was triggered.
For Number
, Text
, Enum
the only event is change
. Event info will contain value
-- the new value of the virtual component.
// for event `change` the `ev_info` object contains
some_text.on("change", function(ev_info) {
print("new value is", ev.value);
print("set from", ev.source);
});
For Button
, supported events are:
single_push
double_push
triple_push
long_push
a_button.on("single_push", function(ev) { a_number.setValue(25); });
a_button.on("double_push", function(ev) { a_number.setValue(50); });
a_button.on("long_push", function(ev) { a_number.setValue(75); });
Utilities
Timer
Timer
global object can be used for one-shot delayed code execution, or to run some code periodically.
Timer.set()
To arm a timer, use:
Timer.set(period, repeat, callback[, userdata]) -> timer_handle
Property | Type | Description | ||||||
---|---|---|---|---|---|---|---|---|
| number | In milliseconds | ||||||
| boolean | If | ||||||
| function | To be invoked when the timer fires
| ||||||
| any type | Can be used to pass data to the callback |
- Return value: On success, returns a handle which can be used to stop the timer with
Timer.clear(timer_handle)
. If invoked with invalid arguments the script is aborted.
Timer.clear()
To stop the execution of a timer, use:
Timer.clear(timer_handle) -> boolean or undefined
Property | Type | Description |
---|---|---|
| handle | handle previously returned by |
- Return value:
true
if the timer was armed and destroyed,false
if no such timer existed orundefined
if the giventimer_handle
was not valid.
btoh()
To convert a string to hexadecimal representation, use:
btoh(data) -> string
Property | Type | Description |
---|---|---|
| string | The string to convert |
- Return value: A string that represents every byte of
data
as 2 heaxadecimal digits.
MQTT support
MQTT
MQTT
global object provides MQTT functionality. JS code can monitor connection status, subscribe and publish to miltiple topics.
MQTT.isConnected()
- Return value:
true
if device is connected to a MQTT broker orfalse
otherwise.
MQTT.subscribe()
Subscribes to a topic on the MQTT broker.
MQTT.subscribe(topic, callback[, userdata]) -> undefined
Property | Type | Description | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| string | The topic filter to subscribe to | ||||||||||||
| function | Function to be called when a message is published on the topic
| ||||||||||||
| any type | Can be used to pass data to the callback |
- This method doesn't return a value. It will throw an exception if:
- Parameters
topic
andcallback
are missing or not of the correct type - MQTT is disabled
- The limit for number of subscriptions has been reached
topic
is not a valid topic filter
- Parameters
If a subscription with the same topic filter already exists, its callback and user data will be replaced by the ones given.
If an incoming message matches multiple subscriptions, we invoke the callback of the first match only. Subscriptions are ordered in the sequence they are added by the script.
MQTT.unsubscribe()
Unsubscribes from a topic previously subscribed, can be called only for topics subscribed in the same script.
MQTT.unsubscribe(topic) -> boolean
Property | Type | Description |
---|---|---|
| string | The topic to unsubscribe from |
- Return value:
true
if unsubscribed orfalse
if subscription to the topic does not exist. The script is aborted if the argument is not valid.
MQTT.publish()
Publishes a message to a topic.
MQTT.publish(topic, message[, qos[, retain]]) -> boolean
Property | Type | Description |
---|---|---|
| string | The topic to publish |
| string | Тhe message to publish |
| integer | Can be |
| boolean | If |
- Return value:
true
if the message was enqueued for publishing,false
if MQTT is currently disconnected. In the latter case, the message is dropped.
MQTT.setConnectHandler()
Registers a handler for the MQTT connection established event.
MQTT.setConnectHandler(callback[, userdata])
Property | Type | Description | ||||||
---|---|---|---|---|---|---|---|---|
| function | Function to be called when event is received
| ||||||
| any type | Can be used to pass data to the callback |
- This method doesn't return a value. If invoked with invalid arguments the script is aborted.
MQTT.setDisconnectHandler()
Registers a handler for the MQTT connection closed event.
MQTT.setDisconnectHandler(callback[, userdata])
Property | Type | Description | ||||||
---|---|---|---|---|---|---|---|---|
| function | Function to be called when event is received
| ||||||
| any type | Can be used to pass data to the callback |
- This method doesn't return a value. If invoked with invalid arguments the script is aborted.
Bluetooth support
Since version 0.12.0
A global BLE
object provides a namespace for for various different layers of the bluetooth protocol stack. Shelly devices currently support the scanner
role.
BLE.Scanner
The Scanner
object provides access to functionality related to the discovery of bluetooth devices. It allows the script to start a scan and listen for scan events. It defines the following constants:
Scan events passed to subscribers:
BLE.Scanner.SCAN_START
= 0;BLE.Scanner.SCAN_STOP
= 1;BLE.Scanner.SCAN_RESULT
= 2;
duration_ms
value for perpetual scanning:BLE.Scanner.INFINITE_SCAN
= -1;
Processing advertisement packets can be very resource intensive, especially in environments with many broadcasters and advertisers. It is recommended that scripts filter advertisement data for specific devices or device types to prevent memory and bandwidth starvation.
Since version 1.0.0 the scanner will silence identical scan results for a 3-second window. Scripts which have subscribed for scan events will receive the first result, but any results from the same MAC, ADV packet payload and SCAN_RESP packet payload will be ignored and will not invoke the event handler for the next 3 seconds. This is done to mitigate the risk of resource starvation which a high rate of scan results may cause.
BLE.Scanner.Subscribe()
Subscribes for scan events and register a listener. A script must subscribe in order to receive events, can subscribe at any time, regardless of the status of the scan or previous subscription. Only one subscription can be active in a script and previous subscriptions are replaced by a new one.
BLE.Scanner.Subscribe(callback[, userdata]) -> undefined
Property | Type | Description | ||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| function or null | Function invoked for each scan event. Specify
| ||||||||||||||||||||||||||||||||||||||||||||||||
| any type | Can be used to pass data to the callback |
- This method doesn't return a value. If invoked with invalid arguments the script is aborted.
BLE.Scanner.Start()
Starts a scan and optionally subscribe for scan events.
Scan options allow tuning for scan timings, but some restrictions apply:
- scan window cannot be longer than 1/3 of scan interval
- the maximum scan window is 50 ms, but 30 ms seems to be optimal
- duration must be at least 3 scan intervals long
If these conditions are not met scanning will not start. In the future, some of these options may not be tunable or the device may choose to modify them for performance and compatibility with other firmware features. It is best to use defaults.
To run a perpetual scan, invoke with a single option: {duration_ms: BLE.Scanner.INFINITE_SCAN}
.
BLE.Scanner.Start(options[, callback[, userdata]]) -> object or null
Property | Type | Description | ||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| object | object with scan options. All options are optional and may be omitted, substituting with defaults.
| ||||||||||||||||||||||||||||||||||||||||||||||||
| function | Function invoked for each scan event. If specified the call will also subscribe for scan events.
| ||||||||||||||||||||||||||||||||||||||||||||||||
| any type | Can be used to pass data to the callback |
- Return value: object with the options of the started scan or
null
if start failed. Start will fail also if there is a scan in progress. If invoked with invalid arguments the script is aborted.
BLE.Scanner.Stop()
Stops a running scan. Only the script that started the scan can stop it.
- Return value:
true
if scan is successfully stopped orfalse
if there is an error.
BLE.Scanner.isRunning()
- Return value:
true
if currently there is a running scan orfalse
otherwise.
BLE.Scanner.GetScanOptions()
- Return value: object with the options of the running scan if there is one or the default options otherwise.
BLE.GAP
The GAP
object is responsible for the GAP layer of the bluetooth protocol. It provides helper functions for parsing advertisement data. It defines the following constants:
BLE.GAP.ADDRESS_TYPE_PUBLIC
= 0x1;BLE.GAP.ADDRESS_TYPE_RANDOM_STATIC
= 0x2;BLE.GAP.ADDRESS_TYPE_RANDOM_NON_RESOLVABLE
= 0x3;BLE.GAP.ADDRESS_TYPE_RANDOM_RESOLVABLE
= 0x4;BLE.GAP.EIR_FLAGS
= 0x1;BLE.GAP.EIR_SERVICE_16_INCOMPLETE
= 0x2;BLE.GAP.EIR_SERVICE_16
= 0x3;BLE.GAP.EIR_SERVICE_32_INCOMPLETE
= 0x4;BLE.GAP.EIR_SERVICE_32
= 0x5;BLE.GAP.EIR_SERVICE_128_INCOMPLETE
= 0x6;BLE.GAP.EIR_SERVICE_128
= 0x7;BLE.GAP.EIR_SHORT_NAME
= 0x8;BLE.GAP.EIR_FULL_NAME
= 0x9;BLE.GAP.EIR_TX_POWER_LEVEL
= 0xA;BLE.GAP.EIR_DEVICE_ID
= 0x10;BLE.GAP.EIR_SERVICE_DATA_16
= 0x16;BLE.GAP.EIR_SERVICE_DATA_32
= 0x20;BLE.GAP.EIR_SERVICE_DATA_128
= 0x21;BLE.GAP.EIR_URL
= 0x24;BLE.GAP.EIR_MANUFACTURER_SPECIFIC_DATA
= 0xff;
BLE.GAP.parseName()
Parses device name from advertisement data or scan response.
BLE.GAP.parseName(data) -> string
Property | Type | Description |
---|---|---|
| string | data to parse, should be either advertisment data or scan response. |
- Return value: extracted name, may be empty string if data is not available. If invoked with invalid arguments the script is aborted.
BLE.GAP.parseManufacturerData()
Parses manufacturer data from advertisement data or scan response.
BLE.GAP.parseManufacturerData(data) -> string
Property | Type | Description |
---|---|---|
| string | data to parse, should be either advertisment data or scan response. |
- Return value: extracted data, may be empty string if data is not available. If invoked with invalid arguments the script is aborted.
BLE.GAP.ParseDataByEIRType()
Parses data for specified EIR type (Extended Inquiry Response) from advertisement data or scan response.
BLE.GAP.ParseDataByEIRType(data, type) -> string
Property | Type | Description |
---|---|---|
| string | data to parse, should be either advertisment data or scan response. |
| number | EIR type, should be one of the defined constants. |
- Return value: extracted data, may be empty string if data is not available. If invoked with invalid arguments the script is aborted.
BLE.GAP.HasService()
Searches for specified service UUID in the advertisement data or scan response.
BLE.GAP.HasService(data, uuid) -> boolean
Property | Type | Description |
---|---|---|
| string | data to parse, should be either advertisment data or scan response. |
| string | service UUID to check. |
- Return value:
true
if advertisement data or scan response lists the specified service uuid orfalse
otherwise. If invoked with invalid arguments the script is aborted.
BLE.GAP.ParseServiceData()
Parses service data string for the specified service UUID if present in the advertisement data or scan response.
BLE.GAP.ParseServiceData(data, uuid) -> string
Property | Type | Description |
---|---|---|
| string | data to parse, should be either advertisment data or scan response. |
| string | service UUID to check. |
- Return value: extracted data, may be empty string if data is not available. If invoked with invalid arguments the script is aborted.
UUIDs can be 16, 32 or 128 bits. They are represented by hexadecimal strings of the correponding length with lowercase hexadecimal digits. 128-bit UUIDS are represented in format XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
.
HTTP handlers
HTTPServer
HTTPServer
is a global object which allows a script to register handlers to incoming HTTP requests on specific to the script endpoints. The URL of the endpoint follows the format http://<SHELLY_IP>/script/<script_id>/<endpoint_name>
.
HTTPServer.registerEndpoint()
Registers an endpoint with a corresponding handler
HTTPServer.registerEndpoint(endpoint_name, callback[, userdata]) -> string
Property | Type | Description | ||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| string | The name of the endpoint to register | ||||||||||||||||||||||||||||||||||||||||||
| function | Function to be called when a request comes on the registered endpoint
| ||||||||||||||||||||||||||||||||||||||||||
| any type | Can be used to pass data to the callback |
- Return value: the part of the endpoint URL following the
SHELLY_IP
. The script is aborted if the arguments are not valid.
Handling of POST
and PUT
requests with payload of type multipart/*
are not supported. Response to such requests is HTTP
status code 415
(Unsupported Media Type).
The HTTP server cannot process requests of total size greater than 3072
bytes. This includes the HTTP
request line, headers and body. In this case connection is reset and no response is sent.
Content-Length
and Connection: close
headers are automatically set in the response and and override any such headers set by the script. Content-Type
header defaults to text/plain
if not set by the script.
HTTP transaction (request and response) times out after 10 seconds if repsonse is not sent. If timeout is reached response of HTTP
status code 504
(Gateway Timeout) is automatically sent and transaction is cancelled (send
method will return false
). No more than 5 concurrent transactions are allowed. If this limit reached further requests are not processed and response of HTTP
status code 503
(Service Unavailable) is sent to the client.
HTTP endpoints exposed through scripts will require authentication if enabled on the device
Resource Limits
There are some limitations of the resources used in a script. At the moment, these are as follows:
- No more than 5 timers used in a script;
- No more than 5 subscriptions for events used in a script;
- No more than 5 subscriptions for status changes used in a script;
- No more than 5 RPC calls used in a script.
- No more than 10 MQTT topic subscriptions used in a script.
- No more than 5 HTTP registered endpoints used in a script.
Error Handling
When the script contains errors (either a javascript error or parameters error) its execution is aborted. An error message is printed on the console and a status change event is issued with information of the type of error. This information is also avalibale in the Script.GetStatus
RPC call. When the error affects the behavior of script API this is reflected in the documentation.
A special case of error is when a script causes a device crash. The script causing the crash is detected during the reboot after the crash and is disabled. The error is reported in a status change event and also in the Script.GetStatus
RPC call.
Known Issues
Non-blocking execution
Shelly scripts are executed in an environment which shares CPU time with the rest of the firmware. Code in the scripts runs on the main system task and is not allowed to block for long. This is why any APIs which can potentially take a long while are callback-based. Still, it is possible to write code which will hoard the CPU for longer then acceptable. Such code may cause issues with other firmware features, communication or even cause the device to crash. One obvious example is an infinite (or near infinite) loop:
let n = 0;
while (n < 500000) {
n = n + 1;
}
If a script manages to crash the device the system will detect this and disable the script at the next boot.
Limited levels of nested anonymous functions
A limitation of the javascript engine that it cannot parse too many levels of nested anonymous functions. With more than 2 or 3 levels the device crashes when attempting to execute the code. To avoid this problem it is recommended that asynchronous callback functions are defined at the top level and passed as a named reference. Also, where possible prefer synchronous calls like Shelly.getComponentStatus
and Shelly.getComponentConfig
to avoid the need for async callbacks altogether.
For example, instead of using an anonymous function for a callback:
Shelly.call(
"HTTP.GET",
{url: "http://example.com/"},
function(result, error_code, error_message) {
if (error_code != 0) {
// process error
} else {
// process result
}
});
Prefer a named function:
function processHttpResponse(result, error_code, error) {
if (error_code != 0) {
// process error
} else {
// process result
}
}
Shelly.call("HTTP.GET", {url: "http://example.com/"}, processHttpResponse);