Hoy aprendemos a Integrar Shelly LoRa Add-on en HA para incorporar hasta 5 kilómetros de conectividad en tu domótica!
Índice
Qué es LoRa y el Add-on de Shelly
Como ya te contamos hace poco, LoRa (acrónimo de «Long Range» o «Largo Alcance) es un protocolo de comunicación de radiofrecuencia que permite la transmisión de datos a larga distancia (hasta 10km en campo abierto) con muy bajo consumo energético. Puedes consultar la guía de referencia si quieres más detalle!
Pues bien, el Add-on de Shelly es uno de sus últimos productos que funciona como un complemento a los relés que ya conocemos (compatible con los dispositivos GEN 3 en adelante, con el firmware actualizado a la versión 1.6 o superior).
Este se acopla fácilmente al relé, permitiendo la comunicación inalámbrica a larga distancia (según Shelly, hasta 5 km en línea de vista) utilizando su antena y el protocolo LoRa, con un consumo menor a 150 mW.

¿Y para qué podemos usuarlos? Su principal utilidad es poder controlar dispositivos en lugares remotos sin cobertura Wi-Fi, como fincas grandes, trasteros o garajes (por ejemplo, puertas, riegos, bombas de agua, iluminación, etc.).
¿Cómo funciona el add-on LoRa?
Antes de integrar Shelly LoRa Add-on en HA conviene entender conceptualmente cómo funciona:
- Estos dispositivos funcionan por pares, lo que significa que al menos necesitas 2 (un transmisor y un receptor). El transmisor será el que actúe sobre el elemento final (por ejemplo, la puerta de entrada a una finca) y el receptor estará dentro del alcance de HA, conectado a tu red WiFi local.
- Para que el transmisor y el receptor se comuniquen hay que configurarlos a través de la interfaz de Shelly a través de ‘scripts‘. Al principio puede asustar un poco, pero en un momento te explico cómo se hace.
- Eso sí, ten en cuenta que este protocolo también tiene limitaciones. La comunicación LoRa es de baja velocidad, por lo que no es adecuada para enviar grandes cantidades de datos (como imágenes o videos) ni para requerir respuestas instantáneas (ya que tiene un pequeño retardo).
- Además, el alcance máximo se logra en campo abierto y sin obstáculos, pero se reduce considerablemente si hay edificios u otras interferencias.
Integración con Home Assistant
Entendido lo anterior, vamos a llevarlo a la práctica en la parcela de un familiar, situada a unos 500 m (sí, ¡medio km domótico!). Cabe destacar que entre medias hay varias casas, terrenos y obstáculos (y, por supuesto, la WiFi diciendo “hasta aquí hemos llegao’” 😅).
El objetivo es que (tanto él como yo) pueda regar las plantas y abrir la puerta desde Home Assistant. Para ello he tenido que seguir estos pasos:
- Acopla los add-ons de LoRa a tu relé Shelly. Para ello simplemente tienes que montarlo sobre el propio relé, haciendo coincidir los pines pequeñitos que traen, de forma que se ajusten con un «clic».
- Asegúrate de tener integrados tus relés Shelly en Home Assistant y que estén actualizados a la versión 1.6.0 o superior. Puedes acceder a la interfaz del dispositivo introduciendo su IP en tu navegador, o desde HA, haciendo ‘clic’ en el enlace que indica «Visitar» dentro del dispositivo correspondiente.

- Una vez en su interfaz, entra en el apartado Settings > Device Settings > Firmware para comprobar si hay alguna actualización disponible. También puedes pulsar en el botón «Check for updates» para forzar la comprobación.
- Desde esta misma interfaz entra en el apartado «Add-on» y marca la casilla «LoRa Add-on» en ambos dispositivos (emisor y receptor).

- Aprovecha para actualizar también el ‘firmware’ de los add-on, pulsando sobre el icono del lápiz azul que aparece en la esquina superior derecha del bloque inferior y en «LoRa add-on firmware update».

- Ahora tenemos que configurar cada add-on LoRa para que se comuniquen entre sí. Para ello, pulsa sobre la opción «LoRa configuration», bajo el bloque «LoRa settings».

