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
StringobjectNumberobjectFunctionArrayobjectMathobjectDateobjectnew,deleteoperatorsObject.keys- Exceptions
ArrayBufferAES
ArrayBufferandAESsupported since version1.6.0for Gen 3 and Gen 4 devices only.
Not supported
- Hoisting
- Classes as in ES6, function prototypes are supported
- Promises and async functions
Specifics
arguments.lengthwill return number of arguments passed if more than defined, or number of defined, if the number of arguments passed to function are less than defineddeleteoperator works without brackets onlyFunctionsupports 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.
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
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\uHHHHsurrogate pairs \uHHHHescapes and surrogate pairs encoded as\uHHHH\uHHHHare 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.
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);
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:
- 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);