Shelly LoRa Add-On

What is Shelly LoRa Add-on

With the the LoRa add-ons we get the options to communicate small amount of data at long distances between Shelly devices.

Currently, this is handled through scripting.
Find How to add a script guide here.

Note: Requires a Gen3 device with matching form factor.

Read more about the LoRa Add-on, by clicking the image

Supported devices

Currently (January 2026)

Shelly Gen3 Devices: Shelly 1 Gen3, Shelly 1PM Gen3, Shelly 2PM Gen3, Shelly Shutter, Shelly EM Gen3, Shelly Dimmer 0/1-10V PM Gen3, Shelly DALI Dimmer Gen3

Shelly Gen4 Devices: Shelly 1 Gen4, Shelly 1PM Gen4, Shelly 2PM Gen4

Encryption

To make sure it’s only the two devices that can understand what is sendt, the scripts has an encryption.
You can change the key in “const aesKey = ” to what ever yo want as long it’s exactly 24 characters and also make sure that it’s the same in both the sender and receiver script.

The scripts:

When a relay switches ON or OFF, it signals the other relay to do the same

Click below to expand.

Sender script - When relay turns ON/OFF it sends ON/OFF command
// AES-128 nøgle i base64 (16 byte): base64 af "1234567890abcdef"
const aesKey = "MTIzNDU2Nzg5MGFiY2RlZg==";
const CHECKSUM_SIZE = 4;
let state = "unknown";

// --- AES Hjælpefunktioner ---

function padRight(msg, blockSize) {
  try {
    const paddingSize = (blockSize - msg.length % blockSize) % blockSize;
    let padding = "";
    for (let i = 0; i < paddingSize; i++) padding += " ";
    return msg + padding;
  } catch (err) {
    console.log("Fejl i padRight:", err.message);
    return msg;
  }
}

function base64ToBytes(b64) {
  try {
    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;
  } catch (err) {
    console.log("Fejl i base64ToBytes:", err.message);
    return new Uint8Array();
  }
}

function generateChecksum(msg) {
  try {
    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);
  } catch (err) {
    console.log("Fejl i generateChecksum:", err.message);
    return "0000";
  }
}

function encryptMessage(msg, keyB64) {
  try {
    if (typeof AES === 'undefined') {
      console.log("ERROR: AES ikke understøttet på enheden");
      return null;
    }
    const key = base64ToBytes(keyB64);
    const formattedMsg = padRight(msg.trim(), 16);
    return AES.encrypt(formattedMsg, key, { mode: 'ECB' });
  } catch (err) {
    console.log("Fejl i encryptMessage:", err.message);
    return null;
  }
}

// --- Krypteret beskedsender via LoRa ---

function sendMessage(message) {
  try {
    const checkSumMessage = generateChecksum(message) + message;
    const encryptedMessage = encryptMessage(checkSumMessage, aesKey);

    if (!encryptedMessage) {
      console.log("Kryptering fejlede – besked ikke sendt");
      return;
    }

    Shelly.call('Lora.SendBytes', {
      id: 100,
      data: btoa(encryptedMessage),
    }, function (_, err_code, err_msg) {
      if (err_code !== 0) {
        console.log('LoRa fejl:', err_code, err_msg);
      }
    });
  } catch (err) {
    console.log("Fejl i sendMessage:", err.message);
  }
}

// --- Sender krypteret ON/OFF besked ---

function send(state) {
  try {
    console.log('Relæstatus ændret – sender krypteret:', state);
    sendMessage(state);
  } catch (err) {
    console.log("Fejl i send:", err.message);
  }
}

// --- Relæovervågning ---

function checkRelayStatus() {
  try {
    Shelly.call("Switch.GetStatus", { id: 0 }, function (result, error) {
      if (error) {
        console.log("Fejl ved hentning af relæstatus:", error);
        return;
      }

      try {
        let newState = result.output ? "ON" : "OFF";
        if (newState !== state) {
          state = newState;
          console.log("Relæ tilstand ændret til:", state);
          send(state);
        }
      } catch (innerErr) {
        console.log("Fejl i behandling af relæstatus:", innerErr.message);
      }
    });
  } catch (err) {
    console.log("Fejl i checkRelayStatus:", err.message);
  }
}

// --- EventHandler for relæ ---

