LoRa Add-on
Shelly
Description
Download Signed DDF
min. OS version: 10384-07
With this DDF, Shelly devices with a LoRa add-on can be integrated, allowing one input and one output to be used in each case. Note: Please observe the regionally applicable regulations for LoRa usage. A Shelly with a LoRa add-on is operated in the same IP network as the myGEKKO controller. It serves as a LoRa master gateway and communicates with other Shelly devices (slaves) via LoRa.
Connect to the Wi-Fi access point of the Shelly via 192.168.33.1, then connect it to your Wi-Fi and perform a firmware update if necessary. Tested with 2.0.0-beta1, add-on 2.1.1.
Shelly Master (Gateway in Wi-Fi)
- Create and activate script
lora_master.js - Enable "Run on startup"
- Activate LoRa add-on, configuration must be identical on all Shellys!
- LoRa transport layer Device Address:
0001(HEX) - Generate and note down the Cryptography-Key
- Enable "User LoRa calls"
Script Code: lora_master.js
let lastByAddr = {};
function nowIso() {
return new Date().toISOString();
}
function nowUnix() {
return Math.floor(Date.now() / 1000);
}
// Shelly-safe Query Parser (NO decodeURIComponent!)
function parseQuery(qs) {
let out = {};
if (!qs) return out;
let parts = qs.split("&");
for (let i = 0; i < parts.length; i++) {
let kv = parts[i].split("=");
let key = kv[0];
let val = kv.length > 1 ? kv[1] : "";
if (key) out[key] = val;
}
return out;
}
Shelly.addEventHandler(function (ev) {
if (!ev || !ev.info) return;
let addr = ev.info.lr_addr || ev.info.addr || ev.info.sender;
let data = ev.info.data;
if (addr === undefined || data === undefined) return;
addr = String(addr);
try {
data = JSON.parse(atob(String(data)));
} catch (e) {
try {
data = atob(String(data));
} catch (e2) {
data = String(data);
}
}
lastByAddr[addr] = {
addr: addr,
data: data,
rssi: ev.info.rssi,
snr: ev.info.snr,
received: nowIso(),
received_unix: nowUnix()
};
print("LoRa received: " + addr);
});
HTTPServer.registerEndpoint("lora", function (req, res) {
let query = parseQuery(req.query);
let addr = query.addr;
if (!addr) {
res.code = 400;
res.headers = [["Content-Type", "application/json"]];
res.body = JSON.stringify({ error: "addr missing" });
res.send();
return;
}
let result = lastByAddr[String(addr)];
if (!result) {
res.code = 404;
res.headers = [["Content-Type", "application/json"]];
res.body = JSON.stringify({ error: "not found", addr: addr });
res.send();
return;
}
res.code = 200;
res.headers = [["Content-Type", "application/json"]];
res.body = JSON.stringify(result);
res.send();
});
print("Endpoint active: /script//lora?addr=");
Shelly Slave (LoRa End Devices)
- Create and activate script
lora_slave.js - Enable "Run on startup"
- LoRa transport layer Device Address:
0002(HEX) – first device0003to000F(HEX) – additional slaves
- Insert Cryptography-Key from the Master
- Enable "User LoRa calls"
Script Code: lora_slave.js
let LORA_ID = 100;
let LORA_ADDR = "00000001";
let OUTPUT_ID = 0;
let di = 0;
let doo = 0;
let last = "";
function send(force) {
// Send status as array
let msg = JSON.stringify([di, doo]);
if (!force && msg === last) return;
last = msg;
console.log("SENDING:", msg);
Shelly.call("LoRa.Send", {
id: LORA_ID,
lr_addr: LORA_ADDR,
data: btoa(msg)
}, function (res, err, msg) {
if (err !== 0) {
console.log("LoRa Error:", err, msg);
}
});
}
function startPeriodicSend() {
// a status heartbeat every 15 minutes
Timer.set(15 * 60 * 1000, true, function () {
send(true);
});
}
function handleCommand(rawPayload) {
let payload = rawPayload;
// If data arrives base64 encoded, decode it
try {
payload = atob(rawPayload);
} catch (e) {
// if not base64, use rawPayload directly
}
console.log("LoRa RX raw:", rawPayload, "decoded:", payload);
if (payload === "1" || payload === "ON" || payload === "true") {
Shelly.call("Switch.Set", { id: OUTPUT_ID, on: true }, function (res, err, msg) {
console.log("Switch.Set ON -> err:", err, "msg:", msg);
});
} else if (payload === "0" || payload === "OFF" || payload === "false") {
Shelly.call("Switch.Set", { id: OUTPUT_ID, on: false }, function (res, err, msg) {
console.log("Switch.Set OFF -> err:", err, "msg:", msg);
});
} else {
console.log("Unknown payload:", payload);
}
}
Shelly.addEventHandler(function (ev) {
if (ev.name !== "lora") return;
if (!ev.info) return;
if (ev.info.component !== "lora:100") return;
if (ev.info.event !== "user_rx") return;
handleCommand(ev.info.data);
});
Shelly.addStatusHandler(function (e) {
if (!e || !e.component || !e.delta) return;
if (e.component === "input:0" && e.delta.state !== undefined) {
di = e.delta.state ? 1 : 0;
send(false);
}
if (e.component === "switch:0" && e.delta.output !== undefined) {
doo = e.delta.output ? 1 : 0;
send(false);
}
});
// one-time random startup delay
var startDelay = Math.floor(Math.random() * 15 * 60 * 1000);
console.log("Starting in", startDelay, "ms");
Timer.set(startDelay, false, function () {
send(true);
startPeriodicSend();
});
console.log("LoRa combo script started");
DeviceStation
- Domain:
http://<IP_Shelly_Master> - Slave addresses:
1,2, ... ,15
"Device" System
- Create one block per Shelly and select Dashboard
- Link as widgets on the homepage or rooms/areas
"Light" or "Socket" System
- Switching output:
SET Output - Feedback:
State Output
To Do: The current slave script does not use a send queue. In case of rapid status changes, multiple
LoRa.Send calls can be started simultaneously, which can cause the Shelly error Too many calls in progress. It is recommended to implement a busy-flag or queue/debounce logic so that only one LoRa transmission process is active at a time.
Script code with queue and sequential input, output: lora_slave_queue.js
let LORA_ID = 100;
let LORA_ADDR = "00000001";
let OUTPUT_ID = 0;
let LORA_SEND_GAP_MS = 2000;
let di = 0;
let doo = 0;
let loraBusy = false;
let sendQueue = [];
let queuePos = 0;
let lastSent = "";
let lastQueued = "";
let nextSendAllowedAt = 0;
let retryTimerActive = false;
function getNowMs() {
return Shelly.getUptimeMs();
}
function enqueueSend(force) {
let msg = JSON.stringify([di, doo]);
if (!force && msg === lastQueued) {
return;
}
if (!force &&
msg === lastSent &&
queuePos >= sendQueue.length &&
!loraBusy) {
return;
}
sendQueue.push({
msg: msg,
force: force
});
lastQueued = msg;
processSendQueue();
}
function processSendQueue() {
if (loraBusy) {
return;
}
if (queuePos >= sendQueue.length) {
sendQueue = [];
queuePos = 0;
lastQueued = lastSent;
return;
}
let now = getNowMs();
if (now < nextSendAllowedAt) {
if (!retryTimerActive) {
retryTimerActive = true;
Timer.set(nextSendAllowedAt - now, false, function () {
retryTimerActive = false;
processSendQueue();
});
}
return;
}
let item = sendQueue[queuePos];
queuePos++;
loraBusy = true;
lastSent = item.msg;
console.log("SENDING:", item.msg, item.force ? "(force)" : "");
Shelly.call("LoRa.Send", {
id: LORA_ID,
lr_addr: LORA_ADDR,
data: btoa(item.msg)
}, function (res, err, errmsg) {
if (err !== 0) {
console.log("LoRa Error:", err, errmsg);
}
nextSendAllowedAt = getNowMs() + LORA_SEND_GAP_MS;
loraBusy = false;
processSendQueue();
});
}
function startPeriodicSend() {
Timer.set(15 * 60 * 1000, true, function () {
enqueueSend(true);
});
}
function handleCommand(rawPayload) {
let payload = rawPayload;
try {
payload = atob(rawPayload);
} catch (e) {
// use rawPayload directly
}
console.log("LoRa RX raw:", rawPayload, "decoded:", payload);
if (payload === "1" || payload === "ON" || payload === "true") {
Shelly.call("Switch.Set", { id: OUTPUT_ID, on: true }, function (res, err, msg) {
console.log("Switch.Set ON -> err:", err, "msg:", msg);
});
} else if (payload === "0" || payload === "OFF" || payload === "false") {
Shelly.call("Switch.Set", { id: OUTPUT_ID, on: false }, function (res, err, msg) {
console.log("Switch.Set OFF -> err:", err, "msg:", msg);
});
} else {
console.log("Unknown payload:", payload);
}
}
Shelly.addEventHandler(function (ev) {
if (ev.name !== "lora") return;
if (!ev.info) return;
if (ev.info.component !== "lora:100") return;
if (ev.info.event !== "user_rx") return;
handleCommand(ev.info.data);
});
Shelly.addStatusHandler(function (e) {
if (!e || !e.component || !e.delta) return;
if (e.component === "input:0" && e.delta.state !== undefined) {
di = e.delta.state ? 1 : 0;
enqueueSend(false);
}
if (e.component === "switch:0" && e.delta.output !== undefined) {
doo = e.delta.output ? 1 : 0;
enqueueSend(false);
}
});
var startDelay = Math.floor(Math.random() * 15 * 60 * 1000);
console.log("Starting in", startDelay, "ms");
Timer.set(startDelay, false, function () {
enqueueSend(true);
startPeriodicSend();
});
General Info
| Manufacturer | Type | Protocol | Model | Version | ID |
|---|---|---|---|---|---|
| Shelly | Gateway | REST-API (DDF) | 2 | 1 | 0x0D00002D00020100 |
Documents
No documents.
DDF Items
| ID | Name | Unit | Type | Direction |
|---|---|---|---|---|
| 0 | Slave | |||
| 1 | State Input | |||
| 2 | State Output | |||
| 3 | RSSI | |||
| 4 | Received Unix | |||
| 5 | Received | |||
| 6 | Message Age | |||
| 100 | SET Output | |||
| 200 | Quality |
Note
*2026-06-15 13:30:01 (hw) ******************************
Last Commit: ac75e22f | 2026-06-10 07:25:55
Last Commit: ac75e22f | 2026-06-10 07:25:55