Cumulative power limitation

I've been trying to implement a function for some time, but unfortunately I'm not really getting anywhere and hope someone has an idea.

I would like to set up a lighting control with 3 channels (I have 3 sliders in Dashboard 2.0 with 0-100%), each channel should be controlled individually. There are luminaires with 1500 watts each on each channel. However, the total power of all 3 channels may not exceed 3500 watts. Depending on the setting of the individual channels, the total power is determined individually. Do you have any ideas on how to build this so that the sliders cannot be increased any further when the total output is reached, but only reduced?
Thanks

My take.... very basic.

You do know how to use context?

You have 3 lights you control. Each can put up to 1500 watts.
But you don't want the TOTAL output to be more than 3500 - yes?

Ok, each light has it's own topic (qualifier - if you want)

Take another wire from the sliders into the function node.

example code - NOT tested.
And not really code. But to give you an overview.

let L1 = context.get("light_1") || 0
let L2 = context.get("light_2") || 0
let L3 = context.get("light_3") || 0

if (msg.topic == "light_1")
{
       context.set("light_1",L1 + msg.payload)
}
(copy above for the other 2 instances)

let total_power = L1 + L2 + L3

if (total_power > 3500)
{
      do something here
}

Does that help?

Thanks for the idea, it sounds sensefull to get the total consumption from the individual outputs, but what I'm still missing is the option of limiting the sliders if the total consumption > 3500W so that the sliders cannot be increased any further but can only be reduced. If the total consumption is below 3500W again, all sliders should also be able to be increased again (until 3500W is cumulatively reached again to prevent an overload).

Well, ok, you need a bit of control of the sliders.

(I don't know if you can)

But.....

Hang on. I'm having a snack just now.

BRB soon

Ok, again:

THOUGHTS

It is lucky the maximum value is 3500

So let's start with all the lights OFF - 0 power.

And the names used here are examples.

Light 1 set to 100%. (aka 1500 Watts)

Light 2 set to 100% (aka 1500 watts)
Total power 3000

So the next light is limited to 30% power. 500 watts.

So here's an example bit of code.

The values are also not going to be the exact ones.
Brain is on strike just now.

//  do something here code below.
}
else
{
    const max_power = 3500
    let available_power = max_power - total_power
    let slider_max = 1500 * available_power / 100      // percentage of.
    //  send this value to the *remaining* slider node with attributes to set the `maximum` value available.
}

So that tweaks the range on the last slider from 100 to the percentage of it.
In this example, it would be 33%.
33% of 1500 = 500.
3000 + 500 = 3500. Max power.

There is some more magic needed to determine which slider needs tweaking.
But this is a way.

With me?

OK, you can't (adjust the range on a slider)

But you can still adjust the value seen downstream.
You will have to have 3 outputs on the function node.
1 for each of the lights.

So you would have your 3 sliders (each with their own topic) going into the function node.
Then the function node has 3 outputs to the 3 lights.

All scaling is done in the function node rather than with the slider.

This will be a bit confusing for the person moving the slider, but.....

You could add a 4'th output to a text node so once 3500 is exceeded, you get a message saying you can't have more power.

Try adding your limiting logic after the slider, but if you do limit it then feed the limited value back in the front of the slider so that it goes back to the previous position. Here is a simple flow showing that in operation, limiting the slider position in this case to a max of 5.

