Strange behaviour - Rounded up values being sent as commands when loading UI widgets

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

If that’s the case (no events.log snippet, rule not command-driven?) then it’s a UI widget problem.

example for system stepper widget.

Boils down to widget treating a visible internal update the same as a user click.

Thanks.
Does this mean it’s a known bug and it’s on someones to do list?

Hmm, well it is a per-widget bug.

The linked issue is for the stepper widget, and any progress will be shown there. (None at the moment).

Github issues are everyones/someones/no-ones to-do list in a community project.

Its previously been fixed for knob widget, I would expect that to be included in OH3.2

I don’t know enough to even pick out what widget you are using. Looks like stepper mentioned in YAML?

Have you looked in your events.log to confirm what is going on?

It’s a custom widget, and the Target Temperature is being referenced more than once

A workaround that is working for me now is to include the ‘step’ parameter with 0.1 for the stepper item holding the Target Temperature item.

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.