Widget (Nibe) Heatpump

Hi there,

Time to try and make a widget. If my search skills aren’t lacking I couldn’t find one for a Nibe Heatpump. And of source it would be nice to see what it’s doing. A good reason to dive into widgets and make one. Configuration is easiest if you own a Nibe and use the Nibe uplink binding, just assign the name of the group. But … if needed you can assign all separate items if you own another brand or use a different binding (there’s three to choose from for a Nibe).

It took some trial and error but I think I found a way to determine if it’s off, active heating the room, passive heating the room, cooling the room or heating the boiler. It’s styled from the cute little house that is also on the thermostat. I have more plans (and maybe in the end make it an official marketplace widget), but for new it seems good enough to share. Maybe you all have suggestions or tips :wink:

Summary

A screenshot of the widget. Base is the little house (from the thermostat) with inside and outside temperature. The other things are styled to match. In the window it shows an icon to summarize what the heatpump is doing. And the three heat exchangers show the details with matching color to indicate where the heat is flowing. The color of the pump speed makes is the easy indicator, if it’s red it’s heating and blue when cooling.

nb-h

As far as I can see, it has two glitches. The first minute from heating the boiler it thinks it’s cooling (because the brine startup). And the widget is not responsive, i.e. doesn’t scale. Partly (I think) because of the absolute positions of the images I use. But also the standard components don’t size really well on my fixed layout dashboard. So probably I miss some knowledge here. A good link for this is appreciated.

Binding

Although the binding which can use my beloved ESP/Arduino solutions sounds tempting I started with the Nibe Uplink binding. Configuration is straight forward using the documentation. Some of the channels don’t work, maybe because my F1245 doesn’t match completely with the F1145. But the important data is read.

Installation and configuration

My widgets requires a few pictures. Download and unzip them and copy them in the ‘html’ folder in your openhab installation. Next create a new widget and copy the code. Both images and code are at the end of the post (or is it more usual - as it is big - to zip the code also into a file?)

Last step is configuring it, now you have a choice. If you use the same binding, only the first one is needed. Tell the group of the Nibe items and you’re done.

If you use another binding or own a different heatpump. Don’t use the group item but assign all separate items. Since all heatpumps more or less work the same, I assume they have the items needed. Of course, it’s an assumption. If you try I’m curious if it works as planned. I could only test this setup by assigning my separate items. Yeah, there’s a trick to make this work. It’s explained below,

Details

The big trick to support both the group item and the separate items is by using the default values of properties. Default the group is empty (but not null because of the “”) and the separate items contain as a default the second part/postfix after the group (as openHab makes them when creating equipment from a thing).

In the code I combine them. So

  • If you supply only the group the default postfixes of the items will be in effect. And the formula points to the right items in the group
  • If you supply all the separate items (and not the group), the group will be the default “”. And the formula just points to the items you assigned.
 text: "= items[props.heatpump+ props.supplyPump].state"

props:
  parameters:
    - context: item
      default: ""
      description: Heatpump group
      label: Heatpump group
      name: heatpump
      required: false
      type: TEXT
    - context: item
      default: _BT50RoomTempS1
      description: Item with room temperature
      label: Room Temperature
      name: roomTemp
      required: false
      type: TEXT

The binding (as it’s using the Nibe website) doesn’t supply indicators. So after some trial and error this seems like the best way to determine the status without getting (too) crazy on conditions. The icon code is the clearest example, so here it is. A big cascaded if statement…

    - component: f7-icon
      config:
        f7: "= items[props.heatpump + props.supplyPump].state=='0 %' ? 'pause_fill' : |
          items[props.heatpump + props.brinePump].state=='0 %' ? 'flame' : | 
          Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>=50 ? 'drop_fill' : |
          items[props.heatpump + props.degreeMin].state==0 ? 'snow' : 'flame_fill' "

If the supply pump is off (running at 0 %) it’s easy. It’s doing nothing.

nb-o

If only the supply pump is running it must be passively heating. After heating the water to a certain point the brine is turned off and it keeps pumping the heated water through the house. It keeps heating the house while slowly cooling down. Until of course it’s becoming too cold and the water needs to be heated again (and the brine is turned on again).

nb-p

Now both pumps are running and it gets interesting. Best next check is a supply temperature over 50°C. It must be heating the water in the boiler.

nb-b

If not, the degreeMinutes is checked. A bit technical but this is the way a heatpump creates on-off intervals while heating. So most important, if this is non-zero it means it’s heating. Since at this point we also know both pumps are running, it must be actively heating.

nb-h

Last but not least, if none of the above applies (but both pumps are running) it must be cooling.

nb-c

It needed two tweaks. First there is long to float error or something like that in the binding. So the temps are quite often 20.40000000001 which is ugly. Solved by adding a .toFixed(1).

Second the sensors don’t seem to be extremely accurate (or the surrounding is influencing them). So when they’re close (difference 0.1-0.2, usually when cooling or at the end of passively heating) the values may invert. This annoyed me, that’s why the min/max in some of the temperatures. To keep them logic to what the heatpump is doing.

Testing

Last but not least. The code is a bit complex with all the repeating conditions. Testing took far to much time if you have to wait for the right conditions (and soon cooling won’t even happen anymore :wink: ). So I created a test setup. Just created a fake group with the right items and made a rule that copies an old state into that. Point the widget to the test group (I named it fake) and there you go. No waiting, instant the values you need to see if all works out. Code (for the new ECMA2021 script plugin). If you found an easier way to test, let me know!

console.info("creating test data");

//let t=time.ZonedDateTime.parse("2022-09-02T08:09:00+02:00[SYSTEM]"); // active heat
//let t=time.ZonedDateTime.parse("2022-09-02T08:20:00+02:00[SYSTEM]"); // passive heat
//let t=time.ZonedDateTime.parse("2022-09-03T11:34:00+02:00[SYSTEM]"); // hot water
//let t=time.ZonedDateTime.parse("2022-08-25T20:30:00+02:00[SYSTEM]"); // cooling
let t=time.ZonedDateTime.parse("2022-09-03T13:30:00+02:00[SYSTEM]"); // off

//iterate through Nibe group
let group=items.getItem("NibeF1145");
for (let i in group.members){
  //find corresponding item in fake group
  let member=group.members[i];
  let fake=member.name.replace("NibeF1145","fake");
  let item=items.getItem(fake,true);
  if (item!=null){
    //if it exists (don't need al items), copy state from desired time
    let his=member.history.historicState(t);
    console.info("for "+item.name+" set "+his.toString());
    item.postUpdate(his.toString());    
  }
}

images

Link to (zipfile with) the images

code

The full code …