[{"id":"ff125e200cfb3a62","type":"ui-slider","z":"997da33a0beedade","group":"4f87bd59a15b847e","name":"","label":"slider","tooltip":"","order":0,"width":0,"height":0,"passthru":false,"outs":"all","topic":"topic","topicType":"msg","thumbLabel":true,"min":0,"max":10,"step":1,"className":"","x":190,"y":6480,"wires":[["4a29ebc3c29cc82b"]]},{"id":"4a29ebc3c29cc82b","type":"switch","z":"997da33a0beedade","name":"<=5?","property":"payload","propertyType":"msg","rules":[{"t":"lte","v":"5","vt":"num"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":360,"y":6480,"wires":[["1a2010a80a7f22e3"],["72ade609db1ee586"]]},{"id":"77c68758ee842fe4","type":"debug","z":"997da33a0beedade","name":"output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":840,"y":6520,"wires":[]},{"id":"72ade609db1ee586","type":"change","z":"997da33a0beedade","name":"msg.payload 5","rules":[{"t":"set","p":"payload","pt":"msg","to":"5","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":470,"y":6580,"wires":[["1a2010a80a7f22e3","ff125e200cfb3a62"]]},{"id":"29f6b5a26013be98","type":"rbe","z":"997da33a0beedade","name":"","func":"rbe","gap":"","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":700,"y":6520,"wires":[["77c68758ee842fe4"]]},{"id":"1a2010a80a7f22e3","type":"junction","z":"997da33a0beedade","x":610,"y":6520,"wires":[["29f6b5a26013be98"]]},{"id":"4f87bd59a15b847e","type":"ui-group","name":"test","page":"c6ff182a4185f2f2","width":"6","height":"1","order":1,"showTitle":false,"className":"","visible":"true","disabled":"false"},{"id":"c6ff182a4185f2f2","type":"ui-page","name":"Test page","ui":"ID-BASE-1","path":"/testpage","icon":"home","layout":"grid","theme":"f9b6670b127dc219","order":1,"className":"","visible":"true","disabled":"false"},{"id":"ID-BASE-1","type":"ui-base","name":"Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-control","ui-notification"]},{"id":"f9b6670b127dc219","type":"ui-theme","name":"FlowForge Theme","colors":{"surface":"#152a47","primary":"#005aff","bgPage":"#ffffff","groupBg":"#ffffff","groupOutline":"#cc3e3e"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]

This is a basic idea of what to do in the function node.

let LIGHT1 = {}
let LIGHT2 = {}
let LIGHT3 = {}
let message = {}
let L1 = context.get("light_1") || 0
let L2 = context.get("light_2") || 0
let L3 = context.get("light_3") || 0
const max_power = 3500
const range = 100
let available_power
let a = 0
let b = 0
let c = 0
let d = 0

if (msg.topic == "light_1")
{
       context.set("light_1",L1 + msg.payload)
}
if (msg.topic == "light_2") {
      context.set("light_2", L2 + msg.payload)
}
if (msg.topic == "light_3") {
      context.set("light_3", L3 + msg.payload)
}

let total_power = L1 + L2 + L3

if (total_power > 3500)
{
      //do something here
      message.payload = "Too much power"
      return {null,null,null,message}
}
else
{
      //
      available_power = max_power - total_power
      //    Map `rang` to the available_power.
      //a = (1500 / available_power)
      //    then send the newest message to it's output.
      switch (msg.topic)
      case ("light_1")
      {
            LIGHT1.payload = msg.payload
            return [LIGHT1,null,null,null]
      }
      case ("light_2")
      {
            LIGHT2.payload = msg.payload
            return [null,LIGHT2, null, null,]
      }
      case ("light_3")
      {
            LIGHT3.payload = msg.payload
            return [null,null,LIGHT31, null]
      }

}

This is NOT tested or complete.

You have to tweak the node to have 4 outputs.

The slider node will also NOT want to have the if message arrives, pass it to the output - yes?

It is not nice if the slider jumps away from the cursor during dragging but there's nothing to stop that behavior.
If to configure sliders to send output only on release, it may look and feel a bit better but the functionality may then suffer.

Anyway - something to play with

[{"id":"739b4c54cd61dba5","type":"ui-slider","z":"f7524a639caff2a3","group":"6f47b8907c5da63a","name":"","label":"S1","tooltip":"","order":2,"width":"6","height":"1","passthru":false,"outs":"all","topic":"S1","topicType":"str","thumbLabel":true,"min":0,"max":"1500","step":1,"className":"","x":570,"y":2180,"wires":[["6229d6d33703184d"]]},{"id":"c6c8de7eebfbf1a3","type":"ui-slider","z":"f7524a639caff2a3","group":"6f47b8907c5da63a","name":"","label":"S2","tooltip":"","order":3,"width":"6","height":"1","passthru":false,"outs":"all","topic":"S2","topicType":"str","thumbLabel":true,"min":0,"max":"1500","step":1,"className":"","x":570,"y":2240,"wires":[["6229d6d33703184d"]]},{"id":"25ad8cffdcdb9817","type":"ui-slider","z":"f7524a639caff2a3","group":"6f47b8907c5da63a","name":"","label":"S3","tooltip":"","order":4,"width":"6","height":"1","passthru":false,"outs":"all","topic":"S3","topicType":"str","thumbLabel":true,"min":0,"max":"1500","step":1,"className":"","x":570,"y":2300,"wires":[["6229d6d33703184d"]]},{"id":"6229d6d33703184d","type":"function","z":"f7524a639caff2a3","name":"Max Limiter","func":"let powerstate = global.get('powerstate') ?? {\n    \"S1\":0,\"S2\":0,\"S3\":0,\"T\":0\n}\nconst MAX = 3500\nconst SLIDERS = [\"S1\",\"S2\",\"S3\"]\n\nconst store = () => {\n    global.set('powerstate',powerstate)\n}\n\nconst getOthers = (current) => {\n    return SLIDERS.filter(s => s != current)\n}\n\nconst sumWithOthers  = (c,o) => {\n   let t = 0\n   o.forEach(function(s) {\n        t += powerstate[s]\n   });\n   t += c\n   return t\n} \n\n//clear limited slider red thumb\nnode.send([null, null, { topic: \"all\", class: \"\" }])\n\n\nlet currentSilder = msg.topic\nlet others = getOthers(currentSilder)\nlet potentionalTotal = sumWithOthers(msg.payload,others)\nif(potentionalTotal <= MAX){\n    powerstate[currentSilder] = msg.payload\n    powerstate.T = potentionalTotal\n    store()\n    //send total and current slider messages\n    node.send([{payload:powerstate.T},msg,null])    \n}\nelse{\n    //set limited slider back to valid position and show red thumb\n    node.send([null,null, {payload:powerstate[currentSilder],topic:currentSilder,class:\"limited\"}])\n}\n\n\n","outputs":3,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":750,"y":2240,"wires":[["dacfae04eeebdd2e"],["4fceb9f36afd020c"],["e1cb78f9b374d023"]],"outputLabels":["TOTAL","SLIDER","LIMIT"]},{"id":"4fceb9f36afd020c","type":"debug","z":"f7524a639caff2a3","name":"Current slider out","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":950,"y":2240,"wires":[]},{"id":"dacfae04eeebdd2e","type":"ui-text","z":"f7524a639caff2a3","group":"6f47b8907c5da63a","order":1,"width":0,"height":0,"name":"","label":"TOTAL","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"","fontSize":16,"color":"#717171","className":"","x":920,"y":2180,"wires":[]},{"id":"9f341c949f7d0373","type":"switch","z":"f7524a639caff2a3","name":"","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"S1","vt":"str"},{"t":"eq","v":"S2","vt":"str"},{"t":"eq","v":"S3","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":4,"x":370,"y":2240,"wires":[["739b4c54cd61dba5"],["c6c8de7eebfbf1a3"],["25ad8cffdcdb9817"],["25ad8cffdcdb9817","c6c8de7eebfbf1a3","739b4c54cd61dba5"]]},{"id":"e1cb78f9b374d023","type":"link out","z":"f7524a639caff2a3","name":"link out 1","mode":"link","links":["3a343c5b5ac0f0df"],"x":875,"y":2280,"wires":[]},{"id":"3a343c5b5ac0f0df","type":"link in","z":"f7524a639caff2a3","name":"link in 1","links":["e1cb78f9b374d023"],"x":245,"y":2260,"wires":[["9f341c949f7d0373"]]},{"id":"9f7c7f21600d0970","type":"ui-template","z":"f7524a639caff2a3","group":"","page":"","ui":"ae3d4aeb3f977a90","name":"","order":0,"width":0,"height":0,"head":"","format":".limited .v-slider-thumb__surface {\n    background-color: red;\n}","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"site:style","className":"","x":580,"y":2120,"wires":[[]]},{"id":"a09405e12a01403d","type":"ui-event","z":"f7524a639caff2a3","ui":"ae3d4aeb3f977a90","name":"","x":170,"y":2080,"wires":[["92d5632155ec844e"]]},{"id":"92d5632155ec844e","type":"switch","z":"f7524a639caff2a3","name":"pageview","property":"topic","propertyType":"msg","rules":[{"t":"cont","v":"pageview","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":180,"y":2120,"wires":[["18173e1b7dd24971"]]},{"id":"18173e1b7dd24971","type":"switch","z":"f7524a639caff2a3","name":"page name match","property":"payload.page.name","propertyType":"msg","rules":[{"t":"eq","v":"Examples","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":210,"y":2160,"wires":[["4a404233a5f5a459"]]},{"id":"4a404233a5f5a459","type":"function","z":"f7524a639caff2a3","name":"sliders state","func":"let powerstate = global.get('powerstate') ?? {\n    \"S1\": 0, \"S2\": 0, \"S3\": 0, \"T\": 0\n}\nObject.keys(powerstate).map(k => {\n    if(k != \"T\"){\n        node.send({ payload: powerstate[k], topic: k })\n    }\n    \n})\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":190,"y":2200,"wires":[["9f341c949f7d0373"]]},{"id":"6f47b8907c5da63a","type":"ui-group","name":"Sliders","page":"19eb6d108e9275e2","width":"6","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"ae3d4aeb3f977a90","type":"ui-base","name":"Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"navigationStyle":"icon","titleBarStyle":"default"},{"id":"19eb6d108e9275e2","type":"ui-page","name":"Examples","ui":"ae3d4aeb3f977a90","path":"/examples","icon":"","layout":"grid","theme":"a965ccfef139317a","order":3,"className":"","visible":true,"disabled":"false"},{"id":"a965ccfef139317a","type":"ui-theme","name":"Default","colors":{"surface":"#ffffff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]

Correct .

This seems to be a good approach, but the usability is difficult because the status of the slider is not visible, it passes a value but always jumps back to position 0. Perhaps there are other useful alternative(s) to sliders

It doesn't for me. It jumps back to last valid value. Not 0.

Unfortunately, the slider does not allow you to dynamically change the 'min' or 'max' settings. If it did that would make your quest much easier because each time a slider sent as msg, you could calculate the remaining maximum and send that to each slider to reset the maximum value.

I wonder if the node could be changed to allow that...

Are you using dashboard 1 or 2?

Im using dashboard 2, but thats not mandatory, could switch to 1

It's not difficult if a slider is set to a value that takes the total above 3500 to reset that slider to a lower value.

image

So if slider 2 and 3 are set to 100% and you try and change slider 1 it will reset itself to maximum 33.33% (233.33 * 15 = 3499.95)
You would have to give an explanation of why the slider was refusing to go past 33% though.

[{"id":"6b6e077bdea867a7","type":"ui-slider","z":"b2ace107dbff8c0d","group":"192c891a3de910f5","name":"slider1","label":"slider","tooltip":"","order":1,"width":0,"height":0,"passthru":false,"outs":"end","topic":"topic","topicType":"msg","thumbLabel":true,"min":0,"max":"100","step":"1","className":"","x":90,"y":60,"wires":[["98d557637fd70ba6"]]},{"id":"60d4eb38385f3c84","type":"ui-slider","z":"b2ace107dbff8c0d","group":"192c891a3de910f5","name":"","label":"slider2","tooltip":"","order":2,"width":0,"height":0,"passthru":false,"outs":"end","topic":"topic","topicType":"msg","thumbLabel":true,"min":0,"max":"100","step":"1","className":"","x":90,"y":120,"wires":[["915d05c56287193c"]]},{"id":"7083c3097e176227","type":"ui-slider","z":"b2ace107dbff8c0d","group":"192c891a3de910f5","name":"","label":"slider3","tooltip":"","order":3,"width":0,"height":0,"passthru":false,"outs":"end","topic":"topic","topicType":"msg","thumbLabel":true,"min":0,"max":"100","step":"1","className":"","x":90,"y":180,"wires":[["a5eba635bce3fa0b"]]},{"id":"98d557637fd70ba6","type":"change","z":"b2ace107dbff8c0d","name":"","rules":[{"t":"set","p":"slider1","pt":"flow","to":"payload * 15","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":260,"y":60,"wires":[["9ba63e98b961089c"]]},{"id":"915d05c56287193c","type":"change","z":"b2ace107dbff8c0d","name":"","rules":[{"t":"set","p":"slider2","pt":"flow","to":"payload * 15","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":260,"y":120,"wires":[["774966c506685a9a"]]},{"id":"a5eba635bce3fa0b","type":"change","z":"b2ace107dbff8c0d","name":"","rules":[{"t":"set","p":"slider3","pt":"flow","to":"msg.payload * 15","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":260,"y":180,"wires":[["b56b22c9e0ba8196"]]},{"id":"9ba63e98b961089c","type":"function","z":"b2ace107dbff8c0d","name":"Reset if too high","func":"const slider1 = flow.get(\"slider1\") || 0\nconst slider2 = flow.get(\"slider2\") || 0\nconst slider3 = flow.get(\"slider3\") || 0\nconst total = slider1 + slider2 + slider3\nnode.warn (total)\nif (total > 3500) {\n    msg.payload = (3500 - slider2 - slider3)/15\n    node.warn (msg.payload)\n    msg.topic = \"reset\"\n}\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":460,"y":60,"wires":[["4095514a84b40ebd"]]},{"id":"4095514a84b40ebd","type":"switch","z":"b2ace107dbff8c0d","name":"","property":"topic","propertyType":"msg","rules":[{"t":"neq","v":"reset","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":610,"y":60,"wires":[["5a69a32654fefbcb"],["6b6e077bdea867a7"]]},{"id":"5a69a32654fefbcb","type":"debug","z":"b2ace107dbff8c0d","name":"debug 417","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":810,"y":120,"wires":[]},{"id":"774966c506685a9a","type":"function","z":"b2ace107dbff8c0d","name":"Reset if too high","func":"const slider1 = flow.get(\"slider1\") || 0\nconst slider2 = flow.get(\"slider2\") || 0\nconst slider3 = flow.get(\"slider3\") || 0\nconst total = slider1 + slider2 + slider3\nnode.warn (total)\nif (total > 3500) {\n    msg.payload = (3500 - slider1 - slider3)/15\n    node.warn (msg.payload)\n    msg.topic = \"reset\"\n}\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":460,"y":120,"wires":[["32d3b52f214d2ad0"]]},{"id":"32d3b52f214d2ad0","type":"switch","z":"b2ace107dbff8c0d","name":"","property":"topic","propertyType":"msg","rules":[{"t":"neq","v":"reset","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":610,"y":120,"wires":[["5a69a32654fefbcb"],["60d4eb38385f3c84"]]},{"id":"b56b22c9e0ba8196","type":"function","z":"b2ace107dbff8c0d","name":"Reset if too high","func":"const slider1 = flow.get(\"slider1\") || 0\nconst slider2 = flow.get(\"slider2\") || 0\nconst slider3 = flow.get(\"slider3\") || 0\nconst total = slider1 + slider2 + slider3\nnode.warn (total)\nif (total > 3500) {\n    msg.payload = (3500 - slider1 - slider2)/15\n    node.warn (msg.payload)\n    msg.topic = \"reset\"\n}\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":460,"y":180,"wires":[["7a5830a425048fa8"]]},{"id":"7a5830a425048fa8","type":"switch","z":"b2ace107dbff8c0d","name":"","property":"topic","propertyType":"msg","rules":[{"t":"neq","v":"reset","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":610,"y":180,"wires":[["5a69a32654fefbcb"],["7083c3097e176227"]]},{"id":"192c891a3de910f5","type":"ui-group","name":"Tasks","page":"11a250b56df8dffa","width":"3","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"11a250b56df8dffa","type":"ui-page","name":"Page 1","ui":"ac5e535515ebb9c6","path":"/page1","icon":"home","layout":"grid","theme":"f36403a3012e6880","order":1,"className":"","visible":"true","disabled":"false"},{"id":"ac5e535515ebb9c6","type":"ui-base","name":"My Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"navigationStyle":"default"},{"id":"f36403a3012e6880","type":"ui-theme","name":"Default Theme","colors":{"surface":"#ffffff","primary":"#0094CE","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]

An alternative approach, probably more user friendly, would be, if the total exceeds 3500, to multiply all 3 values by 3500/totalValue

this looks for a good solution! Thanks!

i was playing around but its not working stable, dont know why. There are some plausible constellations but there are also inplausible constellations were the percentage doesnt fit with the node.warn value and respective not with the slider1-3 variable values (e.g. sometimes its only possible to move slider 1 to a low value like 40, 2 to 40 and 3 also, cant go higher)

Add debug nodes to the flow and work out where it is going wrong.

I believe you have to pass incoming messages through to the output, in contrast to what other folks have suggested.

It is possible to set the max via a msg.ui_update:

So, when your computing that youve reached capacity, you could read your values from your context store, and then set the maximum values to match