Hot Tank monitoring, with energy calculations

In an effort to understand OpenHAB better and get into writing rules, I decided to work on a project which I last operated on 1-wire and programmed through bash; strictly using nano, no vi for me.

The concept is that we have a whole-house, indirect hot-water tank rated at 300L and insulated using spray-insulation. I wanted to be able to see the state of the tank in terms of the amount of hot water available within.
I fitted it with four sensor holes in the insulation so that I could mount four DS18B20 1-wire sensors and I connected them to the Fibaro Universal Sensor. The sensors should ideally touch the copper vessel, but frankly that’s difficult to achieve when retrofitting these sensors without affecting the functionality of the insulation.

Here are my item mappings for the Fibaro:

// Fibaro Hot Tank Sensors
Number FF_HotTank_01     "HotTank_01 [%.1f °C]"     <temperature>     (gHotTankTemperature)     {channel="zwave:device:c7937d7e:node16:sensor_temperature5"}
Number FF_HotTank_02     "HotTank_02 [%.1f °C]"     <temperature>     (gHotTankTemperature)     {channel="zwave:device:c7937d7e:node16:sensor_temperature6"}
Number FF_HotTank_03     "HotTank_03 [%.1f °C]"     <temperature>     (gHotTankTemperature)     {channel="zwave:device:c7937d7e:node16:sensor_temperature3"}
Number FF_HotTank_04     "HotTank_04 [%.1f °C]"     <temperature>     (gHotTankTemperature)     {channel="zwave:device:c7937d7e:node16:sensor_temperature4"}

Here are my 12hr min/max items for database storage, and some items for energy calcs:

Number FF_HotTank_01_Min12h "[%.1f °C]" <temperature>
Number FF_HotTank_01_Min12h_Val "- Max. Temp [%.1f °C]" <temperature>
Number FF_HotTank_01_Max12h_Val "- Max. Temp [%.1f °C]" <temperature>

Number FF_HotTank_02_Min12h "[%.1f °C]" <temperature>
Number FF_HotTank_02_Min12h_Val "- Max. Temp [%.1f °C]" <temperature>
Number FF_HotTank_02_Max12h_Val "- Max. Temp [%.1f °C]" <temperature>

Number FF_HotTank_03_Min12h "[%.1f °C]" <temperature>
Number FF_HotTank_03_Min12h_Val "- Max. Temp [%.1f °C]" <temperature>
Number FF_HotTank_03_Max12h_Val "- Max. Temp [%.1f °C]" <temperature>

Number FF_HotTank_04_Min12h "[%.1f °C]" <temperature>
Number FF_HotTank_04_Min12h_Val "- Max. Temp [%.1f °C]" <temperature>
Number FF_HotTank_04_Max12h_Val "- Max. Temp [%.1f °C]" <temperature>

Number FF_HotTank_TotEnergy "Energy in hot water" <energy>
Number FF_HotTank_Delta "Difference in Energy" <energy>

To derive some of the min/max values from our data, we create a rule such as the following which is triggered when a device in group “FF_HotTank_01” changes. To save some space, the section of the rule below is only complete for the first two sensors!

rule "Calculate high and low of hot tank sensor 01"
when
  Item FF_HotTank_01 changed
then
        var Number FF_HotTank_01_Min12h
        var Number FF_HotTank_01_Max12h
        // var Number FF_HotTank_01_Min24h
        // var Number FF_HotTank_01_Max24h

        if (FF_HotTank_01.state instanceof DecimalType) {
                FF_HotTank_01_Min12h = (FF_HotTank_01.minimumSince(now.minusHours(12), "influxdb").state as DecimalType)
                postUpdate(FF_HotTank_01_Min12h_Val, FF_HotTank_01_Min12h)

                FF_HotTank_01_Max12h = (FF_HotTank_01.maximumSince(now.minusHours(12), "influxdb").state as DecimalType)
                postUpdate(FF_HotTank_01_Max12h_Val, FF_HotTank_01_Max12h)

                }
end

rule "Calculate high and low of hot tank sensor 02"
when
  Item FF_HotTank_02 changed
