Jump to content
⚠️ Deprecation Notice: Cloud Control API V1 – Switch to V2 ×

Recommended Posts

Posted

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 :-)

  • 1 month later...
Posted (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.

image.thumb.png.fcdd319ea2b2f1e763f7dcda787053cb.png

 

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:

image.thumb.png.390b69ff35fb495dadc37fca583a287a.png

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 by johsnon
option labels
  • 2 months later...
Posted

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

  • 2 months later...
Posted (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 by BenL
bug fix ;-)
  • 3 months later...
Posted (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 by ErikDB

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Unfortunately, your content contains terms that we do not allow. Please edit your content to remove the highlighted words below.
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...