try {
  Shelly.addEventHandler(function (event) {
    try {
      if (event.name === "switch" && event.info.id === 0) {
        checkRelayStatus();
      }
    } catch (err) {
      console.log("Fejl i event handler:", err.message);
    }
  });
} catch (err) {
  console.log("Fejl ved opsætning af event handler:", err.message);
}
Receiver script – The relay switches ON/OFF based on the received command.
// AES-128 nøgle i base64 (samme som afsender)
const aesKey = 'MTIzNDU2Nzg5MGFiY2RlZg==';
const CHECKSUM_SIZE = 4;

// Hjælpefunktioner

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('[LoRa] Ugyldig besked – for kort');
    return;
  }

  const receivedCheckSum = message.slice(0, CHECKSUM_SIZE);
  const _message = message.slice(CHECKSUM_SIZE);
  const expectedChecksum = generateChecksum(_message);

  if (receivedCheckSum !== expectedChecksum) {
    console.log('[LoRa] Ugyldig besked – forkert checksum');
    return;
  }

  return _message;
}

function decryptMessage(buffer, keyBase64) {
  if (typeof AES === 'undefined') {
    console.log("ERROR: AES ikke understøttet på enheden");
    return;
  }

  function toHex(buffer) {
    let s = '';
    for (let i = 0; i < buffer.length; i++) {
      s += (256 + buffer[i]).toString(16).substr(-2);
    }
    return s;
  }

  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;
  }

  const key = base64ToBytes(keyBase64);
  const decrypted = AES.decrypt(buffer, key, { mode: 'ECB' });

  if (!decrypted || decrypted.byteLength === 0) {
    console.log('[LoRa] Dekryptering gav tomt resultat');
    return;
  }

  const hex = toHex(decrypted);
  const fullMessage = hex2a(hex).trim();
  return verifyMessage(fullMessage);
}

// Håndtering af LoRa-beskeder

Shelly.addEventHandler(function (event) {
  if (!event || event.name !== 'lora' || event.id !== 100 || !event.info || !event.info.data) {
    return;
  }

  const encryptedMsg = atob(event.info.data);
  const decryptedMessage = decryptMessage(encryptedMsg, aesKey);

  if (typeof decryptedMessage === "undefined") {
    return; // Ignorer ugyldig besked
  }

  console.log("Dekrypteret besked:", decryptedMessage);

  if (decryptedMessage === "ON") {
    console.log("Tænder relæ (modtog 'ON')");
    Shelly.call("Switch.set", { id: 0, on: true });
  } else if (decryptedMessage === "OFF") {
    console.log("Slukker relæ (modtog 'OFF')");
    Shelly.call("Switch.set", { id: 0, on: false });
  } else {
    console.log("Ukendt kommando:", decryptedMessage);
  }
});

Extra - Feedback from receiver device

If you want confirmation that the receiver device has actually received and executed the command, there is a way to do that as well.
Currently, we haven’t had the time to integrate this feature into the other scripts.
Therefore, you’ll need to run additional scripts on the devices.
The feedback can be seen in the script’s console log, and it is also written to both:

  • a Virtual Boolean component (ID: 200) — showing true or false.
  • a Virtual Text component (ID: 200) — with detailed feedback.

This is the opposite of the first two scripts.
Now, the feedback sender script should be added to the actual receiver device — and vice versa.

Feedback Sender and Receiver script

Feedback Sender (added to the actual receiver device)
// AES-128 nøgle i base64 (16 byte): base64 af "1234567890abcdef"
const aesKey = "MTIzNDU2Nzg5MGFiY2RlZg==";
const CHECKSUM_SIZE = 4;
let state = "unknown";

// --- AES Hjælpefunktioner ---

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 ikke understøttet på enheden");
    return null;
  }
  const key = base64ToBytes(keyB64);
  const formattedMsg = padRight(msg.trim(), 16);
  return AES.encrypt(formattedMsg, key, { mode: 'ECB' });
}

// --- Krypteret beskedsender via LoRa ---

function sendMessage(message) {
  const checkSumMessage = generateChecksum(message) + message;
  const encryptedMessage = encryptMessage(checkSumMessage, aesKey);

  if (!encryptedMessage) {
    console.log("Kryptering fejlede – besked ikke sendt");
    return;
  }

  Shelly.call('Lora.SendBytes', {
    id: 100,
    data: btoa(encryptedMessage),
  }, function (_, err_code, err_msg) {
    if (err_code !== 0) {
      console.log('LoRa fejl:', err_code, err_msg);
    }
  });
}

