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.
- Gen2
- Gen3
- Gen4
- Pro
// 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
Before you install
- 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.
- 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
- 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.
- Tap Create new script
If the device has no scripts yet, the screen says No scripts created.
- 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).
- 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.
- 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
- 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.
- Click Create script
If the device has no scripts yet, the list says so.
- 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).
- 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.
- 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
- 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.
- 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