Skip to main content
Version: 1.0

Language Reference

This page documents the JavaScript language features supported in Shelly Scripts. For the scripting APIs (Shelly, Timer, MQTT, BLE, etc.), see the Script APIs section.

For information about managing scripts on your device, see the Script component documentation.

Supported

  • global scope variables, let, var
  • function binding
  • String object
  • Number object
  • Function
  • Array object
  • Math object
  • Date object
  • new, delete operators
  • Object.keys
  • Exceptions
  • ArrayBuffer
  • AES
note
  • ArrayBuffer and AES supported since version 1.6.0 for Gen 3 and Gen 4 devices only.

Not supported

  • Hoisting
  • Classes as in ES6, function prototypes are supported
  • Promises and async functions

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 defined
  • delete operator works without brackets only
  • Function supports an additional method replaceWith

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.

Function.replaceWith

The replaceWith method allows you to replace a function's implementation at runtime. This is useful for dynamically changing behavior without reassigning variables.

function normalMode() {
console.log("Normal mode active");
Shelly.call("Switch.Toggle", {id: 0});
}

function awayMode() {
console.log("Away mode - ignoring input");
}

// initial behavior
let buttonAction = normalMode;

// later, switch to away mode
buttonAction.replaceWith(awayMode);

// now calling buttonAction() runs awayMode instead
buttonAction();

Object.keys

Iterate over object properties dynamically:

let config = {
threshold: 25,
timeout: 5000,
enabled: true
};

let keys = Object.keys(config);
for (let i = 0; i < keys.length; i++) {
console.log(keys[i], "=", config[keys[i]]);
}

Array Methods

Common array operations for managing collections of data:

let readings = [22.5, 23.1, 24.0, 22.8];

// add and remove elements
readings.push(25.2); // add to end
let last = readings.pop(); // remove from end

// find elements
let idx = readings.indexOf(24.0); // returns 2

// extract portion
let subset = readings.slice(1, 3); // [23.1, 24.0]

// calculate average
let sum = 0;
for (let i = 0; i < readings.length; i++) {
sum += readings[i];
}
let average = sum / readings.length;

Strings

info

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.
  • 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.

Exceptions

Shelly Scripts support JavaScript exceptions using try, catch, finally, and throw. Use exceptions to handle errors gracefully and prevent script termination when operations fail.

Basic try/catch/finally

function parseConfig(jsonString) {
let config = null;
try {
config = JSON.parse(jsonString);
console.log("Config loaded successfully");
} catch (e) {
// e.message contains the error description
console.log("Failed to parse config:", e.message);
} finally {
// always runs, whether or not an exception occurred
console.log("Parse attempt complete");
}
return config;
}

// valid JSON
parseConfig('{"threshold": 25}');

// invalid JSON - caught by exception handler
parseConfig('not valid json');

Input Validation with throw

Use throw to create custom exceptions when validating input or enforcing constraints.

function setTemperatureThreshold(value) {
if (typeof value !== "number") {
throw new Error("threshold must be a number");
}
if (value < -40 || value > 85) {
throw new Error("threshold must be between -40 and 85");
}

Shelly.call("KVS.Set", {
key: "temp_threshold",
value: value
});
console.log("Threshold set to", value);
}

// usage with error handling
function handleSetThreshold(value) {
try {
setTemperatureThreshold(value);
} catch (e) {
console.log("Invalid threshold:", e.message);
}
}

handleSetThreshold(25); // works
handleSetThreshold("hot"); // logs: Invalid threshold: threshold must be a number
handleSetThreshold(100); // logs: Invalid threshold: threshold must be between -40 and 85

Retry Logic

Implement retry patterns for operations that may fail temporarily, such as network requests. Since Shelly.call is asynchronous, error handling must be done inside the callback using the error_code parameter.

let CONFIG = {
url: "http://example.com/api/status",
maxRetries: 3,
retryDelayMs: 2000
};

let retryCount = 0;

function handleResponse(result, error_code, error_message) {
if (error_code !== 0) {
console.log("Request failed:", error_message);

if (retryCount < CONFIG.maxRetries) {
retryCount++;
console.log("Retrying in", CONFIG.retryDelayMs, "ms (attempt", retryCount, "of", CONFIG.maxRetries + ")");
Timer.set(CONFIG.retryDelayMs, false, attemptRequest);
} else {
console.log("Max retries reached, giving up");
retryCount = 0;
}
return;
}

console.log("Request succeeded:", result.body);
retryCount = 0;
}

function attemptRequest() {
console.log("Attempting request...");
Shelly.call("HTTP.GET", {url: CONFIG.url}, handleResponse);
}

// start the request
attemptRequest();

Use try/catch inside callbacks when parsing response data that might be malformed:

function handleResponse(result, error_code, error_message) {
if (error_code !== 0) {
console.log("Request failed:", error_message);
return;
}

try {
let data = JSON.parse(result.body);
console.log("Received value:", data.value);
} catch (e) {
console.log("Failed to parse response:", e.message);
}
}

Shelly.call("HTTP.GET", {url: "http://example.com/api/data"}, handleResponse);
note

Exceptions thrown inside asynchronous callbacks (such as Shelly.call callbacks) will terminate the script if not caught within that callback. Always wrap callback code in try/catch when the operation might fail.

Resource Limits

There are some limitations of the resources used in a script. At the moment, these are as follows:

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:

Loop for too long
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);