uid: widget_heatpump
tags: []
props:
  parameters:
    - context: item
      default: ""
      description: Heatpump group
      label: Heatpump group
      name: heatpump
      required: false
      type: TEXT
    - context: item
      default: _BT50RoomTempS1
      description: Item with room temperature
      label: Room Temperature
      name: roomTemp
      required: false
      type: TEXT
    - context: item
      default: _BT1OutdoorTemperature
      description: Item with outside temperature
      label: Outside Temperature
      name: outTemp
      required: false
      type: TEXT
    - context: item
      default: _EP14GP2BrinePumpSpeed
      description: Item with Brine Pump Speed (percentage)
      label: Brine Pump
      name: brinePump
      required: false
      type: TEXT
    - context: item
      default: _EB100EP14BT10BrineinTemperature
      description: Item with Brine In (return to heatpump, usualy warm side)
      label: Brine In
      name: brineIn
      required: false
      type: TEXT
    - context: item
      default: _EB100EP14BT11BrineoutTemperature
      description: Item with Brine Out (leaving heatpump, usualy cold side)
      label: Brine Out
      name: brineOut
      required: false
      type: TEXT
    - context: item
      default: _SupplyPumpSpeedEP14
      description: Item with Supply Pump Speed (percentage)
      label: Supply Pump
      name: supplyPump
      required: false
      type: TEXT
    - context: item
      default: _EB100EP14BT3ReturnTemp
      description: Item with Supply In (return to heatpump, usualy cold side)
      label: Supply In
      name: supplyIn
      required: false
      type: TEXT
    - context: item
      default: _BT2SupplyTempS1
      description: Item with Supply Out (leaving heatpump, usualy warm side)
      label: Supply Out
      name: supplyOut
      required: false
      type: TEXT
    - context: item
      default: _DegreeMinutes16Bit
      description: Item with DegreeMinutes (number used as indicator heating)
      label: Degree Minutes
      name: degreeMin
      required: false
      type: TEXT
    - context: item
      default: _BT7HWTop
      description: Item with boiler temperature
      label: Boiler Temperature
      name: boilerTemp
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Sep 5, 2022, 2:02:18 PM
component: f7-card
config: {}
slots:
  default:
    - component: f7-badge
      config:
        bgColor: rgba(0,0,0,0.0)
        style:
          background-image: url(/static/nibe-ground.png)
          background-size: 300px
          border-radius: 12px
          height: 185px
          left: 20px
          position: absolute
          top: 100px
          width: 267px
    - component: f7-badge
      config:
        bgColor: rgba(0,0,0,0.0)
        style:
          background-image: url(/static/nibe-house.png)
          background-size: 220px
          border-radius: 12px
          height: 220px
          left: 42px
          position: absolute
          top: 5px
          width: 220px
    - component: f7-badge
      config:
        bgColor: rgba(0,0,0,0.0)
        visible: true
        style:
          background-image: url(/static/nibe-heatpump.png)
          background-size: 28px
          border-radius: 0px
          height: 50px
          left: 210px
          position: absolute
          top: 152px
          width: 28px
    - component: f7-badge
      config:
        bgColor: rgba(0,0,0,0.0)
        style:
          background-image: url(/static/nibe-boiler.png)
          background-size: 28px
          border-radius: 0px
          height: 20px
          left: 210px
          position: absolute
          top: 104px
          width: 28px
    - component: f7-icon
      config:
        f7: "= items[props.heatpump + props.supplyPump].state=='0 %' ? 'pause_fill' : items[props.heatpump + props.brinePump].state=='0 %' ? 'flame' : Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>=50 ? 'drop_fill' : items[props.heatpump + props.degreeMin].state==0 ? 'snow' : 'flame_fill' "
        size: 27px
        style:
          position: absolute
          left: 164px
          top: 69px
          color: rgb(78,103,105)
    - component: f7-badge
      config:
        bgColor: rgba(0,0,0,0.0)
        visible: true
        style:
          background-image: "=items[props.heatpump + props.supplyPump].state=='0 %' || Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>=50 ? 'url(/static/nibe-supply-off.png)' : items[props.heatpump + props.degreeMin].state==0 ? 'url(/static/nibe-supply-cool.png)' : 'url(/static/nibe-supply-heat.png)' "
          background-size: 111px
          border-radius: 0px
          height: 29px
          left: 90px
          position: absolute
          top: 175px
          width: 111px
    - component: f7-badge
      config:
        bgColor: rgba(0,0,0,0.0)
        visible: true
        style:
          background-image: "=items[props.heatpump + props.brinePump].state=='0 %' ? 'url(/static/nibe-brine-off.png)' :  Number.parseFloat(items[props.heatpump + props.brineIn].state) - Number.parseFloat(items[props.heatpump + props.brineOut].state) <=1  ? 'url(/static/nibe-brine-cool.png)' :  'url(/static/nibe-brine-heat.png)'  "
          background-size: 44px
          border-radius: 0px
          height: 77px
          left: 203px
          position: absolute
          top: 205px
          width: 44px
    - component: f7-badge
      config:
        bgColor: rgba(0,0,0,0.0)
        visible: true
        style:
          background-image: "= items[props.heatpump + props.supplyPump].state!='0 %' && Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>= 50 ? 'url(/static/nibe-boiler-heat.png)' : 'url(/static/nibe-boiler-off.png)' "
          background-size: 35px
          border-radius: 0px
          height: 28px
          left: 207px
          position: absolute
          top: 124px
          width: 34px
    - component: Label
      config:
        text: = Number.parseFloat(items[props.heatpump + props.roomTemp].state).toFixed(1)
        style:
          font-size: 18px
          color: white
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          text-align: center
          position: absolute
          top: 110px
          left: 150px
          width: 50px
    - component: Label
      config:
        text: = Number.parseFloat(items[props.heatpump +props.outTemp].state).toFixed(1)
        style:
          font-size: 18px
          color: gray
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          position: absolute
          top: 10px
          left: 20px
    - component: Label
      config:
        text: "=items[props.heatpump + props.supplyPump].state=='0 %' || Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>=50 ? '' : items[props.heatpump + props.degreeMin].state==0 ? Math.min(Number.parseFloat(items[props.heatpump + props.supplyIn].state),Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])).toFixed(1) : Math.max(Number.parseFloat(items[props.heatpump + props.supplyIn].state),Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])).toFixed(1)"
        style:
          color: "=items[props.heatpump + props.supplyPump].state=='0 %' || Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>=50 ? 'rgb(95,99,49)' : items[props.heatpump + props.degreeMin].state==0 ? 'blue' : 'red'"
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          position: absolute
          top: 178px
          left: 95px
    - component: Label
      config:
        text: "=items[props.heatpump + props.supplyPump].state=='0 %' || Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>=50 ? 'OFF' : items[props.heatpump + props.supplyPump].state.replace(' ','') "
        style:
          color: "=items[props.heatpump + props.supplyPump].state=='0 %' || Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>=50 ? 'rgb(95,99,49)' : items[props.heatpump + props.degreeMin].state==0 ? 'blue' : 'red'"
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          position: absolute
          top: 178px
          left: 125px
          width: 40px
          text-align: center
    - component: Label
      config:
        text: "=items[props.heatpump + props.supplyPump].state=='0 %'  || Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>=50 ? '' : items[props.heatpump + props.degreeMin].state==0 ? Math.max(Number.parseFloat(items[props.heatpump + props.supplyIn].state),Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])).toFixed(1) : Math.min(Number.parseFloat(items[props.heatpump + props.supplyIn].state),Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])).toFixed(1) "
        style:
          color: "=items[props.heatpump + props.supplyPump].state=='0 %'  || Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>=50 ? 'rgb(95,99,49)' : items[props.heatpump + props.degreeMin].state==0 ? 'red' : 'blue'"
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          position: absolute
          top: 178px
          left: 161px
          text-align: right
          width: 35px
    - component: Label
      config:
        text: "=items[props.heatpump + props.brinePump].state=='0 %' ? '' : Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>=50 || items[props.heatpump + props.degreeMin].state!=0 ? Math.min( Number.parseFloat(items[props.heatpump + props.brineOut].state),Number.parseFloat(items[props.heatpump + props.brineIn].state)).toFixed(1) :  Math.max( Number.parseFloat(items[props.heatpump + props.brineOut].state),Number.parseFloat(items[props.heatpump + props.brineIn].state)).toFixed(1) "
        style:
          color: "=items[props.heatpump + props.brinePump].state=='0 %' ? 'rgb(64,66,33)' : Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>=50 || items[props.heatpump + props.degreeMin].state!=0 ? 'blue' :  'red' "
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          position: absolute
          text-align: center
          top: 210px
          left: 199px
          width: 50px
    - component: Label
      config:
        text: "=items[props.heatpump + props.brinePump].state=='0 %' ? 'OFF' : items[props.heatpump + props.brinePump].state.replace(' ','') "
        style:
          color: "=items[props.heatpump + props.brinePump].state=='0 %' ? 'rgb(64,66,33)' : Number.parseFloat(i[props.heatpump + props.supplyOut].state.split(' ')[0])>=50 || items[props.heatpump + props.degreeMin].state!=0 ? 'red' :  'blue' "
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          text-align: center
          position: absolute
          width: 50px
          top: 230px
          left: 199px
    - component: Label
      config:
        text: "=items[props.heatpump + props.brinePump].state=='0 %' ? '' : Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>=50 || items[props.heatpump + props.degreeMin].state!=0 ? Math.max( Number.parseFloat(items[props.heatpump + props.brineOut].state),Number.parseFloat(items[props.heatpump + props.brineIn].state)).toFixed(1) :  Math.min( Number.parseFloat(items[props.heatpump + props.brineOut].state),Number.parseFloat(items[props.heatpump + props.brineIn].state)).toFixed(1) "
        style:
          color: "=items[props.heatpump + props.brinePump].state=='0 %' ? 'rgb(64,66,33)' : Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>=50 || items[props.heatpump + props.degreeMin].state!=0 ? 'red' :  'blue' "
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          text-align: center
          position: absolute
          top: 255px
          left: 199px
          width: 50px
    - component: Label
      config:
        text: =Number.parseFloat(items[props.heatpump +props.boilerTemp].state).toFixed(1)
        style:
          color: black
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          position: absolute
          top: 104px
          left: 212px
    - component: Label
      config:
        text1: =
        text: "= items[props.heatpump + props.supplyPump].state!='0 %' && Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>=50 ? Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0]).toFixed(1) : 'OFF'"
        style:
          color: "= items[props.heatpump + props.supplyPump].state!='0 %' && Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>=50 ? 'red' : 'rgb(95,99,49)' "
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          text-align: center
          position: absolute
          top: 126px
          left: 199px
          width: 50px

