Topic/Latest version moved to UI Widgets !!!
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
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.
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.
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).
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.
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.
Last but not least, if none of the above applies (but both pumps are running) it must be cooling.
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 ). 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