Shelly Logo Squared

Script Shelly scripts

Phone alert when a globe blows, from the power the light draws

A Shelly dimmer or metering relay learns what your lights normally draw and messages your phone through ntfy when that drops, as it does when one globe on the circuit fails.

Updated v1.0

  • Gen2
  • Gen3
  • Gen4
  • Pro
blown-globe-alert.js · 233 lines Download .js
// Blown globe alert: learns what a light circuit normally draws at each brightness and// messages your phone through ntfy when the draw drops, as it does when a globe fails.// Needs a Shelly dimmer or relay that measures power. let CONFIG = {  lightName: "Hallway lights",  outputId: 0,  dropPct: 12,  minWatts: 2,  ntfyTopic: "",}; // Power is read this often. A failed globe stays failed, so there's no hurry.let CHECK_SEC = 30;// After the light is switched or dimmed, readings wait this long. LED drivers take a// moment to settle, and a fade is still running.let SETTLE_SEC = 60;// Steady readings averaged before a brightness level counts as learnt (3 minutes).let LEARN_READINGS = 6;// Low readings in a row before an alert (2 minutes), so a flicker or a brownout// doesn't send one.let CONFIRM_READINGS = 4;// Brightness levels remembered. Keeps the saved copy inside KVS's 253 characters.let MAX_LEVELS = 16;// How far a learnt figure moves towards each normal reading, so it follows a globe// slowly dimming with age.let DRIFT = 0.05;// Learnt figures are saved at most this often while drifting, to spare the flash.let SAVE_EVERY_SEC = 1800;let KVS_KEY = "blown-globe-alert-";// Below this the meter reads an LED circuit as off: the bench lamp read 0.0 W at 10%.let NO_POWER_W = 0.5; let kind = "";let base = {};let levelCount = 0;let level = -1;let steadySince = 0;let wasOn = false;let learnSum = 0;let learnN = 0;let lowN = 0;let highN = 0;let noPower = false;let dirty = false;let savedAt = 0;let queue = [];let sending = false; function readOutput() {  return Shelly.getComponentStatus(kind, CONFIG.outputId);} function uptime() {  return Shelly.getComponentStatus("sys").uptime;} function round1(w) {  return Math.round(w * 10) / 10;} function onSent(res, err) {  sending = false;  if (err === 0 && res.code === 200) {    queue.splice(0, 1);    sendNext();  } else {    print("Couldn't reach ntfy. Will try again after the next check.");  }} function sendNext() {  if (sending || queue.length === 0 || CONFIG.ntfyTopic === "") return;  sending = true;  Shelly.call("HTTP.Request", {    method: "POST",    url: "https://ntfy.sh/" + CONFIG.ntfyTopic,    body: queue[0],    headers: { "Content-Type": "text/plain" },    timeout: 15,  }, onSent);} function alert(msg) {  print(msg);  if (queue.length >= 3) queue.splice(0, 1);  queue.push(msg);} function save() {  let parts = [];  for (let k in base) parts.push(k + ":" + round1(base[k]));  Shelly.call("KVS.Set", { key: KVS_KEY + CONFIG.outputId, value: parts.join(",") });  dirty = false;  savedAt = uptime();} function where(lv) {  return kind === "light" ? " at " + lv + "% brightness" : "";} // Every learnt figure moves by the same ratio: a failed or replaced globe changes// the draw at every brightness, not just this one.function rescale(ratio) {  for (let k in base) base[k] = base[k] * ratio;  save();} function judge(lv, w) {  let b = base["" + lv];  if (b < CONFIG.minWatts) return;  let limit = CONFIG.dropPct / 100;  if (w < b * (1 - limit)) {    highN = 0;    lowN++;    if (lowN < CONFIRM_READINGS) return;    lowN = 0;    if (w < NO_POWER_W) {      // Nothing to scale to. The learnt figures stay, so the draw coming back is seen.      if (!noPower) {        alert(CONFIG.lightName + ": on but drawing no power" + where(lv) +          " (normally " + round1(b) + " W). Every globe may be out, or there's a wiring fault.");      }      noPower = true;      return;    }    alert(CONFIG.lightName + ": drawing " + Math.round((1 - w / b) * 100) + "% less" +      where(lv) + " (" + round1(w) + " W, normally " + round1(b) +      " W). A globe has probably failed.");    rescale(w / b);    return;  }  lowN = 0;  if (noPower) {    noPower = false;    print(CONFIG.lightName + ": drawing power again.");  }  if (w > b * (1 + limit)) {    highN++;    if (highN < CONFIRM_READINGS) return;    highN = 0;    print(CONFIG.lightName + ": drawing more than before (" + round1(w) + " W, was " +      round1(b) + " W). Taken as a globe replaced.");    rescale(w / b);    return;  }  highN = 0;  base["" + lv] = b + (w - b) * DRIFT;  dirty = true;} function learn(lv, w) {  if (levelCount >= MAX_LEVELS) return;  learnSum += w;  learnN++;  if (learnN < LEARN_READINGS) return;  base["" + lv] = learnSum / learnN;  levelCount++;  learnN = 0;  learnSum = 0;  print("Learnt: " + CONFIG.lightName + where(lv) + ", " + round1(base["" + lv]) + " W.");  if (base["" + lv] < CONFIG.minWatts) {    print("That's under minWatts, too little to judge reliably. This level won't be checked.");  }  save();} function check() {  let st = readOutput();  let now = uptime();  let lv = kind === "light" ? Math.round(st.brightness) : 100;  let busy = !st.output || typeof st.transition === "object" || st.errors !== undefined;  if (busy || lv !== level || !wasOn) {    // Switched, dimmed, fading or faulted: start the settle wait again.    level = lv;    wasOn = st.output;    steadySince = now;    learnN = 0;    learnSum = 0;    lowN = 0;    highN = 0;    if (busy) wasOn = false;  } else if (now - steadySince >= SETTLE_SEC) {    if (base["" + lv] === undefined) {      learn(lv, st.apower);    } else {      judge(lv, st.apower);    }  }  if (dirty && now - savedAt >= SAVE_EVERY_SEC) save();  sendNext();} function start() {  if (CONFIG.ntfyTopic === "") print("No ntfy topic set. Alerts show in this console only.");  Timer.set(CHECK_SEC * 1000, true, check);  print("Blown globe alert started for " + CONFIG.lightName + ". " + levelCount +    " brightness level(s) already learnt.");} function onLoaded(res, err) {  if (err === 0 && res && typeof res.value === "string" && res.value !== "") {    let parts = res.value.split(",");    for (let i = 0; i < parts.length; i++) {      let p = parts[i].split(":");      base[p[0]] = Number(p[1]);      levelCount++;    }  }  savedAt = uptime();  start();} function init() {  let st = Shelly.getComponentStatus("light", CONFIG.outputId);  if (st !== null) {    kind = "light";  } else {    st = Shelly.getComponentStatus("switch", CONFIG.outputId);    kind = "switch";  }  if (st === null) {    print("There's no output " + CONFIG.outputId + " on this Shelly. Check outputId.");    return;  }  if (typeof st.apower !== "number") {    print("This output doesn't measure power, so the script can't watch it.");    return;  }  Shelly.call("KVS.Get", { key: KVS_KEY + CONFIG.outputId }, onLoaded);} init();

