Home heating time remaining

Hello
For my home heating I use a dry wood boiler and a buffer tank. I need a function to calculate how much time of heating I have left when the boiler goes out.
Im measuring the water temperature in the buffer and converting it to percentage. So 100% is buffer full and at 0% my circulation pump goes off. It usually takes around 12h to go from 100 to 0%. The percentage value gets updated every 10 seconds.

You may need to clarify the maths used.

Though I don't get what you have, if you express it as a problem (an old term used when talking to children to create a scenario that needs solving) it shouldn't be hard to resolve.

So, there is an input:
boiler has gone out

I don't get the next part:

Temperature and buffer full are not related.

Anyway, so my take is something like this:

boiler off --> Enable timer loop.

Timer loop --> (every 10 seconds?) --> Read temperature. (Don't know why)

Get temperature --> check if it is a value --> act on it if it is needing.

But you will have to put more details in that.

Based on the percentage drop over a period of time (let's say 5 min) calculate the remaining time until percentage drops to 0

Sorry.

Reading what you say I think you have an idea of what you want to do, but still don't really understand it.

Try to write it so it is a program.
Similar to what I did in my first reply.

I am still not getting what you want to do.

If "it usually takes 12 hours to go from 100% to 0%" I would detect the first event (boiler off) and wait 10 hours.
Then do something.

But all this is speculative as I still don't know what needs to be done.

Hope that helps you in some way.

This example could be one way of predicting the remaining heating time

Assuming the following:

  • input is percentage values of remaining "fuel" as integer numbers (100 - 0)
  • input 100 means fully refueled

I decided to write the logic in javascript in a function node and it works like this:

To start, assume we are fully refueled. When the first message arrives at the input, we are saving the value (fuelLeft) and time as references. Next we will wait for a message with a changed value

When this happens, we record the new value and time, we do the calculation of the "fuel" consumed during the time elapsed since the first reading. Then we know how much "fuel" we have consumed during that time period and we can make a forecast of expected time the remaining "fuel" may last. Finally we update our references with the actual values (time and remaining "fuel") to be ready for the next calculation when it happens

In the example below I have created a simulator that can be tried in the following way:

  • click in the upper inject node "100" will send a value of 100 (fully "fueled") to the function node
  • the function node will then wait for the next value (99) that will be sent after 60 seconds from the trigger node
  • the calculation will execute and you should see in the debug a message saying "Time remaining: 99 min"

Consuming 1% in one minute gives a total heating time of 100 minutes. In reality, when you connect the function node to your real signal, you will of course see "a more realistic forecast"

The other test buttons can be used for simulations to set the values of remaining fuel and you can also "manually" check the calculation using the values seen in the debug window using the formula "fuel left" * "time elapsed (ms)" / 60000

Please note that the remaining heating time always will be re-calculated every time a changed value is incoming. I thought about this and I think it is realistic, you could imagine weather temperature changes during a day that will require more or less heating depending on the outdoor temperature and moisture

If you "fuel up" and the incoming value is 100 again, everything should start from scratch

[{"id":"81ab88ac.aea668","type":"function","z":"60f50465.20a53c","name":"","func":"var nTime = new Date().getTime();\nvar nValue = msg.payload;\n\ncontext.set('refTime', context.get('refTime')||nTime);\ncontext.set('fuelLeft', context.get('fuelLeft')||100);\n\nvar refTime = context.get('refTime');\nvar fuelLeft = context.get('fuelLeft');\n\nif (nValue > fuelLeft || nValue === 100){\n    //we have refueled, update the references\n    context.set('refTime', nTime);\n    context.set('fuelLeft', nValue);\n}\n\nif (nValue < fuelLeft){\n    //value has changed\n    var change =  fuelLeft - nValue; //consumed fuel in percentage\n    fuelLeft = fuelLeft - change;\n    var timeLapse = nTime - refTime;\n    node.warn(\"fuel left:\"+fuelLeft);\n    node.warn(\"time elapsed (ms): \"+timeLapse);\n    var timeLeft = fuelLeft * (timeLapse/(1000*60));\n    timeLeft = Number((timeLeft).toFixed(1));\n    msg.payload = \"Time remaining: \"+timeLeft.toString()+\" min\";\n    node.send(msg);\n\n    //set new references\n    context.set('refTime', nTime);\n    context.set('fuelLeft', fuelLeft);\n    \n}\n\n\n\n","outputs":1,"noerr":0,"x":670,"y":1550,"wires":[["58c640ad.508a8"]]},{"id":"c6544976.c1d168","type":"inject","z":"60f50465.20a53c","name":"","topic":"","payload":"100","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":390,"y":1550,"wires":[["81ab88ac.aea668","1f0dcfe8.a504b"]]},{"id":"caf2531a.46151","type":"inject","z":"60f50465.20a53c","name":"","topic":"","payload":"99","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":390,"y":1770,"wires":[["81ab88ac.aea668"]]},{"id":"633af1f9.817cb","type":"inject","z":"60f50465.20a53c","name":"","topic":"","payload":"98","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":390,"y":1830,"wires":[["81ab88ac.aea668"]]},{"id":"40fff7d3.2d0398","type":"inject","z":"60f50465.20a53c","name":"","topic":"","payload":"97","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":390,"y":1890,"wires":[["81ab88ac.aea668"]]},{"id":"58c640ad.508a8","type":"debug","z":"60f50465.20a53c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":870,"y":1550,"wires":[]},{"id":"1f0dcfe8.a504b","type":"trigger","z":"60f50465.20a53c","op1":"","op2":"99","op1type":"nul","op2type":"num","duration":"60","extend":false,"units":"s","reset":"","bytopic":"all","name":"","x":390,"y":1610,"wires":[["81ab88ac.aea668"]]},{"id":"5a13b193.5cf43","type":"inject","z":"60f50465.20a53c","name":"","topic":"","payload":"100","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":390,"y":1710,"wires":[["81ab88ac.aea668"]]},{"id":"97a1c5a6.38a0d8","type":"inject","z":"60f50465.20a53c","name":"","topic":"","payload":"0","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":390,"y":2060,"wires":[["81ab88ac.aea668"]]},{"id":"fd4b7dcc.d2bb3","type":"inject","z":"60f50465.20a53c","name":"","topic":"","payload":"1","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":390,"y":2000,"wires":[["81ab88ac.aea668"]]}]

This is exactly what I wanted, I Tested the function and it occasionally updates the time even thoth the % value stayed the same.

OK, great to hear!

However, I do suspect the calculation of the estimated time left will be incorrect if we receive the same value more than once, I did expect that values where changed when a new arrived in the msg

So we need to block repeated values. One way could eventually be to use the RBE node, i.e add a RBE node in front of the function node. But I do not know how the complete msg look like so it might not help. A safer way could therefore be to filter in the function node code

Take this code and replace it in the function node

var nValue = msg.payload;
context.set('fuelLeft', context.get('fuelLeft')||100);
var fuelLeft = context.get('fuelLeft');

function timeConvert(n) {
    var num = n;
    var hours = (num / 60);
    var rhours = Math.floor(hours);
    var minutes = (hours - rhours) * 60;
    var rminutes = Math.round(minutes);
    return rhours + " hour(s) and " + rminutes + " minute(s)";
}

if(nValue != context.get('fuelLeft') || nValue === 100){
    var nTime = new Date().getTime();
    context.set('refTime', context.get('refTime')||nTime);
    var refTime = context.get('refTime');

    if (nValue >= 100){
        //we have refueled, update the references
        if(nValue === 100){
            context.set('refTime', nTime);
        }
        if(nValue > 100){ // just a ripple???
            nValue = 100;
        }
        context.set('fuelLeft', nValue);
    }
    
    if (nValue < fuelLeft){
        //value has changed
        var change =  fuelLeft - nValue; //consumed fuel in percentage
        fuelLeft = fuelLeft - change;
        var timeLapse = nTime - refTime;
        node.warn("fuel left:"+fuelLeft);
        node.warn("time elapsed (ms): "+timeLapse);
        var timeLeft = fuelLeft * (timeLapse/(1000*60));
        timeLeft = Number((timeLeft).toFixed(1));
        timeLeft = timeConvert(timeLeft);
        msg.payload = "Time remaining: "+timeLeft.toString();
        node.send(msg);
    
        //set new references
        context.set('refTime', nTime);
        context.set('fuelLeft', fuelLeft);
    }
}



Next, could you also enable the debug to show "Time remaining" and paste some examples here so I can do some calculation checks?

Here is the debug log with the new code in the function node,

12/21/2019, 8:01:05 PMnode: Time Left
function : (warn)
"fuel left:90"
12/21/2019, 8:01:05 PMnode: Time Left
function : (warn)
"time elapsed (ms): 3845"
12/21/2019, 8:01:05 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[7]
"5.8 min"
12/21/2019, 8:02:06 PMnode: Time Left
function : (warn)
"fuel left:89"
12/21/2019, 8:02:06 PMnode: Time Left
function : (warn)
"time elapsed (ms): 60383"
12/21/2019, 8:02:06 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[8]
"89.6 min"
12/21/2019, 8:16:32 PMnode: Time Left
function : (warn)
"fuel left:88"
12/21/2019, 8:16:32 PMnode: Time Left
function : (warn)
"time elapsed (ms): 866140"
12/21/2019, 8:16:32 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[10]
"1270.3 min"
12/21/2019, 8:25:46 PMnode: Time Left
function : (warn)
"fuel left:87"
12/21/2019, 8:25:46 PMnode: Time Left
function : (warn)
"time elapsed (ms): 553900"
12/21/2019, 8:25:46 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[9]
"803.2 min"
12/21/2019, 8:26:16 PMnode: Time Left
function : (warn)
"fuel left:87"
12/21/2019, 8:26:16 PMnode: Time Left
function : (warn)
"time elapsed (ms): 10025"
12/21/2019, 8:26:16 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[8]
"14.5 min"
12/21/2019, 8:37:41 PMnode: Time Left
function : (warn)
"fuel left:86"
12/21/2019, 8:37:41 PMnode: Time Left
function : (warn)
"time elapsed (ms): 684861"
12/21/2019, 8:37:41 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[9]
"981.6 min"
12/21/2019, 8:38:11 PMnode: Time Left
function : (warn)
"fuel left:86"
12/21/2019, 8:38:11 PMnode: Time Left
function : (warn)
"time elapsed (ms): 10116"
12/21/2019, 8:38:11 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[8]
"14.5 min"

The payload being passed to the function is a number value 0 to 100.
I would be also good to convert the minutes to Hours and minutes.

I will add the RBE node and report.

Here is the graph of one day of heating in %:


Thanks

First, I made "a bug" in the code, it was not filtering correctly, I am currently updating the code above. You should not need the RBE node. Will also add function to convert time into hours and minutes

I have some questions related to your graph, to understand what is happening

  • I see that you have some "ripple", value is sometimes more than 100 etc. Is this true? Can it happen that values received can be > 100 (I added code so if value is > 100 it is set = 100 and as example I ignore if value changes from lets say 87 to 88 and then back to 87) ?

  • When the value increases, is this all the time because you did refuel

EDIT: updated the code above, please use instead. I have also tried to compensate for the "ripple"

I inserted the new code and removed the RBE. I will post the result.

I use a wood burning boiler, when I fill it up with wood it starts charging then buffer until it hits a set temperature and then stops burning, sometime there is still unburned wood left so when the temperature drops it starts burning again until it hits the set point again or is out of wood.
I'm using the rage function to convert temp to % so some time the temp goes over the set one, that's when the percentage goes over 100%.
Sometimes there is not enough wood to recharge the buffer to 100% so it should do the calculation even if the 100% point is not reached.
Rather it could say charging if it see the percentage has gone up a few percent. Only if the percentage starts dropping should do the calculation. the ripple of 1% can be ignored.

My boiler:

Right, I think now I understand better how it works :wink:

I have changed the code again, sorry, maybe this will be more accurate and simpler:

  • every time the received value is higher than previous saved value, it is considered as "fueling up" (like if value goes from 95 to 96 etc), the value is updating "fuel left" reference but no further calculation is made
  • every time value decreases compared to previous like going from 95 to 94 etc, it is considered that fuel is consumed. The received value and current time is updating the references and calculation is executed
var nValue = msg.payload;
context.set('fuelLeft', context.get('fuelLeft')||nValue);
var fuelLeft = context.get('fuelLeft');

function timeConvert(n) {
    var num = n;
    var hours = (num / 60);
    var rhours = Math.floor(hours);
    var minutes = (hours - rhours) * 60;
    var rminutes = Math.round(minutes);
    return rhours + " hour(s) and " + rminutes + " minute(s)";
}

if(nValue != context.get('fuelLeft')){
    var nTime = new Date().getTime();
    context.set('refTime', context.get('refTime')||nTime);
    var refTime = context.get('refTime');

    if (nValue > fuelLeft){
        //we have refueled, update the references
        context.set('fuelLeft', nValue);
    }
    
    if (nValue < fuelLeft){
        //value has changed, fuel consumed
        var change =  fuelLeft - nValue; //consumed fuel in percentage
        fuelLeft = fuelLeft - change;
        var timeLapse = nTime - refTime;
        node.warn("fuel left:"+fuelLeft);
        node.warn("time elapsed (ms): "+timeLapse);
        var timeLeft = fuelLeft * (timeLapse/(1000*60));
        timeLeft = Number((timeLeft).toFixed(1));
        timeLeft = timeConvert(timeLeft);
        msg.payload = "Time remaining: "+timeLeft.toString();
        node.send(msg);
    
        //set new references
        context.set('refTime', nTime);
        context.set('fuelLeft', fuelLeft);
    }
}




This is the debug log with the new code:
It seems it receives same value a few times.
I'll add RBE again and see how it works

12/23/2019, 1:49:47 PMnode: Time Left
function : (warn)
"fuel left:37"
12/23/2019, 1:49:47 PMnode: Time Left
function : (warn)
"time elapsed (ms): 211485"
12/23/2019, 1:49:47 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"2 hour(s) and 10 minute(s)"
12/23/2019, 1:55:39 PMnode: Time Left
function : (warn)
"fuel left:36"
12/23/2019, 1:55:39 PMnode: Time Left
function : (warn)
"time elapsed (ms): 352454"
12/23/2019, 1:55:39 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"3 hour(s) and 31 minute(s)"
12/23/2019, 2:04:43 PMnode: Time Left
function : (warn)
"fuel left:35"
12/23/2019, 2:04:43 PMnode: Time Left
function : (warn)
"time elapsed (ms): 543834"
12/23/2019, 2:04:43 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"5 hour(s) and 17 minute(s)"
12/23/2019, 2:09:15 PMnode: Time Left
function : (warn)
"fuel left:34"
12/23/2019, 2:09:15 PMnode: Time Left
function : (warn)
"time elapsed (ms): 271894"
12/23/2019, 2:09:15 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"2 hour(s) and 34 minute(s)"
12/23/2019, 2:16:58 PMnode: Time Left
function : (warn)
"fuel left:33"
12/23/2019, 2:16:58 PMnode: Time Left
function : (warn)
"time elapsed (ms): 463197"
12/23/2019, 2:16:58 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"4 hour(s) and 15 minute(s)"
12/23/2019, 2:17:18 PMnode: Time Left
function : (warn)
"fuel left:33"
12/23/2019, 2:17:18 PMnode: Time Left
function : (warn)
"time elapsed (ms): 20190"
12/23/2019, 2:17:18 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"0 hour(s) and 11 minute(s)"
12/23/2019, 2:23:41 PMnode: Time Left
function : (warn)
"fuel left:32"
12/23/2019, 2:23:41 PMnode: Time Left
function : (warn)
"time elapsed (ms): 382666"
12/23/2019, 2:23:41 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"3 hour(s) and 24 minute(s)"
12/23/2019, 2:30:44 PMnode: Time Left
function : (warn)
"fuel left:31"
12/23/2019, 2:30:44 PMnode: Time Left
function : (warn)
"time elapsed (ms): 422961"
12/23/2019, 2:30:44 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"3 hour(s) and 39 minute(s)"
12/23/2019, 2:40:28 PMnode: Time Left
function : (warn)
"fuel left:30"
12/23/2019, 2:40:28 PMnode: Time Left
function : (warn)
"time elapsed (ms): 584076"
12/23/2019, 2:40:28 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"4 hour(s) and 52 minute(s)"
12/23/2019, 2:47:41 PMnode: Time Left
function : (warn)
"fuel left:29"
12/23/2019, 2:47:41 PMnode: Time Left
function : (warn)
"time elapsed (ms): 432982"
12/23/2019, 2:47:41 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"3 hour(s) and 29 minute(s)"
12/23/2019, 2:56:35 PMnode: Time Left
function : (warn)
"fuel left:28"
12/23/2019, 2:56:35 PMnode: Time Left
function : (warn)
"time elapsed (ms): 533791"
12/23/2019, 2:56:35 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[25]
"4 hour(s) and 9 minute(s)"
12/23/2019, 2:56:55 PMnode: Time Left
function : (warn)
"fuel left:28"
12/23/2019, 2:56:55 PMnode: Time Left
function : (warn)
"time elapsed (ms): 20129"
12/23/2019, 2:56:55 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[25]
"0 hour(s) and 9 minute(s)"
12/23/2019, 3:03:38 PMnode: Time Left
function : (warn)
"fuel left:27"
12/23/2019, 3:03:38 PMnode: Time Left
function : (warn)
"time elapsed (ms): 402808"
12/23/2019, 3:03:38 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[25]
"3 hour(s) and 1 minute(s)"
12/23/2019, 3:10:41 PMnode: Time Left
function : (warn)
"fuel left:26"
12/23/2019, 3:10:41 PMnode: Time Left
function : (warn)
"time elapsed (ms): 422980"
12/23/2019, 3:10:41 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[25]
"3 hour(s) and 3 minute(s)"
12/23/2019, 3:18:14 PMnode: Time Left
function : (warn)
"fuel left:25"
12/23/2019, 3:18:14 PMnode: Time Left
function : (warn)
"time elapsed (ms): 453153"
12/23/2019, 3:18:14 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[25]
"3 hour(s) and 9 minute(s)"
12/23/2019, 3:24:47 PMnode: Time Left
function : (warn)
"fuel left:24"
12/23/2019, 3:24:47 PMnode: Time Left
function : (warn)
"time elapsed (ms): 392740"
12/23/2019, 3:24:47 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"2 hour(s) and 37 minute(s)"
12/23/2019, 3:32:20 PMnode: Time Left
function : (warn)
"fuel left:23"
12/23/2019, 3:32:20 PMnode: Time Left
function : (warn)
"time elapsed (ms): 453174"
12/23/2019, 3:32:20 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"2 hour(s) and 54 minute(s)"
12/23/2019, 3:40:03 PMnode: Time Left
function : (warn)
"fuel left:22"
12/23/2019, 3:40:03 PMnode: Time Left
function : (warn)
"time elapsed (ms): 463189"
12/23/2019, 3:40:03 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"2 hour(s) and 50 minute(s)"
12/23/2019, 3:47:26 PMnode: Time Left
function : (warn)
"fuel left:21"
12/23/2019, 3:47:26 PMnode: Time Left
function : (warn)
"time elapsed (ms): 443161"
12/23/2019, 3:47:26 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"2 hour(s) and 35 minute(s)"
12/23/2019, 3:56:30 PMnode: Time Left
function : (warn)
"fuel left:20"
12/23/2019, 3:56:30 PMnode: Time Left
function : (warn)
"time elapsed (ms): 543745"
12/23/2019, 3:56:30 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[25]
"3 hour(s) and 1 minute(s)"
12/23/2019, 4:04:23 PMnode: Time Left
function : (warn)
"fuel left:19"
12/23/2019, 4:04:23 PMnode: Time Left
function : (warn)
"time elapsed (ms): 473370"
12/23/2019, 4:04:23 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"2 hour(s) and 30 minute(s)"
12/23/2019, 4:13:37 PMnode: Time Left
function : (warn)
"fuel left:18"
12/23/2019, 4:13:37 PMnode: Time Left
function : (warn)
"time elapsed (ms): 553869"
12/23/2019, 4:13:37 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"2 hour(s) and 46 minute(s)"
12/23/2019, 4:21:30 PMnode: Time Left
function : (warn)
"fuel left:17"
12/23/2019, 4:21:30 PMnode: Time Left
function : (warn)
"time elapsed (ms): 473312"
12/23/2019, 4:21:30 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"2 hour(s) and 14 minute(s)"
12/23/2019, 4:29:04 PMnode: Time Left
function : (warn)
"fuel left:16"
12/23/2019, 4:29:04 PMnode: Time Left
function : (warn)
"time elapsed (ms): 453171"
12/23/2019, 4:29:04 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[25]
"2 hour(s) and 1 minute(s)"
12/23/2019, 4:38:58 PMnode: Time Left
function : (warn)
"fuel left:15"
12/23/2019, 4:38:58 PMnode: Time Left
function : (warn)
"time elapsed (ms): 594100"
12/23/2019, 4:38:58 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"2 hour(s) and 29 minute(s)"
12/23/2019, 4:46:41 PMnode: Time Left
function : (warn)
"fuel left:14"
12/23/2019, 4:46:41 PMnode: Time Left
function : (warn)
"time elapsed (ms): 463270"
12/23/2019, 4:46:41 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"1 hour(s) and 48 minute(s)"
12/23/2019, 4:57:56 PMnode: Time Left
function : (warn)
"fuel left:13"
12/23/2019, 4:57:56 PMnode: Time Left
function : (warn)
"time elapsed (ms): 674721"
12/23/2019, 4:57:56 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"2 hour(s) and 26 minute(s)"
12/23/2019, 5:04:38 PMnode: Time Left
function : (warn)
"fuel left:12"
12/23/2019, 5:04:38 PMnode: Time Left
function : (warn)
"time elapsed (ms): 402760"
12/23/2019, 5:04:39 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"1 hour(s) and 21 minute(s)"
12/23/2019, 5:14:23 PMnode: Time Left
function : (warn)
"fuel left:11"
12/23/2019, 5:14:23 PMnode: Time Left
function : (warn)
"time elapsed (ms): 584142"
12/23/2019, 5:14:23 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"1 hour(s) and 47 minute(s)"
12/23/2019, 5:38:03 PMnode: Time Left
function : (warn)
"fuel left:10"
12/23/2019, 5:38:03 PMnode: Time Left
function : (warn)
"time elapsed (ms): 1419939"
12/23/2019, 5:38:03 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"3 hour(s) and 57 minute(s)"
12/23/2019, 5:38:43 PMnode: Time Left
function : (warn)
"fuel left:10"
12/23/2019, 5:38:43 PMnode: Time Left
function : (warn)
"time elapsed (ms): 40267"
12/23/2019, 5:38:43 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[25]
"0 hour(s) and 7 minute(s)"
12/23/2019, 5:39:43 PMnode: Time Left
function : (warn)
"fuel left:10"
12/23/2019, 5:39:43 PMnode: Time Left
function : (warn)
"time elapsed (ms): 60440"
12/23/2019, 5:39:43 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"0 hour(s) and 10 minute(s)"
12/23/2019, 5:43:05 PMnode: Time Left
function : (warn)
"fuel left:10"
12/23/2019, 5:43:05 PMnode: Time Left
function : (warn)
"time elapsed (ms): 201407"
12/23/2019, 5:43:05 PMnode: d07fe37f.9767f
Bafer Dno : msg.payload : string[26]
"0 hour(s) and 34 minute(s)"

Can you try this code below then. I think I have to set the reference time evry time a refuel happens, otherwise the first prediction will be wrong

Also just asking you if this is the best way to predict? I mean what happens now is that for each time the value goes down a new prediction will be calculated based on the previous reading and we are expecting the same burn rate forward. Why I ask is because I was thinking of making some average calculation of several readings to "smooth" peak consumptions. In reality, could it be that you have some higher peaks and after a short while it drops down? For instance, let's say you have used a lot of hot water, the burner works hard, the prediction says the fuel will not last long. After a while the burner works less hard and the fuel prediction will tell you that it will last longer? Could it be worth trying?

var nValue = msg.payload;
var nTime = new Date().getTime();
context.set('fuelLeft', context.get('fuelLeft')||nValue);
context.set('refTime', context.get('refTime') || nTime);
context.set('pValue', context.get('pValue')||0);
var fuelLeft = context.get('fuelLeft');

function timeConvert(n) {
    var num = n;
    var hours = (num / 60);
    var rhours = Math.floor(hours);
    var minutes = (hours - rhours) * 60;
    var rminutes = Math.round(minutes);
    return rhours + " hour(s) and " + rminutes + " minute(s)";
}

if(nValue != context.get('fuelLeft')){

    if (nValue > fuelLeft){
        //we have refueled, update the references
        context.set('refTime', nTime);
        context.set('fuelLeft', nValue);
    }

    if (nValue < fuelLeft && nValue != context.get('pValue')){
        //value has changed, fuel consumed
        var refTime = context.get('refTime');
        var change =  fuelLeft - nValue; //consumed fuel in percentage
        fuelLeft = fuelLeft - change;
        var timeLapse = nTime - refTime;
        node.warn("fuel left:"+fuelLeft);
        node.warn("time elapsed (ms): "+timeLapse);
        var timeLeft = fuelLeft * (timeLapse/(1000*60));
        timeLeft = Number((timeLeft).toFixed(1));
        timeLeft = timeConvert(timeLeft);
        msg.payload = "Time remaining: "+timeLeft.toString();
        node.send(msg);
    
        //set new references
        context.set('fuelLeft', nValue);
        context.set('refTime', nTime);
        context.set('pValue', nValue);
    }
}




1 Like

I could not resist, this is a solution where I am smoothing the predicted time left as discussed in previous post. I believe this will be better since it will predict ...smoother... to filter out "false" predictions that could otherwise happen (example of that is if the value goes up and back down several times during a relative short time period)

[{"id":"1e3a3f30.890b51","type":"function","z":"1abba8e0.c29407","name":"","func":"return msg","outputs":1,"noerr":0,"x":590,"y":90,"wires":[["2e0a7d3.9916782"]]},{"id":"f37c95ff.8bce48","type":"inject","z":"1abba8e0.c29407","name":"","topic":"","payload":"100","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":90,"wires":[["1e3a3f30.890b51","c6da39f9.d2a3d8"]]},{"id":"a4a59a94.21e818","type":"inject","z":"1abba8e0.c29407","name":"","topic":"","payload":"99","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":370,"wires":[["1e3a3f30.890b51"]]},{"id":"5cc15e16.d4341","type":"inject","z":"1abba8e0.c29407","name":"","topic":"","payload":"98","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":430,"wires":[["1e3a3f30.890b51"]]},{"id":"6bcb8727.f2b378","type":"inject","z":"1abba8e0.c29407","name":"","topic":"","payload":"97","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":490,"wires":[["1e3a3f30.890b51"]]},{"id":"c6da39f9.d2a3d8","type":"trigger","z":"1abba8e0.c29407","op1":"","op2":"99","op1type":"nul","op2type":"num","duration":"10","extend":false,"units":"s","reset":"","bytopic":"all","name":"","x":150,"y":150,"wires":[["1e3a3f30.890b51"]]},{"id":"3ae3a95c.8eac06","type":"inject","z":"1abba8e0.c29407","name":"","topic":"","payload":"100","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":310,"wires":[["1e3a3f30.890b51"]]},{"id":"5ab38f4d.ba106","type":"inject","z":"1abba8e0.c29407","name":"","topic":"","payload":"0","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":730,"wires":[["1e3a3f30.890b51"]]},{"id":"9f91cbff.e429d8","type":"inject","z":"1abba8e0.c29407","name":"","topic":"","payload":"1","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":670,"wires":[["1e3a3f30.890b51"]]},{"id":"e1926249.7ef63","type":"comment","z":"1abba8e0.c29407","name":"Heating System - fuel consumption prediction","info":"","x":250,"y":40,"wires":[]},{"id":"1f28eb0.87ec215","type":"inject","z":"1abba8e0.c29407","name":"","topic":"","payload":"96","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":550,"wires":[["1e3a3f30.890b51"]]},{"id":"b7f87df8.adf4a","type":"inject","z":"1abba8e0.c29407","name":"","topic":"","payload":"101","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":250,"wires":[["1e3a3f30.890b51"]]},{"id":"da5f0d45.4be11","type":"inject","z":"1abba8e0.c29407","name":"","topic":"","payload":"95","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":610,"wires":[["1e3a3f30.890b51"]]},{"id":"2e0a7d3.9916782","type":"function","z":"1abba8e0.c29407","name":"","func":"var nValue = msg.payload;\nvar nTime = new Date().getTime();\ncontext.set('fuelLeft', context.get('fuelLeft')||nValue);\ncontext.set('refTime', context.get('refTime') || nTime);\ncontext.set('pValue', context.get('pValue')||0);\nvar fuelLeft = context.get('fuelLeft');\n\nif(nValue != context.get('fuelLeft')){\n\n    if (nValue > fuelLeft){\n        //we have refueled, update the references\n        context.set('refTime', nTime);\n        context.set('fuelLeft', nValue);\n    }\n\n    if (nValue < fuelLeft && nValue != context.get('pValue')){\n        //value has changed, fuel consumed\n        var refTime = context.get('refTime');\n        var change =  fuelLeft - nValue; //consumed fuel in percentage\n        fuelLeft = fuelLeft - change;\n        var timeLapse = nTime - refTime;\n        node.warn(\"fuel left:\"+fuelLeft);\n        node.warn(\"time elapsed (ms): \"+timeLapse);\n        var timeLeft = fuelLeft * (timeLapse/(1000*60));\n        timeLeft = Number((timeLeft).toFixed(1));\n        msg.payload = timeLeft;\n        node.send(msg);\n    \n        //set new references\n        context.set('fuelLeft', nValue);\n        context.set('refTime', nTime);\n        context.set('pValue', nValue);\n    }\n}\n\n\n\n","outputs":1,"noerr":0,"x":740,"y":280,"wires":[["7630ecef.a0b2e4"]]},{"id":"3cec475d.9d6518","type":"debug","z":"1abba8e0.c29407","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":950,"y":420,"wires":[]},{"id":"21cc1bb7.5868a4","type":"function","z":"1abba8e0.c29407","name":"","func":"function timeConvert(n) {\n    var num = n;\n    var hours = (num / 60);\n    var rhours = Math.floor(hours);\n    var minutes = (hours - rhours) * 60;\n    var rminutes = Math.round(minutes);\n    return rhours + \" hour(s) and \" + rminutes + \" minute(s)\";\n}\n\nvar timeLeft = timeConvert(msg.payload);\nmsg.payload = \"Time remaining: \"+timeLeft.toString();\nreturn msg;","outputs":1,"noerr":0,"x":740,"y":420,"wires":[["3cec475d.9d6518"]]},{"id":"7630ecef.a0b2e4","type":"smooth","z":"1abba8e0.c29407","name":"","property":"payload","action":"mean","count":"10","round":"","mult":"single","reduce":false,"x":740,"y":350,"wires":[["21cc1bb7.5868a4"]]}]

1 Like

Ok I'll try this one with the smoothing.
Tomorrow I'm going on vacation, so Il post again the results in 10 days.

Thanks krambriw

Great have a nice vacation & xmas

Merry christmas to all!!
The time calculation now seems to be good.
Things to add
When % is increasing do not calculate but output message "Charging", when % starts dropping do the calculation

Im publishing my % value to this mqtt server in real time for you to be able to test the function locally
test.mosquitto.org 1883
topic sokomm/info/kot/bafer

Nice to hear! I have connected to your topic and it works fine. In the flow I have also added

  • separate message when "Charging"
  • a reset function if you would like to reset context values used in the calculation (a typical restart) and smoothing node

The smoothing is currently set to 10 values, I think a lower value like 5 is good enough to make a pretty accurate prediction

[{"id":"8095c1d2.1ae6f","type":"comment","z":"1abba8e0.c29407","name":"Heating System - fuel consumption prediction","info":"","x":260,"y":1220,"wires":[]},{"id":"2cf3f159.aa133e","type":"function","z":"1abba8e0.c29407","name":"","func":"var nValue = parseInt(msg.payload);\nvar nTime = new Date().getTime();\nif(msg.reset){\n    context.set('fuelLeft', undefined);\n    context.set('refTime', undefined);\n    context.set('pValue', undefined);\n}\ncontext.set('fuelLeft', context.get('fuelLeft')||nValue);\ncontext.set('refTime', context.get('refTime') || nTime);\ncontext.set('pValue', context.get('pValue')||0);\nvar fuelLeft = context.get('fuelLeft');\n\nif(nValue != context.get('fuelLeft')){\n\n    if (nValue > fuelLeft){\n        //we have refueled, update the references\n        context.set('refTime', nTime);\n        context.set('fuelLeft', nValue);\n        msg.payload = \"Charging\";\n        node.send(msg);\n    }\n\n    if (nValue < fuelLeft && nValue != context.get('pValue')){\n        //value has changed, fuel consumed\n        var refTime = context.get('refTime');\n        var change =  fuelLeft - nValue; //consumed fuel in percentage\n        fuelLeft = fuelLeft - change;\n        var timeLapse = nTime - refTime;\n        node.warn(\"fuel left:\"+fuelLeft);\n        node.warn(\"time elapsed (ms): \"+timeLapse);\n        var timeLeft = fuelLeft * (timeLapse/(1000*60));\n        timeLeft = Number((timeLeft).toFixed(1));\n        msg.payload = timeLeft;\n        node.send(msg);\n    \n        //set new references\n        context.set('fuelLeft', nValue);\n        context.set('refTime', nTime);\n        context.set('pValue', nValue);\n    }\n}\n\n\n\n","outputs":1,"noerr":0,"x":640,"y":1260,"wires":[["3916aa5d.5e8ac6"]]},{"id":"19ece1d6.02b87e","type":"debug","z":"1abba8e0.c29407","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":840,"y":1470,"wires":[]},{"id":"39a6c61a.39f16a","type":"function","z":"1abba8e0.c29407","name":"","func":"function timeConvert(n) {\n    var num = n;\n    var hours = (num / 60);\n    var rhours = Math.floor(hours);\n    var minutes = (hours - rhours) * 60;\n    var rminutes = Math.round(minutes);\n    return rhours + \" hour(s) and \" + rminutes + \" minute(s)\";\n}\n\nvar timeLeft = timeConvert(msg.payload);\nmsg.payload = \"Time remaining: \"+timeLeft.toString();\nreturn msg;","outputs":1,"noerr":0,"x":640,"y":1470,"wires":[["19ece1d6.02b87e"]]},{"id":"49f084a.a5e047c","type":"smooth","z":"1abba8e0.c29407","name":"","property":"payload","action":"mean","count":"10","round":"","mult":"single","reduce":false,"x":640,"y":1400,"wires":[["39a6c61a.39f16a"]]},{"id":"4174e3c0.a003ec","type":"mqtt in","z":"1abba8e0.c29407","name":"","topic":"sokomm/info/kot/bafer","qos":"2","datatype":"auto","broker":"fcc94dbc.db548","x":190,"y":1260,"wires":[["2cf3f159.aa133e","a783573c.d644b8"]]},{"id":"3916aa5d.5e8ac6","type":"switch","z":"1abba8e0.c29407","name":"","property":"payload","propertyType":"msg","rules":[{"t":"cont","v":"Charging","vt":"str"},{"t":"istype","v":"number","vt":"number"}],"checkall":"true","repair":false,"outputs":2,"x":640,"y":1330,"wires":[["1337a981.8e6b36"],["49f084a.a5e047c"]]},{"id":"1337a981.8e6b36","type":"debug","z":"1abba8e0.c29407","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":840,"y":1260,"wires":[]},{"id":"a783573c.d644b8","type":"debug","z":"1abba8e0.c29407","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":190,"y":1330,"wires":[]},{"id":"28eb832d.a207cc","type":"inject","z":"1abba8e0.c29407","name":"","topic":"","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":140,"y":1400,"wires":[["95692a2f.89b518"]]},{"id":"95692a2f.89b518","type":"change","z":"1abba8e0.c29407","name":"","rules":[{"t":"set","p":"reset","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":1400,"wires":[["49f084a.a5e047c","2cf3f159.aa133e"]]},{"id":"fcc94dbc.db548","type":"mqtt-broker","z":"","name":"","broker":"test.mosquitto.org","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]

[{"id":"8095c1d2.1ae6f","type":"comment","z":"1abba8e0.c29407","name":"Heating System - fuel consumption prediction","info":"","x":250,"y":290,"wires":[]},{"id":"2cf3f159.aa133e","type":"function","z":"1abba8e0.c29407","name":"","func":"var nValue = parseInt(msg.payload);\nvar nTime = new Date().getTime();\nif(msg.reset){\n    context.set('fuelLeft', undefined);\n    context.set('refTime', undefined);\n    context.set('pValue', undefined);\n}\ncontext.set('fuelLeft', context.get('fuelLeft')||nValue);\ncontext.set('refTime', context.get('refTime') || nTime);\ncontext.set('pValue', context.get('pValue')||0);\nvar fuelLeft = context.get('fuelLeft');\n\nif(nValue != fuelLeft){\n\n    if (nValue > fuelLeft){\n        //we have refueled, reset the references\n        context.set('refTime', undefined);\n        context.set('fuelLeft', nValue);\n        context.set('pValue', undefined);\n        msg.payload = \"Charging: \"+msg.payload;\n        node.send(msg);\n    }\n\n    if (nValue < fuelLeft && nValue != context.get('pValue')){\n        //value has changed, fuel consumed\n        var refTime = context.get('refTime');\n        var change =  fuelLeft - nValue; //consumed fuel in percentage\n        fuelLeft = fuelLeft - change;\n        var timeLapse = nTime - refTime;\n        node.warn(\"fuel left:\"+fuelLeft);\n        node.warn(\"time elapsed (ms): \"+timeLapse);\n        var timeLeft = fuelLeft * (timeLapse/(1000*60));\n        timeLeft = Number((timeLeft).toFixed(1));\n        msg.payload = timeLeft;\n        node.send(msg);\n    \n        //set new references\n        context.set('fuelLeft', nValue);\n        context.set('refTime', nTime);\n        context.set('pValue', nValue);\n    }\n}\n\n\n\n","outputs":1,"noerr":0,"x":760,"y":330,"wires":[["3916aa5d.5e8ac6"]]},{"id":"19ece1d6.02b87e","type":"debug","z":"1abba8e0.c29407","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":960,"y":540,"wires":[]},{"id":"39a6c61a.39f16a","type":"function","z":"1abba8e0.c29407","name":"","func":"function timeConvert(n) {\n    var num = n;\n    var hours = (num / 60);\n    var rhours = Math.floor(hours);\n    var minutes = (hours - rhours) * 60;\n    var rminutes = Math.round(minutes);\n    return rhours + \" hour(s) and \" + rminutes + \" minute(s)\";\n}\n\nvar timeLeft = timeConvert(msg.payload);\nmsg.payload = \"Time remaining: \"+timeLeft.toString();\nreturn msg;","outputs":1,"noerr":0,"x":760,"y":540,"wires":[["19ece1d6.02b87e"]]},{"id":"49f084a.a5e047c","type":"smooth","z":"1abba8e0.c29407","name":"","property":"payload","action":"mean","count":"10","round":"","mult":"single","reduce":false,"x":760,"y":470,"wires":[["39a6c61a.39f16a"]]},{"id":"4174e3c0.a003ec","type":"mqtt in","z":"1abba8e0.c29407","name":"","topic":"sokomm/info/kot/bafer","qos":"2","datatype":"auto","broker":"fcc94dbc.db548","x":180,"y":330,"wires":[["3e7a0c07.035eb4"]]},{"id":"3916aa5d.5e8ac6","type":"switch","z":"1abba8e0.c29407","name":"","property":"payload","propertyType":"msg","rules":[{"t":"cont","v":"Charging","vt":"str"},{"t":"istype","v":"number","vt":"number"}],"checkall":"true","repair":false,"outputs":2,"x":760,"y":400,"wires":[["1337a981.8e6b36"],["49f084a.a5e047c"]]},{"id":"1337a981.8e6b36","type":"debug","z":"1abba8e0.c29407","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":960,"y":330,"wires":[]},{"id":"a783573c.d644b8","type":"debug","z":"1abba8e0.c29407","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":440,"y":400,"wires":[]},{"id":"28eb832d.a207cc","type":"inject","z":"1abba8e0.c29407","name":"","topic":"","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":470,"wires":[["95692a2f.89b518"]]},{"id":"95692a2f.89b518","type":"change","z":"1abba8e0.c29407","name":"","rules":[{"t":"set","p":"reset","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":440,"y":470,"wires":[["49f084a.a5e047c","2cf3f159.aa133e"]]},{"id":"3e7a0c07.035eb4","type":"delay","z":"1abba8e0.c29407","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"minute","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"x":440,"y":330,"wires":[["2cf3f159.aa133e","a783573c.d644b8"]]},{"id":"fcc94dbc.db548","type":"mqtt-broker","z":"","name":"","broker":"test.mosquitto.org","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]

Been running a test a couple of days with data from your broker. I have found some issues and have some thoughts

  1. I notice a problem when your data quickly changes up/down/up/down... which seems to happen sometimes. Issue is that the calculation gets wrong. So I have tried to solve it by adding a delay node at the input, configured as rate limit, 1 msg/minute which seems to be good enough

  2. I had some thoughts about what "charging" actually means and what is the correct approach when this happens. I think, for now, the correct way to handle this is to just set the actual fuel level and then start the measuring of the burn rate "from the beginning" again (see the code in the function node for details)

What are we actually measuring and predicting? I understand we are measuring the actual "burn rate", i.e. how much fuel has been consumed during a measured time interval. Based on this we predict the remaining time until the fuel is fully consumed, each time assuming the burn rate will continue "at the current level". In reality this is not the case, the energy need will vary, thats why we are re-calculating every time the incoming value changes. To smooth the resulting remaining time we use the smooth node. Configuring it for less number of readings will make it react faster in changing it's output value

I'm currently running the updated flow above, hoping it will work more accurate

When you look at the predictions made, do they seem to be realistic when compared to the reality?

It gets it right within + - 30 min ,that's good enough for me.