Shelly Blu Wall Switch 4 “stepless” Dimming

Shelly Blu Wall Switch 4 “stepless” Dimming

Shelly Blu Wall Switch 4 "stepless" Dimming

Can be used with:

  • Shelly Dimmer Gen3
  • Shelly Pro Dimmer 1PM
  • Shelly Pro dimmer 2PM

NOTE!
When using a script as en Shelly BLU devices event handler/scanner, there can be af slightly delay when toggling the light.

Script for Dimmer Gen3 and Dimmer Pro 1PM

With the option for the dimmer being controlled from multiple Wall switch 4’s and their buttons.

Note:
– If button firmware is below 1.0.0.22,. Change the number in line:233 ” if (parsed.button[i] === 128) {” to 254.

Script for Dimmer Gen3 and Dimmer Pro 1PM
let step_size = 2;          // Lysændring pr. step
let step_interval_ms = 200; // Tid mellem step i ms. OBS må ikke komme under 100
let dim_timeout_ms = 10000;      // Maks tid en dæmpning må køre (failsafe)

let button_macs = {
  0: ["7c:c6:b6:65:83:24", "7c:c6:b6:23:42:17"],  // knap 1
  1: ["7c:c6:b6:65:83:24", "00:c6:b6:00:83:00"],                        // kap 2
  2: ["7c:c6:b6:23:42:17"],                        // knap 3
  3: ["7c:c6:b6:23:42:17"]                         // knap 4
};
let dimLastDirection = null;  // Første gang vælges ud fra lysstyrke
function makeMatch(buttonArray, index) {
  return function (parsed) {
    try {
      let macs = button_macs[index];
      return macs.indexOf(parsed.addr) !== -1 &&
             JSON.stringify(parsed.button) === JSON.stringify(buttonArray);
    } catch (e) {
      print("Fejl i makeMatch:", e.message);
      return false;
    }
  };
}

function handleSinglePress() {
  try {
    print("→ Kort tryk: Light.Toggle");
    Shelly.call("Light.Toggle", { id: 0 });
  } catch (e) {
    print("Fejl i handleSinglePress:", e.message);
  }
}

let CONFIG = {
  actions: [
    { customMatch: makeMatch([1,0,0,0], 0), action: handleSinglePress },
    { customMatch: makeMatch([0,1,0,0], 1), action: handleSinglePress },
    { customMatch: makeMatch([0,0,1,0], 2), action: handleSinglePress },
    { customMatch: makeMatch([0,0,0,1], 3), action: handleSinglePress },
  ]
};

let holdStates = [false, false, false, false];
let holdTimers = [null, null, null, null];
let holdDelays = [null, null, null, null];
let holdFailsafes = [null, null, null, null];

function startHold(index) {
  try {
    if (holdStates[index] || holdDelays[index]) return;

    holdDelays[index] = Timer.set(300, false, function () {
      holdDelays[index] = null;
      holdStates[index] = true;
      print("Hold start: Knap " + (index + 1));

      holdFailsafes[index] = Timer.set(dim_timeout_ms, false, function () {
        print("⚠️ Failsafe: Automatisk stop af dæmpning på knap " + (index + 1));
        stopHold(index);
      }, null);

      try {
        Shelly.call("Light.GetStatus", { id: 0 }, function (res) {
          try {
            let brightness = res.brightness;
            if (typeof brightness !== "number") brightness = 100;

            if (dimLastDirection === null) {
              dimLastDirection = (brightness < 50) ? "up" : "down";
              print("→ Første dæmpning vælger retning ud fra lysstyrke: " + dimLastDirection);
            } else {
              dimLastDirection = (dimLastDirection === "down") ? "up" : "down";
              print("→ Skifter dæmperetning: " + dimLastDirection);
            }

            holdTimers[index] = Timer.set(step_interval_ms, true, function () {
              if (!holdStates[index]) return;

              if (dimLastDirection === "down") {
                brightness = Math.max(1, brightness - step_size);
              } else {
                brightness = Math.min(100, brightness + step_size);
              }
              print("→ Manuel dim: " + brightness + "%");
              try {
                Shelly.call("Light.Set", { id: 0, brightness: brightness, on: true });
              } catch (e) {
                print("Fejl i Light.Set:", e.message);
              }
            }, null);

          } catch (e) {
            print("Fejl i Light.GetStatus callback:", e.message);
          }
        });
      } catch (e) {
        print("Fejl i Shelly.call Light.GetStatus:", e.message);
      }
    }, null);
  } catch (e) {
    print("Fejl i startHold:", e.message);
  }
}

function stopHold(index) {
  try {
    if (holdDelays[index]) {
      Timer.clear(holdDelays[index]);
      holdDelays[index] = null;
    }

    if (holdTimers[index]) {
      Timer.clear(holdTimers[index]);
      holdTimers[index] = null;
    }

    if (holdFailsafes[index]) {
      Timer.clear(holdFailsafes[index]);
      holdFailsafes[index] = null;
    }

    if (holdStates[index]) {
      holdStates[index] = false;
      print("Hold stop: Knap " + (index + 1));
    }
  } catch (e) {
    print("Fejl i stopHold:", e.message);
  }
}

const SCAN_PARAM_WANT = { duration_ms: BLE.Scanner.INFINITE_SCAN, active: false };
const BTHOME_SVC_ID_STR = "fcd2";
const uint8 = 0;
const BTH = {
  0x00: { n: "pid", t: uint8 },
  0x01: { n: "battery", t: uint8 },
  0x3a: { n: "button", t: uint8 },
};

function getByteSize(type) {
  if (type === 0 || type === 1) return 1;
  if (type === 2 || type === 3) return 2;
  if (type === 4 || type === 5) return 3;
  return 255;
}

const BTHomeDecoder = {
  utoi: function(num, bitsz) {
    let mask = 1 << (bitsz - 1);
    return (num & mask) ? num - (1 << bitsz) : num;
  },
  getUInt8: function(buffer) {
    return buffer.at(0);
  },
  getBufValue: function(type, buffer) {
    if (buffer.length < getByteSize(type)) return null;
    if (type === uint8) return this.getUInt8(buffer);
    return null;
  },
  unpack: function(buffer) {
    try {
      if (typeof buffer !== "string" || buffer.length === 0) return null;
      let result = {};
      let dib = buffer.at(0);
      result.encryption = (dib & 0x1) ? true : false;
      result.BTHome_version = dib >> 5;
      if (result.BTHome_version !== 2 || result.encryption) return null;
      buffer = buffer.slice(1);
      while (buffer.length > 0) {
        let type_id = buffer.at(0);
        let bth = BTH[type_id];
        if (!bth) break;
        buffer = buffer.slice(1);
        let value = this.getBufValue(bth.t, buffer);
        if (value === null) break;
        if (typeof result[bth.n] === "undefined") result[bth.n] = value;
        else if (Array.isArray(result[bth.n])) result[bth.n].push(value);
        else result[bth.n] = [result[bth.n], value];
        buffer = buffer.slice(getByteSize(bth.t));
      }
      return result;
    } catch (e) {
      print("Fejl i BTHomeDecoder.unpack:", e.message);
      return null;
    }
  }
};

let ShellyBLUParser = {
  getData: function(res) {
    try {
      let result = BTHomeDecoder.unpack(res.service_data[BTHOME_SVC_ID_STR]);
      if (!result) return null;
      result.addr = res.addr;
      result.rssi = res.rssi;
      return result;
    } catch (e) {
      print("Fejl i ShellyBLUParser.getData:", e.message);
      return null;
    }
  }
};

let last_packet_id = 0x100;

function scanCB(ev, res) {
  try {
    if (ev !== BLE.Scanner.SCAN_RESULT) return;
    if (!res.service_data || !res.service_data[BTHOME_SVC_ID_STR]) return;

    let parsed = ShellyBLUParser.getData(res);
    if (!parsed || parsed.pid === last_packet_id) return;
    last_packet_id = parsed.pid;

    if (!Array.isArray(parsed.button)) {
      parsed.button = [parsed.button, 0, 0, 0];
    }

    let allMacs = [];
    for (let i = 0; i < 4; i++) {
      let macs = button_macs[i];
      for (let j = 0; j < macs.length; j++) {
        if (macs[j] === parsed.addr) {
          print("Signal fra enhed! RSSI:", parsed.rssi, " Button:", JSON.stringify(parsed.button));
          break;
        }
      }
    }

    for (let i = 0; i < 4; i++) {
      if (button_macs[i].indexOf(parsed.addr) === -1) continue;

      if (parsed.button[i] === 128) {
        startHold(i);
      } else if (parsed.button[i] >= 1 && parsed.button[i] <= 4) {
        stopHold(i);
      }
    }

    for (let i = 0; i < CONFIG.actions.length; i++) {
      let actionObj = CONFIG.actions[i];
      if (typeof actionObj.customMatch === "function" && actionObj.customMatch(parsed)) {
        try {
          actionObj.action(parsed);
        } catch (e) {
          print("Fejl i action:", e.message);
        }
      }
    }
  } catch (e) {
    print("Fejl i scanCB:", e.message);
  }
}

function init() {
  try {
    let BLEConfig = Shelly.getComponentConfig("ble");
    if (!BLEConfig.enable) {
      print("Bluetooth er ikke aktiveret");
      return;
    }

    if (typeof BLE.Scanner.SetScanParams === "function") {
      BLE.Scanner.SetScanParams({ interval_ms: 50, window_ms: 50 });
    }

    if (!BLE.Scanner.isRunning()) {
      if (!BLE.Scanner.Start(SCAN_PARAM_WANT)) {
        print("Kunne ikke starte BLE scanner");
        return;
      }
    }

    BLE.Scanner.Subscribe(scanCB);
  } catch (e) {
    print("Fejl i init:", e.message);
  }
}

init();

Script for Dimmer Gen3 and Dimmer Pro 1/2PM

The same as the script above, but with the option to choose which output/ID to control (Specifically for the PRO Dimmer 2PM)
With the option for the dimmer being controlled from multiple Wall switch 4’s and their buttons.

Note:
– If button firmware is below 1.0.0.22,. Change the number in line:237 ” if (parsed.button[i] === 128) {” to 254.

Script for Dimmer Gen3 and Pro Dimmer 1/2PM
let step_size = 2;          // Lysændring pr. step
let step_interval_ms = 200; // Tid mellem step i ms. MUST NOT BE SET BELOW 100.
let dimLastDirection = null;  // Første gang vælges ud fra lysstyrke
let dim_timeout_ms = 10000;      // Maks tid en dæmpning må køre (failsafe)

// Hver knap har nu en liste af enheder med både mac og id
let button_macs = {
  0: [ { mac: "7c:c6:b6:65:83:24", id: 0 }, { mac: "11:22:33:44:55:66", id: 1 } ],
  1: [ { mac: "7c:c6:b6:aa:bb:cc", id: 0 } ],
  2: [ { mac: "7c:c6:b6:11:22:33", id: 0 } ],
  3: [ { mac: "7c:c6:b6:44:55:66", id: 0 } ]
};

function makeMatch(buttonArray, index) {
  return function (parsed) {
    try {
      let entries = button_macs[index];
      for (let i = 0; i < entries.length; i++) {
        if (entries[i].mac === parsed.addr && JSON.stringify(parsed.button) === JSON.stringify(buttonArray)) {
          parsed._target_id = entries[i].id;
          return true;
        }
      }
    } catch (e) {
      print("Fejl i makeMatch:", e.message);
    }
    return false;
  };
}

function handleSinglePress(parsed) {
  try {
    let id = parsed._target_id || 0;
    print("→ Kort tryk: Light.Toggle (id=" + id + ")");
    Shelly.call("Light.Toggle", { id: id });
  } catch (e) {
    print("Fejl i handleSinglePress:", e.message);
  }
}

let CONFIG = {
  actions: [
    { customMatch: makeMatch([1,0,0,0], 0), action: handleSinglePress },
    { customMatch: makeMatch([0,1,0,0], 1), action: handleSinglePress },
    { customMatch: makeMatch([0,0,1,0], 2), action: handleSinglePress },
    { customMatch: makeMatch([0,0,0,1], 3), action: handleSinglePress },
  ]
};

let holdStates = [false, false, false, false];
let holdTimers = [null, null, null, null];
let holdDelays = [null, null, null, null];
let holdFailsafes = [null, null, null, null];

function startHold(index, parsed) {
  try {
    if (holdStates[index] || holdDelays[index]) return;
    let id = parsed._target_id || 0;
    holdDelays[index] = Timer.set(300, false, function () {
      holdDelays[index] = null;
      holdStates[index] = true;
      print("Hold start: Knap " + (index + 1));

      holdFailsafes[index] = Timer.set(dim_timeout_ms, false, function () {
        print("⚠️ Failsafe: Automatisk stop af dæmpning på knap " + (index + 1));
        stopHold(index);
      }, null);

      try {
        Shelly.call("Light.GetStatus", { id: id }, function (res) {
          try {
            let brightness = res.brightness;
            if (typeof brightness !== "number") brightness = 100;

            // Første gang vælges retning ud fra lysstyrke
            if (dimLastDirection === null) {
              dimLastDirection = brightness < 50 ? "down" : "up";
            }

            holdTimers[index] = Timer.set(step_interval_ms, true, function () {
              if (!holdStates[index]) return;

              if (dimLastDirection === "down") {
                brightness = Math.max(1, brightness - step_size);
              } else {
                brightness = Math.min(100, brightness + step_size);
              }
              print("→ Manuel dim: " + brightness + "% (id=" + id + ")");
              Shelly.call("Light.Set", { id: id, brightness: brightness, on: true });
            }, null);

            dimLastDirection = (dimLastDirection === "down") ? "up" : "down";
          } catch (e) {
            print("Fejl i Light.GetStatus callback:", e.message);
          }
        });
      } catch (e) {
        print("Fejl i Shelly.call Light.GetStatus:", e.message);
      }
    }, null);
  } catch (e) {
    print("Fejl i startHold:", e.message);
  }
}

function stopHold(index) {
  try {
    if (holdDelays[index]) {
      Timer.clear(holdDelays[index]);
      holdDelays[index] = null;
    }

    if (holdTimers[index]) {
      Timer.clear(holdTimers[index]);
      holdTimers[index] = null;
    }

    if (holdFailsafes[index]) {
      Timer.clear(holdFailsafes[index]);
      holdFailsafes[index] = null;
    }

    if (holdStates[index]) {
      holdStates[index] = false;
      print("Hold stop: Knap " + (index + 1));
    }
  } catch (e) {
    print("Fejl i stopHold:", e.message);
  }
}

const SCAN_PARAM_WANT = { duration_ms: BLE.Scanner.INFINITE_SCAN, active: false };
const BTHOME_SVC_ID_STR = "fcd2";
const uint8 = 0;
const BTH = {
  0x00: { n: "pid", t: uint8 },
  0x01: { n: "battery", t: uint8 },
  0x3a: { n: "button", t: uint8 },
};

function getByteSize(type) {
  if (type === 0 || type === 1) return 1;
  if (type === 2 || type === 3) return 2;
  if (type === 4 || type === 5) return 3;
  return 255;
}

const BTHomeDecoder = {
  utoi: function(num, bitsz) {
    let mask = 1 << (bitsz - 1);
    return (num & mask) ? num - (1 << bitsz) : num;
  },
  getUInt8: function(buffer) {
    return buffer.at(0);
  },
  getBufValue: function(type, buffer) {
    if (buffer.length < getByteSize(type)) return null;
    if (type === uint8) return this.getUInt8(buffer);
    return null;
  },
  unpack: function(buffer) {
    try {
      if (typeof buffer !== "string" || buffer.length === 0) return null;
      let result = {};
      let dib = buffer.at(0);
      result.encryption = (dib & 0x1) ? true : false;
      result.BTHome_version = dib >> 5;
      if (result.BTHome_version !== 2 || result.encryption) return null;
      buffer = buffer.slice(1);
      while (buffer.length > 0) {
        let type_id = buffer.at(0);
        let bth = BTH[type_id];
        if (!bth) break;
        buffer = buffer.slice(1);
        let value = this.getBufValue(bth.t, buffer);
        if (value === null) break;
        if (typeof result[bth.n] === "undefined") result[bth.n] = value;
        else if (Array.isArray(result[bth.n])) result[bth.n].push(value);
        else result[bth.n] = [result[bth.n], value];
        buffer = buffer.slice(getByteSize(bth.t));
      }
      return result;
    } catch (e) {
      print("Fejl i unpack:", e.message);
      return null;
    }
  }
};

let ShellyBLUParser = {
  getData: function(res) {
    try {
      let result = BTHomeDecoder.unpack(res.service_data[BTHOME_SVC_ID_STR]);
      if (!result) return null;
      result.addr = res.addr;
      result.rssi = res.rssi;
      return result;
    } catch (e) {
      print("Fejl i getData:", e.message);
      return null;
    }
  }
};

let last_packet_id = 0x100;

function scanCB(ev, res) {
  try {
    if (ev !== BLE.Scanner.SCAN_RESULT) return;
    if (!res.service_data || !res.service_data[BTHOME_SVC_ID_STR]) return;

    let parsed = ShellyBLUParser.getData(res);
    if (!parsed || parsed.pid === last_packet_id) return;
    last_packet_id = parsed.pid;

    if (!Array.isArray(parsed.button)) {
      parsed.button = [parsed.button, 0, 0, 0];
    }

    let shouldLog = false;
    for (let i = 0; i < 4; i++) {
      let entries = button_macs[i];
      for (let j = 0; j < entries.length; j++) {
        if (entries[j].mac === parsed.addr) {
          shouldLog = true;
          break;
        }
      }
    }
    if (shouldLog) print("Signal fra enhed! RSSI:", parsed.rssi, " Button:", JSON.stringify(parsed.button));

    for (let i = 0; i < 4; i++) {
      let entries = button_macs[i];
      for (let j = 0; j < entries.length; j++) {
        if (entries[j].mac === parsed.addr) {
          parsed._target_id = entries[j].id;
          if (parsed.button[i] === 128) {
            startHold(i, parsed);
          } else if (parsed.button[i] >= 1 && parsed.button[i] <= 4) {
            stopHold(i);
          }
        }
      }
    }

    for (let i = 0; i < CONFIG.actions.length; i++) {
      let actionObj = CONFIG.actions[i];
      if (typeof actionObj.customMatch === "function" && actionObj.customMatch(parsed)) {
        try {
          actionObj.action(parsed);
        } catch (e) {
          print("Fejl i action:", e.message);
        }
      }
    }
  } catch (e) {
    print("Fejl i scanCB:", e.message);
  }
}

function init() {
  try {
    let BLEConfig = Shelly.getComponentConfig("ble");
    if (!BLEConfig.enable) {
      print("Bluetooth er ikke aktiveret");
      return;
    }

    if (typeof BLE.Scanner.SetScanParams === "function") {
      BLE.Scanner.SetScanParams({ interval_ms: 50, window_ms: 50 });
    }

    if (!BLE.Scanner.isRunning()) {
      if (!BLE.Scanner.Start(SCAN_PARAM_WANT)) {
        print("Kunne ikke starte BLE scanner");
        return;
      }
    }

    BLE.Scanner.Subscribe(scanCB);
  } catch (e) {
    print("Fejl i init:", e.message);
  }
}

init();

Script ONLY DIM ( Only ch.1 for now)

As mention in the beginning there can be a slight delay when turning the light ON/OFF.

For now, as a temporary workaround, you can use this script that only dims the light. Please note that any toggle automation must be implemented through scenes.
Notes to this:
– You must have other Shelly device set as BLE obervers.
– You can’t solve it by just create an action on the device, because the BLE scanner in the script will still interfere.
– Right now this only work for 1channel dimmers lige Dimmer Gen3 and Pro dimmer 1PM
– If button firmware is below 1.0.0.22,. Change the number in line: 190: “if (parsed.button[i] === 128) {” to 254.

Script for ONLY DIM
let step_size = 2;          // Lysændring pr. step
let step_interval_ms = 200; // Tid mellem step i ms. OBS må ikke komme under 100
let dim_timeout_ms = 10000;      // Maks tid en dæmpning må køre (failsafe)

let button_macs = {
  0: ["7c:c6:b6:65:83:24", "7c:c6:b6:23:42:17"],  // knap 1
  1: ["7c:c6:b6:65:83:24"],                        // knap 2
  2: ["7c:c6:b6:23:42:17"],                        // knap 3
  3: ["7c:c6:b6:23:42:17"]                         // knap 4
};
let dimLastDirection = null;  // Første gang vælges ud fra lysstyrke

let holdStates = [false, false, false, false];
let holdTimers = [null, null, null, null];
let holdDelays = [null, null, null, null];
let holdFailsafes = [null, null, null, null];

function startHold(index) {
  try {
    if (holdStates[index] || holdDelays[index]) return;

    holdDelays[index] = Timer.set(300, false, function () {
      holdDelays[index] = null;
      holdStates[index] = true;
      print("Hold start: Knap " + (index + 1));

      holdFailsafes[index] = Timer.set(dim_timeout_ms, false, function () {
        print("⚠️ Failsafe: Automatisk stop af dæmpning på knap " + (index + 1));
        stopHold(index);
      }, null);

      try {
        Shelly.call("Light.GetStatus", { id: 0 }, function (res) {
          try {
            let currentBrightness = res.brightness;
            if (typeof currentBrightness !== "number") currentBrightness = 100;

            if (dimLastDirection === null) {
              dimLastDirection = (currentBrightness < 50) ? "up" : "down";
              print("→ Første dæmpning vælger retning ud fra lysstyrke: " + dimLastDirection);
            } else {
              dimLastDirection = (dimLastDirection === "down") ? "up" : "down";
              print("→ Skifter dæmperetning: " + dimLastDirection);
            }

            let brightness = currentBrightness;

            holdTimers[index] = Timer.set(step_interval_ms, true, function () {
              if (!holdStates[index]) return;

              if (dimLastDirection === "down") {
                brightness = Math.max(1, brightness - step_size);
              } else {
                brightness = Math.min(100, brightness + step_size);
              }
              print("→ Manuel dim: " + brightness + "%");
              try {
                Shelly.call("Light.Set", { id: 0, brightness: brightness, on: true });
              } catch (e) {
                print("Fejl i Light.Set:", e.message);
              }
            }, null);

          } catch (e) {
            print("Fejl i Light.GetStatus callback:", e.message);
          }
        });
      } catch (e) {
        print("Fejl i Shelly.call Light.GetStatus:", e.message);
      }
    }, null);
  } catch (e) {
    print("Fejl i startHold:", e.message);
  }
}

function stopHold(index) {
  try {
    if (holdDelays[index]) {
      Timer.clear(holdDelays[index]);
      holdDelays[index] = null;
    }

    if (holdTimers[index]) {
      Timer.clear(holdTimers[index]);
      holdTimers[index] = null;
    }

    if (holdFailsafes[index]) {
      Timer.clear(holdFailsafes[index]);
      holdFailsafes[index] = null;
    }

    if (holdStates[index]) {
      holdStates[index] = false;
      print("Hold stop: Knap " + (index + 1));
    }
  } catch (e) {
    print("Fejl i stopHold:", e.message);
  }
}

const SCAN_PARAM_WANT = { duration_ms: BLE.Scanner.INFINITE_SCAN, active: false };
const BTHOME_SVC_ID_STR = "fcd2";
const uint8 = 0;
const BTH = {
  0x00: { n: "pid", t: uint8 },
  0x01: { n: "battery", t: uint8 },
  0x3a: { n: "button", t: uint8 },
};

function getByteSize(type) {
  if (type === 0 || type === 1) return 1;
  if (type === 2 || type === 3) return 2;
  if (type === 4 || type === 5) return 3;
  return 255;
}

const BTHomeDecoder = {
  utoi: function(num, bitsz) {
    let mask = 1 << (bitsz - 1);
    return (num & mask) ? num - (1 << bitsz) : num;
  },
  getUInt8: function(buffer) {
    return buffer.at(0);
  },
  getBufValue: function(type, buffer) {
    if (buffer.length < getByteSize(type)) return null;
    if (type === uint8) return this.getUInt8(buffer);
    return null;
  },
  unpack: function(buffer) {
    try {
      if (typeof buffer !== "string" || buffer.length === 0) return null;
      let result = {};
      let dib = buffer.at(0);
      result.encryption = (dib & 0x1) ? true : false;
      result.BTHome_version = dib >> 5;
      if (result.BTHome_version !== 2 || result.encryption) return null;
      buffer = buffer.slice(1);
      while (buffer.length > 0) {
        let type_id = buffer.at(0);
        let bth = BTH[type_id];
        if (!bth) break;
        buffer = buffer.slice(1);
        let value = this.getBufValue(bth.t, buffer);
        if (value === null) break;
        if (typeof result[bth.n] === "undefined") result[bth.n] = value;
        else if (Array.isArray(result[bth.n])) result[bth.n].push(value);
        else result[bth.n] = [result[bth.n], value];
        buffer = buffer.slice(getByteSize(bth.t));
      }
      return result;
    } catch (e) {
      print("Fejl i BTHomeDecoder.unpack:", e.message);
      return null;
    }
  }
};

let ShellyBLUParser = {
  getData: function(res) {
    try {
      let result = BTHomeDecoder.unpack(res.service_data[BTHOME_SVC_ID_STR]);
      if (!result) return null;
      result.addr = res.addr;
      result.rssi = res.rssi;
      return result;
    } catch (e) {
      print("Fejl i ShellyBLUParser.getData:", e.message);
      return null;
    }
  }
};

let last_packet_id = 0x100;

function scanCB(ev, res) {
  try {
    if (ev !== BLE.Scanner.SCAN_RESULT) return;
    if (!res.service_data || !res.service_data[BTHOME_SVC_ID_STR]) return;

    let parsed = ShellyBLUParser.getData(res);
    if (!parsed || parsed.pid === last_packet_id) return;
    last_packet_id = parsed.pid;

    if (!Array.isArray(parsed.button)) {
      parsed.button = [parsed.button, 0, 0, 0];
    }

    let allMacs = [];
    for (let i = 0; i < 4; i++) {
      let macs = button_macs[i];
      for (let j = 0; j < macs.length; j++) {
        if (macs[j] === parsed.addr) {
          print("Signal fra enhed! RSSI:", parsed.rssi, " Button:", JSON.stringify(parsed.button));
          break;
        }
      }
    }

    for (let i = 0; i < 4; i++) {
      if (button_macs[i].indexOf(parsed.addr) === -1) continue;

      if (parsed.button[i] === 254) {
        startHold(i);
      } else if (parsed.button[i] >= 1 && parsed.button[i] <= 4) {
        stopHold(i);
      }
    }
  } catch (e) {
    print("Fejl i scanCB:", e.message);
  }
}

function init() {
  try {
    let BLEConfig = Shelly.getComponentConfig("ble");
    if (!BLEConfig.enable) {
      print("Bluetooth er ikke aktiveret");
      return;
    }

    if (typeof BLE.Scanner.SetScanParams === "function") {
      BLE.Scanner.SetScanParams({ interval_ms: 50, window_ms: 50 });
    }

    if (!BLE.Scanner.isRunning()) {
      if (!BLE.Scanner.Start(SCAN_PARAM_WANT)) {
        print("Kunne ikke starte BLE scanner");
        return;
      }
    }

    BLE.Scanner.Subscribe(scanCB);
  } catch (e) {
    print("Fejl i init:", e.message);
  }
}

init();