Well, talking to myself. What’s new? :slight_smile: Got a popup working that shows the day totals. Of course the thing doesn’t supply this, so I made a rule that derives this data and writes it to ‘free’ items. A little more styling to do but it works. If anyone is interested in the details, let me know.

popup

Still improving. Caught two bugs in the derived information and added a (first version for the) ‘guesstimate’ for the ground temperature using the brine-in temperature. Next plan is a little popup graph of the 24hrs activity when you press the “%” for the three heat exchangers.

hp-widget

Done, although the widget and rule are becoming little big monsters. Let me know if anyone is interested than I’ll post them.

hp-widget-v2

Hi Michel,

Share code , rules, is always important.
If we did not use all the code, we can always learn a little more about widgets , about rules . Learning with others will improve our widgets and our rules.

Br
Artur

Hi Artur, it was a bit lonely on this topic so I presumed this was a bit too specific and sharing with myself seemed a waste of time. But yeah, maybe there’s some general stuff in it so I’ll give it a go.

My Struggles/Lessons
It’s a lot so I highlight my struggles as an intro, these are the thing that took some search time on the forum and documentation to get it working.

For the rule, remember I’m using the new ECMA2021 script. Syntax is a bit different from the previous version.

//group
var grp="NibeF1145_";

Never use a “let” at the main level. The script will run only one time and than produce an error about an already used variable. Probable logic but it took some reading before I discovered the cause.

//get data from cache (as string)
var dataStr = cache.get("heatpump_data", () => (
  JSON.stringify(
    {
      lastHour:-1,
      hours:{
        heat:[],
        passive:[],
        cool:[],
        boiler:[]
      },
      last:{
        heat:0,
        passive:0,
        cool:0,
        boiler:0
      }
      
    }
  )

)); 

Same for the cache with a default value and a JSON object. This is the way to define it AND store it as a string. Also causing weird error messages.

var brineOutTemp=Number.parseFloat(brineOut.state).toFixed(1);
var supplyOutTemp=Number.parseFloat(supplyOut.state.split(' ')[0]).toFixed(1); 

Also troubles while calculation and comparing values that the thing produces with an unit. This seemed the easiest way to get normal floats. Not sure why for some items you have to go the extra mile to do a split (on the space between number and unit) to get the right value.

For the widget is was getting a popover to work. You have a popoverAction where you specify a name with points that you must repeat in the popover without them.

    - component: oh-link
      config:
        action: popover
        popoverOpen: ='.boiler.info'

etc.etc.etc.

    - component: f7-popover
      config:
        class: = 'boiler info'

etc.etc.etc.

Intro to the rule
To get it to work create extra items: NibeF1145_Reset_Calc (as a siwtch!), NibeF1145_Boiler_Temp, NibeF1145_Brine_Temp, NibeF1145_Heat_Temp, NibeF1145_Ground_Temp, NibeF1145_Boiler_Min, NibeF1145_Heat_Min, NibeF1145_Cool_Min, NibeF1145_Passive_Min and NibeF1145_Mode. Add them also to the equipment/group for the heat pump.

In regards to my first version I introduced a script that runs every minute (same as updates of the Nibe thing). The script derives what the heatpump is doing, included some extra conditions. It checks if the boiler temperature is below the set start heating temperature. And triggers only on negative degree minutes for heating. These rules improve the right status on startup or shutdown on boiler heating. Result is written in NibeF1145_Mode. The widget uses this now for showing the right icons and pictures.

If this NibeF1145_Mode is not off it will add 1 minute to the right counter (NibeF1145_Boiler_Min, NibeF1145_Heat_Min, NibeF1145_Cool_Min, NibeF1145_Passive_Min). To get a usable 24hrs count an internal cache (hence the JSON in there) with hourly counts. On every new hour that number is reset. And to get also the right number for the running hour a pecentage of the deleted hour will be used. So if it’s 8:15 than 75% over the “8h” value from a day ago will be added.

Also it will use the NibeF1145_Mode to fill the NibeF1145_Boiler_Temp, NibeF1145_Brine_Temp and NibeF1145_Heat_Temp. Two reasons. If the brine is off, the temperatures measured will rise to the surroundings. Fake values and not nice when shows. So only when the brine it running, the temperature measurement will be used. If not the ground temperature will be stored. Same for the others.

Where developing it I changed/edited things. To a avoid waiting a day for the results the switch NibeF1145_Reset_Calc is used. Setting it to on will delete the cache and recalculate the day before. It does not contain all the logic but enough (for me anyway) to quickly see if something worked. disadvantage, it does require some copied code you have to keep in synch.

Last is the measurement of the ground temperature. I use the 24hrs minimum brine returning temperature for that. Looking on the values there’s a pattern (it get lower it it’s running more frequent in the night because it takes time for the heat in the ground to transfer to the exchanger). It seems to be about a degree, that’s why I add that.

