Hi,
Bit of a strange one that has taken me a long time to figure out what was going on.
I have a Tado heating system which is controlled by a schedule in the Tado app, and I wrote a rule in Openhab to turn the radiators off and on when doors and windows are opened and closed.
I thought there was a problem in my code somewhere because I would often look at my heating controls page of my UI and some radiators would not be on schedule.
I thought I wasn’t sending back correct values when the doors or windows would close, but that’s not what’s happening.
If any radiator has a non-whole number for Target Temperature (eg 19.7 C), when I load the UI page these non-whole number items get a command sent to them and the target temperature changes to the nearest whole number (eg 20 C). If I set everything to SCHEDULE, then reload the page, it instantly sends commands to any TRV without a whole number Target Temperature. There is no other info in the logs.
Any ideas?
Here is my UI widget code
uid: widget_heating-TRVControls
tags: []
props:
parameters:
- description: Location
label: Location
name: location
required: false
type: TEXT
- context: item
label: Humidity
name: humid
required: false
type: TEXT
- context: item
label: Temperature
name: temp
required: false
type: TEXT
- context: item
label: Target Temperature
name: targetTemp
required: false
type: TEXT
- context: item
label: Heating Power
name: heatP
required: false
type: TEXT
- context: item
label: Zone Operation Mode
name: zoneOp
required: false
type: TEXT
- context: item
label: HVAC Mode
name: hvac
required: false
type: TEXT
- context: item
label: Timer Duration
name: timerD
required: false
type: TEXT
- context: item
label: Overlay End Time
name: oETime
required: false
type: TEXT
- context: item
label: Battery Low Alarm
name: battery
required: false
type: TEXT
- context: item
label: Contact Sensor 1
name: open1
required: false
type: TEXT
- context: item
label: Contact Sensor 2
name: open2
required: false
type: TEXT
timestamp: Nov 28, 2021, 1:20:01 PM
component: f7-card
config:
backdrop: true
class:
- card-expandable-animate-width
expandable: true
style:
background: '=(Number.parseFloat(items[props.temp].state) >= 24) ? "linear-gradient(to bottom, hsla(3, 100%, 59%, 1), hsla(3, 100%, 95%, 1), 10% , hsla(0, 0%, 100%, 1) )":
(Number.parseFloat(items[props.temp].state) >= 23) ? "linear-gradient(to bottom, hsla(349, 100%, 59%, 1), hsla(349, 100%, 95%, 1), 10% , hsla(0, 0%, 100%, 1) )":
(Number.parseFloat(items[props.temp].state) >= 22) ? "linear-gradient(to bottom, hsla(20, 100%, 57%, 1), hsla(20, 100%, 95%, 1), 10% , hsla(0, 0%, 100%, 1) )":
(Number.parseFloat(items[props.temp].state) >= 20) ? "linear-gradient(to bottom, hsla(35, 100%, 50%, 1), hsla(35, 100%, 95%, 1), 10% , hsla(0, 0%, 100%, 1) )":
(Number.parseFloat(items[props.temp].state) >= 19) ? "linear-gradient(to bottom, hsla(48, 100%, 50%, 1), hsla(48, 100%, 95%, 1), 10% , hsla(0, 0%, 100%, 1) )":
(Number.parseFloat(items[props.temp].state) >= 18) ? "linear-gradient(to bottom, hsla(66, 70%, 54%, 1), hsla(66, 70%, 95%, 1), 10% , hsla(0, 0%, 100%, 1) )":
(Number.parseFloat(items[props.temp].state) >= 17) ? "linear-gradient(to bottom, hsla(130, 65%, 57%, 1), hsla(130, 65%, 95%, 1), 10% , hsla(0, 0%, 100%, 1) )":
(Number.parseFloat(items[props.temp].state) > 0) ? "linear-gradient(to bottom, hsla(207, 90%, 54%, 1), hsla(207, 90%, 95%, 1), 10% , hsla(0, 0%, 100%, 1) )":
"white"'
height: 170px
margin: 5px
margin-top: 10px
swipeToClose: true
slots:
default:
- component: oh-button
config:
class:
- cell-open-button
- card-opened-fade-out
color: '=(Number.parseFloat(items[props.temp].state) >= 24) ? "red":
(Number.parseFloat(items[props.temp].state) >= 23) ? "pink":
(Number.parseFloat(items[props.temp].state) >= 22) ? "deeporange":
(Number.parseFloat(items[props.temp].state) >= 20) ? "orange":
(Number.parseFloat(items[props.temp].state) >= 19) ? "yellow":
(Number.parseFloat(items[props.temp].state) >= 18) ? "lime":
(Number.parseFloat(items[props.temp].state) >= 17) ? "green":
(Number.parseFloat(items[props.temp].state) > 0) ? "blue":
"white"'
iconF7: ellipsis
iconSize: 30px
style:
padding-bottom: 40px
padding-right: 15px
padding-top: 10px
position: absolute
right: 0
top: 0
z-index: 999
- component: f7-card-content
config:
style:
padding-top: 30px
slots:
default:
- component: f7-icon
config:
color: '=(items[props.open1].state == "OPEN" || items[props.open2].state == "OPEN") ? "lightblue" :
(items[props.heatP].state >= 80) ? "red":
(items[props.heatP].state >= 50) ? "orange":
(items[props.heatP].state >= 25) ? "yellow":
(items[props.heatP].state > 0) ? "blue":
(items[props.heatP].state == 0) ? "black":
"white"'
material: '=(items[props.open1].state == "OPEN" || items[props.open2].state == "OPEN") ? "sensor_window" :
(items[props.heatP].state > 0 ) ? "local_fire_department":
(items[props.hvac].state == "OFF") ? "power_settings_new": ""'
size: '=(items[props.open1].state == "OPEN" || items[props.open2].state == "OPEN") ? "48px" : (items[props.heatP].state == 0) ? "50px" : 20 + +items[props.heatP].state * 0.45 + "px"'
style:
opacity: 100%
position: absolute
right: '=(items[props.heatP].state > 0 ) ? 35 + -items[props.heatP].state * 0.2 + "px": (items[props.open1].state == "OPEN" || items[props.open2].state == "OPEN") ? "16px" : "15px"'
top: '=(items[props.heatP].state > 0 ) ? 125 + -items[props.heatP].state * 0.4 + "px": (items[props.open1].state == "OPEN" || items[props.open2].state == "OPEN") ? "95px" : "95px"'
- component: f7-chip
config:
color: '=(items[props.open1].state == "OPEN" || items[props.open2].state == "OPEN") ? "lightblue" :
(items[props.heatP].state >= 80) ? "red":
(items[props.heatP].state >= 50) ? "orange":
(items[props.heatP].state >= 25) ? "yellow":
(items[props.heatP].state > 0) ? "blue":
(items[props.heatP].state == 0) ? "black":
"white"'
style:
align: center
position: absolute
right: 20px
top: 140px
text: '=(items[props.heatP].state >= 0) ? items[props.heatP].displayState : "error"'
- component: oh-button
config:
class:
- card-opened-fade-in
- cell-close-button
- card-close
color: '=(Number.parseFloat(items[props.temp].state) >= 24) ? "red":
(Number.parseFloat(items[props.temp].state) >= 23) ? "pink":
(Number.parseFloat(items[props.temp].state) >= 22) ? "deeporange":
(Number.parseFloat(items[props.temp].state) >= 20) ? "orange":
(Number.parseFloat(items[props.temp].state) >= 19) ? "yellow":
(Number.parseFloat(items[props.temp].state) >= 18) ? "lime":
(Number.parseFloat(items[props.temp].state) >= 17) ? "green":
(Number.parseFloat(items[props.temp].state) > 0) ? "blue":
"white"'
iconF7: xmark_circle_fill
iconSize: 30px
style:
padding-bottom: 35px
padding-top: 10px
position: absolute
right: 0
shadow: true
top: 0
z-index: 999
- component: oh-link
config:
class:
- cell-open-button
- card-opened-fade-out
- justify-content-flex-end
style:
height: auto
left: 0
position: absolute
top: 0
width: auto
z-index: 0
- component: f7-block
config:
class:
- no-padding
style:
height: 200px
margin: 0px
slots:
default:
- component: f7-row
config:
style:
flex-wrap: nowrap
height: auto
white-space: nowrap
slots:
default:
- component: f7-col
slots:
default:
- component: Label
config:
style:
font-weight: 600
margin-bottom: 8px
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
text: =props.location
- component: oh-gauge
config:
borderColor: '=(Number.parseFloat(items[props.temp].state) >= 24 ) ? "#ff3b30":
(Number.parseFloat(items[props.temp].state) >= 23 ) ? "#ff2d55":
(Number.parseFloat(items[props.temp].state) >= 22 ) ? "#ff6b22":
(Number.parseFloat(items[props.temp].state) >= 20 ) ? "#ff9500":
(Number.parseFloat(items[props.temp].state) >= 19 ) ? "#ffcc00":
(Number.parseFloat(items[props.temp].state) >= 18 ) ? "#cddc39":
(Number.parseFloat(items[props.temp].state) >= 17 ) ? "#4cd964":
(Number.parseFloat(items[props.temp].state) > 0) ? "#2196f3":
"white"'
borderWidth: "15"
item: =props.temp
max: 26
min: 12
size: 87
style:
margin-bottom: 0px
type: semicircle
valueFontSize: 12
valueFontWeight: bold
- component: f7-row
config:
style:
height: auto
margin-bottom: 5px
margin-left: -2px
margin-top: -5px
overflow: hidden
visible: true
slots:
default:
- component: f7-col
config: {}
slots:
default:
- component: f7-row
config:
class:
- justify-content-flex-start
style:
flex-wrap: nowrap
slots:
default:
- component: f7-icon
config:
color: '=(items[props.open1].state == "OPEN" || items[props.open2].state == "OPEN" || items[props.targetTemp].state == "NULL") ? "black" :
(Number.parseFloat(items[props.targetTemp].state) >= 24) ? "red":
(Number.parseFloat(items[props.targetTemp].state) >= 23) ? "pink":
(Number.parseFloat(items[props.targetTemp].state) >= 22) ? "deeporange":
(Number.parseFloat(items[props.targetTemp].state) >= 20) ? "orange":
(Number.parseFloat(items[props.targetTemp].state) >= 19) ? "yellow":
(Number.parseFloat(items[props.targetTemp].state) >= 18) ? "lime":
(Number.parseFloat(items[props.targetTemp].state) >= 17) ? "green":
(Number.parseFloat(items[props.targetTemp].state) > 1) ? "blue":
"gray"'
f7: '=(items[props.open1].state == "OPEN" || items[props.open2].state == "OPEN" || items[props.targetTemp].state == "NULL") ? "power" :
"arrow_up_to_line"'
size: 18px
visible: '=(items[props.open1].state == "OPEN" || items[props.open2].state == "OPEN") ? "false" : "true"'
- component: Label
config:
style:
color: gray
font-size: 12px
margin-left: '=(items[props.open1].state == "OPEN" || items[props.open2].state == "OPEN") ? "3px" : "13px"'
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
text: '=(items[props.open1].state == "OPEN" || items[props.open2].state == "OPEN") ? "OPEN WINDOW" :
(items[props.targetTemp].state == "NULL") ? "OFF": items[props.targetTemp].state'
- component: f7-row
config:
class:
- justify-content-flex-start
style:
flex-wrap: nowrap
height: auto
white-space: nowrap
visible: '=(items[props.timerD].state > 0) ? "true": "false"'
slots:
default:
- component: f7-icon
config:
color: blue
f7: timer
size: 20px
- component: Label
config:
style:
color: gray
font-size: 12px
margin-left: 6px
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
text: =items[props.timerD].displayState
- component: f7-block
config:
class:
- card-prevent-open
- card-content-padding
style:
width: auto
slots:
default:
- component: oh-list
config: {}
slots:
default:
- component: oh-list-item
config:
action: toggle
actionCommand: HEAT
actionCommandAlt: OFF
actionItem: =props.hvac
badge: '=(items[props.hvac].state == "HEAT") ? "ON" : "OFF"'
badge-color: '=(items[props.hvac].state == "HEAT") ? "green" : "red"'
icon: f7:power
iconColor: '=(items[props.hvac].state == "HEAT") ? "green": "red"'
title: On/Off
- component: oh-stepper-item
config:
autorepeat: true
autorepeatDynamic: true
buttonsOnly: true
color: '=(Number.parseFloat(items[props.targetTemp].state) >= 24) ? "red":
(Number.parseFloat(items[props.targetTemp].state) >= 23) ? "pink":
(Number.parseFloat(items[props.targetTemp].state) >= 22) ? "deeporange":
(Number.parseFloat(items[props.targetTemp].state) >= 20) ? "orange":
(Number.parseFloat(items[props.targetTemp].state) >= 19) ? "yellow":
(Number.parseFloat(items[props.targetTemp].state) >= 18) ? "lime":
(Number.parseFloat(items[props.targetTemp].state) >= 17) ? "green":
(Number.parseFloat(items[props.targetTemp].state) > 1) ? "blue":
"white"'
fill: true
icon: f7:arrow_up_to_line
iconColor: '=(Number.parseFloat(items[props.targetTemp].state) >= 24) ? "red":
(Number.parseFloat(items[props.targetTemp].state) >= 23) ? "pink":
(Number.parseFloat(items[props.targetTemp].state) >= 22) ? "deeporange":
(Number.parseFloat(items[props.targetTemp].state) >= 20) ? "orange":
(Number.parseFloat(items[props.targetTemp].state) >= 19) ? "yellow":
(Number.parseFloat(items[props.targetTemp].state) >= 18) ? "lime":
(Number.parseFloat(items[props.targetTemp].state) >= 17) ? "green":
(Number.parseFloat(items[props.targetTemp].state) > 1) ? "blue":
"white"'
iconUseState: true
item: =props.targetTemp
max: 25
min: 15
round: true
small: true
title: ="Target Temperature - " + items[props.targetTemp].state
visible: '=(items[props.targetTemp].state == "NULL") ? "false": "true"'
- component: oh-list-item
config:
action: options
actionItem: =props.zoneOp
badge: '=(items[props.zoneOp].state == "SCHEDULE") ? "SCHEDULE" : (items[props.zoneOp].state == "UNTIL_CHANGE") ? "UNTIL CHANGE" : (items[props.zoneOp].state == "MANUAL") ? "MANUAL" : (items[props.zoneOp].state == "TIMER") ? "TIMER" : "error"'
badge-color: '=(items[props.zoneOp].state == "SCHEDULE") ? "green" : (items[props.zoneOp].state == "UNTIL_CHANGE") ? "yellow" : (items[props.zoneOp].state == "MANUAL") ? "red" : (items[props.zoneOp].state == "TIMER") ? "blue" : "white"'
icon: f7:app_badge
iconColor: '=(items[props.zoneOp].state == "SCHEDULE") ? "green" : (items[props.zoneOp].state == "UNTIL_CHANGE") ? "yellow" : (items[props.zoneOp].state == "MANUAL") ? "red" : (items[props.zoneOp].state == "TIMER") ? "blue" : "white"'
title: Mode
- component: oh-stepper-item
config:
autorepeat: true
autorepeatDynamic: true
buttonsOnly: true
color: '=(items[props.timerD].state > 0) ? "blue": "gray"'
fill: true
footer: '=(items[props.timerD].state > 0) ? "Ending at " + items[props.oETime].displayState: " "'
icon: f7:timer
iconColor: '=(items[props.timerD].state > 0) ? "blue": "gray"'
iconUseState: true
item: =props.timerD
max: 600
round: true
small: true
step: 30
title: ="Timer Duration - " + items[props.timerD].displayState
And here is my Window sensing rule
import java.util.Map
var Map<String, String> zoneOpMemory = newHashMap
var Map<String, String> hvacMemory = newHashMap
var Map<String, String> tTempMemory = newHashMap
var Map<String, String> timerMemory = newHashMap
var Map<String, Timer> deBounce = newHashMap
rule "Group TRV Window Sensing"
when
Member of TadoHeating_TRVSensingZones changed
then
if (TadoHeating_AtHome.state == ON) {
val String trvName = triggeringItem.name.replace("TadoHeating_TRVSensingZones_", "")
val hvacItem = gAll_TadoHeating_HVACMode.members.findFirst[ i | i.name.contains(trvName) ]
val zoneOpItem = gAll_TadoHeating_ZoneOperationMode.members.findFirst[ i | i.name.contains(trvName) ]
val tTempItem = gAll_TadoHeating_TargetTemperatures.members.findFirst[ i | i.name.contains(trvName) ]
val timerItem = gAll_TadoHeating_TimerDuration.members.findFirst[ i | i.name.contains(trvName) ]
val String zoneOpMemoryName = zoneOpItem.name
val String hvacMemoryName = hvacItem.name
val String tTempMemoryName = tTempItem.name
val String timerMemoryName = timerItem.name
if (deBounce.get(triggeringItem.name) === null) {
logInfo("Heating", "A door or window in " + trvName + " zone is " + newState.toString + ". Debounce for 2 mins")
deBounce.put(triggeringItem.name, createTimer( now.plusMinutes(2), [ |
if (triggeringItem.state == OPEN) {
logInfo("Heating", "A door or window in " + trvName + " zone is still OPEN. Turning TRV OFF")
hvacMemory.put(hvacMemoryName, hvacItem.state.toString)
zoneOpMemory.put(zoneOpMemoryName, zoneOpItem.state.toString)
tTempMemory.put(tTempMemoryName, tTempItem.state.toString)
timerMemory.put(timerMemoryName, timerItem.state.toString)
hvacItem.sendCommand("OFF")
zoneOpItem.sendCommand("MANUAL")
deBounce.put(triggeringItem.name, null)
} else if (newState == CLOSED) {
logInfo("Heating", "All doors and windows in " + trvName + " zone are CLOSED. Turning " + trvName + " TRV ON")
if (hvacMemory.get(hvacMemoryName) !== null) {
hvacItem.sendCommand(hvacMemory.get(hvacMemoryName))
} else {
hvacItem.sendCommand("HEAT")
}
if (zoneOpMemory.get(zoneOpMemoryName) !== null) {
zoneOpItem.sendCommand(zoneOpMemory.get(zoneOpMemoryName))
} else {
zoneOpItem.sendCommand("SCHEDULE")
}
if (zoneOpMemory.get(zoneOpMemoryName) == "TIMER" && timerMemory.get(timerMemoryName) !== null && timerMemory.get(timerMemoryName) != 0) {
timerItem.sendCommand(timerMemory.get(timerMemoryName).toString)
}
if (tTempMemory.get(tTempMemoryName) !== null && zoneOpMemory.get(zoneOpMemoryName) != "SCHEDULE") {
tTempItem.sendCommand(tTempMemory.get(tTempMemoryName).toString)
}
deBounce.put(triggeringItem.name, null)
}
] ))
} else if (deBounce.get(triggeringItem.name) !== null) {
deBounce.get(triggeringItem.name).reschedule(now.plusMinutes(2))
}
}
end
And this is how I group my items to make this all work.
The Open/Close and TRV Items are in the semantic model to get the correct locations etc when running the rule
Openhab 3.2
Pi 4