then
        var Number FF_HotTank_02_Min12h
        var Number FF_HotTank_02_Max12h
        // var Number FF_HotTank_01_Min24h
        // var Number FF_HotTank_01_Max24h

        if (FF_HotTank_02.state instanceof DecimalType) {
                FF_HotTank_02_Min12h = (FF_HotTank_02.minimumSince(now.minusHours(12), "influxdb").state as DecimalType)
                postUpdate(FF_HotTank_02_Min12h_Val, FF_HotTank_02_Min12h)

                FF_HotTank_02_Max12h = (FF_HotTank_02.maximumSince(now.minusHours(12), "influxdb").state as DecimalType)
                postUpdate(FF_HotTank_02_Max12h_Val, FF_HotTank_02_Max12h)

                }
end

Then to simply display this min/max data in a sitemap, we can show it as follows:

sitemap hottank label="hottank"
{
        Frame label="Tank State" {
                Text item=FF_HotTank_01
                Text item=FF_HotTank_02
                Text item=FF_HotTank_03
                Text item=FF_HotTank_04
        }

        Frame label="Sensor 01" {
                Text item=FF_HotTank_man label="Current Value"
                Text item=FF_HotTank_01
                Text item=FF_HotTank_01_Min12h_Val label="Min 12hrs"
                Text item=FF_HotTank_01_Max12h_Val label="Max 12hrs"
                Slider item=FF_HotTank_man label=Temperature
        }

        Frame label="Sensor 02" {
                Text item=FF_HotTank_02
                Text item=FF_HotTank_02_Min12h_Val label="Min 12hrs"
                Text item=FF_HotTank_02_Max12h_Val label="Max 12hrs"
        }

        Frame label="Sensor 03" {
                Text item=FF_HotTank_03
                Text item=FF_HotTank_03_Min12h_Val label="Min 12hrs"
                Text item=FF_HotTank_03_Max12h_Val label="Max 12hrs"
        }

        Frame label="Sensor 04" {
                Text item=FF_HotTank_04
                Text item=FF_HotTank_04_Min12h_Val label="Min 12hrs"
                Text item=FF_HotTank_04_Max12h_Val label="Max 12hrs"

        }
}

(continued below…)

1 Like

(…continued from above)
Of course it would be nice to be able to display this data in Grafana, so I devised this dashboard which provides the historical sensor data with a kW value which represents the amount of energy which would be required to heat a quantity of water from a defined value (20 degrees C) to the measured value. Once I have a figure in kW I can then monitor it for deltas and chart the loss/gain on the tank as it goes through the day.


Top left: Historical sensor data.
Top right: Energy in kW (from an offset)
Bottom left: Five minute delta - loss/gain.
Bottom right: Simple ‘gas-gauge’ showing the current state of the tank.

To calculate the Total Energy, Gain/Loss and the gauge, we use the following rule to do the maths…

rule "Calculate the amount of energy in the hot tank in kWh"
when
  Item gHotTankTemperature changed
then
        var Number valEnergy
        var Number valTotEngy
        var Number FF_HotTank_Energy01
        var Number FF_HotTank_Energy02
        var Number FF_HotTank_Energy03
        var Number FF_HotTank_Energy04
        var Number FF_HotTank_5minsBack
        val ColdTemp = 20
        val FF_HT_01 = (FF_HotTank_01.state as DecimalType).floatValue
        val FF_HT_02 = (FF_HotTank_02.state as DecimalType).floatValue
        val FF_HT_03 = (FF_HotTank_03.state as DecimalType).floatValue
        val FF_HT_04 = (FF_HotTank_04.state as DecimalType).floatValue

        // Calculate the energy value in a hot-tank of water.
        // Formula: (Volume in L * 4 * Difference in temperature) / 3412 = Potential energy in kW.
        // This does not take into account the heating method or efficiency - electric, gas, oil, etc,

        // My tank is 300L, I have four sensors installed on it, one on the top, one a quarter of the way down and so forward.
        // Hence I assigned an equivalent volume of 75L to each sensor.

        if (gHotTankTemperature.state instanceof DecimalType) {

                // Run through each sensor and calculate the energy...

                valEnergy = (75*4*(FF_HT_01 - ColdTemp)/3412)
                FF_HotTank_Energy01 = valEnergy
                //logWarn("hotenergy.rules", "FF_HotTank_Energy01=" + FF_HotTank_Energy01 + "valEnergy=" + valEnergy + "FF_HT_01=" + FF_HT_01)

                valEnergy = (75*4*(FF_HT_02 - ColdTemp)/3412)
                FF_HotTank_Energy02 = valEnergy

                valEnergy = (75*4*(FF_HT_03 - ColdTemp)/3412)
                FF_HotTank_Energy03 = valEnergy

                valEnergy = (75*4*(FF_HT_04 - ColdTemp)/3412)
                FF_HotTank_Energy04 = valEnergy

                // Next, add up the combined four values and commit them to the database.
                valEnergy = (FF_HotTank_Energy01 + FF_HotTank_Energy02 + FF_HotTank_Energy03 + FF_HotTank_Energy04)
                postUpdate(FF_HotTank_TotEnergy, valEnergy)

                // Now calculate the delta in kW from 5 mins back and update the database. This will give us data for a bar-chart on utilisation, +/-
                FF_HotTank_5minsBack = (FF_HotTank_TotEnergy.historicState(now.minusMinutes(5), "influxdb").state as DecimalType)
                valTotEngy = FF_HotTank_TotEnergy.state
                //logWarn("hotenergy.rules", "valTotEngy=" + valTotEngy)
                // Calculate the difference and multiply by 1000 to convery from kW to W.
                valEnergy = ((valTotEngy - FF_HotTank_5minsBack) * 1000)
                logWarn("hotenergy.rules", "valTotEngy=" + valTotEngy  + " minus FF_HotTank_5minsBack=" + FF_HotTank_5minsBack + "= valEnergy" + valEnergy)
                postUpdate(FF_HotTank_Delta, valEnergy)
                }