That’s the script. For the widget no major changes except all the popup graphs. And that is uses the “mode” to show the right images/icons. Only interesting part is the guess for the used power. It’s not complete but looking at my energy meters heating takes about 1350 watt, boiler heating 1950 watt and passive heating (running only the supply pomp at 30%) about 30 watts. So that is what I calculate with the minutes one of the modes it used in a day.

                - component: oh-list-item
                  config:
                    title: "= 'power   : '+Math.round(items.NibeF1145_Boiler_Min.state*1950/60+ items.NibeF1145_Heat_Min.state*1350/60+items.NibeF1145_Passive_Min.state*30/60) + ' W/day'"
                    style:

Full code for the rule

configuration: {}
triggers:
  - id: "3"
    configuration:
      cronExpression: 10 * * * * ? *
    type: timer.GenericCronTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >
        
        console.info("heatpump stats running ");


        //current time

        var now = time.ZonedDateTime.now();


        //group

        var grp="NibeF1145_";


        //reset derived info

        var reset=(items.getItem(grp+"Reset_Calc").state=='ON');

        if (reset){
          //reset if needed
          cache.remove("heatpump_data");
          console.info("resetting cache");  
          items.getItem(grp+"Reset_Calc").postUpdate("OFF");
        }


        //get data from cache (as string)

        var dataStr = cache.get("heatpump_data", () => (
          JSON.stringify(
            {
              lastHour:-1,
              hours:{
                heat:[],
                passive:[],
                cool:[],
                boiler:[]
              },
              last:{
                heat:0,
                passive:0,
                cool:0,
                boiler:0
              }
              
            }
          )

        ));


        //to object

        var data=JSON.parse(dataStr);



        //group and items

        var brineIn=items.getItem(grp+"EB100EP14BT10BrineinTemperature");

        var brineOut=items.getItem(grp+"EB100EP14BT11BrineoutTemperature");

        var supplyOut=items.getItem(grp+"BT2SupplyTempS1");

        var supplyPump=items.getItem(grp+"SupplyPumpSpeedEP14");

        var brinePump=items.getItem(grp+"EP14GP2BrinePumpSpeed");

        var degreeMin=items.getItem(grp+"DegreeMinutes16Bit");

        var room=items.getItem(grp+"BT50RoomTempS1");


        var brineInTemp=Number.parseFloat(brineIn.state).toFixed(1);

        var brineOutTemp=Number.parseFloat(brineOut.state).toFixed(1);

        var supplyOutTemp=Number.parseFloat(supplyOut.state.split(' ')[0]).toFixed(1); //get rid of unit

        var supplyPumpSpeed=Number.parseFloat(supplyPump.state.split(' ')[0]).toFixed(1);//get rid of unit

        var brinePumpSpeed=Number.parseFloat(brinePump.state.split(' ')[0]).toFixed(1);//get rid of unit

        var degreeMinValue=Number.parseFloat(degreeMin.state).toFixed(1);

        //also use previous value, it can be temporarely be 0

        var degreeMinOldValue=Number.parseFloat(degreeMin.previousState).toFixed(1);


        var roomTemp=Number.parseFloat(room.state).toFixed(1);



        //hotwater analyse, check if temp is lower than start heating for current mode

        var hwMode=items.getItem(grp+"HotWaterMode").state;

        var hwStart=items.getItem(grp+"StartTemperatureHWEconomy");

        if (hwMode==1){
          hwStart=items.getItem(grp+"StartTemperatureHWNormal");
        }else if (hwMode==2){
          hwStart=items.getItem(grp+"StartTemperatureHWLuxury");  
        }

        var hwCurrent=items.getItem(grp+"BT6HWLoad");

        var hwHeat=(Number.parseFloat(hwCurrent.state)-0.1)<=Number.parseFloat(hwStart.state);



        //what is heatpump doing?

        var action="off";

        var heatpumpMode=0;

        if (brinePumpSpeed==0 && supplyPumpSpeed==0){
          //both pumps off, so for sure doing nothing
          action="off";
          heatpumpMode=0;
        } else if (supplyOutTemp>=50){
          //50 degrees, so for sure heating water
          action="boiler";
          heatpumpMode=3;
        } else if (hwHeat && brinePumpSpeed!=0){
          //brine running and hot water too cold. Probably startup for heating it
          action="boiler";
          heatpumpMode=3;
        } else if ((degreeMinValue>0 || degreeMinOldValue>0) && brinePumpSpeed!=0){
          //brine running but degree minutes still positive. Probably startup for heating it
          action="boiler";
          heatpumpMode=3;
        } else if ((degreeMinValue!=0 || degreeMinOldValue!=0)  && brinePumpSpeed!=0){
          //degreeMinutes in use, brine running, active heating
          action="heating active";
          heatpumpMode=1;
        } else if (( degreeMinValue!=0 || degreeMinOldValue!=0) && brinePumpSpeed==0){
          //degreeMinutes in use, brine not running, passive heating
          action="heating passive";
          heatpumpMode=2;
        }else if (brineInTemp-brineOutTemp>=2){
          //2 degrees difference so active working. Probably heating up for water
          action="boiler";
          heatpumpMode=3;
        }else {
          //no degree minutes, small difference in brine temps. Cooling
          action="cooling";
          heatpumpMode=4;
        }


        items.getItem(grp+"Mode").postUpdate(heatpumpMode);


        //debug message

        console.info("val b-in=%d b-out=%d s-out=%d b-pmp=%d s-pmp=%d degr=%d hwStrt=%d hwMde=%d hwCur=%s actn=%s",brineInTemp,brineOutTemp,supplyOutTemp,brinePumpSpeed,supplyPumpSpeed,degreeMinValue,hwHeat,hwMode,hwCurrent.state,action);

        //console.info(JSON.stringify( data));



        //added (derived) items

        var groundTemp=items.getItem(grp+"Ground_Temp");

        var heatTemp=items.getItem(grp+"Heat_Temp");

        var boilerTemp=items.getItem(grp+"Boiler_Temp");

        var brineTemp=items.getItem(grp+"Brine_Temp");


        //check hour

        if (data.lastHour==-1){
          //first run
          console.info("init")

          /*The extra minute makes forces rr4j to report also the last hour in minutes.
          And makes thr data complete (last minute may not be persisted yet) */
          var yesterday=now.minusHours(24);

          //get lowest brine-in temp, i.e. return temp as guesstimate for ground temperature.
          var min=Number.parseFloat(brineIn.history.minimumSince(yesterday)).toFixed(1);
          groundTemp.postUpdate(min+1.0);
          
         
          //now fetch hour data, change to full hour period and go back one hour more (lose extra minute to keep last hour in 'minute mode')
          var start=yesterday.minusHours(1).minusMinutes(yesterday.minute()).minusMinutes(1);
          console.info("25 hours from "+start);
          
          //now run through the 25 hours
          for (let h=0;h<25;h++){
           
            //init period
            let end=start.plusHours(1); 
            let hour=start.hour();
         
            //reset data
            data.hours.heat[hour]=0;
            data.hours.cool[hour]=0;
            data.hours.boiler[hour]=0;
            data.hours.passive[hour]=0;
            
            let response=actions.HTTP.sendHttpGetRequest("http://hal9000:8080/rest/persistence/items/"+brinePump.name+"?starttime="+start.toLocalDateTime()+"&endtime="+end.toLocalDateTime());
            let brinePumpPoints=JSON.parse(response);
            response=actions.HTTP.sendHttpGetRequest("http://hal9000:8080/rest/persistence/items/"+supplyPump.name+"?starttime="+start.toLocalDateTime()+"&endtime="+end.toLocalDateTime());
            let supplyPumpPoints=JSON.parse(response);
            response=actions.HTTP.sendHttpGetRequest("http://hal9000:8080/rest/persistence/items/"+brineIn.name+"?starttime="+start.toLocalDateTime()+"&endtime="+end.toLocalDateTime());
            let brineInPoints=JSON.parse(response);
            response=actions.HTTP.sendHttpGetRequest("http://hal9000:8080/rest/persistence/items/"+brineOut.name+"?starttime="+start.toLocalDateTime()+"&endtime="+end.toLocalDateTime());
            let brineOutPoints=JSON.parse(response);
            response=actions.HTTP.sendHttpGetRequest("http://hal9000:8080/rest/persistence/items/"+supplyOut.name+"?starttime="+start.toLocalDateTime()+"&endtime="+end.toLocalDateTime());
            let supplyOutPoints=JSON.parse(response);
            response=actions.HTTP.sendHttpGetRequest("http://hal9000:8080/rest/persistence/items/"+degreeMin.name+"?starttime="+start.toLocalDateTime()+"&endtime="+end.toLocalDateTime());
            let degreeMinPoints=JSON.parse(response);

            if (brineInPoints.datapoints>70){
              console.info("!!!! to many datapoints "+brineInPoints.datapoints);
            }
            
            for (let i=0;i<60;i++){
              
              if (i<brineInPoints.datapoints){
                brineInTemp=Number.parseFloat(brineInPoints.data[i].state).toFixed(1);
              }
              if (i<brineOutPoints.datapoints){
                brineOutTemp=Number.parseFloat(brineOutPoints.data[i].state).toFixed(1);
              }
              if (i<supplyOutPoints.datapoints){
               supplyOutTemp=Number.parseFloat(supplyOutPoints.data[i].state.split(' ')[0]).toFixed(1); //get rid of unit
              }
              if (i<supplyPumpPoints.datapoints){
                supplyPumpSpeed=Number.parseFloat(supplyPumpPoints.data[i].state.split(' ')[0]).toFixed(1);//get rid of unit
              }
              if (i<brinePumpPoints.datapoints){
                brinePumpSpeed=Number.parseFloat(brinePumpPoints.data[i].state.split(' ')[0]).toFixed(1);//get rid of unit
              }
              if (i<degreeMinPoints.datapoints){
                degreeMinValue=Number.parseFloat(degreeMinPoints.data[i].state).toFixed(1);
              }
              
              //what is heatpump doing?
              action="off";
              if (brinePumpSpeed==0 && supplyPumpSpeed==0){
                //both pumps off, so for sure doing nothing
                action="off";
              } else if (supplyOutTemp>=50){
                //50 degrees, so for sure heating water
                action="boiler";
              } else if (degreeMinValue!=0 && brinePumpSpeed!=0){
                //degreeMinutes in use, brine running, active heating
                action="heating active";
              } else if (degreeMinValue!=0 && brinePumpSpeed==0){
                //degreeMinutes in use, brine not running, passive heating
                action="heating passive";
              }else if (brineInTemp-brineOutTemp>=2){
                //2 degrees difference so active working. Probably heating up for water
                action="boiler";
              }else {
                //no degree minutes, small difference in brine temps. Cooling
                action="cooling";        
              }

              if (action!="off"){
                //console.info(start,":",i,"action=",action);  
              }

              if (h==0){
                //first hour is 25 hours ago, this is for completing the running hour total
                if (action=="boiler"){
                  data.last.boiler++;
                } else if (action=="heating active"){
                  data.last.heat++;
                } else if (action=="heating passive"){
                  data.last.passive++;
                }else if (action=="cooling"){
                  data.last.cool++;
                }        
              }else {
                //24 hours data per hour
                if (action=="boiler"){
                  data.hours.boiler[hour]++;
                } else if (action=="heating active"){
                  data.hours.heat[hour]++;
                } else if (action=="heating passive"){
                  data.hours.passive[hour]++;
                }else if (action=="cooling"){
                  data.hours.cool[hour]++;
                }
              }     
              
            }
            
            start=start.plusHours(1);

          }
            
          //now switch to normal mode
          data.lastHour=now.hour();

            

          
        } else if (data.lastHour!=now.hour()){
          //next hour

          var yesterday=now.minusHours(24).minusMinutes(1);
          var min=Number.parseFloat(brineIn.history.minimumSince(yesterday)).toFixed(1);
          groundTemp.postUpdate(min+1.0);

          //advance period
          data.lastHour=now.hour();
          
          //keep data of erased hour
          data.last.heat=data.hours.heat[data.lastHour];
          data.last.cool=data.hours.cool[data.lastHour];
          data.last.boiler=data.hours.boiler[data.lastHour];
          data.last.passive=data.hours.passive[data.lastHour];
          
          //reset data for new hour
          data.hours.heat[data.lastHour]=0;
          data.hours.cool[data.lastHour]=0;
          data.hours.boiler[data.lastHour]=0;
          data.hours.passive[data.lastHour]=0;
          
          //add data for first minute
          if (action=="boiler"){ 
            data.hours.boiler[now.hour()]++;
          } else if (action=="heating active"){
            data.hours.heat[now.hour()]++;
          } else if (action=="heating passive"){
            data.hours.passive[now.hour()]++;
          }else if (action=="cooling"){
            data.hours.cool[now.hour()]++;
          }  

          

          
        } else {
          //same hour
          
          if (action=="boiler"){ 
            data.hours.boiler[now.hour()]++;
          } else if (action=="heating active"){
            data.hours.heat[now.hour()]++;
          } else if (action=="heating passive"){
            data.hours.passive[now.hour()]++;
          }else if (action=="cooling"){
            data.hours.cool[now.hour()]++;
          }  

         
        }



        //now calculate 24 hours totals

        var heatMin=0;

        var passiveMin=0;

        var boilerMin=0;

        var coolMin=0;


        for (let h=0;h<24;h++){
          
          heatMin+=data.hours.heat[h];
          passiveMin+=data.hours.passive[h];
          boilerMin+=data.hours.boiler[h];
          coolMin+=data.hours.cool[h];
        }


        //correct missing part of current hour

        var m=now.minute();

        heatMin+=Math.round(data.last.heat*(59-m)/60);

        passiveMin+=Math.round(data.last.passive*(59-m)/60);

        boilerMin+=Math.round(data.last.boiler*(59-m)/60);

        coolMin+=Math.round(data.last.cool*(59-m)/60);



        items.getItem(grp+"Heat_Min").postUpdate(heatMin);

        items.getItem(grp+"Passive_Min").postUpdate(passiveMin);

        items.getItem(grp+"Boiler_Min").postUpdate(boilerMin);

        items.getItem(grp+"Cool_Min").postUpdate(coolMin);


        //calculate temps (only when running)


        //supply heat room

        if (heatpumpMode==1 && supplyOutTemp>=roomTemp) {
          //heating room
          heatTemp.postUpdate(supplyOutTemp);
        //} else if (heatpumpMode==2 && supplyOutTemp>=roomTemp && supplyOutTemp<=roomTemp+10) {

        //  //heating room (ignore overshoot after boiler heating)

        //  heatTemp.postUpdate(supplyOutTemp);

        } else {
          //not running, assume roomtemp
          heatTemp.postUpdate(roomTemp);
        }


        //supply boiler

        if (heatpumpMode==3 && supplyOutTemp>=Number.parseFloat(hwStart.state)){
          //heating boiler
          boilerTemp.postUpdate(supplyOutTemp);
        } else {
          //not running, assume treshold heating
          boilerTemp.postUpdate(Number.parseFloat(hwStart.state).toFixed(1));
        }



        //brine

        if ((heatpumpMode==1 || heatpumpMode==3 || heatpumpMode==4) && brineOutTemp<Number.parseFloat(groundTemp.state)) {
          //running
          brineTemp.postUpdate(brineOutTemp);
        } else {
          //not running, assume groundtemp
          brineTemp.postUpdate(groundTemp.state);
        }



        //keep data

        cache.put("heatpump_data",JSON.stringify(data));



        console.info("heatpump stats done in "+ (-now.getMillisFromNow())+" millisec");
    type: script.ScriptAction