What it does

The script learns what your lights draw at each brightness you use. Once the lights have been steady at a level for four minutes, it remembers the figure, and saves it on the Shelly so a restart doesn't lose it. From then on it compares. If the draw at a learnt level stays more than 12% below normal for two minutes, your phone gets a message such as "Hallway lights: drawing 17% less at 80% brightness (35 W, normally 42 W). A globe has probably failed." If the lights are on but drawing nothing at all, the message says every globe may be out or there's a wiring fault.

It only reads the power while the lights are on and steady, so switching, dimming and fades don't set it off, and a short flicker or brownout passes without an alert. After an alert it takes the lower draw as the new normal, so a second globe failing later sends a second message. When you replace the globe it sees the draw go back up and adjusts. A slow change, such as a globe dimming with age, is followed without an alert. Low brightness levels draw too little to measure reliably (our test lamp read almost nothing at 20%), so levels under minWatts are left unchecked.

This suits lights you can't easily see: a high ceiling, the stairs, outdoor or security lights, and a holiday house or rental. On a circuit of many globes, a single failure is a smaller share of the draw, so lower dropPct for a long run of downlights.

Install it on your Shelly

View the full install guide

Before you install

  1. Check every globe works

    The script learns from the lights as they are now, so replace any dead globe first. On a dimmer, run calibration in the Shelly app after the globes are in.

  2. Subscribe in ntfy

    Install the ntfy app and subscribe to a topic name nobody would guess, such as smith-lights-4821.

Install the script

Install it on the dimmer or relay the lights are wired to.

Shelly Smart Control

  1. Open the device and tap Scripts

    In Shelly Smart Control, open the room, then the device. Tap Scripts, the { } icon in the menu down the left.

  2. Tap Create new script

    If the device has no scripts yet, the screen says No scripts created.

  3. Name it, paste the code, then tap Save

    Name it blown-globe-alert. Tap in the code area, paste the script you copied above, then tap Save (the disk icon at the top right).

  4. Tap the play button

    The play button lights up once the script is saved. Tap it and a message says the script has been started.

  5. Close the editor and turn on Run on startup

    Back on the Scripts screen, turn on Run on startup next to your script, so it starts again after a power cut or a firmware update.

Direct access via IP

  1. Open the device's web page and click Scripts

    Find the IP address in the Shelly app under Device settings, then Device information, and type it into a browser on the same Wi-Fi. Click Scripts in the menu on the left.

  2. Click Create script

    If the device has no scripts yet, the list says so.

  3. Name it, paste the code, then click Save

    Name it blown-globe-alert. Click in the code area, paste the script you copied above, then click Save (the disk icon above the code).

  4. Click Run and check the console

    Run appears once the script is saved. The console under the code then prints what the script is doing.

  5. Turn on Run on startup

    Click Scripts again and turn on Run on startup next to your script, so it starts again after a power cut or a firmware update.

After you install

  1. Let it learn

    Turn the lights on at the brightness you normally use and leave them for four minutes. The console prints a line such as Learnt: Hallway lights at 80% brightness, 42 W. Do the same at any other level you use often.

  2. Test it

    If a globe can come out, switch the lights off, take one out and switch them back on at a level it has learnt. About three minutes later the console and your phone say the lights are drawing less and a globe has probably failed. Put the globe back and it notices the draw has returned.

Changelog

v1.0 First release.

Still need a hand?

We answer most messages within one business day. If your question is about an existing order, have your order number ready. It speeds things up.

Contact us