end

So I have found that the setup is highly sensitive and can be used to profile the rate of loss of energy from the insulated hot-tank. In the graphic below you can see the rate of loss over a period of 5hrs 30mins, where the highest rate of loss can be observed when the tank is hottest and reducing significantly over the period.

DeltaEnergyLoss

So that’s it. I cannot claim to be a programmer so I’ll gladly take feedback on my code, but this works for me right now.
Later on I do hope to provide a closed loop so that I can call for hot-water on demand; but as they say - that’s another day’s work…

5 Likes

very interesting - thank you for posting

Nice tutorial! Very thorough and detailed. I love the graphs.

My only suggestion is you can easily merge your two “Calculate high and low” Rules into one using the triggeringItem implicit variable.

There are a few other minor changes I’d recommend too like using the method instead of the action on postUpdate and use Number instead of DecimalType.

Below I’m applying (for further reading):

rule "Calculate high and low of hot tank sensors"
when
    Member of gHotTankTemperature changed
then
    if(triggeringItem.state == NULL || triggeringItem.state == UNDEF) return; // do nothing if the Item changed to not a number

    // 2. Calculate what to do
    val min = triggeringItem.minimumSince(now.minusHours(12), "influxdb").state as Number
    val max = triggeringItem.maximumSince(now.minusHours(12), "influxdb").state as Number

    // 3. Do it
    postUpdate(triggeringItem.name+"_Min12h_Val", if(min === null) "UNDEF" else min.toString)
    postUpdate(triggeringItem.name+"_Max12h_Val", if(max === null) "UNDEF" else max.toString)
end

The above Rule works for all four sensors and will set the Min and Max Items to UNDEF if for some reason the calls to persistence to get the min and max fails resulting in null. Note that even though I said to use the method instead of the Action for postUpdate, in the above I’m calculating the name of the Item in the Rule which is one of the few places where the Action is more appropriate.

The one line if/else is called the trinary operator.

Shouldn’t the label for FF_HotTank_02_Min12h_Val be “- Min. Temp …” instead of Max?

In the second Rule I’m going to bring out some more advanced stuff using Rules, much of which is documented in Design Pattern: Working with Groups in Rules.

rule "Calculate the amount of energy in the hot tank in kWh"
when
    Item gHotTankTemperature changed