// --- Sender krypteret ON/OFF besked ---

function send(state) {
  console.log('Relæstatus ændret – sender krypteret:', state);
  sendMessage(state);
}

// --- Relæovervågning ---

function checkRelayStatus() {
  Shelly.call("Switch.GetStatus", { id: 0 }, function (result, error) {
    if (error) {
      console.log("Fejl ved hentning af relæstatus:", error);
      return;
    }

    // <-- Ændret her:
    let newState = result.output ? "Is On" : "Is Off";

    if (newState !== state) {
      state = newState;
      console.log("Relæ tilstand ændret til:", state);
      send(state);
    }
  });
}

// --- EventHandler for relæ ---

Shelly.addEventHandler(function (event) {
  if (event.name === "switch" && event.info.id === 0) {
    checkRelayStatus();
  }
});
Feedback Receiver (added to the actual sender device)
// AES-128 nøgle i base64 (samme som afsender)
const aesKey = 'MTIzNDU2Nzg5MGFiY2RlZg==';
const CHECKSUM_SIZE = 4;

// Hjælpefunktioner

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('[LoRa] Ugyldig besked – for kort');
    return;
  }

  const receivedCheckSum = message.slice(0, CHECKSUM_SIZE);
  const _message = message.slice(CHECKSUM_SIZE);
  const expectedChecksum = generateChecksum(_message);

  if (receivedCheckSum !== expectedChecksum) {
    console.log('[LoRa] Ugyldig besked – forkert checksum');
    return;
  }

  return _message;
}

function decryptMessage(buffer, keyBase64) {
  if (typeof AES === 'undefined') {
    console.log("ERROR: AES ikke understøttet på enheden");
    return;
  }

  function toHex(buffer) {
    let s = '';
    for (let i = 0; i < buffer.length; i++) {
      s += (256 + buffer[i]).toString(16).substr(-2);
    }
    return s;
  }

  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;
  }

  const key = base64ToBytes(keyBase64);
  const decrypted = AES.decrypt(buffer, key, { mode: 'ECB' });

  if (!decrypted || decrypted.byteLength === 0) {
    console.log('[LoRa] Dekryptering gav tomt resultat');
    return;
  }

  const hex = toHex(decrypted);
  const fullMessage = hex2a(hex).trim();
  return verifyMessage(fullMessage);
}

// Håndtering af LoRa-beskeder

Shelly.addEventHandler(function (event) {
  if (
    !event ||
    event.name !== 'lora' ||
    event.id !== 100 ||
    !event.info ||
    !event.info.data
  ) {
    return;
  }

  const encryptedMsg = atob(event.info.data);
  const decryptedMessage = decryptMessage(encryptedMsg, aesKey);

  if (typeof decryptedMessage === 'undefined') {
    return; // Ignorer ugyldig besked
  }

  // 1) Log beskeden
  console.log('[LoRa] Modtaget besked:', decryptedMessage);

  // 2) Skriv til virtuel tekst-enhed (id 200)
  Shelly.call('Text.Set', { id: 200, value: decryptedMessage }, function (res, err_code, err_msg) {
    if (err_code !== 0) {
      console.log('Fejl ved opdatering af virtuel tekst-enhed:', err_code, err_msg);
    }
  });

  // 3) Bestem boolean-værdi og skriv til virtuel boolean-enhed (id 200)
  // true hvis præcis "ON", ellers false
  const boolValue = (decryptedMessage === 'Is On');
  Shelly.call('Boolean.Set', { id: 200, value: boolValue }, function (res, err_code, err_msg) {
    if (err_code !== 0) {
      console.log('Fejl ved opdatering af virtuel boolean-enhed:', err_code, err_msg);
    }
  });
});

Monitors and transmits temperature and humidity data at one-minute intervals

To do so you need to pair the sender device directly with a Blu H&T.
Find How to Direct Bluetooth Pairing with Shelly (230V) guide here.

The receiver script also writes the Humidity and Temperature values to each Number Virtual component.
 HUMIDITY_ID = 200
 TEMPERATURE_ID = 201

Click below to expand.

Sender script - Checks and sends temp and humidity each minut
// AES-128 key in base64 (16 bytes): base64 of "1234567890abcdef"
const aesKey = "MTIzNDU2Nzg5MGFiY2RlZg==";

