🛰️ Shelly LoRa Add-on + Home Assistant
In meinem aktuellen YouTube-Video zeige ich, wie du mit dem Shelly LoRa Add-on Geräte über mehrere Kilometer hinweg steuerst – ganz ohne WLAN.
REST-Template & komplette Skripte für LoRa-Wechselschaltung
Hier findest du das passende REST-Template für Home Assistant sowie die Sender- und Empfänger-Skripte mit AES-Verschlüsselung.
✅ Wichtig: Diese Skripte sind speziell für eine LoRa-basierte Wechselschaltung gedacht – du kannst damit den Zustand eines Schalters an ein anderes Shelly-Gerät übertragen, das dann synchron ein- oder ausschaltet.
🔧 Home Assistant REST-Command Template fürs Shelly LoRa Add-on
rest_command:
shelly_send_lora:
url: "http://192.168.178.137/rpc/Script.Eval"
method: POST
headers:
Content-Type: application/json
payload: >
{
"id": 3,
"code": "sendMessage(\"{{ nachricht }}\")"
}
➡️ Passe die IP-Adresse (192.168.178.137
) und die id
an dein auszuführendes Script an. Damit kannst du über eine Automation z. B. "100"
(AN) oder "000"
(AUS) an ein Shelly-Gerät senden, das die Nachricht über LoRa verschlüsselt weiterleitet.
📤 Sender-Skript – Shelly Gen4 (z. B. Shelly 1 Gen4)
Dieses Skript läuft auf dem Gerät, das den Zustand (z. B. per Taster oder Automation) erkennt und per LoRa weitergibt. Es verschlüsselt die Nachricht mit AES-256, fügt eine Prüfsumme hinzu und schickt sie zum Empfänger.
🧠 Einsatz: z. B. im Haus – die Lampe im Garten wird geschaltet.
// 🔐 --- Konfiguration & AES-Verschlüsselung ---
const aesKey = 'dd469421e5f4089a1418ea24ba37c61bdd469421e5f4089a1418ea24ba37c61b';
const CHECKSUM_SIZE = 4;
let lastRemoteState = null; // Letzter per LoRa empfangener Zustand
// 📦 Nachricht verschlüsseln
function encryptMessage(msg, keyHex) {
function fromHex(hex) {
const arr = new ArrayBuffer(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
arr[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return arr;
}
function padRight(msg, blockSize) {
const paddingSize = (blockSize - msg.length % blockSize) % blockSize;
for (let i = 0; i < paddingSize; i++) msg += ' ';
return msg;
}
msg = msg.trim();
const formattedMsg = padRight(msg, 16);
const key = fromHex(keyHex);
return AES.encrypt(formattedMsg, key, { mode: 'ECB' });
}
// ✔️ Prüfsumme erzeugen
function generateChecksum(msg) {
let checksum = 0;
for (let i = 0; i < msg.length; i++) {
checksum ^= msg.charCodeAt(i);
}
let hexChecksum = checksum.toString(16);
while (hexChecksum.length < CHECKSUM_SIZE) {
hexChecksum = '0' + hexChecksum;
}
return hexChecksum.slice(-CHECKSUM_SIZE);
}
// 📡 Nachricht senden + optional Relais lokal schalten
function sendMessage(message) {
const checkSumMessage = generateChecksum(message) + message;
const encryptedMessage = encryptMessage(checkSumMessage, aesKey);
Shelly.call(
'Lora.SendBytes',
{
id: 100, // 🛰 Zielgerät-ID
data: btoa(encryptedMessage)
},
function (_, err_code, err_msg) {
if (err_code !== 0) {
print('LoRa send error:', err_code, err_msg);
} else {
print('LoRa message sent:', message);
}
}
);
// 🟢 Lokales Relais auf Wunsch mitsteuern
if (message === "100") {
Shelly.call("Switch.Set", { id: 0, on: true });
print("Lokales Relais: EIN");
} else if (message === "000") {
Shelly.call("Switch.Set", { id: 0, on: false });
print("Lokales Relais: AUS");
}
}
// 🔁 Lokalen Schaltzustand überwachen und senden
Shelly.addStatusHandler(function (e) {
if (e.component === "switch:0" && e.delta && typeof e.delta.output === "boolean") {
const isOn = e.delta.output;
if (lastRemoteState !== null) {
print("[LoRa] Änderung durch entfernte Nachricht – kein Re-Senden");
lastRemoteState = null;
return;
}
const message = isOn ? "100" : "000";
sendMessage(message);
}
});
// 📥 LoRa-Nachrichten entschlüsseln
function decryptMessage(buffer, keyHex) {
function fromHex(hex) {
const arr = new ArrayBuffer(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
arr[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return arr;
}
function hex2a(hex) {
let str = '';
for (let i = 0; i < hex.length; i += 2) {
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
}
return str;
}
function toHex(buffer) {
let s = '';
for (let i = 0; i < buffer.length; i++) {
s += (256 + buffer[i]).toString(16).substr(-2);
}
return s;
}
const key = fromHex(keyHex);
const decrypted = AES.decrypt(buffer, key, { mode: 'ECB' });
if (!decrypted || decrypted.byteLength === 0) return;
const hex = toHex(decrypted);
return hex2a(hex).trim();
}
// 🛡 Prüfsumme prüfen
function verifyMessage(message) {
if (!message || message.length < CHECKSUM_SIZE + 1) return null;
const receivedChecksum = message.slice(0, CHECKSUM_SIZE);
const payload = message.slice(CHECKSUM_SIZE);
const expectedChecksum = generateChecksum(payload);
return (receivedChecksum === expectedChecksum) ? payload : null;
}
// 🎯 Event-Handler für LoRa-Empfang
Shelly.addEventHandler(function (event) {
if (!event || event.name !== 'lora' || !event.info || !event.info.data) return;
const encryptedMsg = atob(event.info.data);
const decrypted = decryptMessage(encryptedMsg, aesKey);
if (!decrypted) return;
const clean = verifyMessage(decrypted);
if (!clean) {
print("[LoRa] Checksum mismatch");
return;
}
print("[LoRa] Received:", clean);
if (clean === "100") {
lastRemoteState = true;
Shelly.call("Switch.Set", { id: 0, on: true });
} else if (clean === "000") {
lastRemoteState = false;
Shelly.call("Switch.Set", { id: 0, on: false });
} else {
print("[LoRa] Unknown command:", clean);
}
});
📥 Empfänger-Skript – Shelly Gen4 (z. B. im Gartenhaus)
Dieses Skript läuft auf dem Gerät, das per LoRa eine Nachricht empfängt und das eigene Relais entsprechend schaltet. Gleichzeitig wird der Status bei lokaler Änderung wieder zurückgemeldet.
🧠 Einsatz: z. B. Gartenhaus, Carport oder Keller – das Relais spiegelt den Zustand des entfernten Schalters.
// 🔐 --- GRUNDKONFIGURATION ---
const aesKey = 'dd469421e5f4089a1418ea24ba37c61bdd469421e5f4089a1418ea24ba37c61b';
const CHECKSUM_SIZE = 4;
let lastLoRaCommand = null; // Schützt vor Echo bei Status-Rückmeldung
// ✔️ --- CHECKSUMME ERZEUGEN ---
function generateChecksum(msg) {
let checksum = 0;
for (let i = 0; i < msg.length; i++) {
checksum ^= msg.charCodeAt(i);
}
let hexChecksum = checksum.toString(16);
while (hexChecksum.length < CHECKSUM_SIZE) {
hexChecksum = '0' + hexChecksum;
}
return hexChecksum.slice(-CHECKSUM_SIZE);
}
// 🛡 --- CHECKSUMME VERIFIZIEREN ---
function verifyMessage(message) {
if (message.length < CHECKSUM_SIZE + 1) {
print('[LoRa] invalid message (too short)');
return;
}
const receivedCheckSum = message.slice(0, CHECKSUM_SIZE);
const _message = message.slice(CHECKSUM_SIZE);
const expectedChecksum = generateChecksum(_message);
if (receivedCheckSum !== expectedChecksum) {
print('[LoRa] invalid message (checksum mismatch)');
return;
}
return _message;
}
// 🔓 --- NACHRICHT ENTSCHLÜSSELN (AES) ---
function decryptMessage(buffer, keyHex) {
function fromHex(hex) {
const arr = new ArrayBuffer(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
arr[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return arr;
}
function hex2a(hex) {
let str = '';
for (let i = 0; i < hex.length; i += 2) {
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
}
return str;
}
function toHex(buffer) {
let s = '';
for (let i = 0; i < buffer.length; i++) {
s += (256 + buffer[i]).toString(16).substr(-2);
}
return s;
}
const key = fromHex(keyHex);
const decrypted = AES.decrypt(buffer, key, { mode: 'ECB' });
if (!decrypted || decrypted.byteLength === 0) {
print('[LoRa] invalid msg (empty decryption result)');
return;
}
const hex = toHex(decrypted);
const checksumMessage = hex2a(hex).trim();
return verifyMessage(checksumMessage);
}
// 📥 --- LoRa-NACHRICHT EMPFANGEN UND RELAIS SCHALTEN ---
Shelly.addEventHandler(function (event) {
if (!event || event.name !== 'lora' || !event.info || !event.info.data) return;
const encryptedMsg = atob(event.info.data);
const decryptedMessage = decryptMessage(encryptedMsg, aesKey);
if (!decryptedMessage) return;
print("[LoRa] Message received:", decryptedMessage);
const cmd = decryptedMessage.trim();
if (cmd === "100") {
lastLoRaCommand = true;
Shelly.call("Switch.Set", { id: 0, on: true });
} else if (cmd === "000") {
lastLoRaCommand = false;
Shelly.call("Switch.Set", { id: 0, on: false });
} else {
print("[LoRa] Unknown command:", cmd);
}
});
// 🔐 --- NACHRICHT ENTSCHLÜSSELN (ZUM ZURÜCKSENDEN)
function encryptMessage(msg, keyHex) {
function fromHex(hex) {
const arr = new ArrayBuffer(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
arr[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return arr;
}
function padRight(msg, blockSize) {
const paddingSize = (blockSize - msg.length % blockSize) % blockSize;
for (let i = 0; i < paddingSize; i++) {
msg += ' ';
}
return msg;
}
const key = fromHex(keyHex);
const formattedMsg = padRight(msg, 16);
return AES.encrypt(formattedMsg, key, { mode: 'ECB' });
}
// 🔁 --- STATUS ZURÜCKSENDEN (Z.B. AN DEN SENDER)
function sendLoRaStateBack(isOn) {
const msg = isOn ? "100" : "000";
const fullMsg = generateChecksum(msg) + msg;
const encrypted = encryptMessage(fullMsg, aesKey);
Shelly.call("Lora.SendBytes", {
id: 101, // 🛰 Zieladresse (z. B. ursprünglicher Sender)
data: btoa(encrypted)
}, function (_, code, err) {
if (code !== 0) {
print("[LoRa] Send back failed:", err);
} else {
print("[LoRa] State sent back:", msg);
}
});
}
// ⚡ --- LOKALE RELAISÄNDERUNG ERKENNEN UND ANTWORT SENDEN
Shelly.addStatusHandler(function (e) {
if (e.component === "switch:0" && e.delta && typeof e.delta.output === "boolean") {
if (lastLoRaCommand !== null && e.delta.output === lastLoRaCommand) {
print("[LoRa] Ignoring echo from LoRa-triggered action.");
lastLoRaCommand = null;
return;
}
sendLoRaStateBack(e.delta.output);
}
});
📌 Fazit zum Shelly LoRa Add-on
✅ Mit diesen beiden Skripten kannst du eine vollständige, sichere Wechselschaltung über LoRa aufbauen – unabhängig von WLAN oder Cloud. Perfekt für Schuppen, Gartenhäuser, Carports, Keller – überall dort, wo ein Kabel nicht hinreicht.
📽️ Video mit Live-Demo & Erklärung des Shelly LoRa Add-on:
💬 Fragen, Ideen oder eigene Umsetzung? Hinterlass gerne einen Kommentar unter dem Video!