paranerd Posted August 19, 2024 Posted August 19, 2024 I want to be able to shut off the Shelly Plus Plug S when power consumption is below a certain threshold for a defined period of time. Currently I'm solving this via a Home Assistant automation but was thinking of moving it onto the device itself for reliability. Would this be possible via Shelly Scripts? From what I've found in the documentation I cannot access the power consumption in code. Any suggestions would be appreciated :-) Quote Translate Revert translation? English (American) French German Italian Polish Portuguese (European) Spanish
tvbshelly Posted August 19, 2024 Posted August 19, 2024 I believe you can read out the consumption via script with the API for measuring the electrical power. This should work with the Gen2 Plus Plug S. Have a look at the documentation here: https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/PM1 Quote Translate Revert translation? English (American) French German Italian Polish Portuguese (European) Spanish
johsnon Posted October 4, 2024 Posted October 4, 2024 (edited) Option A) In the Shelly App (Cloud) you can use "Scenes" do achieve that. It's propably the most user-friendly solution; easy to setup & understand. BUT it needs your device to be online and connected to the shelly cloud. You might not want that (all the time). Option B) So here is my Scripts solution: let lowPowerThreshold = 5; // in Watt let lowPowerDuartion = 60 * 1000; // in milliseconds let switch0Timer_belowThreshold = null; // if low power for duration: switch off function swOff() { // function for turning OFF the switch print('swOff: switching off'); Shelly.call( 'switch.set', { id:0, on:false }, function (result, error_code, error_message, ud) { print('swOff: switched off'); } ); } Shelly.addEventHandler( function (event, ud) { //print(JSON.stringify(event)); // print all Events in Console output if (typeof event.component !== 'undefined') { //print('HELLO, it is not undefinied'); if (event.info.state === false) { // if the switch is off, clear timer print('Switch is OFF: clearing Timer: ' + Timer.clear(switch0Timer_belowThreshold)); } if (event.info.event === 'power_update') { // if event contains update to power print('Power update [W]: ' + event.info.apower + ' Timerstatus:' + switch0Timer_belowThreshold); if (event.info.apower < lowPowerThreshold && !(switch0Timer_belowThreshold)) { // Just start if power < thrsh AND timer does not exist print('Power < threshold && no timer yet: starting Timer'); switch0Timer_belowThreshold = Timer.set(lowPowerDuartion, // after 60sec false, // repeat swOff, // call function null // function arguments ); print('Timer started (times): ' + switch0Timer_belowThreshold); } else if (event.info.apower < lowPowerThreshold && (switch0Timer_belowThreshold)) { // power is low AND timer exists already print('Power < threshold && timer already exists: do nothing'); print('Timer running (times): ' + switch0Timer_belowThreshold); } else { //print('ELSE PATH'); //print('Timer status: ' + switch0Timer_belowThreshold); print('Power > threshold: clearing Timer: ' + Timer.clear(switch0Timer_belowThreshold)); switch0Timer_belowThreshold = null; // because I don't trust the Timer.clear() } } //endif power_update } //endif != undefined }, null ); Edited October 4, 2024 by johsnon option labels Quote Translate Revert translation? English (American) French German Italian Polish Portuguese (European) Spanish
da Woidl Posted December 12, 2024 Posted December 12, 2024 Hello @johsnon the script is great. Can you extend the script to include a query? I've already tried but can't get it to work. The extension: If the switch is turned on but no power is consumed in 60 seconds, it should also be turned off again. Currently the power must be > 5 watts before the script starts. Sorry for my English Quote Translate Revert translation? English (American) French German Italian Polish Portuguese (European) Spanish
BenL Posted February 18 Posted February 18 (edited) Here's a script that turns off an output of a Shelly Pro4PM when the load power draw drops below a given threshold for a certain time. It handles the case where the switch is turned on but no power is drawn: https://github.com/af3556/shelly/blob/main/underpower-off.js One potential use case could be a "safety interlock" for a machine where it is desired to also turn off the outlet once the machine itself has been turned off / stopped, i.e. so that it can't start up again without instructing the Shelly switch to re-enable the output. I've written a two-part post about creating this script, and "Shelly scripting" in general: https://af3556.github.io/posts/shelly-scripting-part1/ Feedback welcome. For the record, here's the current version of the script with comments turned down: /* Shelly script to turn off an output when the load power is below a given threshold for a given time period. Device: Pro4PM 1.4.4| 679fcca9 1. tracks switch state in a global object that is updated as each new piece of information arrives via the various Shelly notifications - including recording the time of entering 'idle state' (required for a timeout) 2. turns the output off when the idle state and timeout conditions are met */ // configure these as desired: // - switch IDs are 0-based (i.e. 0-3 for the Pro4PM) though they're labelled on // the device as 1-4 var CONFIG = { switchId: 1, // switch to monitor threshold: 10, // idle threshold (Watts) timeout: 30, // timeout timeout (seconds) (rounded up to heartbeat timeout) log: true // enable/disable logging } var switchState = { output: null, // last known switch output State apower: 0, // last known `apower` reading timer: 0 // timestamp of last on or idle transition } var currentTime = 0; // not every notification has a timestamp, have to DIY function _defined(v) { return v !== undefined && v !== null; } // helper to avoid barfing on a TypeError when object properties are missing function _get(obj, path) { var parts = path.split('.'); var current = obj; for (var i = 0; i < parts.length; i++) { if (current && current[parts[i]] !== undefined && current[parts[i]] !== null) { current = current[parts[i]]; } else { return undefined; } } return current; } function _log() { if (CONFIG.log) console.log('[underpower-off]', arguments.join(' ')); } function _callback(result, errorCode, errorMessage) { if (errorCode != 0) { // not _log: always report actual errors console.log('call failed: ', errorCode, errorMessage); } } function _getSwitchTimestamp() { currentTime = Shelly.getComponentStatus('Sys').unixtime; } // 'init' function when the script is first starting up "under load" function _getSwitchState() { var status = Shelly.getComponentStatus('Switch', CONFIG.switchId); _log('_getSwitchState status=', JSON.stringify(status)); switchState.output = status.output; switchState.apower = status.apower; switchState.timer = currentTime; } // update switch state with current output state (on/off) function _updateSwitchOutput(notifyStatus) { var output = _get(notifyStatus, 'delta.output'); if (!_defined(output)) return; // not a delta.output update _log('_updateSwitchOutput output=', JSON.stringify(output)); // reset the timer when turning on ('on/off edge transition') // !== true is not necessarily === false (e.g. on init, where output is null); // just want to determine a _change_ if (output === true && switchState.output !== output) { _log('_updateSwitchOutput reset timer'); switchState.timer = currentTime; } switchState.output = output; } // update switch state with current power function _updateSwitchPower(notifyStatus) { // `delta.apower` notifications are sent on load changes _and_ switch output // state changes (even when power remains 0) var apower = _get(notifyStatus, 'delta.apower'); if (!_defined(apower)) return; // not a delta.apower update _log('_updateSwitchPower apower=', JSON.stringify(apower)); // reset the timer on power idle edge transition; when going from not-idle to // idle var idlePrev = _isPowerIdle(); switchState.apower = apower; if (idlePrev === false && _isPowerIdle() !== idlePrev) { _log('_updateSwitchPower reset timer'); switchState.timer = currentTime; } } function _isTimeExpired() { return currentTime - switchState.timer > CONFIG.timeout; } function _isPowerIdle() { return switchState.apower < CONFIG.threshold; } function statusHandler(notifyStatus) { if (notifyStatus.component !== 'switch:' + CONFIG.switchId) return; _getSwitchTimestamp(); _updateSwitchPower(notifyStatus); _updateSwitchOutput(notifyStatus); switch (switchState.output) { // JS switch uses strict equality case true: // on _log('on p=', switchState.apower, ' dt=', currentTime - switchState.timer); if (_isPowerIdle() && _isTimeExpired()) { Shelly.call('Switch.Set', { id: CONFIG.switchId, on: false }, _callback); } break; case false: // off; nothing to do break; default: // when the script starts up with a constant load output (incl. 0), we won't // see any `delta.output` or `delta.apower` notifications (only // hearbeats), have to "manually" get the current state // this should happen only once; no need to invoke on every iteration _getSwitchState(); } _log(JSON.stringify(switchState)); } Shelly.addStatusHandler(statusHandler); Edited February 18 by BenL bug fix ;-) Quote Translate Revert translation? English (American) French German Italian Polish Portuguese (European) Spanish
Grv Posted May 24 Posted May 24 Hi @BenL If having 2 switch on the 4PM to monitor is there a better solution than to have 2 scripts ? Quote Translate Revert translation? English (American) French German Italian Polish Portuguese (European) Spanish
ErikDB Posted May 29 Posted May 29 (edited) On 2/18/2025 at 7:59 AM, BenL said: Here's a script that turns off an output of a Shelly Pro4PM when the load power draw drops below a given threshold for a certain time. It handles the case where the switch is turned on but no power is drawn: https://github.com/af3556/shelly/blob/main/underpower-off.js One potential use case could be a "safety interlock" for a machine where it is desired to also turn off the outlet once the machine itself has been turned off / stopped, i.e. so that it can't start up again without instructing the Shelly switch to re-enable the output. I've written a two-part post about creating this script, and "Shelly scripting" in general: https://af3556.github.io/posts/shelly-scripting-part1/ Feedback welcome. For the record, here's the current version of the script with comments turned down: (script) Thanks for your wonderful script and intro! I made a script myself a few months ago, based on johsnon's post of 04/10/2024, but it started crashing after the firmware was updated to 1.6.2. I run/ran it on "Shelly Plus Plug S". Now I swapped it out for your script, but added a few things. One is the "restart" functionality: one of my Linux boxes doesn't always restart properly, or doesn't start up properly when power is restored, so I want the Shelly to turn off, and then on again after some time. It's the "stroomWeerAanzetten" function (as well as a timer for it, in function "statusHandler"). Feedback is of course always welcome. The other added thing is that I've got Shellies for different devices, so with different parameters. I don't think it's possible to jam it all into the "CONFIG" var, since it would then rely on variables in itself. But I didn't test that... Maybe (probably) there's also a better solution there... My modified script (my comments and namings are in Dutch, I'm afraid - I wrote it for myself): /* Shelly script to turn off an output when the load power is below a given threshold for a given time period. Bron: https://community.shelly.cloud/topic/2018-turn-off-shelly-plus-plug-s-when-power-lower-than-x-for-y-minutes/ (post van 18/02/2025) Originally written for device: Pro4PM 1.4.4| 679fcca9 ## Use Cases This script was created to turn a regular pressure-controlled water pump into a "one-shot" pump - one that turns off and stays off shortly after the high pressure level is reached. This is being used to transfer water from one tank to another, where the destination tank has a float valve that closes when it is full. The goal is to (manually at this point) turn the pump on when required and then not have to keep a close eye on it from there on. The water pump has a built-in pressure switch that turns the pump off when a certain water pressure is reached and back on when pressure falls below a lower threshold. This is ideal behaviour to feed a water tap (spigot, faucet, outlet), however for my use case (transferring water between tanks) once the pressure cutoff is reached [the pump's job is done](https://www.youtube.com/watch?v=Kmu_UVgk2ZA&t=367s) until restarted at some much later time. Ideally the water would stay pressurised for arbitrarily long periods of time and thus the pump's lower pressure limit never reached and the pump would stay off of its own accord - however it turns out real-world one-way / non-return valves do not always behave as they are named and the pump cycles every 10-20 minutes. A simple off-timer could be used to shut the pump power off after some fixed time, however the time taken for the pump to do its work can vary quite substantially and the pump may either be cut short or will needlessly cycle once it hits its pressure limit. Also, where's the fun in that? Instead, this script is used to monitor the pump's energy use and when it drops to some low value (a proxy for the pump having completed its work for now), turns power to the pump off. A simpler use case could be a machine that for safety reasons it is desired to also turn off the outlet once the machine itself has been turned off / stopped. Ref. https://af3556.github.io/posts/shelly-scripting-part1/ ## Script Design This script: 1. tracks switch state in a global object that is updated as each new piece of information arrives via the various Shelly notifications - including recording the time of entering 'idle state' 2. turns the output off when the idle state and timeout conditions are met */ // configure these as desired: // - switch IDs are 0-based (i.e. 0-3 for the Pro4PM) though they're labelled on // the device as 1-4 // - a timeout of 0 would technically not work: Shelly reports the switch // turning on and the load current as two separate events (which switch state // first), so the moment a switch is turned the load is likely to be zero; the // fix is to defer the decision until after at least one power notification // has arrived (i.e. when we do have all the necessary info) // - this is represented as an output state of 'waiting' // - an alternative approach would be to just hard-code a minimum period but // there's no guarantee when a power update will arrive and it's usually // >7-8s for the Pro4PM var toestelnummer = 2 // Zie `CONFIG.toestellen` var toestellen = [ { toestelnaam: "Linuxtoestelletjes in de kelder", opnieuwaanzetten: true, lowPowerDuartion: 2 * 60, // in seconden tijdzonderstroom: 40 // in seconden }, { toestelnaam: "Fietsenladers in het tuinhuis", opnieuwaanzetten: false, lowPowerDuartion: 2 * 60, // in seconden tijdzonderstroom: 20 // in seconden }, { toestelnaam: "Laptoplader in de studieruimte", opnieuwaanzetten: false, lowPowerDuartion: 25, // in seconden tijdzonderstroom: 20 // in seconden } ] var CONFIG = { switchId: 0, // switch to monitor threshold: 5, // idle threshold (Watts) timeout: toestellen[toestelnummer].lowPowerDuartion, // timeout (seconds) (0 = ASAP) herstartnodig: toestellen[toestelnummer].opnieuwaanzetten, // moet het toestel opnieuw opgestart worden? tijdtotherstart: toestellen[toestelnummer].tijdzonderstroom * 1000, // timeout (milliseconden) (0 = ASAP) log: true, // enable/disable logging opstartprocedureloopt: false, // is er een timer actief voor het opnieuw opstarten? aanzettimer: null // de timer om het toestel opnieuw op te starten } // notifications for switch state changes (e.g. on/off, power) arrive // independently and asynchronously; the state machine logic is greatly // simplified by having all the necessary inputs in the one place/time // // this object is used to accumulate, via each Shelly notification, a complete // view of the device's actual state as at the last change time // - an alternative approach could just query all the necessary bits every // callback, but where's the fun in that #efficiency var switchState = { output: null, // last known switch output State (null, true, false, 'waiting') apower: 0, // last known `apower` reading timer: 0 // timestamp of last on or idle transition } // use uptime as the epoch (it's always available; unixtime requires NTP) var currentTime = 0; // rate limit console.log messages to the given interval var _logQueue = { queue: [], // queued messages maxSize: 20, // limit the size of the queue interval: 100 // interval, ms } // dequeue one message; intended to be called via a Timer function _logWrite() { // Shelly doesn't do array.shift (!), splice instead if (_logQueue.queue.length > 0) { // include a 'tag' in the log messages for easier filtering console.log('[underpower-off]', _logQueue.queue.splice(0, 1)[0]); } } function _log() { // Shelly doesn't support the spread operator `...` // workaround: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments if (!CONFIG.log) return; if (_logQueue.queue.length < _logQueue.maxSize) { _logQueue.queue.push(arguments.join(' ')); } else { console.log('_log: overflow!!'); // you may or may not actually get to see this } } function _notnullish(v) { return v !== undefined && v !== null; } /* Whee javascript... definitely a good idea to avoid attempting to deference a non-existent property - doing so will kill the script and it'll not automatically restart - ES6 addresses this problem w/ 'optional chaining' (?.) operator - this ain't ES6 A simple one-liner would normally suffice: return path.split('.').reduce((o, p) -> (typeof o === 'undefined' || o === null ? o : o[p]), obj); however Shelly's Array object has been neutered of the .reduce() function. */ // helper to avoid barfing on a TypeError when object properties are missing function _get(obj, path) { var parts = path.split('.'); var current = obj; for (var i = 0; i < parts.length; i++) { if (current && current[parts[i]] !== undefined && current[parts[i]] !== null) { current = current[parts[i]]; } else { return undefined; } } return current; } function _callbackLogError(result, errorCode, errorMessage) { if (errorCode != 0) { // not _log: always report actual errors console.log('call failed: ', errorCode, errorMessage); } } // 'init' switch state when the script is starting up with no or constant load, // where statusHandler hasn't been called for the switch yet function _getSwitchState() { var status = Shelly.getComponentStatus('Switch', CONFIG.switchId); _log('_getSwitchState status=', JSON.stringify(status)); switchState.output = status.output; switchState.apower = status.apower; switchState.timer = currentTime; } // update switch state with current output state (on/off) function _updateSwitchOutput(notifyStatus) { var output = _get(notifyStatus, 'delta.output'); if (!_notnullish(output)) return; // not a delta.output update _log('_updateSwitchOutput output=', JSON.stringify(output)); // !== true is not necessarily === false (e.g. on init, where output is null); // just want to determine a _change_ if (switchState.output !== output) { // an edge transition // reset the timer when turning on ('on/off edge transition') if (output === true) { // was off, now on _log('_updateSwitchOutput reset timer (and waiting for power update)'); switchState.timer = currentTime; output = 'waiting'; // await an apower notification } } switchState.output = output; } // update switch state with current power function _updateSwitchPower(notifyStatus) { // `delta.apower` notifications are sent on load changes _and_ switch output // state changes (even when power remains 0) var apower = _get(notifyStatus, 'delta.apower'); if (!_notnullish(apower)) return; // not a delta.apower update _log('_updateSwitchPower apower=', JSON.stringify(apower)); var idlePrev = _isPowerIdle(); switchState.apower = apower; if (_isPowerIdle() !== idlePrev) { // an edge transition if (idlePrev === false) { // reset the idle timer on transition from not-idle to idle _log('_updateSwitchPower reset timer'); switchState.timer = currentTime; } else { _log('_updateSwitchPower no longer waiting'); switchState.output = true; // would have been 'waiting' } } } function _isTimeExpired() { return currentTime - switchState.timer >= CONFIG.timeout; } function _isPowerIdle() { return switchState.apower < CONFIG.threshold; } function statusHandler(notifyStatus) { // only interested in notifications regarding the specific switch if (notifyStatus.component !== 'switch:' + CONFIG.switchId) return; //_log(JSON.stringify(notifyStatus)); // https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Sys#status // use uptime and not unixtime; the latter won't be available without NTP currentTime = Shelly.getComponentStatus('Sys').uptime; // the notification will be _one of_: an `output` notification, an `apower` // notification, or 'something else' // - some notifications may include both switch `output` and `apower` info // (e.g. when a switch is turned on), this could be leveraged to eliminate // some processing but for the sake of simplicity we'll KISS _updateSwitchPower(notifyStatus); _updateSwitchOutput(notifyStatus); switch (switchState.output) { // JS switch uses strict equality case true: // on _log('on p=', switchState.apower, ' dt=', currentTime - switchState.timer); if (_isPowerIdle() && _isTimeExpired()) { _log('idle, timer expired: turning off'); Shelly.call('Switch.Set', { id: CONFIG.switchId, on: false }, _callbackLogError); if (CONFIG.herstartnodig && !CONFIG.opstartprocedureloopt) { CONFIG.aanzettimer = Timer.set(CONFIG.tijdtotherstart, // na x milliseconden (zie CONFIG) false, // repeat stroomWeerAanzetten, // call function null // function arguments ); CONFIG.opstartprocedureloopt = true } } break; case 'waiting': // fall through case false: // off; nothing to do break; default: // when the script starts up with a constant load output (incl. 0), we won't // see any `delta.output` or `delta.apower` notifications (only // hearbeats), have to "manually" get the current state // this should happen only once; no need to invoke on every iteration _getSwitchState(); } _log(JSON.stringify(switchState)); } function stroomWeerAanzetten() { Shelly.call('switch.set', { id: CONFIG.switchId, on: true }, _callbackLogError); CONFIG.opstartprocedureloopt = false CONFIG.aanzettimer = null } function init() { if (CONFIG.log) { // set up the log timer; this burns a relatively precious resource but // could easily be moved to an existing timer callback Timer.set(_logQueue.interval, true, _logWrite); } Shelly.addStatusHandler(statusHandler); } init(); PS It took me a while to figure out why "CONFIG.timeout" wasn't honored, but it's because of the heartbeat cycle being longer, of course. As your comments indicate, but I glanced over it at first. Edited May 29 by ErikDB Quote Translate Revert translation? English (American) French German Italian Polish Portuguese (European) Spanish
Grv Posted May 29 Posted May 29 I've adapted the script for my charging station piloted from a PRO4PM, avoiding a 2~3W permanent consumption Quote Translate Revert translation? English (American) French German Italian Polish Portuguese (European) Spanish
wooly Posted May 30 Posted May 30 On 5/29/2025 at 10:45 AM, Grv said: avoiding a 2~3W permanent consumption ... and how much power absorb the shelly ? Quote Translate Revert translation? English (American) French German Italian Polish Portuguese (European) Spanish
Grv Posted May 30 Posted May 30 27 minutes ago, wooly said: ... and how much power absorb the shelly ? That's not on the 4PM doc I'll monitor but 1~2W + 1~2W +1~2W + … at the ends it's 50~100W and the charging station didn't support well to be connected all the time Quote Translate Revert translation? English (American) French German Italian Polish Portuguese (European) Spanish
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.