const CHECKSUM_SIZE = 4;
const TEMP_SENSOR_ID = 202;     // Temperature sensor ID
const HUM_SENSOR_ID = 201;      // Humidity sensor ID
const INTERVAL_SECS = 60;       // Send every 60 seconds

// --- AES Utilities ---
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 is not supported on this device");
    return null;
  }
  const key = base64ToBytes(keyB64);
  const formattedMsg = padRight(msg.trim(), 16);
  return AES.encrypt(formattedMsg, key, { mode: 'ECB' });
}

// --- Encrypted LoRa Sender ---
function sendMessage(message) {
  const checkSumMessage = generateChecksum(message) + message;
  const encryptedMessage = encryptMessage(checkSumMessage, aesKey);

  if (!encryptedMessage) {
    console.log("Encryption failed – message not sent");
    return;
  }

  Shelly.call('Lora.SendBytes', {
    id: 100,
    data: btoa(encryptedMessage),
  }, function (_, err_code, err_msg) {
    if (err_code !== 0) {
      console.log('LoRa error:', err_code, err_msg);
    }
  });
}

// --- Read Sensors and Send Values ---
function readAndSendTemperatureAndHumidity() {
  Shelly.call("BTHomeSensor.GetStatus", { id: TEMP_SENSOR_ID }, function (tempRes, tempErr) {
    if (tempErr) {
      print("Error reading temperature:", JSON.stringify(tempErr));
      return;
    }

    Shelly.call("BTHomeSensor.GetStatus", { id: HUM_SENSOR_ID }, function (humRes, humErr) {
      if (humErr) {
        print("Error reading humidity:", JSON.stringify(humErr));
        return;
      }

      let temperature = tempRes.value.toFixed(2);
      let humidity = humRes.value.toFixed(1);

      print("Temperature:", temperature, "°C — Humidity:", humidity, "%");

      let combined = temperature + "," + humidity;
      sendMessage(combined);
    });
  });
}

// --- Start periodic sending ---
Timer.set(INTERVAL_SECS * 1000, true, function () {
  readAndSendTemperatureAndHumidity();
});

// Send once immediately
readAndSendTemperatureAndHumidity();
Receiver script - Receives temp and humidity each minut
// CONFIGURATION
let CONFIG = {
  debug: true  // Set to false to disable detailed logging
//snr means Signal-to-noise ratio, typically between -20 and +10. The higher the value, the better the signal quality.
// rssi means Received signal strength, typically between -120 and -30. The closer the value is to 0, the stronger the signal.
};

// AES-128 key in base64 (same as sender)
const aesKey = 'MTIzNDU2Nzg5MGFiY2RlZg==';
const CHECKSUM_SIZE = 4;

// Virtual component IDs
const HUMIDITY_ID = 200;
const TEMPERATURE_ID = 201;

// Statistics for LoRa reception
let totalPacketsReceived = 0;
let correctPacketFormatCount = 0;
let incorrectPacketFormatCount = 0;
let errorPercentage = 0;

// --- Utilities ---
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('[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) {
    console.log('[LoRa] Invalid message – checksum mismatch');
    return;
  }

  return _message;
}

function decryptMessage(buffer, keyBase64) {
  if (typeof AES === 'undefined') {
    console.log("ERROR: AES is not supported on this device");
    return;
  }

  function toHex(buffer) {
    let s = '';
    for (let i = 0; i < buffer.length; i++) {
      s += (256 + buffer[i]).toString(16).substr(-2);
    }
    return s;
  }

  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;
  }

  const key = base64ToBytes(keyBase64);
  const decrypted = AES.decrypt(buffer, key, { mode: 'ECB' });

  if (!decrypted || decrypted.byteLength === 0) {
    console.log('[LoRa] Decryption returned empty result');
    return;
  }

  const hex = toHex(decrypted);
  const fullMessage = hex2a(hex).trim();
  return verifyMessage(fullMessage);
}

// --- Logging function with statistics ---
function logLoRaEventSummary(event, message) {
  let info = (event && event.info) ? event.info : {};

  // Update statistics
  totalPacketsReceived++;
  let isValid = (typeof message === 'string') && message.indexOf(",") !== -1 && message.split(",").length === 2;
  if (isValid) {
    correctPacketFormatCount++;
  } else {
    incorrectPacketFormatCount++;
  }
  errorPercentage = (incorrectPacketFormatCount / totalPacketsReceived) * 100;

  let summary = {
    time: new Date().toISOString(),
    signal_rssi: info.rssi,
    signal_snr: info.snr,
    base64_data: info.data,
    total_received: totalPacketsReceived,
    correct: correctPacketFormatCount,
    incorrect: incorrectPacketFormatCount,
    error_percent: errorPercentage.toFixed(2) + "%"
  };

  // Only add packets_lost if available
  if (typeof info.packets_lost !== 'undefined') {
    summary.packets_lost = info.packets_lost;
  }

  console.log("LoRa Event Summary:", JSON.stringify(summary));
}