Full code for the widget

uid: widget_heatpump2
tags: []
props:
  parameters:
    - context: item
      default: ""
      description: Heatpump group
      label: Heatpump group
      name: heatpump
      required: false
      type: TEXT
    - context: item
      default: _BT50RoomTempS1
      description: Item with room temperature
      label: Room Temperature
      name: roomTemp
      required: false
      type: TEXT
    - context: item
      default: _BT1OutdoorTemperature
      description: Item with outside temperature
      label: Outside Temperature
      name: outTemp
      required: false
      type: TEXT
    - context: item
      default: _EP14GP2BrinePumpSpeed
      description: Item with Brine Pump Speed (percentage)
      label: Brine Pump
      name: brinePump
      required: false
      type: TEXT
    - context: item
      default: _EB100EP14BT10BrineinTemperature
      description: Item with Brine In (return to heatpump, usualy warm side)
      label: Brine In
      name: brineIn
      required: false
      type: TEXT
    - context: item
      default: _EB100EP14BT11BrineoutTemperature
      description: Item with Brine Out (leaving heatpump, usualy cold side)
      label: Brine Out
      name: brineOut
      required: false
      type: TEXT
    - context: item
      default: _SupplyPumpSpeedEP14
      description: Item with Supply Pump Speed (percentage)
      label: Supply Pump
      name: supplyPump
      required: false
      type: TEXT
    - context: item
      default: _EB100EP14BT3ReturnTemp
      description: Item with Supply In (return to heatpump, usualy cold side)
      label: Supply In
      name: supplyIn
      required: false
      type: TEXT
    - context: item
      default: _BT2SupplyTempS1
      description: Item with Supply Out (leaving heatpump, usualy warm side)
      label: Supply Out
      name: supplyOut
      required: false
      type: TEXT
    - context: item
      default: _BT7HWTop
      description: Item with boiler temperature
      label: Boiler Temperature
      name: boilerWaterTemp
      required: false
      type: TEXT
    - context: item
      default: _Ground_Temp
      description: Item with ground
      label: Ground Temperature
      name: groundTemp
      required: false
      type: TEXT
    - context: item
      default: _Brine_Temp
      description: Item with (corrected) brine temp
      label: Brine Temperature
      name: brineTemp
      required: false
      type: TEXT
    - context: item
      default: _Heat_Temp
      description: Item with (filtered) heat temp
      label: Heat Temperature
      name: heatTemp
      required: false
      type: TEXT
    - context: item
      default: _Boiler_Temp
      description: Item with (filtered) boiler temp
      label: Boiler Temperature
      name: boilerTemp
      required: false
      type: TEXT
    - context: item
      default: _Mode
      description: Item with derived heatpump mode (0-5)
      label: Heatpump mode
      name: mode
      required: false
      type: TEXT
  parameterGroups: []
