Delay node: drop intermediate, except last

I'd like to extend the delay node to allow dropping intermediate messages; but always send the last of the rate-limited messages (if any) after the limit.

use-case: I use node-red to control my thermostat; in some cases there's some flickering on/off that I'm trying to avoid, but the last message should always be passed through (to avoid leaving the heater on forever).

Would a PR be accepted for this, or should I look towards a custom component/fork?

Other thoughts/suggestions are of course also welcome

Thanks!

I can't quite visualise what you mean.

Can you describe it in real terms of an example sequence of messages? How would the node be configured and what messages should/shouldn't pass through?

Maybe use a trigger node set to send nothing, then wait, then send last message received after a suitable de-bounce time as well/instead

given:

  • flow to calculate if the heater needs to be turned on (based on several factors)
  • service node to actually turn the heater on/off

The delay/rate limit node would sit in between to limit controls towards the heater, while always ensuring it matches the intended state (after the rate limit period)

Let's say the that flow (within seconds of each other) sends: on, off, on, off, on, off

Using the delay node in rate-limit configuration (let's say to 1msg per minute), it would pass through an on; but never turn it off.

As the flow is based on several infrequent events, it could take a considerable time before the flow outputs another (for simplicity's sake) off signal, actually turning the heater on.

The proposal would: pass through an on (right away) and (providing there are no extra inputs within a minute) send an "off" after the delay (so the heater would've effectively been turned on for a minute; instead of several on/off cycles in rapid succession.); similar to debouncing a switch.

It sounds like you need a bit of hysteresis on the compare, so it does not switch on till just below the target, then does not switch off again till just above.

Ideally, yes. But there are 2 issues with that:

  1. the update gap is significant (the fluctuations would be rather big)
  2. it's not actually controlled by just 1 source; the (central) heater is connected to the house's heating system, the above can occur even if each individual source had some hysteresis implemented.

To me this feels like quite a generic function that can be useful in several cases... but (I guess I missed it before -- I could swear I searched for debounce before :joy: ) the debounce node should actually already do exactly what I want & described :smiley: -- still willing to contribute it into the core delay node though

Does the debounce node do anything that the Trigger node doesn't?

1 Like

image
Should at least get very close, thanks!
The only thing that feels missing (but would be better suited as an extension there then) is to add a "then send" condition to "only if more messages arrived"

Could we potentially do something to improve discoverability? (node aliases for search?)
I don't really think of trigger when thinking of limiting/debouncing :slight_smile:

I don't think the debounce node will do exactly what you have specified there, but I also suspect it is not what you want. Consider what would happen if it received On, then 55 secs later Off and another 10 secs later On. The result would be that it was On for 1 minute, off for 5 secs, then back on again.

What is it that you are actually trying to achieve? Is it that you want to have a minimum on and/or off time? If so then which?

:thinking: debounce (as documented by Rx) would actually slow the "then back on again" message down by the debounce time too. (resulting in 1min on, 1 min off, then back on)

What I'm actually trying to achieve is to limit toggling the (gas-based) heater too frequently.

Could I suggest you give us a couple of sequences of input (with timing info) and what you'd want as an output in each case and then we can hopefully give you some suggestions to achieve your aim :slight_smile:

So is that the same as specifying a minimum on and or off time? In other words if it comes on then it will stay on for a least a minute before going off again? And possible the inverse too?
I use a minimum On time on my oil fired boiler so that it does not just switch on for 5 seconds, for example.

Boils down to it, yes.

Which? Min ON, OFF or both?

Sorry, both ideally

I have a function for that, but I can't immediately see a simple way with core nodes. Perhaps someone will accept today's challenge to do that.

// expects a sequence of messages containing numbers 1 and 0
// Stretches both 1 and 0 states to the specified minimum length

var minWidth = 5 * 1000;   // min width in msec

var initialState = 0;     // initial state
var timer = context.get('timer') || 0;
var lastSentValue = context.get('lastSentValue');   // the value sent last time
if (typeof lastSentValue === 'undefined') {
    // set initial value to the quiescent state
    lastSentValue = initialState;
}
context.set('newValue', msg.payload);   // the value to switch to when timer has run down

// if the new value is different to previous value past on and the timer is not running 
// then pass the new value on immediately and start the timer, the timer stops it 
// changing again till it has run out
if (msg.payload != lastSentValue && timer === 0) {
    // leave msg.payload at new value and set lastSentValue to that
    lastSentValue = msg.payload;
    context.set('lastSentValue', lastSentValue);
    startTimer();
} else {
    // otherwise always pass on the same value as last time, which may be the 
    // new value also
    msg.payload = lastSentValue;
}
return msg;

// only call this if the timer is not already running
function startTimer() {
    var timer = setTimeout(function () {
        // clear the timer in the context
        context.set('timer', null);
        // the timer has run down so can now send the new state
        var newValue = context.get('newValue');
        // have we changed the state?
        if (newValue != context.get('lastSentValue')) {
            // yes, remember it and restart the timer to extend this one if necessary
            // and set holdoff true if we are going to 0
            context.set('lastSentValue', newValue);
            startTimer();
        }
        node.send({ payload: newValue });
    }, minWidth);
    context.set('timer', timer);
}

I'm not sure why just a simple trigger won't suffice - I must be missing something...
If set to pass existing message - then wait a minute and then send last message received - that should filter out any bounces - and meet the criteria of only switching at most once per minute. If it happens to go 1 -> 0 -> 1 during that time it will send another 1 at the end which won't change the state... if you need to filter duplicate messages then follow this with a filter node.

What am I missing ?

I made this thread as I'm up for adding this to core; but wanted to discuss specifics first :wink:

options:

  1. Do we make the addition to the "trigger" node (checkbox or list entry to NOT send the original message, but only latest if changed => caveat = a message sent seconds after the "final" message would simply go through, as it's not a rate limit
  2. (original proposal, preferred) extend rate limit to add a "queue last intermediate message" (or similar); which -if present- would send the queued message counting towards the rate limit => requested behaviour would be present
    f.e. given a 1msg/minute rate limit: "if it received On, then 55 secs later Off and another 10 secs later On." => sends on, queue's off after 55sec, sends off 5 sec later, queue's On 5sec later, sends On 55sec later

or 3 - use a filter node to filter out messages that aren't a change of state...

Trigger (unless I understand it incorrectly) would have the following edge-case:

Above example would still be a state change, so just a filter wouldn't solve this. (and "extend delay if new message arrives" could cause it to get stuck in a specific unwanted state if there are too many inputs received)

Would you be ok with accepting a PR for option 2 described above?