// --- Event handler ---
Shelly.addEventHandler(function (event) {
  if (!event || event.name !== 'lora' || event.id !== 100 || !event.info || !event.info.data) {
    return;
  }

  const encryptedMsg = atob(event.info.data);
  const decryptedMessage = decryptMessage(encryptedMsg, aesKey);

  if (typeof decryptedMessage === "undefined") return;

  let parts = decryptedMessage.split(",");
  if (parts.length !== 2) {
    console.log("Invalid format received:", decryptedMessage);
  }

  let temp = parseFloat(parts[0]);
  let hum = parseFloat(parts[1]);

  if (isNaN(temp) || isNaN(hum)) {
    console.log("Could not parse values:", decryptedMessage);
  } else {
    Timer.set(50, false, function () {
      console.log("Received Temperature:", temp.toFixed(2) + "°C");
      console.log("Received Humidity:", hum.toFixed(1) + "% RH");

      Shelly.call("Number.Set", { id: TEMPERATURE_ID, value: temp });
      Shelly.call("Number.Set", { id: HUMIDITY_ID, value: hum });
    });
  }

  if (CONFIG.debug) {
    Timer.set(100, false, function () {
      logLoRaEventSummary(event, decryptedMessage);
    });
  }
});

Alive Signal System

This system includes a sender and a receiver script.
The sender periodically transmits an “alive” signal to indicate it’s active.
The receiver listens for these signals and toggles a relay if no signal is received within a defined timeout.
(e.g., for safety, alerting, or failover purposes).
The receiver script also writes to two Virtual components.
A Text VC = ID:200
A Boolean VC = ID:200  

Click below to expand.

Sender script - Sends periodic alive signal
/**************** CONFIGURATION ****************/
let CONFIG = {
  sendIntervalMin: 1,                 // How often to send signal (in minutes)
  aesKey: "MTIzNDU2Nzg5MGFiY2RlZg=="  // AES-128 key in base64
};
/************************************************/

const CHECKSUM_SIZE = 4;

// --- AES helper functions ---
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 not supported on this device");
    return null;
  }
  const key = base64ToBytes(keyB64);
  const formattedMsg = padRight(msg.trim(), 16);
  return AES.encrypt(formattedMsg, key, { mode: 'ECB' });
}

function sendMessage(message) {
  const checkSumMessage = generateChecksum(message) + message;
  const encryptedMessage = encryptMessage(checkSumMessage, CONFIG.aesKey);
  if (!encryptedMessage) return;

  Shelly.call("Lora.SendBytes", {
    id: 100,
    data: btoa(encryptedMessage)
  });

  print("Sent LoRa message:", message);
}

// --- Send fixed message at interval ---
function sendFixedSignal() {
  sendMessage("Hello");
}

// --- Set up periodic sending ---
Timer.set(CONFIG.sendIntervalMin * 60 * 1000, true, function () {
  sendFixedSignal();
});

// Optionally send immediately on startup
sendFixedSignal();
Receiver script - Monitors periodic alive signals and toggles relay on timeout
/**************** CONFIGURATION ****************/
let CONFIG = {
  timeoutMinutes: 2,          // Time without signal before triggering action(MUST BE HIGHER THAN INTERVAL FROM SENDER SCRIPT))
  actionIfTimeout: "on",      // "on" or "off" when no signal (What should relay do)
  relayID: 0,                 // Relay output ID
  virtualComponentID: 200,    // Shared ID for Text + Boolean
  debug: true                 // Enable advanced debug logging (LoRa Event Summary)
};
/************************************************/

const aesKey = 'MTIzNDU2Nzg5MGFiY2RlZg==';  // AES-128 key in base64
const CHECKSUM_SIZE = 4;

let totalPacketsReceived = 0;
let correctPacketFormatCount = 0;
let incorrectPacketFormatCount = 0;
let errorPercentage = 0;

let lastReceivedTime = Date.now();  // Start counting immediately
let relayState = null;