then
    // 1. Is there anything to do?
    if(gHotTankTemperature.state == NULL || gHotTankTemperature == UNDEF) return;

    // 2. Calculate what to do

    // ------ Calculate the total energy ---------
    // Formula: (Volume in L * 4 * Difference in temperature) / 3412 = Potential energy in kW.
    // This does not take into account the heating method or efficiency - electric, gas, oil, etc,
    //
    // My tank is 300L, I have four sensors installed on it, one on the top, one a quarter of the way down and so forward.
    // Hence I assigned an equivalent volume of 75L to each sensor.
    //
    // - filter[ t | ... : returns a List of all the members that are not NULL and not UNDEF
    // - map[ state as Number ] : returns a List of all the States of the result from the filter as Numnbers
    // - reduce[ sum, temp | ... : loops through all the Numbers returned from the map, performs the calculation
    //   on each one storing the result in sum
    val totEnergy = gHotTankTemperature.members.filter[ t | t.state != NULL && t.state != UNDEF ].map[ state as Number ].reduce[ sum, temp | 
        sum = sum + (75*4*(temp - 20)/3412) 
    ]

    // ------ Total energy five minutes ago ------
    val 5minsBack = FF_HotTank_TotEnergy.historicState(now.minusMinutes(5), "influxdb").state as Number
    
    // 3. Do it
    FF_HotTank_TotEnergy.postUpdate(valEnergy)
    FF_HotTank_Delta.postUpdate((valEnergy - 5minsBack) * 1000)
end

I just typed in the above. There are likely mistakes and typos. And I don’t expect you or anyone else to have come up with the above code. I mainly am posting it as reference. Perhaps when you or future users come and see duplicative code there will be some techniques illustrated above that can be applied to avoid the duplication.

Thanks for posting! We more need tutorials like this!

Thanks for the constructive feedback Rich.
Yes, as per your links, I would class myself as naive when it comes to excessive if/then loops. :grinning: I clearly see the method in the change suggested in the min/max rule, it’s easy to understand and implement and I have reverted to it in my code.
You are correct about the item names, they should read min instead of max.

The map/reduce method is tricky, it looks like RPN to me! I’ll have to play around with it a bit more to understand the structure, etc.
If I decide to implement a larger house heating system via OpenHAB, I’ll need this method for sure.
Thanks! R

1 Like

It looks tricky and you will find TONS of stuff written about it. But the concept is pretty simple. If we break down the line above…

  1. gHotTankTemperature.members returns a List of all the Members of gHotTankTemperature. A List is kind of like a fancy array. So, if you have 8 members of gHotTankTemperature you will get a List containing 8 Items.

  2. .filter[ t | t.state != NULL && t.state != UNDEF] is a method on List that will return a List of all the members that match the condition. In this case we are filtering out all Items whose state is NULL or UNDEF. So if one of those Items in the List returned by 1 has a state of NULL, this filter will return a list with 7 Items, skipping the one whose state is NULL.

  3. map[ state as Number] is a method on List that will call the indicated method on each Item in the list and return the result as a List. So in this case, this map will create a new List containing the result of calling .state on each Item from the List returned by the filter. So, at this point we have a List of all the states of all the members of gHotTankTemperature that are not NULL and not UNDEF. The as Number part is us telling the Rules DSL that we want to treat those States as Numbers.

  4. reduce[ sum, temp |... is a method on List that will loop through all the members of the List, perform the indicated operations on each one and store the result. The first argument to reduce stores the result. The second argument gives a name to the specific item in the List that is currently being operated upon. The sum is what gets returned by the reduce and since the reduce is the last thing in the chain of calls that is what gets returned.

So at a higher level, what we are doing is looping through all the members of the Group, skipping the ones with non-workable states (filter), extracting the State of each Item in the list (map), and doing the calculations you defined right after the if(gHotTankTemperature.state instanceof DecimalType) and keeping a running summation of the results.

I don’t know if I’ve muddied the water or provided clarity but I’m happy to answer questions.

2 Likes

Hi Rich,
Your post really helped my understanding. I have one question regarding (2). I understand that it returns a list of items from the group that are not null and not undefined. How should I do it if I wanted to grab only the first item from the group that is not null, not undefined, and updatedSince the last X hours?

var highTemp = myGroup.members.filter[ t | t.state != NULL && t.state != UNDEF && t.updatedSince(now.minusHours(1)].map[ state as Number ].reduce[ a, b | b ]

I think I am on the right track but unsure if I used “reduce” correctly. Any suggestions would be greatly appreciated! Thanks!

Update: that worked, but I think it was grabbing the last item from the list. That will work for me for now. Again, thanks Rich for the wonderful explanation!

Remember that the order of the members of the Group are mostly undefined. If you want them in a certain order you’ll have to sort them. There are examples (I think) at Design Pattern: Working with Groups in Rules