RPC Handlers
User scripts can register custom RPC methods on the device using Script.addRpcHandler and Script.removeRpcHandler. Registered methods are callable via HTTP, WebSocket, MQTT, or any other RPC channel, just like built-in component methods.
Overview
Registered methods use the Script.<MethodName> namespace and are routed to the correct script instance via the id parameter in the RPC request. They are callable over any RPC channel and follow the standard RPC protocol, just like built-in component methods. Multiple scripts can register handlers for the same method name; the id parameter determines which script handles the call.
This lets a script expose a clean, structured RPC API to external integrations, instead of parsing raw HTTP requests with HTTPServer. Responses go through the standard RPC machinery, so error codes, authentication, and channel framing all work the same way as for native device methods.
Quick Example
Script.addRpcHandler("Hello", function (request, params) {
request.result({greeting: "Hello, " + (params.name || "world") + "!"});
});
Call it:
curl http://<device_ip>/rpc/Script.Hello -d '{"id":0,"name":"Alice"}'
# {"greeting":"Hello, Alice!"}
API Reference
Script.addRpcHandler()
Registers a handler for a custom RPC method.
Script.addRpcHandler(method, callback[, userdata]) -> number
Parameters
| Property | Type | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| string | Method name (max 32 characters). Exposed as | ||||||||||||
| function | Handler function, called as
| ||||||||||||
| any | Optional data passed as the third argument to the callback. |
Return Value
Returns a handler handle (a number greater than 0) on success, or 0 on error.
Errors
The following conditions raise a JavaScript exception (and return 0):
- Method name is empty or longer than 32 characters.
- Method name is reserved. Reserved names are the built-in
Scriptcomponent methods:GetStatus,GetConfig,SetConfig,Start,Stop,Eval,PutCode,GetCode,Create,Delete,List. - Too many handlers are already registered (default limit: 5 per script).
Script.removeRpcHandler()
Removes a previously registered RPC handler.
Script.removeRpcHandler(handle) -> boolean
Parameters
| Property | Type | Description |
|---|---|---|
| number | Handle returned by |
Return Value
Returns true if the handler was removed, false if no handler with that handle exists.
RPCall object
The request argument passed to the handler callback is an RPCall object representing the incoming call. It carries metadata about the caller and provides the methods used to respond.
Properties
| Property | Type | Description |
|---|---|---|
| string | Full RPC method name (e.g. |
| string | Source address of the caller. |
| string | Destination address. |
| string | Authenticated username, if any. |
| string | Channel type the call arrived on (e.g. |
request.result()
Sends a successful response.
request.result(data) -> boolean
| Property | Type | Description |
|---|---|---|
| any | Any JSON-serializable value (object, array, string, number, boolean, or |
Returns true if the response was sent, false if the call is no longer around - it was already answered, or its deadline passed and the framework answered with a timeout on the handler's behalf.
request.error()
Sends an error response.
request.error(code, message) -> boolean
| Property | Type | Description |
|---|---|---|
| number | Error code (e.g. |
| string | Error message. |
Returns true if the response was sent, false under the same conditions as result().
request.setTimeout()
Sets the deadline for answering this call, counted from the moment of the call to setTimeout(). Use it when a handler needs longer than the default 30 seconds, or when it should give up sooner.
request.setTimeout(timeout_ms) -> boolean
| Property | Type | Description |
|---|---|---|
| number | New deadline in milliseconds. Any positive value is accepted; |
Returns true if the deadline was set, false if the call has already been answered. A negative value raises a JavaScript exception.
With setTimeout(0) the call stays open until the handler answers it or the script stops. It occupies one of the concurrent call slots for as long as it does, so a handler that never answers permanently reduces how many calls the script can serve - prefer a generous deadline over no deadline.
Script.addRpcHandler("Poll", function (request, params) {
request.setTimeout(60000); // This one can take a while.
Shelly.call("HTTP.GET", {url: "http://10.0.0.7/rpc/Shelly.GetStatus"},
function (result, code, msg) {
if (code !== 0) {
request.error(code, msg);
} else {
request.result(JSON.parse(result.body));
}
});
});
Exactly one of result() or error() must be called for each request. If neither is called before the deadline, the framework responds with a timeout error (-104, "timeout") and drops the call; result() and error() then return false and do nothing.
Outbound Shelly.call requests have no timeout of their own, so a handler waiting on one - especially on HTTP.GET to a device that may be slow or unreachable - should raise its own deadline with setTimeout() rather than rely on the default.
Limits
| Limit | Default | Build variable |
|---|---|---|
| Max handlers per script | 5 | SHELLY_SCRIPT_MAX_RPC_HANDLERS |
| Max concurrent in-flight calls | 5 | SHELLY_SCRIPT_MAX_RPC_HANDLER_CALLS |
| Handler response timeout | 30 s | SHELLY_SCRIPT_RPC_HANDLER_TIMEOUT_MS |
| Method name max length | 32 | SHELLY_SCRIPT_RPC_METHOD_MAX_LEN |
The response timeout is a default, not a cap: a handler can set any deadline it likes, or none, with setTimeout().
Lifecycle
- Handlers are automatically unregistered when the script stops or crashes.
- In-flight calls to a stopped script receive a
503(Unavailable) error. - A call that passes its deadline is answered with a timeout error and dropped. The handler may still hold a reference to it - in a timer or an outbound call's callback - but answering it then has no effect and returns
false. - Handlers are per-script-instance; restarting a script clears all of its handlers, so the script must re-register them on startup.
Complete Examples
Basic handler
Script.addRpcHandler("Hello", function (request, params) {
request.result({greeting: "Hello, " + (params.name || "world") + "!"});
});
Call: curl http://<device_ip>/rpc/Script.Hello -d '{"id":0,"name":"Alice"}'
Response: {"greeting":"Hello, Alice!"}
Toggle a switch
Script.addRpcHandler("Toggle", function (request, params) {
let sw = Shelly.getComponentStatus("switch", 0);
if (!sw) {
request.error(-1, "switch not found");
return;
}
Shelly.call("Switch.Toggle", {id: 0}, function (result, code, msg) {
if (code !== 0) {
request.error(code, msg);
} else {
request.result({was: sw.output, now: !sw.output});
}
});
});
Using userdata
let config = {multiplier: 2};
Script.addRpcHandler("Multiply", function (request, params, ud) {
if (typeof params.value !== "number") {
request.error(-1, "value must be a number");
return;
}
request.result({result: params.value * ud.multiplier});
}, config);
Error handling
Script.addRpcHandler("Divide", function (request, params) {
if (typeof params.a !== "number" || typeof params.b !== "number") {
request.error(-1, "a and b must be numbers");
return;
}
if (params.b === 0) {
request.error(-1, "division by zero");
return;
}
request.result({result: params.a / params.b});
});
Removing a handler
let h = Script.addRpcHandler("Temp", function (request, params) {
request.result({ok: true});
});
// Later, when no longer needed:
Script.removeRpcHandler(h);