timestamp: Sep 27, 2022, 6:30:45 PM
component: f7-card
config: {}
slots:
  default:
    - component: f7-badge
      config:
        bgColor: rgba(0,0,0,0.0)
        style:
          background-image: url(/static/nibe-ground.png)
          background-size: 300px
          border-radius: 12px
          height: 185px
          left: 20px
          position: absolute
          top: 100px
          width: 267px
    - component: f7-badge
      config:
        bgColor: rgba(0,0,0,0.0)
        style:
          background-image: url(/static/nibe-house.png)
          background-size: 220px
          border-radius: 12px
          height: 220px
          left: 42px
          position: absolute
          top: 5px
          width: 220px
    - component: f7-badge
      config:
        bgColor: rgba(0,0,0,0.0)
        visible: true
        style:
          background-image: url(/static/nibe-heatpump.png)
          background-size: 28px
          border-radius: 0px
          height: 50px
          left: 210px
          position: absolute
          top: 152px
          width: 28px
    - component: f7-badge
      config:
        bgColor: rgba(0,0,0,0.0)
        style:
          background-image: url(/static/nibe-boiler.png)
          background-size: 28px
          border-radius: 0px
          height: 20px
          left: 210px
          position: absolute
          top: 104px
          width: 28px
    - component: f7-icon
      config:
        comment: window icon
        f7: "= items[props.heatpump + props.mode].state==0 ? 'pause_fill' : items[props.heatpump + props.mode].state==1 ? 'flame_fill' : items[props.heatpump + props.mode].state==2 ? 'flame' : items[props.heatpump + props.mode].state==3 ? 'drop_fill' : 'snow' "
        size: 27px
        style:
          position: absolute
          left: 164px
          top: 69px
          color: rgb(78,103,105)
    - component: f7-badge
      config:
        bgColor: rgba(0,0,0,0.0)
        visible: true
        style:
          comment: room exchanger
          background-image: "= items[props.heatpump + props.mode].state==1 || items[props.heatpump + props.mode].state==2 ? 'url(/static/nibe-supply-heat.png)' : items[props.heatpump + props.mode].state==4 ? 'url(/static/nibe-supply-cool.png)' : 'url(/static/nibe-supply-off.png)'  "
          background-size: 111px
          border-radius: 0px
          height: 29px
          left: 90px
          position: absolute
          top: 175px
          width: 111px
    - component: f7-badge
      config:
        bgColor: rgba(0,0,0,0.0)
        visible: true
        style:
          comment: brine exchanger
          background-image: "=items[props.heatpump + props.brinePump].state=='0 %' ? 'url(/static/nibe-brine-off.png)' :  Number.parseFloat(items[props.heatpump + props.brineIn].state) - Number.parseFloat(items[props.heatpump + props.brineOut].state) <=1  ? 'url(/static/nibe-brine-cool.png)' :  'url(/static/nibe-brine-heat.png)'  "
          background-size: 44px
          border-radius: 0px
          height: 77px
          left: 203px
          position: absolute
          top: 205px
          width: 44px
    - component: f7-badge
      config:
        bgColor: rgba(0,0,0,0.0)
        visible: true
        style:
          comment: boiler exchanger
          background-image1: "= items[props.heatpump + props.supplyPump].state!='0 %' && Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])>= 50 ? 'url(/static/nibe-boiler-heat.png)' : 'url(/static/nibe-boiler-off.png)' "
          background-image: "=items[props.heatpump + props.mode].state==3 ? 'url(/static/nibe-boiler-heat.png)' : 'url(/static/nibe-boiler-off.png)' "
          background-size: 35px
          border-radius: 0px
          height: 28px
          left: 207px
          position: absolute
          top: 124px
          width: 34px
    - component: Label
      config:
        text: = Number.parseFloat(items[props.heatpump + props.roomTemp].state).toFixed(1)
        style:
          font-size: 18px
          color: white
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          text-align: center
          position: absolute
          top: 110px
          left: 150px
          width: 50px
    - component: oh-link
      config:
        action: popover
        popoverOpen: ='.outside.info'
        text: = Number.parseFloat(items[props.heatpump +props.outTemp].state).toFixed(1)
        style:
          font-size: 18px
          color: gray
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          position: absolute
          top: 10px
          left: 20px
    - component: Label
      config:
        text: = (Number.parseFloat(items[props.heatpump +props.groundTemp].state)).toFixed(1)
        style:
          font-size: 18px
          color: rgb(60,40,5)
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          position: absolute
          top: 255px
          left: 105px
    - component: Label
      config:
        comment: left heat
        text: "= items[props.heatpump + props.mode].state==1 || items[props.heatpump + props.mode].state==2 ? Math.max(Number.parseFloat(items[props.heatpump + props.supplyIn].state),Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])).toFixed(1) : items[props.heatpump + props.mode].state==4 ? Math.min(Number.parseFloat(items[props.heatpump + props.supplyIn].state),Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])).toFixed(1) : '' "
        style:
          color: "= items[props.heatpump + props.mode].state==1 || items[props.heatpump + props.mode].state==2 ? 'red' : items[props.heatpump + props.mode].state==4 ? 'blue' : 'rgb(95,99,49)'  "
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          position: absolute
          top: 178px
          left: 95px
    - component: oh-link
      config:
        comment: middle heat
        action: popover
        popoverOpen: ='.heat.info'
        text: "=items[props.heatpump + props.mode].state==1 || items[props.heatpump + props.mode].state==2 || items[props.heatpump + props.mode].state==4 ?  Math.round(items[props.heatpump + props.supplyPump].state.split('%')[0])+'%' :  'OFF' "
        style:
          color: "= items[props.heatpump + props.mode].state==1 || items[props.heatpump + props.mode].state==2 ? 'red' : items[props.heatpump + props.mode].state==4 ? 'blue' : 'rgb(95,99,49)'  "
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          position: absolute
          top: 178px
          left: 125px
          width: 40px
          text-align: center
    - component: Label
      config:
        comment: right heat
        text: "= items[props.heatpump + props.mode].state==1 || items[props.heatpump + props.mode].state==2 ? Math.min(Number.parseFloat(items[props.heatpump + props.supplyIn].state),Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])).toFixed(1) : items[props.heatpump + props.mode].state==4 ? Math.max(Number.parseFloat(items[props.heatpump + props.supplyIn].state),Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0])).toFixed(1) : '' "
        style:
          color: "= items[props.heatpump + props.mode].state==1 || items[props.heatpump + props.mode].state==2 ? 'blue' : items[props.heatpump + props.mode].state==4 ? 'red' : 'rgb(95,99,49)'  "
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          position: absolute
          top: 178px
          left: 161px
          text-align: right
          width: 35px
    - component: Label
      config:
        comment: top brine
        text: "=items[props.heatpump + props.mode].state==1 || items[props.heatpump + props.mode].state==3 ? Math.min( Number.parseFloat(items[props.heatpump + props.brineOut].state),Number.parseFloat(items[props.heatpump + props.brineIn].state)).toFixed(1) : items[props.heatpump + props.mode].state==4 ?  Math.max( Number.parseFloat(items[props.heatpump + props.brineOut].state),Number.parseFloat(items[props.heatpump + props.brineIn].state)).toFixed(1) : '' "
        style:
          color: "=items[props.heatpump + props.mode].state==1 || items[props.heatpump + props.mode].state==3 ? 'blue' : items[props.heatpump + props.mode].state==4 ?  'red' : 'rgb(64,66,33)' "
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          position: absolute
          text-align: center
          top: 210px
          left: 199px
          width: 50px
    - component: oh-link
      config:
        comment: middle brine
        action: popover
        popoverOpen: ='.brine.info'
        text: "=items[props.heatpump + props.brinePump].state=='0 %' ? 'OFF' : Math.round(items[props.heatpump + props.brinePump].state.split('%')[0])+'%' "
        style:
          color: "=items[props.heatpump + props.mode].state==1 || items[props.heatpump + props.mode].state==3 ? 'red' : items[props.heatpump + props.mode].state==4 ?  'blue' : 'rgb(64,66,33)' "
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          text-align: center
          position: absolute
          width: 50px
          top: 230px
          left: 199px
    - component: Label
      config:
        comment: lower brine
        text: "=items[props.heatpump + props.mode].state==1 || items[props.heatpump + props.mode].state==3 ? Math.max( Number.parseFloat(items[props.heatpump + props.brineOut].state),Number.parseFloat(items[props.heatpump + props.brineIn].state)).toFixed(1) : items[props.heatpump + props.mode].state==4 ?  Math.min( Number.parseFloat(items[props.heatpump + props.brineOut].state),Number.parseFloat(items[props.heatpump + props.brineIn].state)).toFixed(1) : '' "
        style:
          color: "=items[props.heatpump + props.mode].state==1 || items[props.heatpump + props.mode].state==3 ? 'red' : items[props.heatpump + props.mode].state==4 ?  'blue' : 'rgb(64,66,33)' "
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          text-align: center
          position: absolute
          top: 255px
          left: 199px
          width: 50px
    - component: Label
      config:
        text: =Number.parseFloat(items[props.heatpump + props.boilerWaterTemp].state).toFixed(1)
        style:
          color: black
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          position: absolute
          top: 104px
          left: 212px
    - component: oh-link
      config:
        comment: boiler heating
        action: popover
        popoverOpen: ='.boiler.info'
        text: "=items[props.heatpump + props.mode].state==3 ? Number.parseFloat(items[props.heatpump + props.supplyOut].state.split(' ')[0]).toFixed(1) : 'OFF'"
        style:
          color: "=items[props.heatpump + props.mode].state==3 ? 'red' : 'rgb(64,66,33)' "
          --f7-card-bg-color: rgba(0,0,0,0.0)
          --f7-card-box-shadow: none
          text-align: center
          position: absolute
          top: 126px
          left: 199px
          width: 50px
    - component: oh-button
      config:
        action: popover
        iconF7: info_circle
        iconSize: 20px
        iconColor1: "=rgb(64,66,33) "
        popoverOpen: ='.heatpump.info'
        style:
          position: absolute
          top: 156px
          left: 202px
          color: rgb(64,66,33)
    - component: f7-popover
      config:
        class: = 'heatpump info'
        style:
          --f7-popover-bg-color: rgb(50,50,25)
      slots:
        default:
          - component: f7-list
            config:
              simpleList: true
              xsmallInset: true
              style:
                background-color: rgb(64,66,33)
                padding: 5px 5px
                marginx: 5px 5px
                overflow: hidden
                color: gray
                border: none
            slots:
              default:
                - component: oh-list-item
                  config:
                    title: "= 'boiler  : '+items.NibeF1145_Boiler_Min.state+ ' min/day'"
                    style:
                      border-style: none
                      padding: 5px 2px
                      height: 15px
                      margin: 0px 0px
                - component: oh-list-item
                  config:
                    title: "= 'heating : '+items.NibeF1145_Heat_Min.state+ ' min/day'"
                    style:
                      padding: 5px 2px
                      height: 15px
                - component: oh-list-item
                  config:
                    title: "= 'passive : '+items.NibeF1145_Passive_Min.state+ ' min/day'"
                    style:
                      padding: 5px 2px
                      height: 15px
                - component: oh-list-item
                  config:
                    title: "= 'cooling : '+items.NibeF1145_Cool_Min.state+ ' min/day'"
                    style:
                      padding: 5px 2px
                      height: 15px
                - component: oh-list-item
                  config:
                    title: "= 'power   : '+Math.round(items.NibeF1145_Boiler_Min.state*1950/60+ items.NibeF1145_Heat_Min.state*1350/60+items.NibeF1145_Passive_Min.state*30/60) + ' W/day'"
                    style:
                      padding: 5px 2px
                      height: 15px
    - component: f7-popover
      config:
        class: = 'brine info'
        style:
          --f7-popover-bg-color: rgb(50,50,25)
      slots:
        default:
          - component: oh-chart
            config:
              height: 150px
              width1: 400px
              options:
                backgroundColor: transparent
              period: D
              periodVisible: false
              stylesheet: >
                .oh-chart {
                  padding: 0px!important;
                  inset: 0px!important;
                  margin: 0px!important;
                }
            slots:
              grid:
                - component: oh-chart-grid
                  config:
                    height: 67%
                    left: 35px
                    right: 10px
                    top: 25px
              series:
                - component: oh-time-series
                  config:
                    areaStyle1: {}
                    item: =props.heatpump+props.brineTemp
                    itemStyle:
                      color: rgb(50, 200, 50)
                    name: Temperature
                    type: line
              tooltip:
                - component: oh-chart-tooltip
                  config:
                    trigger: axis
              xAxis:
                - component: oh-time-axis
                  config:
                    axisLabel:
                      show: true
                    boundaryGap: false
                    gridIndex: 0
                    splitNumber: 3
                    type: time
              yAxis:
                - component: oh-value-axis
                  config:
                    axisLabel:
                      formatter: "{value}C"
                      show: true
                    axisTick:
                      inside: true
                    boundaryGap:
                      - 0%
                      - 0%
                    gridIndex: 0
                    splitLine: true
                    type: value
                    scale: false
              title:
                - component: oh-chart-title
                  config:
                    left: center
                    show: true
                    top: -10px
                    subtext: Brine temperature
                    fontSize: 10px
                    style:
                      font-size: 10px
    - component: f7-popover
      config:
        class: = 'heat info'
        style:
          --f7-popover-bg-color: rgb(50,50,25)
      slots:
        default:
          - component: oh-chart
            config:
              height: 150px
              width1: 400px
              options:
                backgroundColor: transparent
              period: D
              periodVisible: false
              stylesheet: >
                .oh-chart {
                  padding: 0px!important;
                  inset: 0px!important;
                  margin: 0px!important;
                }
            slots:
              grid:
                - component: oh-chart-grid
                  config:
                    height: 67%
                    left: 35px
                    right: 10px
                    top: 25px
              series:
                - component: oh-time-series
                  config:
                    areaStyle1: {}
                    item: =props.heatpump+props.heatTemp
                    itemStyle:
                      color: rgb(50, 200, 50)
                    name: Temperature
                    type: line
              tooltip:
                - component: oh-chart-tooltip
                  config:
                    trigger: axis
              xAxis:
                - component: oh-time-axis
                  config:
                    axisLabel:
                      show: true
                    boundaryGap: false
                    gridIndex: 0
                    splitNumber: 3
                    type: time
              yAxis:
                - component: oh-value-axis
                  config:
                    axisLabel:
                      formatter: "{value}C"
                      show: true
                    axisTick:
                      inside: true
                    boundaryGap:
                      - 0%
                      - 0%
                    gridIndex: 0
                    splitLine: true
                    type: value
                    scale: false
              title:
                - component: oh-chart-title
                  config:
                    left: center
                    show: true
                    top: -10px
                    subtext: Heating temperature
                    fontSize: 10px
                    style:
                      font-size: 10px
    - component: f7-popover
      config:
        class: = 'boiler info'
        style:
          --f7-popover-bg-color: rgb(50,50,25)
      slots:
        default:
          - component: oh-chart
            config:
              height: 150px
              width1: 400px
              options:
                backgroundColor: transparent
              period: D
              periodVisible: false
              stylesheet: >
                .oh-chart {
                  padding: 0px!important;
                  inset: 0px!important;
                  margin: 0px!important;
                }
            slots:
              grid:
                - component: oh-chart-grid
                  config:
                    height: 67%
                    left: 35px
                    right: 10px
                    top: 25px
              series:
                - component: oh-time-series
                  config:
                    areaStyle1: {}
                    item: =props.heatpump+props.boilerTemp
                    itemStyle:
                      color: rgb(50, 200, 50)
                    name: Temperature
                    type: line
              tooltip:
                - component: oh-chart-tooltip
                  config:
                    trigger: axis
              xAxis:
                - component: oh-time-axis
                  config:
                    axisLabel:
                      show: true
                    boundaryGap: false
                    gridIndex: 0
                    splitNumber: 3
                    type: time
              yAxis:
                - component: oh-value-axis
                  config:
                    axisLabel:
                      formatter: "{value}C"
                      show: true
                    axisTick:
                      inside: true
                    boundaryGap:
                      - 0%
                      - 0%
                    gridIndex: 0
                    splitLine: true
                    type: value
                    scale: false
              title:
                - component: oh-chart-title
                  config:
                    left: center
                    show: true
                    top: -10px
                    subtext: Boiler heating
                    fontSize: 10px
                    style:
                      font-size: 10px
    - component: f7-popover
      config:
        class: = 'outside info'
        style:
          --f7-popover-bg-color: rgb(50,50,25)
      slots:
        default:
          - component: oh-chart
            config:
              height: 150px
              width1: 400px
              options:
                backgroundColor: transparent
              period: D
              periodVisible: false
              stylesheet: >
                .oh-chart {
                  padding: 0px!important;
                  inset: 0px!important;
                  margin: 0px!important;
                }
            slots:
              grid:
                - component: oh-chart-grid
                  config:
                    height: 67%
                    left: 35px
                    right: 10px
                    top: 25px
              series:
                - component: oh-time-series
                  config:
                    areaStyle1: {}
                    item: =props.heatpump+props.outTemp
                    itemStyle:
                      color: rgb(50, 200, 50)
                    name: Temperature
                    type: line
              tooltip:
                - component: oh-chart-tooltip
                  config:
                    trigger: axis
              xAxis:
                - component: oh-time-axis
                  config:
                    axisLabel:
                      show: true
                    boundaryGap: false
                    gridIndex: 0
                    splitNumber: 3
                    type: time
              yAxis:
                - component: oh-value-axis
                  config:
                    axisLabel:
                      formatter: "{value}C"
                      show: true
                    axisTick:
                      inside: true
                    boundaryGap:
                      - 0%
                      - 0%
                    gridIndex: 0
                    splitLine: true
                    type: value
                    scale: false
              title:
                - component: oh-chart-title
                  config:
                    left: center
                    show: true
                    top: -10px
                    subtext: Outside temperature
                    fontSize: 10px
                    style:
                      font-size: 10px