- Es importante que ambos add-on tengan la misma configuración, en mi caso:
- Región. Corresponde a la región en la que te encuentras (en mi caso, ‘EU’ de Europa).
- Sub-band. Corresponde a la banda de frecuencia en la que quieres que se comuniquen los relés (en mi caso, ‘L (865 MHz- 868 MHz)’). Puedes jugar con este parámetro si observas que la banda que utilizas está saturada.
- Preset. Aquí puedes regular la relación entre el alcance y la velocidad de comunicación, aunque en mi caso he seleccionado ‘Long Range’.
- Para empezar puedes dejar el resto de campos como vienen por defecto.
Creación de los ‘scripts’
Llegado a este punto, creí que no iba a conseguir fácilmente comunicar los relés con LoRa, ya que, a día de hoy, es necesario crear un script en javascript (y no tenía ni idea de cómo hacerlo 😅). No obstante, al final resultó mucho más fácil gracias a la IA.
- De nuevo, desde la interfaz de Shelly entra en el apartado «Scripts» del menú lateral y pulsa en el botón «Create script». Ten en cuenta que necesitar crear un ‘script’ para el emisor y otro para el receptor.

- A partir de aquí lo que he hecho es pasarle el siguiente comando a la IA, compartiendo con ella los ejemplos de la web de Shelly.
- De esta forma he conseguido que me devuelva los siguientes ‘scripts’ (el primero para el emisor, y el segundo para el receptor).
/***
* SHELLY LORA SENDER SCRIPT
* Controla un relé remoto a través de LoRa
* Emite comandos encriptados para encender/apagar
***/
/********* CONFIGURACIÓN REQUERIDA *********/
const CONFIG = {
aesKey: "MTIzNDU2Nzg5MGFiY2RlZg==", // ⚠️ CAMBIAR: Tu clave AES-128 en base64
relayIdToControl: 0, // ID del relé en ESTE dispositivo (0 o 1)
debug: true // Mostrar mensajes en consola
};
/*******************************************/
const CHECKSUM_SIZE = 4;
// ========== FUNCIONES DE ENCRIPTACIÓN ==========
function padRight(msg, blockSize) {
const paddingSize = (blockSize - msg.length % blockSize) % blockSize;
let padding = "";
for (let i = 0; i < paddingSize; i++) padding += " ";
return msg + padding;
}
function base64ToBytes(b64) {
const bin = atob(b64);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return bytes;
}
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);
}
function encryptMessage(msg, keyB64) {
if (typeof AES === 'undefined') {
console.log("ERROR: AES no soportado en este dispositivo");
return null;
}
const key = base64ToBytes(keyB64);
const formattedMsg = padRight(msg.trim(), 16);
return AES.encrypt(formattedMsg, key, { mode: 'ECB' });
}
// ========== FUNCIONES DE ENVÍO ==========
function sendMessage(message) {
const checkSumMessage = generateChecksum(message) + message;
const encryptedMessage = encryptMessage(checkSumMessage, CONFIG.aesKey);
if (!encryptedMessage) {
console.log("ERROR: No se pudo encriptar el mensaje");
return;
}
Shelly.call("Lora.SendBytes", {
id: 100,
data: btoa(encryptedMessage)
});
if (CONFIG.debug) {
console.log("✓ Mensaje LoRa enviado:", message);
}
}
function sendRelayCommand(isOn) {
const command = isOn ? "REL_ON" : "REL_OFF";
sendMessage(command);
}
// ========== MANEJADOR DE CAMBIOS DE RELÉ ==========
Shelly.addStatusHandler(function(event) {
// Detectar cambios de estado del relé
if (event && event.component === "switch:" + CONFIG.relayIdToControl) {
const state = event.delta && event.delta.output !== undefined
? event.delta.output
: false;
if (CONFIG.debug) {
console.log("Cambio detectado en relé " + CONFIG.relayIdToControl + ": " + (state ? "ON" : "OFF"));
}
sendRelayCommand(state);
}
});
console.log("✓ Script LoRa Sender iniciado");
console.log("Monitoreando relé ID:", CONFIG.relayIdToControl);
/***
* SHELLY LORA RECEIVER SCRIPT
* Recibe comandos encriptados y controla relé local
* Desencripta y ejecuta comandos para encender/apagar
***/
/********* CONFIGURACIÓN REQUERIDA *********/
const CONFIG = {
aesKey: "MTIzNDU2Nzg5MGFiY2RlZg==", // ⚠️ CAMBIAR: Debe ser IDÉNTICA a la del emisor
relayIdToControl: 0, // ID del relé a controlar (0 o 1)
debug: true, // Mostrar mensajes en consola
timeout: 60 // Timeout de seguridad en segundos
};
/*******************************************/
const CHECKSUM_SIZE = 4;
let lastMessageTime = Date.now();
// ========== FUNCIONES DE DESENCRIPTACIÓN ==========
function base64ToBytes(b64) {
const bin = atob(b64);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return bytes;
}
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);
}
function verifyMessage(message) {
if (message.length < CHECKSUM_SIZE + 1) {
console.log("ERROR: Mensaje muy corto");
return null;
}
const receivedCheckSum = message.slice(0, CHECKSUM_SIZE);
const _message = message.slice(CHECKSUM_SIZE);
const calculatedCheckSum = generateChecksum(_message);
if (calculatedCheckSum !== receivedCheckSum) {
console.log("ERROR: Checksum incorrecto. Esperado:", calculatedCheckSum, "Recibido:", receivedCheckSum);
return null;
}
return _message;
}
function decryptMessage(buffer, keyBase64) {
if (typeof AES === 'undefined') {
console.log("ERROR: AES no soportado");
return null;
}
const key = base64ToBytes(keyBase64);
const decrypted = AES.decrypt(buffer, key, { mode: 'ECB' });
if (!decrypted || decrypted.byteLength === 0) {
console.log("ERROR: Fallo en desencriptación");
return null;
}
let hex = "";
for (let i = 0; i < decrypted.length; i++) {
hex += ("0" + decrypted[i].toString(16)).slice(-2);
}
let fullMessage = "";
for (let i = 0; i < hex.length; i += 2) {
fullMessage += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
}
return verifyMessage(fullMessage.trim());
}
// ========== FUNCIONES DE CONTROL ==========
function controlRelay(command) {
if (CONFIG.debug) {
console.log("Ejecutando comando:", command);
}
if (command === "REL_ON") {
Shelly.call("Switch.Set", { id: CONFIG.relayIdToControl, on: true });
console.log("✓ Relé " + CONFIG.relayIdToControl + " ENCENDIDO");
}
else if (command === "REL_OFF") {
Shelly.call("Switch.Set", { id: CONFIG.relayIdToControl, on: false });
console.log("✓ Relé " + CONFIG.relayIdToControl + " APAGADO");
}
else {
console.log("⚠ Comando desconocido:", command);
}
lastMessageTime = Date.now();
}
// ========== MANEJADOR DE MENSAJES LoRa ==========
Shelly.addEventHandler(function(event) {
if (!event || event.name !== 'lora' || event.id !== 100) return;
if (!event.info || !event.info.data) return;
try {
const encryptedMsg = atob(event.info.data);
const decryptedMessage = decryptMessage(encryptedMsg, CONFIG.aesKey);
if (!decryptedMessage) {
console.log("ERROR: No se pudo desencriptar mensaje");
return;
}
console.log("✓ Mensaje recibido:", decryptedMessage);
console.log(" RSSI:", event.info.rssi, "| SNR:", event.info.snr);
controlRelay(decryptedMessage);
} catch (e) {
console.log("ERROR al procesar mensaje LoRa:", e);
}
});
// ========== WATCHDOG DE SEGURIDAD ==========
// Opcional: Apaga el relé si no recibe mensajes en X segundos
Timer.set(10 * 1000, true, function() {
const elapsedSeconds = Math.floor((Date.now() - lastMessageTime) / 1000);
if (elapsedSeconds > CONFIG.timeout) {
console.log("⚠ TIMEOUT: No hay mensajes desde hace " + elapsedSeconds + "s");
// Descomentar la siguiente línea para apagar el relé automáticamente:
// Shelly.call("Switch.Set", { id: CONFIG.relayIdToControl, on: false });
}
});
console.log("✓ Script LoRa Receiver iniciado");
console.log("Esperando comandos para controlar relé ID:", CONFIG.relayIdToControl);
- Copia el ‘script’ en el campo correspondiente al dispositivo emisor, pulsa en «Guardar» y en el icono del ‘Play’ para iniciar el ‘script’. Después haz lo mismo con el módulo receptor y el ‘script’ correspondiente.
- Para comprobar que funciona de forma correcta, pulsa el botón del relé, y observa cómo envía un mensaje al módulo contrario a través de la «Consola» que figura justo debajo del campo donde has pegado el ‘script’. Por supuesto, también deberá actuar de forma sincronizada.
Expectativas y conclusión
Tras realizar varias pruebas, como era de esperar, el alcance se reduce desde los 5 km teóricos (especialmente cuando hay elementos intermedios). Por lo tanto, es evidente que no es la mejor solución para controlar dispositivos a varios kilómetros de distancia.
No obstante, creo que en la mayoría de los casos de uso, estaremos hablando de zonas con poca cobertura o un poco alejadas (garajes, trasteros, parcelas colindantes…) y en estos casos sí que se convierten en el dispositivo perfecto para llevar nuestra instancia «un poco más allá», sin necesidad de «tirar» cables o duplicar nuestra instalación.