// --- Pad helper for Shelly ---
function zeroPad(value, length) {
  let str = String(value);
  while (str.length < length) str = "0" + str;
  return str;
}

// --- Advanced LoRa Event Summary ---
function logLoRaEventSummary(event, message) {
  let info = (event && event.info) ? event.info : {};
  totalPacketsReceived++;
  let isValid = (typeof message === 'string') &&
                message.indexOf(",") !== -1 &&
                message.split(",").length === 2;
  if (isValid) {
    correctPacketFormatCount++;
  } else {
    incorrectPacketFormatCount++;
  }
  errorPercentage = (incorrectPacketFormatCount / totalPacketsReceived) * 100;
  let summary = {
    time: new Date().toISOString(),
    signal_rssi: info.rssi,
    signal_snr: info.snr,
    base64_data: info.data,
    total_received: totalPacketsReceived,
    correct: correctPacketFormatCount,
    incorrect: incorrectPacketFormatCount,
    error_percent: errorPercentage.toFixed(2) + "%"
  };
  if (typeof info.packets_lost !== 'undefined') {
    summary.packets_lost = info.packets_lost;
  }
  console.log("LoRa Event Summary:", JSON.stringify(summary));
}

// --- Utilities ---
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) return;
  const receivedCheckSum = message.slice(0, CHECKSUM_SIZE);
  const _message = message.slice(CHECKSUM_SIZE);
  if (generateChecksum(_message) !== receivedCheckSum) return;
  return _message;
}

function decryptMessage(buffer, keyBase64) {
  if (typeof AES === 'undefined') return;

  const key = base64ToBytes(keyBase64);
  const decrypted = AES.decrypt(buffer, key, { mode: 'ECB' });

  if (!decrypted || decrypted.byteLength === 0) return;

  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());
}

// --- Set relay + virtual components ---
function updateOutput(active) {
  if (relayState === active) return;  // No change
  relayState = active;

  let text = active ? "on" : "off";
  let bool = active ? true : false;

  Shelly.call("Switch.Set", { id: CONFIG.relayID, on: active });
  Shelly.call("Text.Set", { id: CONFIG.virtualComponentID, value: text });
  Shelly.call("Boolean.Set", { id: CONFIG.virtualComponentID, value: bool });

  let now = new Date();
  let timeStr = zeroPad(now.getHours(), 2) + ":" + zeroPad(now.getMinutes(), 2) + ":" + zeroPad(now.getSeconds(), 2);
  print(timeStr + " - Relay changed to:", text);
}

// --- Check for timeout every 5 sec ---
Timer.set(5 * 1000, true, function () {
  let now = Date.now();
  let elapsedMin = (now - lastReceivedTime) / 60000;

  let elapsedMs = now - lastReceivedTime;
  let totalSeconds = Math.floor(elapsedMs / 1000);
  let hours = Math.floor(totalSeconds / 3600);
  let minutes = Math.floor((totalSeconds % 3600) / 60);
  let seconds = totalSeconds % 60;

  let timeStr =
    zeroPad(hours, 2) + ":" +
    zeroPad(minutes, 2) + ":" +
    zeroPad(seconds, 2);

  print("Time since last signal:", timeStr);

  if (elapsedMin >= CONFIG.timeoutMinutes && relayState !== (CONFIG.actionIfTimeout === "on")) {
    print("No signal for " + CONFIG.timeoutMinutes + " min → Relay set to:", CONFIG.actionIfTimeout);
    updateOutput(CONFIG.actionIfTimeout === "on");
  }
});

// --- Handle LoRa messages ---
Shelly.addEventHandler(function (event) {
  if (!event || event.name !== 'lora' || event.id !== 100 || !event.info || !event.info.data) return;

  const encryptedMsg = atob(event.info.data);
  const decryptedMessage = decryptMessage(encryptedMsg, aesKey);
  if (!decryptedMessage) return;

  lastReceivedTime = Date.now();  // Reset timer
  print("Signal received → LoRa message:", decryptedMessage);

  // Restore opposite of timeout action
  updateOutput(CONFIG.actionIfTimeout !== "on");

  if (CONFIG.debug) {
    Timer.set(100, false, function () {
      logLoRaEventSummary(event, decryptedMessage);
    });
  }
});

// --- Initial relay state (opposite of timeout action) ---
updateOutput(CONFIG.actionIfTimeout !== "on");